diff --git a/Makefile b/Makefile index 7d5de89..8f3c3e9 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = python3 all: ponydos.img -FS_FILES = ponydos.wall passion.wall shell.bin hello.bin +FS_FILES = shell.bin ponydos.wall passion.wall viewer.bin hello.bin ponydos.asm ponydos.img: ponydos.bin $(FS_FILES) $(PYTHON) assemble_floppy.py $@ ponydos.bin $(FS_FILES) diff --git a/hello.asm b/hello.asm index 8980241..8cbefea 100644 --- a/hello.asm +++ b/hello.asm @@ -662,4 +662,4 @@ window_status db WINDOW_STATUS_NORMAL window_move_x_offset dw 0 section .bss -window_data resw 25*80 +window_data resw ROWS*COLUMNS diff --git a/viewer.asm b/viewer.asm new file mode 100644 index 0000000..b7eb04c --- /dev/null +++ b/viewer.asm @@ -0,0 +1,906 @@ +%include "ponydos.inc" +cpu 8086 +bits 16 + +WINDOW_STATUS_NORMAL equ 0 +WINDOW_STATUS_MOVE equ 1 +WINDOW_STATUS_RESIZE equ 2 + +; Resize button, title, space, close button +WINDOW_MIN_WIDTH equ 1 + 8 + 1 + 1 +WINDOW_MIN_HEIGHT equ 2 + +; 0x0000 +jmp near process_event + +; 0x0003 PROC_INITIALIZE_ENTRYPOINT +; initialize needs to preserve ds +; in: +; ds:si = text filename +; ax:bx = file address +; cx = text file size in sectors +initialize: + push ds + + ; Setup the input parameters for now + mov bp, cs + mov ds, bp + mov si, tmp_window_title + mov dx, 1 + call PONYDOS_SEG:SYS_OPEN_FILE ; TODO: error + + mov dx, 0xF000 + mov es, dx + xor bx, bx + xor di, di ; read + call PONYDOS_SEG:SYS_MODIFY_SECTORS + mov ax, es + ; End of temporary set-up + + ; On entry, ds and es will not be set correctly for us + mov bp, cs + mov es, bp + + push cx + call strlen + mov di, window_title + rep movsb + pop cx + + mov ds, bp + + mov [cur_file_address + 2], ax + mov [cur_file_address], bx + mov [beg_file_address], bx + shl cx, 1 ; 2 + shl cx, 1 ; 4 + shl cx, 1 ; 8 + shl cx, 1 ; 16 + shl cx, 1 ; 32 + shl cx, 1 ; 64 + shl cx, 1 ; 128 + shl cx, 1 ; 256 + shl cx, 1 ; 512 + add bx, cx + mov [end_file_address], bx + + call hook_self_onto_window_chain + + call render_window + + ; We must explicitly request redraw from the compositor + call request_redraw + + pop ds + retf + +; process_event needs to preserve all registers other than ax +; in: +; al = event +; bx = window ID +; cx, dx = event-specific +; out: +; ax = event-specific +process_event: + push bx + push cx + push dx + push si + push di + push bp + push ds + push es + + ; On entry, ds and es will not be set correctly for us + mov bp, cs + mov ds, bp + mov es, bp + + cmp al, WM_PAINT + jne .not_paint + call event_paint + jmp .end + .not_paint: + + cmp al, WM_MOUSE + jne .not_mouse + call event_mouse + jmp .end + .not_mouse: + + cmp al, WM_KEYBOARD + jne .not_keyboard + call event_keyboard + jmp .end + .not_keyboard: + + cmp al, WM_UNHOOK + jne .not_unhook + call event_unhook + jmp .end + .not_unhook: + + .end: + pop es + pop ds + pop bp + pop di + pop si + pop dx + pop cx + pop bx + retf + +; ------------------------------------------------------------------ +; File handlers +; ------------------------------------------------------------------ + +; in: +; es:di = where to start printing on screen +print_file: + push ax + push bx + push cx + push dx + push si + push di + push ds + + lds si, [cur_file_address] + + mov cx, [cs:window_height] + dec cx + mov dx, [cs:window_width] + mov bl, 1 ; Haven't read anything yet + .window_loop: + push di + .line_loop: + cmp si, [cs:end_file_address] + jne .not_end_file + test bl, bl + jz .end_window_loop ; Need to have read something to hit end-of-file + + .not_end_file: + lodsb + xor bl, bl ; Have read something + + ; Special byte handling + cmp al, 0x0A ; \n + je .next_line + + cmp al, 0x09 ; \t + jne .null_check + add di, 8 + sub dx, 4 + jc .next_line + jz .next_line + jmp .line_loop + + .null_check: + test al, al + jz .end_window_loop + + stosb + inc di + dec dx + jnz .line_loop + .next_line: + mov dx, [cs:window_width] + + pop di + add di, [cs:window_width] + add di, [cs:window_width] + loop .window_loop + + .ret: + pop ds + pop di + pop si + pop dx + pop cx + pop bx + pop ax + ret + + .end_window_loop: + pop di + jmp .ret + +; out: +; dx = non-zero if cur_file_address is updated +file_next_line: + push ax + push si + push ds + + xor dx, dx + + lds si, [cur_file_address] + + .loop: + lodsb + + cmp al, 0x0A ; \n + je .found_next_line + + test al, al + jz .ret + + cmp si, [cs:end_file_address] + je .ret + + jmp .loop + + .found_next_line: + cmp si, [cs:end_file_address] + je .ret + + cmp byte [ds:si], 0 + je .ret + + not dx + mov [cs:cur_file_address], si + + .ret: + pop ds + pop si + pop ax + ret + +; out: +; dx = non-zero if cur_file_address is updated +file_prev_line: + push ax + push si + push ds + + std + + xor dx, dx + + lds si, [cur_file_address] + cmp si, [cs:beg_file_address] ; Already at the beginning? + je .ret + + dec si + cmp si, [cs:beg_file_address] ; Last line was empty? + je .ret + + dec si + .loop: + cmp si, [cs:beg_file_address] + je .found_prev_line + lodsb + + cmp al, 0x0A ; \n + jne .loop + + inc si + inc si + + .found_prev_line: + not dx + mov [cs:cur_file_address], si + + .ret: + cld + + pop ds + pop si + pop ax + ret + +; ------------------------------------------------------------------ +; Event handlers +; ------------------------------------------------------------------ + +; in: +; al = WM_PAINT +; bx = window ID +; out: +; clobbers everything +event_paint: + ; Forward the paint event to the next window in the chain + ; We must do this before we paint ourselves, because painting must + ; happen from the back to the front + ; Because we only have one window, we don't need to save our own ID + mov bx, [window_next] + call send_event + + mov bx, [window_width] ; Buffer width, usually same as window width + mov cx, [window_width] + mov dx, [window_height] + mov si, window_data + mov di, [window_x] + mov bp, [window_y] + + cmp di, 0 + jge .not_clip_left + .clip_left: + ; Adjust the start of buffer to point to the first cell + ; that is on screen + sub si, di + sub si, di + ; Adjust window width to account for non-rendered area + ; that is off screen + add cx, di + ; Set X to 0 + xor di, di + .not_clip_left: + + mov ax, di + add ax, cx + cmp ax, COLUMNS + jle .not_clip_right + .clip_right: + ; Adjust the width to only go as far as the right edge + sub ax, COLUMNS + sub cx, ax + .not_clip_right: + + mov ax, bp + add ax, dx + cmp ax, ROWS + jle .not_clip_bottom + .clip_bottom: + ; Adjust the height to only go as far as the bottom edge + sub ax, ROWS + sub dx, ax + .not_clip_bottom: + + call PONYDOS_SEG:SYS_DRAW_RECT + + ret + +; in: +; al = WM_MOUSE +; bx = window ID +; cl = X +; ch = Y +; dl = mouse buttons held down +; out: +; clobbers everything +event_mouse: + test dl, MOUSE_PRIMARY | MOUSE_SECONDARY + jnz .not_end_window_change + ; If we were moving or resizing the window, releasing the + ; button signals the end of the action + mov byte [window_status], WINDOW_STATUS_NORMAL + .not_end_window_change: + + ; Expand X and Y to 16 bits for easier calculations + ; Because we only have one window, we don't need to save our own ID + xor bx, bx + mov bl, ch + xor ch, ch + + ; Are we moving the window at the moment? + cmp byte [window_status], WINDOW_STATUS_MOVE + jne .not_moving + call move_window + .not_moving: + + ; Are we resizing the window at the moment? + cmp byte [window_status], WINDOW_STATUS_RESIZE + jne .not_resizing + call resize_window + .not_resizing: + + ; Check if the mouse is outside our window + cmp cx, [window_x] + jl .outside ; x < window_x + cmp bx, [window_y] + jl .outside ; y < window_y + mov ax, [window_x] + add ax, [window_width] + cmp ax, cx + jle .outside ; window_x + window_width <= x + mov ax, [window_y] + add ax, [window_height] + cmp ax, bx + jle .outside ; window_y + window_height <= y + + .inside: + cmp byte [window_mouse_released_inside], 0 + je .not_click + test dl, MOUSE_PRIMARY | MOUSE_SECONDARY + jz .not_click + .click: + call event_click + .not_click: + + ; We need to keep track of if the mouse has been inside our + ; window without the buttons held, in order to avoid + ; generating click events in cases where the cursor is + ; dragged into our window while buttons are held + test dl, MOUSE_PRIMARY | MOUSE_SECONDARY + jz .buttons_not_held + .buttons_held: + mov byte [window_mouse_released_inside], 0 + jmp .buttons_end + .buttons_not_held: + mov byte [window_mouse_released_inside], 1 + .buttons_end: + + ; We must forward the event even if it was inside our + ; window, to make sure other windows know when the mouse + ; leaves them + ; Set x and y to 255 so that windows below ours don't think + ; the cursor is inside them + ; Also clear the mouse buttons – not absolutely necessary + ; but it's cleaner if other windows don't get any + ; information about the mouse + mov al, WM_MOUSE + mov bx, [window_next] + mov cx, 0xffff + xor dl, dl + call send_event + ret + + .outside: + mov byte [window_mouse_released_inside], 0 + + ; Not our window, forward the event + mov al, WM_MOUSE + mov ch, bl ; Pack the X and Y back into cx + mov bx, [window_next] + call send_event + ret + ret + +; in: +; bx = Y +; cx = X +; dl = mouse buttons +; out: +; clobbers ax +event_click: + ; This is not a true event passed into our event handler, but + ; rather one we've synthetized from the mouse event + ; The reason we synthetize this event is because most interface + ; elements react to clicks specifically, so having this event + ; making implementing them easier + + ; Raising a window is done by first unhooking, then rehooking it to + ; the window chain + call unhook_self_from_window_chain + call hook_self_onto_window_chain + call request_redraw + + ; Did the user click the title bar? + cmp [window_y], bx + jne .not_title_bar + .title_bar: + ; Did the user click the window close button? + mov ax, [window_x] + add ax, [window_width] + dec ax + cmp ax, cx + jne .not_close + .close: + call unhook_self_from_window_chain + ; Nothing can call into us again after we unhook + ; the window, so deallocate the memory we have + ; reserved + call deallocate_own_memory + ; We don't need to call request_redraw here, since + ; it will be called unconditionally above + jmp .title_bar_end + .not_close: + + ; Did the user click on the resize button? + cmp [window_x], cx + jne .not_resize + .resize: + mov byte [window_status], WINDOW_STATUS_RESIZE + jmp .title_bar_end + .not_resize: + + ; Clicking on the title bar signals beginning of a window + ; move + mov byte [window_status], WINDOW_STATUS_MOVE + mov ax, [window_x] + sub ax, cx + mov [window_move_x_offset], ax + .title_bar_end: + .not_title_bar: + + .end: + ret + +; in: +; al = WM_KEYBOARD +; bx = window ID +; cl = typed character +; ch = pressed key +; out: +; clobbers everything +event_keyboard: + cmp ch, 0x50 ; down key + jne .up_key_check + call file_next_line + test dx, dx + jz .ret + + call render_window + call request_redraw + + .up_key_check: + cmp ch, 0x48 ; up key + jne .ret + call file_prev_line + test dx, dx + jz .ret + + call render_window + call request_redraw + + .ret: + ret + +; in: +; al = WM_UNHOOK +; bx = window ID +; cx = window ID of the window to unhook from the window chain +; out: +; ax = own window ID if we did not unhook +; next window ID if we did +; clobbers everything else +event_unhook: + cmp bx, cx + je .unhook_self + + ; Save our own ID + push bx + + ; Propagate the event + mov bx, [window_next] + call send_event + + ; Update window_next in case the next one unhooked + mov [window_next], ax + + ; Return our own ID + pop ax + ret + + .unhook_self: + ; Return window_next to the caller, unhooking us from the + ; chain + mov ax, [window_next] + ret + +; ------------------------------------------------------------------ +; Event handler subroutines +; ------------------------------------------------------------------ + +; in: +; bx = Y +; cx = X +move_window: + push ax + + ; Offset the X coördinate so that the apparent drag position + ; remains the same + mov ax, cx + add ax, [window_move_x_offset] + + ; Only do an update if something has changed. Reduces flicker + cmp [window_x], ax + jne .update_location + cmp [window_y], bx + jne .update_location + jmp .end + + .update_location: + mov [window_x], ax + mov [window_y], bx + call request_redraw + + .end: + pop ax + ret + +; in: +; bx = Y +; cx = X +resize_window: + push ax + push bx + push bp + + ; Calculate new width + mov ax, [window_width] + add ax, [window_x] + sub ax, cx + + cmp ax, WINDOW_MIN_WIDTH + jge .width_large_enough + mov ax, WINDOW_MIN_WIDTH + .width_large_enough: + + cmp ax, COLUMNS + jle .width_small_enough + mov ax, COLUMNS + .width_small_enough: + + ; Calculate new height + mov bp, [window_height] + add bp, [window_y] + sub bp, bx + + cmp bp, WINDOW_MIN_HEIGHT + jge .height_large_enough + mov bp, WINDOW_MIN_HEIGHT + .height_large_enough: + + cmp bp, ROWS + jle .height_small_engough + mov bp, ROWS + .height_small_engough: + + ; Only do an update if something has changed. Reduces flicker + cmp [window_width], ax + jne .update_size + cmp [window_height], bp + jne .update_size + jmp .end + + .update_size: + mov bx, [window_x] + add bx, [window_width] + sub bx, ax + mov [window_x], bx + mov [window_width], ax + + mov bx, [window_y] + add bx, [window_height] + sub bx, bp + mov [window_y], bx + mov [window_height], bp + + call render_window + + call request_redraw + + .end: + pop bp + pop bx + pop ax + +render_window: + push ax + push cx + push dx + push si + push di + + ; Clear window to be black-on-white + mov di, window_data + mov ax, [window_width] + mov cx, [window_height] + mul cx + mov cx, ax + mov ax, 0xf000 ; Attribute is in the high byte + rep stosw + + ; Set title bar to be white-on-black + mov di, window_data + mov ax, 0x0f00 + mov cx, [window_width] + rep stosw + + ; Add title bar buttons + mov di, window_data + mov byte [di], 0x17 ; Resize arrow + add di, [window_width] + add di, [window_width] + sub di, 2 + mov byte [di], 'x' ; Close button + + ; Add window title + mov di, window_data + add di, 2 + mov si, window_title + mov cx, [window_width] + dec cx + dec cx + cmp cx, FS_DIRENT_NAME_SIZE + jle .copy_title + mov cx, FS_DIRENT_NAME_SIZE + .copy_title: + lodsb + stosb + inc di + loop .copy_title + + ; Print text + mov di, window_data + add di, [window_width] + add di, [window_width] + call print_file + + pop di + pop si + pop dx + pop cx + pop ax + ret + +; ------------------------------------------------------------------ +; Window chain +; ------------------------------------------------------------------ + +; in: +; al = event +; bx = window to send the event to +; cx, dx = event-specific +; out: +; ax = event-specific, 0 if bx=0 +send_event: + test bx, bx + jnz .non_zero_id + + ; Returning 0 if the window ID is 0 makes window unhooking simpler + xor ax, ax + ret + + .non_zero_id: + push bp + + ; Push the return address + push cs + mov bp, .end + push bp + + ; Push the address we're doing a far-call to + mov bp, bx + and bp, 0xf000 ; Highest nybble of window ID marks the segment + push bp + xor bp, bp ; Event handler is always at address 0 + push bp + + retf + + .end: + pop bp + ret + +hook_self_onto_window_chain: + push ax + push es + + mov ax, PONYDOS_SEG + mov es, ax + + ; Window ID is made of the segment (top nybble) and an arbitrary + ; process-specific part (lower three nybbles). Since we only have + ; one window, we can leave the process-specific part as zero + mov ax, cs + + xchg [es:GLOBAL_WINDOW_CHAIN_HEAD], ax + + ; Save the old head of the chain, so that we can propagate events + ; down to it + mov [window_next], ax + + pop es + pop ax + ret + +unhook_self_from_window_chain: + push bx + push cx + push es + + mov ax, PONYDOS_SEG + mov es, ax + + mov al, WM_UNHOOK + mov bx, [es:GLOBAL_WINDOW_CHAIN_HEAD] + ; Our window ID is just our segment, see the comment in + ; hook_self_onto_window_chain + mov cx, cs + call send_event + + ; Update the head of the chain, in case we were at the head + mov [es:GLOBAL_WINDOW_CHAIN_HEAD], ax + + pop es + pop cx + pop bx + ret + +; ------------------------------------------------------------------ +; Memory management +; ------------------------------------------------------------------ + +deallocate_own_memory: + push bx + push cx + push es + + mov bx, PONYDOS_SEG + mov es, bx + + ; Segment 0xn000 corresponds to slot n in the allocation table + mov bx, cs + mov cl, 12 + shr bx, cl + + mov byte [es:GLOBAL_MEMORY_ALLOCATION_MAP + bx], 0 + + pop es + pop cx + pop bx + ret + +; ------------------------------------------------------------------ +; Painting +; ------------------------------------------------------------------ + +request_redraw: + push ax + push es + + mov ax, PONYDOS_SEG + mov es, ax + + mov byte [es:GLOBAL_REDRAW], 1 + + pop es + pop ax + ret + +; ------------------------------------------------------------------ +; String functions +; ------------------------------------------------------------------ + +; in: +; ds:si = string +; out: +; cx = stlen +strlen: + push ax + push di + push es + + mov cx, ds + mov es, cx + mov di, si + + mov cx, -1 + xor ax, ax + repne scasb + not cx + dec cx + + pop es + pop di + pop ax + ret + +; ------------------------------------------------------------------ +; Variables +; ------------------------------------------------------------------ + +tmp_window_title db "ponydos.asm", 0, 0 +window_title times FS_DIRENT_NAME_SIZE + 1 db 0 + +cur_file_address: dw 0 ; Segment + dw 0 +beg_file_address: dw 0 +end_file_address: dw 0 + +window_next dw 0xffff +window_x dw 4 +window_y dw 12 +window_width dw 32 +window_height dw 8 + +window_mouse_released_inside db 0 +window_status db WINDOW_STATUS_NORMAL +window_move_x_offset dw 0 + +section .bss +window_data resw ROWS*COLUMNS