diff --git a/Makefile b/Makefile index 8f3c3e9..2856758 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ PYTHON = python3 all: ponydos.img -FS_FILES = shell.bin ponydos.wall passion.wall viewer.bin hello.bin ponydos.asm +FS_FILES = shell.bin ponydos.wall passion.wall viewer.bin hello.bin memory.bin ponydos.asm ponydos.img: ponydos.bin $(FS_FILES) $(PYTHON) assemble_floppy.py $@ ponydos.bin $(FS_FILES) diff --git a/memory.asm b/memory.asm new file mode 100644 index 0000000..e4ce624 --- /dev/null +++ b/memory.asm @@ -0,0 +1,549 @@ +%include "ponydos.inc" +cpu 8086 +bits 16 + +WINDOW_STATUS_NORMAL equ 0 +WINDOW_STATUS_MOVE equ 1 + +; 0x0000 +jmp near process_event + +; 0x0003 PROC_INITIALIZE_ENTRYPOINT +; initialize needs to preserve ds +initialize: + push ds + + ; On entry, ds and es will not be set correctly for us + mov bp, cs + mov ds, bp + mov es, bp + + call hook_self_onto_window_chain + + ; 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 + +; ------------------------------------------------------------------ +; 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 + + call update_usage_display + + 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 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: + + ; 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 +event_click: + push ax + ; 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: + + ; 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: + pop ax + ret + +; in: +; al = WM_KEYBOARD +; bx = window ID +; cl = typed character +; ch = pressed key +; out: +; clobbers everything +event_keyboard: + ; Unlike other events, keyboard events are not forwarded + ; Since we do not care about the keyboard for this app, we just + ; swallow the event + 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 + +update_usage_display: + push ax + push cx + push si + push di + push ds + + mov cx, PONYDOS_SEG + mov ds, cx + + mov si, GLOBAL_MEMORY_ALLOCATION_MAP + mov di, window_data.indicators + mov cx, MEM_ALLOCATION_MAP_SIZE + .loop: + lodsb + test al, al + jz .unused + .used: + mov al, 0xfe + jmp .loop_bottom + .unused: + xor al, al + + .loop_bottom: + stosb + inc di + loop .loop + + .end: + pop ds + pop di + pop si + 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 + +; ------------------------------------------------------------------ +; Variables +; ------------------------------------------------------------------ + +window_next dw 0xffff +window_x dw 65 +window_y dw 3 +window_width dw 10 +window_height dw 3 + +window_mouse_released_inside db 0 +window_status db WINDOW_STATUS_NORMAL +window_move_x_offset dw 0 + +window_data: + db 'U', 0x0f, 's', 0x0f, 'a', 0x0f, 'g', 0x0f, 'e', 0x0f + times 10-5-1 db 0x00, 0x0f + db 'x', 0x0f + db '0', 0xf0, '1', 0xf0, '2', 0xf0, '3', 0xf0, '4', 0xf0, '5' + db 0xf0, '6', 0xf0, '7', 0xf0, '8', 0xf0, '9', 0xf0 + .indicators: times 10 db 0x00, 0xf0