ponydos/hello.asm
Juhani Krekelä d4c15687c8 Add option for 8-colour mode
Some BIOSs initialize the VGA card by default into a mode where the high
bit of background nybble signals that the cell should blink. The simple
way to avoid this is by restricting the background colours to the range
0…7. However, since our mouse cursor is implemented by swapping the
foreground and the background colours, we also need to restrict the
foreground colours to the range 0…7.
2023-05-11 22:02:18 +03:00

709 lines
14 KiB
NASM
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

%include "ponydos.inc"
cpu 8086
bits 16
%ifdef BLINKY
TITLEBAR_ATTRIBUTE equ 0x07
WINDOW_ATTRIBUTE equ 0x70
%else
TITLEBAR_ATTRIBUTE equ 0x0f
WINDOW_ATTRIBUTE equ 0xf0
%endif
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, WINDOW_ATTRIBUTE<<8 ; Attribute is in the high byte
rep stosw
; Set title bar to be white-on-black
mov di, window_data
mov ax, TITLEBAR_ATTRIBUTE<<8
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