Widget-based UI

Unit: VGAUI

Widget-based UI framework for VGA Mode 13h with keyboard and mouse support, using Delphi-style event handlers.

Types

type
  { Event handler procedure types }
  {$F+}
  TKeyPressEvent = procedure(Sender: PWidget; KeyCode: Byte);
  TMouseEvent = procedure(Sender: PWidget; X, Y: Integer; Button: Byte);
  TFocusEvent = procedure(Sender: PWidget);
  TUpdateProcedure = procedure;
  {$F-}

  TUIStyle = object
    HighColor, NormalColor, LowColor, FocusColor: Byte;
    constructor Init(High, Normal, Low, Focus: Byte);
    procedure RenderPanel(const R: TRectangle; Pressed: Boolean; FrameBuffer: PFrameBuffer); virtual;
  end;

  TWidget = object  { Base class }
    Rectangle: TRectangle;
    Visible, Enabled, Focused, NeedsRedraw: Boolean;
    Tag: Integer;
    WidgetType: TWidgetType;

    { Delphi-style event callbacks }
    OnKeyPress: Pointer;   { TKeyPressEvent }
    OnMouseDown: Pointer;  { TMouseEvent }
    OnMouseUp: Pointer;    { TMouseEvent }
    OnMouseMove: Pointer;  { TMouseEvent }
    OnFocus: Pointer;      { TFocusEvent }
    OnBlur: Pointer;       { TFocusEvent }

    constructor Init(X, Y: Integer; W, H: Word);
    procedure MarkDirty;
    procedure SetVisible(Value: Boolean);
    procedure SetEnabled(Value: Boolean);

    { Event trigger methods - override to intercept events }
    procedure DoKeyPress(KeyCode: Byte); virtual;
    procedure DoMouseDown(X, Y: Integer; Button: Byte); virtual;
    procedure DoMouseUp(X, Y: Integer; Button: Byte); virtual;
    procedure DoMouseMove(X, Y: Integer; Button: Byte); virtual;
    procedure DoFocus; virtual;
    procedure DoBlur; virtual;

    procedure Update(DeltaTime: Real); virtual;
    procedure Render(FrameBuffer: PFrameBuffer; Style: PUIStyle); virtual;
    destructor Done; virtual;
  end;

  TLabel = object(TWidget)
    Text: PShortString;
    Lines: TMultiLineText;
    LineCount: Byte;
    Font: PFont;
    TextAlign: Byte;

    constructor Init(X, Y: Integer; W, H: Word; const TextStr: string; FontPtr: PFont);
    procedure SetText(const NewText: string);
    procedure Render(FrameBuffer: PFrameBuffer; Style: PUIStyle); virtual;
    destructor Done; virtual;
  end;

  TButton = object(TWidget)
    Text: PShortString;
    Lines: TMultiLineText;
    LineCount: Byte;
    Font: PFont;
    TextAlign: Byte;
    Pressed: Boolean;

    constructor Init(X, Y: Integer; W, H: Word; const TextStr: string; FontPtr: PFont);
    procedure SetText(const NewText: string);
    procedure DoKeyPress(KeyCode: Byte); virtual;
    procedure DoMouseDown(X, Y: Integer; Button: Byte); virtual;
    procedure DoBlur; virtual;
    procedure Render(FrameBuffer: PFrameBuffer; Style: PUIStyle); virtual;
    destructor Done; virtual;
  end;

  TCheckbox = object(TWidget)
    Text: PShortString;
    Lines: TMultiLineText;
    LineCount: Byte;
    Font: PFont;
    Image: PImage;
    ImageAlign: Byte;
    Checked: Boolean;

    constructor Init(X, Y: Integer; W, H: Word; const TextStr: string; FontPtr: PFont; CheckboxImage: PImage);
    procedure SetText(const NewText: string);
    procedure SetChecked(Value: Boolean);
    function IsChecked: Boolean;
    procedure DoKeyPress(KeyCode: Byte); virtual;
    procedure DoMouseDown(X, Y: Integer; Button: Byte); virtual;
    procedure Render(FrameBuffer: PFrameBuffer; Style: PUIStyle); virtual;
    destructor Done; virtual;
  end;

  TLineEdit = object(TWidget)
    Text: PShortString;
    Font: PFont;
    MaxLength: Byte;
    CursorVisible: Boolean;
    CursorTimer: Real;

    constructor Init(X, Y: Integer; W, H: Word; FontPtr: PFont; MaxLen: Byte);
    procedure SetText(const NewText: string);
    function GetText: string;
    procedure Clear;
    procedure DoKeyPress(KeyCode: Byte); virtual;
    procedure DoMouseDown(X, Y: Integer; Button: Byte); virtual;
    procedure Update(DeltaTime: Real); virtual;
    procedure Render(FrameBuffer: PFrameBuffer; Style: PUIStyle); virtual;
    destructor Done; virtual;
  end;

  TUIManager = object
    Widgets: TLinkedList;
    FocusedWidget: PWidget;
    BackBuffer, BackgroundBuffer: PFrameBuffer;
    Style: PUIStyle;
    Running: Boolean;
    LastMouseButtons: Byte;

    procedure Init(FrameBuffer, Background: PFrameBuffer);
    procedure AddWidget(Widget: PWidget);
    procedure RemoveWidget(Widget: PWidget);
    procedure SetFocus(Widget: PWidget);
    procedure FocusInDirection(DX, DY: Integer);
    procedure DispatchKeyboardEvents;
    procedure DispatchMouseEvents;
    procedure Update(DeltaTime: Real);
    procedure RenderAll;
    procedure RenderDirty;
    procedure SetStyle(NewStyle: PUIStyle);
    procedure Run(UpdateProcedure: Pointer; VSync: Boolean);
    procedure Stop;
    procedure Done;
  end;

Example

uses VGA, VGAFont, VGAUI, Keyboard, Mouse, RTCTimer;

var
  UI: TUIManager;
  BackBuffer, Background: PFrameBuffer;
  Font: TFont;
  Button: PButton;
  Checkbox: PCheckbox;
  LineEdit: PLineEdit;

{$F+}
procedure OnButtonClick(Sender: PWidget);
begin
  WriteLn('Button clicked!');
end;

procedure OnCheckboxClick(Sender: PWidget);
var
  CB: PCheckbox;
begin
  CB := PCheckbox(Sender);
  { Checkbox already toggled by widget }
  if CB^.IsChecked then
    WriteLn('Checkbox checked')
  else
    WriteLn('Checkbox unchecked');
end;

procedure OnNameSubmit(Sender: PWidget; KeyCode: Byte);
var
  Input: PLineEdit;
begin
  if KeyCode = Key_Enter then
  begin
    Input := PLineEdit(Sender);
    WriteLn('Name submitted: ', Input^.GetText);
  end;
end;

procedure OnUpdate;
begin
  if IsKeyPressed(Key_Escape) then
    UI.Stop;
end;
{$F-}

begin
  InitVGA;
  InitKeyboard;
  InitMouse;
  ShowMouse;
  BackBuffer := CreateFrameBuffer;
  Background := CreateFrameBuffer;
  ClearFrameBuffer(Background);

  { Load font }
  LoadFont('DATA\FONT.XML', Font);

  { Setup UI }
  UI.Init(BackBuffer, Background);

  { Create widgets - MUST use constructor syntax }
  New(Button, Init(10, 10, 120, 20, 'Click Me', @Font));
  Button^.OnClick := @OnButtonClick;
  UI.AddWidget(Button);

  New(Checkbox, Init(10, 35, 120, 16, 'Enable Sound', @Font, @CheckboxImage));
  Checkbox^.OnClick := @OnCheckboxClick;
  Checkbox^.SetChecked(True);
  UI.AddWidget(Checkbox);

  New(LineEdit, Init(10, 55, 120, 16, @Font, 20));
  LineEdit^.OnKeyPress := @OnNameSubmit;
  LineEdit^.SetText('Player Name');
  UI.AddWidget(LineEdit);

  { Focus first widget }
  UI.SetFocus(Button);

  { Run UI loop with VSync }
  UI.Run(@OnUpdate, True);

  { Cleanup }
  UI.RemoveWidget(Button); Dispose(Button, Done);
  UI.RemoveWidget(Checkbox); Dispose(Checkbox, Done);
  UI.RemoveWidget(LineEdit); Dispose(LineEdit, Done);
  UI.Done;

  FreeFont(Font);
  FreeFrameBuffer(BackBuffer);
  FreeFrameBuffer(Background);
  HideMouse;
  DoneMouse;
  DoneKeyboard;
  DoneVGA;
end.

Event Handlers

Delphi-Style Event Callbacks

VGAUI uses Delphi VCL-style event handlers. Event handlers MUST use {$F+} directive:

{$F+}
procedure OnButtonClick(Sender: PWidget);
begin
  { Handle button click (keyboard or mouse) }
end;
{$F-}

{ Assign event handler }
Button^.OnClick := @OnButtonClick;

Available Events

  • OnClick - TClickEvent - Widget activated (Enter/Space or complete mouse click)

  • OnKeyPress - TKeyPressEvent - Key pressed while widget focused

  • OnMouseDown - TMouseEvent - Mouse button pressed on widget

  • OnMouseUp - TMouseEvent - Mouse button released

  • OnMouseMove - TMouseEvent - Mouse moved while button down

  • OnFocus - TFocusEvent - Widget gained focus

  • OnBlur - TFocusEvent - Widget lost focus

When to Use OnClick vs OnKeyPress

Use OnClick for buttons/checkboxes:

  • Fires on complete activation (key release or mouse click)

  • No need to check for specific keys

  • Works for both keyboard (Enter/Space) and mouse clicks

  • Provides tactile feedback (button press/release animation)

Use OnKeyPress for text input or custom key handling:

  • LineEdit uses this for character input

  • When you need to handle specific keys (F1, Escape, etc.)

  • When you need low-level key processing

Pattern for forms with text input + submit button:

{ Shared submission logic }
procedure DoSubmit;
begin
  { Get text from input and process it }
end;

{ Button handler - fires on click }
procedure OnSubmitButton(Sender: PWidget);
begin
  DoSubmit;
end;

{ LineEdit handler - fires on Enter key }
procedure OnInputEnter(Sender: PWidget; KeyCode: Byte);
begin
  if KeyCode = Key_Enter then
    DoSubmit;
end;

{ Assign handlers }
LineEdit^.OnKeyPress := @OnInputEnter;  { Enter in text field }
SubmitButton^.OnClick := @OnSubmitButton;  { Click button }

Virtual Do* Methods

Widgets can override Do* methods to intercept events before callbacks:

procedure TMyButton.DoKeyPress(KeyCode: Byte);
begin
  { Custom handling }
  if KeyCode = Key_F1 then
    ShowHelp
  else
    inherited DoKeyPress(KeyCode);  { Call parent + user callback }
end;

Rendering

{ Full render (all widgets) }
UI.RenderAll;
RenderFrameBuffer(BackBuffer);

{ Optimized dirty rendering (40x faster) }
UI.RenderDirty;  { Only renders widgets with NeedsRedraw=True }

UI Loop

{ Built-in UI loop with timing }
UI.Run(@OnUpdate, True);  { VSync enabled }

{ Manual loop }
InitRTC(1024);
Last := GetTimeSeconds;
while Running do
begin
  Cur := GetTimeSeconds;
  DT := Cur - Last;
  Last := Cur;

  OnUpdate;  { User code }
  UI.Update(DT);
  UI.RenderDirty;
  ClearKeyPressed;
end;
DoneRTC;

Alignment Constants

{ Horizontal }
Align_Left   = 1;
Align_Center = 2;
Align_Right  = 4;

{ Vertical }
Align_Top    = 8;
Align_Middle = 16;
Align_Bottom = 32;

{ Examples }
Label^.TextAlign := Align_Center + Align_Middle;  { Centered text }
Label^.TextAlign := Align_Left + Align_Top;       { Top-left aligned }

Critical Notes

  1. Constructor syntax - MUST use New(Widget, Init(...)) for VMT

  2. Destructor syntax - MUST use Dispose(Widget, Done)

  3. Far calls - Event handlers need {$F+} directive

  4. Remove before dispose - Call UI.RemoveWidget before Dispose

  5. Font/Image lifetime - Caller manages, widgets don’t copy

  6. Event assignment - Use @ operator: Widget^.OnKeyPress := @Handler

Widget Memory

{ Widget owns: }
- Text: PShortString (allocated in Init, freed in Done)
- Lines: TMultiLineText (for multi-line text wrapping)

{ Widget does NOT own: }
- Font: PFont (caller manages)
- Image: PImage (caller manages - for checkbox)

Widget Types

WidgetType_Base     = 0;
WidgetType_Label    = 1;
WidgetType_Button   = 2;
WidgetType_Checkbox = 3;
WidgetType_LineEdit = 4;

Notes

  • Keyboard + mouse navigation (Tab, arrows, Enter/Space, click)

  • 3D beveled panels (Windows 95-style)

  • Dirty rectangle optimization for 40x performance boost

  • Delphi VCL-style event architecture

  • Multi-line text support with automatic wrapping

  • Blinking cursor in LineEdit (500ms interval)

  • See TESTS\UITEST.PAS for complete example

  • See DOCS\DESIGN\UI-REFACTOR.md for event system design