Some minor widget behavior updates.

This commit is contained in:
Scott Duensing 2026-03-10 19:03:58 -05:00
parent 685c8041c1
commit b6c5ee3cf3
7 changed files with 454 additions and 14 deletions

133
README.md
View file

@ -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

View file

@ -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
// ============================================================

View file

@ -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;
}

View file

@ -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
// ============================================================

View file

@ -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;
}

View file

@ -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) {

View file

@ -46,6 +46,7 @@
extern bool sDebugLayout;
extern WidgetT *sOpenPopup;
extern WidgetT *sPressedButton;
extern WidgetT *sDragSlider;
extern WidgetT *sDrawingCanvas;
extern int32_t sDragOffset;