bios-OS/src/services/gui_lib.asm
2026-02-06 23:01:30 +01:00

808 lines
No EOL
21 KiB
NASM

; =============================================================================
; Project : Custom BIOS / ROM
; File : gui_lib.asm
; Author : frater
;
; License : GNU General Public License v3.0 or later (GPL-3.0+)
;
; This program is free software: you can redistribute it and/or modify
; it under the terms of the GNU General Public License as published by
; the Free Software Foundation, either version 3 of the License, or
; (at your option) any later version.
;
; This program is distributed in the hope that it will be useful,
; but WITHOUT ANY WARRANTY; without even the implied warranty of
; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
; GNU General Public License for more details.
;
; You should have received a copy of the GNU General Public License
; along with this program. If not, see <https://www.gnu.org/licenses/>.
;
; =============================================================================
; --- Configuration ---
%define GUI_RAM_SEG 0x0A00 ; Segment de données UI
%define GUI_MAX_WIDGETS 32 ; Nombre max de widgets simultanés
; Dimensions
%define GUI_CHECKBOX_SIZE 10 ;
; --- Drapeaux & États ---
%define GUI_STATE_FREE 0 ; Le slot est vide (mémoire dispo)
%define GUI_STATE_NORMAL 1 ; Affiché, repos
%define GUI_STATE_HOVER 2 ; Souris dessus
%define GUI_STATE_PRESSED 3 ; Clic enfoncé
%define GUI_STATE_DISABLED 4 ; Grisé
; --- Types de Widgets ---
%define WIDGET_TYPE_BUTTON 0
%define WIDGET_TYPE_SLIDER 1
%define WIDGET_TYPE_LABEL 2
%define WIDGET_TYPE_ROUND_BUTTON 3
%define WIDGET_TYPE_CHECKBOX 4
; --- Widgets attributs ---
%define BUTTON_OK 1
; --- Structure d'un OBJET (Bouton, etc) ---
struc widget
.state resb 1 ; État (0=libre, >0=utilisé)
.type resb 1 ; Type (0=Button, 1=Slider...)
.oldstate resb 1 ; widget a-t-il été modifié ?
.user_id resb 1 ; ID unique utilisateur
.x resw 1 ; Position X
.y resw 1 ; Position Y
.x2 resw 1 ; Position X2 calculée
.y2 resw 1 ; Position Y2 calculée
.w resw 1 ; Largeur
.h resw 1 ; Hauteur
.text_ofs resw 1 ; Offset du texte
.text_seg resw 1 ; Segment du texte
.event_click resw 1 ; adresse de la fonction "on click"
; .event_hover resw 1 ; adresse de la fonction "on hover"
; .event_release resw 1 ; adresse de la fonction "on release"
; .event_disable resw 1 ; adresse de la fonction "on disable"
; .event_enable resw 1 ; adresse de la fonction "on enable"
.event_drag resw 1 ; adresse de la fonction "on drag"
.attr_mode resb 1 ; 0=None, 1=Horiz, 2=Vert
.attr_min resw 1 ; Valeur/Position Min
.attr_max resw 1 ; Valeur/Position Max
.attr_val resw 1 ; Valeur/Position Actuelle
.attr_anchor resw 1 ; Offset interne pour le drag
.thumb_pct resb 1 ; Taille du curseur en % (1-100)
alignb 2 ; Alignement mémoire pour performance
endstruc
; =============================================================================
; SECTION : GESTION MÉMOIRE (ALLOCATION / LIBÉRATION)
; =============================================================================
; -----------------------------------------------------------------------------
; gui_init_system
; Initialise toute la mémoire des widgets à 0 (Libre)
; Entrée : DS doit pointer vers GUI_RAM_SEG
; -----------------------------------------------------------------------------
gui_init_system:
push eax
push cx
push es
push di
mov ax, GUI_RAM_SEG
mov es, ax
xor di, di
mov cx, (widget_size * GUI_MAX_WIDGETS) / 2
xor ax, ax
rep stosw ; Plus sûr en mode 16 bits pour l'alignement
pop di
pop es
pop cx
pop eax
ret
; -----------------------------------------------------------------------------
; gui_alloc_widget
; Cherche un slot vide et retourne son adresse
; Sortie :
; - Succès : CF=0 (Carry Clear), GS:SI = Offset du widget
; - Echec : CF=1 (Carry Set), GS:SI = Indéfini (ou 0)
; -----------------------------------------------------------------------------
gui_alloc_widget:
push ax
push cx
push bx
mov ax, GUI_RAM_SEG
mov gs, ax
mov si, 0 ; Début du pool
mov cx, GUI_MAX_WIDGETS
.loop_find:
cmp byte [gs:si + widget.state], GUI_STATE_FREE
je .found ; Si state == 0, c'est libre !
add si, widget_size ; Sinon on passe au suivant
loop .loop_find
; Pas trouvé (Plein)
stc ; set Carry Flag : erreur
jmp .done
.found:
; On initialise le slot trouvé
mov byte [gs:si + widget.state], GUI_STATE_NORMAL ; Marquer comme occupé
mov byte [gs:si + widget.type], WIDGET_TYPE_BUTTON ; Type par défaut
mov byte [gs:si + widget.oldstate], 0 ; doit etre dessiné
; Reset des champs critiques pour éviter les déchets
mov word [gs:si + widget.event_click], 0
mov word [gs:si + widget.event_drag], 0
mov byte [gs:si + widget.thumb_pct], 10 ; 10% par défaut
clc ; Clear Carry Flag
.done:
pop bx
pop cx
pop ax
ret
; -----------------------------------------------------------------------------
; gui_free_widget
; Libère un widget
; Entrée : SI = Pointeur widget
; -----------------------------------------------------------------------------
gui_free_widget:
push eax
push es
mov ax, GUI_RAM_SEG
mov es, ax
mov di, si
mov cx, (widget_size)
shr cx, 2 ; cx / 4
xor eax, eax
rep stosd ; Remplit tout de 0
mov byte [es:si + widget.state], GUI_STATE_FREE
; On efface visuellement le widget (optionnel, remplit de blanc)
; Pour l'instant on le marque juste libre, il sera redessiné par-dessus.
pop es
pop eax
ret
; =============================================================================
; SECTION : MOTEUR D'ÉVÉNEMENTS (EVENT LOOP)
; =============================================================================
; -----------------------------------------------------------------------------
; gui_process_all
; 1. Redessine tous les widgets actifs
; 2. Vérifie les clics et appelle les callbacks
; Entrée :
; CX = Mouse X
; DX = Mouse Y
; BL = Mouse Buttons
; -----------------------------------------------------------------------------
gui_process_all:
pusha
push ds
push gs
mov ax, GUI_RAM_SEG
mov gs, ax
; Charger l'état de la souris pour toute la passe
push ds
mov ax, BDA_DATA_SEG
mov ds, ax
mov bl, [BDA_MOUSE + mouse.status]
pop ds
mov si, 0 ; Pointeur début tableau
mov di, GUI_MAX_WIDGETS ; Compteur
.loop_widgets:
; Est-ce que ce slot est occupé ?
cmp byte [gs:si + widget.state], GUI_STATE_FREE
je .next_widget
; --- Logique Widget ---
push bx ; Sauver état boutons souris
call gui_update_logic ; Vérifier collision/clic
; Si AL=1 (Clic relâché validé), exécuter le callback
cmp al, 1
jne .no_action
; Vérifier si un callback est défini
cmp word [gs:si + widget.event_click], 0
je .no_action
; APPEL DU CALLBACK (Fonction utilisateur)
; On sauve les registres car le callback peut faire n'importe quoi
pusha
call word [gs:si + widget.event_click]
popa
.no_action:
pop bx ; Restaurer boutons
mov al, byte [gs:si + widget.oldstate]
mov ah, byte [gs:si + widget.state]
; Dessiner (si l'état a changé ou pour refresh)
cmp al,ah
je .next_widget ; Si != 0, le widget est à jour, on passe.
call gui_draw_single_widget
.next_widget:
add si, widget_size
dec di
jnz .loop_widgets
pop gs
pop ds
popa
ret
; =============================================================================
; SECTION : LOGIQUE INTERNE ET DESSIN
; =============================================================================
; (Interne) Met à jour l'état d'un seul widget
; Entrée : SI=Widget, CX=MouseX, DX=MouseY, BL=Buttons
; Sortie : AL=1 si Clicked, 0 sinon. Met à jour [gs:si].state
gui_update_logic:
push ds
mov ax, BDA_DATA_SEG
mov ds, ax
mov al, byte [gs:si + widget.state]
cmp al, GUI_STATE_DISABLED
je .done
xor ax, ax
; les macros peuvent modifer les registres cx, dx & bx
mov cx, [BDA_MOUSE + mouse.x]
mov dx, [BDA_MOUSE + mouse.y]
; --- Hit Test ---
cmp cx, [gs:si + widget.x]
jb .miss ; mouse.x < widget.x ?
mov bx, [gs:si + widget.x]
add bx, [gs:si + widget.w]
cmp cx, bx
ja .miss ; mouse.x >= widget.x ?
cmp dx, [gs:si + widget.y]
jb .miss ; mouse.y < widget.y ?
mov bx, [gs:si + widget.y]
add bx, [gs:si + widget.h]
cmp dx, bx
ja .miss ; mouse.y >= widget.y ?
mov bl, [BDA_MOUSE + mouse.status]
; Dispatch selon le type
cmp byte [gs:si + widget.type], WIDGET_TYPE_SLIDER
jne .case_2
call gui_logic_slider
jmp .done
.case_2:
cmp byte [gs:si + widget.type], WIDGET_TYPE_BUTTON
jne .case_3
call gui_logic_button
jmp .done
.case_3:
cmp byte [gs:si + widget.type], WIDGET_TYPE_ROUND_BUTTON
jne .case_4
call gui_logic_button
jmp .done
.case_4:
cmp byte [gs:si + widget.type], WIDGET_TYPE_CHECKBOX
jne .case_5
call gui_logic_checkbox
.case_5:
jmp .done
.miss:
; Si on n'est plus dessus, mais qu'on l'était avant (HOVER/PRESSED), il faut redessiner !
cmp byte [gs:si + widget.state], GUI_STATE_NORMAL
je .done ; Déjà normal, rien à faire
; Cas particulier : si c'est un slider en cours de drag, on continue la logique
; cmp byte [gs:si + widget.type], WIDGET_TYPE_SLIDER
; jne .reset_state
; cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
; je .gui_logic_slider
.reset_state:
mov byte [gs:si + widget.state], GUI_STATE_NORMAL
xor ax, ax
.done:
pop ds
ret
; --- Logique spécifique Bouton ---
gui_logic_button:
test bl, 1 ; Clic gauche ?
jz .released
; Clic enfoncé
mov byte [gs:si + widget.state], GUI_STATE_PRESSED
xor ax, ax
ret
.released:
cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
jne .hover
; Clic validé !
mov al, 1
mov byte [gs:si + widget.state], GUI_STATE_HOVER
ret
.hover:
mov byte [gs:si + widget.state], GUI_STATE_HOVER
xor ax, ax
ret
; --- Logique spécifique Slider ---
gui_logic_slider:
test bl, 1 ; Bouton enfoncé ?
jz .released
; Si on vient de cliquer (pas encore PRESSED), on initialise l'ancrage
cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
je .do_drag
mov byte [gs:si + widget.state], GUI_STATE_PRESSED
; Calcul de l'ancrage
mov ax, cx
sub ax, [gs:si + widget.attr_val]
mov [gs:si + widget.attr_anchor], ax
cmp byte [gs:si + widget.attr_mode], 2
jne .do_drag
mov ax, dx
sub ax, [gs:si + widget.attr_val]
mov [gs:si + widget.attr_anchor], ax
.do_drag:
; --- LOGIQUE DE DÉPLACEMENT ---
cmp byte [gs:si + widget.attr_mode], 1
je .drag_h
cmp byte [gs:si + widget.attr_mode], 2
je .drag_v
xor ax, ax
ret
.drag_h:
mov ax, cx
sub ax, [gs:si + widget.attr_anchor] ; Nouvelle pos = MouseX - Anchor
; Clamp Min
cmp ax, [gs:si + widget.attr_min]
jge .chk_max_h
mov ax, [gs:si + widget.attr_min]
.chk_max_h:
; Clamp Max
cmp ax, [gs:si + widget.attr_max]
jle .apply_pos
mov ax, [gs:si + widget.attr_max]
jmp .apply_pos
.drag_v:
mov ax, dx
sub ax, [gs:si + widget.attr_anchor]
; Clamp Min
cmp ax, [gs:si + widget.attr_min]
jge .chk_max_v
mov ax, [gs:si + widget.attr_min]
.chk_max_v:
; Clamp Max
cmp ax, [gs:si + widget.attr_max]
jle .apply_pos
mov ax, [gs:si + widget.attr_max]
jmp .apply_pos
.apply_pos:
cmp ax, [gs:si + widget.attr_val]
je .no_change
mov [gs:si + widget.attr_val], ax
; Appel du callback on_drag
cmp word [gs:si + widget.event_drag], 0
je .force_redraw
pusha
call word [gs:si + widget.event_drag]
popa
.force_redraw:
mov byte [gs:si + widget.oldstate], 255 ; Force le redessin
.no_change:
xor ax, ax
ret
.released:
mov byte [gs:si + widget.state], GUI_STATE_HOVER
xor ax, ax
ret
; --- Logique spécifique Checkbox ---
gui_logic_checkbox:
test bl, 1 ; Clic gauche ?
jz .released
; Clic enfoncé
mov byte [gs:si + widget.state], GUI_STATE_PRESSED
xor ax, ax
ret
.released:
cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
jne .hover
; Clic validé ! Toggle value
xor word [gs:si + widget.attr_val], 1
mov byte [gs:si + widget.oldstate], 255 ; Force redraw
mov al, 1
mov byte [gs:si + widget.state], GUI_STATE_HOVER
ret
.hover:
mov byte [gs:si + widget.state], GUI_STATE_HOVER
xor ax, ax
ret
; Dessine le widget pointé par SI
gui_draw_single_widget:
pusha
GFX MOUSE_HIDE ; On cache la souris AVANT de commencer le dessin du widget
mov al, [gs:si + widget.state]
cmp al, [gs:si + widget.oldstate]
je .done
mov [gs:si + widget.oldstate], al ; marque comme à jour
; Chargement coords
mov ax, [gs:si + widget.x]
mov bx, [gs:si + widget.y]
mov cx, [gs:si + widget.w]
mov dx, [gs:si + widget.h]
; Calcul X2, Y2
add cx, ax
add dx, bx
mov [gs:si + widget.x2], cx
mov [gs:si + widget.y2], dx
; Dispatch selon le type
cmp byte [gs:si + widget.type], WIDGET_TYPE_SLIDER
jne .case_2
call draw_slider
jmp .done
.case_2:
cmp byte [gs:si + widget.type], WIDGET_TYPE_BUTTON
jne .case_3
call draw_button
jmp .done
.case_3:
cmp byte [gs:si + widget.type], WIDGET_TYPE_ROUND_BUTTON
jne .case_4
call draw_round_button
jmp .done
.case_4:
cmp byte [gs:si + widget.type], WIDGET_TYPE_CHECKBOX
jne .case_5
call draw_checkbox
jmp .done
.case_5:
.done:
GFX MOUSE_SHOW ; On réaffiche la souris APRES, capturant le widget fini
popa
ret
draw_slider:
; 1. Dessiner la piste (Track)
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE_LIGHT
GFX RECTANGLE, ax, bx, cx, dx, 0
; 2. Calculer la taille du curseur (Thumb)
push ax ; Sauvegarde coords track (X1, Y1, X2, Y2)
push bx
push cx
push dx
cmp byte [gs:si + widget.attr_mode], 2
je .calc_v
; --- Horizontal ---
mov ax, [gs:si + widget.w]
xor bh, bh
mov bl, [gs:si + widget.thumb_pct]
mul bx
mov bx, 100
div bx ; AX = Thumb Width
mov bx, [gs:si + widget.y] ; BX = Y1
mov dx, bx
add dx, [gs:si + widget.h] ; DX = Y2
mov cx, [gs:si + widget.attr_val] ; CX = X1
add ax, cx ; AX = X1 + Width = X2
xchg ax, cx ; AX = X1, CX = X2
jmp .draw_thumb
.calc_v:
; --- Vertical ---
mov ax, [gs:si + widget.h]
xor bh, bh
mov bl, [gs:si + widget.thumb_pct]
mul bx
mov bx, 100
div bx ; AX = Thumb Height
mov bx, [gs:si + widget.attr_val] ; BX = Y1
mov dx, bx
add dx, ax ; DX = Y2
mov ax, [gs:si + widget.x] ; AX = X1
mov cx, ax
add cx, [gs:si + widget.w] ; CX = X2
.draw_thumb:
; 3. Dessiner le curseur (Thumb)
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE
GFX RECTANGLE, ax, bx, cx, dx, 0
add sp, 8 ; Nettoyer la pile des push ax..dx
GFX TXT_MODE, GFX_TXT_BLACK_TRANSPARENT
call draw_text
ret
draw_button:
; Dispatch selon état
cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
je .paint_pressed
cmp byte [gs:si + widget.state], GUI_STATE_HOVER
je .paint_hover
.draw_default_style:
; Bordure extérieure épaisse (2px)
GFX RECTANGLE, ax, bx, cx, dx, 0
inc ax
inc bx
dec cx
dec dx
GFX RECTANGLE, ax, bx, cx, dx, 0
; Bordure intérieure fine (après un gap de 1px blanc)
add ax, 2
add bx, 2
sub cx, 2
sub dx, 2
GFX RECTANGLE, ax, bx, cx, dx, 0
jmp .done
.paint_hover:
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE_LIGHT
GFX RECTANGLE, ax, bx, cx, dx, 0
jmp .done
.paint_pressed:
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_BLACK
GFX TXT_MODE, GFX_TXT_WHITE_TRANSPARENT
.done:
; on affiche le texte
; Recharger les coordonnées de base pour le texte
mov ax, [gs:si + widget.x]
mov bx, [gs:si + widget.y]
GFX TXT_MODE, GFX_TXT_BLACK_TRANSPARENT
call draw_text
ret
;
; dessine un bouton arrondi
;
draw_round_button:
; Dispatch selon état
cmp byte [gs:si + widget.state], GUI_STATE_PRESSED
je .paint_pressed
cmp byte [gs:si + widget.state], GUI_STATE_HOVER
je .paint_hover
.paint_normal:
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE
mov di, 0 ; Couleur Noire
call draw_round_borders
GFX TXT_MODE, GFX_TXT_BLACK_TRANSPARENT
jmp .draw_text_now
.paint_hover:
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE
GFX RECTANGLE_ROUND, ax, bx, cx, dx, 0
; GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE_LIGHT
mov di, 0 ; Couleur Noire
call draw_round_borders
GFX TXT_MODE, GFX_TXT_BLACK_TRANSPARENT
jmp .draw_text_now
.paint_pressed:
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_BLACK
mov di, 1 ; Bordure Blanche sur fond noir
call draw_round_borders
GFX TXT_MODE, GFX_TXT_WHITE_TRANSPARENT
jmp .draw_text_now
.draw_text_now:
; Recharger les coordonnées de base pour le texte
mov ax, [gs:si + widget.x]
mov bx, [gs:si + widget.y]
call draw_text
jmp .done
.done:
ret
draw_round_borders:
; Bordure principale
add ax, 2
add bx, 2
sub cx, 2
sub dx, 2
GFX RECTANGLE_ROUND, ax, bx, cx, dx, di
ret
draw_checkbox:
; met un "fond" blanc
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE
cmp byte [gs:si + widget.state], GUI_STATE_HOVER
jne .no_hover
GFX RECTANGLE_ROUND, ax, bx, cx, dx, 0
.no_hover:
; Calcul coords box (centré verticalement)
mov ax, [gs:si + widget.x]
add ax, 4
mov bx, [gs:si + widget.y]
mov cx, [gs:si + widget.h]
mov dx, [gs:si + widget.w]
sub cx, GUI_CHECKBOX_SIZE
shr cx, 1
add bx, cx ; Y1
mov cx, ax
add cx, GUI_CHECKBOX_SIZE ; X2
mov dx, bx
add dx, GUI_CHECKBOX_SIZE ; Y2
; Sauvegarde coords pour le X
push ax
push bx
push cx
push dx
; Fond blanc + Bordure noire
GFX RECTANGLE_FILL, ax, bx, cx, dx, PATTERN_WHITE
GFX RECTANGLE, ax, bx, cx, dx, 0
; Check if checked
cmp word [gs:si + widget.attr_val], 0
je .draw_label
; Dessin du X (Diagonales)
add ax, 2
add bx, 2
sub cx, 2
sub dx, 2
; Optimisation : Utilisation de LINE au lieu de PUTPIXEL en boucle
; Diagonale 1 : (ax, bx) -> (cx, dx)
GFX LINE, ax, bx, cx, dx, 0
; Diagonale 2 : (ax, dx) -> (cx, bx)
GFX LINE, ax, dx, cx, bx, 0
.draw_label:
pop dx
pop cx
pop bx
pop ax
; Dessin du texte à droite (BoxX2 + 6)
add cx, 6
; Y = Centré par rapport au widget
mov bx, [gs:si + widget.y]
mov ax, [gs:si + widget.h]
sub ax, 8
shr ax, 1
add bx, ax
inc bx
GFX GOTOXY, cx, bx
mov dx, [gs:si + widget.text_seg]
mov ax, [gs:si + widget.text_ofs]
GFX WRITE, dx, ax
ret
;
; affiche le texte au centre du widget gs:si
;
draw_text:
; Petit hack pour recentrer le texte après l'effet gras
mov cx, [gs:si + widget.w]
mov dx, [gs:si + widget.h]
.text:
; --- Centrage Texte (Simplifié) ---
mov es, [gs:si + widget.text_seg]
mov di, [gs:si + widget.text_ofs]
xor cx, cx
.strlen:
cmp byte [es:di], 0
je .calc
inc cx
inc di
jmp .strlen
.calc:
shl cx, 3 ; Largeur texte px
push ax ; Sauve X original
mov ax, [gs:si + widget.w]
sub ax, cx
shr ax, 1
pop dx ; Récupère X original (poussé depuis AX)
add ax, dx ; AX = X final centré
push ax ; Sauvegarde X final pour GOTOXY
mov bx, [gs:si + widget.h]
sub bx, 8
shr bx, 1
add bx, [gs:si + widget.y] ; Y final
inc bx
pop cx ; CX = X Final, BX = Y Final
GFX GOTOXY, cx, bx
mov dx, [gs:si + widget.text_seg]
mov ax, [gs:si + widget.text_ofs]
GFX WRITE, dx, ax
ret