536 lines
18 KiB
C
536 lines
18 KiB
C
// widgetClass.c — Widget class vtable definitions
|
|
//
|
|
// This file implements a C vtable pattern for the widget type system.
|
|
// Each widget type has a static const WidgetClassT struct that defines
|
|
// its behavior — paint, layout, mouse handling, keyboard handling, etc.
|
|
// A master table (widgetClassTable[]) maps WidgetTypeE enum values to
|
|
// their class definitions, enabling O(1) dispatch.
|
|
//
|
|
// Why a vtable approach instead of switch statements:
|
|
// 1. Adding a new widget type is purely additive — define a new class
|
|
// struct and add one entry to the table. No existing switch
|
|
// statements need modification, reducing the risk of forgetting
|
|
// a case.
|
|
// 2. NULL function pointers indicate "no behavior" naturally. A box
|
|
// container has no paint function because the framework paints
|
|
// its children directly. A label has no onKey handler because
|
|
// it's not interactive. This is cleaner than empty case blocks.
|
|
// 3. The flags field combines multiple boolean properties into a
|
|
// single bitmask, avoiding per-type if-chains in hot paths
|
|
// like hit testing and layout.
|
|
//
|
|
// Class flags:
|
|
// WCLASS_FOCUSABLE — widget can receive keyboard focus (Tab order)
|
|
// WCLASS_BOX_CONTAINER — uses the generic box layout engine (VBox/HBox)
|
|
// WCLASS_HORIZ_CONTAINER — lays out children horizontally (HBox variant)
|
|
// WCLASS_PAINTS_CHILDREN — widget handles its own child painting (TabControl,
|
|
// TreeView, ScrollPane, Splitter). The default paint
|
|
// walker skips children for these widgets.
|
|
// WCLASS_NO_HIT_RECURSE — hit testing stops at this widget instead of
|
|
// recursing into children. Used by widgets that
|
|
// manage their own internal click regions (TreeView,
|
|
// ScrollPane, ListView, Splitter).
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
// ============================================================
|
|
// Per-type class definitions
|
|
// ============================================================
|
|
//
|
|
// Containers (VBox, HBox, RadioGroup, TabPage, StatusBar, Toolbar)
|
|
// typically have NULL for most function pointers because their
|
|
// behavior is handled generically by the box layout engine and
|
|
// the default child-walking paint logic.
|
|
//
|
|
// Leaf widgets (Button, Label, TextInput, etc.) override paint,
|
|
// calcMinSize, and usually onMouse/onKey for interactivity.
|
|
|
|
static const WidgetClassT sClassVBox = {
|
|
.flags = WCLASS_BOX_CONTAINER,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassHBox = {
|
|
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassLabel = {
|
|
.flags = 0,
|
|
.paint = widgetLabelPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetLabelCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = widgetLabelGetText,
|
|
.setText = widgetLabelSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassButton = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetButtonPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetButtonCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetButtonOnMouse,
|
|
.onKey = widgetButtonOnKey,
|
|
.destroy = NULL,
|
|
.getText = widgetButtonGetText,
|
|
.setText = widgetButtonSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassCheckbox = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetCheckboxPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetCheckboxCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetCheckboxOnMouse,
|
|
.onKey = widgetCheckboxOnKey,
|
|
.destroy = NULL,
|
|
.getText = widgetCheckboxGetText,
|
|
.setText = widgetCheckboxSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassRadioGroup = {
|
|
.flags = WCLASS_BOX_CONTAINER,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassRadio = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetRadioPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetRadioCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetRadioOnMouse,
|
|
.onKey = widgetRadioOnKey,
|
|
.destroy = NULL,
|
|
.getText = widgetRadioGetText,
|
|
.setText = widgetRadioSetText
|
|
};
|
|
|
|
// TextInput and TextArea have destroy functions because they dynamically
|
|
// allocate their text buffer and undo buffer. Most other widgets store
|
|
// all data inline in the WidgetT union and need no cleanup.
|
|
static const WidgetClassT sClassTextInput = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetTextInputPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetTextInputCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetTextInputOnMouse,
|
|
.onKey = widgetTextInputOnKey,
|
|
.destroy = widgetTextInputDestroy,
|
|
.getText = widgetTextInputGetText,
|
|
.setText = widgetTextInputSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassTextArea = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetTextAreaPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetTextAreaCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetTextAreaOnMouse,
|
|
.onKey = widgetTextAreaOnKey,
|
|
.destroy = widgetTextAreaDestroy,
|
|
.getText = widgetTextAreaGetText,
|
|
.setText = widgetTextAreaSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassListBox = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetListBoxPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetListBoxCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetListBoxOnMouse,
|
|
.onKey = widgetListBoxOnKey,
|
|
.destroy = widgetListBoxDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassSpacer = {
|
|
.flags = 0,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSpacerCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassSeparator = {
|
|
.flags = 0,
|
|
.paint = widgetSeparatorPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSeparatorCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassFrame = {
|
|
.flags = WCLASS_BOX_CONTAINER,
|
|
.paint = widgetFramePaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
// Dropdown and ComboBox use paintOverlay to draw their popup lists
|
|
// on top of the entire widget tree. This is rendered in a separate
|
|
// pass after the main paint, so the popup can overlap sibling widgets.
|
|
static const WidgetClassT sClassDropdown = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetDropdownPaint,
|
|
.paintOverlay = widgetDropdownPaintPopup,
|
|
.calcMinSize = widgetDropdownCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetDropdownOnMouse,
|
|
.onKey = widgetDropdownOnKey,
|
|
.destroy = NULL,
|
|
.getText = widgetDropdownGetText,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassComboBox = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetComboBoxPaint,
|
|
.paintOverlay = widgetComboBoxPaintPopup,
|
|
.calcMinSize = widgetComboBoxCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetComboBoxOnMouse,
|
|
.onKey = widgetComboBoxOnKey,
|
|
.destroy = widgetComboBoxDestroy,
|
|
.getText = widgetComboBoxGetText,
|
|
.setText = widgetComboBoxSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassProgressBar = {
|
|
.flags = 0,
|
|
.paint = widgetProgressBarPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetProgressBarCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassSlider = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetSliderPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSliderCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetSliderOnMouse,
|
|
.onKey = widgetSliderOnKey,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
// TabControl has both PAINTS_CHILDREN and a custom layout function
|
|
// because it needs to show only the active tab page's children and
|
|
// position them inside the tab content area (below the tab strip).
|
|
static const WidgetClassT sClassTabControl = {
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
|
|
.paint = widgetTabControlPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetTabControlCalcMinSize,
|
|
.layout = widgetTabControlLayout,
|
|
.onMouse = widgetTabControlOnMouse,
|
|
.onKey = widgetTabControlOnKey,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassTabPage = {
|
|
.flags = WCLASS_BOX_CONTAINER,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassStatusBar = {
|
|
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
|
.paint = widgetStatusBarPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassToolbar = {
|
|
.flags = WCLASS_BOX_CONTAINER | WCLASS_HORIZ_CONTAINER,
|
|
.paint = widgetToolbarPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
// TreeView uses all three special flags:
|
|
// PAINTS_CHILDREN — it renders tree items itself (with indentation,
|
|
// expand/collapse buttons, and selection highlighting)
|
|
// NO_HIT_RECURSE — mouse clicks go to the TreeView widget, which
|
|
// figures out which tree item was clicked based on scroll position
|
|
// and item Y coordinates, rather than letting the hit tester
|
|
// recurse into child TreeItem widgets
|
|
static const WidgetClassT sClassTreeView = {
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
|
.paint = widgetTreeViewPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetTreeViewCalcMinSize,
|
|
.layout = widgetTreeViewLayout,
|
|
.onMouse = widgetTreeViewOnMouse,
|
|
.onKey = widgetTreeViewOnKey,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
// TreeItem has no paint/mouse/key handlers — it's a data-only node.
|
|
// The TreeView parent widget handles all rendering and interaction.
|
|
// TreeItem exists as a WidgetT so it can participate in the tree
|
|
// structure (parent/child/sibling links) for hierarchical data.
|
|
static const WidgetClassT sClassTreeItem = {
|
|
.flags = 0,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = NULL,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = widgetTreeItemGetText,
|
|
.setText = widgetTreeItemSetText
|
|
};
|
|
|
|
static const WidgetClassT sClassImage = {
|
|
.flags = 0,
|
|
.paint = widgetImagePaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetImageCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetImageOnMouse,
|
|
.onKey = NULL,
|
|
.destroy = widgetImageDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassImageButton = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetImageButtonPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetImageButtonCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetImageButtonOnMouse,
|
|
.onKey = widgetImageButtonOnKey,
|
|
.destroy = widgetImageButtonDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassCanvas = {
|
|
.flags = 0,
|
|
.paint = widgetCanvasPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetCanvasCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetCanvasOnMouse,
|
|
.onKey = NULL,
|
|
.destroy = widgetCanvasDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassListView = {
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE,
|
|
.paint = widgetListViewPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetListViewCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetListViewOnMouse,
|
|
.onKey = widgetListViewOnKey,
|
|
.destroy = widgetListViewDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassAnsiTerm = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetAnsiTermPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetAnsiTermCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetAnsiTermOnMouse,
|
|
.onKey = widgetAnsiTermOnKey,
|
|
.destroy = widgetAnsiTermDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
// ScrollPane, Splitter: PAINTS_CHILDREN because they clip/position
|
|
// children in a custom way; NO_HIT_RECURSE because they manage their
|
|
// own scrollbar/divider hit regions.
|
|
static const WidgetClassT sClassScrollPane = {
|
|
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
|
.paint = widgetScrollPanePaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetScrollPaneCalcMinSize,
|
|
.layout = widgetScrollPaneLayout,
|
|
.onMouse = widgetScrollPaneOnMouse,
|
|
.onKey = widgetScrollPaneOnKey,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassSplitter = {
|
|
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
|
.paint = widgetSplitterPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSplitterCalcMinSize,
|
|
.layout = widgetSplitterLayout,
|
|
.onMouse = widgetSplitterOnMouse,
|
|
.onKey = NULL,
|
|
.destroy = NULL,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassTimer = {
|
|
.flags = 0,
|
|
.paint = NULL,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetTimerCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = NULL,
|
|
.onKey = NULL,
|
|
.destroy = widgetTimerDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL
|
|
};
|
|
|
|
static const WidgetClassT sClassSpinner = {
|
|
.flags = WCLASS_FOCUSABLE,
|
|
.paint = widgetSpinnerPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSpinnerCalcMinSize,
|
|
.layout = NULL,
|
|
.onMouse = widgetSpinnerOnMouse,
|
|
.onKey = widgetSpinnerOnKey,
|
|
.destroy = NULL,
|
|
.getText = widgetSpinnerGetText,
|
|
.setText = widgetSpinnerSetText
|
|
};
|
|
|
|
// ============================================================
|
|
// Class table — indexed by WidgetTypeE
|
|
// ============================================================
|
|
//
|
|
// This array is the central dispatch table for the widget system.
|
|
// Indexed by the WidgetTypeE enum, it provides O(1) lookup of any
|
|
// widget type's class definition. Every WidgetT stores a pointer
|
|
// to its class (w->wclass) set at allocation time, so per-widget
|
|
// dispatch doesn't even need to index this table — it's a direct
|
|
// pointer dereference through the vtable.
|
|
//
|
|
// Using C99 designated initializers ensures the array slots match
|
|
// the enum values even if the enum is reordered. If a new enum
|
|
// value is added without a table entry, it will be NULL, which
|
|
// callers check for before dispatching.
|
|
|
|
const WidgetClassT *widgetClassTable[] = {
|
|
[WidgetVBoxE] = &sClassVBox,
|
|
[WidgetHBoxE] = &sClassHBox,
|
|
[WidgetLabelE] = &sClassLabel,
|
|
[WidgetButtonE] = &sClassButton,
|
|
[WidgetCheckboxE] = &sClassCheckbox,
|
|
[WidgetRadioGroupE] = &sClassRadioGroup,
|
|
[WidgetRadioE] = &sClassRadio,
|
|
[WidgetTextInputE] = &sClassTextInput,
|
|
[WidgetTextAreaE] = &sClassTextArea,
|
|
[WidgetListBoxE] = &sClassListBox,
|
|
[WidgetSpacerE] = &sClassSpacer,
|
|
[WidgetSeparatorE] = &sClassSeparator,
|
|
[WidgetFrameE] = &sClassFrame,
|
|
[WidgetDropdownE] = &sClassDropdown,
|
|
[WidgetComboBoxE] = &sClassComboBox,
|
|
[WidgetProgressBarE] = &sClassProgressBar,
|
|
[WidgetSliderE] = &sClassSlider,
|
|
[WidgetTabControlE] = &sClassTabControl,
|
|
[WidgetTabPageE] = &sClassTabPage,
|
|
[WidgetStatusBarE] = &sClassStatusBar,
|
|
[WidgetToolbarE] = &sClassToolbar,
|
|
[WidgetTreeViewE] = &sClassTreeView,
|
|
[WidgetTreeItemE] = &sClassTreeItem,
|
|
[WidgetImageE] = &sClassImage,
|
|
[WidgetImageButtonE] = &sClassImageButton,
|
|
[WidgetCanvasE] = &sClassCanvas,
|
|
[WidgetAnsiTermE] = &sClassAnsiTerm,
|
|
[WidgetListViewE] = &sClassListView,
|
|
[WidgetSpinnerE] = &sClassSpinner,
|
|
[WidgetScrollPaneE] = &sClassScrollPane,
|
|
[WidgetSplitterE] = &sClassSplitter,
|
|
[WidgetTimerE] = &sClassTimer
|
|
};
|