From 9ee73c88061dc6471dbd7071be9c417cd6f9bc6b Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 24 Mar 2026 23:03:05 -0500 Subject: [PATCH] MAJOR refactoring. Everything is dynamically loaded now. All new bugs to squash! --- Makefile | 16 +- apps/Makefile | 2 +- apps/cpanel/cpanel.c | 9 + apps/dvxdemo/dvxdemo.c | 39 +- apps/notepad/notepad.c | 1 + apps/progman/progman.c | 6 +- config/combobox.dep | 2 + config/dropdown.dep | 1 + config/dvxshell.dep | 14 +- config/listbox.dep | 1 + config/listhelp.dep | 2 + config/listview.dep | 1 + config/spinner.dep | 1 + config/terminal.dep | 1 + config/texthelp.dep | 2 + config/textinpt.dep | 1 + config/treeview.dep | 1 + core/Makefile | 16 +- core/README.md | 2 +- core/dvxApp.c | 275 +--- core/dvxApp.h | 4 +- core/dvxDialog.c | 10 +- core/dvxDraw.c | 8 +- core/dvxFont.h | 561 +------ core/{dvxIcon.c => dvxImage.c} | 0 core/dvxTypes.h | 6 +- core/dvxWidget.h | 849 ++-------- core/dvxWidgetPlugin.h | 213 +++ core/dvxWm.c | 48 +- core/dvxWm.h | 2 +- core/platform/dvxPlatform.h | 41 +- core/platform/dvxPlatformDos.c | 140 +- {tasks => core}/thirdparty/stb_ds.h | 0 core/widgetClass.c | 101 +- core/widgetCore.c | 1338 ++-------------- core/widgetEvent.c | 553 +------ core/widgetInternal.h | 494 ------ core/widgetLayout.c | 38 +- core/widgetOps.c | 71 +- core/widgetScrollbar.c | 408 +---- listhelp/Makefile | 42 + listhelp/listHelp.c | 187 +++ listhelp/listHelp.h | 45 + loader/Makefile | 2 +- loader/loaderMain.c | 119 +- shell/Makefile | 5 +- shell/shellApp.c | 46 +- shell/shellApp.h | 7 +- shell/shellExport.c | 49 +- shell/shellMain.c | 147 +- shell/shellTaskMgr.c | 8 +- tasks/Makefile | 5 +- tasks/demo.c | 144 -- tasks/taskswitch.c | 5 +- texthelp/Makefile | 42 + texthelp/textHelp.c | 739 +++++++++ texthelp/textHelp.h | 38 + widgets/Makefile | 19 +- widgets/README.md | 2 +- widgets/widgetAnsiTerm.c | 2258 +++++++-------------------- widgets/widgetAnsiTerm.h | 31 + widgets/widgetBox.c | 155 +- widgets/widgetBox.h | 23 + widgets/widgetButton.c | 170 +- widgets/widgetButton.h | 19 + widgets/widgetCanvas.c | 533 ++++--- widgets/widgetCanvas.h | 43 + widgets/widgetCheckbox.c | 177 ++- widgets/widgetCheckbox.h | 23 + widgets/widgetComboBox.c | 450 ++++-- widgets/widgetComboBox.h | 25 + widgets/widgetDropdown.c | 313 ++-- widgets/widgetDropdown.h | 25 + widgets/widgetImage.c | 259 +-- widgets/widgetImage.h | 23 + widgets/widgetImageButton.c | 319 ++-- widgets/widgetImageButton.h | 23 + widgets/widgetLabel.c | 135 +- widgets/widgetLabel.h | 21 + widgets/widgetListBox.c | 590 +++---- widgets/widgetListBox.h | 37 + widgets/widgetListView.c | 1035 ++++++------ widgets/widgetListView.h | 47 + widgets/widgetProgressBar.c | 186 ++- widgets/widgetProgressBar.h | 25 + widgets/widgetRadio.c | 289 ++-- widgets/widgetRadio.h | 25 + widgets/widgetScrollPane.c | 146 +- widgets/widgetScrollPane.h | 19 + widgets/widgetSeparator.c | 104 +- widgets/widgetSeparator.h | 21 + widgets/widgetSlider.c | 270 ++-- widgets/widgetSlider.h | 23 + widgets/widgetSpacer.c | 50 +- widgets/widgetSpacer.h | 19 + widgets/widgetSpinner.c | 364 +++-- widgets/widgetSpinner.h | 27 + widgets/widgetSplitter.c | 237 ++- widgets/widgetSplitter.h | 23 + widgets/widgetStatusBar.c | 88 +- widgets/widgetStatusBar.h | 19 + widgets/widgetTabControl.c | 308 ++-- widgets/widgetTabControl.h | 25 + widgets/widgetTextInput.c | 871 ++++++----- widgets/widgetTextInput.h | 25 + widgets/widgetTimer.c | 349 +++-- widgets/widgetTimer.h | 32 + widgets/widgetToolbar.c | 88 +- widgets/widgetToolbar.h | 19 + widgets/widgetTreeView.c | 734 +++++---- widgets/widgetTreeView.h | 37 + 111 files changed, 8400 insertions(+), 9686 deletions(-) create mode 100644 config/combobox.dep create mode 100644 config/dropdown.dep create mode 100644 config/listbox.dep create mode 100644 config/listhelp.dep create mode 100644 config/listview.dep create mode 100644 config/spinner.dep create mode 100644 config/terminal.dep create mode 100644 config/texthelp.dep create mode 100644 config/textinpt.dep create mode 100644 config/treeview.dep rename core/{dvxIcon.c => dvxImage.c} (100%) create mode 100644 core/dvxWidgetPlugin.h rename {tasks => core}/thirdparty/stb_ds.h (100%) delete mode 100644 core/widgetInternal.h create mode 100644 listhelp/Makefile create mode 100644 listhelp/listHelp.c create mode 100644 listhelp/listHelp.h delete mode 100644 tasks/demo.c create mode 100644 texthelp/Makefile create mode 100644 texthelp/textHelp.c create mode 100644 texthelp/textHelp.h create mode 100644 widgets/widgetAnsiTerm.h create mode 100644 widgets/widgetBox.h create mode 100644 widgets/widgetButton.h create mode 100644 widgets/widgetCanvas.h create mode 100644 widgets/widgetCheckbox.h create mode 100644 widgets/widgetComboBox.h create mode 100644 widgets/widgetDropdown.h create mode 100644 widgets/widgetImage.h create mode 100644 widgets/widgetImageButton.h create mode 100644 widgets/widgetLabel.h create mode 100644 widgets/widgetListBox.h create mode 100644 widgets/widgetListView.h create mode 100644 widgets/widgetProgressBar.h create mode 100644 widgets/widgetRadio.h create mode 100644 widgets/widgetScrollPane.h create mode 100644 widgets/widgetSeparator.h create mode 100644 widgets/widgetSlider.h create mode 100644 widgets/widgetSpacer.h create mode 100644 widgets/widgetSpinner.h create mode 100644 widgets/widgetSplitter.h create mode 100644 widgets/widgetStatusBar.h create mode 100644 widgets/widgetTabControl.h create mode 100644 widgets/widgetTextInput.h create mode 100644 widgets/widgetTimer.h create mode 100644 widgets/widgetToolbar.h create mode 100644 widgets/widgetTreeView.h diff --git a/Makefile b/Makefile index 87e511f3..2504117c 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ # DVX GUI -- Top-level Makefile # # Builds the full DVX stack: core library, task switcher, -# bootstrap loader, widgets, shell, and apps. +# bootstrap loader, text help library, widgets, shell, and apps. -.PHONY: all clean core tasks loader widgets shell apps +.PHONY: all clean core tasks loader texthelp listhelp widgets shell apps -all: core tasks loader widgets shell apps +all: core tasks loader texthelp listhelp widgets shell apps core: $(MAKE) -C core @@ -16,7 +16,13 @@ tasks: loader: core tasks $(MAKE) -C loader -widgets: core tasks +texthelp: core tasks + $(MAKE) -C texthelp + +listhelp: core tasks + $(MAKE) -C listhelp + +widgets: core tasks texthelp listhelp $(MAKE) -C widgets shell: core tasks @@ -29,6 +35,8 @@ clean: $(MAKE) -C core clean $(MAKE) -C tasks clean $(MAKE) -C loader clean + $(MAKE) -C texthelp clean + $(MAKE) -C listhelp clean $(MAKE) -C widgets clean $(MAKE) -C shell clean $(MAKE) -C apps clean diff --git a/apps/Makefile b/apps/Makefile index 79470611..e5a121f8 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -4,7 +4,7 @@ DJGPP_PREFIX = $(HOME)/djgpp/djgpp DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../core/thirdparty -I../tasks -I../tasks/thirdparty -I../shell +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../core/thirdparty -I../widgets -I../tasks -I../core/thirdparty -I../shell OBJDIR = ../obj/apps BINDIR = ../bin/apps diff --git a/apps/cpanel/cpanel.c b/apps/cpanel/cpanel.c index c340ee93..1ee0ee77 100644 --- a/apps/cpanel/cpanel.c +++ b/apps/cpanel/cpanel.c @@ -13,6 +13,15 @@ #include "dvxDialog.h" #include "dvxPrefs.h" #include "dvxWidget.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetCanvas.h" +#include "widgetDropdown.h" +#include "widgetLabel.h" +#include "widgetListBox.h" +#include "widgetSlider.h" +#include "widgetSpacer.h" +#include "widgetTabControl.h" #include "dvxWm.h" #include "dvxPlatform.h" #include "shellApp.h" diff --git a/apps/dvxdemo/dvxdemo.c b/apps/dvxdemo/dvxdemo.c index 79c8aa8b..8810bdc8 100644 --- a/apps/dvxdemo/dvxdemo.c +++ b/apps/dvxdemo/dvxdemo.c @@ -17,6 +17,30 @@ #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" +#include "widgetAnsiTerm.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetCanvas.h" +#include "widgetCheckbox.h" +#include "widgetComboBox.h" +#include "widgetDropdown.h" +#include "widgetImage.h" +#include "widgetImageButton.h" +#include "widgetLabel.h" +#include "widgetListBox.h" +#include "widgetProgressBar.h" +#include "widgetRadio.h" +#include "widgetScrollPane.h" +#include "widgetSeparator.h" +#include "widgetSlider.h" +#include "widgetSpacer.h" +#include "widgetSpinner.h" +#include "widgetSplitter.h" +#include "widgetStatusBar.h" +#include "widgetTabControl.h" +#include "widgetTextInput.h" +#include "widgetToolbar.h" +#include "widgetTreeView.h" #include "dvxWm.h" #include "shellApp.h" @@ -105,11 +129,8 @@ AppDescriptorT appDescriptor = { // ============================================================ static void onCanvasDraw(WidgetT *w, int32_t cx, int32_t cy, bool drag) { - if (drag && w->as.canvas.lastX >= 0) { - wgtCanvasDrawLine(w, w->as.canvas.lastX, w->as.canvas.lastY, cx, cy); - } else { - wgtCanvasDrawLine(w, cx, cy, cx, cy); - } + (void)drag; + wgtCanvasDrawLine(w, cx, cy, cx, cy); } @@ -709,12 +730,12 @@ static void setupControlsWindow(void) { wgtButton(enCol, "A &button"); WidgetT *enChk = wgtCheckbox(enCol, "&Check me"); - enChk->as.checkbox.checked = true; + wgtCheckboxSetChecked(enChk, true); WidgetT *enRg = wgtRadioGroup(enCol); wgtRadio(enRg, "&Radio 1"); WidgetT *enR2 = wgtRadio(enRg, "R&adio 2"); - enRg->as.radioGroup.selectedIdx = enR2->as.radio.index; + wgtRadioGroupSetSelected(enRg, wgtRadioGetIndex(enR2)); static const char *disItems[] = {"Alpha", "Beta", "Gamma"}; @@ -747,13 +768,13 @@ static void setupControlsWindow(void) { wgtSetEnabled(dBtn, false); WidgetT *dChk = wgtCheckbox(disCol, "&Check me"); - dChk->as.checkbox.checked = true; + wgtCheckboxSetChecked(dChk, true); wgtSetEnabled(dChk, false); WidgetT *dRg = wgtRadioGroup(disCol); WidgetT *dR1 = wgtRadio(dRg, "&Radio 1"); WidgetT *dR2 = wgtRadio(dRg, "R&adio 2"); - dRg->as.radioGroup.selectedIdx = dR2->as.radio.index; + wgtRadioGroupSetSelected(dRg, wgtRadioGetIndex(dR2)); wgtSetEnabled(dR1, false); wgtSetEnabled(dR2, false); diff --git a/apps/notepad/notepad.c b/apps/notepad/notepad.c index a789b363..bb87ac64 100644 --- a/apps/notepad/notepad.c +++ b/apps/notepad/notepad.c @@ -13,6 +13,7 @@ #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" +#include "widgetTextInput.h" #include "dvxWm.h" #include "dvxPlatform.h" #include "shellApp.h" diff --git a/apps/progman/progman.c b/apps/progman/progman.c index 9aca9c07..1de94ecb 100644 --- a/apps/progman/progman.c +++ b/apps/progman/progman.c @@ -22,6 +22,11 @@ #include "dvxDialog.h" #include "dvxPrefs.h" #include "dvxWidget.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetLabel.h" +#include "widgetStatusBar.h" +#include "widgetTextInput.h" #include "dvxWm.h" #include "dvxPlatform.h" #include "shellApp.h" @@ -195,7 +200,6 @@ static void buildPmWindow(void) { btn->onDblClick = onAppButtonClick; btn->onClick = onAppButtonClick; } - } // Status bar at bottom; weight=100 on the label makes it fill the bar diff --git a/config/combobox.dep b/config/combobox.dep new file mode 100644 index 00000000..445885c5 --- /dev/null +++ b/config/combobox.dep @@ -0,0 +1,2 @@ +texthelp +listhelp diff --git a/config/dropdown.dep b/config/dropdown.dep new file mode 100644 index 00000000..458a3a63 --- /dev/null +++ b/config/dropdown.dep @@ -0,0 +1 @@ +listhelp diff --git a/config/dvxshell.dep b/config/dvxshell.dep index a0d55379..e9f18c93 100644 --- a/config/dvxshell.dep +++ b/config/dvxshell.dep @@ -1,14 +1,4 @@ libtasks libdvx -box -button -checkbox -dropdown -label -listbox -listview -radio -separator -spacer -statbar -textinpt +texthelp +listhelp diff --git a/config/listbox.dep b/config/listbox.dep new file mode 100644 index 00000000..458a3a63 --- /dev/null +++ b/config/listbox.dep @@ -0,0 +1 @@ +listhelp diff --git a/config/listhelp.dep b/config/listhelp.dep new file mode 100644 index 00000000..2afaca0b --- /dev/null +++ b/config/listhelp.dep @@ -0,0 +1,2 @@ +libtasks +libdvx diff --git a/config/listview.dep b/config/listview.dep new file mode 100644 index 00000000..458a3a63 --- /dev/null +++ b/config/listview.dep @@ -0,0 +1 @@ +listhelp diff --git a/config/spinner.dep b/config/spinner.dep new file mode 100644 index 00000000..46c87769 --- /dev/null +++ b/config/spinner.dep @@ -0,0 +1 @@ +texthelp diff --git a/config/terminal.dep b/config/terminal.dep new file mode 100644 index 00000000..46c87769 --- /dev/null +++ b/config/terminal.dep @@ -0,0 +1 @@ +texthelp diff --git a/config/texthelp.dep b/config/texthelp.dep new file mode 100644 index 00000000..2afaca0b --- /dev/null +++ b/config/texthelp.dep @@ -0,0 +1,2 @@ +libtasks +libdvx diff --git a/config/textinpt.dep b/config/textinpt.dep new file mode 100644 index 00000000..46c87769 --- /dev/null +++ b/config/textinpt.dep @@ -0,0 +1 @@ +texthelp diff --git a/config/treeview.dep b/config/treeview.dep new file mode 100644 index 00000000..458a3a63 --- /dev/null +++ b/config/treeview.dep @@ -0,0 +1 @@ +listhelp diff --git a/core/Makefile b/core/Makefile index ecb9853d..f6cd6f46 100644 --- a/core/Makefile +++ b/core/Makefile @@ -7,13 +7,13 @@ DJGPP_PREFIX = $(HOME)/djgpp/djgpp CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -Iplatform -I../tasks +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -Iplatform -I../tasks -Ithirdparty OBJDIR = ../obj/core LIBSDIR = ../bin/libs # Core sources -SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxIcon.c dvxImageWrite.c \ +SRCS = dvxVideo.c dvxDraw.c dvxComp.c dvxWm.c dvxImage.c dvxImageWrite.c \ dvxApp.c dvxDialog.c dvxPrefs.c \ widgetClass.c widgetCore.c widgetScrollbar.c \ widgetLayout.c widgetEvent.c widgetOps.c @@ -28,8 +28,7 @@ DVX_EXPORTS = -E _dvx -E _wgt -E _wm -E _prefs -E _rect -E _draw -E _pack -E _te -E _sCursor -E _sDbl -E _sDebug -E _sClosed -E _sFocused -E _sKey \ -E _sOpen -E _sPressed -E _sDrag -E _sDrawing -E _sResize \ -E _sListView -E _sSplitter -E _sTreeView \ - -E _accelParse \ - -E _clearOther -E _clipboard -E _isWord -E _multiClick -E _wordStart -E _wordEnd + -E _accelParse -E _clipboard -E _multiClick .PHONY: all clean @@ -39,7 +38,8 @@ $(LIBSDIR)/libdvx.dep: ../config/libdvx.dep | $(LIBSDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(LIBSDIR) - $(DXE3GEN) -o $@ $(DVX_EXPORTS) -U $(OBJS) + $(DXE3GEN) -o $(LIBSDIR)/libdvx.dxe $(DVX_EXPORTS) -U $(OBJS) + mv $(LIBSDIR)/libdvx.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -56,13 +56,13 @@ $(OBJDIR)/dvxVideo.o: dvxVideo.c dvxVideo.h platform/dvxPlatform.h dvxTypes $(OBJDIR)/dvxDraw.o: dvxDraw.c dvxDraw.h platform/dvxPlatform.h dvxTypes.h $(OBJDIR)/dvxComp.o: dvxComp.c dvxComp.h platform/dvxPlatform.h dvxTypes.h $(OBJDIR)/dvxWm.o: dvxWm.c dvxWm.h dvxTypes.h dvxDraw.h dvxComp.h dvxVideo.h dvxWidget.h thirdparty/stb_image.h -$(OBJDIR)/dvxIcon.o: dvxIcon.c thirdparty/stb_image.h +$(OBJDIR)/dvxImage.o: dvxImage.c thirdparty/stb_image.h $(OBJDIR)/dvxImageWrite.o: dvxImageWrite.c thirdparty/stb_image_write.h $(OBJDIR)/dvxApp.o: dvxApp.c dvxApp.h platform/dvxPlatform.h dvxTypes.h dvxVideo.h dvxDraw.h dvxComp.h dvxWm.h dvxFont.h dvxCursor.h -$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h widgetInternal.h dvxTypes.h dvxDraw.h +$(OBJDIR)/dvxDialog.o: dvxDialog.c dvxDialog.h platform/dvxPlatform.h dvxApp.h dvxWidget.h dvxWidgetPlugin.h dvxTypes.h dvxDraw.h $(OBJDIR)/dvxPrefs.o: dvxPrefs.c dvxPrefs.h -WIDGET_DEPS = widgetInternal.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h +WIDGET_DEPS = dvxWidgetPlugin.h dvxWidget.h dvxTypes.h dvxApp.h dvxDraw.h dvxWm.h dvxVideo.h $(OBJDIR)/widgetClass.o: widgetClass.c $(WIDGET_DEPS) $(OBJDIR)/widgetCore.o: widgetCore.c $(WIDGET_DEPS) $(OBJDIR)/widgetScrollbar.o: widgetScrollbar.c $(WIDGET_DEPS) diff --git a/core/README.md b/core/README.md index 29eca7b3..f3638d9a 100644 --- a/core/README.md +++ b/core/README.md @@ -75,7 +75,7 @@ DXE modules in `../widgets/`. | File | Purpose | |------|---------| -| `widgetInternal.h` | Internal header for widget implementation files: vtable externs, shared state, helper prototypes | +| `dvxWidgetPlugin.h` | Internal header for widget implementation files: vtable externs, shared state, helper prototypes | | `widgetClass.c` | Widget class table (`widgetClassTable[]`, starts empty) and `wgtRegisterClass()` dynamic registration | | `widgetCore.c` | Widget allocation, tree ops, hit testing, focus management, shared helpers | | `widgetScrollbar.c` | Widget-internal scrollbar drawing and hit testing (shared by ListBox, TreeView, etc.) | diff --git a/core/dvxApp.c b/core/dvxApp.c index 83f2c684..06e08281 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // dvx_app.c -- Layer 5: Application API for DVX GUI // // Top-level layer of the DVX windowing system. This is the only layer @@ -31,7 +32,8 @@ #include "dvxApp.h" #include "dvxDialog.h" #include "dvxWidget.h" -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "../widgets/widgetTimer.h" #include "dvxFont.h" #include "dvxCursor.h" @@ -119,8 +121,8 @@ static void openContextMenu(AppContextT *ctx, WindowT *win, MenuT *menu, int32_t static void openPopupAtMenu(AppContextT *ctx, WindowT *win, int32_t menuIdx); static void openSubMenu(AppContextT *ctx); static void openSysMenu(AppContextT *ctx, WindowT *win); -static void pollAnsiTermWidgets(AppContextT *ctx); -static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); +static void pollWidgets(AppContextT *ctx); +static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win); static void pollKeyboard(AppContextT *ctx); static void pollMouse(AppContextT *ctx); static void refreshMinimizedIcons(AppContextT *ctx); @@ -713,135 +715,31 @@ static bool dispatchAccelKey(AppContextT *ctx, char key) { if (win->widgetRoot) { WidgetT *target = widgetFindByAccel(win->widgetRoot, key); - if (target) { - switch (target->type) { - case WidgetButtonE: + if (target && target->wclass) { + if (target->wclass->flags & WCLASS_FOCUS_FORWARD) { + // Label/Frame: focus-forward to next focusable + WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target); + + if (next) { if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = target; - target->focused = true; - target->as.button.pressed = true; - sKeyPressedBtn = target; - wgtInvalidate(target); - return true; + sFocusedWidget = next; + next->focused = true; - case WidgetCheckboxE: - if (sFocusedWidget) { sFocusedWidget->focused = false; } - if (target->wclass && target->wclass->onMouse) { - target->wclass->onMouse(target, win->widgetRoot, 0, 0); - } - sFocusedWidget = target; - wgtInvalidate(target); - return true; - - case WidgetRadioE: - if (sFocusedWidget) { sFocusedWidget->focused = false; } - if (target->wclass && target->wclass->onMouse) { - target->wclass->onMouse(target, win->widgetRoot, 0, 0); - } - sFocusedWidget = target; - wgtInvalidate(target); - return true; - - case WidgetImageButtonE: - if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = target; - target->focused = true; - target->as.imageButton.pressed = true; - sKeyPressedBtn = target; - wgtInvalidate(target); - return true; - - case WidgetTabPageE: - { - // Close any open dropdown/combobox popup - if (sOpenPopup) { - if (sOpenPopup->type == WidgetDropdownE) { - sOpenPopup->as.dropdown.open = false; - } else if (sOpenPopup->type == WidgetComboBoxE) { - sOpenPopup->as.comboBox.open = false; - } - - sOpenPopup = NULL; + if (next->wclass && next->wclass->onAccelActivate) { + next->wclass->onAccelActivate(next, win->widgetRoot); } - // Activate this tab in its parent TabControl - if (target->parent && target->parent->type == WidgetTabControlE) { - int32_t tabIdx = 0; - - for (WidgetT *c = target->parent->firstChild; c; c = c->nextSibling) { - if (c == target) { - wgtTabControlSetActive(target->parent, tabIdx); - break; - } - - if (c->type == WidgetTabPageE) { - tabIdx++; - } - } - } - - return true; - } - - case WidgetDropdownE: - target->as.dropdown.open = true; - target->as.dropdown.hoverIdx = target->as.dropdown.selectedIdx; - sOpenPopup = target; - if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = target; - target->focused = true; wgtInvalidate(win->widgetRoot); - return true; - - case WidgetComboBoxE: - target->as.comboBox.open = true; - target->as.comboBox.hoverIdx = target->as.comboBox.selectedIdx; - sOpenPopup = target; - if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = target; - target->focused = true; - wgtInvalidate(win->widgetRoot); - return true; - - case WidgetLabelE: - case WidgetFrameE: - { - // Focus the next focusable widget - WidgetT *next = widgetFindNextFocusable(win->widgetRoot, target); - - if (next) { - if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = next; - next->focused = true; - - // Open dropdown/combobox if that's the focused target - if (next->type == WidgetDropdownE) { - next->as.dropdown.open = true; - next->as.dropdown.hoverIdx = next->as.dropdown.selectedIdx; - sOpenPopup = next; - } else if (next->type == WidgetComboBoxE) { - next->as.comboBox.open = true; - next->as.comboBox.hoverIdx = next->as.comboBox.selectedIdx; - sOpenPopup = next; - } - - wgtInvalidate(win->widgetRoot); - } - - return true; } - - default: - // For focusable widgets, just focus them - if (widgetIsFocusable(target->type)) { - if (sFocusedWidget) { sFocusedWidget->focused = false; } - sFocusedWidget = target; - target->focused = true; - wgtInvalidate(win->widgetRoot); - return true; - } - break; + } else if (target->wclass->onAccelActivate) { + if (sFocusedWidget) { sFocusedWidget->focused = false; } + sFocusedWidget = target; + target->focused = true; + target->wclass->onAccelActivate(target, win->widgetRoot); + wgtInvalidate(win->widgetRoot); } + + return true; } } @@ -1237,20 +1135,12 @@ static void dispatchEvents(AppContextT *ctx) { // itself or a ScrollPane ancestor that contains it. WidgetT *target = NULL; - if (focus->type == WidgetListBoxE || - focus->type == WidgetListViewE || - focus->type == WidgetTreeViewE || - focus->type == WidgetTextAreaE || - focus->type == WidgetAnsiTermE || - focus->type == WidgetSliderE || - focus->type == WidgetSpinnerE || - focus->type == WidgetDropdownE || - focus->type == WidgetComboBoxE) { + if (focus->wclass && (focus->wclass->flags & WCLASS_SCROLLABLE)) { target = focus; } else { // Walk up to find a ScrollPane ancestor for (WidgetT *p = focus->parent; p; p = p->parent) { - if (p->type == WidgetScrollPaneE) { + if (p->wclass && (p->wclass->flags & WCLASS_SCROLL_CONTAINER)) { target = p; break; } @@ -1704,7 +1594,7 @@ void dvxResetColorScheme(AppContextT *ctx) { // Arranges windows in the classic cascade pattern: each window is the same // size (2/3 of screen), offset diagonally by the title bar height so each // title bar remains visible. When the cascade would go off-screen, it wraps -// back to (0,0). This matches DESQview/X and Windows 3.x cascade behavior. +// back to (0,0). This matches the Windows 3.x cascade behavior. // The step size is title_height + border_width so exactly one title bar's // worth of the previous window peeks out above and to the left. @@ -2692,7 +2582,7 @@ bool dvxUpdate(AppContextT *ctx) { pollKeyboard(ctx); dispatchEvents(ctx); updateTooltip(ctx); - pollAnsiTermWidgets(ctx); + pollWidgets(ctx); wgtUpdateCursorBlink(); wgtUpdateTimers(); @@ -2715,10 +2605,8 @@ bool dvxUpdate(AppContextT *ctx) { // onClick. The one-frame delay ensures the pressed visual state // renders before the callback runs (which may open a dialog, etc.). if (sKeyPressedBtn) { - if (sKeyPressedBtn->type == WidgetImageButtonE) { - sKeyPressedBtn->as.imageButton.pressed = false; - } else { - sKeyPressedBtn->as.button.pressed = false; + if (sKeyPressedBtn->wclass && sKeyPressedBtn->wclass->setPressed) { + sKeyPressedBtn->wclass->setPressed(sKeyPressedBtn, false); } if (sKeyPressedBtn->onClick) { @@ -3088,7 +2976,7 @@ static void interactiveWindowScreenshot(AppContextT *ctx, WindowT *win) { // ============================================================ // // Executes a system menu (window control menu) command. The system menu -// is the DESQview/X equivalent of the Win3.x control-menu box -- it +// is the DVX equivalent of the Win3.x control-menu box -- it // provides Restore, Move, Size, Minimize, Maximize, and Close. Keyboard // move/resize mode is entered by setting kbMoveResize state, which causes // pollKeyboard to intercept arrow keys until Enter/Esc. @@ -3188,7 +3076,7 @@ static WindowT *findWindowById(AppContextT *ctx, int32_t id) { // hitPart 0: content area (forwarded to window's onMouse callback) // hitPart 1: title bar (begins mouse drag) // hitPart 2: close/sys-menu gadget (single-click opens sys menu, -// double-click closes -- DESQview/X convention) +// double-click closes -- DVX convention) // hitPart 3: resize border (begins edge/corner resize) // hitPart 4: menu bar (opens popup for clicked menu) // hitPart 5/6: vertical/horizontal scrollbar @@ -3371,7 +3259,7 @@ static void handleMouseButton(AppContextT *ctx, int32_t mx, int32_t my, int32_t // by GEOS Ensemble with Motif-style 3D bevels: teal desktop, grey window // chrome with white highlights and dark shadows to create the raised/sunken // illusion. The dark charcoal active title bar distinguishes it from -// GEOS's blue, giving DV/X its own identity. +// GEOS's blue, giving DVX its own identity. static void initColorScheme(AppContextT *ctx) { // Load defaults into the RGB source array, then pack all at once @@ -3660,56 +3548,37 @@ static void openSysMenu(AppContextT *ctx, WindowT *win) { // ============================================================ -// pollAnsiTermWidgets -- poll and repaint all ANSI term widgets +// pollWidgets -- poll widgets that need periodic updates // ============================================================ // -// ANSI terminal widgets have asynchronous data sources (PTYs, serial -// ports) that produce output between frames. This function walks every -// window's widget tree looking for AnsiTerm widgets, polls them for new -// data, and if data arrived, triggers a targeted repaint of just the -// affected rows. The fine-grained dirty rect (just the changed rows -// rather than the whole window) is critical for terminal performance -- -// a single character echo should only flush one row to the LFB, not -// the entire terminal viewport. +// Widgets with WCLASS_NEEDS_POLL (e.g. ANSI terminals with asynchronous +// data sources) need periodic polling between frames. This function walks +// every window's widget tree and calls the poll vtable method on matching +// widgets. The poll method handles data ingestion and targeted dirty +// rect generation for efficient repainting. -static void pollAnsiTermWidgets(AppContextT *ctx) { +static void pollWidgets(AppContextT *ctx) { for (int32_t i = 0; i < ctx->stack.count; i++) { WindowT *win = ctx->stack.windows[i]; if (win->widgetRoot) { - pollAnsiTermWidgetsWalk(ctx, win->widgetRoot, win); + pollWidgetsWalk(ctx, win->widgetRoot, win); } } } // ============================================================ -// pollAnsiTermWidgetsWalk -- recursive helper +// pollWidgetsWalk -- recursive helper // ============================================================ -static void pollAnsiTermWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) { - if (w->type == WidgetAnsiTermE) { - wgtAnsiTermPoll(w); - - int32_t dirtyY = 0; - int32_t dirtyH = 0; - - if (wgtAnsiTermRepaint(w, &dirtyY, &dirtyH) > 0) { - win->contentDirty = true; - - // Dirty only the affected rows (in screen coords) instead of - // the entire window. This dramatically reduces compositor and - // LFB flush work for cursor blink and single-row updates. - int32_t scrollY = win->vScroll ? win->vScroll->value : 0; - int32_t rectX = win->x + win->contentX; - int32_t rectY = win->y + win->contentY + dirtyY - scrollY; - int32_t rectW = win->contentW; - dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); - } +static void pollWidgetsWalk(AppContextT *ctx, WidgetT *w, WindowT *win) { + if (w->wclass && (w->wclass->flags & WCLASS_NEEDS_POLL) && w->wclass->poll) { + w->wclass->poll(w, win); } for (WidgetT *child = w->firstChild; child; child = child->nextSibling) { - pollAnsiTermWidgetsWalk(ctx, child, win); + pollWidgetsWalk(ctx, child, win); } } @@ -4273,16 +4142,16 @@ static void pollKeyboard(AppContextT *ctx) { int32_t popY; int32_t popW; int32_t popH; - widgetDropdownPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH); + if (sOpenPopup->wclass && sOpenPopup->wclass->getPopupRect) { + sOpenPopup->wclass->getPopupRect(sOpenPopup, &ctx->font, popWin->contentH, &popX, &popY, &popW, &popH); + } dirtyListAdd(&ctx->dirty, popWin->x + popWin->contentX + popX, popWin->y + popWin->contentY + popY, popW, popH); WidgetT *closing = sOpenPopup; sOpenPopup = NULL; - if (closing->type == WidgetDropdownE) { - closing->as.dropdown.open = false; - } else if (closing->type == WidgetComboBoxE) { - closing->as.comboBox.open = false; + if (closing->wclass && closing->wclass->closePopup) { + closing->wclass->closePopup(closing); } wgtInvalidate(closing); @@ -4323,8 +4192,8 @@ static void pollKeyboard(AppContextT *ctx) { WidgetT *w = fstack[--ftop]; if (w->focused && widgetIsFocusable(w->type)) { - // Don't tab out of the terminal -- it swallows Tab - if (w->type == WidgetAnsiTermE) { + // Don't tab out of widgets that swallow Tab + if (w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { current = NULL; break; } @@ -4350,7 +4219,7 @@ static void pollKeyboard(AppContextT *ctx) { while (ftop > 0) { WidgetT *w = fstack[--ftop]; - if (w->focused && w->type == WidgetAnsiTermE) { + if (w->focused && w->wclass && (w->wclass->flags & WCLASS_SWALLOWS_TAB)) { termFocused = true; break; } @@ -4592,7 +4461,9 @@ static void updateCursorShape(AppContextT *ctx) { } // Active splitter drag else if (sDragSplitter) { - newCursor = sDragSplitter->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V; + if (sDragSplitter->wclass && sDragSplitter->wclass->getCursorShape) { + newCursor = sDragSplitter->wclass->getCursorShape(sDragSplitter, 0, 0); + } } // Not in an active drag/resize -- check what we're hovering else if (ctx->stack.dragWindow < 0 && ctx->stack.scrollWindow < 0) { @@ -4632,38 +4503,12 @@ static void updateCursorShape(AppContextT *ctx) { WidgetT *hit = widgetHitTest(win->widgetRoot, vx, vy); - if (hit && hit->type == WidgetListViewE && sListViewColBorderHitFn && sListViewColBorderHitFn(hit, vx, vy)) { - newCursor = CURSOR_RESIZE_H; - } + if (hit && hit->wclass && hit->wclass->getCursorShape) { + int32_t shape = hit->wclass->getCursorShape(hit, vx, vy); - // Walk into splitters (NO_HIT_RECURSE stops widgetHitTest at the outermost one) - while (hit && hit->type == WidgetSplitterE) { - int32_t pos = hit->as.splitter.dividerPos; - bool onBar; - - if (hit->as.splitter.vertical) { - int32_t barX = hit->x + pos; - onBar = (vx >= barX && vx < barX + SPLITTER_BAR_W); - } else { - int32_t barY = hit->y + pos; - onBar = (vy >= barY && vy < barY + SPLITTER_BAR_W); + if (shape > 0) { + newCursor = shape; } - - if (onBar) { - newCursor = hit->as.splitter.vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V; - break; - } - - // Not on this splitter's bar -- check children for nested splitters - WidgetT *inner = NULL; - - for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { - if (c->visible && vx >= c->x && vx < c->x + c->w && vy >= c->y && vy < c->y + c->h) { - inner = c; - } - } - - hit = inner; } } } diff --git a/core/dvxApp.h b/core/dvxApp.h index c3818557..cfe5e6e0 100644 --- a/core/dvxApp.h +++ b/core/dvxApp.h @@ -72,7 +72,7 @@ typedef struct AppContextT { // buffer down to icon size. int32_t iconRefreshIdx; // next minimized icon to refresh (staggered) int32_t frameCount; // frame counter for periodic tasks - // The idle callback allows the host application (e.g. the DV/X shell) + // The idle callback allows the host application (e.g. the DVX shell) // to do background work (poll serial ports, service DXE apps) during // frames where no GUI events occurred, instead of just yielding the CPU. void (*idleCallback)(void *ctx); // called instead of yield when non-NULL @@ -184,7 +184,7 @@ void dvxShutdown(AppContextT *ctx); void dvxRun(AppContextT *ctx); // Process exactly one frame of the event loop. Provided for applications -// that need to integrate the GUI into their own main loop (e.g. the DV/X +// that need to integrate the GUI into their own main loop (e.g. the DVX // shell polling serial ports between frames). Returns false when the GUI // wants to exit. bool dvxUpdate(AppContextT *ctx); diff --git a/core/dvxDialog.c b/core/dvxDialog.c index 9dee1dca..af0d2276 100644 --- a/core/dvxDialog.c +++ b/core/dvxDialog.c @@ -23,7 +23,14 @@ #include "dvxDialog.h" #include "dvxPlatform.h" #include "dvxWidget.h" -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "../widgets/widgetBox.h" +#include "../widgets/widgetButton.h" +#include "../widgets/widgetDropdown.h" +#include "../widgets/widgetLabel.h" +#include "../widgets/widgetListBox.h" +#include "../widgets/widgetSpacer.h" +#include "../widgets/widgetTextInput.h" #include #include @@ -864,7 +871,6 @@ static void fdLoadDir(void) { wgtListBoxSetItems(sFd.fileList, sFd.listItems, sFd.entryCount); wgtListBoxSetSelected(sFd.fileList, 0); - sFd.fileList->as.listBox.scrollPos = 0; // Update path display wgtSetText(sFd.pathInput, sFd.curDir); diff --git a/core/dvxDraw.c b/core/dvxDraw.c index 6d44bf8d..6de33302 100644 --- a/core/dvxDraw.c +++ b/core/dvxDraw.c @@ -169,16 +169,16 @@ static inline void clipRect(const DisplayT *d, int32_t *x, int32_t *y, int32_t * // drawBevel // ============================================================ // -// Draws a Motif/DESQview-style beveled rectangular frame. The bevel +// Draws a Motif-style beveled rectangular frame. The bevel // creates the illusion of a raised or sunken 3D surface by drawing // lighter "highlight" edges on the top and left, and darker "shadow" // edges on the bottom and right. Swapping highlight and shadow gives // a sunken appearance (see BEVEL_RAISED/BEVEL_SUNKEN macros in // dvxTypes.h). // -// BevelStyleT.width controls the border thickness. DV/X uses 2px -// bevels for most window chrome (matching the original DESQview/X -// and Motif look), 1px for inner borders and scrollbar elements. +// BevelStyleT.width controls the border thickness. DVX uses 2px +// bevels for most window chrome (matching the Motif look), 1px for +// inner borders and scrollbar elements. // // The implementation has special-cased fast paths for bw==2 and bw==1 // that emit exact spans via rectFill rather than looping. This diff --git a/core/dvxFont.h b/core/dvxFont.h index f2b8f782..78e178ee 100644 --- a/core/dvxFont.h +++ b/core/dvxFont.h @@ -1,11 +1,11 @@ // dvx_font.h -- Embedded VGA bitmap font data (CP437) for DVX GUI // -// Contains the raw glyph bitmaps for two standard VGA ROM fonts (8x14 and -// 8x16) covering the full IBM Code Page 437 character set (256 glyphs). -// These are byte-for-byte copies of the fonts stored in the VGA BIOS ROM. +// Contains the raw glyph bitmaps for the 8x16 VGA ROM font covering the +// full IBM Code Page 437 character set (256 glyphs). This is a byte-for- +// byte copy of the font stored in the VGA BIOS ROM. // -// Embedding them as static const arrays (rather than reading from the VGA -// ROM at INT 10h) serves two purposes: +// Embedding as a static const array (rather than reading from the VGA ROM +// at INT 10h) serves two purposes: // 1. The data is available on non-DOS platforms (Linux/SDL) where no // VGA BIOS exists. // 2. On DOS, reading the VGA ROM font requires a real-mode interrupt @@ -13,13 +13,13 @@ // anyway. Compiling it in is simpler and more portable. // // The glyph format is 1 bit per pixel, 8 pixels wide, with MSB = leftmost -// pixel. Each glyph occupies charHeight consecutive bytes (14 or 16). -// The drawing code in dvxDraw.c processes one byte per scanline, testing -// each bit to decide foreground vs. background -- the 8-pixel width means -// no bit-shifting across byte boundaries is ever needed. +// pixel. Each glyph occupies 16 consecutive bytes. The drawing code in +// dvxDraw.c processes one byte per scanline, testing each bit to decide +// foreground vs. background -- the 8-pixel width means no bit-shifting +// across byte boundaries is ever needed. // // CP437 includes the full ASCII printable range (32-126) plus box-drawing -// characters (176-223), which are essential for the Motif/DV/X look +// characters (176-223), which are essential for the Motif/DVX look // (scrollbar arrows, window close gadget, etc.), as well as international // characters, math symbols, and the classic smiley/card suit characters // in positions 1-31. @@ -29,529 +29,7 @@ #include "dvxTypes.h" // ============================================================ -// 8x14 VGA ROM font -- standard IBM CP437 character set -// 256 glyphs, 14 bytes per glyph, MSB = leftmost pixel -// ============================================================ - -static const uint8_t font8x14[256 * 14] = { - // Char 0: NULL (blank) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 1: Smiley (outline) - 0x00, 0x00, 0x7E, 0x81, 0xA5, 0x81, 0x81, 0xBD, 0x99, 0x81, 0x7E, 0x00, 0x00, 0x00, - // Char 2: Smiley (filled) - 0x00, 0x00, 0x7E, 0xFF, 0xDB, 0xFF, 0xFF, 0xC3, 0xE7, 0xFF, 0x7E, 0x00, 0x00, 0x00, - // Char 3: Heart - 0x00, 0x00, 0x00, 0x6C, 0xFE, 0xFE, 0xFE, 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, - // Char 4: Diamond - 0x00, 0x00, 0x00, 0x10, 0x38, 0x7C, 0xFE, 0x7C, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, - // Char 5: Club - 0x00, 0x00, 0x18, 0x3C, 0x3C, 0xE7, 0xE7, 0xE7, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 6: Spade - 0x00, 0x00, 0x18, 0x3C, 0x7E, 0xFF, 0xFF, 0x7E, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, - // Char 7: Bullet - 0x00, 0x00, 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 8: Inverse bullet - 0xFF, 0xFF, 0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, - // Char 9: Circle (outline) - 0x00, 0x00, 0x00, 0x00, 0x3C, 0x66, 0x42, 0x66, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 10: Inverse circle - 0xFF, 0xFF, 0xFF, 0xFF, 0xC3, 0x99, 0xBD, 0x99, 0xC3, 0xFF, 0xFF, 0xFF, 0x00, 0x00, - // Char 11: Male sign - 0x00, 0x00, 0x1E, 0x0E, 0x1A, 0x32, 0x78, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, 0x00, - // Char 12: Female sign - 0x00, 0x00, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 13: Note single - 0x00, 0x00, 0x3F, 0x33, 0x3F, 0x30, 0x30, 0x30, 0x70, 0xF0, 0xE0, 0x00, 0x00, 0x00, - // Char 14: Note double - 0x00, 0x00, 0x7F, 0x63, 0x7F, 0x63, 0x63, 0x63, 0x67, 0xE7, 0xE6, 0xC0, 0x00, 0x00, - // Char 15: Sun - 0x00, 0x00, 0x18, 0xDB, 0x3C, 0xE7, 0x3C, 0xDB, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 16: Right triangle - 0x00, 0x00, 0x80, 0xC0, 0xE0, 0xF8, 0xFE, 0xF8, 0xE0, 0xC0, 0x80, 0x00, 0x00, 0x00, - // Char 17: Left triangle - 0x00, 0x00, 0x02, 0x06, 0x0E, 0x3E, 0xFE, 0x3E, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, - // Char 18: Up-down arrow - 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, - // Char 19: Double exclamation - 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00, - // Char 20: Paragraph - 0x00, 0x00, 0x7F, 0xDB, 0xDB, 0x7B, 0x1B, 0x1B, 0x1B, 0x1B, 0x00, 0x00, 0x00, 0x00, - // Char 21: Section - 0x00, 0x7C, 0xC6, 0x60, 0x38, 0x6C, 0xC6, 0x6C, 0x38, 0x0C, 0xC6, 0x7C, 0x00, 0x00, - // Char 22: Bottom bar - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFE, 0xFE, 0x00, 0x00, 0x00, - // Char 23: Up-down arrow with base - 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x7E, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, - // Char 24: Up arrow - 0x00, 0x00, 0x18, 0x3C, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, - // Char 25: Down arrow - 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x3C, 0x18, 0x00, 0x00, 0x00, 0x00, - // Char 26: Right arrow - 0x00, 0x00, 0x00, 0x00, 0x18, 0x0C, 0xFE, 0x0C, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 27: Left arrow - 0x00, 0x00, 0x00, 0x00, 0x30, 0x60, 0xFE, 0x60, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 28: Right angle - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xC0, 0xC0, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 29: Left-right arrow - 0x00, 0x00, 0x00, 0x00, 0x24, 0x66, 0xFF, 0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 30: Up triangle - 0x00, 0x00, 0x00, 0x10, 0x38, 0x38, 0x7C, 0x7C, 0xFE, 0xFE, 0x00, 0x00, 0x00, 0x00, - // Char 31: Down triangle - 0x00, 0x00, 0x00, 0xFE, 0xFE, 0x7C, 0x7C, 0x38, 0x38, 0x10, 0x00, 0x00, 0x00, 0x00, - - // Char 32: Space - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 33: ! - 0x00, 0x00, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 34: " - 0x00, 0x66, 0x66, 0x66, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 35: # - 0x00, 0x00, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0xFE, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, - // Char 36: $ - 0x00, 0x18, 0x18, 0x7C, 0xC6, 0xC0, 0x7C, 0x06, 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00, - // Char 37: % - 0x00, 0x00, 0x00, 0x00, 0xC2, 0xC6, 0x0C, 0x18, 0x30, 0x66, 0xC6, 0x00, 0x00, 0x00, - // Char 38: & - 0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x76, 0xDC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 39: ' - 0x00, 0x30, 0x30, 0x30, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 40: ( - 0x00, 0x00, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, - // Char 41: ) - 0x00, 0x00, 0x30, 0x18, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x18, 0x30, 0x00, 0x00, 0x00, - // Char 42: * - 0x00, 0x00, 0x00, 0x00, 0x66, 0x3C, 0xFF, 0x3C, 0x66, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 43: + - 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 44: , - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x30, 0x00, 0x00, - // Char 45: - - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 46: . - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 47: / - 0x00, 0x00, 0x02, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC0, 0x80, 0x00, 0x00, 0x00, 0x00, - // Char 48: 0 - 0x00, 0x00, 0x7C, 0xC6, 0xCE, 0xDE, 0xF6, 0xE6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 49: 1 - 0x00, 0x00, 0x18, 0x38, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x7E, 0x00, 0x00, 0x00, - // Char 50: 2 - 0x00, 0x00, 0x7C, 0xC6, 0x06, 0x0C, 0x18, 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, - // Char 51: 3 - 0x00, 0x00, 0x7C, 0xC6, 0x06, 0x06, 0x3C, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 52: 4 - 0x00, 0x00, 0x0C, 0x1C, 0x3C, 0x6C, 0xCC, 0xFE, 0x0C, 0x0C, 0x1E, 0x00, 0x00, 0x00, - // Char 53: 5 - 0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xFC, 0x06, 0x06, 0x06, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 54: 6 - 0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xFC, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 55: 7 - 0x00, 0x00, 0xFE, 0xC6, 0x06, 0x0C, 0x18, 0x30, 0x30, 0x30, 0x30, 0x00, 0x00, 0x00, - // Char 56: 8 - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 57: 9 - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x06, 0x0C, 0x78, 0x00, 0x00, 0x00, - // Char 58: : - 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, - // Char 59: ; - 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30, 0x00, 0x00, 0x00, - // Char 60: < - 0x00, 0x00, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x00, 0x00, 0x00, - // Char 61: = - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 62: > - 0x00, 0x00, 0x60, 0x30, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00, 0x00, 0x00, - // Char 63: ? - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x0C, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 64: @ - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xDE, 0xDE, 0xDE, 0xDC, 0xC0, 0x7C, 0x00, 0x00, 0x00, - // Char 65: A - 0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 66: B - 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00, - // Char 67: C - 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, 0xC2, 0x66, 0x3C, 0x00, 0x00, 0x00, - // Char 68: D - 0x00, 0x00, 0xF8, 0x6C, 0x66, 0x66, 0x66, 0x66, 0x66, 0x6C, 0xF8, 0x00, 0x00, 0x00, - // Char 69: E - 0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, - // Char 70: F - 0x00, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, - // Char 71: G - 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xDE, 0xC6, 0x66, 0x3A, 0x00, 0x00, 0x00, - // Char 72: H - 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 73: I - 0x00, 0x00, 0x3C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 74: J - 0x00, 0x00, 0x1E, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xCC, 0xCC, 0x78, 0x00, 0x00, 0x00, - // Char 75: K - 0x00, 0x00, 0xE6, 0x66, 0x6C, 0x6C, 0x78, 0x6C, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00, - // Char 76: L - 0x00, 0x00, 0xF0, 0x60, 0x60, 0x60, 0x60, 0x60, 0x62, 0x66, 0xFE, 0x00, 0x00, 0x00, - // Char 77: M - 0x00, 0x00, 0xC6, 0xEE, 0xFE, 0xFE, 0xD6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 78: N - 0x00, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 79: O - 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, - // Char 80: P - 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, - // Char 81: Q - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xDE, 0x7C, 0x0C, 0x0E, 0x00, 0x00, - // Char 82: R - 0x00, 0x00, 0xFC, 0x66, 0x66, 0x66, 0x7C, 0x6C, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, - // Char 83: S - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0x60, 0x38, 0x0C, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 84: T - 0x00, 0x00, 0x7E, 0x7E, 0x5A, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 85: U - 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 86: V - 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, - // Char 87: W - 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xD6, 0xD6, 0xFE, 0x7C, 0x6C, 0x00, 0x00, 0x00, - // Char 88: X - 0x00, 0x00, 0xC6, 0xC6, 0x6C, 0x38, 0x38, 0x38, 0x6C, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 89: Y - 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 90: Z - 0x00, 0x00, 0xFE, 0xC6, 0x8C, 0x18, 0x30, 0x60, 0xC2, 0xC6, 0xFE, 0x00, 0x00, 0x00, - // Char 91: [ - 0x00, 0x00, 0x3C, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3C, 0x00, 0x00, 0x00, - // Char 92: backslash - 0x00, 0x00, 0x80, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x06, 0x02, 0x00, 0x00, 0x00, - // Char 93: ] - 0x00, 0x00, 0x3C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0x3C, 0x00, 0x00, 0x00, - // Char 94: ^ - 0x10, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 95: _ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, - // Char 96: ` - 0x00, 0x30, 0x18, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 97: a - 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 98: b - 0x00, 0x00, 0xE0, 0x60, 0x60, 0x78, 0x6C, 0x66, 0x66, 0x66, 0x7C, 0x00, 0x00, 0x00, - // Char 99: c - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 100: d - 0x00, 0x00, 0x1C, 0x0C, 0x0C, 0x3C, 0x6C, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 101: e - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 102: f - 0x00, 0x00, 0x1C, 0x36, 0x32, 0x30, 0x7C, 0x30, 0x30, 0x30, 0x78, 0x00, 0x00, 0x00, - // Char 103: g - 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0xCC, 0x78, 0x00, - // Char 104: h - 0x00, 0x00, 0xE0, 0x60, 0x60, 0x6C, 0x76, 0x66, 0x66, 0x66, 0xE6, 0x00, 0x00, 0x00, - // Char 105: i - 0x00, 0x00, 0x18, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 106: j - 0x00, 0x00, 0x06, 0x06, 0x00, 0x0E, 0x06, 0x06, 0x06, 0x06, 0x66, 0x66, 0x3C, 0x00, - // Char 107: k - 0x00, 0x00, 0xE0, 0x60, 0x60, 0x66, 0x6C, 0x78, 0x6C, 0x66, 0xE6, 0x00, 0x00, 0x00, - // Char 108: l - 0x00, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 109: m - 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0xFE, 0xD6, 0xD6, 0xD6, 0xC6, 0x00, 0x00, 0x00, - // Char 110: n - 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, - // Char 111: o - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 112: p - 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xF0, 0x00, - // Char 113: q - 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xCC, 0xCC, 0xCC, 0x7C, 0x0C, 0x0C, 0x1E, 0x00, - // Char 114: r - 0x00, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x76, 0x66, 0x60, 0x60, 0xF0, 0x00, 0x00, 0x00, - // Char 115: s - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0x70, 0x1C, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 116: t - 0x00, 0x00, 0x10, 0x30, 0x30, 0xFC, 0x30, 0x30, 0x30, 0x36, 0x1C, 0x00, 0x00, 0x00, - // Char 117: u - 0x00, 0x00, 0x00, 0x00, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 118: v - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x10, 0x00, 0x00, 0x00, - // Char 119: w - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xD6, 0xD6, 0xFE, 0x6C, 0x00, 0x00, 0x00, - // Char 120: x - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0x6C, 0x38, 0x38, 0x6C, 0xC6, 0x00, 0x00, 0x00, - // Char 121: y - 0x00, 0x00, 0x00, 0x00, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0xF8, 0x00, - // Char 122: z - 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xCC, 0x18, 0x30, 0x66, 0xFE, 0x00, 0x00, 0x00, - // Char 123: { - 0x00, 0x00, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x00, 0x00, 0x00, - // Char 124: | - 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 125: } - 0x00, 0x00, 0x70, 0x18, 0x18, 0x18, 0x0E, 0x18, 0x18, 0x18, 0x70, 0x00, 0x00, 0x00, - // Char 126: ~ - 0x00, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 127: House/Delete - 0x00, 0x00, 0x00, 0x00, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0x00, 0x00, 0x00, 0x00, - - // Char 128: C-cedilla (upper) - 0x00, 0x00, 0x3C, 0x66, 0xC2, 0xC0, 0xC0, 0xC0, 0xC2, 0x66, 0x3C, 0x0C, 0x06, 0x7C, - // Char 129: u-diaeresis - 0x00, 0x00, 0xCC, 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 130: e-acute - 0x00, 0x0C, 0x18, 0x30, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 131: a-circumflex - 0x00, 0x10, 0x38, 0x6C, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 132: a-diaeresis - 0x00, 0x00, 0xCC, 0xCC, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 133: a-grave - 0x00, 0x60, 0x30, 0x18, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 134: a-ring - 0x00, 0x38, 0x6C, 0x38, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 135: c-cedilla - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0xC6, 0xC0, 0xC0, 0xC6, 0x7C, 0x18, 0x0C, 0x78, - // Char 136: e-circumflex - 0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 137: e-diaeresis - 0x00, 0x00, 0xC6, 0xC6, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 138: e-grave - 0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xFE, 0xC0, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 139: i-diaeresis - 0x00, 0x00, 0x66, 0x66, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 140: i-circumflex - 0x00, 0x18, 0x3C, 0x66, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 141: i-grave - 0x00, 0x60, 0x30, 0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 142: A-diaeresis - 0x00, 0xC6, 0xC6, 0x10, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 143: A-ring - 0x38, 0x6C, 0x38, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 144: E-acute - 0x0C, 0x18, 0x00, 0xFE, 0x66, 0x62, 0x68, 0x78, 0x68, 0x66, 0xFE, 0x00, 0x00, 0x00, - // Char 145: ae ligature - 0x00, 0x00, 0x00, 0x00, 0x00, 0xEC, 0x36, 0x76, 0xDC, 0xD8, 0x6E, 0x00, 0x00, 0x00, - // Char 146: AE ligature - 0x00, 0x00, 0x3E, 0x6C, 0xCC, 0xCC, 0xFE, 0xCC, 0xCC, 0xCC, 0xCE, 0x00, 0x00, 0x00, - // Char 147: o-circumflex - 0x00, 0x10, 0x38, 0x6C, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 148: o-diaeresis - 0x00, 0x00, 0xC6, 0xC6, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 149: o-grave - 0x00, 0x60, 0x30, 0x18, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 150: u-circumflex - 0x00, 0x30, 0x78, 0xCC, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 151: u-grave - 0x00, 0x60, 0x30, 0x18, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 152: y-diaeresis - 0x00, 0x00, 0xC6, 0xC6, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0x7E, 0x06, 0x0C, 0x78, 0x00, - // Char 153: O-diaeresis - 0x00, 0xC6, 0xC6, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, - // Char 154: U-diaeresis - 0x00, 0xC6, 0xC6, 0x00, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 155: Cent sign - 0x00, 0x18, 0x18, 0x7C, 0xC6, 0xC0, 0xC0, 0xC6, 0x7C, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 156: Pound sterling - 0x00, 0x38, 0x6C, 0x64, 0x60, 0xF0, 0x60, 0x60, 0x60, 0xE6, 0xFC, 0x00, 0x00, 0x00, - // Char 157: Yen sign - 0x00, 0x00, 0x66, 0x66, 0x3C, 0x18, 0x7E, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 158: Peseta - 0x00, 0xF8, 0xCC, 0xCC, 0xF8, 0xC4, 0xCC, 0xDE, 0xCC, 0xCC, 0xC6, 0x00, 0x00, 0x00, - // Char 159: f-hook - 0x00, 0x0E, 0x1B, 0x18, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x18, 0xD8, 0x70, 0x00, 0x00, - // Char 160: a-acute - 0x00, 0x18, 0x30, 0x60, 0x00, 0x78, 0x0C, 0x7C, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 161: i-acute - 0x00, 0x0C, 0x18, 0x30, 0x00, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3C, 0x00, 0x00, 0x00, - // Char 162: o-acute - 0x00, 0x18, 0x30, 0x60, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 163: u-acute - 0x00, 0x18, 0x30, 0x60, 0x00, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0x76, 0x00, 0x00, 0x00, - // Char 164: n-tilde - 0x00, 0x00, 0x76, 0xDC, 0x00, 0xDC, 0x66, 0x66, 0x66, 0x66, 0x66, 0x00, 0x00, 0x00, - // Char 165: N-tilde - 0x76, 0xDC, 0x00, 0xC6, 0xE6, 0xF6, 0xFE, 0xDE, 0xCE, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 166: Feminine ordinal - 0x00, 0x00, 0x3C, 0x6C, 0x6C, 0x3E, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 167: Masculine ordinal - 0x00, 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 168: Inverted question mark - 0x00, 0x00, 0x30, 0x30, 0x00, 0x30, 0x30, 0x60, 0xC6, 0xC6, 0x7C, 0x00, 0x00, 0x00, - // Char 169: Reversed not sign (left corner) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0x00, - // Char 170: Not sign (right corner) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06, 0x06, 0x06, 0x00, 0x00, 0x00, 0x00, - // Char 171: 1/2 - 0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30, 0x66, 0xCE, 0x9E, 0x06, 0x00, 0x00, - // Char 172: 1/4 - 0x00, 0xC0, 0xC0, 0xC2, 0xC6, 0xCC, 0x18, 0x30, 0x60, 0xDC, 0x86, 0x0C, 0x18, 0x00, - // Char 173: Inverted exclamation mark - 0x00, 0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x3C, 0x3C, 0x3C, 0x18, 0x00, 0x00, 0x00, - // Char 174: Left-pointing double angle - 0x00, 0x00, 0x00, 0x00, 0x36, 0x6C, 0xD8, 0x6C, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 175: Right-pointing double angle - 0x00, 0x00, 0x00, 0x00, 0xD8, 0x6C, 0x36, 0x6C, 0xD8, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 176: Light shade - 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, 0x11, 0x44, - // Char 177: Medium shade - 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, - // Char 178: Dark shade - 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, - // Char 179: Box light vertical - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 180: Box light vertical and left - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 181: Box vertical single and left double - 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 182: Box double vertical and left - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xF6, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 183: Box down single and left double - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 184: Box down double and left single - 0x00, 0x00, 0x00, 0x00, 0xF8, 0x18, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 185: Box double vertical and left - 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xF6, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 186: Box double vertical - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 187: Box double down and left - 0x00, 0x00, 0x00, 0x00, 0xFE, 0x06, 0xF6, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 188: Box double up and left - 0x36, 0x36, 0x36, 0x36, 0xF6, 0x06, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 189: Box double up and right (via left) - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 190: Box up single and left double - 0x18, 0x18, 0x18, 0x18, 0xF8, 0x18, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 191: Box light down and left - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 192: Box light up and right - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 193: Box light up and horizontal - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 194: Box light down and horizontal - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 195: Box light vertical and right - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 196: Box light horizontal - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 197: Box light vertical and horizontal - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 198: Box vertical single and right double - 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 199: Box double vertical and right - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x37, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 200: Box double up and right - 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 201: Box double down and right - 0x00, 0x00, 0x00, 0x00, 0x3F, 0x30, 0x37, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 202: Box double up and horizontal - 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 203: Box double down and horizontal - 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xF7, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 204: Box double vertical and right - 0x36, 0x36, 0x36, 0x36, 0x37, 0x30, 0x37, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 205: Box double horizontal - 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 206: Box double vertical and horizontal - 0x36, 0x36, 0x36, 0x36, 0xF7, 0x00, 0xF7, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 207: Box up double and horizontal single - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 208: Box double up and horizontal single (upper) - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 209: Box down double and horizontal single - 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 210: Box double down and horizontal single (upper) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 211: Box double up and right single - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x3F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 212: Box up single and right double - 0x18, 0x18, 0x18, 0x18, 0x1F, 0x18, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 213: Box down single and right double - 0x00, 0x00, 0x00, 0x00, 0x1F, 0x18, 0x1F, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 214: Box double down and right single - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 215: Box double vertical and horizontal single - 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0xFF, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, 0x36, - // Char 216: Box vertical single and horizontal double - 0x18, 0x18, 0x18, 0x18, 0xFF, 0x18, 0xFF, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 217: Box light up and left - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 218: Box light down and right - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 219: Full block - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - // Char 220: Lower half block - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, - // Char 221: Left half block - 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, - // Char 222: Right half block - 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, - // Char 223: Upper half block - 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 224: alpha - 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0xD8, 0xD8, 0xDC, 0x76, 0x00, 0x00, 0x00, - // Char 225: beta (sharp s) - 0x00, 0x00, 0x78, 0xCC, 0xCC, 0xD8, 0xCC, 0xC6, 0xC6, 0xC6, 0xCC, 0x00, 0x00, 0x00, - // Char 226: Gamma - 0x00, 0x00, 0xFE, 0xC6, 0xC6, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0xC0, 0x00, 0x00, 0x00, - // Char 227: pi - 0x00, 0x00, 0x00, 0x00, 0xFE, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, - // Char 228: Sigma (upper) - 0x00, 0x00, 0xFE, 0xC6, 0x60, 0x30, 0x18, 0x30, 0x60, 0xC6, 0xFE, 0x00, 0x00, 0x00, - // Char 229: sigma (lower) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xD8, 0xD8, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00, - // Char 230: mu - 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x66, 0x7C, 0x60, 0x60, 0xC0, 0x00, - // Char 231: tau - 0x00, 0x00, 0x00, 0x00, 0x76, 0xDC, 0x18, 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, - // Char 232: Phi - 0x00, 0x00, 0x7E, 0x18, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x18, 0x7E, 0x00, 0x00, 0x00, - // Char 233: Theta - 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xFE, 0xC6, 0xC6, 0x6C, 0x38, 0x00, 0x00, 0x00, - // Char 234: Omega - 0x00, 0x00, 0x38, 0x6C, 0xC6, 0xC6, 0xC6, 0x6C, 0x6C, 0x6C, 0xEE, 0x00, 0x00, 0x00, - // Char 235: delta - 0x00, 0x00, 0x1C, 0x30, 0x18, 0x0C, 0x3C, 0x66, 0x66, 0x66, 0x3C, 0x00, 0x00, 0x00, - // Char 236: infinity - 0x00, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xDB, 0xDB, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 237: phi (lower) - 0x00, 0x00, 0x06, 0x0C, 0x7C, 0xDE, 0xD6, 0xF6, 0x7C, 0x60, 0xC0, 0x00, 0x00, 0x00, - // Char 238: epsilon - 0x00, 0x00, 0x38, 0x60, 0xC0, 0xC0, 0xF8, 0xC0, 0xC0, 0x60, 0x38, 0x00, 0x00, 0x00, - // Char 239: intersection - 0x00, 0x00, 0x7C, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0xC6, 0x00, 0x00, 0x00, - // Char 240: triple bar (equiv) - 0x00, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x00, 0x00, - // Char 241: plus-minus - 0x00, 0x00, 0x18, 0x18, 0x7E, 0x18, 0x18, 0x00, 0x00, 0x7E, 0x00, 0x00, 0x00, 0x00, - // Char 242: greater-than-or-equal - 0x00, 0x00, 0x30, 0x18, 0x0C, 0x06, 0x0C, 0x18, 0x30, 0x00, 0x7E, 0x00, 0x00, 0x00, - // Char 243: less-than-or-equal - 0x00, 0x00, 0x0C, 0x18, 0x30, 0x60, 0x30, 0x18, 0x0C, 0x00, 0x7E, 0x00, 0x00, 0x00, - // Char 244: top of integral - 0x00, 0x0E, 0x1B, 0x1B, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, - // Char 245: bottom of integral - 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0xD8, 0xD8, 0x70, 0x00, 0x00, 0x00, - // Char 246: division sign - 0x00, 0x00, 0x18, 0x18, 0x00, 0x7E, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 247: approximately equal - 0x00, 0x00, 0x00, 0x76, 0xDC, 0x00, 0x76, 0xDC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 248: degree - 0x00, 0x38, 0x6C, 0x6C, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 249: bullet (middle dot) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 250: small bullet - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 251: square root - 0x00, 0x0F, 0x0C, 0x0C, 0x0C, 0x0C, 0x0C, 0xEC, 0x6C, 0x3C, 0x1C, 0x00, 0x00, 0x00, - // Char 252: superscript n - 0x00, 0xD8, 0x6C, 0x6C, 0x6C, 0x6C, 0x6C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 253: superscript 2 - 0x00, 0x70, 0xD8, 0x30, 0x60, 0xC8, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 254: filled square - 0x00, 0x00, 0x00, 0x00, 0x7C, 0x7C, 0x7C, 0x7C, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, - // Char 255: Non-breaking space (blank) - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, -}; - -// ============================================================ -// 8x16 VGA ROM font -- standard IBM CP437 character set +// 8x16 VGA ROM font -- standard IBM CP437 character set // 256 glyphs, 16 bytes per glyph, MSB = leftmost pixel // ============================================================ @@ -1073,23 +551,8 @@ static const uint8_t font8x16[256 * 16] = { }; // ============================================================ -// Font descriptor structs +// Font descriptor struct // ============================================================ -// -// Pre-built BitmapFontT instances ready to pass to dvxInit() or use -// directly with draw functions. The 8x14 font fits more rows on screen -// (34 rows at 480 pixels) while 8x16 matches the standard VGA text mode -// cell height (30 rows at 480). DVX defaults to 8x14 for maximum usable -// content area, but 8x16 is available for applications that want the -// classic DOS text mode look. - -static const BitmapFontT dvxFont8x14 = { - .charWidth = 8, - .charHeight = 14, - .firstChar = 0, - .numChars = 256, - .glyphData = font8x14, -}; static const BitmapFontT dvxFont8x16 = { .charWidth = 8, diff --git a/core/dvxIcon.c b/core/dvxImage.c similarity index 100% rename from core/dvxIcon.c rename to core/dvxImage.c diff --git a/core/dvxTypes.h b/core/dvxTypes.h index 9d346bc1..d5796feb 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -121,7 +121,7 @@ typedef struct { // Bevel style // ============================================================ // -// Bevels are the defining visual element of the Motif/DESQview/X aesthetic. +// Bevels are the defining visual element of the Motif aesthetic. // Swapping highlight and shadow colors flips between raised and sunken // appearance -- the BEVEL_RAISED and BEVEL_SUNKEN macros encode this // convention so callers don't get the colors backwards. @@ -271,7 +271,7 @@ typedef struct { // ============================================================ // // These define the pixel geometry of the window frame (border, title bar, -// menu bar). They're compile-time constants because the DV/X look demands +// menu bar). They're compile-time constants because the DVX look demands // a fixed, uniform chrome thickness across all windows -- there's no // per-window customization of frame geometry. // @@ -484,7 +484,7 @@ typedef struct { #define SCROLL_HORIZONTAL 1 // Minimized windows display as icons at the bottom of the screen, -// similar to DESQview/X's original icon bar behavior. +// similar to a classic desktop icon bar. #define ICON_SIZE 64 #define ICON_BORDER 2 #define ICON_TOTAL_SIZE (ICON_SIZE + ICON_BORDER * 2) diff --git a/core/dvxWidget.h b/core/dvxWidget.h index 512e2e35..f04afb36 100644 --- a/core/dvxWidget.h +++ b/core/dvxWidget.h @@ -8,11 +8,15 @@ // // Design decisions and rationale: // -// - Single WidgetT struct with a tagged union: avoids the overhead of a -// class hierarchy and virtual dispatch (no C++ vtable indirection). -// The wclass pointer provides vtable-style dispatch only for the few -// operations that genuinely differ per widget type (paint, layout, events). -// Type-specific data lives in the `as` union to keep the struct compact. +// - Generic WidgetT with void *data: core knows nothing about individual +// widget types. Each widget DXE allocates and manages its own private +// data struct via w->data. The wclass vtable provides polymorphic +// dispatch for paint, layout, events, and all type-specific operations. +// No widget-specific enums, structs, or union members in this header. +// +// - Dynamic type IDs: wgtRegisterClass() returns a runtime ID at load +// time. No compile-time enum. Adding a new widget requires zero core +// changes -- just drop a .wgt DXE in the widgets directory. // // - Tagged size values (wgtPixels/wgtChars/wgtPercent): encoding the unit // in the high bits of a single int32_t avoids extra struct fields for @@ -23,11 +27,6 @@ // this halves the pointer overhead per widget and insertion/removal is // still O(n) in the worst case, which is acceptable given typical tree // depths of 5-10 nodes. -// -// - Large widget data (AnsiTermDataT, ListViewDataT) is separately -// allocated and stored as a pointer in the union rather than inlined, -// because these structures are hundreds of bytes and would bloat every -// WidgetT even for simple labels and buttons. #ifndef DVX_WIDGET_H #define DVX_WIDGET_H @@ -63,62 +62,14 @@ struct WidgetClassT; #define wgtChars(v) ((int32_t)(WGT_SIZE_CHARS | ((uint32_t)(v) & WGT_SIZE_VAL_MASK))) #define wgtPercent(v) ((int32_t)(WGT_SIZE_PERCENT | ((uint32_t)(v) & WGT_SIZE_VAL_MASK))) -// ============================================================ -// Widget type enum -// ============================================================ -// -// Used as the index into widgetClassTable[] to look up the vtable for -// each widget type. Built-in types occupy slots 0..WGT_TYPE_BUILTIN_COUNT-1. -// Dynamic types registered via wgtRegisterClass() start at -// WGT_TYPE_DYNAMIC_BASE. - -typedef enum { - WidgetVBoxE, - WidgetHBoxE, - WidgetLabelE, - WidgetButtonE, - WidgetCheckboxE, - WidgetRadioGroupE, - WidgetRadioE, - WidgetTextInputE, - WidgetTextAreaE, - WidgetListBoxE, - WidgetSpacerE, - WidgetSeparatorE, - WidgetFrameE, - WidgetDropdownE, - WidgetComboBoxE, - WidgetProgressBarE, - WidgetSliderE, - WidgetTabControlE, - WidgetTabPageE, - WidgetStatusBarE, - WidgetToolbarE, - WidgetTreeViewE, - WidgetTreeItemE, - WidgetImageE, - WidgetImageButtonE, - WidgetCanvasE, - WidgetAnsiTermE, - WidgetListViewE, - WidgetSpinnerE, - WidgetScrollPaneE, - WidgetSplitterE, - WidgetTimerE, - WGT_TYPE_BUILTIN_COUNT // must be last built-in entry -} WidgetTypeE; - -// Dynamic widget type IDs start here. External DXEs register new -// widget types via wgtRegisterClass() and receive IDs >= this value. -#define WGT_TYPE_DYNAMIC_BASE 32 -#define WGT_MAX_TYPES 64 +// Widget type IDs are assigned dynamically by wgtRegisterClass() at +// runtime. There is no compile-time enum. Each widget DXE stores +// its assigned ID(s) in file-scope globals. // ============================================================ -// ListView types +// ListView column types (used by ListView API in widgetListView.h) // ============================================================ -#define LISTVIEW_MAX_COLS 16 - typedef enum { ListViewAlignLeftE, ListViewAlignCenterE, @@ -152,131 +103,12 @@ typedef enum { } WidgetAlignE; // ============================================================ -// Frame style enum -// ============================================================ - -typedef enum { - FrameInE, // beveled inward (sunken) -- default - FrameOutE, // beveled outward (raised) - FrameFlatE // solid color line -} FrameStyleE; - -// ============================================================ -// Text input mode enum -// ============================================================ - -typedef enum { - InputNormalE, // default free-form text - InputPasswordE, // displays bullets, no copy - InputMaskedE // format mask (e.g. "(###) ###-####") -} InputModeE; - -// ============================================================ -// Large widget data (separately allocated to reduce WidgetT size) +// Widget-specific enums and types used by the public API // ============================================================ // -// AnsiTermDataT and ListViewDataT are heap-allocated and stored as -// pointers in the WidgetT union. Inlining them would add hundreds of -// bytes to every WidgetT instance (even labels and buttons), wasting -// memory on the 30+ simple widgets that don't need terminal or listview -// state. The pointer indirection adds one dereference but saves -// significant memory across a typical widget tree. - -// AnsiTermDataT -- full VT100/ANSI terminal emulator state. -// Implements a subset of DEC VT100 escape sequences sufficient for BBS -// and DOS ANSI art rendering: cursor movement, color attributes (16-color -// with bold-as-bright), scrolling regions, and blink. The parser is a -// simple state machine (normal -> ESC -> CSI) with parameter accumulation. -typedef struct { - uint8_t *cells; // character cells: (ch, attr) pairs, cols*rows*2 bytes - int32_t cols; // columns (default 80) - int32_t rows; // rows (default 25) - int32_t cursorRow; // 0-based cursor row - int32_t cursorCol; // 0-based cursor column - bool cursorVisible; - bool wrapMode; // auto-wrap at right margin - bool bold; // SGR bold flag (brightens foreground) - bool originMode; // cursor positioning relative to scroll region - bool csiPrivate; // '?' prefix in CSI sequence - uint8_t curAttr; // current text attribute (fg | bg<<4) - uint8_t parseState; // 0=normal, 1=ESC, 2=CSI - int32_t params[8]; // CSI parameter accumulator - int32_t paramCount; // number of CSI params collected - int32_t savedRow; // saved cursor position (SCP) - int32_t savedCol; - // Scrolling region (0-based, inclusive) - int32_t scrollTop; // top row of scroll region - int32_t scrollBot; // bottom row of scroll region - // Scrollback -- circular buffer so old lines age out naturally without - // memmove. Each line is cols*2 bytes (same ch/attr format as cells). - // scrollPos tracks the view offset: when equal to scrollbackCount, - // the user sees the live screen; when less, they're viewing history. - uint8_t *scrollback; // circular buffer of scrollback lines - int32_t scrollbackMax; // max lines in scrollback buffer - int32_t scrollbackCount; // current number of lines stored - int32_t scrollbackHead; // write position (circular index) - int32_t scrollPos; // view position (scrollbackCount = live) - // Blink support - bool blinkVisible; // current blink phase (true = text visible) - clock_t blinkTime; // timestamp of last blink toggle - // Cursor blink - bool cursorOn; // current cursor blink phase - clock_t cursorTime; // timestamp of last cursor toggle - // Dirty tracking -- a 32-bit bitmask where each bit corresponds to one - // terminal row. Only dirty rows are repainted, which is critical because - // the ANSI terminal can receive data every frame (at 9600+ baud) and - // re-rendering all 25 rows of 80 columns each frame would dominate the - // CPU budget. Limits terminal height to 32 rows, which is fine for the - // 25-row default target. - uint32_t dirtyRows; // bitmask of rows needing repaint - int32_t lastCursorRow; // cursor row at last repaint - int32_t lastCursorCol; // cursor col at last repaint - // Pre-packed 16-color palette avoids calling packColor() (which involves - // shift/mask arithmetic) 80*25 = 2000 times per full repaint. - uint32_t packedPalette[16]; - bool paletteValid; - // Selection (line indices in scrollback+screen space) - int32_t selStartLine; - int32_t selStartCol; - int32_t selEndLine; - int32_t selEndCol; - bool selecting; - // Communications interface -- abstracted so the terminal can connect to - // different backends (serial port, secLink channel, local pipe) without - // knowing the transport details. When all are NULL, the terminal is in - // offline/disconnected mode (useful for viewing .ANS files). - void *commCtx; - int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); - int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); -} AnsiTermDataT; - -// ListViewDataT -- multi-column list with sortable headers and optional -// multi-select. cellData is a flat array of strings indexed as -// cellData[row * colCount + col]. The sortIndex is an indirection array -// that maps displayed row numbers to data row numbers, allowing sort -// without rearranging the actual data. resolvedColW[] caches pixel widths -// after resolving tagged column sizes, avoiding re-resolution on every paint. -typedef struct { - const ListViewColT *cols; - int32_t colCount; - const char **cellData; - int32_t rowCount; - int32_t selectedIdx; - int32_t scrollPos; - int32_t scrollPosH; - int32_t sortCol; - ListViewSortE sortDir; - int32_t resolvedColW[LISTVIEW_MAX_COLS]; - int32_t totalColW; - int32_t *sortIndex; - bool multiSelect; - int32_t anchorIdx; - uint8_t *selBits; - bool reorderable; - int32_t dragIdx; - int32_t dropIdx; - void (*onHeaderClick)(struct WidgetT *w, int32_t col, ListViewSortE dir); -} ListViewDataT; +// These types are referenced by application code through the wgtApi +// function table. Widget-private types (data structs) are defined +// in their respective widget .c files. // ============================================================ // Widget class vtable @@ -294,23 +126,79 @@ typedef struct { // PAINTS_CHILDREN -- widget handles child rendering itself // NO_HIT_RECURSE -- hit testing stops here, no child recursion -#define WCLASS_FOCUSABLE 0x0001 -#define WCLASS_BOX_CONTAINER 0x0002 -#define WCLASS_HORIZ_CONTAINER 0x0004 -#define WCLASS_PAINTS_CHILDREN 0x0008 -#define WCLASS_NO_HIT_RECURSE 0x0010 +// Flags encode static properties checked by the framework. +// Widgets set these in their WidgetClassT definition. +#define WCLASS_FOCUSABLE 0x00000001 +#define WCLASS_BOX_CONTAINER 0x00000002 +#define WCLASS_HORIZ_CONTAINER 0x00000004 +#define WCLASS_PAINTS_CHILDREN 0x00000008 +#define WCLASS_NO_HIT_RECURSE 0x00000010 +#define WCLASS_FOCUS_FORWARD 0x00000020 // accel hit forwards focus to next focusable +#define WCLASS_HAS_POPUP 0x00000040 // has dropdown popup overlay +#define WCLASS_SCROLLABLE 0x00000080 // accepts mouse wheel events +#define WCLASS_SCROLL_CONTAINER 0x00000100 // scroll container (ScrollPane) +#define WCLASS_NEEDS_POLL 0x00000200 // needs periodic polling (AnsiTerm comms) +#define WCLASS_SWALLOWS_TAB 0x00000400 // Tab key goes to widget, not focus nav +#define WCLASS_RELAYOUT_ON_SCROLL 0x00000800 // full relayout on scrollbar drag +#define WCLASS_PRESS_RELEASE 0x00001000 // click = press+release (Button, ImageButton) +#define WCLASS_ACCEL_WHEN_HIDDEN 0x00002000 // accel matching works even when invisible typedef struct WidgetClassT { uint32_t flags; + + // Rendering void (*paint)(struct WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); void (*paintOverlay)(struct WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); + + // Layout void (*calcMinSize)(struct WidgetT *w, const BitmapFontT *font); void (*layout)(struct WidgetT *w, const BitmapFontT *font); + void (*getLayoutMetrics)(const struct WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW); + + // Input void (*onMouse)(struct WidgetT *w, struct WidgetT *root, int32_t vx, int32_t vy); void (*onKey)(struct WidgetT *w, int32_t key, int32_t mod); + void (*onAccelActivate)(struct WidgetT *w, struct WidgetT *root); + + // Lifecycle void (*destroy)(struct WidgetT *w); + void (*onChildChanged)(struct WidgetT *parent, struct WidgetT *child); + + // Text const char *(*getText)(const struct WidgetT *w); void (*setText)(struct WidgetT *w, const char *text); + + // Selection / drag-select + bool (*clearSelection)(struct WidgetT *w); + void (*dragSelect)(struct WidgetT *w, struct WidgetT *root, int32_t vx, int32_t vy); + + // Popup (dropdown/combobox) + void (*openPopup)(struct WidgetT *w); + void (*closePopup)(struct WidgetT *w); + int32_t (*getPopupItemCount)(const struct WidgetT *w); + void (*getPopupRect)(const struct WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); + + // Button press state + void (*setPressed)(struct WidgetT *w, bool pressed); + + // Drag reorder (listbox/listview/treeview) + void (*reorderDrop)(struct WidgetT *w); + void (*reorderUpdate)(struct WidgetT *w, struct WidgetT *root, int32_t x, int32_t y); + + // Cursor shape (returns cursor ID, 0 = default) + int32_t (*getCursorShape)(const struct WidgetT *w, int32_t vx, int32_t vy); + + // Polling (AnsiTerm comms) + void (*poll)(struct WidgetT *w, WindowT *win); + + // Fast incremental repaint (returns dirty row count, 0 = none) + int32_t (*quickRepaint)(struct WidgetT *w, int32_t *outY, int32_t *outH); + + // Text field width (for scroll calculations; 0 = use widget width) + int32_t (*getTextFieldWidth)(const struct WidgetT *w); + + // Scrollbar drag update (orient: 0=vert, 1=horiz) + void (*scrollDragUpdate)(struct WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); } WidgetClassT; // ============================================================ @@ -320,17 +208,13 @@ typedef struct WidgetClassT { #define MAX_WIDGET_NAME 32 typedef struct WidgetT { - WidgetTypeE type; + int32_t type; // assigned by wgtRegisterClass() // wclass points to the vtable for this widget type. Looked up once at // creation from widgetClassTable[type]. This avoids a switch on type // in every paint/layout/event dispatch -- the cost is one pointer per // widget, which is negligible. const struct WidgetClassT *wclass; char name[MAX_WIDGET_NAME]; - // djb2 hash of the name string, computed at wgtSetName() time. - // wgtFind() compares hashes before strcmp, making name lookups fast - // for the common no-match case in a tree with many unnamed widgets. - uint32_t nameHash; // djb2 hash of name, 0 if unnamed // Tree linkage struct WidgetT *parent; @@ -389,7 +273,7 @@ typedef struct WidgetT { // Type-specific handlers (e.g. button press animation, listbox // selection) run first, then these universal callbacks fire. void *userData; - void *customData; // DXE widget plugin private data (NULL for built-in types) + void *data; // widget-private data (allocated by widget DXE) const char *tooltip; // tooltip text (NULL = none, caller owns string) MenuT *contextMenu; // right-click context menu (NULL = none, caller owns) void (*onClick)(struct WidgetT *w); @@ -398,249 +282,6 @@ typedef struct WidgetT { void (*onFocus)(struct WidgetT *w); void (*onBlur)(struct WidgetT *w); - // Type-specific data -- tagged union keyed by the `type` field. - // Only the member corresponding to `type` is valid. This is the C - // equivalent of a discriminated union / variant type. Using a union - // instead of separate structs per widget type keeps all widget data - // in a single allocation, which simplifies memory management and - // avoids pointer chasing during layout/paint traversal. - union { - struct { - const char *text; - WidgetAlignE textAlign; // AlignStartE=left, AlignCenterE, AlignEndE=right - } label; - - struct { - const char *text; - bool pressed; - } button; - - struct { - const char *text; - bool checked; - } checkbox; - - struct { - int32_t selectedIdx; - } radioGroup; - - struct { - const char *text; - int32_t index; - } radio; - - // Text input has its own edit buffer (not a pointer to external - // storage) so the widget fully owns its text lifecycle. The undo - // buffer holds a single-level snapshot taken before each edit - // operation -- Ctrl+Z restores to the snapshot. This is simpler - // than a full undo stack but sufficient for single-line fields. - struct { - char *buf; - int32_t bufSize; - int32_t len; - int32_t cursorPos; - int32_t scrollOff; - int32_t selStart; // selection anchor (-1 = none) - int32_t selEnd; // selection end (-1 = none) - char *undoBuf; - int32_t undoLen; - int32_t undoCursor; - InputModeE inputMode; - const char *mask; // format mask for InputMaskedE - } textInput; - - // Multi-line text editor. desiredCol implements the "sticky column" - // behavior where pressing Up/Down tries to return to the column - // the cursor was at before traversing shorter lines (standard - // text editor UX). cachedLines/cachedMaxLL cache values that - // require full-buffer scans, invalidated to -1 on any text change. - struct { - char *buf; - int32_t bufSize; - int32_t len; - int32_t cursorRow; - int32_t cursorCol; - int32_t scrollRow; - int32_t scrollCol; - int32_t desiredCol; // sticky column for up/down movement - int32_t selAnchor; // selection anchor byte offset (-1 = none) - int32_t selCursor; // selection cursor byte offset (-1 = none) - char *undoBuf; - int32_t undoLen; - int32_t undoCursor; // byte offset at time of snapshot - int32_t cachedLines; // cached line count (-1 = dirty) - int32_t cachedMaxLL; // cached max line length (-1 = dirty) - } textArea; - - struct { - const char **items; - int32_t itemCount; - int32_t selectedIdx; // cursor position (always valid); also sole selection in single-select - int32_t scrollPos; - int32_t maxItemLen; // cached max strlen of items - bool multiSelect; - int32_t anchorIdx; // anchor for shift+click range selection - uint8_t *selBits; // per-item selection flags (multi-select only) - bool reorderable; // allow drag-reorder of items - int32_t dragIdx; // item being dragged (-1 = none) - int32_t dropIdx; // insertion point (-1 = none) - } listBox; - - struct { - bool vertical; - } separator; - - struct { - const char *title; - FrameStyleE style; // FrameInE (default), FrameOutE, FrameFlatE - uint32_t color; // border color for FrameFlatE (0 = use windowShadow) - } frame; - - struct { - const char **items; - int32_t itemCount; - int32_t selectedIdx; - bool open; - int32_t hoverIdx; - int32_t scrollPos; - int32_t maxItemLen; // cached max strlen of items - } dropdown; - - struct { - char *buf; - int32_t bufSize; - int32_t len; - int32_t cursorPos; - int32_t scrollOff; - int32_t selStart; // selection anchor (-1 = none) - int32_t selEnd; // selection end (-1 = none) - char *undoBuf; - int32_t undoLen; - int32_t undoCursor; - const char **items; - int32_t itemCount; - int32_t selectedIdx; - bool open; - int32_t hoverIdx; - int32_t listScrollPos; - int32_t maxItemLen; // cached max strlen of items - } comboBox; - - struct { - int32_t value; - int32_t maxValue; - bool vertical; - } progressBar; - - struct { - int32_t value; - int32_t minValue; - int32_t maxValue; - bool vertical; - } slider; - - struct { - int32_t activeTab; - int32_t scrollOffset; // horizontal scroll of tab headers - } tabControl; - - struct { - const char *title; - } tabPage; - - // TreeView uses widget children (WidgetTreeItemE) as its items, - // unlike ListBox which uses a string array. This allows nested - // hierarchies with expand/collapse state per item. The tree is - // rendered by flattening visible items during paint, with TREE_INDENT - // pixels of indentation per nesting level. - struct { - int32_t scrollPos; - int32_t scrollPosH; - struct WidgetT *selectedItem; - struct WidgetT *anchorItem; // anchor for shift+click range selection - bool multiSelect; - bool reorderable; // allow drag-reorder of items - struct WidgetT *dragItem; // item being dragged (NULL = none) - struct WidgetT *dropTarget; // insertion target (NULL = none) - bool dropAfter; // true = insert after target, false = before - int32_t cachedTotalHeight; // cached result of calcTreeItemsHeight - int32_t cachedMaxWidth; // cached result of calcTreeItemsMaxWidth - bool dimsValid; // false = recalculate on next scrollbar check - } treeView; - - struct { - const char *text; - bool expanded; - bool selected; // per-item flag for multi-select - } treeItem; - - struct { - uint8_t *data; // pixel buffer in display format - int32_t imgW; - int32_t imgH; - int32_t imgPitch; - bool pressed; - } image; - - struct { - uint8_t *data; // pixel buffer in display format - int32_t imgW; - int32_t imgH; - int32_t imgPitch; - bool pressed; - } imageButton; - - struct { - uint8_t *data; // pixel buffer in display format - int32_t canvasW; - int32_t canvasH; - int32_t canvasPitch; - int32_t canvasBpp; // cached bytes per pixel (avoids pitch/w division) - uint32_t penColor; - int32_t penSize; - int32_t lastX; - int32_t lastY; - void (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag); - } canvas; - - AnsiTermDataT *ansiTerm; - - ListViewDataT *listView; - - struct { - int32_t value; - int32_t minValue; - int32_t maxValue; - int32_t step; - char buf[16]; // formatted value text - int32_t len; - int32_t cursorPos; - int32_t scrollOff; - int32_t selStart; // selection anchor (-1 = none) - int32_t selEnd; // selection end (-1 = none) - char undoBuf[16]; // undo snapshot - int32_t undoLen; - int32_t undoCursor; - bool editing; // true when user is typing - } spinner; - - struct { - int32_t scrollPosV; - int32_t scrollPosH; - } scrollPane; - - struct { - int32_t dividerPos; // pixels from left/top edge - bool vertical; // true = vertical divider (left|right panes) - } splitter; - - struct { - int32_t intervalMs; // timer period in milliseconds - bool repeat; // true = repeating, false = one-shot - bool running; // true = active - clock_t lastFire; // clock() value at last fire (or start) - } timer; - } as; } WidgetT; // ============================================================ @@ -655,299 +296,6 @@ typedef struct WidgetT { // root container. The window's userData is set to the AppContextT pointer. WidgetT *wgtInitWindow(struct AppContextT *ctx, WindowT *win); -// ============================================================ -// Container creation -// ============================================================ -// -// VBox and HBox are the primary layout containers, analogous to CSS -// flexbox with column/row direction. Children are laid out sequentially -// along the main axis (vertical for VBox, horizontal for HBox) with -// spacing between them. Extra space is distributed according to each -// child's weight. Frame is a titled groupbox container with a bevel border. - -WidgetT *wgtVBox(WidgetT *parent); -WidgetT *wgtHBox(WidgetT *parent); -WidgetT *wgtFrame(WidgetT *parent, const char *title); - -// ============================================================ -// Basic widgets -// ============================================================ - -WidgetT *wgtLabel(WidgetT *parent, const char *text); -void wgtLabelSetAlign(WidgetT *w, WidgetAlignE align); -WidgetT *wgtButton(WidgetT *parent, const char *text); -WidgetT *wgtCheckbox(WidgetT *parent, const char *text); -WidgetT *wgtTextInput(WidgetT *parent, int32_t maxLen); -WidgetT *wgtPasswordInput(WidgetT *parent, int32_t maxLen); -WidgetT *wgtMaskedInput(WidgetT *parent, const char *mask); - -// ============================================================ -// Radio buttons -// ============================================================ -// -// Radio buttons must be children of a RadioGroup container. The group -// tracks which child is selected (by index). Clicking a radio button -// automatically deselects the previously selected sibling. This parent- -// tracking approach avoids explicit "radio group ID" parameters. - -WidgetT *wgtRadioGroup(WidgetT *parent); -WidgetT *wgtRadio(WidgetT *parent, const char *text); - -// ============================================================ -// Spacing and visual dividers -// ============================================================ -// -// Spacer is an invisible flexible widget (weight=100) that absorbs -// extra space -- useful for pushing subsequent siblings to the end of -// a container (like CSS flex: 1 auto). Separators are thin beveled -// lines for visual grouping. - -WidgetT *wgtSpacer(WidgetT *parent); -WidgetT *wgtHSeparator(WidgetT *parent); -WidgetT *wgtVSeparator(WidgetT *parent); - -// ============================================================ -// Complex widgets -// ============================================================ - -WidgetT *wgtListBox(WidgetT *parent); -WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); - -// ============================================================ -// Dropdown and ComboBox -// ============================================================ - -WidgetT *wgtDropdown(WidgetT *parent); -void wgtDropdownSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtDropdownGetSelected(const WidgetT *w); -void wgtDropdownSetSelected(WidgetT *w, int32_t idx); - -WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen); -void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtComboBoxGetSelected(const WidgetT *w); -void wgtComboBoxSetSelected(WidgetT *w, int32_t idx); - -// ============================================================ -// ProgressBar -// ============================================================ - -WidgetT *wgtProgressBar(WidgetT *parent); -WidgetT *wgtProgressBarV(WidgetT *parent); -void wgtProgressBarSetValue(WidgetT *w, int32_t value); -int32_t wgtProgressBarGetValue(const WidgetT *w); - -// ============================================================ -// Slider (TrackBar) -// ============================================================ - -WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal); -void wgtSliderSetValue(WidgetT *w, int32_t value); -int32_t wgtSliderGetValue(const WidgetT *w); - -// ============================================================ -// Spinner (numeric input) -// ============================================================ - -WidgetT *wgtSpinner(WidgetT *parent, int32_t minVal, int32_t maxVal, int32_t step); -void wgtSpinnerSetValue(WidgetT *w, int32_t value); -int32_t wgtSpinnerGetValue(const WidgetT *w); -void wgtSpinnerSetRange(WidgetT *w, int32_t minVal, int32_t maxVal); -void wgtSpinnerSetStep(WidgetT *w, int32_t step); - -// ============================================================ -// TabControl -// ============================================================ - -WidgetT *wgtTabControl(WidgetT *parent); -WidgetT *wgtTabPage(WidgetT *parent, const char *title); -void wgtTabControlSetActive(WidgetT *w, int32_t idx); -int32_t wgtTabControlGetActive(const WidgetT *w); - -// ============================================================ -// StatusBar and Toolbar -// ============================================================ - -WidgetT *wgtStatusBar(WidgetT *parent); -WidgetT *wgtToolbar(WidgetT *parent); - -// ============================================================ -// TreeView -// ============================================================ - -WidgetT *wgtTreeView(WidgetT *parent); -WidgetT *wgtTreeViewGetSelected(const WidgetT *w); -void wgtTreeViewSetSelected(WidgetT *w, WidgetT *item); -void wgtTreeViewSetMultiSelect(WidgetT *w, bool multi); -void wgtTreeViewSetReorderable(WidgetT *w, bool reorderable); -WidgetT *wgtTreeItem(WidgetT *parent, const char *text); -void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); -bool wgtTreeItemIsExpanded(const WidgetT *w); -bool wgtTreeItemIsSelected(const WidgetT *w); -void wgtTreeItemSetSelected(WidgetT *w, bool selected); - -// ============================================================ -// ListView (multi-column list) -// ============================================================ - -WidgetT *wgtListView(WidgetT *parent); -void wgtListViewSetColumns(WidgetT *w, const ListViewColT *cols, int32_t count); -void wgtListViewSetData(WidgetT *w, const char **cellData, int32_t rowCount); -int32_t wgtListViewGetSelected(const WidgetT *w); -void wgtListViewSetSelected(WidgetT *w, int32_t idx); -void wgtListViewSetSort(WidgetT *w, int32_t col, ListViewSortE dir); -void wgtListViewSetHeaderClickCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t col, ListViewSortE dir)); -void wgtListViewSetMultiSelect(WidgetT *w, bool multi); -bool wgtListViewIsItemSelected(const WidgetT *w, int32_t idx); -void wgtListViewSetItemSelected(WidgetT *w, int32_t idx, bool selected); -void wgtListViewSelectAll(WidgetT *w); -void wgtListViewClearSelection(WidgetT *w); -void wgtListViewSetReorderable(WidgetT *w, bool reorderable); - -// ============================================================ -// ScrollPane -// ============================================================ - -WidgetT *wgtScrollPane(WidgetT *parent); - -// ============================================================ -// Splitter (draggable divider between two child regions) -// ============================================================ - -// Create a splitter. If vertical is true, children are arranged left|right; -// if false, top|bottom. Add exactly two children. -WidgetT *wgtSplitter(WidgetT *parent, bool vertical); -void wgtSplitterSetPos(WidgetT *w, int32_t pos); -int32_t wgtSplitterGetPos(const WidgetT *w); - -// ============================================================ -// Timer (invisible, callback-driven) -// ============================================================ - -// Create a timer widget. The timer is invisible and takes no space in -// layout. When running, it calls onChange at the specified interval. -// intervalMs: period in milliseconds. repeat: true for repeating, -// false for one-shot (auto-stops after first fire). -WidgetT *wgtTimer(WidgetT *parent, int32_t intervalMs, bool repeat); - -// Start/restart the timer. Resets the elapsed time. -void wgtTimerStart(WidgetT *w); - -// Stop the timer. -void wgtTimerStop(WidgetT *w); - -// Change the interval (takes effect on next fire). -void wgtTimerSetInterval(WidgetT *w, int32_t intervalMs); - -// Check if the timer is currently running. -bool wgtTimerIsRunning(const WidgetT *w); - -// Called once per frame by dvxUpdate. Checks all active timers. -void wgtUpdateTimers(void); - -// ============================================================ -// ImageButton -// ============================================================ - -// Create an image button from raw pixel data (display format). -// Takes ownership of the data buffer (freed on destroy). -WidgetT *wgtImageButton(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); - -// Load an image button from a file (BMP, PNG, JPEG, GIF). -// Returns NULL on load failure; falls through gracefully. -WidgetT *wgtImageButtonFromFile(WidgetT *parent, const char *path); - -// Replace the image data. Takes ownership of the new buffer. -void wgtImageButtonSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); - -// ============================================================ -// Image -// ============================================================ - -// Create an image widget from raw pixel data (display format). -// Takes ownership of the data buffer (freed on destroy). -WidgetT *wgtImage(WidgetT *parent, uint8_t *data, int32_t w, int32_t h, int32_t pitch); - -// Load an image widget from a file (BMP, PNG, JPEG, GIF). -// Returns NULL on load failure. -WidgetT *wgtImageFromFile(WidgetT *parent, const char *path); - -// Replace the image data. Takes ownership of the new buffer. -void wgtImageSetData(WidgetT *w, uint8_t *data, int32_t imgW, int32_t imgH, int32_t pitch); - -// ============================================================ -// Canvas -// ============================================================ - -// Create a drawable canvas widget with the given dimensions. -WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h); - -// Clear the canvas to the specified color. -void wgtCanvasClear(WidgetT *w, uint32_t color); - -// Set the pen color (in display pixel format). -void wgtCanvasSetPenColor(WidgetT *w, uint32_t color); - -// Set the pen size in pixels (diameter). -void wgtCanvasSetPenSize(WidgetT *w, int32_t size); - -// Set the mouse callback. Called on click (drag=false) and drag (drag=true) -// with canvas-relative coordinates. If NULL (default), mouse events are ignored. -void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)); - -// Save the canvas to a PNG file. Returns 0 on success, -1 on failure. -int32_t wgtCanvasSave(WidgetT *w, const char *path); - -// Load a PNG file onto the canvas. Returns 0 on success, -1 on failure. -int32_t wgtCanvasLoad(WidgetT *w, const char *path); - -// Programmatic drawing (coordinates are in canvas space) -void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); -void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); -void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); -void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); -void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color); -uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y); - -// ============================================================ -// ANSI Terminal -// ============================================================ -// -// A VT100/ANSI terminal emulator widget. Supports the subset of escape -// sequences needed for DOS BBS programs and ANSI art: cursor positioning, -// SGR color/attribute codes, scrolling regions, and text blink. Pairs -// with the secLink communications layer for remote serial connections -// or can be used standalone for viewing .ANS files. - -// Create an ANSI terminal widget (0 for cols/rows = 80x25 default) -WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows); - -// Write raw data through the ANSI escape sequence parser. Used for -// loading .ANS files or feeding data from a source that isn't using -// the comm interface. -void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); - -// Clear the terminal screen and reset cursor to home -void wgtAnsiTermClear(WidgetT *w); - -// Set the communications interface (NULL function pointers = disconnected) -void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)); - -// Set the scrollback buffer size in lines (default 500). Clears existing scrollback. -void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines); - -// Poll the comm interface for incoming data and process it. Returns bytes processed. -int32_t wgtAnsiTermPoll(WidgetT *w); - -// Fast-path repaint for the terminal widget. Instead of going through the -// full widget paint pipeline (which would repaint the entire widget), this -// renders only the dirty rows (tracked via the dirtyRows bitmask) directly -// into the window's content buffer. This is essential for responsive terminal -// output -- incoming serial data can dirty a few rows per frame, and -// repainting only those rows keeps the cost proportional to the actual -// change rather than the full 80x25 grid. Returns the number of rows -// repainted; outY/outH report the affected region for dirty-rect tracking. -int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH); - // ============================================================ // Operations // ============================================================ @@ -958,11 +306,6 @@ int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH); // function call. struct AppContextT *wgtGetContext(const WidgetT *w); -// Update text cursor blink state. Call once per frame from dvxUpdate. -// Toggles the cursor visibility at 250ms intervals, matching the ANSI -// terminal cursor rate. -void wgtUpdateCursorBlink(void); - // Mark a widget as needing both re-layout (measure + position) and // repaint. Propagates upward to ancestors since a child's size change // can affect parent layout. Use this after structural changes (adding/ @@ -1000,20 +343,6 @@ WidgetT *wgtFind(WidgetT *root, const char *name); // Destroy a widget and all its children (removes from parent) void wgtDestroy(WidgetT *w); -// ============================================================ -// List box operations -// ============================================================ - -void wgtListBoxSetItems(WidgetT *w, const char **items, int32_t count); -int32_t wgtListBoxGetSelected(const WidgetT *w); -void wgtListBoxSetSelected(WidgetT *w, int32_t idx); -void wgtListBoxSetMultiSelect(WidgetT *w, bool multi); -bool wgtListBoxIsItemSelected(const WidgetT *w, int32_t idx); -void wgtListBoxSetItemSelected(WidgetT *w, int32_t idx, bool selected); -void wgtListBoxSelectAll(WidgetT *w); -void wgtListBoxClearSelection(WidgetT *w); -void wgtListBoxSetReorderable(WidgetT *w, bool reorderable); - // ============================================================ // Tooltip // ============================================================ @@ -1034,7 +363,7 @@ void wgtSetDebugLayout(struct AppContextT *ctx, bool enabled); // ============================================================ // Register a new widget class at runtime. Returns the assigned type ID -// (>= WGT_TYPE_DYNAMIC_BASE) on success, or -1 if the table is full. +// Appends wclass to widgetClassTable and returns the assigned type ID. // The WidgetClassT must remain valid for the lifetime of the process // (typically a static const in the registering DXE). int32_t wgtRegisterClass(const WidgetClassT *wclass); @@ -1063,4 +392,20 @@ void wgtLayout(WidgetT *root, int32_t availW, int32_t availH, const BitmapFontT // after the main tree so they render on top of everything. void wgtPaint(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +// ============================================================ +// Widget API registry +// ============================================================ +// +// Each widget DXE registers a small API struct under a name +// during wgtRegister(). Callers retrieve it via wgtGetApi() +// and cast to the widget-specific API type. Per-widget headers +// (e.g. widgetButton.h) provide typed accessors and convenience +// macros so callers don't need manual casts. +// +// This replaces the monolithic WidgetApiT -- adding a new widget +// requires zero changes to dvxWidget.h. + +void wgtRegisterApi(const char *name, const void *api); +const void *wgtGetApi(const char *name); + #endif // DVX_WIDGET_H diff --git a/core/dvxWidgetPlugin.h b/core/dvxWidgetPlugin.h new file mode 100644 index 00000000..eb703dc7 --- /dev/null +++ b/core/dvxWidgetPlugin.h @@ -0,0 +1,213 @@ +// dvxWidgetPlugin.h -- Plugin API for DVX widget DXE modules +// +// Included by widget .c files to access core infrastructure. +// Provides: widget allocation, tree ops, class table, shared +// interaction state, scrollbar helpers, layout helpers, and +// common utility functions. +// +// Does NOT contain any per-widget prototypes. Each widget's +// internal functions (paint, onMouse, onKey, calcMinSize, etc.) +// are private to its own .c file and referenced only through +// the WidgetClassT vtable. +#ifndef DVX_WIDGET_PLUGIN_H +#define DVX_WIDGET_PLUGIN_H + +#include "dvxWidget.h" +#include "dvxApp.h" +#include "dvxDraw.h" +#include "dvxWm.h" +#include "dvxVideo.h" + +#include +#include +#include + +// ============================================================ +// Widget class table +// ============================================================ + +// stb_ds dynamic array. Each widget DXE appends its class(es) +// via wgtRegisterClass() during wgtRegister(). +extern const WidgetClassT **widgetClassTable; + +// ============================================================ +// Validation macros +// ============================================================ + +#define VALIDATE_WIDGET(w, wtype, retval) \ + do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0) + +#define VALIDATE_WIDGET_VOID(w, wtype) \ + do { if (!(w) || (w)->type != (wtype)) { return; } } while (0) + +// ============================================================ +// Cursor shape IDs (mirrors dvxCursor.h defines) +// ============================================================ + +#define CURSOR_ARROW 0 +#define CURSOR_RESIZE_H 1 +#define CURSOR_RESIZE_V 2 +#define CURSOR_RESIZE_DIAG_NWSE 3 +#define CURSOR_RESIZE_DIAG_NESW 4 + +// ============================================================ +// Keyboard modifier flags +// ============================================================ + +#define KEY_MOD_SHIFT 0x03 +#define KEY_MOD_CTRL 0x04 +#define KEY_MOD_ALT 0x08 + +// ============================================================ +// Core layout constants +// ============================================================ + +#define DEFAULT_SPACING 4 +#define DEFAULT_PADDING 4 +#define SB_MIN_THUMB 14 +#define WGT_SB_W 14 + +// ============================================================ +// Inline helpers +// ============================================================ + +static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) { + if (val < lo) { return lo; } + if (val > hi) { return hi; } + return val; +} + + +static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { + drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); + drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false); +} + + +static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { + drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); + drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false); +} + +// ============================================================ +// Shared interaction state (defined in widgetCore.c) +// ============================================================ + +extern bool sCursorBlinkOn; +extern clock_t sDblClickTicks; +extern bool sDebugLayout; +extern WidgetT *sClosedPopup; +extern WidgetT *sFocusedWidget; +extern WidgetT *sKeyPressedBtn; +extern WidgetT *sOpenPopup; +extern WidgetT *sPressedButton; +extern WidgetT *sDragSlider; +extern WidgetT *sDrawingCanvas; +extern WidgetT *sDragTextSelect; +extern int32_t sDragOffset; +extern WidgetT *sResizeListView; +extern WidgetT *sDragSplitter; +extern int32_t sDragSplitStart; +extern WidgetT *sDragReorder; +extern WidgetT *sDragScrollbar; +extern int32_t sDragScrollbarOff; +extern int32_t sDragScrollbarOrient; + +// ============================================================ +// Core widget functions (widgetCore.c) +// ============================================================ + +// Tree manipulation +void widgetAddChild(WidgetT *parent, WidgetT *child); +void widgetRemoveChild(WidgetT *parent, WidgetT *child); +void widgetDestroyChildren(WidgetT *w); + +// Allocation +WidgetT *widgetAlloc(WidgetT *parent, int32_t type); + +// Focus management +void widgetClearFocus(WidgetT *root); +WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); +WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); +WidgetT *widgetFindByAccel(WidgetT *root, char key); + +// Utility queries +int32_t widgetCountVisibleChildren(const WidgetT *w); +int32_t widgetFrameBorderWidth(const WidgetT *w); +bool widgetIsFocusable(int32_t type); +bool widgetIsBoxContainer(int32_t type); +bool widgetIsHorizContainer(int32_t type); +int32_t multiClickDetect(int32_t vx, int32_t vy); + +// Clipboard +void clipboardCopy(const char *text, int32_t len); +const char *clipboardGet(int32_t *outLen); +int32_t clipboardMaxLen(void); + +// Hit testing +WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); + +// Scrollbar helpers +void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); + +// ============================================================ +// Scrollbar drawing and hit testing (widgetScrollbar.c) +// ============================================================ + +typedef enum { + ScrollHitNoneE, + ScrollHitArrowDecE, + ScrollHitArrowIncE, + ScrollHitPageDecE, + ScrollHitPageIncE, + ScrollHitThumbE +} ScrollHitE; + +void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); +void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); +void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); +ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); + +// ============================================================ +// Layout functions (widgetLayout.c) +// ============================================================ + +void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font); +void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font); +void widgetLayoutBox(WidgetT *w, const BitmapFontT *font); +void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font); + +// ============================================================ +// Event dispatch (widgetEvent.c) +// ============================================================ + +void widgetManageScrollbars(WindowT *win, AppContextT *ctx); +void widgetOnKey(WindowT *win, int32_t key, int32_t mod); +void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); +void widgetOnPaint(WindowT *win, RectT *dirtyArea); +void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); +void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value); + +// ============================================================ +// Paint helpers (widgetOps.c) +// ============================================================ + +void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); +void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); + +// Shared helper libraries: +// texthelp/textHelp.h -- clipboard, text editing, word boundaries +// listhelp/listHelp.h -- dropdown arrow, popup list, keyboard nav +// Widget DXEs that use these include the headers directly. + +// widgetTextDragUpdate stays in core (widgetCore.c) +void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); + +// ============================================================ +// Drag-reorder helpers (widgetEvent.c) +// ============================================================ + +void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); +void widgetReorderDrop(WidgetT *w); + +#endif // DVX_WIDGET_PLUGIN_H diff --git a/core/dvxWm.c b/core/dvxWm.c index 5b2fc4fe..b045e6d4 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -144,9 +144,9 @@ static void computeMenuBarPositions(WindowT *win, const BitmapFontT *font) { // Compute title bar gadget positions. Used by both drawTitleBar() // and wmHitTest() to keep geometry in sync. // -// Layout follows the DESQview/X + GEOS Ensemble convention: +// Layout follows the Motif + GEOS Ensemble convention: // [Close] ---- Title Text ---- [Minimize] [Maximize] -// Close is on the far left (also doubles as system menu in DV/X style), +// Close is on the far left (also doubles as system menu), // minimize and maximize pack right-to-left from the far right edge. // Modal windows suppress the minimize gadget since they must stay visible. // Non-resizable windows suppress the maximize gadget. @@ -1410,8 +1410,8 @@ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT * // 2. A live thumbnail of the window's content buffer (also scaled) // 3. A grey fill if neither is available // -// The live thumbnail approach (option 2) is a DESQview/X homage -- DV/X -// showed miniature window contents in its icon/task view. The thumbnail +// The live thumbnail (option 2) shows miniature window contents in the +// icon/task view, giving a quick preview of each minimized window. The thumbnail // is rendered from the existing content buffer, so no extra rendering // pass is needed. contentDirty tracks whether the content has changed // since the last icon refresh. @@ -1441,30 +1441,30 @@ void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * continue; } - // Draw beveled border - BevelStyleT bevel; - bevel.highlight = colors->windowHighlight; - bevel.shadow = colors->windowShadow; - bevel.face = colors->windowFace; - bevel.width = ICON_BORDER; - drawBevel(d, ops, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, &bevel); - - // Draw icon content - int32_t contentX = ix + ICON_BORDER; - int32_t contentY = iy + ICON_BORDER; - int32_t bpp = d->format.bytesPerPixel; + int32_t bpp = d->format.bytesPerPixel; if (win->iconData && win->iconW > 0 && win->iconH > 0) { - // Draw loaded icon image, scaled to ICON_SIZE x ICON_SIZE - drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, + // Provided icon image -- draw edge-to-edge, no bevel + drawScaledRect(d, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, win->iconData, win->iconW, win->iconH, win->iconPitch, bpp); - } else if (win->contentBuf && win->contentW > 0 && win->contentH > 0) { - // Draw scaled copy of window contents - drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, - win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp); } else { - // No content -- draw grey fill - rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace); + // Live thumbnail or grey fill -- draw with beveled border + BevelStyleT bevel; + bevel.highlight = colors->windowHighlight; + bevel.shadow = colors->windowShadow; + bevel.face = colors->windowFace; + bevel.width = ICON_BORDER; + drawBevel(d, ops, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE, &bevel); + + int32_t contentX = ix + ICON_BORDER; + int32_t contentY = iy + ICON_BORDER; + + if (win->contentBuf && win->contentW > 0 && win->contentH > 0) { + drawScaledRect(d, contentX, contentY, ICON_SIZE, ICON_SIZE, + win->contentBuf, win->contentW, win->contentH, win->contentPitch, bpp); + } else { + rectFill(d, ops, contentX, contentY, ICON_SIZE, ICON_SIZE, colors->windowFace); + } } } } diff --git a/core/dvxWm.h b/core/dvxWm.h index 5b562760..89de17c9 100644 --- a/core/dvxWm.h +++ b/core/dvxWm.h @@ -111,7 +111,7 @@ void wmDrawScrollbars(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colo // Draw icons for all minimized windows along the bottom of the screen. // Each icon shows a scaled-down preview of the window's content (or a -// default icon) with a beveled border, similar to DESQview/X's icon bar. +// default icon) with a beveled border, similar to a classic desktop icon bar. void wmDrawMinimizedIcons(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, const WindowStackT *stack, const RectT *clipTo); // Determine which window and which part of that window is under the given diff --git a/core/platform/dvxPlatform.h b/core/platform/dvxPlatform.h index bac77ab6..38859929 100644 --- a/core/platform/dvxPlatform.h +++ b/core/platform/dvxPlatform.h @@ -24,6 +24,8 @@ #include "dvxTypes.h" +#include + // ============================================================ // Keyboard event // ============================================================ @@ -47,7 +49,7 @@ void platformInit(void); // Cooperative yield -- give up the CPU timeslice when the event loop has // nothing to do. On DOS this calls __dpmi_yield() to be friendly to -// multitaskers (Windows 3.x, OS/2, DESQview). On Linux this calls +// multitaskers (Windows 3.x, OS/2). On Linux this calls // SDL_Delay(1) to avoid busy-spinning at 100% CPU. void platformYield(void); @@ -241,4 +243,41 @@ int32_t platformStripLineEndings(char *buf, int32_t len); // export table. On platforms without DXE (Linux/SDL), this is a no-op. void platformRegisterDxeExports(void); +// ============================================================ +// Crash recovery +// ============================================================ + +// Log function signature for crash diagnostics. +typedef void (*PlatformLogFnT)(const char *fmt, ...); + +// Install signal handlers for fatal exceptions (SIGSEGV, SIGFPE, SIGILL). +// On crash, logs platform-specific diagnostics (register dump on DJGPP) +// via logFn, stores the signal number in *crashSignal, then longjmps to +// recoveryBuf. The caller is responsible for app-level diagnostics in +// the setjmp recovery block. +void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn); + +// Log platform-specific crash diagnostics (signal name and register dump +// on DJGPP). Called internally by the crash handler before longjmp; +// also available for manual invocation if needed. +void platformLogCrashDetail(int sig, PlatformLogFnT logFn); + +// ============================================================ +// DXE symbol overrides +// ============================================================ + +// A name/function-pointer pair for registering symbol overrides with +// the dynamic module loader. On DJGPP this maps directly to the +// dxe_symbol_table struct used by dlregsym(). +typedef struct { + const char *name; + void *func; +} PlatformSymOverrideT; + +// Register function pointer overrides for subsequently loaded modules. +// entries is an array of {name, funcPtr} pairs terminated by {NULL, NULL}. +// On DJGPP this wraps dlregsym(). On platforms without DXE, this is a +// no-op (RTLD_GLOBAL or equivalent handles symbol resolution). +void platformRegisterSymOverrides(const PlatformSymOverrideT *entries); + #endif // DVX_PLATFORM_H diff --git a/core/platform/dvxPlatformDos.c b/core/platform/dvxPlatformDos.c index 9373803d..86ba5dc6 100644 --- a/core/platform/dvxPlatformDos.c +++ b/core/platform/dvxPlatformDos.c @@ -51,6 +51,7 @@ #include #include #include +#include #include #include @@ -71,6 +72,7 @@ static int32_t findBestMode(int32_t requestedW, int32_t requestedH, int32_t pre static void getModeInfo(uint16_t mode, DisplayT *d, int32_t *score, int32_t requestedW, int32_t requestedH, int32_t preferredBpp); static bool hasCpuid(void); static int32_t mapLfb(DisplayT *d, uint32_t physAddr); +static void platformCrashHandler(int sig); void platformVideoEnumModes(void (*cb)(int32_t w, int32_t h, int32_t bpp, void *userData), void *userData); static int32_t setVesaMode(uint16_t mode); static void sysInfoAppend(const char *fmt, ...); @@ -1891,7 +1893,7 @@ void platformVideoShutdown(DisplayT *d) { // ============================================================ // // Cooperative yield to the DPMI host. In a multitasking DOS -// environment (Windows 3.x DOS box, OS/2 VDM, or DESQview), +// environment (Windows 3.x DOS box or OS/2 VDM), // this gives other tasks a chance to run. Under a single-tasking // DPMI server (CWSDPMI) it's essentially a no-op, but it doesn't // hurt. Called once per event loop iteration when idle. @@ -1926,6 +1928,103 @@ static int32_t setVesaMode(uint16_t mode) { } +// ============================================================ +// Crash recovery state +// ============================================================ + +static jmp_buf *sPlatformCrashJmp = NULL; +static volatile int *sPlatformCrashSignal = NULL; +static PlatformLogFnT sPlatformLogFn = NULL; + + +// ============================================================ +// platformCrashHandler -- signal handler for fatal exceptions +// ============================================================ +// +// DJGPP uses System V signal semantics where the handler is reset to +// SIG_DFL after each delivery, so we must re-install before doing +// anything else. After logging platform-specific diagnostics and +// storing the signal number, we longjmp back to the caller's recovery +// point (typically the shell's main loop). + +static void platformCrashHandler(int sig) { + if (sPlatformLogFn) { + platformLogCrashDetail(sig, sPlatformLogFn); + } + + signal(sig, platformCrashHandler); + + if (sPlatformCrashSignal) { + *sPlatformCrashSignal = sig; + } + + if (sPlatformCrashJmp) { + longjmp(*sPlatformCrashJmp, 1); + } +} + + +// ============================================================ +// platformInstallCrashHandler +// ============================================================ + +void platformInstallCrashHandler(jmp_buf *recoveryBuf, volatile int *crashSignal, PlatformLogFnT logFn) { + sPlatformCrashJmp = recoveryBuf; + sPlatformCrashSignal = crashSignal; + sPlatformLogFn = logFn; + + signal(SIGSEGV, platformCrashHandler); + signal(SIGFPE, platformCrashHandler); + signal(SIGILL, platformCrashHandler); +} + + +// ============================================================ +// platformLogCrashDetail -- log signal name and CPU register dump +// ============================================================ + +void platformLogCrashDetail(int sig, PlatformLogFnT logFn) { + const char *sigName = "UNKNOWN"; + + if (sig == SIGSEGV) { + sigName = "SIGSEGV (page fault)"; + } else if (sig == SIGFPE) { + sigName = "SIGFPE (floating point exception)"; + } else if (sig == SIGILL) { + sigName = "SIGILL (illegal instruction)"; + } + + logFn("=== CRASH ==="); + logFn("Signal: %d (%s)", sig, sigName); + + // __djgpp_exception_state_ptr captures the full CPU register state + // at the point of the exception. This gives us the faulting EIP, + // stack pointer, and all GPRs -- invaluable for post-mortem debugging. + jmp_buf *estate = __djgpp_exception_state_ptr; + + if (estate) { + struct __jmp_buf *regs = &(*estate)[0]; + logFn("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs); + logFn("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx); + logFn("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp); + logFn("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss); + logFn("EFLAGS: 0x%08lx", regs->__eflags); + } +} + + +// ============================================================ +// platformRegisterSymOverrides +// ============================================================ +// +// PlatformSymOverrideT has the same layout as dxe_symbol_table +// ({const char *name, void *ptr}), so we cast directly. + +void platformRegisterSymOverrides(const PlatformSymOverrideT *entries) { + dlregsym((const dxe_symbol_table *)entries); +} + + // ============================================================ // DXE3 export table // ============================================================ @@ -1952,8 +2051,8 @@ extern unsigned long long __udivdi3(unsigned long long, unsigned long long); extern unsigned long long __udivmoddi4(unsigned long long, unsigned long long, unsigned long long *); extern unsigned long long __umoddi3(unsigned long long, unsigned long long); -// DJGPP runtime internals -extern void *__djgpp_exception_state_ptr; +// DJGPP runtime internals (__djgpp_exception_state_ptr comes from +// included above, with proper jmp_buf* typing) extern void __dj_assert(const char *, const char *, int); extern unsigned short __dj_ctype_flags[]; extern unsigned char __dj_ctype_tolower[]; @@ -1961,6 +2060,22 @@ extern unsigned char __dj_ctype_tolower[]; // GCC emulated thread-local storage extern void *__emutls_get_address(void *); +// stb_ds internals (implementation compiled into dvx.exe via loaderMain.c) +extern void *stbds_arrgrowf(void *a, size_t elemsize, size_t addlen, size_t min_cap); +extern void stbds_arrfreef(void *a); +extern void stbds_hmfree_func(void *a, size_t elemsize); +extern void *stbds_hmget_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void *stbds_hmget_key_ts(void *a, size_t elemsize, void *key, size_t keysize, ptrdiff_t *temp, int mode); +extern void *stbds_hmput_default(void *a, size_t elemsize); +extern void *stbds_hmput_key(void *a, size_t elemsize, void *key, size_t keysize, int mode); +extern void *stbds_hmdel_key(void *a, size_t elemsize, void *key, size_t keysize, size_t keyoffset, int mode); +extern void *stbds_shmode_func(size_t elemsize, int mode); +extern size_t stbds_hash_string(char *str, size_t seed); +extern size_t stbds_hash_bytes(void *p, size_t len, size_t seed); +extern void stbds_rand_seed(size_t seed); +extern char *stbds_stralloc(void *a, char *str); +extern void stbds_strreset(void *a); + DXE_EXPORT_TABLE(sDxeExportTable) // --- platform --- @@ -1970,9 +2085,11 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformGetMemoryInfo) DXE_EXPORT(platformGetSystemInfo) DXE_EXPORT(platformInit) + DXE_EXPORT(platformInstallCrashHandler) DXE_EXPORT(platformKeyboardGetModifiers) DXE_EXPORT(platformKeyboardRead) DXE_EXPORT(platformLineEnding) + DXE_EXPORT(platformLogCrashDetail) DXE_EXPORT(platformMkdirRecursive) DXE_EXPORT(platformMouseInit) DXE_EXPORT(platformMousePoll) @@ -1982,6 +2099,7 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(platformMouseWheelPoll) DXE_EXPORT(platformPathDirEnd) DXE_EXPORT(platformRegisterDxeExports) + DXE_EXPORT(platformRegisterSymOverrides) DXE_EXPORT(platformSpanCopy8) DXE_EXPORT(platformSpanCopy16) DXE_EXPORT(platformSpanCopy32) @@ -2187,6 +2305,22 @@ DXE_EXPORT_TABLE(sDxeExportTable) DXE_EXPORT(dlerror) DXE_EXPORT(dlregsym) + // --- stb_ds (implementation lives in dvx.exe) --- + DXE_EXPORT(stbds_arrgrowf) + DXE_EXPORT(stbds_arrfreef) + DXE_EXPORT(stbds_hmfree_func) + DXE_EXPORT(stbds_hmget_key) + DXE_EXPORT(stbds_hmget_key_ts) + DXE_EXPORT(stbds_hmput_default) + DXE_EXPORT(stbds_hmput_key) + DXE_EXPORT(stbds_hmdel_key) + DXE_EXPORT(stbds_shmode_func) + DXE_EXPORT(stbds_hash_string) + DXE_EXPORT(stbds_hash_bytes) + DXE_EXPORT(stbds_rand_seed) + DXE_EXPORT(stbds_stralloc) + DXE_EXPORT(stbds_strreset) + DXE_EXPORT_END diff --git a/tasks/thirdparty/stb_ds.h b/core/thirdparty/stb_ds.h similarity index 100% rename from tasks/thirdparty/stb_ds.h rename to core/thirdparty/stb_ds.h diff --git a/core/widgetClass.c b/core/widgetClass.c index e923857f..31e31a22 100644 --- a/core/widgetClass.c +++ b/core/widgetClass.c @@ -1,49 +1,86 @@ -// widgetClass.c -- Widget class table and dynamic registration +#define DVX_WIDGET_IMPL +// widgetClass.c -- Widget class table, API registry, and dynamic registration // -// The widgetClassTable[] is the central dispatch table for the widget -// system. All entries start NULL and are filled at runtime by widget -// DXE registration functions (wgtBoxRegister, wgtButtonRegister, etc.). -// The loader loads each widget DXE and calls its register function -// before starting the shell. +// widgetClassTable is a stb_ds dynamic array. Widget DXEs call +// wgtRegisterClass() during wgtRegister() to append their class +// definition and receive a runtime type ID. No widget types are +// known at compile time. // -// This file contains only the table and the dynamic registration API. -// All class definitions live in their respective widget .c files. +// The API registry uses an stb_ds string hashmap for O(1) lookup. +// Each widget DXE calls wgtRegisterApi("name", &sApi) during +// wgtRegister(). App/core code calls wgtGetApi("name") to get +// the API pointer, then casts and calls through it. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "stb_ds.h" -// ============================================================ -// Class table -- indexed by WidgetTypeE -// ============================================================ -// -// Every entry is NULL until the corresponding widget DXE calls its -// registration function (e.g. wgtBoxRegister sets VBox/HBox/Frame). -// Sized WGT_MAX_TYPES to accommodate both built-in widget types -// (0..WGT_TYPE_BUILTIN_COUNT-1) and dynamically registered types -// (WGT_TYPE_DYNAMIC_BASE..WGT_MAX_TYPES-1). +#include -const WidgetClassT *widgetClassTable[WGT_MAX_TYPES]; +// stb_ds dynamic array of class pointers. Grows on each +// wgtRegisterClass() call. Index = type ID. +const WidgetClassT **widgetClassTable = NULL; + +// stb_ds string hashmap: key = widget name, value = API pointer +typedef struct { + char *key; // stb_ds string key (heap-allocated by shput) + const void *value; +} ApiMapEntryT; + +static ApiMapEntryT *sApiMap = NULL; // ============================================================ -// wgtRegisterClass -- register a dynamic widget type at runtime +// wgtGetApi // ============================================================ // -// External DXE plugins call this to add new widget types without -// modifying the core library. Returns the assigned type ID -// (>= WGT_TYPE_DYNAMIC_BASE) so the caller can use it with -// widgetAlloc(). Returns -1 if the table is full. +// Look up a widget API by name. O(1) via stb_ds string hashmap. +// Returns NULL if the widget is not loaded. Callers should still +// cache the result in a static local to skip even the hash. + +const void *wgtGetApi(const char *name) { + if (!name) { + return NULL; + } + + int32_t idx = shgeti(sApiMap, name); + + if (idx < 0) { + return NULL; + } + + return sApiMap[idx].value; +} + + +// ============================================================ +// wgtRegisterApi +// ============================================================ +// +// Register a widget's public API struct under a name. Called by +// each widget DXE during wgtRegister(). + +void wgtRegisterApi(const char *name, const void *api) { + if (!name || !api) { + return; + } + + shput(sApiMap, name, api); +} + + +// ============================================================ +// wgtRegisterClass +// ============================================================ +// +// Appends a class to the table and returns the assigned type ID. +// The ID is simply the array index. int32_t wgtRegisterClass(const WidgetClassT *wclass) { if (!wclass) { return -1; } - for (int32_t i = WGT_TYPE_DYNAMIC_BASE; i < WGT_MAX_TYPES; i++) { - if (!widgetClassTable[i]) { - widgetClassTable[i] = wclass; - return i; - } - } - - return -1; + int32_t id = arrlen(widgetClassTable); + arrput(widgetClassTable, wclass); + return id; } diff --git a/core/widgetCore.c b/core/widgetCore.c index 57a8a53c..80bf7027 100644 --- a/core/widgetCore.c +++ b/core/widgetCore.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetCore.c -- Core widget infrastructure (alloc, tree ops, helpers) // // This file provides the foundation for the widget tree: allocation, @@ -17,9 +18,9 @@ // created and destroyed at runtime (dialog dynamics, tree item insertion), // which doesn't map cleanly to an arena pattern. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "stb_ds.h" -#include #include // ============================================================ @@ -40,6 +41,7 @@ // otherwise dangling pointers would cause crashes. widgetDestroyChildren() // and wgtDestroy() handle this cleanup. +bool sCursorBlinkOn = true; // text cursor blink phase (toggled by wgtUpdateCursorBlink) clock_t sDblClickTicks = 0; // set from ctx->dblClickTicks during first paint bool sDebugLayout = false; WidgetT *sFocusedWidget = NULL; // currently focused widget (O(1) access, avoids tree walk) @@ -50,19 +52,86 @@ WidgetT *sDrawingCanvas = NULL; // canvas receiving paint strokes WidgetT *sDragTextSelect = NULL; // text widget in drag-select mode int32_t sDragOffset = 0; // pixel offset from drag start to thumb center WidgetT *sResizeListView = NULL; // ListView undergoing column resize -int32_t sResizeCol = -1; // which column is being resized -int32_t sResizeStartX = 0; // mouse X at resize start -int32_t sResizeOrigW = 0; // column width at resize start -bool sResizeDragging = false; // true once mouse moves during column resize WidgetT *sDragSplitter = NULL; // splitter being dragged int32_t sDragSplitStart = 0; // mouse offset from splitter edge at drag start WidgetT *sDragReorder = NULL; // list/tree widget in drag-reorder mode WidgetT *sDragScrollbar = NULL; // widget whose scrollbar thumb is being dragged int32_t sDragScrollbarOff = 0; // mouse offset within thumb at drag start int32_t sDragScrollbarOrient = 0; // 0=vertical, 1=horizontal -bool (*sListViewColBorderHitFn)(const WidgetT *w, int32_t vx, int32_t vy) = NULL; -void (*sSplitterClampPosFn)(WidgetT *w, int32_t *pos) = NULL; -WidgetT *(*sTreeViewNextVisibleFn)(WidgetT *item, WidgetT *treeView) = NULL; + +// Shared clipboard -- process-wide, not per-widget. +#define CLIPBOARD_MAX 4096 +static char sClipboard[CLIPBOARD_MAX]; +static int32_t sClipboardLen = 0; + +// Multi-click state (used by widgetEvent.c for universal dbl-click detection) +static clock_t sLastClickTime = 0; +static int32_t sLastClickX = -1; +static int32_t sLastClickY = -1; +static int32_t sClickCount = 0; + + +// ============================================================ +// clipboardCopy +// ============================================================ + +void clipboardCopy(const char *text, int32_t len) { + if (!text || len <= 0) { + return; + } + + if (len > CLIPBOARD_MAX - 1) { + len = CLIPBOARD_MAX - 1; + } + + memcpy(sClipboard, text, len); + sClipboard[len] = '\0'; + sClipboardLen = len; +} + + +// ============================================================ +// clipboardGet +// ============================================================ + +const char *clipboardGet(int32_t *outLen) { + if (outLen) { + *outLen = sClipboardLen; + } + + return sClipboard; +} + + +// ============================================================ +// clipboardMaxLen +// ============================================================ + +int32_t clipboardMaxLen(void) { + return CLIPBOARD_MAX - 1; +} + + +// ============================================================ +// multiClickDetect +// ============================================================ + +int32_t multiClickDetect(int32_t vx, int32_t vy) { + clock_t now = clock(); + + if ((now - sLastClickTime) < sDblClickTicks && + abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { + sClickCount++; + } else { + sClickCount = 1; + } + + sLastClickTime = now; + sLastClickX = vx; + sLastClickY = vy; + + return sClickCount; +} // ============================================================ @@ -104,8 +173,8 @@ void widgetAddChild(WidgetT *parent, WidgetT *child) { // widget in the tree can find its owning window without walking // to the root. -WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type) { - if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) { +WidgetT *widgetAlloc(WidgetT *parent, int32_t type) { + if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return NULL; } @@ -213,8 +282,6 @@ void widgetDestroyChildren(WidgetT *w) { if (sResizeListView == child) { sResizeListView = NULL; - sResizeCol = -1; - sResizeDragging = false; } if (sDragScrollbar == child) { @@ -230,73 +297,6 @@ void widgetDestroyChildren(WidgetT *w) { } -// ============================================================ -// widgetDropdownPopupRect -// ============================================================ -// -// Calculates the screen rectangle for a dropdown/combobox popup list. -// Shared between Dropdown and ComboBox since they have identical -// popup positioning logic. -// -// The popup tries to open below the widget first. If there isn't -// enough room (popup would extend past the content area bottom), -// it flips to open above instead. This ensures the popup is always -// visible, even for dropdowns near the bottom of a window. -// -// Popup height is capped at DROPDOWN_MAX_VISIBLE items to prevent -// huge popups from dominating the screen. - -void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { - int32_t itemCount = 0; - - if (w->type == WidgetDropdownE) { - itemCount = w->as.dropdown.itemCount; - } else if (w->type == WidgetComboBoxE) { - itemCount = w->as.comboBox.itemCount; - } - - int32_t visibleItems = itemCount; - - if (visibleItems > DROPDOWN_MAX_VISIBLE) { - visibleItems = DROPDOWN_MAX_VISIBLE; - } - - if (visibleItems < 1) { - visibleItems = 1; - } - - *popX = w->x; - *popW = w->w; - *popH = visibleItems * font->charHeight + 4; // 2px border each side - - // Try below first, then above if no room - if (w->y + w->h + *popH <= contentH) { - *popY = w->y + w->h; - } else { - *popY = w->y - *popH; - - if (*popY < 0) { - *popY = 0; - } - } -} - - -// ============================================================ -// widgetDrawDropdownArrow -// ============================================================ -// -// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels -// wide across 4 rows) centered at the given position. Used by both -// Dropdown and ComboBox for the drop button arrow glyph. - -void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) { - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color); - } -} - - // ============================================================ // widgetFindByAccel // ============================================================ @@ -315,10 +315,10 @@ WidgetT *widgetFindByAccel(WidgetT *root, char key) { return NULL; } - // Invisible tab pages: match the page itself (for tab switching) - // but don't recurse into children (their accels shouldn't be active) + // Widgets with WCLASS_ACCEL_WHEN_HIDDEN (e.g. tab pages) can match + // their accel even when invisible, but children are not searched. if (!root->visible) { - if (root->type == WidgetTabPageE && root->accelKey == key) { + if (root->wclass && (root->wclass->flags & WCLASS_ACCEL_WHEN_HIDDEN) && root->accelKey == key) { return root; } @@ -475,15 +475,18 @@ WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before) { // ============================================================ int32_t widgetFrameBorderWidth(const WidgetT *w) { - if (w->type != WidgetFrameE) { + if (!w->wclass || !w->wclass->getLayoutMetrics) { return 0; } - if (w->as.frame.style == FrameFlatE) { - return FRAME_FLAT_BORDER; - } + int32_t pad = 0; + int32_t gap = 0; + int32_t extraTop = 0; + int32_t borderW = 0; - return FRAME_BEVEL_BORDER; + w->wclass->getLayoutMetrics(w, NULL, &pad, &gap, &extraTop, &borderW); + + return borderW; } @@ -540,8 +543,8 @@ WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y) { // widgetIsFocusable // ============================================================ -bool widgetIsFocusable(WidgetTypeE type) { - if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) { +bool widgetIsFocusable(int32_t type) { + if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } @@ -555,8 +558,8 @@ bool widgetIsFocusable(WidgetTypeE type) { // // Returns true for widget types that use the generic box layout. -bool widgetIsBoxContainer(WidgetTypeE type) { - if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) { +bool widgetIsBoxContainer(int32_t type) { + if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } @@ -570,8 +573,8 @@ bool widgetIsBoxContainer(WidgetTypeE type) { // // Returns true for container types that lay out children horizontally. -bool widgetIsHorizContainer(WidgetTypeE type) { - if (type < 0 || type >= WGT_MAX_TYPES || !widgetClassTable[type]) { +bool widgetIsHorizContainer(int32_t type) { + if (type < 0 || type >= arrlen(widgetClassTable) || !widgetClassTable[type]) { return false; } @@ -579,127 +582,6 @@ bool widgetIsHorizContainer(WidgetTypeE type) { } -// ============================================================ -// widgetMaxItemLen -// ============================================================ -// -// Scans an array of string items and returns the maximum strlen. -// Shared by ListBox, Dropdown, and ComboBox to cache the widest -// item length for calcMinSize without duplicating the loop. - -int32_t widgetMaxItemLen(const char **items, int32_t count) { - int32_t maxLen = 0; - - for (int32_t i = 0; i < count; i++) { - int32_t slen = (int32_t)strlen(items[i]); - - if (slen > maxLen) { - maxLen = slen; - } - } - - return maxLen; -} - - -// ============================================================ -// widgetNavigateIndex -// ============================================================ -// -// Shared keyboard navigation for list-like widgets (ListBox, Dropdown, -// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic -// so each widget doesn't have to reimplement index clamping. -// -// Key values use the 0x100 flag to mark extended scan codes (arrow -// keys, Home, End, etc.) -- this is the DVX convention for passing -// scan codes through the same int32_t channel as ASCII values. -// -// Returns -1 for unrecognized keys so callers can check whether the -// key was consumed. - -int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) { - if (key == (0x50 | 0x100)) { - // Down arrow - if (current < count - 1) { - return current + 1; - } - - return current < 0 ? 0 : current; - } - - if (key == (0x48 | 0x100)) { - // Up arrow - if (current > 0) { - return current - 1; - } - - return current < 0 ? 0 : current; - } - - if (key == (0x47 | 0x100)) { - // Home - return 0; - } - - if (key == (0x4F | 0x100)) { - // End - return count - 1; - } - - if (key == (0x51 | 0x100)) { - // Page Down - int32_t n = current + pageSize; - return n >= count ? count - 1 : n; - } - - if (key == (0x49 | 0x100)) { - // Page Up - int32_t n = current - pageSize; - return n < 0 ? 0 : n; - } - - return -1; -} - - -// ============================================================ -// widgetPaintPopupList -// ============================================================ -// -// Shared popup list painting for Dropdown and ComboBox. - -void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) { - // Draw popup border - BevelStyleT bevel; - bevel.highlight = colors->windowHighlight; - bevel.shadow = colors->windowShadow; - bevel.face = colors->contentBg; - bevel.width = 2; - drawBevel(d, ops, popX, popY, popW, popH, &bevel); - - // Draw items - int32_t visibleItems = popH / font->charHeight; - int32_t textX = popX + TEXT_INPUT_PAD; - int32_t textY = popY + 2; - int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; - - for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { - int32_t idx = scrollPos + i; - int32_t iy = textY + i * font->charHeight; - uint32_t ifg = colors->contentFg; - uint32_t ibg = colors->contentBg; - - if (idx == hoverIdx) { - ifg = colors->menuHighlightFg; - ibg = colors->menuHighlightBg; - rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); - } - - drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); - } -} - - // ============================================================ // widgetScrollbarThumb // ============================================================ @@ -768,380 +650,6 @@ void widgetRemoveChild(WidgetT *parent, WidgetT *child) { } -// ============================================================ -// Shared text editing infrastructure -// ============================================================ -// -// The following functions provide clipboard, multi-click detection, -// word boundary logic, cross-widget selection clearing, and the -// single-line text editing engine. They are shared across multiple -// widget DXEs (TextInput, TextArea, ComboBox, Spinner, AnsiTerm) -// and live here in the core library so all DXEs can link to them. - -#define CLIPBOARD_MAX 4096 - -// Shared clipboard -- process-wide, not per-widget. -static char sClipboard[CLIPBOARD_MAX]; -static int32_t sClipboardLen = 0; - -// Multi-click state -static clock_t sLastClickTime = 0; -static int32_t sLastClickX = -1; -static int32_t sLastClickY = -1; -static int32_t sClickCount = 0; - -// Track the widget that last had an active selection so we can -// clear it in O(1) instead of walking every widget in every window. -static WidgetT *sLastSelectedWidget = NULL; - -// TextArea line helpers (static copies for widgetTextDragUpdate) -static int32_t textAreaCountLines(const char *buf, int32_t len); -static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col); -static int32_t textAreaGetLineCount(WidgetT *w); -static int32_t textAreaGetMaxLineLen(WidgetT *w); -static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row); -static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row); -static int32_t textAreaMaxLineLen(const char *buf, int32_t len); - -// Shared undo/selection helpers -static bool clearSelectionOnWidget(WidgetT *w); -static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); -static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); -static int32_t wordBoundaryLeft(const char *buf, int32_t pos); -static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); - - -static bool clearSelectionOnWidget(WidgetT *w) { - if (w->type == WidgetTextInputE) { - if (w->as.textInput.selStart != w->as.textInput.selEnd) { - w->as.textInput.selStart = -1; - w->as.textInput.selEnd = -1; - return true; - } - - w->as.textInput.selStart = -1; - w->as.textInput.selEnd = -1; - } else if (w->type == WidgetTextAreaE) { - if (w->as.textArea.selAnchor != w->as.textArea.selCursor) { - w->as.textArea.selAnchor = -1; - w->as.textArea.selCursor = -1; - return true; - } - - w->as.textArea.selAnchor = -1; - w->as.textArea.selCursor = -1; - } else if (w->type == WidgetComboBoxE) { - if (w->as.comboBox.selStart != w->as.comboBox.selEnd) { - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; - return true; - } - - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; - } else if (w->type == WidgetAnsiTermE) { - if (w->as.ansiTerm->selStartLine >= 0 && - (w->as.ansiTerm->selStartLine != w->as.ansiTerm->selEndLine || - w->as.ansiTerm->selStartCol != w->as.ansiTerm->selEndCol)) { - w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; - w->as.ansiTerm->selStartLine = -1; - w->as.ansiTerm->selStartCol = -1; - w->as.ansiTerm->selEndLine = -1; - w->as.ansiTerm->selEndCol = -1; - w->as.ansiTerm->selecting = false; - return true; - } - - w->as.ansiTerm->selStartLine = -1; - w->as.ansiTerm->selStartCol = -1; - w->as.ansiTerm->selEndLine = -1; - w->as.ansiTerm->selEndCol = -1; - w->as.ansiTerm->selecting = false; - } - - return false; -} - - -// Clears selection on the previously-selected widget (if different -// from the newly-focused one). Validates that the previous widget's -// window is still in the window stack before touching it -- the -// window may have been closed since sLastSelectedWidget was set. -// If the previous widget was in a different window, that window -// gets a full repaint to clear the stale selection highlight. -void clearOtherSelections(WidgetT *except) { - if (!except || !except->window || !except->window->widgetRoot) { - return; - } - - WidgetT *prev = sLastSelectedWidget; - sLastSelectedWidget = except; - - if (!prev || prev == except) { - return; - } - - // Verify the widget is still alive (its window still in the stack) - WindowT *prevWin = prev->window; - - if (!prevWin) { - return; - } - - AppContextT *ctx = wgtGetContext(except); - - if (!ctx) { - return; - } - - bool found = false; - - for (int32_t i = 0; i < ctx->stack.count; i++) { - if (ctx->stack.windows[i] == prevWin) { - found = true; - break; - } - } - - if (!found) { - return; - } - - if (clearSelectionOnWidget(prev) && prevWin != except->window) { - dvxInvalidateWindow(ctx, prevWin); - } -} - - -void clipboardCopy(const char *text, int32_t len) { - if (!text || len <= 0) { - return; - } - - if (len > CLIPBOARD_MAX - 1) { - len = CLIPBOARD_MAX - 1; - } - - memcpy(sClipboard, text, len); - sClipboard[len] = '\0'; - sClipboardLen = len; -} - - -const char *clipboardGet(int32_t *outLen) { - if (outLen) { - *outLen = sClipboardLen; - } - - return sClipboard; -} - - -bool isWordChar(char c) { - return isalnum((unsigned char)c) || c == '_'; -} - - -int32_t multiClickDetect(int32_t vx, int32_t vy) { - clock_t now = clock(); - - if ((now - sLastClickTime) < sDblClickTicks && - abs(vx - sLastClickX) < 4 && abs(vy - sLastClickY) < 4) { - sClickCount++; - } else { - sClickCount = 1; - } - - sLastClickTime = now; - sLastClickX = vx; - sLastClickY = vy; - - return sClickCount; -} - - -// ============================================================ -// TextArea line helpers (static copies for widgetTextDragUpdate) -// ============================================================ - -static int32_t textAreaCountLines(const char *buf, int32_t len) { - int32_t lines = 1; - - for (int32_t i = 0; i < len; i++) { - if (buf[i] == '\n') { - lines++; - } - } - - return lines; -} - - -static int32_t textAreaGetLineCount(WidgetT *w) { - if (w->as.textArea.cachedLines < 0) { - w->as.textArea.cachedLines = textAreaCountLines(w->as.textArea.buf, w->as.textArea.len); - } - - return w->as.textArea.cachedLines; -} - - -static int32_t textAreaLineStart(const char *buf, int32_t len, int32_t row) { - (void)len; - int32_t off = 0; - - for (int32_t r = 0; r < row; r++) { - while (off < len && buf[off] != '\n') { - off++; - } - - if (off < len) { - off++; - } - } - - return off; -} - - -static int32_t textAreaLineLen(const char *buf, int32_t len, int32_t row) { - int32_t start = textAreaLineStart(buf, len, row); - int32_t end = start; - - while (end < len && buf[end] != '\n') { - end++; - } - - return end - start; -} - - -static int32_t textAreaMaxLineLen(const char *buf, int32_t len) { - int32_t maxLen = 0; - int32_t curLen = 0; - - for (int32_t i = 0; i < len; i++) { - if (buf[i] == '\n') { - if (curLen > maxLen) { - maxLen = curLen; - } - curLen = 0; - } else { - curLen++; - } - } - - if (curLen > maxLen) { - maxLen = curLen; - } - - return maxLen; -} - - -static int32_t textAreaGetMaxLineLen(WidgetT *w) { - if (w->as.textArea.cachedMaxLL < 0) { - w->as.textArea.cachedMaxLL = textAreaMaxLineLen(w->as.textArea.buf, w->as.textArea.len); - } - - return w->as.textArea.cachedMaxLL; -} - - -static int32_t textAreaCursorToOff(const char *buf, int32_t len, int32_t row, int32_t col) { - int32_t start = textAreaLineStart(buf, len, row); - int32_t lineL = textAreaLineLen(buf, len, row); - int32_t clampC = col < lineL ? col : lineL; - - return start + clampC; -} - - -// ============================================================ -// Shared undo/selection helpers -// ============================================================ - -static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) { - if (!undoBuf) { - return; - } - - int32_t copyLen = len < bufSize ? len : bufSize - 1; - memcpy(undoBuf, buf, copyLen); - undoBuf[copyLen] = '\0'; - *pUndoLen = copyLen; - *pUndoCursor = cursor; -} - - -static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) { - int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd; - int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart; - - if (lo < 0) { - lo = 0; - } - - if (hi > *pLen) { - hi = *pLen; - } - - if (lo >= hi) { - *pSelStart = -1; - *pSelEnd = -1; - return; - } - - memmove(buf + lo, buf + hi, *pLen - hi + 1); - *pLen -= (hi - lo); - *pCursor = lo; - *pSelStart = -1; - *pSelEnd = -1; -} - - -// ============================================================ -// Word boundary helpers -// ============================================================ - -static int32_t wordBoundaryLeft(const char *buf, int32_t pos) { - if (pos <= 0) { - return 0; - } - - // Skip non-word characters - while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') { - pos--; - } - - // Skip word characters - while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) { - pos--; - } - - return pos; -} - - -static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) { - if (pos >= len) { - return len; - } - - // Skip word characters - while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) { - pos++; - } - - // Skip non-word characters - while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') { - pos++; - } - - return pos; -} - - // ============================================================ // widgetTextDragUpdate -- update selection during mouse drag // ============================================================ @@ -1151,655 +659,9 @@ static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) { // Handles auto-scroll: when the mouse is past the widget edges, the // scroll offset is nudged by one unit per event, creating a smooth // scroll-while-dragging effect. + void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { - AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - - if (w->type == WidgetTextInputE) { - int32_t leftEdge = w->x + TEXT_INPUT_PAD; - int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2) / font->charWidth; - widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.textInput.len, &w->as.textInput.cursorPos, &w->as.textInput.scrollOff, &w->as.textInput.selEnd); - } else if (w->type == WidgetTextAreaE) { - int32_t innerX = w->x + 2 + 2; // TEXTAREA_BORDER + TEXTAREA_PAD - int32_t innerY = w->y + 2; // TEXTAREA_BORDER - int32_t innerW = w->w - 2 * 2 - 2 * 2 - 14; // borders, pads, scrollbar - int32_t visCols = innerW / font->charWidth; - int32_t maxLL = textAreaGetMaxLineLen(w); - bool needHSb = (maxLL > visCols); - int32_t innerH = w->h - 2 * 2 - (needHSb ? 14 : 0); - int32_t visRows = innerH / font->charHeight; - int32_t totalLines = textAreaGetLineCount(w); - - if (visRows < 1) { - visRows = 1; - } - - if (visCols < 1) { - visCols = 1; - } - - // Auto-scroll vertically - if (vy < innerY && w->as.textArea.scrollRow > 0) { - w->as.textArea.scrollRow--; - } else if (vy >= innerY + visRows * font->charHeight && w->as.textArea.scrollRow + visRows < totalLines) { - w->as.textArea.scrollRow++; - } - - // Auto-scroll horizontally - int32_t rightEdge = innerX + visCols * font->charWidth; - - if (vx < innerX && w->as.textArea.scrollCol > 0) { - w->as.textArea.scrollCol--; - } else if (vx >= rightEdge && w->as.textArea.scrollCol < maxLL - visCols) { - w->as.textArea.scrollCol++; - } - - int32_t relX = vx - innerX; - int32_t relY = vy - innerY; - - int32_t clickRow = w->as.textArea.scrollRow + relY / font->charHeight; - int32_t clickCol = w->as.textArea.scrollCol + relX / font->charWidth; - - if (clickRow < 0) { - clickRow = 0; - } - - if (clickRow >= totalLines) { - clickRow = totalLines - 1; - } - - if (clickCol < 0) { - clickCol = 0; - } - - int32_t lineL = textAreaLineLen(w->as.textArea.buf, w->as.textArea.len, clickRow); - - if (clickCol > lineL) { - clickCol = lineL; - } - - w->as.textArea.cursorRow = clickRow; - w->as.textArea.cursorCol = clickCol; - w->as.textArea.desiredCol = clickCol; - w->as.textArea.selCursor = textAreaCursorToOff(w->as.textArea.buf, w->as.textArea.len, clickRow, clickCol); - } else if (w->type == WidgetComboBoxE) { - int32_t leftEdge = w->x + TEXT_INPUT_PAD; - int32_t maxChars = (w->w - TEXT_INPUT_PAD * 2 - 16) / font->charWidth; - widgetTextEditDragUpdateLine(vx, leftEdge, maxChars, font, w->as.comboBox.len, &w->as.comboBox.cursorPos, &w->as.comboBox.scrollOff, &w->as.comboBox.selEnd); - } else if (w->type == WidgetAnsiTermE) { - int32_t baseX = w->x + 2; // ANSI_BORDER - int32_t baseY = w->y + 2; - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; - - int32_t clickRow = (vy - baseY) / font->charHeight; - int32_t clickCol = (vx - baseX) / font->charWidth; - - if (clickRow < 0) { - clickRow = 0; - } - - if (clickRow >= rows) { - clickRow = rows - 1; - } - - if (clickCol < 0) { - clickCol = 0; - } - - if (clickCol >= cols) { - clickCol = cols; - } - - w->as.ansiTerm->selEndLine = w->as.ansiTerm->scrollPos + clickRow; - w->as.ansiTerm->selEndCol = clickCol; - w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; + if (w->wclass && w->wclass->dragSelect) { + w->wclass->dragSelect(w, root, vx, vy); } } - - -// ============================================================ -// widgetTextEditDragUpdateLine -// ============================================================ -// -// Called during mouse drag to extend the selection for single-line -// text widgets. Auto-scrolls when the mouse moves past the visible -// text edges. -void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) { - int32_t rightEdge = leftEdge + maxChars * font->charWidth; - - if (vx < leftEdge && *pScrollOff > 0) { - (*pScrollOff)--; - } else if (vx >= rightEdge && *pScrollOff + maxChars < len) { - (*pScrollOff)++; - } - - int32_t relX = vx - leftEdge; - int32_t charPos = relX / font->charWidth + *pScrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > len) { - charPos = len; - } - - *pCursorPos = charPos; - *pSelEnd = charPos; -} - - -// ============================================================ -// widgetTextEditMouseClick -// ============================================================ -// -// Computes cursor position from pixel coordinates, handles multi-click -// (double = word select, triple = select all), and optionally starts -// drag-select. -void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) { - int32_t relX = vx - textLeftX; - int32_t charPos = relX / font->charWidth + scrollOff; - - if (charPos < 0) { - charPos = 0; - } - - if (charPos > len) { - charPos = len; - } - - int32_t clicks = multiClickDetect(vx, vy); - - if (clicks >= 3) { - *pSelStart = 0; - *pSelEnd = len; - *pCursorPos = len; - sDragTextSelect = NULL; - return; - } - - if (clicks == 2) { - if (wordSelect && buf) { - int32_t ws = wordStart(buf, charPos); - int32_t we = wordEnd(buf, len, charPos); - *pSelStart = ws; - *pSelEnd = we; - *pCursorPos = we; - } else { - *pSelStart = 0; - *pSelEnd = len; - *pCursorPos = len; - } - - sDragTextSelect = NULL; - return; - } - - // Single click: place cursor - *pCursorPos = charPos; - *pSelStart = charPos; - *pSelEnd = charPos; - sDragTextSelect = dragSelect ? w : NULL; -} - - -// ============================================================ -// widgetTextEditOnKey -- shared single-line text editing logic -// ============================================================ -// -// This is the core single-line text editing engine, parameterized by -// pointer to allow reuse across TextInput, Spinner, and ComboBox. -void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) { - bool shift = (mod & KEY_MOD_SHIFT) != 0; - bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd); - int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1; - int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1; - - // Clamp selection to buffer bounds - if (hasSel) { - if (selLo < 0) { - selLo = 0; - } - - if (selHi > *pLen) { - selHi = *pLen; - } - - if (selLo >= selHi) { - hasSel = false; - selLo = -1; - selHi = -1; - } - } - - // Ctrl+A -- select all - if (key == 1 && pSelStart && pSelEnd) { - *pSelStart = 0; - *pSelEnd = *pLen; - *pCursor = *pLen; - goto adjustScroll; - } - - // Ctrl+C -- copy - if (key == 3) { - if (hasSel) { - clipboardCopy(buf + selLo, selHi - selLo); - } - - return; - } - - // Ctrl+V -- paste - if (key == 22) { - if (sClipboardLen > 0) { - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - if (hasSel) { - textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); - } - - int32_t canFit = bufSize - 1 - *pLen; - // For single-line, skip newlines in clipboard - int32_t paste = 0; - - for (int32_t i = 0; i < sClipboardLen && paste < canFit; i++) { - if (sClipboard[i] != '\n' && sClipboard[i] != '\r') { - paste++; - } - } - - if (paste > 0) { - int32_t pos = *pCursor; - memmove(buf + pos + paste, buf + pos, *pLen - pos + 1); - - int32_t j = 0; - - for (int32_t i = 0; i < sClipboardLen && j < paste; i++) { - if (sClipboard[i] != '\n' && sClipboard[i] != '\r') { - buf[pos + j] = sClipboard[i]; - j++; - } - } - - *pLen += paste; - *pCursor += paste; - } - - if (w->onChange) { - w->onChange(w); - } - } - - goto adjustScroll; - } - - // Ctrl+X -- cut - if (key == 24) { - if (hasSel) { - clipboardCopy(buf + selLo, selHi - selLo); - - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); - - if (w->onChange) { - w->onChange(w); - } - } - - goto adjustScroll; - } - - // Ctrl+Z -- undo - if (key == 26 && undoBuf && pUndoLen && pUndoCursor) { - // Swap current and undo - char tmpBuf[CLIPBOARD_MAX]; - int32_t tmpLen = *pLen; - int32_t tmpCursor = *pCursor; - int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1; - - memcpy(tmpBuf, buf, copyLen); - tmpBuf[copyLen] = '\0'; - - int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1; - memcpy(buf, undoBuf, restLen); - buf[restLen] = '\0'; - *pLen = restLen; - - int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; - - // Save old as new undo - int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; - memcpy(undoBuf, tmpBuf, saveLen); - undoBuf[saveLen] = '\0'; - *pUndoLen = saveLen; - *pUndoCursor = tmpCursor; - - *pCursor = restoreCursor; - - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - if (w->onChange) { - w->onChange(w); - } - - goto adjustScroll; - } - - if (key >= 32 && key < 127) { - // Printable character - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - if (hasSel) { - textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); - } - - if (*pLen < bufSize - 1) { - int32_t pos = *pCursor; - memmove(buf + pos + 1, buf + pos, *pLen - pos + 1); - buf[pos] = (char)key; - (*pLen)++; - (*pCursor)++; - - if (w->onChange) { - w->onChange(w); - } - } - } else if (key == 8) { - // Backspace - if (hasSel) { - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); - - if (w->onChange) { - w->onChange(w); - } - } else if (*pCursor > 0) { - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - int32_t pos = *pCursor; - memmove(buf + pos - 1, buf + pos, *pLen - pos + 1); - (*pLen)--; - (*pCursor)--; - - if (w->onChange) { - w->onChange(w); - } - } - } else if (key == (0x4B | 0x100)) { - // Left arrow - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - if (*pCursor > 0) { - (*pCursor)--; - } - - *pSelEnd = *pCursor; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - if (*pCursor > 0) { - (*pCursor)--; - } - } - } else if (key == (0x4D | 0x100)) { - // Right arrow - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - if (*pCursor < *pLen) { - (*pCursor)++; - } - - *pSelEnd = *pCursor; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - if (*pCursor < *pLen) { - (*pCursor)++; - } - } - } else if (key == (0x73 | 0x100)) { - // Ctrl+Left -- word left - int32_t newPos = wordBoundaryLeft(buf, *pCursor); - - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - *pCursor = newPos; - *pSelEnd = newPos; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - *pCursor = newPos; - } - } else if (key == (0x74 | 0x100)) { - // Ctrl+Right -- word right - int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor); - - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - *pCursor = newPos; - *pSelEnd = newPos; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - *pCursor = newPos; - } - } else if (key == (0x47 | 0x100)) { - // Home - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - *pCursor = 0; - *pSelEnd = 0; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - *pCursor = 0; - } - } else if (key == (0x4F | 0x100)) { - // End - if (shift && pSelStart && pSelEnd) { - if (*pSelStart < 0) { - *pSelStart = *pCursor; - *pSelEnd = *pCursor; - } - - *pCursor = *pLen; - *pSelEnd = *pLen; - } else { - if (pSelStart) { - *pSelStart = -1; - } - - if (pSelEnd) { - *pSelEnd = -1; - } - - *pCursor = *pLen; - } - } else if (key == (0x53 | 0x100)) { - // Delete - if (hasSel) { - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); - - if (w->onChange) { - w->onChange(w); - } - } else if (*pCursor < *pLen) { - if (undoBuf) { - textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); - } - - int32_t pos = *pCursor; - memmove(buf + pos, buf + pos + 1, *pLen - pos); - (*pLen)--; - - if (w->onChange) { - w->onChange(w); - } - } - } else { - return; - } - -adjustScroll: - // Adjust scroll offset to keep cursor visible - { - AppContextT *ctx = wgtGetContext(w); - const BitmapFontT *font = &ctx->font; - int32_t fieldW = w->w; - - if (w->type == WidgetComboBoxE) { - fieldW -= DROPDOWN_BTN_WIDTH; - } - - int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; - - if (*pCursor < *pScrollOff) { - *pScrollOff = *pCursor; - } - - if (*pCursor >= *pScrollOff + visibleChars) { - *pScrollOff = *pCursor - visibleChars + 1; - } - } - - wgtInvalidatePaint(w); -} - - -// ============================================================ -// widgetTextEditPaintLine -// ============================================================ -// -// Renders a single line of text with optional selection highlighting -// and a blinking cursor. -void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) { - // Normalize selection to low/high - int32_t selLo = -1; - int32_t selHi = -1; - - if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) { - selLo = selStart < selEnd ? selStart : selEnd; - selHi = selStart < selEnd ? selEnd : selStart; - } - - // Map selection to visible range - int32_t visSelLo = selLo - scrollOff; - int32_t visSelHi = selHi - scrollOff; - - if (visSelLo < 0) { visSelLo = 0; } - if (visSelHi > visLen) { visSelHi = visLen; } - - if (selLo >= 0 && visSelLo < visSelHi) { - if (visSelLo > 0) { - drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true); - } - - drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); - - if (visSelHi < visLen) { - drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true); - } - } else if (visLen > 0) { - drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true); - } - - // Blinking cursor - if (showCursor && sCursorBlinkOn) { - int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth; - - if (cursorX >= cursorMinX && cursorX < cursorMaxX) { - drawVLine(d, ops, cursorX, textY, font->charHeight, fg); - } - } -} - - -int32_t wordEnd(const char *buf, int32_t len, int32_t pos) { - while (pos < len && isWordChar(buf[pos])) { - pos++; - } - - return pos; -} - - -int32_t wordStart(const char *buf, int32_t pos) { - while (pos > 0 && isWordChar(buf[pos - 1])) { - pos--; - } - - return pos; -} diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 6f15e9f3..91e1fcda 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetEvent.c -- Window event handlers for widget system // // This file routes window-level events (mouse, keyboard, paint, resize, @@ -17,7 +18,7 @@ // still adjusts the value, because sDragSlider captures the event // before hit testing runs. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" // Widget whose popup was just closed by click-outside -- prevents // immediate re-open on the same click. Without this, clicking the @@ -206,10 +207,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Close popups from other windows if (sOpenPopup && sOpenPopup->window != win) { - if (sOpenPopup->type == WidgetDropdownE) { - sOpenPopup->as.dropdown.open = false; - } else if (sOpenPopup->type == WidgetComboBoxE) { - sOpenPopup->as.comboBox.open = false; + if (sOpenPopup->wclass && sOpenPopup->wclass->closePopup) { + sOpenPopup->wclass->closePopup(sOpenPopup); } sOpenPopup = NULL; @@ -225,13 +224,13 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { if (sDragTextSelect && (buttons & MOUSE_LEFT)) { widgetTextDragUpdate(sDragTextSelect, root, x, y); - if (sDragTextSelect->type == WidgetAnsiTermE) { - // Fast path: repaint only dirty terminal rows into the - // content buffer, then dirty just that screen stripe. + if (sDragTextSelect->wclass && sDragTextSelect->wclass->quickRepaint) { + // Fast path: repaint only dirty rows into the content + // buffer, then dirty just that screen stripe. int32_t dirtyY = 0; int32_t dirtyH = 0; - if (wgtAnsiTermRepaint(sDragTextSelect, &dirtyY, &dirtyH) > 0) { + if (sDragTextSelect->wclass->quickRepaint(sDragTextSelect, &dirtyY, &dirtyH) > 0) { AppContextT *ctx = (AppContextT *)root->userData; int32_t scrollY2 = win->vScroll ? win->vScroll->value : 0; int32_t rectX = win->x + win->contentX; @@ -249,8 +248,6 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle canvas drawing release if (sDrawingCanvas && !(buttons & MOUSE_LEFT)) { - sDrawingCanvas->as.canvas.lastX = -1; - sDrawingCanvas->as.canvas.lastY = -1; sDrawingCanvas = NULL; wgtInvalidatePaint(root); return; @@ -274,38 +271,9 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle slider drag (mouse move while pressed) if (sDragSlider && (buttons & MOUSE_LEFT)) { - int32_t range = sDragSlider->as.slider.maxValue - sDragSlider->as.slider.minValue; - - if (range > 0) { - int32_t newVal; - - if (sDragSlider->as.slider.vertical) { - int32_t thumbRange = sDragSlider->h - SLIDER_THUMB_W; - int32_t relY = y - sDragSlider->y - sDragOffset; - newVal = sDragSlider->as.slider.minValue + (relY * range) / thumbRange; - } else { - int32_t thumbRange = sDragSlider->w - SLIDER_THUMB_W; - int32_t relX = x - sDragSlider->x - sDragOffset; - newVal = sDragSlider->as.slider.minValue + (relX * range) / thumbRange; - } - - if (newVal < sDragSlider->as.slider.minValue) { - newVal = sDragSlider->as.slider.minValue; - } - - if (newVal > sDragSlider->as.slider.maxValue) { - newVal = sDragSlider->as.slider.maxValue; - } - - if (newVal != sDragSlider->as.slider.value) { - sDragSlider->as.slider.value = newVal; - - if (sDragSlider->onChange) { - sDragSlider->onChange(sDragSlider); - } - - wgtInvalidatePaint(root); - } + if (sDragSlider->wclass && sDragSlider->wclass->scrollDragUpdate) { + sDragSlider->wclass->scrollDragUpdate(sDragSlider, 0, sDragOffset, x, y); + wgtInvalidatePaint(root); } return; @@ -314,44 +282,13 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle ListView column resize release if (sResizeListView && !(buttons & MOUSE_LEFT)) { sResizeListView = NULL; - sResizeCol = -1; - sResizeDragging = false; return; } - // Handle ListView column resize drag. The drag is deferred: on the - // initial click sResizeDragging is false and no resize happens until - // the mouse actually moves from the click point. This allows double- - // click auto-size to work (the first click sets sResizeListView but - // doesn't start dragging; the release clears it; the second click - // reaches widgetListViewOnMouse where multiClickDetect returns 2). + // Handle ListView column resize drag via vtable if (sResizeListView && (buttons & MOUSE_LEFT)) { - if (!sResizeDragging) { - if (x == sResizeStartX) { - return; - } - - sResizeDragging = true; - } - - int32_t delta = x - sResizeStartX; - int32_t newW = sResizeOrigW + delta; - - if (newW < LISTVIEW_MIN_COL_W) { - newW = LISTVIEW_MIN_COL_W; - } - - if (newW != sResizeListView->as.listView->resolvedColW[sResizeCol]) { - sResizeListView->as.listView->resolvedColW[sResizeCol] = newW; - - // Recalculate totalColW - int32_t total = 0; - - for (int32_t c = 0; c < sResizeListView->as.listView->colCount; c++) { - total += sResizeListView->as.listView->resolvedColW[c]; - } - - sResizeListView->as.listView->totalColW = total; + if (sResizeListView->wclass && sResizeListView->wclass->scrollDragUpdate) { + sResizeListView->wclass->scrollDragUpdate(sResizeListView, -1, 0, x, y); wgtInvalidatePaint(root); } @@ -379,28 +316,10 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { return; } - // Handle splitter drag + // Handle splitter drag via vtable if (sDragSplitter && (buttons & MOUSE_LEFT)) { - int32_t pos; - - if (sDragSplitter->as.splitter.vertical) { - pos = x - sDragSplitter->x - sDragSplitStart; - } else { - pos = y - sDragSplitter->y - sDragSplitStart; - } - - if (sSplitterClampPosFn) { - sSplitterClampPosFn(sDragSplitter, &pos); - } - - if (pos != sDragSplitter->as.splitter.dividerPos) { - sDragSplitter->as.splitter.dividerPos = pos; - - if (sDragSplitter->onChange) { - sDragSplitter->onChange(sDragSplitter); - } - - wgtInvalidate(sDragSplitter); + if (sDragSplitter->wclass && sDragSplitter->wclass->scrollDragUpdate) { + sDragSplitter->wclass->scrollDragUpdate(sDragSplitter, 0, sDragSplitStart, x, y); } return; @@ -417,9 +336,9 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { if (sDragScrollbar && (buttons & MOUSE_LEFT)) { widgetScrollbarDragUpdate(sDragScrollbar, sDragScrollbarOrient, sDragScrollbarOff, x, y); - // ScrollPane needs full relayout because it positions children + // Scroll containers need full relayout because they position children // at absolute coordinates incorporating the scroll offset. - if (sDragScrollbar->type == WidgetScrollPaneE) { + if (sDragScrollbar->wclass && (sDragScrollbar->wclass->flags & WCLASS_RELAYOUT_ON_SCROLL)) { wgtInvalidate(sDragScrollbar); } else { wgtInvalidatePaint(root); @@ -430,10 +349,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Handle button press release if (sPressedButton && !(buttons & MOUSE_LEFT)) { - if (sPressedButton->type == WidgetImageButtonE) { - sPressedButton->as.imageButton.pressed = false; - } else { - sPressedButton->as.button.pressed = false; + if (sPressedButton->wclass && sPressedButton->wclass->setPressed) { + sPressedButton->wclass->setPressed(sPressedButton, false); } // Fire onClick if released over the same button in the same window @@ -460,20 +377,11 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { y >= sPressedButton->y && y < sPressedButton->y + sPressedButton->h); } - bool curPressed = (sPressedButton->type == WidgetImageButtonE) - ? sPressedButton->as.imageButton.pressed - : sPressedButton->as.button.pressed; - - if (curPressed != over) { - if (sPressedButton->type == WidgetImageButtonE) { - sPressedButton->as.imageButton.pressed = over; - } else { - sPressedButton->as.button.pressed = over; - } - - wgtInvalidatePaint(sPressedButton); + if (sPressedButton->wclass && sPressedButton->wclass->setPressed) { + sPressedButton->wclass->setPressed(sPressedButton, over); } + wgtInvalidatePaint(sPressedButton); return; } @@ -487,47 +395,17 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { int32_t popW; int32_t popH; - widgetDropdownPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH); + if (!sOpenPopup->wclass || !sOpenPopup->wclass->getPopupRect) { + sOpenPopup = NULL; + return; + } + + sOpenPopup->wclass->getPopupRect(sOpenPopup, font, win->contentH, &popX, &popY, &popW, &popH); if (x >= popX && x < popX + popW && y >= popY && y < popY + popH) { - // Click on popup item - int32_t itemIdx = (y - popY - 2) / font->charHeight; - int32_t scrollP = 0; - - if (sOpenPopup->type == WidgetDropdownE) { - scrollP = sOpenPopup->as.dropdown.scrollPos; - } else { - scrollP = sOpenPopup->as.comboBox.listScrollPos; - } - - itemIdx += scrollP; - - if (sOpenPopup->type == WidgetDropdownE) { - if (itemIdx >= 0 && itemIdx < sOpenPopup->as.dropdown.itemCount) { - sOpenPopup->as.dropdown.selectedIdx = itemIdx; - sOpenPopup->as.dropdown.open = false; - - if (sOpenPopup->onChange) { - sOpenPopup->onChange(sOpenPopup); - } - } - } else if (sOpenPopup->type == WidgetComboBoxE) { - if (itemIdx >= 0 && itemIdx < sOpenPopup->as.comboBox.itemCount) { - sOpenPopup->as.comboBox.selectedIdx = itemIdx; - sOpenPopup->as.comboBox.open = false; - - // Copy selected item text to buffer - const char *itemText = sOpenPopup->as.comboBox.items[itemIdx]; - strncpy(sOpenPopup->as.comboBox.buf, itemText, sOpenPopup->as.comboBox.bufSize - 1); - sOpenPopup->as.comboBox.buf[sOpenPopup->as.comboBox.bufSize - 1] = '\0'; - sOpenPopup->as.comboBox.len = (int32_t)strlen(sOpenPopup->as.comboBox.buf); - sOpenPopup->as.comboBox.cursorPos = sOpenPopup->as.comboBox.len; - sOpenPopup->as.comboBox.scrollOff = 0; - - if (sOpenPopup->onChange) { - sOpenPopup->onChange(sOpenPopup); - } - } + // Click on popup item -- dispatch to widget's onMouse + if (sOpenPopup->wclass && sOpenPopup->wclass->onMouse) { + sOpenPopup->wclass->onMouse(sOpenPopup, root, x, y); } sOpenPopup = NULL; @@ -538,10 +416,8 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // Click outside popup -- close it and remember which widget it was sClosedPopup = sOpenPopup; - if (sOpenPopup->type == WidgetDropdownE) { - sOpenPopup->as.dropdown.open = false; - } else if (sOpenPopup->type == WidgetComboBoxE) { - sOpenPopup->as.comboBox.open = false; + if (sOpenPopup->wclass && sOpenPopup->wclass->closePopup) { + sOpenPopup->wclass->closePopup(sOpenPopup); } sOpenPopup = NULL; @@ -587,7 +463,7 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { // in the release handler above. They still get onDblClick here. if (hit->enabled) { int32_t clicks = multiClickDetect(vx, vy); - bool isBtn = (hit->type == WidgetButtonE || hit->type == WidgetImageButtonE); + bool isBtn = (hit->wclass && (hit->wclass->flags & WCLASS_PRESS_RELEASE)); if (clicks >= 2 && hit->onDblClick) { hit->onDblClick(hit); @@ -755,223 +631,12 @@ void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value) { // widgetReorderDrop -- finalize drag-reorder on mouse release // ============================================================ // -// Completes a drag-reorder operation for ListBox, ListView, or TreeView. -// Moves the dragged item from its original position to the drop position -// by shifting intermediate elements. This is an in-place array rotation -// for ListBox/ListView (O(n) element moves) and a linked-list splice -// for TreeView. -// -// For ListBox and ListView, the item array, selection bits, and sort -// indices are all shifted together to maintain consistency. The selected -// index is updated to follow the moved item. -// -// For TreeView, the operation is a tree node re-parenting: unlink the -// dragged item from its old parent's child list, then insert it before -// or after the drop target in the target's parent's child list. +// Dispatches to the widget's reorderDrop vtable method, which handles +// the type-specific logic for completing the drag-reorder operation. void widgetReorderDrop(WidgetT *w) { - if (w->type == WidgetListBoxE) { - int32_t drag = w->as.listBox.dragIdx; - int32_t drop = w->as.listBox.dropIdx; - w->as.listBox.dragIdx = -1; - w->as.listBox.dropIdx = -1; - - if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) { - return; - } - - // Move item at dragIdx to before dropIdx - const char *temp = w->as.listBox.items[drag]; - uint8_t selBit = 0; - - if (w->as.listBox.multiSelect && w->as.listBox.selBits) { - selBit = w->as.listBox.selBits[drag]; - } - - if (drag < drop) { - for (int32_t i = drag; i < drop - 1; i++) { - w->as.listBox.items[i] = w->as.listBox.items[i + 1]; - - if (w->as.listBox.selBits) { - w->as.listBox.selBits[i] = w->as.listBox.selBits[i + 1]; - } - } - - w->as.listBox.items[drop - 1] = temp; - - if (w->as.listBox.selBits) { - w->as.listBox.selBits[drop - 1] = selBit; - } - - w->as.listBox.selectedIdx = drop - 1; - } else { - for (int32_t i = drag; i > drop; i--) { - w->as.listBox.items[i] = w->as.listBox.items[i - 1]; - - if (w->as.listBox.selBits) { - w->as.listBox.selBits[i] = w->as.listBox.selBits[i - 1]; - } - } - - w->as.listBox.items[drop] = temp; - - if (w->as.listBox.selBits) { - w->as.listBox.selBits[drop] = selBit; - } - - w->as.listBox.selectedIdx = drop; - } - - if (w->onChange) { - w->onChange(w); - } - } else if (w->type == WidgetListViewE) { - int32_t drag = w->as.listView->dragIdx; - int32_t drop = w->as.listView->dropIdx; - int32_t colCnt = w->as.listView->colCount; - w->as.listView->dragIdx = -1; - w->as.listView->dropIdx = -1; - - if (drag < 0 || drop < 0 || drag == drop || drag == drop - 1) { - return; - } - - // Move row at dragIdx to before dropIdx - const char *temp[LISTVIEW_MAX_COLS]; - - for (int32_t c = 0; c < colCnt; c++) { - temp[c] = w->as.listView->cellData[drag * colCnt + c]; - } - - uint8_t selBit = 0; - - if (w->as.listView->multiSelect && w->as.listView->selBits) { - selBit = w->as.listView->selBits[drag]; - } - - int32_t sortVal = 0; - - if (w->as.listView->sortIndex) { - sortVal = w->as.listView->sortIndex[drag]; - } - - if (drag < drop) { - for (int32_t i = drag; i < drop - 1; i++) { - for (int32_t c = 0; c < colCnt; c++) { - w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i + 1) * colCnt + c]; - } - - if (w->as.listView->selBits) { - w->as.listView->selBits[i] = w->as.listView->selBits[i + 1]; - } - - if (w->as.listView->sortIndex) { - w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i + 1]; - } - } - - int32_t dest = drop - 1; - - for (int32_t c = 0; c < colCnt; c++) { - w->as.listView->cellData[dest * colCnt + c] = temp[c]; - } - - if (w->as.listView->selBits) { - w->as.listView->selBits[dest] = selBit; - } - - if (w->as.listView->sortIndex) { - w->as.listView->sortIndex[dest] = sortVal; - } - - w->as.listView->selectedIdx = dest; - } else { - for (int32_t i = drag; i > drop; i--) { - for (int32_t c = 0; c < colCnt; c++) { - w->as.listView->cellData[i * colCnt + c] = w->as.listView->cellData[(i - 1) * colCnt + c]; - } - - if (w->as.listView->selBits) { - w->as.listView->selBits[i] = w->as.listView->selBits[i - 1]; - } - - if (w->as.listView->sortIndex) { - w->as.listView->sortIndex[i] = w->as.listView->sortIndex[i - 1]; - } - } - - for (int32_t c = 0; c < colCnt; c++) { - w->as.listView->cellData[drop * colCnt + c] = temp[c]; - } - - if (w->as.listView->selBits) { - w->as.listView->selBits[drop] = selBit; - } - - if (w->as.listView->sortIndex) { - w->as.listView->sortIndex[drop] = sortVal; - } - - w->as.listView->selectedIdx = drop; - } - - if (w->onChange) { - w->onChange(w); - } - } else if (w->type == WidgetTreeViewE) { - WidgetT *drag = w->as.treeView.dragItem; - WidgetT *target = w->as.treeView.dropTarget; - bool after = w->as.treeView.dropAfter; - w->as.treeView.dragItem = NULL; - w->as.treeView.dropTarget = NULL; - - if (!drag || !target || drag == target) { - return; - } - - w->as.treeView.dimsValid = false; - - // Unlink drag from its current parent - WidgetT *oldParent = drag->parent; - - if (oldParent->firstChild == drag) { - oldParent->firstChild = drag->nextSibling; - } else { - for (WidgetT *c = oldParent->firstChild; c; c = c->nextSibling) { - if (c->nextSibling == drag) { - c->nextSibling = drag->nextSibling; - break; - } - } - } - - drag->nextSibling = NULL; - - // Insert drag before or after target (same parent level) - WidgetT *newParent = target->parent; - drag->parent = newParent; - - if (after) { - drag->nextSibling = target->nextSibling; - target->nextSibling = drag; - } else { - if (newParent->firstChild == target) { - drag->nextSibling = target; - newParent->firstChild = drag; - } else { - for (WidgetT *c = newParent->firstChild; c; c = c->nextSibling) { - if (c->nextSibling == target) { - c->nextSibling = drag; - drag->nextSibling = target; - break; - } - } - } - } - - if (w->onChange) { - w->onChange(w); - } + if (w->wclass && w->wclass->reorderDrop) { + w->wclass->reorderDrop(w); } } @@ -980,141 +645,11 @@ void widgetReorderDrop(WidgetT *w) { // widgetReorderUpdate -- update drop position during drag // ============================================================ // -// Tracks the mouse during a drag-reorder, updating the drop indicator -// position and auto-scrolling when the mouse is near the widget's edges. -// -// The drop position is computed from the mouse Y relative to item -// boundaries: if the mouse is in the top half of an item, the drop -// indicator goes before that item; if in the bottom half, it goes -// after. This gives intuitive "insert between items" behavior. -// -// Auto-scrolling happens when the mouse is within one row-height of -// the top or bottom edge, allowing the user to drag items to positions -// not currently visible. +// Dispatches to the widget's reorderUpdate vtable method, which handles +// type-specific drop indicator positioning and auto-scrolling. void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) { - (void)x; - AppContextT *ctx = (AppContextT *)root->userData; - const BitmapFontT *font = &ctx->font; - - if (w->type == WidgetListBoxE) { - int32_t innerY = w->y + LISTBOX_BORDER; - int32_t innerH = w->h - LISTBOX_BORDER * 2; - int32_t visibleRows = innerH / font->charHeight; - int32_t maxScroll = w->as.listBox.itemCount - visibleRows; - - if (maxScroll < 0) { - maxScroll = 0; - } - - // Auto-scroll when dragging near edges - if (y < innerY + font->charHeight && w->as.listBox.scrollPos > 0) { - w->as.listBox.scrollPos--; - } else if (y > innerY + innerH - font->charHeight && w->as.listBox.scrollPos < maxScroll) { - w->as.listBox.scrollPos++; - } - - int32_t relY = y - innerY; - int32_t row = w->as.listBox.scrollPos + relY / font->charHeight; - int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0; - int32_t drop = row + halfRow; - - if (drop < 0) { - drop = 0; - } - - if (drop > w->as.listBox.itemCount) { - drop = w->as.listBox.itemCount; - } - - w->as.listBox.dropIdx = drop; - } else if (w->type == WidgetListViewE) { - int32_t headerH = font->charHeight + 4; - int32_t innerY = w->y + LISTVIEW_BORDER + headerH; - int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; - int32_t visibleRows = innerH / font->charHeight; - int32_t maxScroll = w->as.listView->rowCount - visibleRows; - - if (maxScroll < 0) { - maxScroll = 0; - } - - // Auto-scroll when dragging near edges - if (y < innerY + font->charHeight && w->as.listView->scrollPos > 0) { - w->as.listView->scrollPos--; - } else if (y > innerY + innerH - font->charHeight && w->as.listView->scrollPos < maxScroll) { - w->as.listView->scrollPos++; - } - - int32_t relY = y - innerY; - int32_t row = w->as.listView->scrollPos + relY / font->charHeight; - int32_t halfRow = (relY % font->charHeight) >= font->charHeight / 2 ? 1 : 0; - int32_t drop = row + halfRow; - - if (drop < 0) { - drop = 0; - } - - if (drop > w->as.listView->rowCount) { - drop = w->as.listView->rowCount; - } - - w->as.listView->dropIdx = drop; - } else if (w->type == WidgetTreeViewE) { - int32_t innerY = w->y + TREE_BORDER; - int32_t innerH = w->h - TREE_BORDER * 2; - - // Auto-scroll when dragging near edges (pixel-based scroll) - if (y < innerY + font->charHeight && w->as.treeView.scrollPos > 0) { - w->as.treeView.scrollPos -= font->charHeight; - - if (w->as.treeView.scrollPos < 0) { - w->as.treeView.scrollPos = 0; - } - } else if (y > innerY + innerH - font->charHeight) { - w->as.treeView.scrollPos += font->charHeight; - // Paint will clamp to actual max - } - - // Find which visible item the mouse is over - int32_t curY = w->y + TREE_BORDER - w->as.treeView.scrollPos; - - // Walk visible items to find drop target - WidgetT *first = NULL; - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (c->type == WidgetTreeItemE && c->visible) { - first = c; - break; - } - } - - WidgetT *target = NULL; - bool after = false; - - for (WidgetT *v = first; v; v = sTreeViewNextVisibleFn ? sTreeViewNextVisibleFn(v, w) : NULL) { - int32_t itemBot = curY + font->charHeight; - int32_t mid = curY + font->charHeight / 2; - - if (y < mid) { - target = v; - after = false; - break; - } - - curY = itemBot; - - // Check if mouse is between this item and next - WidgetT *next = sTreeViewNextVisibleFn ? sTreeViewNextVisibleFn(v, w) : NULL; - - if (!next || y < itemBot) { - target = v; - after = true; - break; - } - } - - w->as.treeView.dropTarget = target; - w->as.treeView.dropAfter = after; + if (w->wclass && w->wclass->reorderUpdate) { + w->wclass->reorderUpdate(w, root, x, y); } } diff --git a/core/widgetInternal.h b/core/widgetInternal.h deleted file mode 100644 index 465ee7a2..00000000 --- a/core/widgetInternal.h +++ /dev/null @@ -1,494 +0,0 @@ -// widgetInternal.h -- Shared internal header for widget implementation files -// -// This header is included only by widget implementation .c files (in the -// widgets/ directory), never by application code. It exposes the vtable -// system, shared module-level state, and internal helper functions that -// widget implementations need but the public API should not expose. -// -// The widget system is split across multiple .c files by concern: -// widgetCore.c -- allocation, tree ops, focus management, shared helpers -// widgetLayout.c -- measure + layout algorithms for box containers -// widgetEvent.c -- mouse/keyboard dispatch, scrollbar management -// widgetOps.c -- paint dispatch, overlay rendering -// widget*.c -- per-type paint, event, and layout implementations -#ifndef WIDGET_INTERNAL_H -#define WIDGET_INTERNAL_H - -#include "dvxWidget.h" -#include "dvxApp.h" -#include "dvxDraw.h" -#include "dvxWm.h" -#include "dvxVideo.h" - -#include -#include -#include - -// ============================================================ -// Widget class table -// ============================================================ -// -// WidgetClassT and WCLASS_* flags are now in dvxWidget.h (public API) -// so that DXE plugins can register new widget types. -// -// Global vtable array -- sized WGT_MAX_TYPES. Built-in entries -// (0..WGT_TYPE_BUILTIN_COUNT-1) are initialized at startup. Dynamic -// entries (WGT_TYPE_DYNAMIC_BASE..WGT_MAX_TYPES-1) are filled by -// wgtRegisterClass(). Defined in widgetClass.c. -extern const WidgetClassT *widgetClassTable[WGT_MAX_TYPES]; - -// ============================================================ -// Validation macros -// ============================================================ -// -// Defensive type-checking at the top of public widget API functions. -// Since WidgetT is a tagged union, calling a ListBox function with a -// Button widget would access the wrong union member and corrupt state. -// These macros provide a consistent early-return guard pattern. - -#define VALIDATE_WIDGET(w, wtype, retval) \ - do { if (!(w) || (w)->type != (wtype)) { return (retval); } } while (0) - -#define VALIDATE_WIDGET_VOID(w, wtype) \ - do { if (!(w) || (w)->type != (wtype)) { return; } } while (0) - -// ============================================================ -// Constants -// ============================================================ - -// Modifier flags (BIOS INT 16h shift state bits). Duplicated from -// dvxTypes.h ACCEL_xxx constants because widget code references them -// frequently and the KEY_MOD_ prefix reads better in event handlers. -#define KEY_MOD_SHIFT 0x03 -#define KEY_MOD_CTRL 0x04 -#define KEY_MOD_ALT 0x08 - -// Layout and geometry constants. These define the visual metrics of -// each widget type and are tuned for readability at 640x480 with 8px -// wide fonts. Changing these affects every instance of the widget type. -#define DEFAULT_SPACING 4 -#define DEFAULT_PADDING 4 -#define SEPARATOR_THICKNESS 2 -#define BUTTON_PAD_H 8 -#define BUTTON_PAD_V 4 -#define BUTTON_FOCUS_INSET 3 // focus rect inset from button edge -#define BUTTON_PRESS_OFFSET 1 // text/focus shift when button is pressed -#define CHECKBOX_BOX_SIZE 12 -#define CHECKBOX_GAP 4 -#define FRAME_BEVEL_BORDER 2 -#define FRAME_FLAT_BORDER 1 -#define TEXT_INPUT_PAD 3 -#define DROPDOWN_BTN_WIDTH 16 -#define DROPDOWN_MAX_VISIBLE 8 -#define SLIDER_TRACK_H 4 -#define SLIDER_THUMB_W 11 -#define TAB_PAD_H 8 -#define TAB_PAD_V 4 -#define TAB_BORDER 2 -#define LISTBOX_BORDER 2 -#define LISTVIEW_BORDER 2 -#define LISTVIEW_MIN_COL_W 20 // minimum column width during resize -#define TREE_INDENT 16 -#define TREE_EXPAND_SIZE 9 -#define TREE_ICON_GAP 4 -#define TREE_BORDER 2 -// WGT_SB_W is the widget-internal scrollbar width (slightly narrower than -// the window-level SCROLLBAR_WIDTH) to fit within widget content areas. -#define WGT_SB_W 14 -#define TREE_MIN_ROWS 4 -#define SB_MIN_THUMB 14 -#define SPLITTER_BAR_W 5 -#define SPLITTER_MIN_PANE 20 -#define TOOLBAR_PAD 2 // padding inside toolbar/statusbar -#define TOOLBAR_GAP 2 // gap between toolbar/statusbar children - -// ============================================================ -// Inline helpers -// ============================================================ - -static inline int32_t clampInt(int32_t val, int32_t lo, int32_t hi) { - if (val < lo) { return lo; } - if (val > hi) { return hi; } - return val; -} - -// Classic Windows 3.1 embossed (etched) text for disabled widgets. -// The illusion works by drawing the text twice: first offset +1,+1 in -// the highlight color (creating a light "shadow" behind the text), then -// at the original position in the shadow color. The result is text that -// appears chiseled into the surface -- the universal visual indicator for -// "greyed out" in Motif and Windows 3.x era GUIs. -static inline void drawTextEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { - drawText(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); - drawText(d, ops, font, x, y, text, colors->windowShadow, 0, false); -} - -static inline void drawTextAccelEmbossed(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t x, int32_t y, const char *text, const ColorSchemeT *colors) { - drawTextAccel(d, ops, font, x + 1, y + 1, text, colors->windowHighlight, 0, false); - drawTextAccel(d, ops, font, x, y, text, colors->windowShadow, 0, false); -} - -// ============================================================ -// Shared state (defined in widgetCore.c) -// ============================================================ -// -// Module-level globals for tracking active interactive operations. -// Only one of these can be active at a time (e.g. you can't drag a -// slider and resize a listview column simultaneously). Using globals -// rather than per-widget state avoids bloating every widget with fields -// that are only relevant during a single drag operation. The tradeoff -// is that these are truly global -- but since the GUI is single-threaded -// and only one mouse can exist, this is safe. - -extern bool sCursorBlinkOn; // text cursor blink phase (toggled by wgtUpdateCursorBlink) -extern clock_t sDblClickTicks; // double-click threshold (set from AppContextT.dblClickTicks) -extern bool sDebugLayout; -extern WidgetT *sClosedPopup; // popup that was just closed (prevents immediate reopen) -extern WidgetT *sFocusedWidget; // currently focused widget across all windows -extern WidgetT *sKeyPressedBtn; // button being held via keyboard (Space/Enter) -extern WidgetT *sOpenPopup; // dropdown/combobox with open popup list -extern WidgetT *sPressedButton; // button/imagebutton being held via mouse -extern WidgetT *sDragSlider; // slider being dragged -extern WidgetT *sDrawingCanvas; // canvas receiving drag events -extern WidgetT *sDragTextSelect; // text widget with active mouse selection drag -extern int32_t sDragOffset; // mouse offset within thumb/handle at drag start -extern WidgetT *sResizeListView; // listview whose column is being resized -extern int32_t sResizeCol; // which column is being resized -extern int32_t sResizeStartX; // mouse X at start of column resize -extern int32_t sResizeOrigW; // original column width at start of resize -extern bool sResizeDragging; // true once mouse has moved from click point -extern WidgetT *sDragSplitter; // splitter being dragged -extern int32_t sDragSplitStart; // mouse position at start of splitter drag -extern WidgetT *sDragReorder; // listbox/treeview item being drag-reordered -extern WidgetT *sDragScrollbar; // widget whose scrollbar thumb is being dragged -// Hook for cursor shape change over ListView column borders. -// Set by wgtListViewRegister(); NULL if ListView DXE not loaded. -extern bool (*sListViewColBorderHitFn)(const WidgetT *w, int32_t vx, int32_t vy); -// Hook for splitter divider clamping during drag. -// Set by wgtSplitterRegister(); NULL if Splitter DXE not loaded. -extern void (*sSplitterClampPosFn)(WidgetT *w, int32_t *pos); -// Hook for TreeView visible item iteration during drag-reorder. -// Set by wgtTreeViewRegister(); NULL if TreeView DXE not loaded. -extern WidgetT *(*sTreeViewNextVisibleFn)(WidgetT *item, WidgetT *treeView); -extern int32_t sDragScrollbarOff; // mouse offset within thumb at drag start -extern int32_t sDragScrollbarOrient; // 0=vertical, 1=horizontal - -// ============================================================ -// Core functions (widgetCore.c) -// ============================================================ - -// Tree manipulation -void widgetAddChild(WidgetT *parent, WidgetT *child); -void widgetRemoveChild(WidgetT *parent, WidgetT *child); -void widgetDestroyChildren(WidgetT *w); - -// Allocate a new widget, set its type and vtable, and add it as a child. -WidgetT *widgetAlloc(WidgetT *parent, WidgetTypeE type); - -// Focus management -- Tab/Shift-Tab navigation walks the tree in creation -// order, skipping non-focusable and hidden widgets. -void widgetClearFocus(WidgetT *root); -WidgetT *widgetFindNextFocusable(WidgetT *root, WidgetT *after); -WidgetT *widgetFindPrevFocusable(WidgetT *root, WidgetT *before); - -// Alt+key accelerator lookup -- searches the tree for a widget whose -// accelKey matches, used for keyboard navigation of labeled controls. -WidgetT *widgetFindByAccel(WidgetT *root, char key); - -// Utility queries -int32_t widgetCountVisibleChildren(const WidgetT *w); -int32_t widgetFrameBorderWidth(const WidgetT *w); -bool widgetIsFocusable(WidgetTypeE type); -bool widgetIsBoxContainer(WidgetTypeE type); -bool widgetIsHorizContainer(WidgetTypeE type); - -// Hit testing -- find the deepest widget containing the point (x,y). -// Respects WCLASS_NO_HIT_RECURSE to stop at list/tree widgets. -WidgetT *widgetHitTest(WidgetT *w, int32_t x, int32_t y); - -// Translate arrow/PgUp/PgDn/Home/End keys into list index changes. -// Shared by ListBox, ListView, Dropdown, and TreeView. -int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); - -// Compute popup position for a dropdown/combobox, clamped to screen bounds. -void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); - -// Draw the small downward-pointing triangle glyph used by dropdown buttons. -void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); - -// Compute the maximum strlen across an array of string items. -int32_t widgetMaxItemLen(const char **items, int32_t count); - -// Paint a generic popup item list (used by dropdown, combobox, and popup menus). -void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); - -// Compute scrollbar thumb position and size for a given track length, -// content size, visible size, and scroll position. Used by both widget- -// internal scrollbars and the window-level scrollbar drawing code. -void widgetScrollbarThumb(int32_t trackLen, int32_t totalSize, int32_t visibleSize, int32_t scrollPos, int32_t *thumbPos, int32_t *thumbSize); - -// ============================================================ -// Shared scrollbar functions (widgetScrollbar.c) -// ============================================================ -// -// Widget-internal scrollbar rendering and hit testing, shared across -// ListBox, ListView, TreeView, TextArea, ScrollPane, and AnsiTerm. -// These operate on abstract track coordinates (position + length) so -// the same code handles both horizontal and vertical scrollbars. - -typedef enum { - ScrollHitNoneE, - ScrollHitArrowDecE, - ScrollHitArrowIncE, - ScrollHitPageDecE, - ScrollHitPageIncE, - ScrollHitThumbE -} ScrollHitE; - -void widgetDrawScrollbarH(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbW, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); -void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t sbX, int32_t sbY, int32_t sbH, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); -void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY); -ScrollHitE widgetScrollbarHitTest(int32_t sbLen, int32_t relPos, int32_t totalSize, int32_t visibleSize, int32_t scrollPos); - -// ============================================================ -// Layout functions (widgetLayout.c) -// ============================================================ -// -// The layout algorithm is a two-pass flexbox-like system: -// -// calcMinSize (bottom-up): each leaf widget reports its minimum size -// (e.g. button = text width + padding). Containers sum their children's -// minimums plus spacing along the main axis, and take the maximum of -// children's minimums on the cross axis. -// -// layout (top-down): starting from the available space (window content -// area), each container distributes space to children. Children that fit -// at their minimum get their minimum. Remaining space is distributed -// proportionally by weight to flexible children. This happens recursively -// down the tree. - -void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font); -void widgetCalcMinSizeTree(WidgetT *w, const BitmapFontT *font); -void widgetLayoutBox(WidgetT *w, const BitmapFontT *font); -void widgetLayoutChildren(WidgetT *w, const BitmapFontT *font); - -// ============================================================ -// Event functions (widgetEvent.c) -// ============================================================ -// -// These are the WindowT callback implementations installed by wgtInitWindow(). -// They bridge the window manager's callback interface to the widget tree's -// event dispatch. Each one locates the appropriate widget (via hit-test for -// mouse events, or the focused widget for keyboard events) and calls the -// widget's vtable handler. - -// Sync window-level scrollbar ranges to match the widget tree's overflow. -void widgetManageScrollbars(WindowT *win, AppContextT *ctx); - -void widgetOnKey(WindowT *win, int32_t key, int32_t mod); -void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons); -void widgetOnPaint(WindowT *win, RectT *dirtyArea); -void widgetOnResize(WindowT *win, int32_t newW, int32_t newH); -void widgetOnScroll(WindowT *win, ScrollbarOrientE orient, int32_t value); - -// ============================================================ -// Paint/ops functions (widgetOps.c) -// ============================================================ - -void widgetPaintOne(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetPaintOverlays(WidgetT *root, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); - -// ============================================================ -// Per-widget paint functions -// ============================================================ - -void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetImageButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetDropdownPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetDropdownPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetImagePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetLabelPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetListBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetListViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -bool widgetListViewColBorderHit(const WidgetT *w, int32_t vx, int32_t vy); -void widgetProgressBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetRadioPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetSeparatorPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetScrollPanePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetSliderPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetSpinnerPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetStatusBarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetTextAreaPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetTextInputPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetToolbarPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); -void widgetTreeViewPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors); - -// ============================================================ -// Per-widget calcMinSize functions -// ============================================================ - -void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetImageButtonCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetDropdownCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetImageCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetLabelCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetListBoxCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetListViewCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetProgressBarCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetRadioCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetSeparatorCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetScrollPaneCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetSliderCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetSpinnerCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetSpacerCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetTextAreaCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetTextInputCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetTreeViewCalcMinSize(WidgetT *w, const BitmapFontT *font); - -// ============================================================ -// Per-widget layout functions (for special containers) -// ============================================================ - -void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font); -void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font); -void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font); -void widgetTreeViewLayout(WidgetT *w, const BitmapFontT *font); - -// ============================================================ -// Per-widget getText/setText functions -// ============================================================ - -const char *widgetButtonGetText(const WidgetT *w); -void widgetButtonSetText(WidgetT *w, const char *text); -const char *widgetCheckboxGetText(const WidgetT *w); -void widgetCheckboxSetText(WidgetT *w, const char *text); -const char *widgetComboBoxGetText(const WidgetT *w); -void widgetComboBoxSetText(WidgetT *w, const char *text); -const char *widgetDropdownGetText(const WidgetT *w); -const char *widgetLabelGetText(const WidgetT *w); -void widgetLabelSetText(WidgetT *w, const char *text); -const char *widgetRadioGetText(const WidgetT *w); -void widgetRadioSetText(WidgetT *w, const char *text); -const char *widgetTextAreaGetText(const WidgetT *w); -void widgetTextAreaSetText(WidgetT *w, const char *text); -const char *widgetTextInputGetText(const WidgetT *w); -void widgetTextInputSetText(WidgetT *w, const char *text); -const char *widgetTreeItemGetText(const WidgetT *w); -void widgetTreeItemSetText(WidgetT *w, const char *text); - -// ============================================================ -// Per-widget destroy functions -// ============================================================ - -void widgetAnsiTermDestroy(WidgetT *w); -void widgetCanvasDestroy(WidgetT *w); -void widgetComboBoxDestroy(WidgetT *w); -void widgetImageButtonDestroy(WidgetT *w); -void widgetImageDestroy(WidgetT *w); -void widgetListBoxDestroy(WidgetT *w); -void widgetListViewDestroy(WidgetT *w); -void widgetTextAreaDestroy(WidgetT *w); -void widgetTextInputDestroy(WidgetT *w); - -// ============================================================ -// Per-widget mouse/key functions -// ============================================================ -// -// Shared text editing helpers used by TextInput, TextArea, ComboBox, and -// Spinner. The widgetTextEditOnKey function implements the common subset -// of single-line text editing behavior (cursor movement, selection, -// copy/paste, undo) shared across all text-editing widgets, so changes -// to editing behavior only need to happen in one place. - -// Clear text selections in all widgets except the given one (ensures only -// one text selection is active at a time across the entire GUI). -void clearOtherSelections(WidgetT *except); - -void clipboardCopy(const char *text, int32_t len); -const char *clipboardGet(int32_t *outLen); -bool isWordChar(char c); - -// Detect double/triple clicks based on timing and position proximity. -// Returns 1 for single, 2 for double, 3 for triple click. -int32_t multiClickDetect(int32_t vx, int32_t vy); -void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetAnsiTermOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetCanvasOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetDropdownOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetDropdownOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetImageButtonOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetImageButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetImageOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetListBoxOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetListBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetListViewOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetListViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetRadioOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetRadioOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetScrollPaneOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetScrollPaneOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetSplitterClampPos(WidgetT *w, int32_t *pos); -void widgetSplitterOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetSliderOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetSliderOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetSpinnerOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetSpinnerOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -const char *widgetSpinnerGetText(const WidgetT *w); -void widgetSpinnerSetText(WidgetT *w, const char *text); -void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetTabControlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetTextAreaOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetTextAreaOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -void widgetTextDragUpdate(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -// Shared single-line text editing logic. Takes pointers to all the edit -// state fields so the same code works for TextInput, ComboBox, and Spinner -// (which store these fields in different union members). This avoids -// duplicating the full editing logic (cursor movement, word selection, -// clipboard, undo) three times. -void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd); -void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect); -void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); -void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX); -void widgetTextInputOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetTextInputOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); -int32_t wordEnd(const char *buf, int32_t len, int32_t pos); -int32_t wordStart(const char *buf, int32_t pos); -void widgetTreeViewOnKey(WidgetT *w, int32_t key, int32_t mod); -void widgetTreeViewOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); - -// Timer internals -void widgetTimerCalcMinSize(WidgetT *w, const BitmapFontT *font); -void widgetTimerDestroy(WidgetT *w); - -// Drag-reorder helpers -- generic drag-and-drop reordering shared by -// ListBox, ListView, and TreeView. The update function tracks the mouse -// position and computes the drop target, the drop function commits the -// reorder by relinking the item in the data structure. -void widgetReorderUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y); -void widgetReorderDrop(WidgetT *w); - -// Iterate to the next visible item in a tree view (skipping collapsed -// subtrees). Used for keyboard navigation and rendering. -WidgetT *widgetTreeViewNextVisible(WidgetT *item, WidgetT *treeView); - -#endif // WIDGET_INTERNAL_H diff --git a/core/widgetLayout.c b/core/widgetLayout.c index d029efe6..6d57d10f 100644 --- a/core/widgetLayout.c +++ b/core/widgetLayout.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetLayout.c -- Layout engine (measure + arrange) // // Implements a two-pass layout algorithm inspired by CSS flexbox: @@ -26,7 +27,7 @@ // unit types into a single int32_t without needing a separate struct. // The wgtResolveSize() function decodes the tag and converts to pixels. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" // ============================================================ @@ -58,21 +59,12 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) { gap = DEFAULT_SPACING; } - // Frame reserves extra vertical space at the top so the frame title - // text can sit centered on the top border line (like Windows group boxes). - // Without this, the first child would overlap the title. + // Widgets with getLayoutMetrics override default padding/gap/extraTop int32_t frameExtraTop = 0; - if (w->type == WidgetFrameE) { - frameExtraTop = font->charHeight / 2; - pad = DEFAULT_PADDING; - } - - // Toolbar and StatusBar use minimal padding to pack controls tightly, - // matching the compact feel of classic Windows/Motif toolbars. - if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) { - pad = TOOLBAR_PAD; - gap = TOOLBAR_GAP; + if (w->wclass && w->wclass->getLayoutMetrics) { + int32_t metricBorderW = 0; + w->wclass->getLayoutMetrics(w, font, &pad, &gap, &frameExtraTop, &metricBorderW); } for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { @@ -110,8 +102,8 @@ void widgetCalcMinSizeBox(WidgetT *w, const BitmapFontT *font) { w->calcMinH = mainSize + frameExtraTop; } - // Frame border - if (w->type == WidgetFrameE) { + // Border (Frame and similar containers with getLayoutMetrics) + if (w->wclass && w->wclass->getLayoutMetrics) { int32_t fb = widgetFrameBorderWidth(w); w->calcMinW += fb * 2; w->calcMinH += fb * 2; @@ -205,20 +197,12 @@ void widgetLayoutBox(WidgetT *w, const BitmapFontT *font) { gap = DEFAULT_SPACING; } - // Frame adjustments + // Widgets with getLayoutMetrics override default padding/gap/extraTop/border int32_t frameExtraTop = 0; int32_t fb = 0; - if (w->type == WidgetFrameE) { - frameExtraTop = font->charHeight / 2; - fb = widgetFrameBorderWidth(w); - pad = DEFAULT_PADDING; - } - - // Toolbar and StatusBar use tighter padding - if (w->type == WidgetToolbarE || w->type == WidgetStatusBarE) { - pad = TOOLBAR_PAD; - gap = TOOLBAR_GAP; + if (w->wclass && w->wclass->getLayoutMetrics) { + w->wclass->getLayoutMetrics(w, font, &pad, &gap, &frameExtraTop, &fb); } int32_t innerX = w->x + pad + fb; diff --git a/core/widgetOps.c b/core/widgetOps.c index 14b0156a..02799379 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetOps.c -- Paint dispatcher and public widget operations // // This file contains two categories of functions: @@ -10,7 +11,8 @@ // because they share the same invalidation infrastructure (wgtInvalidate // and wgtInvalidatePaint). -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "../widgets/widgetBox.h" // ============================================================ @@ -153,11 +155,11 @@ void wgtDestroy(WidgetT *w) { return; } - // Invalidate tree dimension cache if destroying a tree item - if (w->type == WidgetTreeItemE && w->parent) { + // Notify parent chain of child destruction via onChildChanged vtable + if (w->parent) { for (WidgetT *p = w->parent; p; p = p->parent) { - if (p->type == WidgetTreeViewE) { - p->as.treeView.dimsValid = false; + if (p->wclass && p->wclass->onChildChanged) { + p->wclass->onChildChanged(p, w); break; } } @@ -200,43 +202,16 @@ void wgtDestroy(WidgetT *w) { // ============================================================ -// widgetHashName -- djb2 hash for widget name lookup +// wgtFind // ============================================================ -// -// The djb2 hash function (Dan Bernstein) is used for fast name-based -// widget lookup. The hash is stored in each widget's nameHash field -// so that wgtFind() can reject non-matches without calling strcmp() -// on every node. In a tree of 100 widgets, this typically reduces -// the search from 100 strcmp calls to 1-2. -static uint32_t widgetHashName(const char *s) { - uint32_t h = 5381; - - while (*s) { - h = ((h << 5) + h) + (uint8_t)*s; - s++; +static WidgetT *wgtFindImpl(WidgetT *w, const char *name) { + if (w->name[0] && strcmp(w->name, name) == 0) { + return w; } - return h; -} - - -// ============================================================ -// wgtFindByHash -- recursive search with hash fast-reject -// ============================================================ -// -// Depth-first search by name with hash pre-screening. The hash -// comparison (integer ==) is much cheaper than strcmp and eliminates -// most non-matches. A full strcmp is only done when hashes match, -// which should be the target widget or a rare collision. - -static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) { - if (root->nameHash == hash && root->name[0] && strcmp(root->name, name) == 0) { - return root; - } - - for (WidgetT *c = root->firstChild; c; c = c->nextSibling) { - WidgetT *found = wgtFindByHash(c, name, hash); + for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { + WidgetT *found = wgtFindImpl(c, name); if (found) { return found; @@ -246,19 +221,12 @@ static WidgetT *wgtFindByHash(WidgetT *root, const char *name, uint32_t hash) { return NULL; } - -// ============================================================ -// wgtFind -// ============================================================ - WidgetT *wgtFind(WidgetT *root, const char *name) { if (!root || !name) { return NULL; } - uint32_t hash = widgetHashName(name); - - return wgtFindByHash(root, name, hash); + return wgtFindImpl(root, name); } @@ -273,7 +241,6 @@ void wgtSetName(WidgetT *w, const char *name) { strncpy(w->name, name, MAX_WIDGET_NAME - 1); w->name[MAX_WIDGET_NAME - 1] = '\0'; - w->nameHash = widgetHashName(w->name); } @@ -339,7 +306,7 @@ const char *wgtGetText(const WidgetT *w) { // widget function -- any widget can retrieve it via wgtGetContext(). WidgetT *wgtInitWindow(AppContextT *ctx, WindowT *win) { - WidgetT *root = widgetAlloc(NULL, WidgetVBoxE); + WidgetT *root = dvxBoxApi() ? dvxBoxApi()->vBox(NULL) : NULL; if (!root) { return NULL; @@ -566,11 +533,11 @@ void wgtSetVisible(WidgetT *w, bool visible) { if (w) { w->visible = visible; - // Invalidate tree dimension cache if this is a tree item - if (w->type == WidgetTreeItemE && w->parent) { + // Notify parent chain of child visibility change via onChildChanged vtable + if (w->parent) { for (WidgetT *p = w->parent; p; p = p->parent) { - if (p->type == WidgetTreeViewE) { - p->as.treeView.dimsValid = false; + if (p->wclass && p->wclass->onChildChanged) { + p->wclass->onChildChanged(p, w); break; } } diff --git a/core/widgetScrollbar.c b/core/widgetScrollbar.c index ccb2cb04..ef14f5af 100644 --- a/core/widgetScrollbar.c +++ b/core/widgetScrollbar.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetScrollbar.c -- Shared scrollbar painting and hit-testing // // These are not widgets themselves -- they are stateless rendering and @@ -20,17 +21,7 @@ // If the container is too small, the scrollbar is simply not drawn // rather than rendering a corrupted mess. -#include "widgetInternal.h" - -// Constants duplicated from widgetTextInput.c and widgetScrollPane.c -// for use in widgetScrollbarDragUpdate. These are file-local in their -// source files, so we repeat the values here to avoid cross-file -// coupling. They must stay in sync with the originals. -#define TEXTAREA_BORDER 2 -#define TEXTAREA_PAD 2 -#define TEXTAREA_SB_W 14 -#define SP_BORDER 2 -#define SP_SB_W 14 +#include "dvxWidgetPlugin.h" // ============================================================ @@ -150,395 +141,14 @@ void widgetDrawScrollbarV(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT * // ============================================================ // Handles ongoing scrollbar thumb drag for widget-internal scrollbars. -// Converts the mouse pixel position into a scroll value using linear -// interpolation, matching the WM-level wmScrollbarDrag logic. -// orient: 0=vertical, 1=horizontal. -// dragOff: mouse offset within thumb captured at drag start. -// -// Each widget type stores scroll state differently (row counts vs -// pixel offsets, different struct fields), so this function switches -// on widget type to extract the scrollbar geometry and update the -// correct scroll field. +// Dispatches to the widget's scrollDragUpdate vtable method, which +// handles type-specific geometry extraction and scroll position update. void widgetScrollbarDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) { - // Determine scrollbar geometry per widget type. - // sbOrigin: screen coordinate of the scrollbar's top/left edge - // sbLen: total length of the scrollbar (including arrow buttons) - // totalSize: total content size (items, pixels, or columns) - // visibleSize: visible portion of content - // scrollPos: current scroll position (pointer to update) - // maxScroll: maximum scroll value - int32_t sbOrigin = 0; - int32_t sbLen = 0; - int32_t totalSize = 0; - int32_t visibleSize = 0; - int32_t maxScroll = 0; - int32_t sbWidth = WGT_SB_W; - - if (w->type == WidgetTextAreaE) { - sbWidth = TEXTAREA_SB_W; - - if (orient == 0) { - // Vertical scrollbar - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; - int32_t visCols = innerW / font->charWidth; - int32_t maxLL = 0; - - // Compute max line length by scanning lines - const char *buf = w->as.textArea.buf; - int32_t len = w->as.textArea.len; - int32_t lineStart = 0; - - for (int32_t i = 0; i <= len; i++) { - if (i == len || buf[i] == '\n') { - int32_t ll = i - lineStart; - - if (ll > maxLL) { - maxLL = ll; - } - - lineStart = i + 1; - } - } - - bool needHSb = (maxLL > visCols); - int32_t innerH = w->h - TEXTAREA_BORDER * 2 - (needHSb ? TEXTAREA_SB_W : 0); - int32_t visRows = innerH / font->charHeight; - - if (visRows < 1) { - visRows = 1; - } - - // Count total lines - int32_t totalLines = 1; - - for (int32_t i = 0; i < len; i++) { - if (buf[i] == '\n') { - totalLines++; - } - } - - sbOrigin = w->y + TEXTAREA_BORDER; - sbLen = innerH; - totalSize = totalLines; - visibleSize = visRows; - maxScroll = totalLines - visRows; - } else { - // Horizontal scrollbar - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - int32_t innerW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_PAD * 2 - TEXTAREA_SB_W; - int32_t visCols = innerW / font->charWidth; - int32_t maxLL = 0; - - const char *buf = w->as.textArea.buf; - int32_t len = w->as.textArea.len; - int32_t lineStart = 0; - - for (int32_t i = 0; i <= len; i++) { - if (i == len || buf[i] == '\n') { - int32_t ll = i - lineStart; - - if (ll > maxLL) { - maxLL = ll; - } - - lineStart = i + 1; - } - } - - int32_t hsbW = w->w - TEXTAREA_BORDER * 2 - TEXTAREA_SB_W; - - sbOrigin = w->x + TEXTAREA_BORDER; - sbLen = hsbW; - totalSize = maxLL; - visibleSize = visCols; - maxScroll = maxLL - visCols; - } - } else if (w->type == WidgetListBoxE) { - // Vertical only - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - int32_t innerH = w->h - LISTBOX_BORDER * 2; - int32_t visibleRows = innerH / font->charHeight; - - sbOrigin = w->y + LISTBOX_BORDER; - sbLen = innerH; - totalSize = w->as.listBox.itemCount; - visibleSize = visibleRows; - maxScroll = totalSize - visibleSize; - } else if (w->type == WidgetListViewE) { - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - int32_t headerH = font->charHeight + 4; - int32_t innerH = w->h - LISTVIEW_BORDER * 2 - headerH; - int32_t innerW = w->w - LISTVIEW_BORDER * 2; - int32_t visibleRows = innerH / font->charHeight; - int32_t totalColW = w->as.listView->totalColW; - bool needVSb = (w->as.listView->rowCount > visibleRows); - - if (needVSb) { - innerW -= WGT_SB_W; - } - - if (totalColW > innerW) { - innerH -= WGT_SB_W; - visibleRows = innerH / font->charHeight; - - if (!needVSb && w->as.listView->rowCount > visibleRows) { - innerW -= WGT_SB_W; - } - } - - if (visibleRows < 1) { - visibleRows = 1; - } - - if (orient == 0) { - // Vertical - sbOrigin = w->y + LISTVIEW_BORDER + headerH; - sbLen = innerH; - totalSize = w->as.listView->rowCount; - visibleSize = visibleRows; - maxScroll = totalSize - visibleSize; - } else { - // Horizontal - sbOrigin = w->x + LISTVIEW_BORDER; - sbLen = innerW; - totalSize = totalColW; - visibleSize = innerW; - maxScroll = totalColW - innerW; - } - } else if (w->type == WidgetTreeViewE) { - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - int32_t totalH; - int32_t totalW; - int32_t innerH; - int32_t innerW; - bool needVSb; - - // Walk the visible tree to compute total content dimensions. - // This duplicates treeCalcScrollbarNeeds which is file-static, - // so we compute it inline using the same logic. - int32_t treeH = 0; - int32_t treeW = 0; - - // Count visible items and max width - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (c->type == WidgetTreeItemE && c->visible) { - // Walk visible items - WidgetT *item = c; - - while (item) { - treeH += font->charHeight; - - // Compute depth - int32_t depth = 0; - WidgetT *p = item->parent; - - while (p && p != w) { - depth++; - p = p->parent; - } - - int32_t itemW = depth * TREE_INDENT + TREE_EXPAND_SIZE + TREE_ICON_GAP; - - if (item->as.treeItem.text) { - itemW += (int32_t)strlen(item->as.treeItem.text) * font->charWidth; - } - - if (itemW > treeW) { - treeW = itemW; - } - - item = widgetTreeViewNextVisible(item, w); - } - - break; // Only process first top-level visible chain - } - } - - totalH = treeH; - totalW = treeW; - innerH = w->h - TREE_BORDER * 2; - innerW = w->w - TREE_BORDER * 2; - needVSb = (totalH > innerH); - - if (needVSb) { - innerW -= WGT_SB_W; - } - - if (totalW > innerW) { - innerH -= WGT_SB_W; - - if (!needVSb && totalH > innerH) { - needVSb = true; - innerW -= WGT_SB_W; - } - } - - if (orient == 0) { - // Vertical (pixel-based scroll) - sbOrigin = w->y + TREE_BORDER; - sbLen = innerH; - totalSize = totalH; - visibleSize = innerH; - maxScroll = totalH - innerH; - } else { - // Horizontal (pixel-based scroll) - sbOrigin = w->x + TREE_BORDER; - sbLen = innerW; - totalSize = totalW; - visibleSize = innerW; - maxScroll = totalW - innerW; - } - } else if (w->type == WidgetScrollPaneE) { - sbWidth = SP_SB_W; - - AppContextT *ctx = (AppContextT *)w->window->widgetRoot->userData; - const BitmapFontT *font = &ctx->font; - - // Compute content min size (must match spCalcNeeds in widgetScrollPane.c) - int32_t contentMinW = 0; - int32_t contentMinH = 0; - int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth); - int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth); - int32_t count = 0; - - for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { - if (c->visible) { - if (c->wclass && c->wclass->calcMinSize) { - c->wclass->calcMinSize(c, font); - } - - if (c->calcMinW > contentMinW) { - contentMinW = c->calcMinW; - } - - contentMinH += c->calcMinH; - count++; - } - } - - if (count > 1) { - contentMinH += gap * (count - 1); - } - - contentMinW += pad * 2; - contentMinH += pad * 2; - - int32_t innerH = w->h - SP_BORDER * 2; - int32_t innerW = w->w - SP_BORDER * 2; - bool needVSb = (contentMinH > innerH); - - if (needVSb) { - innerW -= SP_SB_W; - } - - if (contentMinW > innerW) { - innerH -= SP_SB_W; - - if (!needVSb && contentMinH > innerH) { - needVSb = true; - innerW -= SP_SB_W; - } - } - - if (orient == 0) { - // Vertical - sbOrigin = w->y + SP_BORDER; - sbLen = innerH; - totalSize = contentMinH; - visibleSize = innerH; - maxScroll = contentMinH - innerH; - } else { - // Horizontal - sbOrigin = w->x + SP_BORDER; - sbLen = innerW; - totalSize = contentMinW; - visibleSize = innerW; - maxScroll = contentMinW - innerW; - } - } else { - return; - } - - if (maxScroll < 0) { - maxScroll = 0; - } - - if (maxScroll == 0) { - return; - } - - // Compute thumb geometry - int32_t trackLen = sbLen - sbWidth * 2; - - if (trackLen <= 0) { - return; - } - - int32_t thumbPos; - int32_t thumbSize; - widgetScrollbarThumb(trackLen, totalSize, visibleSize, 0, &thumbPos, &thumbSize); - - if (trackLen <= thumbSize) { - return; - } - - // Convert mouse position to scroll value - int32_t mousePos; - - if (orient == 0) { - mousePos = mouseY - sbOrigin - sbWidth - dragOff; - } else { - mousePos = mouseX - sbOrigin - sbWidth - dragOff; - } - - int32_t newScroll = (mousePos * maxScroll) / (trackLen - thumbSize); - - if (newScroll < 0) { - newScroll = 0; - } - - if (newScroll > maxScroll) { - newScroll = maxScroll; - } - - // Update the widget's scroll position - if (w->type == WidgetTextAreaE) { - if (orient == 0) { - w->as.textArea.scrollRow = newScroll; - } else { - w->as.textArea.scrollCol = newScroll; - } - } else if (w->type == WidgetListBoxE) { - w->as.listBox.scrollPos = newScroll; - } else if (w->type == WidgetListViewE) { - if (orient == 0) { - w->as.listView->scrollPos = newScroll; - } else { - w->as.listView->scrollPosH = newScroll; - } - } else if (w->type == WidgetTreeViewE) { - if (orient == 0) { - w->as.treeView.scrollPos = newScroll; - } else { - w->as.treeView.scrollPosH = newScroll; - } - } else if (w->type == WidgetScrollPaneE) { - if (orient == 0) { - w->as.scrollPane.scrollPosV = newScroll; - } else { - w->as.scrollPane.scrollPosH = newScroll; - } + // Dispatch to the widget's scrollDragUpdate vtable method, which + // handles type-specific geometry extraction, thumb computation, + // and scroll position update. + if (w->wclass && w->wclass->scrollDragUpdate) { + w->wclass->scrollDragUpdate(w, orient, dragOff, mouseX, mouseY); } } diff --git a/listhelp/Makefile b/listhelp/Makefile new file mode 100644 index 00000000..ba73ed89 --- /dev/null +++ b/listhelp/Makefile @@ -0,0 +1,42 @@ +# List Help Library Makefile for DJGPP cross-compilation +# +# Builds listhelp.lib -- shared list/dropdown helper infrastructure +# (dropdown arrow, item length, keyboard navigation, popup list painting). + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -I../core -I../core/platform -I../tasks -I../core/thirdparty + +OBJDIR = ../obj/listhelp +LIBSDIR = ../bin/libs + +SRCS = listHelp.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(LIBSDIR)/listhelp.lib + +.PHONY: all clean + +all: $(TARGET) $(LIBSDIR)/listhelp.dep + +$(LIBSDIR)/listhelp.dep: ../config/listhelp.dep | $(LIBSDIR) + sed 's/$$/\r/' $< > $@ + +$(TARGET): $(OBJS) | $(LIBSDIR) + $(DXE3GEN) -o $(LIBSDIR)/listhelp.dxe -E _widgetDraw -E _widgetDropdown -E _widgetMax -E _widgetNavigate -E _widgetPaint -U $(OBJS) + mv $(LIBSDIR)/listhelp.dxe $@ + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(LIBSDIR): + mkdir -p $(LIBSDIR) + +# Dependencies +$(OBJDIR)/listHelp.o: listHelp.c listHelp.h ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h + +clean: + rm -f $(OBJS) $(TARGET) $(LIBSDIR)/listhelp.dep diff --git a/listhelp/listHelp.c b/listhelp/listHelp.c new file mode 100644 index 00000000..795b3a91 --- /dev/null +++ b/listhelp/listHelp.c @@ -0,0 +1,187 @@ +// listHelp.c -- Shared list/dropdown helper functions +// +// Implements dropdown arrow drawing, item length scanning, keyboard +// navigation, and popup list painting used by ListBox, Dropdown, +// ComboBox, ListView, and TreeView widgets. + +#include "listHelp.h" +#include "../texthelp/textHelp.h" + +#include + + +// ============================================================ +// widgetDrawDropdownArrow +// ============================================================ +// +// Draws a small downward-pointing filled triangle (7, 5, 3, 1 pixels +// wide across 4 rows) centered at the given position. Used by both +// Dropdown and ComboBox for the drop button arrow glyph. + +void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color) { + for (int32_t i = 0; i < 4; i++) { + drawHLine(d, ops, centerX - 3 + i, centerY + i, 7 - i * 2, color); + } +} + + +// ============================================================ +// widgetMaxItemLen +// ============================================================ +// +// Scans an array of string items and returns the maximum strlen. +// Shared by ListBox, Dropdown, and ComboBox to cache the widest +// item length for calcMinSize without duplicating the loop. + +int32_t widgetMaxItemLen(const char **items, int32_t count) { + int32_t maxLen = 0; + + for (int32_t i = 0; i < count; i++) { + int32_t slen = (int32_t)strlen(items[i]); + + if (slen > maxLen) { + maxLen = slen; + } + } + + return maxLen; +} + + +// ============================================================ +// widgetNavigateIndex +// ============================================================ +// +// Shared keyboard navigation for list-like widgets (ListBox, Dropdown, +// ListView, etc.). Encapsulates the Up/Down/Home/End/PgUp/PgDn logic +// so each widget doesn't have to reimplement index clamping. +// +// Key values use the 0x100 flag to mark extended scan codes (arrow +// keys, Home, End, etc.) -- this is the DVX convention for passing +// scan codes through the same int32_t channel as ASCII values. +// +// Returns -1 for unrecognized keys so callers can check whether the +// key was consumed. + +int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize) { + if (key == (0x50 | 0x100)) { + // Down arrow + if (current < count - 1) { + return current + 1; + } + + return current < 0 ? 0 : current; + } + + if (key == (0x48 | 0x100)) { + // Up arrow + if (current > 0) { + return current - 1; + } + + return current < 0 ? 0 : current; + } + + if (key == (0x47 | 0x100)) { + // Home + return 0; + } + + if (key == (0x4F | 0x100)) { + // End + return count - 1; + } + + if (key == (0x51 | 0x100)) { + // Page Down + int32_t n = current + pageSize; + return n >= count ? count - 1 : n; + } + + if (key == (0x49 | 0x100)) { + // Page Up + int32_t n = current - pageSize; + return n < 0 ? 0 : n; + } + + return -1; +} + + +// ============================================================ +// widgetPaintPopupList +// ============================================================ +// +// Shared popup list painting for Dropdown and ComboBox. + +void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos) { + // Draw popup border + BevelStyleT bevel; + bevel.highlight = colors->windowHighlight; + bevel.shadow = colors->windowShadow; + bevel.face = colors->contentBg; + bevel.width = 2; + drawBevel(d, ops, popX, popY, popW, popH, &bevel); + + // Draw items + int32_t visibleItems = popH / font->charHeight; + int32_t textX = popX + TEXT_INPUT_PAD; + int32_t textY = popY + 2; + int32_t textW = popW - TEXT_INPUT_PAD * 2 - 4; + + for (int32_t i = 0; i < visibleItems && (scrollPos + i) < itemCount; i++) { + int32_t idx = scrollPos + i; + int32_t iy = textY + i * font->charHeight; + uint32_t ifg = colors->contentFg; + uint32_t ibg = colors->contentBg; + + if (idx == hoverIdx) { + ifg = colors->menuHighlightFg; + ibg = colors->menuHighlightBg; + rectFill(d, ops, popX + 2, iy, textW + TEXT_INPUT_PAD * 2, font->charHeight, ibg); + } + + drawText(d, ops, font, textX, iy, items[idx], ifg, ibg, false); + } +} + + +// ============================================================ +// widgetDropdownPopupRect +// ============================================================ +// +// Calculates the screen rectangle for a dropdown/combobox popup list. +// Shared between Dropdown and ComboBox since they have identical +// popup positioning logic. + +void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { + int32_t itemCount = 0; + + if (w->wclass && w->wclass->getPopupItemCount) { + itemCount = w->wclass->getPopupItemCount(w); + } + + int32_t visibleItems = itemCount; + + if (visibleItems > DROPDOWN_MAX_VISIBLE) { + visibleItems = DROPDOWN_MAX_VISIBLE; + } + + if (visibleItems < 1) { + visibleItems = 1; + } + + *popX = w->x; + *popW = w->w; + *popH = visibleItems * font->charHeight + 4; + + if (w->y + w->h + *popH <= contentH) { + *popY = w->y + w->h; + } else { + *popY = w->y - *popH; + + if (*popY < 0) { + *popY = 0; + } + } +} diff --git a/listhelp/listHelp.h b/listhelp/listHelp.h new file mode 100644 index 00000000..c7001d3a --- /dev/null +++ b/listhelp/listHelp.h @@ -0,0 +1,45 @@ +// listHelp.h -- Public API for the shared list/dropdown helper library +// +// Declares dropdown arrow drawing, item length scanning, keyboard +// navigation, and popup list painting shared across widget DXEs +// (ListBox, Dropdown, ComboBox, ListView, TreeView). + +#ifndef LIST_HELP_H +#define LIST_HELP_H + +#include "../core/dvxWidgetPlugin.h" + +#define DROPDOWN_BTN_WIDTH 16 +#define DROPDOWN_MAX_VISIBLE 8 + +// ============================================================ +// Dropdown arrow glyph +// ============================================================ + +void widgetDrawDropdownArrow(DisplayT *d, const BlitOpsT *ops, int32_t centerX, int32_t centerY, uint32_t color); + +// ============================================================ +// Item measurement +// ============================================================ + +int32_t widgetMaxItemLen(const char **items, int32_t count); + +// ============================================================ +// Keyboard navigation (Up/Down/Home/End/PgUp/PgDn) +// ============================================================ + +int32_t widgetNavigateIndex(int32_t key, int32_t current, int32_t count, int32_t pageSize); + +// ============================================================ +// Popup rect calculation +// ============================================================ + +void widgetDropdownPopupRect(WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH); + +// ============================================================ +// Popup list painting +// ============================================================ + +void widgetPaintPopupList(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t popX, int32_t popY, int32_t popW, int32_t popH, const char **items, int32_t itemCount, int32_t hoverIdx, int32_t scrollPos); + +#endif // LIST_HELP_H diff --git a/loader/Makefile b/loader/Makefile index 9ce538d2..b118c867 100644 --- a/loader/Makefile +++ b/loader/Makefile @@ -9,7 +9,7 @@ DJGPP_LIBPATH = $(HOME)/claude/windriver/tools/lib CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc EXE2COFF = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/exe2coff CWSDSTUB = $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/CWSDSTUB.EXE -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../tasks/thirdparty +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../core/thirdparty LDFLAGS = -lm OBJDIR = ../obj/loader diff --git a/loader/loaderMain.c b/loader/loaderMain.c index b63a8d34..73dcc0ec 100644 --- a/loader/loaderMain.c +++ b/loader/loaderMain.c @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,26 @@ #define LIBS_DIR "LIBS" #define WIDGET_DIR "WIDGETS" +#define LOG_PATH "dvx.log" + +// ============================================================ +// loaderLog -- write to dvx.log (before shell is loaded) +// ============================================================ + +static void loaderLog(const char *fmt, ...) { + FILE *f = fopen(LOG_PATH, "a"); + + if (!f) { + return; + } + + va_list ap; + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + fprintf(f, "\n"); + fclose(f); +} // ============================================================ // Module entry for dependency resolution @@ -49,8 +70,10 @@ typedef struct { static bool allDepsLoaded(const ModuleT *mod, const ModuleT *mods); static void extractBaseName(const char *path, const char *ext, char *out, int32_t outSize); static void *findSymbol(void **handles, const char *symbol); +static void freeMods(ModuleT *mods); static void **loadAllModules(void); static void loadInOrder(ModuleT *mods); +static void logAndReadDeps(ModuleT *mods); static void readDeps(ModuleT *mod); static void scanDir(const char *dirPath, const char *ext, ModuleT **mods); @@ -246,14 +269,25 @@ static void loadInOrder(ModuleT *mods) { continue; } + loaderLog("Loading: %s", mods[i].path); mods[i].handle = dlopen(mods[i].path, RTLD_GLOBAL); - if (mods[i].handle) { - RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); + if (!mods[i].handle) { + const char *err = dlerror(); + loaderLog(" FAILED: %s", err ? err : "(unknown)"); + mods[i].loaded = true; + loaded++; + progress = true; + continue; + } - if (regFn) { - regFn(); - } + loaderLog(" OK"); + + RegFnT regFn = (RegFnT)dlsym(mods[i].handle, "_wgtRegister"); + + if (regFn) { + loaderLog(" Calling wgtRegister()"); + regFn(); } mods[i].loaded = true; @@ -280,31 +314,28 @@ static void loadInOrder(ModuleT *mods) { // // Returns a stb_ds dynamic array of dlopen handles. -static void **loadAllModules(void) { - ModuleT *mods = NULL; +static void logAndReadDeps(ModuleT *mods) { + loaderLog("Discovered %d modules:", arrlen(mods)); - // Discover all modules - scanDir(LIBS_DIR, ".lib", &mods); - scanDir(WIDGET_DIR, ".wgt", &mods); + for (int32_t i = 0; i < arrlen(mods); i++) { + loaderLog(" [%d] %s (base: %s)", i, mods[i].path, mods[i].baseName); + } - // Read dependency files for (int32_t i = 0; i < arrlen(mods); i++) { readDeps(&mods[i]); - } - // Load in dependency order - loadInOrder(mods); + if (arrlen(mods[i].deps) > 0) { + loaderLog(" %s deps:", mods[i].baseName); - // Collect handles - void **handles = NULL; - - for (int32_t i = 0; i < arrlen(mods); i++) { - if (mods[i].handle) { - arrput(handles, mods[i].handle); + for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { + loaderLog(" %s", mods[i].deps[d]); + } } } +} - // Free dep arrays + +static void freeMods(ModuleT *mods) { for (int32_t i = 0; i < arrlen(mods); i++) { for (int32_t d = 0; d < arrlen(mods[i].deps); d++) { free(mods[i].deps[d]); @@ -314,6 +345,40 @@ static void **loadAllModules(void) { } arrfree(mods); +} + + +static void **loadAllModules(void) { + void **handles = NULL; + + // Phase 1: load libraries in dependency order (libs before widgets) + ModuleT *libs = NULL; + scanDir(LIBS_DIR, ".lib", &libs); + logAndReadDeps(libs); + loadInOrder(libs); + + for (int32_t i = 0; i < arrlen(libs); i++) { + if (libs[i].handle) { + arrput(handles, libs[i].handle); + } + } + + freeMods(libs); + + // Phase 2: load widgets in dependency order (all libs already loaded) + ModuleT *widgets = NULL; + scanDir(WIDGET_DIR, ".wgt", &widgets); + logAndReadDeps(widgets); + loadInOrder(widgets); + + for (int32_t i = 0; i < arrlen(widgets); i++) { + if (widgets[i].handle) { + arrput(handles, widgets[i].handle); + } + } + + freeMods(widgets); + return handles; } @@ -353,8 +418,18 @@ int main(int argc, char *argv[]) { platformChdir(exeDir); } + // Truncate log, then use append-per-write + FILE *logInit = fopen(LOG_PATH, "w"); + + if (logInit) { + fclose(logInit); + } + + loaderLog("DVX Loader starting..."); + // Register platform + libc/libm/runtime symbols for DXE resolution platformRegisterDxeExports(); + loaderLog("Platform exports registered."); // Load all modules from libs/ and widgets/ in dependency order. // Each module may have a .dep file specifying load-before deps. @@ -372,7 +447,7 @@ int main(int argc, char *argv[]) { ShellMainFnT shellMain = (ShellMainFnT)findSymbol(handles, "_shellMain"); if (!shellMain) { - fprintf(stderr, "No module exports shellMain\n"); + loaderLog("ERROR: No module exports shellMain"); for (int32_t i = arrlen(handles) - 1; i >= 0; i--) { dlclose(handles[i]); diff --git a/shell/Makefile b/shell/Makefile index ba4dfe9b..60f042d2 100644 --- a/shell/Makefile +++ b/shell/Makefile @@ -5,7 +5,7 @@ DJGPP_PREFIX = $(HOME)/djgpp/djgpp CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../tasks/thirdparty +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../widgets -I../tasks -I../core/thirdparty OBJDIR = ../obj/shell LIBSDIR = ../bin/libs @@ -28,7 +28,8 @@ $(LIBSDIR)/dvxshell.dep: ../config/dvxshell.dep | $(LIBSDIR) sed 's/$$/\r/' $< > $@ $(TARGET): $(OBJS) | $(LIBSDIR) - $(DXE3GEN) -o $@ -E _shell -U $(OBJS) + $(DXE3GEN) -o $(LIBSDIR)/dvxshell.dxe -E _shell -U $(OBJS) + mv $(LIBSDIR)/dvxshell.dxe $@ $(CONFIGDIR)/dvx.ini: ../config/dvx.ini | $(CONFIGDIR) sed 's/$$/\r/' $< > $@ diff --git a/shell/shellApp.c b/shell/shellApp.c index 3638dfb4..fae39d7d 100644 --- a/shell/shellApp.c +++ b/shell/shellApp.c @@ -6,6 +6,7 @@ #include "shellApp.h" #include "dvxDialog.h" #include "dvxPlatform.h" +#include "stb_ds.h" #include #include @@ -18,13 +19,12 @@ // Module state // ============================================================ -// Fixed-size app slot table. Using a static array (not dynamic) because: -// 1. The number of concurrent apps is bounded by available DOS memory -// 2. App IDs are slot indices, so a fixed array gives O(1) lookup -// 3. No fragmentation concerns from repeated malloc/free cycles -// Slot 0 is reserved (represents the shell itself); apps use slots 1..31. -static ShellAppT sApps[SHELL_MAX_APPS]; -int32_t sCurrentAppId = 0; +// Dynamic app slot table (stb_ds array). App IDs are array indices. +// Slot 0 is reserved (represents the shell itself); apps use slots 1+. +// New slots are appended as needed; freed slots are recycled. +static ShellAppT *sApps = NULL; +static int32_t sAppsCap = 0; // number of slots allocated (including slot 0) +int32_t sCurrentAppId = 0; // ============================================================ // Prototypes @@ -52,15 +52,20 @@ void shellTerminateAllApps(AppContextT *ctx); // Find the first free slot, starting at 1 (slot 0 is the shell). // Returns the slot index which also serves as the app's unique ID. -// This linear scan is fine for 32 slots. +// If no free slot exists, appends a new one. static int32_t allocSlot(void) { - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < arrlen(sApps); i++) { if (sApps[i].state == AppStateFreeE) { return i; } } - return -1; + // No free slot -- grow the array + ShellAppT newSlot; + memset(&newSlot, 0, sizeof(newSlot)); + arrput(sApps, newSlot); + + return arrlen(sApps) - 1; } @@ -144,7 +149,7 @@ static int32_t copyFile(const char *src, const char *dst) { // Find an active slot with the given DXE path. Returns the slot pointer, // or NULL if no running app was loaded from this path. static ShellAppT *findLoadedPath(const char *path) { - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < arrlen(sApps); i++) { if (sApps[i].state != AppStateFreeE && strcmp(sApps[i].path, path) == 0) { return &sApps[i]; } @@ -194,7 +199,11 @@ static int32_t makeTempPath(const char *origPath, int32_t id, char *out, int32_t // ============================================================ void shellAppInit(void) { - memset(sApps, 0, sizeof(sApps)); + // Seed slot 0 (reserved for the shell) + ShellAppT slot0; + memset(&slot0, 0, sizeof(slot0)); + arrput(sApps, slot0); + sAppsCap = 1; } @@ -255,7 +264,7 @@ void shellForceKillApp(AppContextT *ctx, ShellAppT *app) { ShellAppT *shellGetApp(int32_t appId) { - if (appId < 1 || appId >= SHELL_MAX_APPS) { + if (appId < 1 || appId >= arrlen(sApps)) { return NULL; } @@ -508,7 +517,7 @@ void shellReapApp(AppContextT *ctx, ShellAppT *app) { bool shellReapApps(AppContextT *ctx) { bool reaped = false; - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < arrlen(sApps); i++) { if (sApps[i].state == AppStateTerminatingE) { shellReapApp(ctx, &sApps[i]); reaped = true; @@ -519,10 +528,15 @@ bool shellReapApps(AppContextT *ctx) { } +int32_t shellAppSlotCount(void) { + return arrlen(sApps); +} + + int32_t shellRunningAppCount(void) { int32_t count = 0; - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < arrlen(sApps); i++) { if (sApps[i].state == AppStateRunningE || sApps[i].state == AppStateLoadedE) { count++; } @@ -533,7 +547,7 @@ int32_t shellRunningAppCount(void) { void shellTerminateAllApps(AppContextT *ctx) { - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < arrlen(sApps); i++) { if (sApps[i].state != AppStateFreeE) { shellForceKillApp(ctx, &sApps[i]); } diff --git a/shell/shellApp.h b/shell/shellApp.h index 94fd89b4..d674a984 100644 --- a/shell/shellApp.h +++ b/shell/shellApp.h @@ -34,10 +34,6 @@ // ============================================================ #define SHELL_APP_NAME_MAX 64 -// Fixed array size for app slots. A small fixed limit avoids dynamic -// allocation complexity and matches the practical limit of a DOS desktop. -// Slot 0 is reserved for the shell itself, so 31 app slots are usable. -#define SHELL_MAX_APPS 32 // Every DXE app exports a global AppDescriptorT named "appDescriptor". // The shell reads it at load time to determine how to launch the app. @@ -148,6 +144,9 @@ void shellTerminateAllApps(AppContextT *ctx); // Get app slot by ID (returns NULL if invalid/free) ShellAppT *shellGetApp(int32_t appId); +// Total number of app slots (for iteration bounds) +int32_t shellAppSlotCount(void); + // Count running apps (not counting the shell itself) int32_t shellRunningAppCount(void); diff --git a/shell/shellExport.c b/shell/shellExport.c index 0b809090..000e5cef 100644 --- a/shell/shellExport.c +++ b/shell/shellExport.c @@ -1,26 +1,26 @@ // shellExport.c -- DXE wrapper overrides for resource tracking // -// Registers three wrapper functions via dlregsym that override the -// real dvxCreateWindow, dvxCreateWindowCentered, and dvxDestroyWindow -// for all subsequently loaded app DXEs. +// Registers three wrapper functions that override the real +// dvxCreateWindow, dvxCreateWindowCentered, and dvxDestroyWindow +// for all subsequently loaded app modules. // -// The key mechanic: dlregsym takes precedence over RTLD_GLOBAL exports. -// Since libdvx.dxe (which has the real functions) was loaded before -// shellExportInit() registers these wrappers, libdvx.dxe keeps the -// real implementations. But any app DXE loaded afterward gets the +// The key mechanic: symbol overrides registered via +// platformRegisterSymOverrides take precedence over previously loaded +// symbols. Since libdvx (which has the real functions) was loaded +// before shellExportInit() registers these wrappers, libdvx keeps the +// real implementations. But any app module loaded afterward gets the // wrappers, which add resource tracking (appId stamping, last-window // reaping) transparently. // // All other symbol exports (dvx*, wgt*, platform*, libc) are handled -// by the DXE modules and the loader's dlregsym table -- they no longer -// need to be listed here. +// by the loaded modules and the platform layer's export table -- they +// no longer need to be listed here. #include "shellApp.h" #include "dvxApp.h" +#include "dvxPlatform.h" #include "dvxWm.h" -#include - // ============================================================ // Prototypes // ============================================================ @@ -33,7 +33,7 @@ static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win); // Wrapper: dvxCreateWindow -- stamps win->appId // ============================================================ -// The wrapper calls the real dvxCreateWindow (resolved from libdvx.dxe +// The wrapper calls the real dvxCreateWindow (resolved from libdvx // at shellcore load time), then tags the result with sCurrentAppId. // This is how the shell knows which app owns which window, enabling // per-app window cleanup on crash/termination. @@ -101,26 +101,19 @@ static void shellWrapDestroyWindow(AppContextT *ctx, WindowT *win) { } -// ============================================================ -// Wrapper export table -// ============================================================ - -// Only three entries: the resource-tracking wrappers exported under -// the original function names. All other symbols come from the -// loaded DXE modules (via RTLD_GLOBAL) and the loader's dlregsym. -DXE_EXPORT_TABLE(sWrapperTable) - { "_dvxCreateWindow", (void *)shellWrapCreateWindow }, - { "_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered }, - { "_dvxDestroyWindow", (void *)shellWrapDestroyWindow }, -DXE_EXPORT_END - - // ============================================================ // shellExportInit // ============================================================ // Register the wrapper overrides. Must be called before any -// shellLoadApp() -- wrappers only affect subsequently loaded DXEs. +// shellLoadApp() -- wrappers only affect subsequently loaded modules. void shellExportInit(void) { - dlregsym(sWrapperTable); + static const PlatformSymOverrideT sOverrides[] = { + {"_dvxCreateWindow", (void *)shellWrapCreateWindow}, + {"_dvxCreateWindowCentered", (void *)shellWrapCreateWindowCentered}, + {"_dvxDestroyWindow", (void *)shellWrapDestroyWindow}, + {NULL, NULL} + }; + + platformRegisterSymOverrides(sOverrides); } diff --git a/shell/shellMain.c b/shell/shellMain.c index 8d2a113e..0aaf2bd4 100644 --- a/shell/shellMain.c +++ b/shell/shellMain.c @@ -16,11 +16,13 @@ // apps were reaped, so it can refresh its task list. // // Crash recovery uses setjmp/longjmp: -// The shell installs signal handlers for SIGSEGV, SIGFPE, SIGILL. If a -// crash occurs in an app task, the handler longjmps back to the setjmp -// point in main(). This works because longjmp restores the main task's -// stack frame regardless of which task was running. tsRecoverToMain() -// then fixes the scheduler's bookkeeping, and the crashed app is killed. +// The platform layer installs signal handlers for SIGSEGV, SIGFPE, SIGILL +// via platformInstallCrashHandler(). If a crash occurs in an app task, the +// handler logs platform-specific diagnostics (register dump on DJGPP), +// then longjmps back to the setjmp point in main(). This works because +// longjmp restores the main task's stack frame regardless of which task +// was running. tsRecoverToMain() then fixes the scheduler's bookkeeping, +// the shell logs app-specific info, and the crashed app is killed. // This gives the shell Windows 3.1-style fault tolerance -- one bad app // doesn't take down the whole system. @@ -34,12 +36,8 @@ #include #include -#include #include #include -// DJGPP-specific: provides __djgpp_exception_state_ptr for accessing -// CPU register state at the point of the exception -#include // ============================================================ // Module state @@ -61,37 +59,9 @@ static DesktopUpdateFnT *sDesktopUpdateFns = NULL; // Prototypes // ============================================================ -static void crashHandler(int sig); static void idleYield(void *ctx); -static void installCrashHandler(void); -static void logCrash(int sig); static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData); -// ============================================================ -// crashHandler -- catch page faults and other fatal signals -// ============================================================ - -// Signal handler for fatal exceptions. DJGPP uses System V signal -// semantics where the handler is reset to SIG_DFL after each delivery, -// so we must re-install it before doing anything else. -// -// The longjmp is the key to crash recovery: it unwinds whatever stack -// we're on (potentially a crashed app's task stack) and restores the -// main task's stack frame to the setjmp point in main(). This is safe -// because cooperative switching means the main task's stack is always -// intact -- it was cleanly suspended at a yield point. The crashed -// task's stack is abandoned (and later freed by tsKill). -static void crashHandler(int sig) { - logCrash(sig); - - // Re-install handler (DJGPP resets to SIG_DFL after delivery) - signal(sig, crashHandler); - - sCrashSignal = sig; - longjmp(sCrashJmp, 1); -} - - // ============================================================ // shellDesktopUpdate -- notify desktop app of state change // ============================================================ @@ -141,71 +111,6 @@ static void idleYield(void *ctx) { } -// ============================================================ -// installCrashHandler -// ============================================================ - -static void installCrashHandler(void) { - signal(SIGSEGV, crashHandler); - signal(SIGFPE, crashHandler); - signal(SIGILL, crashHandler); -} - - -// ============================================================ -// logCrash -- write exception details to the log -// ============================================================ - -// Dump as much diagnostic info as possible before longjmp destroys the -// crash context. This runs inside the signal handler, so only -// async-signal-safe functions should be used -- but since we're in -// DJGPP (single-threaded DOS), reentrancy isn't a practical concern -// and vfprintf/fflush are safe to call here. -static void logCrash(int sig) { - const char *sigName = "UNKNOWN"; - - if (sig == SIGSEGV) { - sigName = "SIGSEGV (page fault)"; - } else if (sig == SIGFPE) { - sigName = "SIGFPE (floating point exception)"; - } else if (sig == SIGILL) { - sigName = "SIGILL (illegal instruction)"; - } - - shellLog("=== CRASH ==="); - shellLog("Signal: %d (%s)", sig, sigName); - shellLog("Current app ID: %ld", (long)sCurrentAppId); - - if (sCurrentAppId > 0) { - ShellAppT *app = shellGetApp(sCurrentAppId); - - if (app) { - shellLog("App name: %s", app->name); - shellLog("App path: %s", app->path); - shellLog("Has main loop: %s", app->hasMainLoop ? "yes" : "no"); - shellLog("Task ID: %lu", (unsigned long)app->mainTaskId); - } - } else { - shellLog("Crashed in shell (task 0)"); - } - - // __djgpp_exception_state_ptr is a DJGPP extension that captures the - // full CPU register state at the point of the exception. This gives - // us the faulting EIP, stack pointer, and all GPRs -- invaluable for - // post-mortem debugging of app crashes from the log file. - jmp_buf *estate = __djgpp_exception_state_ptr; - - if (estate) { - struct __jmp_buf *regs = &(*estate)[0]; - shellLog("EIP: 0x%08lx CS: 0x%04x", regs->__eip, regs->__cs); - shellLog("EAX: 0x%08lx EBX: 0x%08lx ECX: 0x%08lx EDX: 0x%08lx", regs->__eax, regs->__ebx, regs->__ecx, regs->__edx); - shellLog("ESI: 0x%08lx EDI: 0x%08lx EBP: 0x%08lx ESP: 0x%08lx", regs->__esi, regs->__edi, regs->__ebp, regs->__esp); - shellLog("DS: 0x%04x ES: 0x%04x FS: 0x%04x GS: 0x%04x SS: 0x%04x", regs->__ds, regs->__es, regs->__fs, regs->__gs, regs->__ss); - shellLog("EFLAGS: 0x%08lx", regs->__eflags); - } -} - - static void logVideoMode(int32_t w, int32_t h, int32_t bpp, void *userData) { (void)userData; shellLog(" %ldx%ld %ldbpp", (long)w, (long)h, (long)bpp); @@ -268,14 +173,10 @@ int shellMain(int argc, char *argv[]) { (void)argc; (void)argv; - // Truncate the log file, then use append-per-write so the file - // isn't held open (allows Notepad to read it while the shell runs). + // The loader already truncates and writes boot info to dvx.log. + // We just append from here on, opening/closing per-write so the + // file isn't held open (allows Notepad to read it while the shell runs). sLogPath = "dvx.log"; - FILE *logInit = fopen(sLogPath, "w"); - - if (logInit) { - fclose(logInit); - } shellLog("DVX Shell starting..."); @@ -403,21 +304,22 @@ int shellMain(int argc, char *argv[]) { sCtx.onTitleChange = titleChangeHandler; sCtx.titleChangeCtx = &sCtx; - // Load the desktop app - int32_t desktopId = shellLoadApp(&sCtx, SHELL_DESKTOP_APP); + // Load the desktop app (configurable via [shell] desktop= in dvx.ini) + const char *desktopApp = prefsGetString("shell", "desktop", SHELL_DESKTOP_APP); + int32_t desktopId = shellLoadApp(&sCtx, desktopApp); if (desktopId < 0) { - shellLog("Failed to load desktop app '%s'", SHELL_DESKTOP_APP); + shellLog("Failed to load desktop app '%s'", desktopApp); tsShutdown(); dvxShutdown(&sCtx); return 1; } // Install crash handler after everything is initialized -- if - // initialization itself crashes, we want the default DJGPP behavior + // initialization itself crashes, we want the default behavior // (abort with register dump) rather than our recovery path, because // the system isn't in a recoverable state yet. - installCrashHandler(); + platformInstallCrashHandler(&sCrashJmp, &sCrashSignal, shellLog); shellLog("DVX Shell ready."); @@ -432,6 +334,23 @@ int shellMain(int argc, char *argv[]) { // Fix it before doing anything else so the scheduler is consistent. tsRecoverToMain(); + // Platform handler already logged signal name and register dump. + // Log app-specific info here. + shellLog("Current app ID: %ld", (long)sCurrentAppId); + + if (sCurrentAppId > 0) { + ShellAppT *crashedApp = shellGetApp(sCurrentAppId); + + if (crashedApp) { + shellLog("App name: %s", crashedApp->name); + shellLog("App path: %s", crashedApp->path); + shellLog("Has main loop: %s", crashedApp->hasMainLoop ? "yes" : "no"); + shellLog("Task ID: %lu", (unsigned long)crashedApp->mainTaskId); + } + } else { + shellLog("Crashed in shell (task 0)"); + } + shellLog("Recovering from crash, killing app %ld", (long)sCurrentAppId); if (sCurrentAppId > 0) { diff --git a/shell/shellTaskMgr.c b/shell/shellTaskMgr.c index c0e0fa33..c5739506 100644 --- a/shell/shellTaskMgr.c +++ b/shell/shellTaskMgr.c @@ -8,6 +8,10 @@ #include "shellApp.h" #include "dvxDialog.h" #include "dvxWidget.h" +#include "widgetBox.h" +#include "widgetButton.h" +#include "widgetLabel.h" +#include "widgetListView.h" #include "dvxWm.h" #include "dvxPlatform.h" #include "stb_ds.h" @@ -65,7 +69,7 @@ static TmRowStringsT *sRowStrs = NULL; // dynamic array of per-row strings static ShellAppT *getRunningAppByIndex(int32_t sel) { int32_t idx = 0; - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < shellAppSlotCount(); i++) { ShellAppT *app = shellGetApp(i); if (app && app->state == AppStateRunningE) { @@ -202,7 +206,7 @@ static void refreshTaskList(void) { static int32_t *appIds = NULL; arrsetlen(appIds, 0); - for (int32_t i = 1; i < SHELL_MAX_APPS; i++) { + for (int32_t i = 1; i < shellAppSlotCount(); i++) { ShellAppT *app = shellGetApp(i); if (app && app->state == AppStateRunningE) { diff --git a/tasks/Makefile b/tasks/Makefile index 1bf07062..bbab00ac 100644 --- a/tasks/Makefile +++ b/tasks/Makefile @@ -19,7 +19,8 @@ TARGET = $(LIBSDIR)/libtasks.lib all: $(TARGET) $(TARGET): $(OBJS) | $(LIBSDIR) - $(DXE3GEN) -o $@ -E _ts -E _stbds_ -U $(OBJS) + $(DXE3GEN) -o $(LIBSDIR)/libtasks.dxe -E _ts -U $(OBJS) + mv $(LIBSDIR)/libtasks.dxe $@ $(OBJDIR)/%.o: %.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -31,7 +32,7 @@ $(LIBSDIR): mkdir -p $(LIBSDIR) # Dependencies -$(OBJDIR)/taskswitch.o: taskswitch.c taskswitch.h thirdparty/stb_ds.h +$(OBJDIR)/taskswitch.o: taskswitch.c taskswitch.h ../core/thirdparty/stb_ds.h clean: rm -f $(OBJS) $(TARGET) diff --git a/tasks/demo.c b/tasks/demo.c deleted file mode 100644 index 58e9e7ae..00000000 --- a/tasks/demo.c +++ /dev/null @@ -1,144 +0,0 @@ -// demo.c -- Demonstration of the cooperative task switching library -// -// Shows priority scheduling, round-robin, pausing/resuming, dynamic -// priority changes, and slot reuse after task termination. -// -// This is a standalone test harness that runs on the host (Linux x86_64) -// or DJGPP target to verify the task switcher without the GUI stack. -// Each phase exercises a different scheduler feature and prints output -// so you can verify the scheduling order visually. - -#include -#include "taskswitch.h" - -// Forward declarations -static void pauseTarget(void *arg); -static void shortLived(void *arg); -static void worker(void *arg); - - -static void pauseTarget(void *arg) { - (void)arg; - for (int32_t i = 0; i < 10; i++) { - printf(" [pauser] iteration %d\n", (int)i); - tsYield(); - } - printf(" [pauser] finished\n"); -} - - -static void shortLived(void *arg) { - int32_t id = (int32_t)(intptr_t)arg; - printf(" [short-%d] running (id=%d)\n", (int)id, (int)tsCurrentId()); - tsYield(); - printf(" [short-%d] done\n", (int)id); -} - - -static void worker(void *arg) { - const char *name = (const char *)arg; - int32_t pri = tsGetPriority(tsCurrentId()); - - for (int32_t i = 0; i < 5; i++) { - printf(" [%s] step %d (priority %d)\n", name, (int)i, (int)pri); - tsYield(); - } - printf(" [%s] finished\n", name); -} - - -int main(void) { - printf("=== Cooperative Task Switching Demo ===\n\n"); - - if (tsInit() != TS_OK) { - printf("ERROR: failed to initialize task system\n"); - return 1; - } - - int32_t tHigh = tsCreate("high", worker, "high", 0, TS_PRIORITY_HIGH); - int32_t tNorm = tsCreate("normal", worker, "normal", 0, TS_PRIORITY_NORMAL); - int32_t tLow = tsCreate("low", worker, "low", 0, TS_PRIORITY_LOW); - int32_t tPauser = tsCreate("pauser", pauseTarget, NULL, 0, TS_PRIORITY_NORMAL); - - if (tHigh < 0 || tNorm < 0 || tLow < 0 || tPauser < 0) { - printf("ERROR: failed to create tasks\n"); - tsShutdown(); - return 1; - } - - printf("Created: high=%d normal=%d low=%d pauser=%d\n\n", (int)tHigh, (int)tNorm, (int)tLow, (int)tPauser); - - // Phase 1: all tasks run, but high-priority tasks get more turns per round. - // Low (priority 0) gets 1 turn, normal (5) gets 6, high (10) gets 11. - // In the output you'll see "high" appearing much more frequently than - // "low", demonstrating the weighted fair-share scheduling. - printf("--- Phase 1: Priority scheduling ---\n"); - for (int32_t i = 0; i < 6; i++) { - printf("[main] yield %d\n", (int)i); - tsYield(); - } - - // Phase 2: pause the pauser task so it stops being scheduled - printf("\n--- Phase 2: Pause 'pauser' ---\n"); - tsPause((uint32_t)tPauser); - printf("[main] pauser state: %d (expect %d=paused)\n", tsGetState((uint32_t)tPauser), TaskStatePaused); - for (int32_t i = 0; i < 4; i++) { - printf("[main] yield %d (pauser is paused)\n", (int)i); - tsYield(); - } - - // Phase 3: resume pauser - printf("\n--- Phase 3: Resume 'pauser' ---\n"); - tsResume((uint32_t)tPauser); - for (int32_t i = 0; i < 4; i++) { - printf("[main] yield %d\n", (int)i); - tsYield(); - } - - // Phase 4: boost low to the highest priority so it gets more turns - printf("\n--- Phase 4: Boost 'low' to highest ---\n"); - tsSetPriority((uint32_t)tLow, TS_PRIORITY_HIGH + 5); - for (int32_t i = 0; i < 6; i++) { - printf("[main] yield %d\n", (int)i); - tsYield(); - } - - // Let remaining tasks finish - printf("\n--- Draining remaining tasks ---\n"); - while (tsActiveCount() > 1) { - tsYield(); - } - - // Phase 5: dynamic creation with slot reuse. - // After all tasks from phases 1-4 have terminated, their slots are free. - // Creating new tasks should recycle those slots (IDs 1-4) rather than - // growing the array. Each "wave" creates 3 tasks, lets them finish, - // then creates 3 more -- the IDs should repeat across waves. - printf("\n--- Phase 5: Slot reuse ---\n"); - printf("[main] active before: %u\n", (unsigned)tsActiveCount()); - - // Create short-lived tasks that will terminate and free their slots - for (int32_t wave = 0; wave < 3; wave++) { - printf("[main] wave %d: creating 3 tasks\n", (int)wave); - int32_t ids[3]; - for (int32_t j = 0; j < 3; j++) { - int32_t tag = wave * 3 + j; - char name[TS_NAME_MAX]; - snprintf(name, sizeof(name), "short-%d", (int)tag); - ids[j] = tsCreate(name, shortLived, (void *)(intptr_t)tag, 0, TS_PRIORITY_NORMAL); - printf("[main] created id=%d\n", (int)ids[j]); - } - - // Let them all finish - while (tsActiveCount() > 1) { - tsYield(); - } - printf("[main] wave %d complete, active=%u\n", (int)wave, (unsigned)tsActiveCount()); - } - - printf("\nActive tasks: %u\n", (unsigned)tsActiveCount()); - printf("=== Demo complete ===\n"); - - tsShutdown(); - return 0; -} diff --git a/tasks/taskswitch.c b/tasks/taskswitch.c index b23e732b..fd88f9f5 100644 --- a/tasks/taskswitch.c +++ b/tasks/taskswitch.c @@ -21,8 +21,7 @@ // scheduler's linear scan. The downside (holes after termination) is // mitigated by slot recycling in findFreeSlot(). -#define STB_DS_IMPLEMENTATION -#include "thirdparty/stb_ds.h" +#include "../core/thirdparty/stb_ds.h" #include "taskswitch.h" #include @@ -219,7 +218,7 @@ static void __attribute__((noinline)) contextSwitch(TaskContextT *save, TaskCont // Find a free (terminated or unallocated) slot in the task array. // Returns the index, or -1 if no free slot exists. // Starts at 1 because slot 0 is always the main task and cannot be reused. -// Linear scan is fine -- SHELL_MAX_APPS caps the practical limit at ~32 tasks. +// Linear scan is fine for the typical task count (tens, not hundreds). static int32_t findFreeSlot(void) { ptrdiff_t count = arrlen(tasks); for (ptrdiff_t i = 1; i < count; i++) { diff --git a/texthelp/Makefile b/texthelp/Makefile new file mode 100644 index 00000000..a51c2feb --- /dev/null +++ b/texthelp/Makefile @@ -0,0 +1,42 @@ +# Text Help Library Makefile for DJGPP cross-compilation +# +# Builds texthelp.lib -- shared text editing infrastructure +# (clipboard, multi-click, word boundaries, single-line editing engine). + +DJGPP_PREFIX = $(HOME)/djgpp/djgpp +CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc +DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I. -I../core -I../core/platform -I../tasks -I../core/thirdparty + +OBJDIR = ../obj/texthelp +LIBSDIR = ../bin/libs + +SRCS = textHelp.c +OBJS = $(patsubst %.c,$(OBJDIR)/%.o,$(SRCS)) +TARGET = $(LIBSDIR)/texthelp.lib + +.PHONY: all clean + +all: $(TARGET) $(LIBSDIR)/texthelp.dep + +$(LIBSDIR)/texthelp.dep: ../config/texthelp.dep | $(LIBSDIR) + sed 's/$$/\r/' $< > $@ + +$(TARGET): $(OBJS) | $(LIBSDIR) + $(DXE3GEN) -o $(LIBSDIR)/texthelp.dxe -E _clipboard -E _clearOther -E _isWordChar -E _multiClick -E _widgetText -E _wordStart -E _wordEnd -U $(OBJS) + mv $(LIBSDIR)/texthelp.dxe $@ + +$(OBJDIR)/%.o: %.c | $(OBJDIR) + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJDIR): + mkdir -p $(OBJDIR) + +$(LIBSDIR): + mkdir -p $(LIBSDIR) + +# Dependencies +$(OBJDIR)/textHelp.o: textHelp.c textHelp.h ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h + +clean: + rm -f $(OBJS) $(TARGET) $(LIBSDIR)/texthelp.dep diff --git a/texthelp/textHelp.c b/texthelp/textHelp.c new file mode 100644 index 00000000..d0613653 --- /dev/null +++ b/texthelp/textHelp.c @@ -0,0 +1,739 @@ +// textHelp.c -- Shared text editing infrastructure +// +// Provides clipboard, multi-click detection, word boundary logic, +// cross-widget selection clearing, and the single-line text editing +// engine. Shared across multiple widget DXEs (TextInput, TextArea, +// ComboBox, Spinner, AnsiTerm) via the texthelp.lib DXE. + +#include "textHelp.h" + +#include +#include + +// ============================================================ +// Static state +// ============================================================ + +// Track the widget that last had an active selection so we can +// clear it in O(1) instead of walking every widget in every window. +static WidgetT *sLastSelectedWidget = NULL; + +// ============================================================ +// Static helper prototypes +// ============================================================ + +static bool clearSelectionOnWidget(WidgetT *w); +static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd); +static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize); +static int32_t wordBoundaryLeft(const char *buf, int32_t pos); +static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos); + + +// ============================================================ +// clearOtherSelections +// ============================================================ +// +// Clears selection on the previously-selected widget (if different +// from the newly-focused one). Validates that the previous widget's +// window is still in the window stack before touching it -- the +// window may have been closed since sLastSelectedWidget was set. +// If the previous widget was in a different window, that window +// gets a full repaint to clear the stale selection highlight. + +void clearOtherSelections(WidgetT *except) { + if (!except || !except->window || !except->window->widgetRoot) { + return; + } + + WidgetT *prev = sLastSelectedWidget; + sLastSelectedWidget = except; + + if (!prev || prev == except) { + return; + } + + // Verify the widget is still alive (its window still in the stack) + WindowT *prevWin = prev->window; + + if (!prevWin) { + return; + } + + AppContextT *ctx = wgtGetContext(except); + + if (!ctx) { + return; + } + + bool found = false; + + for (int32_t i = 0; i < ctx->stack.count; i++) { + if (ctx->stack.windows[i] == prevWin) { + found = true; + break; + } + } + + if (!found) { + return; + } + + if (clearSelectionOnWidget(prev) && prevWin != except->window) { + dvxInvalidateWindow(ctx, prevWin); + } +} + + +static bool clearSelectionOnWidget(WidgetT *w) { + if (w->wclass && w->wclass->clearSelection) { + return w->wclass->clearSelection(w); + } + + return false; +} + + + + +bool isWordChar(char c) { + return isalnum((unsigned char)c) || c == '_'; +} + + +// ============================================================ +// Shared undo/selection helpers +// ============================================================ + +static void textEditDeleteSelection(char *buf, int32_t *pLen, int32_t *pCursor, int32_t *pSelStart, int32_t *pSelEnd) { + int32_t lo = *pSelStart < *pSelEnd ? *pSelStart : *pSelEnd; + int32_t hi = *pSelStart < *pSelEnd ? *pSelEnd : *pSelStart; + + if (lo < 0) { + lo = 0; + } + + if (hi > *pLen) { + hi = *pLen; + } + + if (lo >= hi) { + *pSelStart = -1; + *pSelEnd = -1; + return; + } + + memmove(buf + lo, buf + hi, *pLen - hi + 1); + *pLen -= (hi - lo); + *pCursor = lo; + *pSelStart = -1; + *pSelEnd = -1; +} + + +static void textEditSaveUndo(char *buf, int32_t len, int32_t cursor, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor, int32_t bufSize) { + if (!undoBuf) { + return; + } + + int32_t copyLen = len < bufSize ? len : bufSize - 1; + memcpy(undoBuf, buf, copyLen); + undoBuf[copyLen] = '\0'; + *pUndoLen = copyLen; + *pUndoCursor = cursor; +} + + +// ============================================================ +// widgetTextEditDragUpdateLine +// ============================================================ +// +// Called during mouse drag to extend the selection for single-line +// text widgets. Auto-scrolls when the mouse moves past the visible +// text edges. + +void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd) { + int32_t rightEdge = leftEdge + maxChars * font->charWidth; + + if (vx < leftEdge && *pScrollOff > 0) { + (*pScrollOff)--; + } else if (vx >= rightEdge && *pScrollOff + maxChars < len) { + (*pScrollOff)++; + } + + int32_t relX = vx - leftEdge; + int32_t charPos = relX / font->charWidth + *pScrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > len) { + charPos = len; + } + + *pCursorPos = charPos; + *pSelEnd = charPos; +} + + +// ============================================================ +// widgetTextEditMouseClick +// ============================================================ +// +// Computes cursor position from pixel coordinates, handles multi-click +// (double = word select, triple = select all), and optionally starts +// drag-select. + +void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect) { + int32_t relX = vx - textLeftX; + int32_t charPos = relX / font->charWidth + scrollOff; + + if (charPos < 0) { + charPos = 0; + } + + if (charPos > len) { + charPos = len; + } + + int32_t clicks = multiClickDetect(vx, vy); + + if (clicks >= 3) { + *pSelStart = 0; + *pSelEnd = len; + *pCursorPos = len; + sDragTextSelect = NULL; + return; + } + + if (clicks == 2) { + if (wordSelect && buf) { + int32_t ws = wordStart(buf, charPos); + int32_t we = wordEnd(buf, len, charPos); + *pSelStart = ws; + *pSelEnd = we; + *pCursorPos = we; + } else { + *pSelStart = 0; + *pSelEnd = len; + *pCursorPos = len; + } + + sDragTextSelect = NULL; + return; + } + + // Single click: place cursor + *pCursorPos = charPos; + *pSelStart = charPos; + *pSelEnd = charPos; + sDragTextSelect = dragSelect ? w : NULL; +} + + +// ============================================================ +// widgetTextEditOnKey -- shared single-line text editing logic +// ============================================================ +// +// This is the core single-line text editing engine, parameterized by +// pointer to allow reuse across TextInput, Spinner, and ComboBox. + +void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor) { + bool shift = (mod & KEY_MOD_SHIFT) != 0; + bool hasSel = (pSelStart && pSelEnd && *pSelStart >= 0 && *pSelEnd >= 0 && *pSelStart != *pSelEnd); + int32_t selLo = hasSel ? (*pSelStart < *pSelEnd ? *pSelStart : *pSelEnd) : -1; + int32_t selHi = hasSel ? (*pSelStart < *pSelEnd ? *pSelEnd : *pSelStart) : -1; + + // Clamp selection to buffer bounds + if (hasSel) { + if (selLo < 0) { + selLo = 0; + } + + if (selHi > *pLen) { + selHi = *pLen; + } + + if (selLo >= selHi) { + hasSel = false; + selLo = -1; + selHi = -1; + } + } + + // Ctrl+A -- select all + if (key == 1 && pSelStart && pSelEnd) { + *pSelStart = 0; + *pSelEnd = *pLen; + *pCursor = *pLen; + goto adjustScroll; + } + + // Ctrl+C -- copy + if (key == 3) { + if (hasSel) { + clipboardCopy(buf + selLo, selHi - selLo); + } + + return; + } + + // Ctrl+V -- paste + if (key == 22) { + int32_t cbLen = 0; + const char *cb = clipboardGet(&cbLen); + + if (cbLen > 0) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + if (hasSel) { + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + } + + int32_t canFit = bufSize - 1 - *pLen; + // For single-line, skip newlines in clipboard + int32_t paste = 0; + + for (int32_t i = 0; i < cbLen && paste < canFit; i++) { + if (cb[i] != '\n' && cb[i] != '\r') { + paste++; + } + } + + if (paste > 0) { + int32_t pos = *pCursor; + memmove(buf + pos + paste, buf + pos, *pLen - pos + 1); + + int32_t j = 0; + + for (int32_t i = 0; i < cbLen && j < paste; i++) { + if (cb[i] != '\n' && cb[i] != '\r') { + buf[pos + j] = cb[i]; + j++; + } + } + + *pLen += paste; + *pCursor += paste; + } + + if (w->onChange) { + w->onChange(w); + } + } + + goto adjustScroll; + } + + // Ctrl+X -- cut + if (key == 24) { + if (hasSel) { + clipboardCopy(buf + selLo, selHi - selLo); + + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } + + goto adjustScroll; + } + + // Ctrl+Z -- undo + if (key == 26 && undoBuf && pUndoLen && pUndoCursor) { + // Swap current and undo + char tmpBuf[clipboardMaxLen() + 1]; + int32_t tmpLen = *pLen; + int32_t tmpCursor = *pCursor; + int32_t copyLen = tmpLen < (int32_t)sizeof(tmpBuf) - 1 ? tmpLen : (int32_t)sizeof(tmpBuf) - 1; + + memcpy(tmpBuf, buf, copyLen); + tmpBuf[copyLen] = '\0'; + + int32_t restLen = *pUndoLen < bufSize - 1 ? *pUndoLen : bufSize - 1; + memcpy(buf, undoBuf, restLen); + buf[restLen] = '\0'; + *pLen = restLen; + + int32_t restoreCursor = *pUndoCursor < *pLen ? *pUndoCursor : *pLen; + + // Save old as new undo + int32_t saveLen = copyLen < bufSize - 1 ? copyLen : bufSize - 1; + memcpy(undoBuf, tmpBuf, saveLen); + undoBuf[saveLen] = '\0'; + *pUndoLen = saveLen; + *pUndoCursor = tmpCursor; + + *pCursor = restoreCursor; + + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (w->onChange) { + w->onChange(w); + } + + goto adjustScroll; + } + + if (key >= 32 && key < 127) { + // Printable character + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + if (hasSel) { + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + } + + if (*pLen < bufSize - 1) { + int32_t pos = *pCursor; + memmove(buf + pos + 1, buf + pos, *pLen - pos + 1); + buf[pos] = (char)key; + (*pLen)++; + (*pCursor)++; + + if (w->onChange) { + w->onChange(w); + } + } + } else if (key == 8) { + // Backspace + if (hasSel) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } else if (*pCursor > 0) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + int32_t pos = *pCursor; + memmove(buf + pos - 1, buf + pos, *pLen - pos + 1); + (*pLen)--; + (*pCursor)--; + + if (w->onChange) { + w->onChange(w); + } + } + } else if (key == (0x4B | 0x100)) { + // Left arrow + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + if (*pCursor > 0) { + (*pCursor)--; + } + + *pSelEnd = *pCursor; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (*pCursor > 0) { + (*pCursor)--; + } + } + } else if (key == (0x4D | 0x100)) { + // Right arrow + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + if (*pCursor < *pLen) { + (*pCursor)++; + } + + *pSelEnd = *pCursor; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + if (*pCursor < *pLen) { + (*pCursor)++; + } + } + } else if (key == (0x73 | 0x100)) { + // Ctrl+Left -- word left + int32_t newPos = wordBoundaryLeft(buf, *pCursor); + + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = newPos; + *pSelEnd = newPos; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = newPos; + } + } else if (key == (0x74 | 0x100)) { + // Ctrl+Right -- word right + int32_t newPos = wordBoundaryRight(buf, *pLen, *pCursor); + + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = newPos; + *pSelEnd = newPos; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = newPos; + } + } else if (key == (0x47 | 0x100)) { + // Home + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = 0; + *pSelEnd = 0; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = 0; + } + } else if (key == (0x4F | 0x100)) { + // End + if (shift && pSelStart && pSelEnd) { + if (*pSelStart < 0) { + *pSelStart = *pCursor; + *pSelEnd = *pCursor; + } + + *pCursor = *pLen; + *pSelEnd = *pLen; + } else { + if (pSelStart) { + *pSelStart = -1; + } + + if (pSelEnd) { + *pSelEnd = -1; + } + + *pCursor = *pLen; + } + } else if (key == (0x53 | 0x100)) { + // Delete + if (hasSel) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + textEditDeleteSelection(buf, pLen, pCursor, pSelStart, pSelEnd); + + if (w->onChange) { + w->onChange(w); + } + } else if (*pCursor < *pLen) { + if (undoBuf) { + textEditSaveUndo(buf, *pLen, *pCursor, undoBuf, pUndoLen, pUndoCursor, bufSize); + } + + int32_t pos = *pCursor; + memmove(buf + pos, buf + pos + 1, *pLen - pos); + (*pLen)--; + + if (w->onChange) { + w->onChange(w); + } + } + } else { + return; + } + +adjustScroll: + // Adjust scroll offset to keep cursor visible + { + AppContextT *ctx = wgtGetContext(w); + const BitmapFontT *font = &ctx->font; + int32_t fieldW = w->w; + + if (w->wclass && w->wclass->getTextFieldWidth) { + fieldW = w->wclass->getTextFieldWidth(w); + } + + int32_t visibleChars = (fieldW - TEXT_INPUT_PAD * 2) / font->charWidth; + + if (*pCursor < *pScrollOff) { + *pScrollOff = *pCursor; + } + + if (*pCursor >= *pScrollOff + visibleChars) { + *pScrollOff = *pCursor - visibleChars + 1; + } + } + + wgtInvalidatePaint(w); +} + + +// ============================================================ +// widgetTextEditPaintLine +// ============================================================ +// +// Renders a single line of text with optional selection highlighting +// and a blinking cursor. + +void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX) { + // Normalize selection to low/high + int32_t selLo = -1; + int32_t selHi = -1; + + if (selStart >= 0 && selEnd >= 0 && selStart != selEnd) { + selLo = selStart < selEnd ? selStart : selEnd; + selHi = selStart < selEnd ? selEnd : selStart; + } + + // Map selection to visible range + int32_t visSelLo = selLo - scrollOff; + int32_t visSelHi = selHi - scrollOff; + + if (visSelLo < 0) { visSelLo = 0; } + if (visSelHi > visLen) { visSelHi = visLen; } + + if (selLo >= 0 && visSelLo < visSelHi) { + if (visSelLo > 0) { + drawTextN(d, ops, font, textX, textY, buf, visSelLo, fg, bg, true); + } + + drawTextN(d, ops, font, textX + visSelLo * font->charWidth, textY, buf + visSelLo, visSelHi - visSelLo, colors->menuHighlightFg, colors->menuHighlightBg, true); + + if (visSelHi < visLen) { + drawTextN(d, ops, font, textX + visSelHi * font->charWidth, textY, buf + visSelHi, visLen - visSelHi, fg, bg, true); + } + } else if (visLen > 0) { + drawTextN(d, ops, font, textX, textY, buf, visLen, fg, bg, true); + } + + // Blinking cursor + if (showCursor && sCursorBlinkOn) { + int32_t cursorX = textX + (cursorPos - scrollOff) * font->charWidth; + + if (cursorX >= cursorMinX && cursorX < cursorMaxX) { + drawVLine(d, ops, cursorX, textY, font->charHeight, fg); + } + } +} + + +// ============================================================ +// Word boundary helpers +// ============================================================ + +static int32_t wordBoundaryLeft(const char *buf, int32_t pos) { + if (pos <= 0) { + return 0; + } + + // Skip non-word characters + while (pos > 0 && !isalnum((unsigned char)buf[pos - 1]) && buf[pos - 1] != '_') { + pos--; + } + + // Skip word characters + while (pos > 0 && (isalnum((unsigned char)buf[pos - 1]) || buf[pos - 1] == '_')) { + pos--; + } + + return pos; +} + + +static int32_t wordBoundaryRight(const char *buf, int32_t len, int32_t pos) { + if (pos >= len) { + return len; + } + + // Skip word characters + while (pos < len && (isalnum((unsigned char)buf[pos]) || buf[pos] == '_')) { + pos++; + } + + // Skip non-word characters + while (pos < len && !isalnum((unsigned char)buf[pos]) && buf[pos] != '_') { + pos++; + } + + return pos; +} + + +int32_t wordEnd(const char *buf, int32_t len, int32_t pos) { + while (pos < len && isWordChar(buf[pos])) { + pos++; + } + + return pos; +} + + +int32_t wordStart(const char *buf, int32_t pos) { + while (pos > 0 && isWordChar(buf[pos - 1])) { + pos--; + } + + return pos; +} diff --git a/texthelp/textHelp.h b/texthelp/textHelp.h new file mode 100644 index 00000000..ca9bfa8e --- /dev/null +++ b/texthelp/textHelp.h @@ -0,0 +1,38 @@ +// textHelp.h -- Public API for the shared text editing infrastructure library +// +// Declares clipboard operations, multi-click detection, word boundary +// logic, cross-widget selection clearing, and the single-line text +// editing engine shared across widget DXEs (TextInput, Spinner, +// ComboBox, AnsiTerm, etc.). + +#ifndef TEXT_HELP_H +#define TEXT_HELP_H + +#include "../core/dvxWidgetPlugin.h" + +#define TEXT_INPUT_PAD 3 + +// ============================================================ +// Selection management +// ============================================================ + +void clearOtherSelections(WidgetT *except); + +// ============================================================ +// Word / character helpers +// ============================================================ + +bool isWordChar(char c); +int32_t wordEnd(const char *buf, int32_t len, int32_t pos); +int32_t wordStart(const char *buf, int32_t pos); + +// ============================================================ +// Single-line text editing engine +// ============================================================ + +void widgetTextEditDragUpdateLine(int32_t vx, int32_t leftEdge, int32_t maxChars, const BitmapFontT *font, int32_t len, int32_t *pCursorPos, int32_t *pScrollOff, int32_t *pSelEnd); +void widgetTextEditMouseClick(WidgetT *w, int32_t vx, int32_t vy, int32_t textLeftX, const BitmapFontT *font, const char *buf, int32_t len, int32_t scrollOff, int32_t *pCursorPos, int32_t *pSelStart, int32_t *pSelEnd, bool wordSelect, bool dragSelect); +void widgetTextEditOnKey(WidgetT *w, int32_t key, int32_t mod, char *buf, int32_t bufSize, int32_t *pLen, int32_t *pCursor, int32_t *pScrollOff, int32_t *pSelStart, int32_t *pSelEnd, char *undoBuf, int32_t *pUndoLen, int32_t *pUndoCursor); +void widgetTextEditPaintLine(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors, int32_t textX, int32_t textY, const char *buf, int32_t visLen, int32_t scrollOff, int32_t cursorPos, int32_t selStart, int32_t selEnd, uint32_t fg, uint32_t bg, bool showCursor, int32_t cursorMinX, int32_t cursorMaxX); + +#endif // TEXT_HELP_H diff --git a/widgets/Makefile b/widgets/Makefile index 73c8cb65..44dc5e64 100644 --- a/widgets/Makefile +++ b/widgets/Makefile @@ -6,7 +6,7 @@ DJGPP_PREFIX = $(HOME)/djgpp/djgpp CC = $(DJGPP_PREFIX)/bin/i586-pc-msdosdjgpp-gcc DXE3GEN = PATH=$(DJGPP_PREFIX)/bin:$(PATH) DJDIR=$(DJGPP_PREFIX)/i586-pc-msdosdjgpp $(DJGPP_PREFIX)/i586-pc-msdosdjgpp/bin/dxe3gen -CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../tasks/thirdparty +CFLAGS = -O2 -Wall -Wextra -march=i486 -mtune=i586 -I../core -I../core/platform -I../tasks -I../core/thirdparty OBJDIR = ../obj/widgets WGTDIR = ../bin/widgets @@ -69,11 +69,18 @@ WGT_MODS = $(WGTDIR)/box.wgt \ .PHONY: all clean -all: $(WGT_MODS) +DEPFILES = textinpt combobox spinner terminal listbox dropdown listview treeview +WGT_DEPS = $(DEPFILES:%=$(WGTDIR)/%.dep) -# Generic widget module rule: -E _wgt exports all public API + wgtRegister +all: $(WGT_MODS) $(WGT_DEPS) + +$(WGTDIR)/%.dep: ../config/%.dep | $(WGTDIR) + sed 's/$$/\r/' $< > $@ + +# Generic widget module rule: export only the registration function $(WGTDIR)/%.wgt: | $(WGTDIR) - $(DXE3GEN) -o $@ -E _wgt -U $< + $(DXE3GEN) -o $(WGTDIR)/$*.dxe -E _wgtRegister -U $< + mv $(WGTDIR)/$*.dxe $@ # Map .wgt name to .o file $(WGTDIR)/box.wgt: $(OBJDIR)/widgetBox.o @@ -114,7 +121,7 @@ $(WGTDIR): mkdir -p $(WGTDIR) # Dependencies -WIDGET_DEPS = ../core/widgetInternal.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h +WIDGET_DEPS = ../core/dvxWidgetPlugin.h ../core/dvxWidget.h ../core/dvxTypes.h ../core/dvxApp.h ../core/dvxDraw.h ../core/dvxWm.h ../core/dvxVideo.h $(OBJDIR)/widgetAnsiTerm.o: widgetAnsiTerm.c $(WIDGET_DEPS) $(OBJDIR)/widgetBox.o: widgetBox.c $(WIDGET_DEPS) $(OBJDIR)/widgetButton.o: widgetButton.c $(WIDGET_DEPS) @@ -143,5 +150,5 @@ $(OBJDIR)/widgetToolbar.o: widgetToolbar.c $(WIDGET_DEPS) $(OBJDIR)/widgetTreeView.o: widgetTreeView.c $(WIDGET_DEPS) clean: - rm -f $(OBJS) $(WGT_MODS) + rm -f $(OBJS) $(WGT_MODS) $(WGT_DEPS) -rmdir $(WGTDIR) 2>/dev/null diff --git a/widgets/README.md b/widgets/README.md index 6973b708..f2262c34 100644 --- a/widgets/README.md +++ b/widgets/README.md @@ -66,7 +66,7 @@ multiple related types). ## Writing a New Widget 1. Create `widgets/myWidget.c` -2. Include `"widgetInternal.h"` for infrastructure access +2. Include `"dvxWidgetPlugin.h"` for infrastructure access 3. Implement paint, calcMinSize, and event handlers as needed 4. Define a `static const WidgetClassT` with your vtable 5. Add a `wgtRegister()` function: diff --git a/widgets/widgetAnsiTerm.c b/widgets/widgetAnsiTerm.c index a4fac25e..32e4157c 100644 --- a/widgets/widgetAnsiTerm.c +++ b/widgets/widgetAnsiTerm.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetAnsiTerm.c -- ANSI BBS terminal emulator widget // // Implements a VT100/ANSI-compatible terminal emulator widget designed for @@ -35,69 +36,74 @@ // the terminal to work with raw serial ports, the secLink encrypted channel, // or any other byte-oriented transport. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "../texthelp/textHelp.h" - -// ============================================================ -// Constants -// ============================================================ +static int32_t sTypeId = -1; #define ANSI_BORDER 2 #define ANSI_MAX_PARAMS 8 #define ANSI_SB_W 14 #define ANSI_DEFAULT_SCROLLBACK 500 - -// Three-state ANSI parser: NORMAL processes printable chars and C0 controls, -// ESC waits for the CSI introducer '[' (or standalone ESC sequences like ESC D/M/c), -// CSI accumulates numeric parameters until a final byte (0x40-0x7E) dispatches. #define PARSE_NORMAL 0 #define PARSE_ESC 1 #define PARSE_CSI 2 - -// Default attribute: light gray on black #define ANSI_DEFAULT_ATTR 0x07 - -// Attribute byte layout matches CGA text mode exactly so that ANSI art -// designed for DOS renders correctly without any translation. -// Bit 7 = blink, bits 6-4 = bg (0-7), bits 3-0 = fg (0-15) #define ATTR_BLINK_BIT 0x80 -#define ATTR_BG_MASK 0x70 #define ATTR_FG_MASK 0x0F - -// Blink/cursor rates in milliseconds #define BLINK_MS 500 #define CURSOR_MS 250 -// ============================================================ -// CGA palette (RGB values for 16 standard colors) -// ============================================================ +typedef struct { + uint8_t *cells; + int32_t cols; + int32_t rows; + int32_t cursorRow; + int32_t cursorCol; + bool cursorVisible; + bool wrapMode; + bool bold; + bool originMode; + bool csiPrivate; + uint8_t curAttr; + uint8_t parseState; + int32_t params[8]; + int32_t paramCount; + int32_t savedRow; + int32_t savedCol; + int32_t scrollTop; + int32_t scrollBot; + uint8_t *scrollback; + int32_t scrollbackMax; + int32_t scrollbackCount; + int32_t scrollbackHead; + int32_t scrollPos; + bool blinkVisible; + clock_t blinkTime; + bool cursorOn; + clock_t cursorTime; + uint32_t dirtyRows; + int32_t lastCursorRow; + int32_t lastCursorCol; + uint32_t packedPalette[16]; + bool paletteValid; + int32_t selStartLine; + int32_t selStartCol; + int32_t selEndLine; + int32_t selEndCol; + bool selecting; + void *commCtx; + int32_t (*commRead)(void *ctx, uint8_t *buf, int32_t maxLen); + int32_t (*commWrite)(void *ctx, const uint8_t *buf, int32_t len); +} AnsiTermDataT; static const uint8_t sCgaPalette[16][3] = { - { 0, 0, 0}, // 0: black - { 0, 0, 170}, // 1: blue - { 0, 170, 0}, // 2: green - { 0, 170, 170}, // 3: cyan - {170, 0, 0}, // 4: red - {170, 0, 170}, // 5: magenta - {170, 85, 0}, // 6: brown - {170, 170, 170}, // 7: light gray - { 85, 85, 85}, // 8: dark gray - { 85, 85, 255}, // 9: bright blue - { 85, 255, 85}, // 10: bright green - { 85, 255, 255}, // 11: bright cyan - {255, 85, 85}, // 12: bright red - {255, 85, 255}, // 13: bright magenta - {255, 255, 85}, // 14: bright yellow - {255, 255, 255}, // 15: bright white + { 0, 0, 0}, { 0, 0, 170}, { 0, 170, 0}, { 0, 170, 170}, + {170, 0, 0}, {170, 0, 170}, {170, 85, 0}, {170, 170, 170}, + { 85, 85, 85}, { 85, 85, 255}, { 85, 255, 85}, { 85, 255, 255}, + {255, 85, 85}, {255, 85, 255}, {255, 255, 85}, {255, 255, 255}, }; -// ANSI SGR color index to CGA color index. -// ANSI and CGA use different orderings for the base 8 colors. ANSI follows -// the RGB bit-field convention (0=black, 1=red, 2=green, 3=yellow, 4=blue, -// 5=magenta, 6=cyan, 7=white) while CGA uses (0=black, 1=blue, 2=green, -// 3=cyan, 4=red, 5=magenta, 6=brown, 7=light gray). This table maps from -// ANSI SGR color numbers (30-37 minus 30) to CGA palette indices. The -// high bit (bright) is handled separately via the bold attribute. static const int32_t sAnsiToCga[8] = { 0, 4, 2, 6, 1, 5, 3, 7 }; @@ -128,2027 +134,782 @@ static void ansiTermPutChar(WidgetT *w, uint8_t ch); static void ansiTermScrollDown(WidgetT *w); static void ansiTermScrollUp(WidgetT *w); static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol); +void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len); -// ============================================================ -// ansiTermAddToScrollback -// ============================================================ -// -// Copy a screen row into the scrollback circular buffer. -// The circular buffer avoids any need for memmove when the buffer fills -- -// the head index simply wraps around, overwriting the oldest entry. This -// is O(1) per row regardless of scrollback size, which matters when BBS -// software rapidly dumps text (e.g. file listings, ANSI art). - static void ansiTermAddToScrollback(WidgetT *w, int32_t screenRow) { - if (!w->as.ansiTerm->scrollback || w->as.ansiTerm->scrollbackMax <= 0) { - return; - } - - int32_t cols = w->as.ansiTerm->cols; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (!at->scrollback || at->scrollbackMax <= 0) { return; } + int32_t cols = at->cols; int32_t bytesPerRow = cols * 2; - int32_t head = w->as.ansiTerm->scrollbackHead; - - memcpy(w->as.ansiTerm->scrollback + head * bytesPerRow, - w->as.ansiTerm->cells + screenRow * bytesPerRow, - bytesPerRow); - - w->as.ansiTerm->scrollbackHead = (head + 1) % w->as.ansiTerm->scrollbackMax; - - if (w->as.ansiTerm->scrollbackCount < w->as.ansiTerm->scrollbackMax) { - w->as.ansiTerm->scrollbackCount++; - } + int32_t head = at->scrollbackHead; + memcpy(at->scrollback + head * bytesPerRow, at->cells + screenRow * bytesPerRow, bytesPerRow); + at->scrollbackHead = (head + 1) % at->scrollbackMax; + if (at->scrollbackCount < at->scrollbackMax) { at->scrollbackCount++; } } -// ============================================================ -// ansiTermBuildPalette -// ============================================================ -// -// Build the packed 16-color palette and cache it in the widget. -// Only recomputed when paletteValid is false (first use or -// after a display format change). -// -// Caching avoids calling packColor() 16 times per repaint. Since packColor -// involves shifting and masking per the display's pixel format, and the -// terminal repaints rows frequently during data reception, this saves -// significant overhead on a 486 where each function call costs ~30 cycles. - static void ansiTermBuildPalette(WidgetT *w, const DisplayT *d) { - if (w->as.ansiTerm->paletteValid) { - return; - } - + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (at->paletteValid) { return; } for (int32_t i = 0; i < 16; i++) { - w->as.ansiTerm->packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); + at->packedPalette[i] = packColor(d, sCgaPalette[i][0], sCgaPalette[i][1], sCgaPalette[i][2]); } - - w->as.ansiTerm->paletteValid = true; + at->paletteValid = true; } -// ============================================================ -// ansiTermClearSelection -// ============================================================ - static void ansiTermClearSelection(WidgetT *w) { - // Dirty all rows when clearing a selection so the selection highlight - // is erased on the next repaint - if (ansiTermHasSelection(w)) { - w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; - } - - w->as.ansiTerm->selStartLine = -1; - w->as.ansiTerm->selStartCol = -1; - w->as.ansiTerm->selEndLine = -1; - w->as.ansiTerm->selEndCol = -1; - w->as.ansiTerm->selecting = false; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (ansiTermHasSelection(w)) { at->dirtyRows = 0xFFFFFFFF; } + at->selStartLine = -1; + at->selStartCol = -1; + at->selEndLine = -1; + at->selEndCol = -1; + at->selecting = false; } -// ============================================================ -// ansiTermCopySelection -// ============================================================ - static void ansiTermCopySelection(WidgetT *w) { - if (!ansiTermHasSelection(w)) { - return; - } - - int32_t sLine; - int32_t sCol; - int32_t eLine; - int32_t eCol; + if (!ansiTermHasSelection(w)) { return; } + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t sLine, sCol, eLine, eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); - - int32_t cols = w->as.ansiTerm->cols; - - // Build text from selected cells (strip trailing spaces per line). - // Trailing spaces are stripped because BBS text mode fills the entire - // row with spaces -- without stripping, pasting would include many - // unwanted trailing blanks. Fixed 4KB buffer is sufficient for typical - // terminal selections (80 cols * 50 rows = 4000 chars max). + int32_t cols = at->cols; char buf[4096]; int32_t pos = 0; - for (int32_t line = sLine; line <= eLine && pos < 4095; line++) { const uint8_t *lineData = ansiTermGetLine(w, line); - int32_t colStart = (line == sLine) ? sCol : 0; int32_t colEnd = (line == eLine) ? eCol : cols; - - // Find last non-space character in this line's selection int32_t lastNonSpace = colStart - 1; - for (int32_t c = colStart; c < colEnd; c++) { - if (lineData[c * 2] != ' ') { - lastNonSpace = c; - } + if (lineData[c * 2] != ' ') { lastNonSpace = c; } } - for (int32_t c = colStart; c <= lastNonSpace && pos < 4095; c++) { buf[pos++] = (char)lineData[c * 2]; } - - // Add newline between lines (not after last) - if (line < eLine && pos < 4095) { - buf[pos++] = '\n'; - } + if (line < eLine && pos < 4095) { buf[pos++] = '\n'; } } - buf[pos] = '\0'; - - if (pos > 0) { - clipboardCopy(buf, pos); - } + if (pos > 0) { clipboardCopy(buf, pos); } } -// ============================================================ -// ansiTermDirtyRange -// ============================================================ -// -// Mark rows dirty that are touched by a cell range [startCell, startCell+count). -// Uses a 32-bit bitmask -- one bit per row -- which limits tracking to the first -// 32 rows. This is fine because standard terminal sizes are 24-25 rows, and -// bitmask operations are single-cycle on the target CPU. The bitmask approach -// is much cheaper than maintaining a dirty rect list for per-row tracking. - static void ansiTermDirtyRange(WidgetT *w, int32_t startCell, int32_t count) { - int32_t cols = w->as.ansiTerm->cols; - int32_t startRow = startCell / cols; - int32_t endRow = (startCell + count - 1) / cols; - + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t startRow = startCell / at->cols; + int32_t endRow = (startCell + count - 1) / at->cols; for (int32_t r = startRow; r <= endRow && r < 32; r++) { - w->as.ansiTerm->dirtyRows |= (1U << r); + at->dirtyRows |= (1U << r); } } -// ============================================================ -// ansiTermDirtyRow -// ============================================================ - static void ansiTermDirtyRow(WidgetT *w, int32_t row) { if (row >= 0 && row < 32) { - w->as.ansiTerm->dirtyRows |= (1U << row); + ((AnsiTermDataT *)w->data)->dirtyRows |= (1U << row); } } -// ============================================================ -// ansiTermDeleteLines -// ============================================================ - -// Delete lines within the scroll region by shifting rows up with memmove. -// The vacated lines at the bottom are filled with the current attribute. -// This is the CSI 'M' (DL) handler. Used by BBS software for scroll-region -// tricks (e.g., split-screen chat windows). static void ansiTermDeleteLines(WidgetT *w, int32_t count) { - int32_t cols = w->as.ansiTerm->cols; - int32_t bot = w->as.ansiTerm->scrollBot; - int32_t row = w->as.ansiTerm->cursorRow; - - if (count > bot - row + 1) { - count = bot - row + 1; - } - - if (count <= 0) { - return; - } - - uint8_t *cells = w->as.ansiTerm->cells; - int32_t bytesPerRow = cols * 2; - - // Shift lines up within region + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t bot = at->scrollBot; + int32_t row = at->cursorRow; + if (count > bot - row + 1) { count = bot - row + 1; } + if (count <= 0) { return; } + int32_t bytesPerRow = cols * 2; if (row + count <= bot) { - memmove(cells + row * bytesPerRow, - cells + (row + count) * bytesPerRow, - (bot - row - count + 1) * bytesPerRow); - } - - // Clear the bottom lines of the region - for (int32_t r = bot - count + 1; r <= bot; r++) { - ansiTermFillCells(w, r * cols, cols); - } - - // Dirty affected rows - for (int32_t r = row; r <= bot; r++) { - ansiTermDirtyRow(w, r); + memmove(at->cells + row * bytesPerRow, at->cells + (row + count) * bytesPerRow, (bot - row - count + 1) * bytesPerRow); } + for (int32_t r = bot - count + 1; r <= bot; r++) { ansiTermFillCells(w, r * cols, cols); } + for (int32_t r = row; r <= bot; r++) { ansiTermDirtyRow(w, r); } } -// ============================================================ -// ansiTermDispatchCsi -// ============================================================ - -// Central CSI dispatcher. After the parser accumulates parameters in the CSI -// state, the final byte triggers dispatch here. Only sequences commonly used -// by BBS software are implemented -- exotic VT220+ sequences are silently ignored. -// DEC private modes (ESC[?...) are handled separately since they use a different -// parameter namespace than standard ECMA-48 sequences. static void ansiTermDispatchCsi(WidgetT *w, uint8_t cmd) { - int32_t *p = w->as.ansiTerm->params; - int32_t n = w->as.ansiTerm->paramCount; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t *p = at->params; + int32_t n = at->paramCount; - // DEC private modes (ESC[?...) - // Mode 6: origin mode (cursor addressing relative to scroll region) - // Mode 7: auto-wrap at right margin - // Mode 25: cursor visibility (DECTCEM) - if (w->as.ansiTerm->csiPrivate) { + if (at->csiPrivate) { int32_t mode = (n >= 1) ? p[0] : 0; - if (cmd == 'h') { - if (mode == 6) { - w->as.ansiTerm->originMode = true; - w->as.ansiTerm->cursorRow = w->as.ansiTerm->scrollTop; - w->as.ansiTerm->cursorCol = 0; - } - - if (mode == 7) { - w->as.ansiTerm->wrapMode = true; - } - - if (mode == 25) { - w->as.ansiTerm->cursorVisible = true; - } + if (mode == 6) { at->originMode = true; at->cursorRow = at->scrollTop; at->cursorCol = 0; } + if (mode == 7) { at->wrapMode = true; } + if (mode == 25) { at->cursorVisible = true; } } else if (cmd == 'l') { - if (mode == 6) { - w->as.ansiTerm->originMode = false; - w->as.ansiTerm->cursorRow = 0; - w->as.ansiTerm->cursorCol = 0; - } - - if (mode == 7) { - w->as.ansiTerm->wrapMode = false; - } - - if (mode == 25) { - w->as.ansiTerm->cursorVisible = false; - } + if (mode == 6) { at->originMode = false; at->cursorRow = 0; at->cursorCol = 0; } + if (mode == 7) { at->wrapMode = false; } + if (mode == 25) { at->cursorVisible = false; } } - return; } switch (cmd) { - case '@': // ICH - insert character - { + case '@': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t cols = w->as.ansiTerm->cols; - int32_t row = w->as.ansiTerm->cursorRow; - int32_t col = w->as.ansiTerm->cursorCol; - - if (count > cols - col) { - count = cols - col; - } - + int32_t cols = at->cols; + int32_t row = at->cursorRow; + int32_t col = at->cursorCol; + if (count > cols - col) { count = cols - col; } if (count > 0) { - uint8_t *base = w->as.ansiTerm->cells + row * cols * 2; + uint8_t *base = at->cells + row * cols * 2; memmove(base + (col + count) * 2, base + col * 2, (cols - col - count) * 2); - - for (int32_t i = col; i < col + count; i++) { - base[i * 2] = ' '; - base[i * 2 + 1] = w->as.ansiTerm->curAttr; - } - + for (int32_t i = col; i < col + count; i++) { base[i * 2] = ' '; base[i * 2 + 1] = at->curAttr; } ansiTermDirtyRow(w, row); } - break; } - - case 'A': // CUU - cursor up - { + case 'A': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t minRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollTop : 0; - w->as.ansiTerm->cursorRow -= count; - - if (w->as.ansiTerm->cursorRow < minRow) { - w->as.ansiTerm->cursorRow = minRow; - } - + int32_t minRow = at->originMode ? at->scrollTop : 0; + at->cursorRow -= count; + if (at->cursorRow < minRow) { at->cursorRow = minRow; } break; } - - case 'B': // CUD - cursor down - { + case 'B': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t maxRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollBot : w->as.ansiTerm->rows - 1; - w->as.ansiTerm->cursorRow += count; - - if (w->as.ansiTerm->cursorRow > maxRow) { - w->as.ansiTerm->cursorRow = maxRow; - } - + int32_t maxRow = at->originMode ? at->scrollBot : at->rows - 1; + at->cursorRow += count; + if (at->cursorRow > maxRow) { at->cursorRow = maxRow; } break; } - - case 'C': // CUF - cursor forward - { + case 'C': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - w->as.ansiTerm->cursorCol += count; - - if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) { - w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1; - } - + at->cursorCol += count; + if (at->cursorCol >= at->cols) { at->cursorCol = at->cols - 1; } break; } - - case 'D': // CUB - cursor back - { + case 'D': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - w->as.ansiTerm->cursorCol -= count; - - if (w->as.ansiTerm->cursorCol < 0) { - w->as.ansiTerm->cursorCol = 0; - } - + at->cursorCol -= count; + if (at->cursorCol < 0) { at->cursorCol = 0; } break; } - - case 'c': // DA - device attributes - { - if (w->as.ansiTerm->commWrite) { - // Respond as VT100 with advanced video option (AVO). - // Many BBS door games query DA to detect terminal capabilities. - // Claiming VT100+AVO is the safest response -- it tells the remote - // side we support ANSI color without implying VT220+ features. + case 'c': { + if (at->commWrite) { const uint8_t reply[] = "\033[?1;2c"; - w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, reply, 7); + at->commWrite(at->commCtx, reply, 7); } - break; } - - case 'E': // CNL - cursor next line - { + case 'E': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - - for (int32_t i = 0; i < count; i++) { - ansiTermNewline(w); - } - - w->as.ansiTerm->cursorCol = 0; + for (int32_t i = 0; i < count; i++) { ansiTermNewline(w); } + at->cursorCol = 0; break; } - - case 'H': // CUP - cursor position - case 'f': // HVP - same - { + case 'H': + case 'f': { int32_t row = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t col = (n >= 2 && p[1]) ? p[1] - 1 : 0; - - // Origin mode: row is relative to scroll region - if (w->as.ansiTerm->originMode) { - row += w->as.ansiTerm->scrollTop; - - if (row > w->as.ansiTerm->scrollBot) { - row = w->as.ansiTerm->scrollBot; - } - } - - if (row < 0) { - row = 0; - } - - if (row >= w->as.ansiTerm->rows) { - row = w->as.ansiTerm->rows - 1; - } - - if (col < 0) { - col = 0; - } - - if (col >= w->as.ansiTerm->cols) { - col = w->as.ansiTerm->cols - 1; - } - - w->as.ansiTerm->cursorRow = row; - w->as.ansiTerm->cursorCol = col; + if (at->originMode) { row += at->scrollTop; if (row > at->scrollBot) { row = at->scrollBot; } } + if (row < 0) { row = 0; } + if (row >= at->rows) { row = at->rows - 1; } + if (col < 0) { col = 0; } + if (col >= at->cols) { col = at->cols - 1; } + at->cursorRow = row; + at->cursorCol = col; break; } - - case 'J': // ED - erase display - { - int32_t mode = (n >= 1) ? p[0] : 0; - ansiTermEraseDisplay(w, mode); - break; - } - - case 'K': // EL - erase line - { - int32_t mode = (n >= 1) ? p[0] : 0; - ansiTermEraseLine(w, mode); - break; - } - - case 'L': // IL - insert lines - { + case 'J': { ansiTermEraseDisplay(w, (n >= 1) ? p[0] : 0); break; } + case 'K': { ansiTermEraseLine(w, (n >= 1) ? p[0] : 0); break; } + case 'L': { ansiTermInsertLines(w, (n >= 1 && p[0]) ? p[0] : 1); break; } + case 'M': { ansiTermDeleteLines(w, (n >= 1 && p[0]) ? p[0] : 1); break; } + case 'P': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - ansiTermInsertLines(w, count); - break; - } - - case 'M': // DL - delete lines - { - int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - ansiTermDeleteLines(w, count); - break; - } - - case 'P': // DCH - delete character - { - int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - int32_t cols = w->as.ansiTerm->cols; - int32_t row = w->as.ansiTerm->cursorRow; - int32_t col = w->as.ansiTerm->cursorCol; - - if (count > cols - col) { - count = cols - col; - } - + int32_t cols = at->cols; + int32_t row = at->cursorRow; + int32_t col = at->cursorCol; + if (count > cols - col) { count = cols - col; } if (count > 0) { - uint8_t *base = w->as.ansiTerm->cells + row * cols * 2; + uint8_t *base = at->cells + row * cols * 2; memmove(base + col * 2, base + (col + count) * 2, (cols - col - count) * 2); - - for (int32_t i = cols - count; i < cols; i++) { - base[i * 2] = ' '; - base[i * 2 + 1] = w->as.ansiTerm->curAttr; - } - + for (int32_t i = cols - count; i < cols; i++) { base[i * 2] = ' '; base[i * 2 + 1] = at->curAttr; } ansiTermDirtyRow(w, row); } - break; } - - case 'S': // SU - scroll up - { + case 'S': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - - for (int32_t i = 0; i < count; i++) { - ansiTermScrollUp(w); - } - + for (int32_t i = 0; i < count; i++) { ansiTermScrollUp(w); } break; } - - case 'T': // SD - scroll down - { + case 'T': { int32_t count = (n >= 1 && p[0]) ? p[0] : 1; - - for (int32_t i = 0; i < count; i++) { - ansiTermScrollDown(w); - } - + for (int32_t i = 0; i < count; i++) { ansiTermScrollDown(w); } break; } - - case 'Z': // CBT - back tab - { - // Move cursor back to the previous tab stop (every 8 columns). - // Uses integer math: ((col-1)/8)*8 rounds down to the nearest - // multiple of 8 strictly before the current position. - int32_t col = w->as.ansiTerm->cursorCol; - - if (col > 0) { - col = ((col - 1) / 8) * 8; - } - - w->as.ansiTerm->cursorCol = col; + case 'Z': { + int32_t col = at->cursorCol; + if (col > 0) { col = ((col - 1) / 8) * 8; } + at->cursorCol = col; break; } - - case 'm': // SGR - select graphic rendition - ansiTermProcessSgr(w); - break; - - case 'n': // DSR - device status report - { + case 'm': ansiTermProcessSgr(w); break; + case 'n': { int32_t mode = (n >= 1) ? p[0] : 0; - - if (w->as.ansiTerm->commWrite) { + if (at->commWrite) { if (mode == 6) { - // CPR -- cursor position report: ESC[row;colR (1-based). - // BBS software uses this for screen-size detection and - // to synchronize cursor positioning in door games. char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(w->as.ansiTerm->cursorRow + 1), (long)(w->as.ansiTerm->cursorCol + 1)); - w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)(at->cursorRow + 1), (long)(at->cursorCol + 1)); + at->commWrite(at->commCtx, (const uint8_t *)reply, len); } else if (mode == 255) { - // Custom extension: screen size report (non-standard). - // Allows the remote side to query our terminal dimensions - // without relying on NAWS or other Telnet extensions. char reply[16]; - int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)w->as.ansiTerm->rows, (long)w->as.ansiTerm->cols); - w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, (const uint8_t *)reply, len); + int32_t len = snprintf(reply, sizeof(reply), "\033[%ld;%ldR", (long)at->rows, (long)at->cols); + at->commWrite(at->commCtx, (const uint8_t *)reply, len); } } - break; } - - case 'r': // DECSTBM - set scrolling region - { - // Defines the top and bottom rows of the scroll region. Lines - // outside this region are not affected by scroll operations. This - // is heavily used by BBS split-screen chat (status line at top, - // chat scrolling below) and door game scoreboards. - int32_t rows = w->as.ansiTerm->rows; + case 'r': { + int32_t rows = at->rows; int32_t top = (n >= 1 && p[0]) ? p[0] - 1 : 0; int32_t bot = (n >= 2 && p[1]) ? p[1] - 1 : rows - 1; - - if (top < 0) { - top = 0; - } - - if (bot >= rows) { - bot = rows - 1; - } - - if (top < bot) { - w->as.ansiTerm->scrollTop = top; - w->as.ansiTerm->scrollBot = bot; - } else { - // Invalid or reset -- restore full screen - w->as.ansiTerm->scrollTop = 0; - w->as.ansiTerm->scrollBot = rows - 1; - } - - // Home cursor (relative to region if origin mode) - w->as.ansiTerm->cursorRow = w->as.ansiTerm->originMode ? w->as.ansiTerm->scrollTop : 0; - w->as.ansiTerm->cursorCol = 0; + if (top < 0) { top = 0; } + if (bot >= rows) { bot = rows - 1; } + if (top < bot) { at->scrollTop = top; at->scrollBot = bot; } + else { at->scrollTop = 0; at->scrollBot = rows - 1; } + at->cursorRow = at->originMode ? at->scrollTop : 0; + at->cursorCol = 0; break; } - - case 's': // SCP - save cursor position - w->as.ansiTerm->savedRow = w->as.ansiTerm->cursorRow; - w->as.ansiTerm->savedCol = w->as.ansiTerm->cursorCol; - break; - - case 'u': // RCP - restore cursor position - w->as.ansiTerm->cursorRow = w->as.ansiTerm->savedRow; - w->as.ansiTerm->cursorCol = w->as.ansiTerm->savedCol; - break; - - default: - break; + case 's': at->savedRow = at->cursorRow; at->savedCol = at->cursorCol; break; + case 'u': at->cursorRow = at->savedRow; at->cursorCol = at->savedCol; break; + default: break; } } -// ============================================================ -// ansiTermEraseDisplay -// ============================================================ - -// Erase Display (ED): mode 0 = cursor to end, 1 = start to cursor, 2 = all. -// Mode 2 pushes all visible lines to scrollback before clearing, preserving -// content that was on screen -- this is what users expect when a BBS sends -// a clear-screen sequence (they can scroll back to see previous content). -// The wasAtBottom check ensures auto-scroll tracking: if the user was -// already viewing the latest content, they stay at the bottom after new -// scrollback lines are added. static void ansiTermEraseDisplay(WidgetT *w, int32_t mode) { - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; - int32_t cur = w->as.ansiTerm->cursorRow * cols + w->as.ansiTerm->cursorCol; - + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t rows = at->rows; + int32_t cur = at->cursorRow * cols + at->cursorCol; if (mode == 0) { - // Erase from cursor to end of screen ansiTermFillCells(w, cur, cols * rows - cur); } else if (mode == 1) { - // Erase from start to cursor ansiTermFillCells(w, 0, cur + 1); } else if (mode == 2) { - // Erase entire screen -- push all lines to scrollback first - bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); - - for (int32_t r = 0; r < rows; r++) { - ansiTermAddToScrollback(w, r); - } - - if (wasAtBottom) { - w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; - } - + bool wasAtBottom = (at->scrollPos == at->scrollbackCount); + for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } + if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } ansiTermFillCells(w, 0, cols * rows); } } -// ============================================================ -// ansiTermEraseLine -// ============================================================ - static void ansiTermEraseLine(WidgetT *w, int32_t mode) { - int32_t cols = w->as.ansiTerm->cols; - int32_t row = w->as.ansiTerm->cursorRow; - int32_t col = w->as.ansiTerm->cursorCol; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t row = at->cursorRow; + int32_t col = at->cursorCol; int32_t base = row * cols; - - if (mode == 0) { - // Erase from cursor to end of line - ansiTermFillCells(w, base + col, cols - col); - } else if (mode == 1) { - // Erase from start of line to cursor - ansiTermFillCells(w, base, col + 1); - } else if (mode == 2) { - // Erase entire line - ansiTermFillCells(w, base, cols); - } + if (mode == 0) { ansiTermFillCells(w, base + col, cols - col); } + else if (mode == 1) { ansiTermFillCells(w, base, col + 1); } + else if (mode == 2) { ansiTermFillCells(w, base, cols); } } -// ============================================================ -// ansiTermFillCells -// ============================================================ -// -// Fill a range of cells with space + current attribute. -// Uses the current attribute (not default) so that erasing respects the -// currently active background color -- this is correct per ECMA-48 and -// matches what BBS software expects (e.g., colored backgrounds that -// persist after a line erase). - static void ansiTermFillCells(WidgetT *w, int32_t start, int32_t count) { - uint8_t *cells = w->as.ansiTerm->cells; - uint8_t attr = w->as.ansiTerm->curAttr; - int32_t total = w->as.ansiTerm->cols * w->as.ansiTerm->rows; - - if (start < 0) { - count += start; - start = 0; - } - - if (start + count > total) { - count = total - start; - } - + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + uint8_t *cells = at->cells; + uint8_t attr = at->curAttr; + int32_t total = at->cols * at->rows; + if (start < 0) { count += start; start = 0; } + if (start + count > total) { count = total - start; } for (int32_t i = 0; i < count; i++) { cells[(start + i) * 2] = ' '; cells[(start + i) * 2 + 1] = attr; } - ansiTermDirtyRange(w, start, count); } -// ============================================================ -// ansiTermGetLine -// ============================================================ -// -// Get a pointer to the cell data for a given line index in the -// combined scrollback+screen view. -// lineIndex < scrollbackCount -> scrollback line -// lineIndex >= scrollbackCount -> screen line -// -// The unified line index simplifies paint and selection code -- they don't -// need to know whether a given line is in scrollback or on screen. The -// circular buffer index computation uses modular arithmetic to map from -// logical scrollback line number to physical buffer position. - static const uint8_t *ansiTermGetLine(WidgetT *w, int32_t lineIndex) { - int32_t cols = w->as.ansiTerm->cols; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; int32_t bytesPerRow = cols * 2; - int32_t sbCount = w->as.ansiTerm->scrollbackCount; - + int32_t sbCount = at->scrollbackCount; if (lineIndex < sbCount) { - // Scrollback line (circular buffer) - int32_t sbMax = w->as.ansiTerm->scrollbackMax; - int32_t actual = (w->as.ansiTerm->scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; - return w->as.ansiTerm->scrollback + actual * bytesPerRow; + int32_t sbMax = at->scrollbackMax; + int32_t actual = (at->scrollbackHead - sbCount + lineIndex + sbMax) % sbMax; + return at->scrollback + actual * bytesPerRow; } - - // Screen line - int32_t screenRow = lineIndex - sbCount; - return w->as.ansiTerm->cells + screenRow * bytesPerRow; + return at->cells + (lineIndex - sbCount) * bytesPerRow; } -// ============================================================ -// ansiTermHasSelection -// ============================================================ - static bool ansiTermHasSelection(const WidgetT *w) { - if (w->as.ansiTerm->selStartLine < 0) { - return false; - } - - if (w->as.ansiTerm->selStartLine == w->as.ansiTerm->selEndLine && - w->as.ansiTerm->selStartCol == w->as.ansiTerm->selEndCol) { - return false; - } - + const AnsiTermDataT *at = (const AnsiTermDataT *)w->data; + if (at->selStartLine < 0) { return false; } + if (at->selStartLine == at->selEndLine && at->selStartCol == at->selEndCol) { return false; } return true; } -// ============================================================ -// ansiTermInsertLines -// ============================================================ - -// Insert blank lines at the cursor position within the scroll region by -// shifting rows down with memmove. This is the CSI 'L' (IL) handler, -// the inverse of ansiTermDeleteLines. static void ansiTermInsertLines(WidgetT *w, int32_t count) { - int32_t cols = w->as.ansiTerm->cols; - int32_t bot = w->as.ansiTerm->scrollBot; - int32_t row = w->as.ansiTerm->cursorRow; - - if (count > bot - row + 1) { - count = bot - row + 1; - } - - if (count <= 0) { - return; - } - - uint8_t *cells = w->as.ansiTerm->cells; - int32_t bytesPerRow = cols * 2; - - // Shift lines down within region + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t bot = at->scrollBot; + int32_t row = at->cursorRow; + if (count > bot - row + 1) { count = bot - row + 1; } + if (count <= 0) { return; } + int32_t bytesPerRow = cols * 2; if (row + count <= bot) { - memmove(cells + (row + count) * bytesPerRow, - cells + row * bytesPerRow, - (bot - row - count + 1) * bytesPerRow); - } - - // Clear the inserted lines - for (int32_t r = row; r < row + count; r++) { - ansiTermFillCells(w, r * cols, cols); - } - - // Dirty affected rows - for (int32_t r = row; r <= bot; r++) { - ansiTermDirtyRow(w, r); + memmove(at->cells + (row + count) * bytesPerRow, at->cells + row * bytesPerRow, (bot - row - count + 1) * bytesPerRow); } + for (int32_t r = row; r < row + count; r++) { ansiTermFillCells(w, r * cols, cols); } + for (int32_t r = row; r <= bot; r++) { ansiTermDirtyRow(w, r); } } -// ============================================================ -// ansiTermNewline -// ============================================================ -// -// Move cursor to next line, scrolling if at the bottom. - static void ansiTermNewline(WidgetT *w) { - int32_t bot = w->as.ansiTerm->scrollBot; - - if (w->as.ansiTerm->cursorRow == bot) { - ansiTermScrollUp(w); - } else if (w->as.ansiTerm->cursorRow < w->as.ansiTerm->rows - 1) { - w->as.ansiTerm->cursorRow++; - } + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (at->cursorRow == at->scrollBot) { ansiTermScrollUp(w); } + else if (at->cursorRow < at->rows - 1) { at->cursorRow++; } } -// ============================================================ -// ansiTermProcessByte -// ============================================================ -// -// Feed one byte through the ANSI parser state machine. -// This is the hot path for data reception -- called once per byte received -// from the serial link. The state machine is kept as simple as possible -// (no tables, no indirect calls) because this runs on every incoming byte -// and branch prediction on a 486/Pentium benefits from straightforward -// if/switch chains. -// -// In PARSE_NORMAL, C0 control characters (CR, LF, BS, TAB, FF, BEL, ESC) -// are handled first. All other bytes -- including CP437 graphic characters -// in the 0x01-0x1F range that aren't C0 controls, plus 0x80-0xFF -- are -// treated as printable and placed at the cursor via ansiTermPutChar. - static void ansiTermProcessByte(WidgetT *w, uint8_t ch) { - switch (w->as.ansiTerm->parseState) { + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + switch (at->parseState) { case PARSE_NORMAL: - if (ch == 0x1B) { - w->as.ansiTerm->parseState = PARSE_ESC; - } else if (ch == '\r') { - w->as.ansiTerm->cursorCol = 0; - } else if (ch == '\n') { - ansiTermNewline(w); - } else if (ch == '\b') { - if (w->as.ansiTerm->cursorCol > 0) { - w->as.ansiTerm->cursorCol--; - } - } else if (ch == '\t') { - // Advance to next 8-column tab stop using bit-mask trick: - // (col+8) & ~7 rounds up to the next multiple of 8 - w->as.ansiTerm->cursorCol = (w->as.ansiTerm->cursorCol + 8) & ~7; - - if (w->as.ansiTerm->cursorCol >= w->as.ansiTerm->cols) { - w->as.ansiTerm->cursorCol = w->as.ansiTerm->cols - 1; - } - } else if (ch == '\f') { - // Form feed -- clear screen and home cursor - ansiTermEraseDisplay(w, 2); - w->as.ansiTerm->cursorRow = 0; - w->as.ansiTerm->cursorCol = 0; - } else if (ch == '\a') { - // Bell -- ignored - } else { - // CP437 graphic characters (smileys, card suits, etc.) - // and all printable characters - ansiTermPutChar(w, ch); + if (ch == 0x1B) { at->parseState = PARSE_ESC; } + else if (ch == '\r') { at->cursorCol = 0; } + else if (ch == '\n') { ansiTermNewline(w); } + else if (ch == '\b') { if (at->cursorCol > 0) { at->cursorCol--; } } + else if (ch == '\t') { + at->cursorCol = (at->cursorCol + 8) & ~7; + if (at->cursorCol >= at->cols) { at->cursorCol = at->cols - 1; } } + else if (ch == '\f') { ansiTermEraseDisplay(w, 2); at->cursorRow = 0; at->cursorCol = 0; } + else if (ch == '\a') { /* Bell -- ignored */ } + else { ansiTermPutChar(w, ch); } break; - case PARSE_ESC: if (ch == '[') { - w->as.ansiTerm->parseState = PARSE_CSI; - w->as.ansiTerm->paramCount = 0; - w->as.ansiTerm->csiPrivate = false; - memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params)); - } else if (ch == 'D') { - // IND -- scroll up one line - ansiTermScrollUp(w); - w->as.ansiTerm->parseState = PARSE_NORMAL; - } else if (ch == 'M') { - // RI -- scroll down one line - ansiTermScrollDown(w); - w->as.ansiTerm->parseState = PARSE_NORMAL; - } else if (ch == 'c') { - // RIS -- terminal reset - w->as.ansiTerm->cursorRow = 0; - w->as.ansiTerm->cursorCol = 0; - w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm->bold = false; - w->as.ansiTerm->wrapMode = true; - w->as.ansiTerm->originMode = false; - w->as.ansiTerm->cursorVisible = true; - w->as.ansiTerm->scrollTop = 0; - w->as.ansiTerm->scrollBot = w->as.ansiTerm->rows - 1; - ansiTermEraseDisplay(w, 2); - w->as.ansiTerm->parseState = PARSE_NORMAL; - } else { - // Unknown escape -- return to normal - w->as.ansiTerm->parseState = PARSE_NORMAL; + at->parseState = PARSE_CSI; + at->paramCount = 0; + at->csiPrivate = false; + memset(at->params, 0, sizeof(at->params)); + } else if (ch == 'D') { ansiTermScrollUp(w); at->parseState = PARSE_NORMAL; } + else if (ch == 'M') { ansiTermScrollDown(w); at->parseState = PARSE_NORMAL; } + else if (ch == 'c') { + at->cursorRow = 0; at->cursorCol = 0; at->curAttr = ANSI_DEFAULT_ATTR; + at->bold = false; at->wrapMode = true; at->originMode = false; + at->cursorVisible = true; at->scrollTop = 0; at->scrollBot = at->rows - 1; + ansiTermEraseDisplay(w, 2); at->parseState = PARSE_NORMAL; } + else { at->parseState = PARSE_NORMAL; } break; - case PARSE_CSI: - // CSI parameter accumulation. Parameters are separated by ';'. - // Digits are accumulated into the current parameter slot using - // decimal shift (p*10 + digit). The '?' prefix marks DEC private - // mode sequences which have separate semantics from standard CSI. - // Final bytes (0x40-0x7E) terminate the sequence and dispatch. - if (ch == '?') { - w->as.ansiTerm->csiPrivate = true; - } else if (ch >= '0' && ch <= '9') { - if (w->as.ansiTerm->paramCount == 0) { - w->as.ansiTerm->paramCount = 1; - } - - int32_t idx = w->as.ansiTerm->paramCount - 1; - - if (idx < ANSI_MAX_PARAMS) { - w->as.ansiTerm->params[idx] = w->as.ansiTerm->params[idx] * 10 + (ch - '0'); - } - } else if (ch == ';') { - if (w->as.ansiTerm->paramCount < ANSI_MAX_PARAMS) { - w->as.ansiTerm->paramCount++; - } - } else if (ch >= 0x40 && ch <= 0x7E) { - // Final byte -- dispatch the CSI sequence - ansiTermDispatchCsi(w, ch); - w->as.ansiTerm->parseState = PARSE_NORMAL; - } else { - // Unexpected byte -- abort sequence - w->as.ansiTerm->parseState = PARSE_NORMAL; + if (ch == '?') { at->csiPrivate = true; } + else if (ch >= '0' && ch <= '9') { + if (at->paramCount == 0) { at->paramCount = 1; } + int32_t idx = at->paramCount - 1; + if (idx < ANSI_MAX_PARAMS) { at->params[idx] = at->params[idx] * 10 + (ch - '0'); } } + else if (ch == ';') { if (at->paramCount < ANSI_MAX_PARAMS) { at->paramCount++; } } + else if (ch >= 0x40 && ch <= 0x7E) { ansiTermDispatchCsi(w, ch); at->parseState = PARSE_NORMAL; } + else { at->parseState = PARSE_NORMAL; } break; } } -// ============================================================ -// ansiTermProcessSgr -// ============================================================ -// -// Handle SGR (Select Graphic Rendition) escape sequence. -// Maps ANSI color codes to the CGA attribute byte. The attribute byte -// is packed as (bg<<4)|fg, matching CGA text mode. Bold (code 1) sets -// the high bit of the foreground (giving bright colors), while blink -// (code 5) sets bit 3 of the background nibble (the CGA blink/bright-bg bit). -// -// Multiple SGR codes in a single sequence are processed left-to-right, -// each modifying the running attribute. This handles sequences like -// ESC[1;33;44m (bold yellow on blue) correctly. -// -// Codes 90-97/100-107 (bright colors) are non-standard but widely used -// by BBS software as an alternative to bold+color for explicit bright colors. - static void ansiTermProcessSgr(WidgetT *w) { - if (w->as.ansiTerm->paramCount == 0) { - // ESC[m with no params = reset - w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm->bold = false; - return; - } - - for (int32_t i = 0; i < w->as.ansiTerm->paramCount; i++) { - int32_t code = w->as.ansiTerm->params[i]; - uint8_t fg = w->as.ansiTerm->curAttr & 0x0F; - uint8_t bg = (w->as.ansiTerm->curAttr >> 4) & 0x0F; - - if (code == 0) { - fg = 7; - bg = 0; - w->as.ansiTerm->bold = false; - } else if (code == 1) { - w->as.ansiTerm->bold = true; - fg |= 8; - } else if (code == 5) { - // Blink -- sets bit 7 of attr byte via bg bit 3 - bg |= 8; - } else if (code == 25) { - // Blink off - bg &= 7; - } else if (code == 7) { - // Reverse video - uint8_t tmp = fg; - fg = bg; - bg = tmp; - } else if (code == 8) { - // Invisible -- foreground same as background - fg = bg & 0x07; - } else if (code == 22) { - // Normal intensity - w->as.ansiTerm->bold = false; - fg &= 7; - } else if (code >= 30 && code <= 37) { - fg = (uint8_t)sAnsiToCga[code - 30]; - - if (w->as.ansiTerm->bold) { - fg |= 8; - } - } else if (code >= 40 && code <= 47) { - bg = (uint8_t)(sAnsiToCga[code - 40] | (bg & 8)); - } else if (code >= 90 && code <= 97) { - // Bright foreground - fg = (uint8_t)(sAnsiToCga[code - 90] | 8); - } else if (code >= 100 && code <= 107) { - // Bright background - bg = (uint8_t)(sAnsiToCga[code - 100] | 8); - } - - w->as.ansiTerm->curAttr = (uint8_t)((bg << 4) | fg); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (at->paramCount == 0) { at->curAttr = ANSI_DEFAULT_ATTR; at->bold = false; return; } + for (int32_t i = 0; i < at->paramCount; i++) { + int32_t code = at->params[i]; + uint8_t fg = at->curAttr & 0x0F; + uint8_t bg = (at->curAttr >> 4) & 0x0F; + if (code == 0) { fg = 7; bg = 0; at->bold = false; } + else if (code == 1) { at->bold = true; fg |= 8; } + else if (code == 5) { bg |= 8; } + else if (code == 25) { bg &= 7; } + else if (code == 7) { uint8_t tmp = fg; fg = bg; bg = tmp; } + else if (code == 8) { fg = bg & 0x07; } + else if (code == 22) { at->bold = false; fg &= 7; } + else if (code >= 30 && code <= 37) { fg = (uint8_t)sAnsiToCga[code - 30]; if (at->bold) { fg |= 8; } } + else if (code >= 40 && code <= 47) { bg = (uint8_t)(sAnsiToCga[code - 40] | (bg & 8)); } + else if (code >= 90 && code <= 97) { fg = (uint8_t)(sAnsiToCga[code - 90] | 8); } + else if (code >= 100 && code <= 107) { bg = (uint8_t)(sAnsiToCga[code - 100] | 8); } + at->curAttr = (uint8_t)((bg << 4) | fg); } } -// ============================================================ -// ansiTermPasteToComm -// ============================================================ - static void ansiTermPasteToComm(WidgetT *w) { - if (!w->as.ansiTerm->commWrite) { - return; - } - - int32_t clipLen; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (!at->commWrite) { return; } + int32_t clipLen; const char *clip = clipboardGet(&clipLen); - - if (clipLen <= 0) { - return; - } - - // Transmit clipboard contents, converting \n to \r for the terminal. - // Terminals expect CR as the line-ending character; the remote system - // will echo back CR+LF if needed. + if (clipLen <= 0) { return; } for (int32_t i = 0; i < clipLen; i++) { uint8_t ch = (uint8_t)clip[i]; - - if (ch == '\n') { - ch = '\r'; - } - - w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, &ch, 1); + if (ch == '\n') { ch = '\r'; } + at->commWrite(at->commCtx, &ch, 1); } } -// ============================================================ -// ansiTermPutChar -// ============================================================ -// -// Place a character at the cursor and advance. - static void ansiTermPutChar(WidgetT *w, uint8_t ch) { - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; - int32_t row = w->as.ansiTerm->cursorRow; - int32_t col = w->as.ansiTerm->cursorCol; - + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t rows = at->rows; + int32_t row = at->cursorRow; + int32_t col = at->cursorCol; if (row >= 0 && row < rows && col >= 0 && col < cols) { int32_t idx = (row * cols + col) * 2; - w->as.ansiTerm->cells[idx] = ch; - w->as.ansiTerm->cells[idx + 1] = w->as.ansiTerm->curAttr; + at->cells[idx] = ch; + at->cells[idx + 1] = at->curAttr; ansiTermDirtyRow(w, row); } - - w->as.ansiTerm->cursorCol++; - - if (w->as.ansiTerm->cursorCol >= cols) { - if (w->as.ansiTerm->wrapMode) { - w->as.ansiTerm->cursorCol = 0; - ansiTermNewline(w); - } else { - w->as.ansiTerm->cursorCol = cols - 1; - } + at->cursorCol++; + if (at->cursorCol >= cols) { + if (at->wrapMode) { at->cursorCol = 0; ansiTermNewline(w); } + else { at->cursorCol = cols - 1; } } } -// ============================================================ -// ansiTermScrollDown -// ============================================================ - static void ansiTermScrollDown(WidgetT *w) { - int32_t cols = w->as.ansiTerm->cols; - int32_t top = w->as.ansiTerm->scrollTop; - int32_t bot = w->as.ansiTerm->scrollBot; - uint8_t *cells = w->as.ansiTerm->cells; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t top = at->scrollTop; + int32_t bot = at->scrollBot; int32_t bytesPerRow = cols * 2; - - // Shift lines within region down by one - if (bot > top) { - memmove(cells + (top + 1) * bytesPerRow, - cells + top * bytesPerRow, - (bot - top) * bytesPerRow); - } - - // Clear the top line of the region + if (bot > top) { memmove(at->cells + (top + 1) * bytesPerRow, at->cells + top * bytesPerRow, (bot - top) * bytesPerRow); } ansiTermFillCells(w, top * cols, cols); - - // Dirty affected rows - for (int32_t r = top; r <= bot && r < 32; r++) { - w->as.ansiTerm->dirtyRows |= (1U << r); - } + for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); } } -// ============================================================ -// ansiTermScrollUp -// ============================================================ -// -// Scroll the screen up by one line. The top line is pushed into -// the scrollback buffer before being discarded. -// -// Scrollback capture only occurs for full-screen scrolls (top=0, -// bot=last row). When a sub-region is scrolling (e.g., a BBS -// split-screen chat window), those lines are NOT added to scrollback -// because they represent transient UI content, not conversation history. - static void ansiTermScrollUp(WidgetT *w) { - int32_t cols = w->as.ansiTerm->cols; - int32_t top = w->as.ansiTerm->scrollTop; - int32_t bot = w->as.ansiTerm->scrollBot; - uint8_t *cells = w->as.ansiTerm->cells; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + int32_t top = at->scrollTop; + int32_t bot = at->scrollBot; int32_t bytesPerRow = cols * 2; - - // Only push to scrollback when scrolling the full screen - if (top == 0 && bot == w->as.ansiTerm->rows - 1) { - bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); + if (top == 0 && bot == at->rows - 1) { + bool wasAtBottom = (at->scrollPos == at->scrollbackCount); ansiTermAddToScrollback(w, 0); - - if (wasAtBottom) { - w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; - } + if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } } - - // Shift lines within region up by one - if (bot > top) { - memmove(cells + top * bytesPerRow, - cells + (top + 1) * bytesPerRow, - (bot - top) * bytesPerRow); - } - - // Clear the bottom line of the region + if (bot > top) { memmove(at->cells + top * bytesPerRow, at->cells + (top + 1) * bytesPerRow, (bot - top) * bytesPerRow); } ansiTermFillCells(w, bot * cols, cols); - - // Dirty affected rows - for (int32_t r = top; r <= bot && r < 32; r++) { - w->as.ansiTerm->dirtyRows |= (1U << r); - } + for (int32_t r = top; r <= bot && r < 32; r++) { at->dirtyRows |= (1U << r); } } -// ============================================================ -// ansiTermSelectionRange -// ============================================================ -// -// Return selection start/end in normalized order (start <= end). - static void ansiTermSelectionRange(const WidgetT *w, int32_t *startLine, int32_t *startCol, int32_t *endLine, int32_t *endCol) { - int32_t sl = w->as.ansiTerm->selStartLine; - int32_t sc = w->as.ansiTerm->selStartCol; - int32_t el = w->as.ansiTerm->selEndLine; - int32_t ec = w->as.ansiTerm->selEndCol; - - if (sl > el || (sl == el && sc > ec)) { - *startLine = el; - *startCol = ec; - *endLine = sl; - *endCol = sc; - } else { - *startLine = sl; - *startCol = sc; - *endLine = el; - *endCol = ec; - } + const AnsiTermDataT *at = (const AnsiTermDataT *)w->data; + int32_t sl = at->selStartLine, sc = at->selStartCol; + int32_t el = at->selEndLine, ec = at->selEndCol; + if (sl > el || (sl == el && sc > ec)) { *startLine = el; *startCol = ec; *endLine = sl; *endCol = sc; } + else { *startLine = sl; *startCol = sc; *endLine = el; *endCol = ec; } } -// ============================================================ -// wgtAnsiTerm -// ============================================================ - -// Create a new ANSI terminal widget. The cell buffer is allocated as a -// flat array of (char, attr) pairs -- rows * cols * 2 bytes. The separate -// heap allocation for AnsiTermDataT (via calloc) keeps the WidgetT union -// small; terminal state is substantial (~100+ bytes of fields plus the -// cell and scrollback buffers) so it's pointed to rather than inlined. -WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { - if (!parent) { - return NULL; - } - - if (cols <= 0) { - cols = 80; - } - - if (rows <= 0) { - rows = 25; - } - - WidgetT *w = widgetAlloc(parent, WidgetAnsiTermE); - - if (!w) { - return NULL; - } - - w->as.ansiTerm = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); - - if (!w->as.ansiTerm) { - free(w); - return NULL; - } - - int32_t cellCount = cols * rows; - w->as.ansiTerm->cells = (uint8_t *)malloc(cellCount * 2); - - if (!w->as.ansiTerm->cells) { - free(w->as.ansiTerm); - free(w); - return NULL; - } - - // Allocate scrollback buffer - int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; - w->as.ansiTerm->scrollback = (uint8_t *)malloc(sbMax * cols * 2); - - if (!w->as.ansiTerm->scrollback) { - free(w->as.ansiTerm->cells); - free(w->as.ansiTerm); - free(w); - return NULL; - } - - w->as.ansiTerm->cols = cols; - w->as.ansiTerm->rows = rows; - w->as.ansiTerm->cursorRow = 0; - w->as.ansiTerm->cursorCol = 0; - w->as.ansiTerm->cursorVisible = true; - w->as.ansiTerm->wrapMode = true; - w->as.ansiTerm->bold = false; - w->as.ansiTerm->originMode = false; - w->as.ansiTerm->csiPrivate = false; - w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm->parseState = PARSE_NORMAL; - w->as.ansiTerm->paramCount = 0; - w->as.ansiTerm->savedRow = 0; - w->as.ansiTerm->savedCol = 0; - w->as.ansiTerm->scrollTop = 0; - w->as.ansiTerm->scrollBot = rows - 1; - w->as.ansiTerm->scrollbackMax = sbMax; - w->as.ansiTerm->scrollbackCount = 0; - w->as.ansiTerm->scrollbackHead = 0; - w->as.ansiTerm->scrollPos = 0; - w->as.ansiTerm->commCtx = NULL; - w->as.ansiTerm->commRead = NULL; - w->as.ansiTerm->commWrite = NULL; - w->as.ansiTerm->selStartLine = -1; - w->as.ansiTerm->selStartCol = -1; - w->as.ansiTerm->selEndLine = -1; - w->as.ansiTerm->selEndCol = -1; - w->as.ansiTerm->selecting = false; - w->as.ansiTerm->blinkVisible = true; - w->as.ansiTerm->blinkTime = clock(); - w->as.ansiTerm->cursorOn = true; - w->as.ansiTerm->cursorTime = clock(); - w->as.ansiTerm->dirtyRows = 0xFFFFFFFF; - w->as.ansiTerm->lastCursorRow = -1; - w->as.ansiTerm->lastCursorCol = -1; - - memset(w->as.ansiTerm->params, 0, sizeof(w->as.ansiTerm->params)); - - // Initialize all cells to space with default attribute - for (int32_t i = 0; i < cellCount; i++) { - w->as.ansiTerm->cells[i * 2] = ' '; - w->as.ansiTerm->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; - } - - return w; -} - - -// ============================================================ -// wgtAnsiTermClear -// ============================================================ - void wgtAnsiTermClear(WidgetT *w) { - VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE); - - int32_t rows = w->as.ansiTerm->rows; - int32_t cols = w->as.ansiTerm->cols; - - // Push all visible lines to scrollback - bool wasAtBottom = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); - - for (int32_t r = 0; r < rows; r++) { - ansiTermAddToScrollback(w, r); - } - - if (wasAtBottom) { - w->as.ansiTerm->scrollPos = w->as.ansiTerm->scrollbackCount; - } - - // Clear the screen + VALIDATE_WIDGET_VOID(w, sTypeId); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t rows = at->rows; + int32_t cols = at->cols; + bool wasAtBottom = (at->scrollPos == at->scrollbackCount); + for (int32_t r = 0; r < rows; r++) { ansiTermAddToScrollback(w, r); } + if (wasAtBottom) { at->scrollPos = at->scrollbackCount; } int32_t cellCount = cols * rows; - - for (int32_t i = 0; i < cellCount; i++) { - w->as.ansiTerm->cells[i * 2] = ' '; - w->as.ansiTerm->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; - } - - w->as.ansiTerm->cursorRow = 0; - w->as.ansiTerm->cursorCol = 0; - w->as.ansiTerm->curAttr = ANSI_DEFAULT_ATTR; - w->as.ansiTerm->bold = false; - w->as.ansiTerm->parseState = PARSE_NORMAL; + for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } + at->cursorRow = 0; at->cursorCol = 0; at->curAttr = ANSI_DEFAULT_ATTR; + at->bold = false; at->parseState = PARSE_NORMAL; } -// ============================================================ -// wgtAnsiTermSetScrollback -// ============================================================ - -void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { - if (!w || w->type != WidgetAnsiTermE || maxLines <= 0) { - return; - } - - int32_t cols = w->as.ansiTerm->cols; - uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); - - if (!newBuf) { - return; - } - - free(w->as.ansiTerm->scrollback); - w->as.ansiTerm->scrollback = newBuf; - w->as.ansiTerm->scrollbackMax = maxLines; - w->as.ansiTerm->scrollbackCount = 0; - w->as.ansiTerm->scrollbackHead = 0; - w->as.ansiTerm->scrollPos = 0; -} - - -// ============================================================ -// wgtAnsiTermPoll -// ============================================================ - -// Poll the terminal for incoming data and update timers. This should be called -// from the application's main loop at a reasonable frequency. It handles three -// things: -// 1. Text blink timer (500ms) -- toggles visibility of cells with the blink -// attribute. Only rows containing blink cells are dirtied, avoiding -// unnecessary repaints of static content. -// 2. Cursor blink timer (250ms) -- faster than text blink to feel responsive. -// Only the cursor's row is dirtied. -// 3. Comm read -- pulls up to 256 bytes from the transport and feeds them -// through the ANSI parser. The 256-byte chunk size balances between -// responsiveness (smaller = more frequent repaints) and throughput -// (larger = fewer function call overhead per byte). int32_t wgtAnsiTermPoll(WidgetT *w) { - VALIDATE_WIDGET(w, WidgetAnsiTermE, 0); - - // Text blink timer -- toggle visibility and dirty rows with blinking cells + VALIDATE_WIDGET(w, sTypeId, 0); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; clock_t now = clock(); clock_t blinkInterval = (clock_t)BLINK_MS * CLOCKS_PER_SEC / 1000; clock_t curInterval = (clock_t)CURSOR_MS * CLOCKS_PER_SEC / 1000; - - if ((now - w->as.ansiTerm->blinkTime) >= blinkInterval) { - w->as.ansiTerm->blinkTime = now; - w->as.ansiTerm->blinkVisible = !w->as.ansiTerm->blinkVisible; - - // Dirty any rows that contain cells with blink attribute - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; - + if ((now - at->blinkTime) >= blinkInterval) { + at->blinkTime = now; at->blinkVisible = !at->blinkVisible; + int32_t cols = at->cols; int32_t rows = at->rows; for (int32_t row = 0; row < rows && row < 32; row++) { for (int32_t col = 0; col < cols; col++) { - uint8_t attr = w->as.ansiTerm->cells[(row * cols + col) * 2 + 1]; - - if (attr & ATTR_BLINK_BIT) { - w->as.ansiTerm->dirtyRows |= (1U << row); - break; - } + if (at->cells[(row * cols + col) * 2 + 1] & ATTR_BLINK_BIT) { at->dirtyRows |= (1U << row); break; } } } } - - // Cursor blink timer - if ((now - w->as.ansiTerm->cursorTime) >= curInterval) { - w->as.ansiTerm->cursorTime = now; - w->as.ansiTerm->cursorOn = !w->as.ansiTerm->cursorOn; - - int32_t cRow = w->as.ansiTerm->cursorRow; - - if (cRow >= 0 && cRow < 32) { - w->as.ansiTerm->dirtyRows |= (1U << cRow); - } + if ((now - at->cursorTime) >= curInterval) { + at->cursorTime = now; at->cursorOn = !at->cursorOn; + int32_t cRow = at->cursorRow; + if (cRow >= 0 && cRow < 32) { at->dirtyRows |= (1U << cRow); } } - - // Read from comm - if (!w->as.ansiTerm->commRead) { - return 0; - } - + if (!at->commRead) { return 0; } uint8_t buf[256]; - int32_t n = w->as.ansiTerm->commRead(w->as.ansiTerm->commCtx, buf, (int32_t)sizeof(buf)); - - if (n > 0) { - wgtAnsiTermWrite(w, buf, n); - } - + int32_t n = at->commRead(at->commCtx, buf, (int32_t)sizeof(buf)); + if (n > 0) { wgtAnsiTermWrite(w, buf, n); } return n > 0 ? n : 0; } -// ============================================================ -// wgtAnsiTermRepaint -// ============================================================ -// -// Fast repaint: renders only dirty rows directly into the window's -// content buffer, bypassing the full widget paint pipeline (no clear, -// no relayout, no other widgets). This keeps ACK turnaround fast -// for the serial link. - -// Fast repaint: renders dirty rows directly into the window's content buffer, -// completely bypassing the normal widget paint pipeline (widgetOnPaint -> -// widgetPaintOne -> full tree walk). Returns the number of rows repainted -// and optionally reports the vertical extent via outY/outH so the caller -// can issue a minimal compositor dirty rect. -// -// This exists because the normal paint path clears the entire content area -// and repaints all widgets, which is far too expensive to do on every -// incoming serial byte. With fast repaint, the path from data reception -// to LFB flush is: poll -> write -> dirtyRows -> repaint -> compositor dirty, -// keeping the round-trip under 1ms on a Pentium. int32_t wgtAnsiTermRepaint(WidgetT *w, int32_t *outY, int32_t *outH) { - if (!w || w->type != WidgetAnsiTermE || !w->window) { - return 0; + if (!w || w->type != sTypeId || !w->window) { return 0; } + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t prevRow = at->lastCursorRow; + int32_t curRow = at->cursorRow; + if (prevRow != curRow || at->lastCursorCol != at->cursorCol) { + if (prevRow >= 0 && prevRow < at->rows) { at->dirtyRows |= (1U << prevRow); } + if (curRow >= 0 && curRow < at->rows) { at->dirtyRows |= (1U << curRow); } } - - // Always repaint the row the cursor was on last time (to erase it) - // and the row it's on now (to draw it) - int32_t prevRow = w->as.ansiTerm->lastCursorRow; - int32_t curRow = w->as.ansiTerm->cursorRow; - int32_t prevCol = w->as.ansiTerm->lastCursorCol; - int32_t curCol = w->as.ansiTerm->cursorCol; - - if (prevRow != curRow || prevCol != curCol) { - if (prevRow >= 0 && prevRow < w->as.ansiTerm->rows) { - w->as.ansiTerm->dirtyRows |= (1U << prevRow); - } - if (curRow >= 0 && curRow < w->as.ansiTerm->rows) { - w->as.ansiTerm->dirtyRows |= (1U << curRow); - } - } - - uint32_t dirty = w->as.ansiTerm->dirtyRows; - if (dirty == 0) { - return 0; - } - + uint32_t dirty = at->dirtyRows; + if (dirty == 0) { return 0; } WindowT *win = w->window; - if (!win->contentBuf || !win->widgetRoot) { - return 0; - } - + if (!win->contentBuf || !win->widgetRoot) { return 0; } AppContextT *ctx = wgtGetContext(win->widgetRoot); - if (!ctx) { - return 0; - } - - // Set up a temporary DisplayT pointing at the window's content buffer - // instead of the backbuffer. This lets us reuse all the drawing - // primitives (drawTermRow, etc.) while rendering directly into the - // window's private content bitmap. - DisplayT cd = ctx->display; - cd.backBuf = win->contentBuf; - cd.width = win->contentW; - cd.height = win->contentH; - cd.pitch = win->contentPitch; - cd.clipX = 0; - cd.clipY = 0; - cd.clipW = win->contentW; - cd.clipH = win->contentH; - - const BlitOpsT *ops = &ctx->blitOps; + if (!ctx) { return 0; } + DisplayT cd = ctx->display; + cd.backBuf = win->contentBuf; cd.width = win->contentW; cd.height = win->contentH; + cd.pitch = win->contentPitch; cd.clipX = 0; cd.clipY = 0; cd.clipW = win->contentW; cd.clipH = win->contentH; + const BlitOpsT *ops = &ctx->blitOps; const BitmapFontT *font = &ctx->font; - - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; + int32_t cols = at->cols; int32_t rows = at->rows; int32_t cellH = font->charHeight; - int32_t baseX = w->x + ANSI_BORDER; - int32_t baseY = w->y + ANSI_BORDER; - - // Use cached palette + int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; ansiTermBuildPalette(w, &cd); - const uint32_t *palette = w->as.ansiTerm->packedPalette; - - bool viewingLive = (w->as.ansiTerm->scrollPos == w->as.ansiTerm->scrollbackCount); - int32_t repainted = 0; - int32_t minRow = rows; - int32_t maxRow = -1; - + const uint32_t *palette = at->packedPalette; + bool viewingLive = (at->scrollPos == at->scrollbackCount); + int32_t repainted = 0; int32_t minRow = rows; int32_t maxRow = -1; for (int32_t row = 0; row < rows; row++) { - if (!(dirty & (1U << row))) { - continue; - } - - int32_t lineIndex = w->as.ansiTerm->scrollPos + row; + if (!(dirty & (1U << row))) { continue; } + int32_t lineIndex = at->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - - // Cursor column for this row (-1 if cursor not on this row) int32_t curCol2 = -1; - if (viewingLive && w->as.ansiTerm->cursorVisible && w->as.ansiTerm->cursorOn && - row == w->as.ansiTerm->cursorRow) { - curCol2 = w->as.ansiTerm->cursorCol; - } - - drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm->blinkVisible, curCol2); + if (viewingLive && at->cursorVisible && at->cursorOn && row == at->cursorRow) { curCol2 = at->cursorCol; } + drawTermRow(&cd, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol2); ansiTermPaintSelRow(w, &cd, ops, font, row, baseX, baseY); - if (row < minRow) { minRow = row; } if (row > maxRow) { maxRow = row; } repainted++; } - - w->as.ansiTerm->dirtyRows = 0; - w->as.ansiTerm->lastCursorRow = w->as.ansiTerm->cursorRow; - w->as.ansiTerm->lastCursorCol = w->as.ansiTerm->cursorCol; - + at->dirtyRows = 0; at->lastCursorRow = at->cursorRow; at->lastCursorCol = at->cursorCol; if (outY) { *outY = baseY + minRow * cellH; } if (outH) { *outH = (maxRow - minRow + 1) * cellH; } - return repainted; } -// ============================================================ -// wgtAnsiTermSetComm -// ============================================================ - -// Attach communication callbacks to the terminal. The read/write function -// pointers are transport-agnostic -- the terminal doesn't care whether -// data comes from a raw UART, the secLink encrypted channel, or a -// socket-based proxy. The ctx pointer is passed through opaquely. -void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { - VALIDATE_WIDGET_VOID(w, WidgetAnsiTermE); - - w->as.ansiTerm->commCtx = ctx; - w->as.ansiTerm->commRead = readFn; - w->as.ansiTerm->commWrite = writeFn; -} - - -// ============================================================ -// wgtAnsiTermWrite -// ============================================================ - -// Write raw bytes into the terminal for parsing and display. Any active -// selection is cleared first since the screen content is changing and the -// selection coordinates would become stale. Each byte is fed through the -// ANSI parser individually -- the parser maintains state between calls so -// multi-byte sequences split across writes are handled correctly. void wgtAnsiTermWrite(WidgetT *w, const uint8_t *data, int32_t len) { - if (!w || w->type != WidgetAnsiTermE || !data || len <= 0) { - return; - } - + if (!w || w->type != sTypeId || !data || len <= 0) { return; } ansiTermClearSelection(w); - - for (int32_t i = 0; i < len; i++) { - ansiTermProcessByte(w, data[i]); - } + for (int32_t i = 0; i < len; i++) { ansiTermProcessByte(w, data[i]); } } -// ============================================================ -// widgetAnsiTermDestroy -// ============================================================ - void widgetAnsiTermDestroy(WidgetT *w) { - free(w->as.ansiTerm->cells); - free(w->as.ansiTerm->scrollback); - free(w->as.ansiTerm); - w->as.ansiTerm = NULL; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + if (at) { free(at->cells); free(at->scrollback); free(at); w->data = NULL; } } -// ============================================================ -// widgetAnsiTermCalcMinSize -// ============================================================ - -// Min size = exact pixel dimensions needed for the grid plus border and -// scrollbar. The terminal is not designed to be resizable -- the grid -// dimensions (cols x rows) are fixed at creation time, matching the BBS -// convention of 80x25 or similar fixed screen sizes. void widgetAnsiTermCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = w->as.ansiTerm->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; - w->calcMinH = w->as.ansiTerm->rows * font->charHeight + ANSI_BORDER * 2; + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + w->calcMinW = at->cols * font->charWidth + ANSI_BORDER * 2 + ANSI_SB_W; + w->calcMinH = at->rows * font->charHeight + ANSI_BORDER * 2; } -// ============================================================ -// ansiTermPaintSelRow -// ============================================================ -// -// Overlay selection highlighting for a single terminal row. -// Selected cells are redrawn with fg/bg swapped (fg becomes bg and vice versa), -// which is the traditional terminal selection highlight method. This avoids -// needing a separate "selection color" and works regardless of the underlying -// cell colors. The swap is done in palette-index space (not pixel space) so -// it's just a few index lookups, keeping the per-cell cost minimal. - static void ansiTermPaintSelRow(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, int32_t screenRow, int32_t baseX, int32_t baseY) { - if (!ansiTermHasSelection(w)) { - return; - } - - int32_t sLine; - int32_t sCol; - int32_t eLine; - int32_t eCol; + if (!ansiTermHasSelection(w)) { return; } + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t sLine, sCol, eLine, eCol; ansiTermSelectionRange(w, &sLine, &sCol, &eLine, &eCol); - - int32_t cols = w->as.ansiTerm->cols; - int32_t lineIndex = w->as.ansiTerm->scrollPos + screenRow; - - if (lineIndex < sLine || lineIndex > eLine) { - return; - } - + int32_t cols = at->cols; + int32_t lineIndex = at->scrollPos + screenRow; + if (lineIndex < sLine || lineIndex > eLine) { return; } int32_t colStart = (lineIndex == sLine) ? sCol : 0; int32_t colEnd = (lineIndex == eLine) ? eCol : cols; - - if (colStart >= colEnd) { - return; - } - - const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - const uint32_t *palette = w->as.ansiTerm->packedPalette; - int32_t cellH = font->charHeight; - int32_t cellW = font->charWidth; - + if (colStart >= colEnd) { return; } + const uint8_t *lineData = ansiTermGetLine(w, lineIndex); + const uint32_t *palette = at->packedPalette; + int32_t cellH = font->charHeight; + int32_t cellW = font->charWidth; for (int32_t col = colStart; col < colEnd; col++) { - uint8_t ch = lineData[col * 2]; - uint8_t attr = lineData[col * 2 + 1]; - uint32_t fg = palette[(attr >> 4) & 0x07]; // swap: bg becomes fg - uint32_t bg = palette[attr & 0x0F]; // swap: fg becomes bg - + uint8_t ch = lineData[col * 2]; + uint8_t attr = lineData[col * 2 + 1]; + uint32_t fg = palette[(attr >> 4) & 0x07]; + uint32_t bg = palette[attr & 0x0F]; drawChar(d, ops, font, baseX + col * cellW, baseY + screenRow * cellH, ch, fg, bg, true); } } -// ============================================================ -// widgetAnsiTermOnKey -// ============================================================ -// -// Translate keyboard input to ANSI escape sequences and send -// via the comm interface. Does nothing if commWrite is NULL. - -// Key handling for the terminal. Keyboard input is translated to ANSI -// escape sequences and transmitted via the comm interface. The key codes -// use the BIOS INT 16h convention: extended keys have bit 0x100 set, with -// the scan code in the low byte. This matches the dvxApp keyboard layer. -// -// Ctrl+C has dual behavior: copy selection if one exists, otherwise send -// the ^C control character to the remote (for breaking running programs). -// Ctrl+V pastes clipboard contents to the terminal as keystrokes. void widgetAnsiTermOnKey(WidgetT *w, int32_t key, int32_t mod) { - // Ctrl+C: copy if selection exists, otherwise send ^C + AnsiTermDataT *at = (AnsiTermDataT *)w->data; if (key == 0x03 && (mod & KEY_MOD_CTRL)) { - if (ansiTermHasSelection(w)) { - ansiTermCopySelection(w); - ansiTermClearSelection(w); - wgtInvalidatePaint(w); - return; - } - - // No selection -- fall through to send ^C to terminal + if (ansiTermHasSelection(w)) { ansiTermCopySelection(w); ansiTermClearSelection(w); wgtInvalidatePaint(w); return; } } - - // Ctrl+V: paste from clipboard to terminal - if (key == 0x16 && (mod & KEY_MOD_CTRL)) { - ansiTermPasteToComm(w); - wgtInvalidatePaint(w); - return; - } - - if (!w->as.ansiTerm->commWrite) { - return; - } - - // Any keypress clears selection - if (ansiTermHasSelection(w)) { - ansiTermClearSelection(w); - } - + if (key == 0x16 && (mod & KEY_MOD_CTRL)) { ansiTermPasteToComm(w); wgtInvalidatePaint(w); return; } + if (!at->commWrite) { return; } + if (ansiTermHasSelection(w)) { ansiTermClearSelection(w); } uint8_t buf[8]; int32_t len = 0; - - if (key >= 32 && key < 127) { - // Printable ASCII - buf[0] = (uint8_t)key; - len = 1; - } else if (key == 0x1B) { - // Escape - buf[0] = 0x1B; - len = 1; - } else if (key == 0x09) { - // Tab - buf[0] = 0x09; - len = 1; - } else if (key == 13 || key == 10) { - // Enter - buf[0] = '\r'; - len = 1; - } else if (key == 8) { - // Backspace - buf[0] = 0x08; - len = 1; - } else if (key == (0x48 | 0x100)) { - // Up arrow -> ESC[A - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; - len = 3; - } else if (key == (0x50 | 0x100)) { - // Down arrow -> ESC[B - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; - len = 3; - } else if (key == (0x4D | 0x100)) { - // Right arrow -> ESC[C - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; - len = 3; - } else if (key == (0x4B | 0x100)) { - // Left arrow -> ESC[D - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; - len = 3; - } else if (key == (0x47 | 0x100)) { - // Home -> ESC[H - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; - len = 3; - } else if (key == (0x4F | 0x100)) { - // End -> ESC[F - buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; - len = 3; - } else if (key == (0x49 | 0x100)) { - // PgUp -> ESC[5~ - buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; - len = 4; - } else if (key == (0x51 | 0x100)) { - // PgDn -> ESC[6~ - buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; - len = 4; - } else if (key == (0x53 | 0x100)) { - // Delete -> ESC[3~ - buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; - len = 4; - } else if (key >= 1 && key < 32) { - // Control characters (^A=1, ^B=2, ^C=3, etc.) - buf[0] = (uint8_t)key; - len = 1; - } - - if (len > 0) { - w->as.ansiTerm->commWrite(w->as.ansiTerm->commCtx, buf, len); - } - + if (key >= 32 && key < 127) { buf[0] = (uint8_t)key; len = 1; } + else if (key == 0x1B) { buf[0] = 0x1B; len = 1; } + else if (key == 0x09) { buf[0] = 0x09; len = 1; } + else if (key == 13 || key == 10) { buf[0] = '\r'; len = 1; } + else if (key == 8) { buf[0] = 0x08; len = 1; } + else if (key == (0x48 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'A'; len = 3; } + else if (key == (0x50 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'B'; len = 3; } + else if (key == (0x4D | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'C'; len = 3; } + else if (key == (0x4B | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'D'; len = 3; } + else if (key == (0x47 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'H'; len = 3; } + else if (key == (0x4F | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = 'F'; len = 3; } + else if (key == (0x49 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '5'; buf[3] = '~'; len = 4; } + else if (key == (0x51 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '6'; buf[3] = '~'; len = 4; } + else if (key == (0x53 | 0x100)) { buf[0] = 0x1B; buf[1] = '['; buf[2] = '3'; buf[3] = '~'; len = 4; } + else if (key >= 1 && key < 32) { buf[0] = (uint8_t)key; len = 1; } + if (len > 0) { at->commWrite(at->commCtx, buf, len); } wgtInvalidatePaint(w); } -// ============================================================ -// widgetAnsiTermOnMouse -// ============================================================ -// -// Handle mouse clicks: scrollbar interaction and focus. - -// Mouse handling: the text area (left of the scrollbar) supports click-to-select -// with single-click (anchor), double-click (word), and triple-click (line). -// The scrollbar area uses direct hit testing on up/down arrow buttons and -// page-up/page-down regions, with proportional thumb positioning. -// Selection drag is handled externally by the widget event dispatcher via -// sDragTextSelect -- on mouse-down we set the anchor and enable drag mode. void widgetAnsiTermOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { + AnsiTermDataT *at = (AnsiTermDataT *)hit->data; AppContextT *actx = (AppContextT *)root->userData; const BitmapFontT *font = &actx->font; hit->focused = true; clearOtherSelections(hit); - - int32_t cols = hit->as.ansiTerm->cols; - int32_t rows = hit->as.ansiTerm->rows; - int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; - int32_t sbY = hit->y + ANSI_BORDER; - int32_t sbH = rows * font->charHeight; + int32_t cols = at->cols; + int32_t rows = at->rows; + int32_t sbX = hit->x + ANSI_BORDER + cols * font->charWidth; + int32_t sbY = hit->y + ANSI_BORDER; + int32_t sbH = rows * font->charHeight; int32_t arrowH = ANSI_SB_W; - // Click in text area -- start selection if (vx < sbX) { int32_t baseX = hit->x + ANSI_BORDER; int32_t baseY = hit->y + ANSI_BORDER; int32_t clickRow = (vy - baseY) / font->charHeight; int32_t clickCol = (vx - baseX) / font->charWidth; - - if (clickRow < 0) { - clickRow = 0; - } - - if (clickRow >= rows) { - clickRow = rows - 1; - } - - if (clickCol < 0) { - clickCol = 0; - } - - if (clickCol >= cols) { - clickCol = cols - 1; - } - - int32_t lineIndex = hit->as.ansiTerm->scrollPos + clickRow; - + if (clickRow < 0) { clickRow = 0; } + if (clickRow >= rows) { clickRow = rows - 1; } + if (clickCol < 0) { clickCol = 0; } + if (clickCol >= cols) { clickCol = cols - 1; } + int32_t lineIndex = at->scrollPos + clickRow; int32_t clicks = multiClickDetect(vx, vy); - if (clicks >= 3) { - // Triple-click: select entire line - hit->as.ansiTerm->selStartLine = lineIndex; - hit->as.ansiTerm->selStartCol = 0; - hit->as.ansiTerm->selEndLine = lineIndex; - hit->as.ansiTerm->selEndCol = cols; - hit->as.ansiTerm->selecting = false; - sDragTextSelect = NULL; + at->selStartLine = lineIndex; at->selStartCol = 0; + at->selEndLine = lineIndex; at->selEndCol = cols; + at->selecting = false; sDragTextSelect = NULL; } else if (clicks == 2) { - // Double-click: select word const uint8_t *lineData = ansiTermGetLine(hit, lineIndex); - int32_t ws = clickCol; - int32_t we = clickCol; - - while (ws > 0 && isWordChar((char)lineData[(ws - 1) * 2])) { - ws--; - } - - while (we < cols && isWordChar((char)lineData[we * 2])) { - we++; - } - - hit->as.ansiTerm->selStartLine = lineIndex; - hit->as.ansiTerm->selStartCol = ws; - hit->as.ansiTerm->selEndLine = lineIndex; - hit->as.ansiTerm->selEndCol = we; - hit->as.ansiTerm->selecting = false; - sDragTextSelect = NULL; + int32_t ws = clickCol; int32_t we = clickCol; + while (ws > 0 && isWordChar((char)lineData[(ws - 1) * 2])) { ws--; } + while (we < cols && isWordChar((char)lineData[we * 2])) { we++; } + at->selStartLine = lineIndex; at->selStartCol = ws; + at->selEndLine = lineIndex; at->selEndCol = we; + at->selecting = false; sDragTextSelect = NULL; } else { - // Single click: start selection anchor - hit->as.ansiTerm->selStartLine = lineIndex; - hit->as.ansiTerm->selStartCol = clickCol; - hit->as.ansiTerm->selEndLine = lineIndex; - hit->as.ansiTerm->selEndCol = clickCol; - hit->as.ansiTerm->selecting = true; - sDragTextSelect = hit; + at->selStartLine = lineIndex; at->selStartCol = clickCol; + at->selEndLine = lineIndex; at->selEndCol = clickCol; + at->selecting = true; sDragTextSelect = hit; } - - hit->as.ansiTerm->dirtyRows = 0xFFFFFFFF; - return; - } - - int32_t sbCount = hit->as.ansiTerm->scrollbackCount; - - if (sbCount == 0) { + at->dirtyRows = 0xFFFFFFFF; return; } + int32_t sbCount = at->scrollbackCount; + if (sbCount == 0) { return; } int32_t maxScroll = sbCount; - - if (vy >= sbY && vy < sbY + arrowH) { - // Up arrow - hit->as.ansiTerm->scrollPos--; - } else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { - // Down arrow - hit->as.ansiTerm->scrollPos++; - } else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { - // Track area -- compute thumb position to determine page direction - int32_t trackY = sbY + arrowH; - int32_t trackH = sbH - arrowH * 2; - - if (trackH <= 0) { - return; - } - + if (vy >= sbY && vy < sbY + arrowH) { at->scrollPos--; } + else if (vy >= sbY + sbH - arrowH && vy < sbY + sbH) { at->scrollPos++; } + else if (vy >= sbY + arrowH && vy < sbY + sbH - arrowH) { + int32_t trackY = sbY + arrowH; int32_t trackH = sbH - arrowH * 2; + if (trackH <= 0) { return; } int32_t totalLines = sbCount + rows; - int32_t thumbH = (rows * trackH) / totalLines; - - if (thumbH < 8) { - thumbH = 8; - } - + int32_t thumbH = (rows * trackH) / totalLines; + if (thumbH < 8) { thumbH = 8; } int32_t thumbRange = trackH - thumbH; - int32_t thumbY = trackY; - - if (maxScroll > 0 && thumbRange > 0) { - thumbY = trackY + (hit->as.ansiTerm->scrollPos * thumbRange) / maxScroll; - } - - if (vy < thumbY) { - // Page up - hit->as.ansiTerm->scrollPos -= rows; - } else if (vy >= thumbY + thumbH) { - // Page down - hit->as.ansiTerm->scrollPos += rows; - } - } - - // Clamp - if (hit->as.ansiTerm->scrollPos < 0) { - hit->as.ansiTerm->scrollPos = 0; - } - - if (hit->as.ansiTerm->scrollPos > maxScroll) { - hit->as.ansiTerm->scrollPos = maxScroll; + int32_t thumbY = trackY; + if (maxScroll > 0 && thumbRange > 0) { thumbY = trackY + (at->scrollPos * thumbRange) / maxScroll; } + if (vy < thumbY) { at->scrollPos -= rows; } + else if (vy >= thumbY + thumbH) { at->scrollPos += rows; } } + if (at->scrollPos < 0) { at->scrollPos = 0; } + if (at->scrollPos > maxScroll) { at->scrollPos = maxScroll; } } -// ============================================================ -// widgetAnsiTermPaint -// ============================================================ - -// Full paint: renders the complete terminal widget including border, all text -// rows, selection overlay, and scrollbar. This is called through the normal -// widget paint pipeline (e.g., on window expose or full invalidation). -// For incremental updates during data reception, wgtAnsiTermRepaint is used -// instead -- it's much faster since it only repaints dirty rows and skips -// the border/scrollbar. -// -// The terminal renders its own scrollbar rather than using the shared -// widgetDrawScrollbarV because the scrollbar's total/visible/position -// semantics are different -- the terminal scrollbar represents scrollback -// lines (historical content above the screen), not a viewport over a -// virtual content area. void widgetAnsiTermPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - // Draw sunken bevel border + AnsiTermDataT *at = (AnsiTermDataT *)w->data; BevelStyleT bevel; - bevel.highlight = colors->windowShadow; - bevel.shadow = colors->windowHighlight; - bevel.face = 0; - bevel.width = ANSI_BORDER; + bevel.highlight = colors->windowShadow; bevel.shadow = colors->windowHighlight; + bevel.face = 0; bevel.width = ANSI_BORDER; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); - - // Build/cache the 16-color packed palette ansiTermBuildPalette(w, d); - const uint32_t *palette = w->as.ansiTerm->packedPalette; - - int32_t cols = w->as.ansiTerm->cols; - int32_t rows = w->as.ansiTerm->rows; - int32_t cellW = font->charWidth; - int32_t cellH = font->charHeight; - int32_t baseX = w->x + ANSI_BORDER; - int32_t baseY = w->y + ANSI_BORDER; - int32_t sbCount = w->as.ansiTerm->scrollbackCount; - - // Determine if viewing live terminal or scrollback - bool viewingLive = (w->as.ansiTerm->scrollPos == sbCount); - - // Render character cells row by row using bulk renderer. - // Only repaint rows marked dirty; 0xFFFFFFFF means all rows. - uint32_t dirty = w->as.ansiTerm->dirtyRows; - if (dirty == 0) { - dirty = 0xFFFFFFFF; - } - + const uint32_t *palette = at->packedPalette; + int32_t cols = at->cols; int32_t rows = at->rows; + int32_t cellW = font->charWidth; int32_t cellH = font->charHeight; + int32_t baseX = w->x + ANSI_BORDER; int32_t baseY = w->y + ANSI_BORDER; + int32_t sbCount = at->scrollbackCount; + bool viewingLive = (at->scrollPos == sbCount); + uint32_t dirty = at->dirtyRows; + if (dirty == 0) { dirty = 0xFFFFFFFF; } for (int32_t row = 0; row < rows; row++) { - if (row < 32 && !(dirty & (1U << row))) { - continue; - } - - int32_t lineIndex = w->as.ansiTerm->scrollPos + row; + if (row < 32 && !(dirty & (1U << row))) { continue; } + int32_t lineIndex = at->scrollPos + row; const uint8_t *lineData = ansiTermGetLine(w, lineIndex); - - // Cursor column for this row (-1 if cursor not on this row) int32_t curCol = -1; - if (viewingLive && w->as.ansiTerm->cursorVisible && w->as.ansiTerm->cursorOn && - row == w->as.ansiTerm->cursorRow) { - curCol = w->as.ansiTerm->cursorCol; - } - - drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, w->as.ansiTerm->blinkVisible, curCol); + if (viewingLive && at->cursorVisible && at->cursorOn && row == at->cursorRow) { curCol = at->cursorCol; } + drawTermRow(d, ops, font, baseX, baseY + row * cellH, cols, lineData, palette, at->blinkVisible, curCol); ansiTermPaintSelRow(w, d, ops, font, row, baseX, baseY); } + at->dirtyRows = 0; - w->as.ansiTerm->dirtyRows = 0; - - // Draw scrollbar - int32_t sbX = baseX + cols * cellW; - int32_t sbY = baseY; - int32_t sbW = ANSI_SB_W; - int32_t sbH = rows * cellH; + // Scrollbar + int32_t sbX = baseX + cols * cellW; int32_t sbY = baseY; + int32_t sbW = ANSI_SB_W; int32_t sbH = rows * cellH; int32_t arrowH = ANSI_SB_W; - - if (sbCount == 0) { - // No scrollback -- fill scrollbar area with trough color - rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); - return; - } - - // Track background - int32_t trackY = sbY + arrowH; - int32_t trackH = sbH - arrowH * 2; + if (sbCount == 0) { rectFill(d, ops, sbX, sbY, sbW, sbH, colors->scrollbarTrough); return; } + int32_t trackY = sbY + arrowH; int32_t trackH = sbH - arrowH * 2; rectFill(d, ops, sbX, trackY, sbW, trackH, colors->scrollbarTrough); - - // Up arrow button BevelStyleT btnBevel; - btnBevel.highlight = colors->windowHighlight; - btnBevel.shadow = colors->windowShadow; - btnBevel.face = colors->scrollbarBg; - btnBevel.width = 1; + btnBevel.highlight = colors->windowHighlight; btnBevel.shadow = colors->windowShadow; + btnBevel.face = colors->scrollbarBg; btnBevel.width = 1; drawBevel(d, ops, sbX, sbY, sbW, arrowH, &btnBevel); - - // Up arrow glyph (small triangle) - int32_t arrowMidX = sbX + sbW / 2; - int32_t arrowMidY = sbY + arrowH / 2; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, arrowMidX - i, arrowMidY - 1 + i, i * 2 + 1, colors->scrollbarFg); - } - - // Down arrow button + int32_t arrowMidX = sbX + sbW / 2; int32_t arrowMidY = sbY + arrowH / 2; + for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, arrowMidX - i, arrowMidY - 1 + i, i * 2 + 1, colors->scrollbarFg); } drawBevel(d, ops, sbX, sbY + sbH - arrowH, sbW, arrowH, &btnBevel); - arrowMidY = sbY + sbH - arrowH + arrowH / 2; - - for (int32_t i = 0; i < 4; i++) { - drawHLine(d, ops, arrowMidX - i, arrowMidY + 1 - i, i * 2 + 1, colors->scrollbarFg); - } - - // Thumb + for (int32_t i = 0; i < 4; i++) { drawHLine(d, ops, arrowMidX - i, arrowMidY + 1 - i, i * 2 + 1, colors->scrollbarFg); } if (trackH > 0) { int32_t totalLines = sbCount + rows; - int32_t thumbH = (rows * trackH) / totalLines; - - if (thumbH < 8) { - thumbH = 8; - } - - int32_t thumbRange = trackH - thumbH; - int32_t maxScroll = sbCount; - int32_t thumbY = trackY; - - if (maxScroll > 0 && thumbRange > 0) { - thumbY = trackY + (w->as.ansiTerm->scrollPos * thumbRange) / maxScroll; - } - + int32_t thumbH = (rows * trackH) / totalLines; + if (thumbH < 8) { thumbH = 8; } + int32_t thumbRange = trackH - thumbH; int32_t maxScroll = sbCount; + int32_t thumbY = trackY; + if (maxScroll > 0 && thumbRange > 0) { thumbY = trackY + (at->scrollPos * thumbRange) / maxScroll; } drawBevel(d, ops, sbX, thumbY, sbW, thumbH, &btnBevel); } - - if (w->focused) { - drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); - } + if (w->focused) { drawFocusRect(d, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg); } } -// ============================================================ -// DXE registration -// ============================================================ +void widgetAnsiTermPollVtable(WidgetT *w, WindowT *win) { + (void)win; + wgtAnsiTermPoll(w); +} + static const WidgetClassT sClassAnsiTerm = { - .flags = WCLASS_FOCUSABLE, + .flags = WCLASS_FOCUSABLE | WCLASS_SCROLLABLE | WCLASS_NEEDS_POLL | WCLASS_SWALLOWS_TAB, .paint = widgetAnsiTermPaint, .paintOverlay = NULL, .calcMinSize = widgetAnsiTermCalcMinSize, @@ -2157,10 +918,79 @@ static const WidgetClassT sClassAnsiTerm = { .onKey = widgetAnsiTermOnKey, .destroy = widgetAnsiTermDestroy, .getText = NULL, - .setText = NULL + .setText = NULL, + .poll = widgetAnsiTermPollVtable, + .quickRepaint = wgtAnsiTermRepaint }; -void wgtRegister(void) { - widgetClassTable[WidgetAnsiTermE] = &sClassAnsiTerm; +WidgetT *wgtAnsiTerm(WidgetT *parent, int32_t cols, int32_t rows) { + if (!parent) { return NULL; } + if (cols <= 0) { cols = 80; } + if (rows <= 0) { rows = 25; } + WidgetT *w = widgetAlloc(parent, sTypeId); + if (!w) { return NULL; } + AnsiTermDataT *at = (AnsiTermDataT *)calloc(1, sizeof(AnsiTermDataT)); + if (!at) { free(w); return NULL; } + w->data = at; + int32_t cellCount = cols * rows; + at->cells = (uint8_t *)malloc(cellCount * 2); + if (!at->cells) { free(at); free(w); return NULL; } + int32_t sbMax = ANSI_DEFAULT_SCROLLBACK; + at->scrollback = (uint8_t *)malloc(sbMax * cols * 2); + if (!at->scrollback) { free(at->cells); free(at); free(w); return NULL; } + at->cols = cols; at->rows = rows; + at->cursorVisible = true; at->wrapMode = true; + at->curAttr = ANSI_DEFAULT_ATTR; at->parseState = PARSE_NORMAL; + at->scrollBot = rows - 1; at->scrollbackMax = sbMax; + at->selStartLine = -1; at->selStartCol = -1; + at->selEndLine = -1; at->selEndCol = -1; + at->blinkVisible = true; at->blinkTime = clock(); + at->cursorOn = true; at->cursorTime = clock(); + at->dirtyRows = 0xFFFFFFFF; at->lastCursorRow = -1; at->lastCursorCol = -1; + for (int32_t i = 0; i < cellCount; i++) { at->cells[i * 2] = ' '; at->cells[i * 2 + 1] = ANSI_DEFAULT_ATTR; } + return w; +} + + +void wgtAnsiTermSetComm(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)) { + VALIDATE_WIDGET_VOID(w, sTypeId); + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + at->commCtx = ctx; at->commRead = readFn; at->commWrite = writeFn; +} + + +void wgtAnsiTermSetScrollback(WidgetT *w, int32_t maxLines) { + if (!w || w->type != sTypeId || maxLines <= 0) { return; } + AnsiTermDataT *at = (AnsiTermDataT *)w->data; + int32_t cols = at->cols; + uint8_t *newBuf = (uint8_t *)malloc(maxLines * cols * 2); + if (!newBuf) { return; } + free(at->scrollback); + at->scrollback = newBuf; at->scrollbackMax = maxLines; + at->scrollbackCount = 0; at->scrollbackHead = 0; at->scrollPos = 0; +} + + +static const struct { + WidgetT *(*create)(WidgetT *parent, int32_t cols, int32_t rows); + void (*write)(WidgetT *w, const uint8_t *data, int32_t len); + void (*clear)(WidgetT *w); + void (*setComm)(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)); + void (*setScrollback)(WidgetT *w, int32_t maxLines); + int32_t (*poll)(WidgetT *w); + int32_t (*repaint)(WidgetT *w, int32_t *outY, int32_t *outH); +} sApi = { + .create = wgtAnsiTerm, + .write = wgtAnsiTermWrite, + .clear = wgtAnsiTermClear, + .setComm = wgtAnsiTermSetComm, + .setScrollback = wgtAnsiTermSetScrollback, + .poll = wgtAnsiTermPoll, + .repaint = wgtAnsiTermRepaint +}; + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClassAnsiTerm); + wgtRegisterApi("ansiterm", &sApi); } diff --git a/widgets/widgetAnsiTerm.h b/widgets/widgetAnsiTerm.h new file mode 100644 index 00000000..96430139 --- /dev/null +++ b/widgets/widgetAnsiTerm.h @@ -0,0 +1,31 @@ +// widgetAnsiTerm.h -- ANSI terminal widget API +#ifndef WIDGET_ANSITERM_H +#define WIDGET_ANSITERM_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent, int32_t cols, int32_t rows); + void (*write)(WidgetT *w, const uint8_t *data, int32_t len); + void (*clear)(WidgetT *w); + void (*setComm)(WidgetT *w, void *ctx, int32_t (*readFn)(void *, uint8_t *, int32_t), int32_t (*writeFn)(void *, const uint8_t *, int32_t)); + void (*setScrollback)(WidgetT *w, int32_t maxLines); + void (*poll)(WidgetT *w); + int32_t (*repaint)(WidgetT *w, int32_t *outY, int32_t *outH); +} AnsiTermApiT; + +static inline const AnsiTermApiT *dvxAnsiTermApi(void) { + static const AnsiTermApiT *sApi; + if (!sApi) { sApi = (const AnsiTermApiT *)wgtGetApi("ansiterm"); } + return sApi; +} + +#define wgtAnsiTerm(parent, cols, rows) dvxAnsiTermApi()->create(parent, cols, rows) +#define wgtAnsiTermWrite(w, data, len) dvxAnsiTermApi()->write(w, data, len) +#define wgtAnsiTermClear(w) dvxAnsiTermApi()->clear(w) +#define wgtAnsiTermSetComm(w, ctx, readFn, writeFn) dvxAnsiTermApi()->setComm(w, ctx, readFn, writeFn) +#define wgtAnsiTermSetScrollback(w, maxLines) dvxAnsiTermApi()->setScrollback(w, maxLines) +#define wgtAnsiTermPoll(w) dvxAnsiTermApi()->poll(w) +#define wgtAnsiTermRepaint(w, outY, outH) dvxAnsiTermApi()->repaint(w, outY, outH) + +#endif // WIDGET_ANSITERM_H diff --git a/widgets/widgetBox.c b/widgets/widgetBox.c index b375c6f3..85709db5 100644 --- a/widgets/widgetBox.c +++ b/widgets/widgetBox.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetBox.c -- VBox, HBox, and Frame container widgets // // VBox and HBox are the primary layout containers. They have no visual @@ -18,7 +19,23 @@ // "break" the border behind the title -- this is the classic Win3.1/Motif // group box appearance. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" + +static int32_t sVBoxTypeId = -1; +static int32_t sHBoxTypeId = -1; +static int32_t sFrameTypeId = -1; + +typedef enum { + FrameInE, + FrameOutE, + FrameFlatE +} FrameStyleE; + +typedef struct { + const char *title; + FrameStyleE style; + uint32_t color; +} FrameDataT; // ============================================================ @@ -37,15 +54,16 @@ // FrameOutE -- Motif "ridge" (outset bevel: highlight-then-shadow) // The groove/ridge are each two nested 1px bevels with swapped colors. void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - int32_t fb = widgetFrameBorderWidth(w); - int32_t boxY = w->y + font->charHeight / 2; - int32_t boxH = w->h - font->charHeight / 2; - uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; - uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; + FrameDataT *fd = (FrameDataT *)w->data; + int32_t fb = widgetFrameBorderWidth(w); + int32_t boxY = w->y + font->charHeight / 2; + int32_t boxH = w->h - font->charHeight / 2; + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; - if (w->as.frame.style == FrameFlatE) { + if (fd->style == FrameFlatE) { // Flat: solid color rectangle outline - uint32_t fc = w->as.frame.color ? w->as.frame.color : colors->windowShadow; + uint32_t fc = fd->color ? fd->color : colors->windowShadow; drawHLine(d, ops, w->x, boxY, w->w, fc); drawHLine(d, ops, w->x, boxY + boxH - 1, w->w, fc); @@ -56,7 +74,7 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap BevelStyleT outer; BevelStyleT inner; - if (w->as.frame.style == FrameInE) { + if (fd->style == FrameInE) { outer.highlight = colors->windowShadow; outer.shadow = colors->windowHighlight; inner.highlight = colors->windowHighlight; @@ -77,8 +95,8 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap } // Draw title centered vertically on the top border line - if (w->as.frame.title && w->as.frame.title[0]) { - int32_t titleW = textWidthAccel(font, w->as.frame.title); + if (fd->title && fd->title[0]) { + int32_t titleW = textWidthAccel(font, fd->title); int32_t titleX = w->x + DEFAULT_PADDING + fb; int32_t titleY = boxY + (fb - font->charHeight) / 2; @@ -86,50 +104,33 @@ void widgetFramePaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitmap titleW + 4, font->charHeight, bg); if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, titleX, titleY, w->as.frame.title, colors); + drawTextAccelEmbossed(d, ops, font, titleX, titleY, fd->title, colors); } else { - drawTextAccel(d, ops, font, titleX, titleY, w->as.frame.title, fg, bg, true); + drawTextAccel(d, ops, font, titleX, titleY, fd->title, fg, bg, true); } } } // ============================================================ -// wgtFrame +// widgetFrameGetLayoutMetrics // ============================================================ -// Create a Frame container. The title string supports accelerator keys -// (prefixed with '&') which are parsed by accelParse. The title pointer -// is stored directly (not copied) -- the caller must ensure it remains valid. -WidgetT *wgtFrame(WidgetT *parent, const char *title) { - WidgetT *w = widgetAlloc(parent, WidgetFrameE); - - if (w) { - w->as.frame.title = title; - w->as.frame.style = FrameInE; - w->as.frame.color = 0; - w->accelKey = accelParse(title); - } - - return w; +void widgetFrameGetLayoutMetrics(const WidgetT *w, const BitmapFontT *font, int32_t *pad, int32_t *gap, int32_t *extraTop, int32_t *borderW) { + FrameDataT *fd = (FrameDataT *)w->data; + *pad = DEFAULT_PADDING; + *gap = 0; + *extraTop = font ? font->charHeight / 2 : 0; + *borderW = (fd && fd->style == FrameFlatE) ? 1 : 2; } // ============================================================ -// wgtHBox +// widgetFrameDestroy // ============================================================ -WidgetT *wgtHBox(WidgetT *parent) { - return widgetAlloc(parent, WidgetHBoxE); -} - - -// ============================================================ -// wgtVBox -// ============================================================ - -WidgetT *wgtVBox(WidgetT *parent) { - return widgetAlloc(parent, WidgetVBoxE); +void widgetFrameDestroy(WidgetT *w) { + free(w->data); } @@ -165,20 +166,68 @@ static const WidgetClassT sClassHBox = { }; 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 + .flags = WCLASS_BOX_CONTAINER | WCLASS_FOCUS_FORWARD, + .paint = widgetFramePaint, + .paintOverlay = NULL, + .calcMinSize = NULL, + .layout = NULL, + .onMouse = NULL, + .onKey = NULL, + .destroy = widgetFrameDestroy, + .getText = NULL, + .setText = NULL, + .getLayoutMetrics = widgetFrameGetLayoutMetrics +}; + +// ============================================================ +// Widget creation functions +// ============================================================ + + +WidgetT *wgtFrame(WidgetT *parent, const char *title) { + WidgetT *w = widgetAlloc(parent, sFrameTypeId); + + if (w) { + FrameDataT *fd = calloc(1, sizeof(FrameDataT)); + fd->title = title; + fd->style = FrameInE; + fd->color = 0; + w->data = fd; + w->accelKey = accelParse(title); + } + + return w; +} + + +WidgetT *wgtHBox(WidgetT *parent) { + return widgetAlloc(parent, sHBoxTypeId); +} + + +WidgetT *wgtVBox(WidgetT *parent) { + return widgetAlloc(parent, sVBoxTypeId); +} + + +// ============================================================ +// DXE registration +// ============================================================ + + +static const struct { + WidgetT *(*vBox)(WidgetT *parent); + WidgetT *(*hBox)(WidgetT *parent); + WidgetT *(*frame)(WidgetT *parent, const char *title); +} sApi = { + .vBox = wgtVBox, + .hBox = wgtHBox, + .frame = wgtFrame }; void wgtRegister(void) { - widgetClassTable[WidgetVBoxE] = &sClassVBox; - widgetClassTable[WidgetHBoxE] = &sClassHBox; - widgetClassTable[WidgetFrameE] = &sClassFrame; + sVBoxTypeId = wgtRegisterClass(&sClassVBox); + sHBoxTypeId = wgtRegisterClass(&sClassHBox); + sFrameTypeId = wgtRegisterClass(&sClassFrame); + wgtRegisterApi("box", &sApi); } diff --git a/widgets/widgetBox.h b/widgets/widgetBox.h new file mode 100644 index 00000000..d8406cab --- /dev/null +++ b/widgets/widgetBox.h @@ -0,0 +1,23 @@ +// widgetBox.h -- Box container widget API (VBox, HBox, Frame) +#ifndef WIDGET_BOX_H +#define WIDGET_BOX_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*vBox)(WidgetT *parent); + WidgetT *(*hBox)(WidgetT *parent); + WidgetT *(*frame)(WidgetT *parent, const char *title); +} BoxApiT; + +static inline const BoxApiT *dvxBoxApi(void) { + static const BoxApiT *sApi; + if (!sApi) { sApi = (const BoxApiT *)wgtGetApi("box"); } + return sApi; +} + +#define wgtVBox(parent) dvxBoxApi()->vBox(parent) +#define wgtHBox(parent) dvxBoxApi()->hBox(parent) +#define wgtFrame(parent, title) dvxBoxApi()->frame(parent, title) + +#endif // WIDGET_BOX_H diff --git a/widgets/widgetButton.c b/widgets/widgetButton.c index bd864178..c5e8d34b 100644 --- a/widgets/widgetButton.c +++ b/widgets/widgetButton.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetButton.c -- Button widget // // Standard push button with text label, Motif-style 2px beveled border, @@ -19,42 +20,38 @@ // Text supports accelerator keys via '&' prefix (e.g., "&OK" underlines 'O' // and Alt+O activates the button). -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" + +#define BUTTON_PAD_H 8 +#define BUTTON_PAD_V 4 +#define BUTTON_FOCUS_INSET 3 +#define BUTTON_PRESS_OFFSET 1 + +static int32_t sTypeId = -1; + +typedef struct { + const char *text; + bool pressed; +} ButtonDataT; // ============================================================ -// wgtButton +// Prototypes // ============================================================ -WidgetT *wgtButton(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, WidgetButtonE); - - if (w) { - w->as.button.text = text; - w->as.button.pressed = false; - w->accelKey = accelParse(text); - } - - return w; -} +static void widgetButtonDestroy(WidgetT *w); // ============================================================ -// widgetButtonGetText +// widgetButtonAccelActivate // ============================================================ -const char *widgetButtonGetText(const WidgetT *w) { - return w->as.button.text; -} - - -// ============================================================ -// widgetButtonSetText -// ============================================================ - -void widgetButtonSetText(WidgetT *w, const char *text) { - w->as.button.text = text; - w->accelKey = accelParse(text); +void widgetButtonAccelActivate(WidgetT *w, WidgetT *root) { + (void)root; + ButtonDataT *d = (ButtonDataT *)w->data; + d->pressed = true; + sKeyPressedBtn = w; + wgtInvalidatePaint(w); } @@ -66,11 +63,32 @@ void widgetButtonSetText(WidgetT *w, const char *text) { // width is computed by textWidthAccel which excludes the '&' accelerator prefix // character from the measurement. void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { - w->calcMinW = textWidthAccel(font, w->as.button.text) + BUTTON_PAD_H * 2; + ButtonDataT *d = (ButtonDataT *)w->data; + w->calcMinW = textWidthAccel(font, d->text) + BUTTON_PAD_H * 2; w->calcMinH = font->charHeight + BUTTON_PAD_V * 2; } +// ============================================================ +// widgetButtonDestroy +// ============================================================ + +static void widgetButtonDestroy(WidgetT *w) { + free(w->data); + w->data = NULL; +} + + +// ============================================================ +// widgetButtonGetText +// ============================================================ + +const char *widgetButtonGetText(const WidgetT *w) { + ButtonDataT *d = (ButtonDataT *)w->data; + return d->text; +} + + // ============================================================ // widgetButtonOnKey // ============================================================ @@ -79,7 +97,8 @@ void widgetButtonOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; if (key == ' ' || key == 0x0D) { - w->as.button.pressed = true; + ButtonDataT *d = (ButtonDataT *)w->data; + d->pressed = true; sKeyPressedBtn = w; wgtInvalidatePaint(w); } @@ -94,8 +113,9 @@ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; + ButtonDataT *d = (ButtonDataT *)w->data; w->focused = true; - w->as.button.pressed = true; + d->pressed = true; sPressedButton = w; } @@ -111,56 +131,112 @@ void widgetButtonOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // shadow color at 0,0) to create a chiseled/etched look -- this is the // standard Windows 3.1 disabled control appearance. void widgetButtonPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + ButtonDataT *bd = (ButtonDataT *)w->data; uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; uint32_t bgFace = w->bgColor ? w->bgColor : colors->buttonFace; BevelStyleT bevel; - bevel.highlight = w->as.button.pressed ? colors->windowShadow : colors->windowHighlight; - bevel.shadow = w->as.button.pressed ? colors->windowHighlight : colors->windowShadow; + bevel.highlight = bd->pressed ? colors->windowShadow : colors->windowHighlight; + bevel.shadow = bd->pressed ? colors->windowHighlight : colors->windowShadow; bevel.face = bgFace; bevel.width = 2; drawBevel(d, ops, w->x, w->y, w->w, w->h, &bevel); - int32_t textW = textWidthAccel(font, w->as.button.text); + int32_t textW = textWidthAccel(font, bd->text); int32_t textX = w->x + (w->w - textW) / 2; int32_t textY = w->y + (w->h - font->charHeight) / 2; - if (w->as.button.pressed) { + if (bd->pressed) { textX += BUTTON_PRESS_OFFSET; textY += BUTTON_PRESS_OFFSET; } if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, textX, textY, w->as.button.text, colors); + drawTextAccelEmbossed(d, ops, font, textX, textY, bd->text, colors); } else { - drawTextAccel(d, ops, font, textX, textY, w->as.button.text, fg, bgFace, true); + drawTextAccel(d, ops, font, textX, textY, bd->text, fg, bgFace, true); } if (w->focused) { - int32_t off = w->as.button.pressed ? BUTTON_PRESS_OFFSET : 0; + int32_t off = bd->pressed ? BUTTON_PRESS_OFFSET : 0; drawFocusRect(d, ops, w->x + BUTTON_FOCUS_INSET + off, w->y + BUTTON_FOCUS_INSET + off, w->w - BUTTON_FOCUS_INSET * 2, w->h - BUTTON_FOCUS_INSET * 2, fg); } } +// ============================================================ +// widgetButtonSetPressed +// ============================================================ + +void widgetButtonSetPressed(WidgetT *w, bool pressed) { + ButtonDataT *d = (ButtonDataT *)w->data; + d->pressed = pressed; + wgtInvalidatePaint(w); +} + + +// ============================================================ +// widgetButtonSetText +// ============================================================ + +void widgetButtonSetText(WidgetT *w, const char *text) { + ButtonDataT *d = (ButtonDataT *)w->data; + d->text = text; + w->accelKey = accelParse(text); +} + + // ============================================================ // DXE registration // ============================================================ 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 + .flags = WCLASS_FOCUSABLE | WCLASS_PRESS_RELEASE, + .paint = widgetButtonPaint, + .paintOverlay = NULL, + .calcMinSize = widgetButtonCalcMinSize, + .layout = NULL, + .onMouse = widgetButtonOnMouse, + .onKey = widgetButtonOnKey, + .onAccelActivate = widgetButtonAccelActivate, + .destroy = widgetButtonDestroy, + .getText = widgetButtonGetText, + .setText = widgetButtonSetText, + .setPressed = widgetButtonSetPressed +}; + +// ============================================================ +// Widget creation functions +// ============================================================ + + +WidgetT *wgtButton(WidgetT *parent, const char *text) { + WidgetT *w = widgetAlloc(parent, sTypeId); + + if (w) { + ButtonDataT *d = calloc(1, sizeof(ButtonDataT)); + w->data = d; + d->text = text; + w->accelKey = accelParse(text); + } + + return w; +} + + +// ============================================================ +// DXE registration +// ============================================================ + + +static const struct { + WidgetT *(*create)(WidgetT *parent, const char *text); +} sApi = { + .create = wgtButton }; void wgtRegister(void) { - widgetClassTable[WidgetButtonE] = &sClassButton; + sTypeId = wgtRegisterClass(&sClassButton); + wgtRegisterApi("button", &sApi); } diff --git a/widgets/widgetButton.h b/widgets/widgetButton.h new file mode 100644 index 00000000..92f6b5bc --- /dev/null +++ b/widgets/widgetButton.h @@ -0,0 +1,19 @@ +// widgetButton.h -- Button widget API +#ifndef WIDGET_BUTTON_H +#define WIDGET_BUTTON_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent, const char *text); +} ButtonApiT; + +static inline const ButtonApiT *dvxButtonApi(void) { + static const ButtonApiT *sApi; + if (!sApi) { sApi = (const ButtonApiT *)wgtGetApi("button"); } + return sApi; +} + +#define wgtButton(parent, text) dvxButtonApi()->create(parent, text) + +#endif // WIDGET_BUTTON_H diff --git a/widgets/widgetCanvas.c b/widgets/widgetCanvas.c index 76d3b558..fd3ca231 100644 --- a/widgets/widgetCanvas.c +++ b/widgets/widgetCanvas.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetCanvas.c -- Drawable canvas widget (freehand draw, PNG save/load) // // The canvas widget provides a pixel buffer in the display's native pixel @@ -21,17 +22,32 @@ // top-left of the canvas content, not the widget. Mouse events translate // widget-space coordinates to canvas-space by subtracting the border offset. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" + +static int32_t sTypeId = -1; #define CANVAS_BORDER 2 +typedef struct { + uint8_t *pixelData; // pixel buffer in display format + int32_t canvasW; + int32_t canvasH; + int32_t canvasPitch; + int32_t canvasBpp; // cached bytes per pixel + uint32_t penColor; + int32_t penSize; + int32_t lastX; + int32_t lastY; + void (*onMouse)(struct WidgetT *w, int32_t cx, int32_t cy, bool drag); +} CanvasDataT; + // ============================================================ // Prototypes // ============================================================ -static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy); -static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); +static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy); +static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1); // ============================================================ @@ -72,14 +88,14 @@ static inline void canvasPutPixel(uint8_t *dst, uint32_t color, int32_t bpp) { // // Draw a filled circle of diameter penSize at (cx, cy) in canvas coords. -static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) { - int32_t bpp = w->as.canvas.canvasBpp; - int32_t pitch = w->as.canvas.canvasPitch; - uint8_t *data = w->as.canvas.data; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; - uint32_t color = w->as.canvas.penColor; - int32_t rad = w->as.canvas.penSize / 2; +static void canvasDrawDot(CanvasDataT *cd, int32_t cx, int32_t cy) { + int32_t bpp = cd->canvasBpp; + int32_t pitch = cd->canvasPitch; + uint8_t *data = cd->pixelData; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; + uint32_t color = cd->penColor; + int32_t rad = cd->penSize / 2; if (rad < 1) { // Single pixel @@ -161,7 +177,7 @@ static void canvasDrawDot(WidgetT *w, int32_t cx, int32_t cy) { // chosen over DDA because it's pure integer arithmetic -- no floating point // needed, which matters on 486SX (no FPU). -static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { +static void canvasDrawLine(CanvasDataT *cd, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { int32_t dx = x1 - x0; int32_t dy = y1 - y0; int32_t sx = (dx >= 0) ? 1 : -1; @@ -173,7 +189,7 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32 int32_t err = dx - dy; for (;;) { - canvasDrawDot(w, x0, y0); + canvasDrawDot(cd, x0, y0); if (x0 == x1 && y0 == y1) { break; @@ -194,79 +210,18 @@ static void canvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32 } -// ============================================================ -// wgtCanvas -// ============================================================ - -// Create a canvas widget with the specified dimensions. The canvas buffer -// is allocated in the display's native pixel format by walking up the widget -// tree to find the AppContextT (which holds the display format info). This -// tree-walk pattern is necessary because the widget doesn't have direct access -// to the display -- only the root widget's userData points to the AppContextT. -// The buffer is initialized to white using spanFill for performance. -WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { - if (!parent || w <= 0 || h <= 0) { - return NULL; - } - - // Find the AppContextT to get display format - WidgetT *root = parent; - - while (root->parent) { - root = root->parent; - } - - AppContextT *ctx = (AppContextT *)root->userData; - - if (!ctx) { - return NULL; - } - - const DisplayT *d = &ctx->display; - int32_t bpp = d->format.bytesPerPixel; - int32_t pitch = w * bpp; - - uint8_t *data = (uint8_t *)malloc(pitch * h); - - if (!data) { - return NULL; - } - - // Fill with white using span fill for performance - uint32_t white = packColor(d, 255, 255, 255); - BlitOpsT canvasOps; - drawInit(&canvasOps, d); - - for (int32_t y = 0; y < h; y++) { - canvasOps.spanFill(data + y * pitch, white, w); - } - - WidgetT *wgt = widgetAlloc(parent, WidgetCanvasE); - - if (wgt) { - wgt->as.canvas.data = data; - wgt->as.canvas.canvasW = w; - wgt->as.canvas.canvasH = h; - wgt->as.canvas.canvasPitch = pitch; - wgt->as.canvas.canvasBpp = bpp; - wgt->as.canvas.penColor = packColor(d, 0, 0, 0); - wgt->as.canvas.penSize = 2; - wgt->as.canvas.lastX = -1; - wgt->as.canvas.lastY = -1; - } else { - free(data); - } - - return wgt; -} - - // ============================================================ // wgtCanvasClear // ============================================================ void wgtCanvasClear(WidgetT *w, uint32_t color) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { + return; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { return; } @@ -280,12 +235,12 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { return; } - int32_t pitch = w->as.canvas.canvasPitch; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; + int32_t pitch = cd->canvasPitch; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; for (int32_t y = 0; y < ch; y++) { - ctx->blitOps.spanFill(w->as.canvas.data + y * pitch, color, cw); + ctx->blitOps.spanFill(cd->pixelData + y * pitch, color, cw); } } @@ -297,11 +252,17 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { // Draw a line using the current pen color and pen size. void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { return; } - canvasDrawLine(w, x0, y0, x1, y1); + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { + return; + } + + canvasDrawLine(cd, x0, y0, x1, y1); } @@ -312,7 +273,13 @@ void wgtCanvasDrawLine(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y // Draw a 1px outlined rectangle using the current pen color. void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { + return; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { return; } @@ -320,12 +287,12 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t return; } - int32_t bpp = w->as.canvas.canvasBpp; - int32_t pitch = w->as.canvas.canvasPitch; - uint8_t *data = w->as.canvas.data; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; - uint32_t color = w->as.canvas.penColor; + int32_t bpp = cd->canvasBpp; + int32_t pitch = cd->canvasPitch; + uint8_t *data = cd->pixelData; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; + uint32_t color = cd->penColor; // Top and bottom edges for (int32_t px = x; px < x + width; px++) { @@ -378,7 +345,13 @@ void wgtCanvasDrawRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t // Draw a filled circle using the current pen color. void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { + return; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { return; } @@ -386,12 +359,12 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) { return; } - int32_t bpp = w->as.canvas.canvasBpp; - int32_t pitch = w->as.canvas.canvasPitch; - uint8_t *data = w->as.canvas.data; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; - uint32_t color = w->as.canvas.penColor; + int32_t bpp = cd->canvasBpp; + int32_t pitch = cd->canvasPitch; + uint8_t *data = cd->pixelData; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; + uint32_t color = cd->penColor; int32_t r2 = radius * radius; for (int32_t dy = -radius; dy <= radius; dy++) { @@ -449,7 +422,13 @@ void wgtCanvasFillCircle(WidgetT *w, int32_t cx, int32_t cy, int32_t radius) { // Draw a filled rectangle using the current pen color. void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { + return; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { return; } @@ -457,12 +436,12 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t return; } - int32_t bpp = w->as.canvas.canvasBpp; - int32_t pitch = w->as.canvas.canvasPitch; - uint8_t *data = w->as.canvas.data; - int32_t cw = w->as.canvas.canvasW; - int32_t ch = w->as.canvas.canvasH; - uint32_t color = w->as.canvas.penColor; + int32_t bpp = cd->canvasBpp; + int32_t pitch = cd->canvasPitch; + uint8_t *data = cd->pixelData; + int32_t cw = cd->canvasW; + int32_t ch = cd->canvasH; + uint32_t color = cd->penColor; // Clip to canvas bounds int32_t x0 = x < 0 ? 0 : x; @@ -505,129 +484,48 @@ void wgtCanvasFillRect(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t // ============================================================ uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { return 0; } - if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) { + const CanvasDataT *cd = (const CanvasDataT *)w->data; + + if (!cd->pixelData) { return 0; } - int32_t bpp = w->as.canvas.canvasBpp; - const uint8_t *src = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp; + if (x < 0 || x >= cd->canvasW || y < 0 || y >= cd->canvasH) { + return 0; + } + + int32_t bpp = cd->canvasBpp; + const uint8_t *src = cd->pixelData + y * cd->canvasPitch + x * bpp; return canvasGetPixel(src, bpp); } -// ============================================================ -// wgtCanvasLoad -// ============================================================ - -// Load an image file into the canvas, replacing the current content. -// Delegates to dvxLoadImage for format decoding and pixel conversion. -// The old buffer is freed and replaced with the new one -- canvas -// dimensions change to match the loaded image. -int32_t wgtCanvasLoad(WidgetT *w, const char *path) { - if (!w || w->type != WidgetCanvasE || !path) { - return -1; - } - - AppContextT *ctx = wgtGetContext(w); - - if (!ctx) { - return -1; - } - - int32_t imgW; - int32_t imgH; - int32_t pitch; - uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); - - if (!data) { - return -1; - } - - free(w->as.canvas.data); - w->as.canvas.data = data; - w->as.canvas.canvasW = imgW; - w->as.canvas.canvasH = imgH; - w->as.canvas.canvasPitch = pitch; - w->as.canvas.canvasBpp = ctx->display.format.bytesPerPixel; - - return 0; -} - - -// ============================================================ -// wgtCanvasSave -// ============================================================ - -// Save the canvas content to a PNG file. Delegates to dvxSaveImage -// which handles native-to-RGB conversion and PNG encoding. -int32_t wgtCanvasSave(WidgetT *w, const char *path) { - if (!w || w->type != WidgetCanvasE || !path || !w->as.canvas.data) { - return -1; - } - - AppContextT *ctx = wgtGetContext(w); - - if (!ctx) { - return -1; - } - - return dvxSaveImage(ctx, w->as.canvas.data, w->as.canvas.canvasW, w->as.canvas.canvasH, w->as.canvas.canvasPitch, path); -} - - -// ============================================================ -// wgtCanvasSetMouseCallback -// ============================================================ - -void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) { - if (w && w->type == WidgetCanvasE) { - w->as.canvas.onMouse = cb; - } -} - - -// ============================================================ -// wgtCanvasSetPenColor -// ============================================================ - -void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) { - if (w && w->type == WidgetCanvasE) { - w->as.canvas.penColor = color; - } -} - - -// ============================================================ -// wgtCanvasSetPenSize -// ============================================================ - -void wgtCanvasSetPenSize(WidgetT *w, int32_t size) { - if (w && w->type == WidgetCanvasE && size > 0) { - w->as.canvas.penSize = size; - } -} - - // ============================================================ // wgtCanvasSetPixel // ============================================================ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) { - if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + if (!w || w->type != sTypeId) { return; } - if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) { + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { return; } - int32_t bpp = w->as.canvas.canvasBpp; - uint8_t *dst = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp; + if (x < 0 || x >= cd->canvasW || y < 0 || y >= cd->canvasH) { + return; + } + + int32_t bpp = cd->canvasBpp; + uint8_t *dst = cd->pixelData + y * cd->canvasPitch + x * bpp; canvasPutPixel(dst, color, bpp); } @@ -638,7 +536,10 @@ void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) { // ============================================================ void widgetCanvasDestroy(WidgetT *w) { - free(w->as.canvas.data); + CanvasDataT *cd = (CanvasDataT *)w->data; + + free(cd->pixelData); + free(cd); } @@ -652,8 +553,10 @@ void widgetCanvasDestroy(WidgetT *w) { // minimum, and the layout engine should respect that. void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { (void)font; - w->calcMinW = w->as.canvas.canvasW + CANVAS_BORDER * 2; - w->calcMinH = w->as.canvas.canvasH + CANVAS_BORDER * 2; + CanvasDataT *cd = (CanvasDataT *)w->data; + + w->calcMinW = cd->canvasW + CANVAS_BORDER * 2; + w->calcMinH = cd->canvasH + CANVAS_BORDER * 2; } @@ -668,8 +571,9 @@ void widgetCanvasCalcMinSize(WidgetT *w, const BitmapFontT *font) { // and continuing one. void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { (void)root; + CanvasDataT *cd = (CanvasDataT *)hit->data; - if (!hit->as.canvas.onMouse) { + if (!cd->onMouse) { return; } @@ -681,12 +585,14 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { if (!drag) { sDrawingCanvas = hit; + cd->lastX = -1; + cd->lastY = -1; } - hit->as.canvas.lastX = cx; - hit->as.canvas.lastY = cy; + cd->lastX = cx; + cd->lastY = cy; - hit->as.canvas.onMouse(hit, cx, cy, drag); + cd->onMouse(hit, cx, cy, drag); wgtInvalidatePaint(hit); } @@ -701,8 +607,9 @@ void widgetCanvasOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { // repaint essentially free relative to the display bandwidth. void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { (void)font; + CanvasDataT *cd = (CanvasDataT *)w->data; - if (!w->as.canvas.data) { + if (!cd->pixelData) { return; } @@ -715,13 +622,13 @@ void widgetCanvasPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bitma drawBevel(d, ops, w->x, w->y, w->w, w->h, &sunken); // Blit the canvas data inside the border - int32_t imgW = w->as.canvas.canvasW; - int32_t imgH = w->as.canvas.canvasH; + int32_t imgW = cd->canvasW; + int32_t imgH = cd->canvasH; int32_t dx = w->x + CANVAS_BORDER; int32_t dy = w->y + CANVAS_BORDER; rectCopy(d, ops, dx, dy, - w->as.canvas.data, w->as.canvas.canvasPitch, + cd->pixelData, cd->canvasPitch, 0, 0, imgW, imgH); } @@ -744,6 +651,188 @@ static const WidgetClassT sClassCanvas = { }; -void wgtRegister(void) { - widgetClassTable[WidgetCanvasE] = &sClassCanvas; +// ============================================================ +// Widget creation functions +// ============================================================ + + +WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h) { + if (!parent || w <= 0 || h <= 0) { + return NULL; + } + + // Find the AppContextT to get display format + WidgetT *root = parent; + + while (root->parent) { + root = root->parent; + } + + AppContextT *ctx = (AppContextT *)root->userData; + + if (!ctx) { + return NULL; + } + + const DisplayT *d = &ctx->display; + int32_t bpp = d->format.bytesPerPixel; + int32_t pitch = w * bpp; + + uint8_t *data = (uint8_t *)malloc(pitch * h); + + if (!data) { + return NULL; + } + + // Fill with white using span fill for performance + uint32_t white = packColor(d, 255, 255, 255); + BlitOpsT canvasOps; + drawInit(&canvasOps, d); + + for (int32_t y = 0; y < h; y++) { + canvasOps.spanFill(data + y * pitch, white, w); + } + + WidgetT *wgt = widgetAlloc(parent, sTypeId); + + if (wgt) { + CanvasDataT *cd = (CanvasDataT *)calloc(1, sizeof(CanvasDataT)); + + cd->pixelData = data; + cd->canvasW = w; + cd->canvasH = h; + cd->canvasPitch = pitch; + cd->canvasBpp = bpp; + cd->penColor = packColor(d, 0, 0, 0); + cd->penSize = 2; + cd->lastX = -1; + cd->lastY = -1; + wgt->data = cd; + } else { + free(data); + } + + return wgt; +} + + +int32_t wgtCanvasLoad(WidgetT *w, const char *path) { + if (!w || w->type != sTypeId || !path) { + return -1; + } + + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return -1; + } + + int32_t imgW; + int32_t imgH; + int32_t pitch; + uint8_t *data = dvxLoadImage(ctx, path, &imgW, &imgH, &pitch); + + if (!data) { + return -1; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + free(cd->pixelData); + cd->pixelData = data; + cd->canvasW = imgW; + cd->canvasH = imgH; + cd->canvasPitch = pitch; + cd->canvasBpp = ctx->display.format.bytesPerPixel; + + return 0; +} + + +int32_t wgtCanvasSave(WidgetT *w, const char *path) { + if (!w || w->type != sTypeId || !path) { + return -1; + } + + CanvasDataT *cd = (CanvasDataT *)w->data; + + if (!cd->pixelData) { + return -1; + } + + AppContextT *ctx = wgtGetContext(w); + + if (!ctx) { + return -1; + } + + return dvxSaveImage(ctx, cd->pixelData, cd->canvasW, cd->canvasH, cd->canvasPitch, path); +} + + +void wgtCanvasSetMouseCallback(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)) { + if (w && w->type == sTypeId) { + CanvasDataT *cd = (CanvasDataT *)w->data; + + cd->onMouse = cb; + } +} + + +void wgtCanvasSetPenColor(WidgetT *w, uint32_t color) { + if (w && w->type == sTypeId) { + CanvasDataT *cd = (CanvasDataT *)w->data; + + cd->penColor = color; + } +} + + +void wgtCanvasSetPenSize(WidgetT *w, int32_t size) { + if (w && w->type == sTypeId && size > 0) { + CanvasDataT *cd = (CanvasDataT *)w->data; + + cd->penSize = size; + } +} + + +// ============================================================ +// DXE registration +// ============================================================ + + +static const struct { + WidgetT *(*create)(WidgetT *parent, int32_t w, int32_t h); + void (*clear)(WidgetT *w, uint32_t color); + void (*setPenColor)(WidgetT *w, uint32_t color); + void (*setPenSize)(WidgetT *w, int32_t size); + void (*setMouseCallback)(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)); + int32_t (*save)(WidgetT *w, const char *path); + int32_t (*load)(WidgetT *w, const char *path); + void (*drawLine)(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); + void (*drawRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); + void (*fillRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); + void (*fillCircle)(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); + void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color); + uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); +} sApi = { + .create = wgtCanvas, + .clear = wgtCanvasClear, + .setPenColor = wgtCanvasSetPenColor, + .setPenSize = wgtCanvasSetPenSize, + .setMouseCallback = wgtCanvasSetMouseCallback, + .save = wgtCanvasSave, + .load = wgtCanvasLoad, + .drawLine = wgtCanvasDrawLine, + .drawRect = wgtCanvasDrawRect, + .fillRect = wgtCanvasFillRect, + .fillCircle = wgtCanvasFillCircle, + .setPixel = wgtCanvasSetPixel, + .getPixel = wgtCanvasGetPixel +}; + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClassCanvas); + wgtRegisterApi("canvas", &sApi); } diff --git a/widgets/widgetCanvas.h b/widgets/widgetCanvas.h new file mode 100644 index 00000000..38050719 --- /dev/null +++ b/widgets/widgetCanvas.h @@ -0,0 +1,43 @@ +// widgetCanvas.h -- Canvas widget API +#ifndef WIDGET_CANVAS_H +#define WIDGET_CANVAS_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent, int32_t w, int32_t h); + void (*clear)(WidgetT *w, uint32_t color); + void (*setPenColor)(WidgetT *w, uint32_t color); + void (*setPenSize)(WidgetT *w, int32_t size); + void (*setMouseCallback)(WidgetT *w, void (*cb)(WidgetT *w, int32_t cx, int32_t cy, bool drag)); + void (*save)(WidgetT *w, const char *path); + void (*load)(WidgetT *w, const char *path); + void (*drawLine)(WidgetT *w, int32_t x0, int32_t y0, int32_t x1, int32_t y1); + void (*drawRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); + void (*fillRect)(WidgetT *w, int32_t x, int32_t y, int32_t width, int32_t height); + void (*fillCircle)(WidgetT *w, int32_t cx, int32_t cy, int32_t radius); + void (*setPixel)(WidgetT *w, int32_t x, int32_t y, uint32_t color); + uint32_t (*getPixel)(const WidgetT *w, int32_t x, int32_t y); +} CanvasApiT; + +static inline const CanvasApiT *dvxCanvasApi(void) { + static const CanvasApiT *sApi; + if (!sApi) { sApi = (const CanvasApiT *)wgtGetApi("canvas"); } + return sApi; +} + +#define wgtCanvas(parent, w, h) dvxCanvasApi()->create(parent, w, h) +#define wgtCanvasClear(w, color) dvxCanvasApi()->clear(w, color) +#define wgtCanvasSetPenColor(w, color) dvxCanvasApi()->setPenColor(w, color) +#define wgtCanvasSetPenSize(w, size) dvxCanvasApi()->setPenSize(w, size) +#define wgtCanvasSetMouseCallback(w, cb) dvxCanvasApi()->setMouseCallback(w, cb) +#define wgtCanvasSave(w, path) dvxCanvasApi()->save(w, path) +#define wgtCanvasLoad(w, path) dvxCanvasApi()->load(w, path) +#define wgtCanvasDrawLine(w, x0, y0, x1, y1) dvxCanvasApi()->drawLine(w, x0, y0, x1, y1) +#define wgtCanvasDrawRect(w, x, y, width, height) dvxCanvasApi()->drawRect(w, x, y, width, height) +#define wgtCanvasFillRect(w, x, y, width, height) dvxCanvasApi()->fillRect(w, x, y, width, height) +#define wgtCanvasFillCircle(w, cx, cy, radius) dvxCanvasApi()->fillCircle(w, cx, cy, radius) +#define wgtCanvasSetPixel(w, x, y, color) dvxCanvasApi()->setPixel(w, x, y, color) +#define wgtCanvasGetPixel(w, x, y) dvxCanvasApi()->getPixel(w, x, y) + +#endif // WIDGET_CANVAS_H diff --git a/widgets/widgetCheckbox.c b/widgets/widgetCheckbox.c index 6f453602..b9ab49dd 100644 --- a/widgets/widgetCheckbox.c +++ b/widgets/widgetCheckbox.c @@ -1,9 +1,10 @@ +#define DVX_WIDGET_IMPL // widgetCheckbox.c -- Checkbox widget // // Classic checkbox: a small box with a sunken bevel (1px) on the left, a text // label to the right. The check mark is drawn as two diagonal lines forming an // "X" pattern rather than a traditional checkmark glyph -- this is simpler to -// render with drawHLine primitives and matches the DV/X aesthetic. +// render with drawHLine primitives and matches the DVX aesthetic. // // State management is simple: a boolean 'checked' flag toggles on each click // or Space/Enter keypress. The onChange callback fires after each toggle so @@ -12,42 +13,34 @@ // Focus is shown via a dotted rectangle around the label text (not the box), // matching the Win3.1 convention where the focus indicator wraps the label. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" + +#define CHECKBOX_BOX_SIZE 12 +#define CHECKBOX_GAP 4 + +static int32_t sTypeId = -1; + +typedef struct { + const char *text; + bool checked; +} CheckboxDataT; // ============================================================ -// wgtCheckbox +// Prototypes // ============================================================ -WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { - WidgetT *w = widgetAlloc(parent, WidgetCheckboxE); - - if (w) { - w->as.checkbox.text = text; - w->as.checkbox.checked = false; - w->accelKey = accelParse(text); - } - - return w; -} +static void widgetCheckboxDestroy(WidgetT *w); +void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy); // ============================================================ -// widgetCheckboxGetText +// widgetCheckboxAccelActivate // ============================================================ -const char *widgetCheckboxGetText(const WidgetT *w) { - return w->as.checkbox.text; -} - - -// ============================================================ -// widgetCheckboxSetText -// ============================================================ - -void widgetCheckboxSetText(WidgetT *w, const char *text) { - w->as.checkbox.text = text; - w->accelKey = accelParse(text); +void widgetCheckboxAccelActivate(WidgetT *w, WidgetT *root) { + (void)root; + widgetCheckboxOnMouse(w, NULL, 0, 0); } @@ -59,12 +52,33 @@ void widgetCheckboxSetText(WidgetT *w, const char *text) { // the font). This ensures the box and text are always vertically centered // relative to each other regardless of font size. void widgetCheckboxCalcMinSize(WidgetT *w, const BitmapFontT *font) { + CheckboxDataT *d = (CheckboxDataT *)w->data; w->calcMinW = CHECKBOX_BOX_SIZE + CHECKBOX_GAP + - textWidthAccel(font, w->as.checkbox.text); + textWidthAccel(font, d->text); w->calcMinH = DVX_MAX(CHECKBOX_BOX_SIZE, font->charHeight); } +// ============================================================ +// widgetCheckboxDestroy +// ============================================================ + +static void widgetCheckboxDestroy(WidgetT *w) { + free(w->data); + w->data = NULL; +} + + +// ============================================================ +// widgetCheckboxGetText +// ============================================================ + +const char *widgetCheckboxGetText(const WidgetT *w) { + CheckboxDataT *d = (CheckboxDataT *)w->data; + return d->text; +} + + // ============================================================ // widgetCheckboxOnKey // ============================================================ @@ -73,7 +87,8 @@ void widgetCheckboxOnKey(WidgetT *w, int32_t key, int32_t mod) { (void)mod; if (key == ' ' || key == 0x0D) { - w->as.checkbox.checked = !w->as.checkbox.checked; + CheckboxDataT *d = (CheckboxDataT *)w->data; + d->checked = !d->checked; if (w->onChange) { w->onChange(w); @@ -92,8 +107,9 @@ void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { (void)root; (void)vx; (void)vy; + CheckboxDataT *d = (CheckboxDataT *)w->data; w->focused = true; - w->as.checkbox.checked = !w->as.checkbox.checked; + d->checked = !d->checked; if (w->onChange) { w->onChange(w); @@ -106,9 +122,10 @@ void widgetCheckboxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // ============================================================ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { - uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; - uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; - int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; + CheckboxDataT *cd = (CheckboxDataT *)w->data; + uint32_t fg = w->fgColor ? w->fgColor : colors->contentFg; + uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; + int32_t boxY = w->y + (w->h - CHECKBOX_BOX_SIZE) / 2; // Draw checkbox box BevelStyleT bevel; @@ -123,7 +140,7 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // instead of a real line drawing algorithm avoids Bresenham overhead // for what's always a small fixed-size glyph (6x6 pixels). The 3px // inset from the box edge keeps the mark visually centered. - if (w->as.checkbox.checked) { + if (cd->checked) { int32_t cx = w->x + 3; int32_t cy = boxY + 3; int32_t cs = CHECKBOX_BOX_SIZE - 6; @@ -138,12 +155,12 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // Draw label int32_t labelX = w->x + CHECKBOX_BOX_SIZE + CHECKBOX_GAP; int32_t labelY = w->y + (w->h - font->charHeight) / 2; - int32_t labelW = textWidthAccel(font, w->as.checkbox.text); + int32_t labelW = textWidthAccel(font, cd->text); if (!w->enabled) { - drawTextAccelEmbossed(d, ops, font, labelX, labelY, w->as.checkbox.text, colors); + drawTextAccelEmbossed(d, ops, font, labelX, labelY, cd->text, colors); } else { - drawTextAccel(d, ops, font, labelX, labelY, w->as.checkbox.text, fg, bg, false); + drawTextAccel(d, ops, font, labelX, labelY, cd->text, fg, bg, false); } if (w->focused) { @@ -152,24 +169,88 @@ void widgetCheckboxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit } +// ============================================================ +// widgetCheckboxSetText +// ============================================================ + +void widgetCheckboxSetText(WidgetT *w, const char *text) { + CheckboxDataT *d = (CheckboxDataT *)w->data; + d->text = text; + w->accelKey = accelParse(text); +} + + // ============================================================ // DXE registration // ============================================================ 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 + .flags = WCLASS_FOCUSABLE, + .paint = widgetCheckboxPaint, + .paintOverlay = NULL, + .calcMinSize = widgetCheckboxCalcMinSize, + .layout = NULL, + .onMouse = widgetCheckboxOnMouse, + .onKey = widgetCheckboxOnKey, + .onAccelActivate = widgetCheckboxAccelActivate, + .destroy = widgetCheckboxDestroy, + .getText = widgetCheckboxGetText, + .setText = widgetCheckboxSetText +}; + +// ============================================================ +// Widget creation functions +// ============================================================ + + +WidgetT *wgtCheckbox(WidgetT *parent, const char *text) { + WidgetT *w = widgetAlloc(parent, sTypeId); + + if (w) { + CheckboxDataT *d = calloc(1, sizeof(CheckboxDataT)); + w->data = d; + d->text = text; + w->accelKey = accelParse(text); + } + + return w; +} + + +bool wgtCheckboxIsChecked(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, false); + CheckboxDataT *d = (CheckboxDataT *)w->data; + + return d->checked; +} + + +void wgtCheckboxSetChecked(WidgetT *w, bool checked) { + VALIDATE_WIDGET_VOID(w, sTypeId); + CheckboxDataT *d = (CheckboxDataT *)w->data; + + d->checked = checked; + wgtInvalidatePaint(w); +} + + +// ============================================================ +// DXE registration +// ============================================================ + + +static const struct { + WidgetT *(*create)(WidgetT *parent, const char *text); + bool (*isChecked)(const WidgetT *w); + void (*setChecked)(WidgetT *w, bool checked); +} sApi = { + .create = wgtCheckbox, + .isChecked = wgtCheckboxIsChecked, + .setChecked = wgtCheckboxSetChecked }; void wgtRegister(void) { - widgetClassTable[WidgetCheckboxE] = &sClassCheckbox; + sTypeId = wgtRegisterClass(&sClassCheckbox); + wgtRegisterApi("checkbox", &sApi); } diff --git a/widgets/widgetCheckbox.h b/widgets/widgetCheckbox.h new file mode 100644 index 00000000..82acd675 --- /dev/null +++ b/widgets/widgetCheckbox.h @@ -0,0 +1,23 @@ +// widgetCheckbox.h -- Checkbox widget API +#ifndef WIDGET_CHECKBOX_H +#define WIDGET_CHECKBOX_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent, const char *text); + bool (*isChecked)(const WidgetT *w); + void (*setChecked)(WidgetT *w, bool checked); +} CheckboxApiT; + +static inline const CheckboxApiT *dvxCheckboxApi(void) { + static const CheckboxApiT *sApi; + if (!sApi) { sApi = (const CheckboxApiT *)wgtGetApi("checkbox"); } + return sApi; +} + +#define wgtCheckbox(parent, text) dvxCheckboxApi()->create(parent, text) +#define wgtCheckboxIsChecked(w) dvxCheckboxApi()->isChecked(w) +#define wgtCheckboxSetChecked(w, checked) dvxCheckboxApi()->setChecked(w, checked) + +#endif // WIDGET_CHECKBOX_H diff --git a/widgets/widgetComboBox.c b/widgets/widgetComboBox.c index 9bf638dc..a44b4187 100644 --- a/widgets/widgetComboBox.c +++ b/widgets/widgetComboBox.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetComboBox.c -- ComboBox widget (editable text + dropdown list) // // Combines a single-line text input with a dropdown list. The text area @@ -20,104 +21,31 @@ // double-click (word select), and triple-click (select all). Drag-select // is tracked via the sDragTextSelect global. -#include "widgetInternal.h" +#include "dvxWidgetPlugin.h" +#include "../texthelp/textHelp.h" +#include "../listhelp/listHelp.h" +static int32_t sTypeId = -1; -// ============================================================ -// wgtComboBox -// ============================================================ - -// Create a combo box. maxLen controls the edit buffer size (0 = default 256). -// Two buffers are allocated: the edit buffer and an undo buffer (for Ctrl+Z -// single-level undo). Selection indices start at -1 (nothing selected). -// Default weight=100 makes combo boxes expand to fill available space in -// a layout container, which is the typical desired behavior. -WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) { - WidgetT *w = widgetAlloc(parent, WidgetComboBoxE); - - if (w) { - int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; - w->as.comboBox.buf = (char *)malloc(bufSize); - w->as.comboBox.undoBuf = (char *)malloc(bufSize); - w->as.comboBox.bufSize = bufSize; - - if (!w->as.comboBox.buf || !w->as.comboBox.undoBuf) { - free(w->as.comboBox.buf); - free(w->as.comboBox.undoBuf); - w->as.comboBox.buf = NULL; - w->as.comboBox.undoBuf = NULL; - } else { - w->as.comboBox.buf[0] = '\0'; - } - - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; - w->as.comboBox.selectedIdx = -1; - w->weight = 100; - } - - return w; -} - - -// ============================================================ -// wgtComboBoxGetSelected -// ============================================================ - -int32_t wgtComboBoxGetSelected(const WidgetT *w) { - VALIDATE_WIDGET(w, WidgetComboBoxE, -1); - - return w->as.comboBox.selectedIdx; -} - - -// ============================================================ -// wgtComboBoxSetItems -// ============================================================ - -void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { - VALIDATE_WIDGET_VOID(w, WidgetComboBoxE); - - w->as.comboBox.items = items; - w->as.comboBox.itemCount = count; - - // Cache max item string length so calcMinSize doesn't need to re-scan - // the entire item array on every layout pass. Items are stored as - // external pointers (not copied) -- the caller owns the string data. - w->as.comboBox.maxItemLen = widgetMaxItemLen(items, count); - - if (w->as.comboBox.selectedIdx >= count) { - w->as.comboBox.selectedIdx = -1; - } - - // wgtInvalidate (not wgtInvalidatePaint) triggers a full relayout because - // changing items may change the widget's minimum width - wgtInvalidate(w); -} - - -// ============================================================ -// wgtComboBoxSetSelected -// ============================================================ - -void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { - VALIDATE_WIDGET_VOID(w, WidgetComboBoxE); - - w->as.comboBox.selectedIdx = idx; - - // Copy selected item text to buffer - if (idx >= 0 && idx < w->as.comboBox.itemCount && w->as.comboBox.buf) { - strncpy(w->as.comboBox.buf, w->as.comboBox.items[idx], w->as.comboBox.bufSize - 1); - w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0'; - w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); - w->as.comboBox.cursorPos = w->as.comboBox.len; - w->as.comboBox.scrollOff = 0; - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; - } - - wgtInvalidatePaint(w); -} +typedef struct { + char *buf; + int32_t bufSize; + int32_t len; + int32_t cursorPos; + int32_t scrollOff; + int32_t selStart; + int32_t selEnd; + char *undoBuf; + int32_t undoLen; + int32_t undoCursor; + const char **items; + int32_t itemCount; + int32_t selectedIdx; + bool open; + int32_t hoverIdx; + int32_t listScrollPos; + int32_t maxItemLen; +} ComboBoxDataT; // ============================================================ @@ -125,7 +53,8 @@ void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { // ============================================================ void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { - int32_t maxItemW = w->as.comboBox.maxItemLen * font->charWidth; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + int32_t maxItemW = d->maxItemLen * font->charWidth; int32_t minW = font->charWidth * 8; if (maxItemW < minW) { @@ -142,8 +71,14 @@ void widgetComboBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) { // ============================================================ void widgetComboBoxDestroy(WidgetT *w) { - free(w->as.comboBox.buf); - free(w->as.comboBox.undoBuf); + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + + if (d) { + free(d->buf); + free(d->undoBuf); + free(d); + w->data = NULL; + } } @@ -152,7 +87,8 @@ void widgetComboBoxDestroy(WidgetT *w) { // ============================================================ const char *widgetComboBoxGetText(const WidgetT *w) { - return w->as.comboBox.buf ? w->as.comboBox.buf : ""; + const ComboBoxDataT *d = (const ComboBoxDataT *)w->data; + return d->buf ? d->buf : ""; } @@ -167,13 +103,15 @@ const char *widgetComboBoxGetText(const WidgetT *w) { // and a list selector depending on popup state. // Key codes: 0x48|0x100 = Up, 0x50|0x100 = Down (BIOS scan codes with extended bit). void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { - if (w->as.comboBox.open) { - if (key == (0x48 | 0x100)) { - if (w->as.comboBox.hoverIdx > 0) { - w->as.comboBox.hoverIdx--; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; - if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) { - w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx; + if (d->open) { + if (key == (0x48 | 0x100)) { + if (d->hoverIdx > 0) { + d->hoverIdx--; + + if (d->hoverIdx < d->listScrollPos) { + d->listScrollPos = d->hoverIdx; } } @@ -182,11 +120,11 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { } if (key == (0x50 | 0x100)) { - if (w->as.comboBox.hoverIdx < w->as.comboBox.itemCount - 1) { - w->as.comboBox.hoverIdx++; + if (d->hoverIdx < d->itemCount - 1) { + d->hoverIdx++; - if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) { - w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) { + d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; } } @@ -195,22 +133,22 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { } if (key == 0x0D) { - int32_t idx = w->as.comboBox.hoverIdx; + int32_t idx = d->hoverIdx; - if (idx >= 0 && idx < w->as.comboBox.itemCount) { - w->as.comboBox.selectedIdx = idx; + if (idx >= 0 && idx < d->itemCount) { + d->selectedIdx = idx; - const char *itemText = w->as.comboBox.items[idx]; - strncpy(w->as.comboBox.buf, itemText, w->as.comboBox.bufSize - 1); - w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0'; - w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); - w->as.comboBox.cursorPos = w->as.comboBox.len; - w->as.comboBox.scrollOff = 0; - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; + const char *itemText = d->items[idx]; + strncpy(d->buf, itemText, d->bufSize - 1); + d->buf[d->bufSize - 1] = '\0'; + d->len = (int32_t)strlen(d->buf); + d->cursorPos = d->len; + d->scrollOff = 0; + d->selStart = -1; + d->selEnd = -1; } - w->as.comboBox.open = false; + d->open = false; sOpenPopup = NULL; if (w->onChange) { @@ -223,17 +161,17 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { } // Down arrow on closed combobox opens the popup - if (!w->as.comboBox.open && key == (0x50 | 0x100)) { - w->as.comboBox.open = true; - w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx; + if (!d->open && key == (0x50 | 0x100)) { + d->open = true; + d->hoverIdx = d->selectedIdx; sOpenPopup = w; - if (w->as.comboBox.hoverIdx >= w->as.comboBox.listScrollPos + DROPDOWN_MAX_VISIBLE) { - w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx - DROPDOWN_MAX_VISIBLE + 1; + if (d->hoverIdx >= d->listScrollPos + DROPDOWN_MAX_VISIBLE) { + d->listScrollPos = d->hoverIdx - DROPDOWN_MAX_VISIBLE + 1; } - if (w->as.comboBox.hoverIdx < w->as.comboBox.listScrollPos) { - w->as.comboBox.listScrollPos = w->as.comboBox.hoverIdx; + if (d->hoverIdx < d->listScrollPos) { + d->listScrollPos = d->hoverIdx; } wgtInvalidatePaint(w); @@ -241,18 +179,18 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { } // Text editing (when popup is closed, or non-navigation keys with popup open) - if (!w->as.comboBox.buf) { + if (!d->buf) { return; } clearOtherSelections(w); - widgetTextEditOnKey(w, key, mod, w->as.comboBox.buf, w->as.comboBox.bufSize, - &w->as.comboBox.len, &w->as.comboBox.cursorPos, - &w->as.comboBox.scrollOff, - &w->as.comboBox.selStart, &w->as.comboBox.selEnd, - w->as.comboBox.undoBuf, &w->as.comboBox.undoLen, - &w->as.comboBox.undoCursor); + widgetTextEditOnKey(w, key, mod, d->buf, d->bufSize, + &d->len, &d->cursorPos, + &d->scrollOff, + &d->selStart, &d->selEnd, + d->undoBuf, &d->undoLen, + &d->undoCursor); } @@ -262,6 +200,7 @@ void widgetComboBoxOnKey(WidgetT *w, int32_t key, int32_t mod) { void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { w->focused = true; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; // Check if click is on the button area int32_t textAreaW = w->w - DROPDOWN_BTN_WIDTH; @@ -273,15 +212,15 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { } // Button click -- toggle popup - w->as.comboBox.open = !w->as.comboBox.open; - w->as.comboBox.hoverIdx = w->as.comboBox.selectedIdx; - sOpenPopup = w->as.comboBox.open ? w : NULL; + d->open = !d->open; + d->hoverIdx = d->selectedIdx; + sOpenPopup = d->open ? w : NULL; } else { // Text area click -- focus for editing clearOtherSelections(w); AppContextT *ctx = (AppContextT *)root->userData; - widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, w->as.comboBox.buf, w->as.comboBox.len, w->as.comboBox.scrollOff, &w->as.comboBox.cursorPos, &w->as.comboBox.selStart, &w->as.comboBox.selEnd, true, true); + widgetTextEditMouseClick(w, vx, vy, w->x + TEXT_INPUT_PAD, &ctx->font, d->buf, d->len, d->scrollOff, &d->cursorPos, &d->selStart, &d->selEnd, true, true); } } @@ -296,7 +235,8 @@ void widgetComboBoxOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) { // post-selection). The dropdown button has a small triangular arrow glyph // drawn as horizontal lines of decreasing width. When the popup is open, // the button bevel is inverted (sunken) to show it's active. -void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { +void widgetComboBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + ComboBoxDataT *d = (ComboBoxDataT *)w->data; uint32_t fg = w->enabled ? (w->fgColor ? w->fgColor : colors->contentFg) : colors->windowShadow; uint32_t bg = w->bgColor ? w->bgColor : colors->contentBg; @@ -307,36 +247,36 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit bevel.shadow = colors->windowHighlight; bevel.face = bg; bevel.width = 2; - drawBevel(d, ops, w->x, w->y, textAreaW, w->h, &bevel); + drawBevel(disp, ops, w->x, w->y, textAreaW, w->h, &bevel); // Draw text content - if (w->as.comboBox.buf) { + if (d->buf) { int32_t textX = w->x + TEXT_INPUT_PAD; int32_t textY = w->y + (w->h - font->charHeight) / 2; int32_t maxChars = (textAreaW - TEXT_INPUT_PAD * 2 - 4) / font->charWidth; - int32_t off = w->as.comboBox.scrollOff; - int32_t len = w->as.comboBox.len - off; + int32_t off = d->scrollOff; + int32_t len = d->len - off; if (len > maxChars) { len = maxChars; } - widgetTextEditPaintLine(d, ops, font, colors, textX, textY, w->as.comboBox.buf + off, len, off, w->as.comboBox.cursorPos, w->as.comboBox.selStart, w->as.comboBox.selEnd, fg, bg, w->focused && w->enabled && !w->as.comboBox.open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD); + widgetTextEditPaintLine(disp, ops, font, colors, textX, textY, d->buf + off, len, off, d->cursorPos, d->selStart, d->selEnd, fg, bg, w->focused && w->enabled && !d->open, w->x + TEXT_INPUT_PAD, w->x + textAreaW - TEXT_INPUT_PAD); } // Drop button BevelStyleT btnBevel; - btnBevel.highlight = w->as.comboBox.open ? colors->windowShadow : colors->windowHighlight; - btnBevel.shadow = w->as.comboBox.open ? colors->windowHighlight : colors->windowShadow; + btnBevel.highlight = d->open ? colors->windowShadow : colors->windowHighlight; + btnBevel.shadow = d->open ? colors->windowHighlight : colors->windowShadow; btnBevel.face = colors->buttonFace; btnBevel.width = 2; - drawBevel(d, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel); + drawBevel(disp, ops, w->x + textAreaW, w->y, DROPDOWN_BTN_WIDTH, w->h, &btnBevel); // Down arrow uint32_t arrowFg = w->enabled ? colors->contentFg : colors->windowShadow; int32_t arrowX = w->x + textAreaW + DROPDOWN_BTN_WIDTH / 2; int32_t arrowY = w->y + w->h / 2 - 1; - widgetDrawDropdownArrow(d, ops, arrowX, arrowY, arrowFg); + widgetDrawDropdownArrow(disp, ops, arrowX, arrowY, arrowFg); } @@ -344,14 +284,15 @@ void widgetComboBoxPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const Bit // widgetComboBoxPaintPopup // ============================================================ -void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { +void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { + ComboBoxDataT *d = (ComboBoxDataT *)w->data; int32_t popX; int32_t popY; int32_t popW; int32_t popH; - widgetDropdownPopupRect(w, font, d->clipH, &popX, &popY, &popW, &popH); - widgetPaintPopupList(d, ops, font, colors, popX, popY, popW, popH, w->as.comboBox.items, w->as.comboBox.itemCount, w->as.comboBox.hoverIdx, w->as.comboBox.listScrollPos); + widgetDropdownPopupRect(w, font, disp->clipH, &popX, &popY, &popW, &popH); + widgetPaintPopupList(disp, ops, font, colors, popX, popY, popW, popH, d->items, d->itemCount, d->hoverIdx, d->listScrollPos); } @@ -360,36 +301,199 @@ void widgetComboBoxPaintPopup(WidgetT *w, DisplayT *d, const BlitOpsT *ops, cons // ============================================================ void widgetComboBoxSetText(WidgetT *w, const char *text) { - if (w->as.comboBox.buf) { - strncpy(w->as.comboBox.buf, text, w->as.comboBox.bufSize - 1); - w->as.comboBox.buf[w->as.comboBox.bufSize - 1] = '\0'; - w->as.comboBox.len = (int32_t)strlen(w->as.comboBox.buf); - w->as.comboBox.cursorPos = w->as.comboBox.len; - w->as.comboBox.scrollOff = 0; - w->as.comboBox.selStart = -1; - w->as.comboBox.selEnd = -1; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + + if (d->buf) { + strncpy(d->buf, text, d->bufSize - 1); + d->buf[d->bufSize - 1] = '\0'; + d->len = (int32_t)strlen(d->buf); + d->cursorPos = d->len; + d->scrollOff = 0; + d->selStart = -1; + d->selEnd = -1; } } +// ============================================================ +// widgetComboBoxAccelActivate +// ============================================================ + +void widgetComboBoxAccelActivate(WidgetT *w, WidgetT *root) { + (void)root; + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + d->open = true; + d->hoverIdx = d->selectedIdx; + sOpenPopup = w; + wgtInvalidatePaint(w); +} + + +// ============================================================ +// widgetComboBoxClosePopup +// ============================================================ + +void widgetComboBoxClosePopup(WidgetT *w) { + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + d->open = false; + wgtInvalidatePaint(w); +} + + +// ============================================================ +// widgetComboBoxGetPopupItemCount +// ============================================================ + +int32_t widgetComboBoxGetPopupItemCount(const WidgetT *w) { + const ComboBoxDataT *d = (const ComboBoxDataT *)w->data; + return d->itemCount; +} + + +// ============================================================ +// widgetComboBoxOpenPopup +// ============================================================ + +void widgetComboBoxOpenPopup(WidgetT *w) { + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + d->open = true; + d->hoverIdx = d->selectedIdx; + sOpenPopup = w; + wgtInvalidatePaint(w); +} + + // ============================================================ // DXE registration // ============================================================ +static void widgetComboBoxGetPopupRect(const WidgetT *w, const BitmapFontT *font, int32_t contentH, int32_t *popX, int32_t *popY, int32_t *popW, int32_t *popH) { + widgetDropdownPopupRect((WidgetT *)w, font, contentH, popX, popY, popW, popH); +} + + 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 + .flags = WCLASS_FOCUSABLE | WCLASS_HAS_POPUP | WCLASS_SCROLLABLE, + .paint = widgetComboBoxPaint, + .paintOverlay = widgetComboBoxPaintPopup, + .calcMinSize = widgetComboBoxCalcMinSize, + .layout = NULL, + .onMouse = widgetComboBoxOnMouse, + .onKey = widgetComboBoxOnKey, + .onAccelActivate = widgetComboBoxAccelActivate, + .destroy = widgetComboBoxDestroy, + .getText = widgetComboBoxGetText, + .setText = widgetComboBoxSetText, + .openPopup = widgetComboBoxOpenPopup, + .closePopup = widgetComboBoxClosePopup, + .getPopupItemCount = widgetComboBoxGetPopupItemCount, + .getPopupRect = widgetComboBoxGetPopupRect }; -void wgtRegister(void) { - widgetClassTable[WidgetComboBoxE] = &sClassComboBox; +// ============================================================ +// Widget creation functions +// ============================================================ + + +WidgetT *wgtComboBox(WidgetT *parent, int32_t maxLen) { + WidgetT *w = widgetAlloc(parent, sTypeId); + + if (w) { + ComboBoxDataT *d = (ComboBoxDataT *)calloc(1, sizeof(ComboBoxDataT)); + + if (!d) { + return w; + } + + w->data = d; + int32_t bufSize = maxLen > 0 ? maxLen + 1 : 256; + d->buf = (char *)malloc(bufSize); + d->undoBuf = (char *)malloc(bufSize); + d->bufSize = bufSize; + + if (!d->buf || !d->undoBuf) { + free(d->buf); + free(d->undoBuf); + d->buf = NULL; + d->undoBuf = NULL; + } else { + d->buf[0] = '\0'; + } + + d->selStart = -1; + d->selEnd = -1; + d->selectedIdx = -1; + w->weight = 100; + } + + return w; +} + + +int32_t wgtComboBoxGetSelected(const WidgetT *w) { + VALIDATE_WIDGET(w, sTypeId, -1); + const ComboBoxDataT *d = (const ComboBoxDataT *)w->data; + + return d->selectedIdx; +} + + +void wgtComboBoxSetItems(WidgetT *w, const char **items, int32_t count) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + + d->items = items; + d->itemCount = count; + d->maxItemLen = widgetMaxItemLen(items, count); + + if (d->selectedIdx >= count) { + d->selectedIdx = -1; + } + + wgtInvalidate(w); +} + + +void wgtComboBoxSetSelected(WidgetT *w, int32_t idx) { + VALIDATE_WIDGET_VOID(w, sTypeId); + ComboBoxDataT *d = (ComboBoxDataT *)w->data; + + d->selectedIdx = idx; + + // Copy selected item text to buffer + if (idx >= 0 && idx < d->itemCount && d->buf) { + strncpy(d->buf, d->items[idx], d->bufSize - 1); + d->buf[d->bufSize - 1] = '\0'; + d->len = (int32_t)strlen(d->buf); + d->cursorPos = d->len; + d->scrollOff = 0; + d->selStart = -1; + d->selEnd = -1; + } + + wgtInvalidatePaint(w); +} + + +// ============================================================ +// DXE registration +// ============================================================ + + +static const struct { + WidgetT *(*create)(WidgetT *parent, int32_t maxLen); + void (*setItems)(WidgetT *w, const char **items, int32_t count); + int32_t (*getSelected)(const WidgetT *w); + void (*setSelected)(WidgetT *w, int32_t index); +} sApi = { + .create = wgtComboBox, + .setItems = wgtComboBoxSetItems, + .getSelected = wgtComboBoxGetSelected, + .setSelected = wgtComboBoxSetSelected +}; + +void wgtRegister(void) { + sTypeId = wgtRegisterClass(&sClassComboBox); + wgtRegisterApi("combobox", &sApi); } diff --git a/widgets/widgetComboBox.h b/widgets/widgetComboBox.h new file mode 100644 index 00000000..92d1224c --- /dev/null +++ b/widgets/widgetComboBox.h @@ -0,0 +1,25 @@ +// widgetComboBox.h -- ComboBox widget API +#ifndef WIDGET_COMBOBOX_H +#define WIDGET_COMBOBOX_H + +#include "../core/dvxWidget.h" + +typedef struct { + WidgetT *(*create)(WidgetT *parent, int32_t maxLen); + void (*setItems)(WidgetT *w, const char **items, int32_t count); + int32_t (*getSelected)(const WidgetT *w); + void (*setSelected)(WidgetT *w, int32_t idx); +} ComboBoxApiT; + +static inline const ComboBoxApiT *dvxComboBoxApi(void) { + static const ComboBoxApiT *sApi; + if (!sApi) { sApi = (const ComboBoxApiT *)wgtGetApi("combobox"); } + return sApi; +} + +#define wgtComboBox(parent, maxLen) dvxComboBoxApi()->create(parent, maxLen) +#define wgtComboBoxSetItems(w, items, count) dvxComboBoxApi()->setItems(w, items, count) +#define wgtComboBoxGetSelected(w) dvxComboBoxApi()->getSelected(w) +#define wgtComboBoxSetSelected(w, idx) dvxComboBoxApi()->setSelected(w, idx) + +#endif // WIDGET_COMBOBOX_H diff --git a/widgets/widgetDropdown.c b/widgets/widgetDropdown.c index 01d8ed3f..ab4ae98f 100644 --- a/widgets/widgetDropdown.c +++ b/widgets/widgetDropdown.c @@ -1,3 +1,4 @@ +#define DVX_WIDGET_IMPL // widgetDropdown.c -- Dropdown (select) widget // // A non-editable dropdown list (HTML