From b6c5ee3cf31512f84a6162cf441a7f82642b5fb3 Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Tue, 10 Mar 2026 19:03:58 -0500 Subject: [PATCH] Some minor widget behavior updates. --- README.md | 133 +++++++++++++++++- dvx/dvxWidget.h | 8 ++ dvx/widgets/widgetButton.c | 8 +- dvx/widgets/widgetCanvas.c | 254 +++++++++++++++++++++++++++++++++++ dvx/widgets/widgetCore.c | 5 + dvx/widgets/widgetEvent.c | 59 +++++++- dvx/widgets/widgetInternal.h | 1 + 7 files changed, 454 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 818e4fe..69f6174 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ VESA VBE 2.0+ linear framebuffer. Motif-style beveled chrome, dirty-rectangle compositing, draggable and resizable windows, dropdown menus, scrollbars, and a declarative widget/layout -system. +system with buttons, checkboxes, radios, text inputs, dropdowns, combo boxes, +sliders, progress bars, tab controls, tree views, toolbars, status bars, +images, and drawable canvases. ## Building @@ -431,7 +433,10 @@ Static text label. Sized to fit its text. ```c WidgetT *wgtButton(WidgetT *parent, const char *text); ``` -Beveled push button. Set `onClick` to handle clicks. +Beveled push button with standard press/release behavior: the button +visually depresses on mouse-down, tracks the cursor while held, and +fires `onClick` on release if still over the button. Dragging off the +button cancels the click. ```c WidgetT *wgtCheckbox(WidgetT *parent, const char *text); @@ -465,13 +470,117 @@ WidgetT *wgtTextArea(WidgetT *parent, int32_t maxLen); ``` Multi-line text area (basic). +### Dropdown and ComboBox + +```c +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); +``` +Non-editable selection widget. Clicking the arrow button opens a popup +list below (or above if no room). Clicking the arrow again while the +popup is open closes it. Set `onChange` to be notified of selection +changes. The items array must remain valid for the widget's lifetime. + +```c +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); +``` +Editable text field with a dropdown list. The text area accepts keyboard +input (same editing keys as `wgtTextInput`). Clicking the arrow button +toggles the popup; clicking the text area focuses it for editing. +Selecting an item copies its text into the edit buffer. Default weight +is 100. + +### ProgressBar and Slider + +```c +WidgetT *wgtProgressBar(WidgetT *parent); +void wgtProgressBarSetValue(WidgetT *w, int32_t value); +int32_t wgtProgressBarGetValue(const WidgetT *w); +``` +Horizontal progress indicator. Value is 0--100 (percentage). The filled +portion draws left-to-right with the system highlight color. + +```c +WidgetT *wgtSlider(WidgetT *parent, int32_t minVal, int32_t maxVal); +void wgtSliderSetValue(WidgetT *w, int32_t value); +int32_t wgtSliderGetValue(const WidgetT *w); +``` +Draggable slider/trackbar. Horizontal by default; set +`w->as.slider.vertical = true` after creation for vertical orientation. +The thumb tracks the mouse while held. Set `onChange` to be notified. + +### TabControl + +```c +WidgetT *wgtTabControl(WidgetT *parent); +WidgetT *wgtTabPage(WidgetT *parent, const char *title); +void wgtTabControlSetActive(WidgetT *w, int32_t idx); +int32_t wgtTabControlGetActive(const WidgetT *w); +``` +Tabbed container. Create a tab control, then add pages as children. +Each page is a VBox that holds its own widget subtree. Only the active +page is visible and receives layout; inactive pages are hidden. Clicking +a tab header switches the active page. + +### StatusBar and Toolbar + +```c +WidgetT *wgtStatusBar(WidgetT *parent); +WidgetT *wgtToolbar(WidgetT *parent); +``` +Horizontal containers with specialized chrome. StatusBar draws a sunken +panel background; Toolbar draws a raised background. Both use horizontal +layout with default spacing. Add child widgets (labels, buttons, etc.) +as with any HBox. StatusBar children with `weight > 0` stretch to fill. + +### TreeView + +```c +WidgetT *wgtTreeView(WidgetT *parent); +WidgetT *wgtTreeItem(WidgetT *parent, const char *text); +void wgtTreeItemSetExpanded(WidgetT *w, bool expanded); +bool wgtTreeItemIsExpanded(const WidgetT *w); +``` +Hierarchical tree with expand/collapse support. Create a tree view, then +add items as children. Nest items by adding them as children of other +items. Items with children show a `[+]`/`[-]` expand box. + +The tree view manages its own internal scrollbars: a vertical scrollbar +appears when expanded items exceed the visible height, and a horizontal +scrollbar appears when item text extends beyond the visible width. Both +scrollbars have arrow buttons, proportional thumbs, and page +scrolling on trough clicks. + +Default weight is 100. Set `onClick` on individual tree items to handle +selection, or `onChange` to be notified of expand/collapse. + +### Image + +```c +WidgetT *wgtImage(WidgetT *parent, uint8_t *data, + int32_t w, int32_t h, int32_t pitch); +WidgetT *wgtImageFromFile(WidgetT *parent, const char *path); +void wgtImageSetData(WidgetT *w, uint8_t *data, + int32_t imgW, int32_t imgH, int32_t pitch); +``` +Display a bitmap image. `wgtImage` takes ownership of the pixel buffer +(freed on destroy). `wgtImageFromFile` loads any format supported by +stb_image and converts to the display pixel format. Set `onClick` to +make the image clickable. + ### Canvas ```c WidgetT *wgtCanvas(WidgetT *parent, int32_t w, int32_t h); ``` -Drawable bitmap canvas with a sunken bevel border. Supports freehand -drawing with Bresenham-interpolated strokes. +Drawable bitmap canvas with a sunken bevel border. Supports both +interactive freehand drawing (mouse strokes are Bresenham-interpolated) +and programmatic drawing from the API. ```c void wgtCanvasClear(WidgetT *w, uint32_t color); @@ -484,6 +593,22 @@ int32_t wgtCanvasLoad(WidgetT *w, const char *path); pixel format to RGB). `wgtCanvasLoad` reads any image format supported by stb_image and converts it to display pixel format. +Programmatic drawing primitives (all coordinates are in canvas space): + +```c +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); +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); +``` +`SetPixel`/`GetPixel` take explicit colors. `DrawLine` uses the current +pen color and pen size. `DrawRect` draws a 1px outline, `FillRect` fills +solid, and `FillCircle` fills a circle -- all using the current pen +color. All operations clip to the canvas bounds. Colors are in display +pixel format (use `packColor()` to create them). + ### Spacing and dividers ```c diff --git a/dvx/dvxWidget.h b/dvx/dvxWidget.h index 4a25803..adb7da4 100644 --- a/dvx/dvxWidget.h +++ b/dvx/dvxWidget.h @@ -416,6 +416,14 @@ 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); + // ============================================================ // Operations // ============================================================ diff --git a/dvx/widgets/widgetButton.c b/dvx/widgets/widgetButton.c index 8fc2c27..e0a2c8a 100644 --- a/dvx/widgets/widgetButton.c +++ b/dvx/widgets/widgetButton.c @@ -35,13 +35,7 @@ void widgetButtonCalcMinSize(WidgetT *w, const BitmapFontT *font) { void widgetButtonOnMouse(WidgetT *hit) { hit->as.button.pressed = true; - wgtInvalidate(hit); - - if (hit->onClick) { - hit->onClick(hit); - } - - hit->as.button.pressed = false; + sPressedButton = hit; } diff --git a/dvx/widgets/widgetCanvas.c b/dvx/widgets/widgetCanvas.c index 2e87240..f999e48 100644 --- a/dvx/widgets/widgetCanvas.c +++ b/dvx/widgets/widgetCanvas.c @@ -245,6 +245,234 @@ void wgtCanvasClear(WidgetT *w, uint32_t color) { } +// ============================================================ +// wgtCanvasDrawLine +// ============================================================ +// +// 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) { + return; + } + + canvasDrawLine(w, x0, y0, x1, y1); +} + + +// ============================================================ +// wgtCanvasDrawRect +// ============================================================ +// +// 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) { + return; + } + + if (width <= 0 || height <= 0) { + return; + } + + int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW; + 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; + + // Top and bottom edges + for (int32_t px = x; px < x + width; px++) { + if (px < 0 || px >= cw) { + continue; + } + + if (y >= 0 && y < ch) { + uint8_t *dst = data + y * pitch + px * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + + int32_t by = y + height - 1; + + if (by >= 0 && by < ch && by != y) { + uint8_t *dst = data + by * pitch + px * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + + // Left and right edges (excluding corners already drawn) + for (int32_t py = y + 1; py < y + height - 1; py++) { + if (py < 0 || py >= ch) { + continue; + } + + if (x >= 0 && x < cw) { + uint8_t *dst = data + py * pitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + + int32_t rx = x + width - 1; + + if (rx >= 0 && rx < cw && rx != x) { + uint8_t *dst = data + py * pitch + rx * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } +} + + +// ============================================================ +// wgtCanvasFillCircle +// ============================================================ +// +// 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) { + return; + } + + if (radius <= 0) { + return; + } + + int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW; + 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 r2 = radius * radius; + + for (int32_t dy = -radius; dy <= radius; dy++) { + int32_t py = cy + dy; + + if (py < 0 || py >= ch) { + continue; + } + + for (int32_t dx = -radius; dx <= radius; dx++) { + int32_t px = cx + dx; + + if (px < 0 || px >= cw) { + continue; + } + + if (dx * dx + dy * dy <= r2) { + uint8_t *dst = data + py * pitch + px * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } + } +} + + +// ============================================================ +// wgtCanvasFillRect +// ============================================================ +// +// 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) { + return; + } + + if (width <= 0 || height <= 0) { + return; + } + + int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW; + 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; + + // Clip to canvas bounds + int32_t x0 = x < 0 ? 0 : x; + int32_t y0 = y < 0 ? 0 : y; + int32_t x1 = x + width > cw ? cw : x + width; + int32_t y1 = y + height > ch ? ch : y + height; + + for (int32_t py = y0; py < y1; py++) { + for (int32_t px = x0; px < x1; px++) { + uint8_t *dst = data + py * pitch + px * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } + } + } +} + + +// ============================================================ +// wgtCanvasGetPixel +// ============================================================ + +uint32_t wgtCanvasGetPixel(const WidgetT *w, int32_t x, int32_t y) { + if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + return 0; + } + + if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) { + return 0; + } + + int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW; + const uint8_t *src = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp; + + if (bpp == 1) { + return *src; + } else if (bpp == 2) { + return *(const uint16_t *)src; + } else { + return *(const uint32_t *)src; + } +} + + // ============================================================ // wgtCanvasLoad // ============================================================ @@ -397,6 +625,32 @@ void wgtCanvasSetPenSize(WidgetT *w, int32_t size) { } +// ============================================================ +// wgtCanvasSetPixel +// ============================================================ + +void wgtCanvasSetPixel(WidgetT *w, int32_t x, int32_t y, uint32_t color) { + if (!w || w->type != WidgetCanvasE || !w->as.canvas.data) { + return; + } + + if (x < 0 || x >= w->as.canvas.canvasW || y < 0 || y >= w->as.canvas.canvasH) { + return; + } + + int32_t bpp = w->as.canvas.canvasPitch / w->as.canvas.canvasW; + uint8_t *dst = w->as.canvas.data + y * w->as.canvas.canvasPitch + x * bpp; + + if (bpp == 1) { + *dst = (uint8_t)color; + } else if (bpp == 2) { + *(uint16_t *)dst = (uint16_t)color; + } else { + *(uint32_t *)dst = color; + } +} + + // ============================================================ // widgetCanvasCalcMinSize // ============================================================ diff --git a/dvx/widgets/widgetCore.c b/dvx/widgets/widgetCore.c index e0d1aed..8eee574 100644 --- a/dvx/widgets/widgetCore.c +++ b/dvx/widgets/widgetCore.c @@ -8,6 +8,7 @@ bool sDebugLayout = false; WidgetT *sOpenPopup = NULL; +WidgetT *sPressedButton = NULL; WidgetT *sDragSlider = NULL; WidgetT *sDrawingCanvas = NULL; int32_t sDragOffset = 0; @@ -101,6 +102,10 @@ void widgetDestroyChildren(WidgetT *w) { sOpenPopup = NULL; } + if (sPressedButton == child) { + sPressedButton = NULL; + } + if (sDragSlider == child) { sDragSlider = NULL; } diff --git a/dvx/widgets/widgetEvent.c b/dvx/widgets/widgetEvent.c index 56afbd4..b80a44a 100644 --- a/dvx/widgets/widgetEvent.c +++ b/dvx/widgets/widgetEvent.c @@ -249,6 +249,7 @@ void widgetOnKey(WindowT *win, int32_t key, int32_t mod) { void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { WidgetT *root = win->widgetRoot; + WidgetT *closedPopup = NULL; if (!root) { return; @@ -327,6 +328,52 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { return; } + // Handle button press release + if (sPressedButton && !(buttons & 1)) { + sPressedButton->as.button.pressed = false; + + // Fire onClick if released over the same button in the same window + if (sPressedButton->window == win) { + int32_t scrollX = win->hScroll ? win->hScroll->value : 0; + int32_t scrollY = win->vScroll ? win->vScroll->value : 0; + int32_t vx = x + scrollX; + int32_t vy = y + scrollY; + + if (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && + vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h) { + if (sPressedButton->onClick) { + sPressedButton->onClick(sPressedButton); + } + } + } + + wgtInvalidate(sPressedButton); + sPressedButton = NULL; + return; + } + + // Handle button press tracking (mouse move while held) + if (sPressedButton && (buttons & 1)) { + bool over = false; + + if (sPressedButton->window == win) { + int32_t scrollX = win->hScroll ? win->hScroll->value : 0; + int32_t scrollY = win->vScroll ? win->vScroll->value : 0; + int32_t vx = x + scrollX; + int32_t vy = y + scrollY; + + over = (vx >= sPressedButton->x && vx < sPressedButton->x + sPressedButton->w && + vy >= sPressedButton->y && vy < sPressedButton->y + sPressedButton->h); + } + + if (sPressedButton->as.button.pressed != over) { + sPressedButton->as.button.pressed = over; + wgtInvalidate(sPressedButton); + } + + return; + } + // Handle open popup clicks if (sOpenPopup && (buttons & 1)) { AppContextT *ctx = (AppContextT *)root->userData; @@ -385,7 +432,9 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { return; } - // Click outside popup — close it + // Click outside popup — close it and remember which widget it was + closedPopup = sOpenPopup; + if (sOpenPopup->type == WidgetDropdownE) { sOpenPopup->as.dropdown.open = false; } else if (sOpenPopup->type == WidgetComboBoxE) { @@ -455,11 +504,15 @@ void widgetOnMouse(WindowT *win, int32_t x, int32_t y, int32_t buttons) { } if (hit->type == WidgetDropdownE && hit->enabled) { - widgetDropdownOnMouse(hit); + if (hit != closedPopup) { + widgetDropdownOnMouse(hit); + } } if (hit->type == WidgetComboBoxE && hit->enabled) { - widgetComboBoxOnMouse(hit, root, vx); + if (hit != closedPopup || vx < hit->x + hit->w - DROPDOWN_BTN_WIDTH) { + widgetComboBoxOnMouse(hit, root, vx); + } } if (hit->type == WidgetSliderE && hit->enabled) { diff --git a/dvx/widgets/widgetInternal.h b/dvx/widgets/widgetInternal.h index 2d3b402..7ccc945 100644 --- a/dvx/widgets/widgetInternal.h +++ b/dvx/widgets/widgetInternal.h @@ -46,6 +46,7 @@ extern bool sDebugLayout; extern WidgetT *sOpenPopup; +extern WidgetT *sPressedButton; extern WidgetT *sDragSlider; extern WidgetT *sDrawingCanvas; extern int32_t sDragOffset;