forked from offtopia/ponydos
0b5fd0d84e
Previously programs deallocated their memory before forwarding a message. If the forwarded message made another program allocate, this could cause the program code to be overwritten while it is still running.
701 lines
14 KiB
NASM
701 lines
14 KiB
NASM
%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 + WINDOW_TITLE_LEN + 1 + 1
|
||
WINDOW_MIN_HEIGHT equ 2
|
||
|
||
; 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
|
||
|
||
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
|
||
; WM_OPEN_FILE needs ds to be left-as is
|
||
cmp al, WM_OPEN_FILE
|
||
je .no_set_ds
|
||
push cs
|
||
pop ds
|
||
.no_set_ds:
|
||
push cs
|
||
pop es
|
||
|
||
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:
|
||
|
||
cmp al, WM_OPEN_FILE
|
||
jne .not_open_file
|
||
call event_open_file
|
||
jmp .end
|
||
.not_open_file:
|
||
|
||
.end:
|
||
cmp byte [exiting], 0
|
||
je .not_exiting
|
||
; Once we have deallocated our own memory, we may not call any
|
||
; external functions that might allocate. Safest place to do the
|
||
; deallocation is just before returning control to our caller
|
||
call deallocate_own_memory
|
||
.not_exiting:
|
||
|
||
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
|
||
|
||
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
|
||
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
|
||
mov byte [exiting], 1
|
||
; 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:
|
||
pop ax
|
||
ret
|
||
|
||
; in:
|
||
; al = WM_KEYBOARD
|
||
; bx = window ID
|
||
; cl = typed character
|
||
; ch = pressed key
|
||
; out:
|
||
; clobbers everything
|
||
event_keyboard:
|
||
; Unlike most 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
|
||
|
||
; in:
|
||
; al = WM_OPEN_FILE
|
||
; ds:cx = filename
|
||
; ds ≠ cs
|
||
; out:
|
||
; ds = cs
|
||
; clobbers everything
|
||
event_open_file:
|
||
; File open events are sent specifically to a process, so we don't
|
||
; need to forward it
|
||
; Unlike other event handlers, event_open_file is called with ds
|
||
; still set to calling process's segment, so that it can read the
|
||
; passed-in filename. For simplicity of code running after the,
|
||
; event handlers, we set ds to point to our own segment on return
|
||
push cs
|
||
pop ds
|
||
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_TITLE_LEN
|
||
.copy_title:
|
||
lodsb
|
||
stosb
|
||
inc di
|
||
loop .copy_title
|
||
|
||
; Add message
|
||
; This may overflow the available visual space, but since we always
|
||
; allocate the maximum size for the window, this is not a buffer
|
||
; overflow
|
||
mov di, window_data
|
||
add di, [window_width]
|
||
add di, [window_width]
|
||
mov si, message
|
||
mov cx, MESSAGE_LEN
|
||
.copy_message:
|
||
lodsb
|
||
stosb
|
||
inc di
|
||
loop .copy_message
|
||
|
||
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
|
||
|
||
; ------------------------------------------------------------------
|
||
; Variables
|
||
; ------------------------------------------------------------------
|
||
|
||
exiting db 0
|
||
|
||
window_title db 'Hello'
|
||
.end:
|
||
WINDOW_TITLE_LEN equ window_title.end - window_title
|
||
|
||
message db 'Hello, world!'
|
||
.end:
|
||
MESSAGE_LEN equ message.end - message
|
||
|
||
window_next dw 0xffff
|
||
window_x dw 9
|
||
window_y dw 20
|
||
window_width dw MESSAGE_LEN
|
||
window_height dw 2
|
||
|
||
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
|