WrapBox added.

This commit is contained in:
Scott Duensing 2026-04-04 15:42:27 -05:00
parent 7bc92549f7
commit 7cd7388607
11 changed files with 513 additions and 44 deletions

View file

@ -33,6 +33,8 @@
#include "shellInfo.h"
#include "dvxResource.h"
#include "widgetImageButton.h"
#include "widgetScrollPane.h"
#include "widgetWrapBox.h"
#include <stdint.h>
#include <stdbool.h>
@ -42,18 +44,16 @@
#include <dirent.h>
#include <sys/stat.h>
#include "dvxMem.h"
#include "stb_ds_wrap.h"
// ============================================================
// Constants
// ============================================================
// 64 entries is generous; limited by screen real estate before this cap
#define MAX_APP_FILES 64
// DOS 8.3 paths are short, but long names under DJGPP can reach ~260
#define MAX_PATH_LEN 260
// Grid layout for app buttons: 4 columns, rows created dynamically
#define PM_TOOLTIP_LEN 128
#define PM_GRID_COLS 4
#define PM_BTN_W 100
#define PM_BTN_H 24
#define PM_CELL_W 80
@ -102,7 +102,7 @@ static WidgetT *sStatusLabel = NULL;
static PrefsHandleT *sPrefs = NULL;
static bool sMinOnRun = false;
static bool sRestoreAlone = false;
static AppEntryT sAppFiles[MAX_APP_FILES];
static AppEntryT *sAppFiles = NULL; // stb_ds dynamic array
static int32_t sAppCount = 0;
// ============================================================
@ -196,18 +196,19 @@ static void buildPmWindow(void) {
if (sAppCount == 0) {
wgtLabel(appFrame, "(No applications found in apps/ directory)");
} else {
// Build grid of app icons. Each cell is a VBox with an image
// button (or text button if no icon) and a label underneath.
WidgetT *hbox = NULL;
// ScrollPane provides scrollbars when icons overflow.
// WrapBox inside flows cells left-to-right, wrapping to
// the next row when the window width is exceeded.
WidgetT *scroll = wgtScrollPane(appFrame);
scroll->weight = 100;
WidgetT *wrap = wgtWrapBox(scroll);
wrap->weight = 100;
wrap->spacing = wgtPixels(PM_GRID_SPACING);
wrap->align = AlignCenterE;
for (int32_t i = 0; i < sAppCount; i++) {
if (i % PM_GRID_COLS == 0) {
hbox = wgtHBox(appFrame);
hbox->spacing = wgtPixels(PM_GRID_SPACING);
hbox->align = AlignCenterE;
}
WidgetT *cell = wgtVBox(hbox);
WidgetT *cell = wgtVBox(wrap);
cell->align = AlignCenterE;
cell->minW = wgtPixels(PM_CELL_W);
cell->minH = wgtPixels(PM_CELL_H);
@ -232,8 +233,8 @@ static void buildPmWindow(void) {
btn->prefH = wgtPixels(PM_BTN_H);
}
btn->userData = &sAppFiles[i];
btn->onClick = onAppButtonClick;
btn->userData = &sAppFiles[i];
btn->onClick = onAppButtonClick;
if (sAppFiles[i].tooltip[0]) {
wgtSetTooltip(btn, sAppFiles[i].tooltip);
@ -252,10 +253,6 @@ static void buildPmWindow(void) {
sStatusLabel->weight = 100;
updateStatusText();
// dvxFitWindow sizes the window to tightly fit the widget tree,
// honoring preferred sizes. Without this, the window would use the
// initial dimensions from dvxCreateWindow even if widgets don't fit.
dvxFitWindow(sAc, sPmWindow);
}
@ -383,6 +380,13 @@ static void onPmMenu(WindowT *win, int32_t menuId) {
// The apps/ path is relative to the working directory, which the shell sets
// to the root of the DVX install before loading any apps.
static void scanAppsDir(void) {
// Free icons from previous scan
for (int32_t i = 0; i < sAppCount; i++) {
free(sAppFiles[i].iconData);
}
arrfree(sAppFiles);
sAppFiles = NULL;
sAppCount = 0;
scanAppsDirRecurse("apps");
dvxLog("Progman: found %ld app(s)", (long)sAppCount);
@ -406,7 +410,7 @@ static void scanAppsDirRecurse(const char *dirPath) {
struct dirent *ent;
while ((ent = readdir(dir)) != NULL && sAppCount < MAX_APP_FILES) {
while ((ent = readdir(dir)) != NULL) {
// Skip . and ..
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
@ -432,20 +436,18 @@ static void scanAppsDirRecurse(const char *dirPath) {
// Check for .app extension (case-insensitive)
const char *ext = ent->d_name + len - 4;
if (strcmp(ext, ".app") != 0 && strcmp(ext, ".APP") != 0) {
if (strcasecmp(ext, ".app") != 0) {
continue;
}
// Skip ourselves
if (strcmp(ent->d_name, "progman.app") == 0 || strcmp(ent->d_name, "PROGMAN.APP") == 0) {
if (strcasecmp(ent->d_name, "progman.app") == 0) {
continue;
}
AppEntryT *entry = &sAppFiles[sAppCount];
snprintf(entry->path, sizeof(entry->path), "%s", fullPath);
entry->iconData = NULL;
entry->tooltip[0] = '\0';
AppEntryT newEntry;
memset(&newEntry, 0, sizeof(newEntry));
snprintf(newEntry.path, sizeof(newEntry.path), "%s", fullPath);
// Default name from filename (without .app extension)
int32_t nameLen = len - 4;
@ -454,25 +456,28 @@ static void scanAppsDirRecurse(const char *dirPath) {
nameLen = SHELL_APP_NAME_MAX - 1;
}
memcpy(entry->name, ent->d_name, nameLen);
entry->name[nameLen] = '\0';
memcpy(newEntry.name, ent->d_name, nameLen);
newEntry.name[nameLen] = '\0';
if (entry->name[0] >= 'a' && entry->name[0] <= 'z') {
entry->name[0] -= 32;
if (newEntry.name[0] >= 'a' && newEntry.name[0] <= 'z') {
newEntry.name[0] -= 32;
}
// Override from embedded resources if available
entry->iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &entry->iconW, &entry->iconH, &entry->iconPitch);
newEntry.iconData = dvxResLoadIcon(sAc, fullPath, "icon32", &newEntry.iconW, &newEntry.iconH, &newEntry.iconPitch);
if (!entry->iconData) {
if (!newEntry.iconData) {
dvxLog("Progman: no icon32 resource in %s", ent->d_name);
}
dvxResLoadText(fullPath, "name", entry->name, SHELL_APP_NAME_MAX);
dvxResLoadText(fullPath, "description", entry->tooltip, sizeof(entry->tooltip));
dvxResLoadText(fullPath, "name", newEntry.name, SHELL_APP_NAME_MAX);
dvxResLoadText(fullPath, "description", newEntry.tooltip, sizeof(newEntry.tooltip));
dvxLog("Progman: found %s (%s) icon=%s", entry->name, ent->d_name, entry->iconData ? "yes" : "no");
sAppCount++;
dvxLog("Progman: found %s (%s) icon=%s", newEntry.name, ent->d_name, newEntry.iconData ? "yes" : "no");
arrput(sAppFiles, newEntry);
sAppCount = (int32_t)arrlen(sAppFiles);
dvxUpdate(sAc);
}
closedir(dir);

View file

@ -4063,6 +4063,20 @@ void dvxFitWindow(AppContextT *ctx, WindowT *win) {
int32_t newW = win->widgetRoot->calcMinW + CHROME_TOTAL_SIDE * 2;
int32_t newH = win->widgetRoot->calcMinH + topChrome + CHROME_TOTAL_BOTTOM;
// Ensure the window is at least as wide/tall as the WM minimum
// (accounts for menu bar width, title bar gadgets, etc.)
int32_t wmMinW;
int32_t wmMinH;
wmMinWindowSize(win, &wmMinW, &wmMinH);
if (newW < wmMinW) {
newW = wmMinW;
}
if (newH < wmMinH) {
newH = wmMinH;
}
dvxResizeWindow(ctx, win, newW, newH);
}

View file

@ -101,7 +101,7 @@ static void drawTitleBar(DisplayT *d, const BlitOpsT *ops, const BitmapFontT *fo
static void drawTitleGadget(DisplayT *d, const BlitOpsT *ops, const ColorSchemeT *colors, int32_t x, int32_t y, int32_t size);
// wmMinimizedIconPos declared in dvxWm.h
static int32_t scrollbarThumbInfo(const ScrollbarT *sb, int32_t *thumbPos, int32_t *thumbSize);
static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
// ============================================================
@ -1957,7 +1957,7 @@ int32_t wmMinimizedIconHit(const WindowStackT *stack, const DisplayT *d, int32_t
// This function is called on every resize move event, so it must be cheap.
// No allocations, no string operations -- just arithmetic on cached values.
static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
int32_t gadgetS = CHROME_TITLE_HEIGHT - GADGET_INSET * 2;
int32_t gadgetPad = GADGET_PAD;
int32_t charW = FONT_CHAR_WIDTH;
@ -1970,10 +1970,32 @@ static void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH) {
*minW = titleMinW + CHROME_BORDER_WIDTH * 2;
// Menu bar width: ensure window is wide enough to show all menu labels
// Menu bar width: compute directly from label text (don't rely on
// cached barX/barW which may not be computed yet before first paint).
if (win->menuBar && win->menuBar->menuCount > 0) {
MenuT *last = &win->menuBar->menus[win->menuBar->menuCount - 1];
int32_t menuW = last->barX + last->barW + CHROME_TOTAL_SIDE;
int32_t menuW = CHROME_TOTAL_SIDE;
for (int32_t i = 0; i < win->menuBar->menuCount; i++) {
// Count visible characters (skip & accelerator prefix)
const char *lbl = win->menuBar->menus[i].label;
int32_t visChars = 0;
for (const char *c = lbl; *c; c++) {
if (*c == '&' && *(c + 1) != '&' && *(c + 1) != '\0') {
continue; // skip accelerator marker
}
visChars++;
}
menuW += visChars * charW + CHROME_TITLE_PAD * 2;
if (i < win->menuBar->menuCount - 1) {
menuW += MENU_BAR_GAP;
}
}
menuW += CHROME_TOTAL_SIDE;
if (menuW > *minW) {
*minW = menuW;

View file

@ -62,6 +62,9 @@ MenuBarT *wmAddMenuBar(WindowT *win);
// Free the menu bar and reclaim the content area.
void wmDestroyMenuBar(WindowT *win);
// Get the minimum window size (accounts for chrome, gadgets, and menu bar).
void wmMinWindowSize(const WindowT *win, int32_t *minW, int32_t *minH);
// Append a dropdown menu to the menu bar. Returns the MenuT to populate
// with items. The label supports & accelerator markers (e.g. "&File").
MenuT *wmAddMenu(MenuBarT *bar, const char *label);

View file

@ -505,6 +505,27 @@ static void writeBmp(const char *path) {
}
static void drawWrapbox(void) {
clear(192, 192, 192);
box(2, 2, 20, 20, 0, 0, 180);
// Row 1: three small boxes
rect(4, 4, 5, 4, 200, 200, 255);
box(4, 4, 5, 4, 0, 0, 180);
rect(10, 4, 5, 4, 200, 200, 255);
box(10, 4, 5, 4, 0, 0, 180);
rect(16, 4, 5, 4, 200, 200, 255);
box(16, 4, 5, 4, 0, 0, 180);
// Row 2: two boxes (wrapped)
rect(4, 10, 5, 4, 200, 200, 255);
box(4, 10, 5, 4, 0, 0, 180);
rect(10, 10, 5, 4, 200, 200, 255);
box(10, 10, 5, 4, 0, 0, 180);
// Row 3: one box
rect(4, 16, 5, 4, 200, 200, 255);
box(4, 16, 5, 4, 0, 0, 180);
}
int main(int argc, char **argv) {
if (argc != 3) {
fprintf(stderr, "Usage: mkwgticon <output.bmp> <type>\n");
@ -544,6 +565,7 @@ int main(int argc, char **argv) {
{"separator", drawSeparator},
{"spacer", drawSpacer},
{"terminal", drawTerminal},
{"wrapbox", drawWrapbox},
{NULL, NULL}
};

View file

@ -40,7 +40,8 @@ WIDGETS = \
textinpt:textInput:widgetTextInput:textinpt \
timer:timer:widgetTimer:timer \
toolbar:toolbar:widgetToolbar:toolbar \
treeview:treeView:widgetTreeView:treeview
treeview:treeView:widgetTreeView:treeview \
wrapbox:wrapBox:widgetWrapBox:wrapbox
# Extract lists
WGT_NAMES = $(foreach w,$(WIDGETS),$(word 1,$(subst :, ,$w)))

View file

@ -381,6 +381,73 @@ void widgetScrollPaneLayout(WidgetT *w, const BitmapFontT *font) {
// Recurse into child containers
widgetLayoutChildren(c, font);
}
// Children with custom layout (e.g., WrapBox) may have updated
// their calcMinH after layout. Re-check if scrollbars are needed.
{
int32_t newMinW2;
int32_t newMinH2;
int32_t newInnerW2;
int32_t newInnerH2;
bool newNeedV2;
bool newNeedH2;
spCalcNeeds(w, font, &newMinW2, &newMinH2, &newInnerW2, &newInnerH2, &newNeedV2, &newNeedH2);
if (newNeedV2 != needVSb || newNeedH2 != needHSb) {
// Scrollbar needs changed — redo layout with updated sizes
contentMinH = newMinH2;
contentMinW = newMinW2;
innerW = newInnerW2;
innerH = newInnerH2;
maxScrollV = contentMinH - innerH;
maxScrollH = contentMinW - innerW;
if (maxScrollV < 0) {
maxScrollV = 0;
}
if (maxScrollH < 0) {
maxScrollH = 0;
}
sp->scrollPosV = clampInt(sp->scrollPosV, 0, maxScrollV);
sp->scrollPosH = clampInt(sp->scrollPosH, 0, maxScrollH);
virtualW = DVX_MAX(innerW, contentMinW);
virtualH = DVX_MAX(innerH, contentMinH);
baseX = w->x + SP_BORDER - sp->scrollPosH;
baseY = w->y + SP_BORDER - sp->scrollPosV;
childW = virtualW - pad * 2;
if (childW < 0) {
childW = 0;
}
pos = baseY + pad;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t ms = c->calcMinH;
if (totalWeight > 0 && c->weight > 0 && extraSpace > 0) {
ms += (extraSpace * c->weight) / totalWeight;
}
c->x = baseX + pad;
c->y = pos;
c->w = childW;
c->h = ms;
pos += ms + gap;
widgetLayoutChildren(c, font);
}
}
}
}

21
widgets/widgetWrapBox.h Normal file
View file

@ -0,0 +1,21 @@
// widgetWrapBox.h -- Flow/wrap layout container
//
// Lays out children left-to-right, wrapping to the next row when
// the available width is exceeded.
#ifndef WIDGET_WRAPBOX_H
#define WIDGET_WRAPBOX_H
#include "dvxWidget.h"
typedef struct {
WidgetT *(*create)(WidgetT *parent);
} WrapBoxApiT;
static inline const WrapBoxApiT *dvxWrapBoxApi(void) {
return (const WrapBoxApiT *)wgtGetApi("wrapbox");
}
#define wgtWrapBox(parent) dvxWrapBoxApi()->create(parent)
#endif // WIDGET_WRAPBOX_H

View file

@ -0,0 +1,306 @@
#define DVX_WIDGET_IMPL
// widgetWrapBox.c -- Flow/wrap layout container
//
// Lays out children left-to-right, wrapping to the next row when
// the available width is exceeded. Each row's height is the max
// child height in that row. Supports spacing between items and
// rows. Supports per-row alignment (center, right) for short rows.
#include "dvxWidgetPlugin.h"
#include <stdlib.h>
#include <string.h>
// ============================================================
// Constants
// ============================================================
#define DEFAULT_PAD 2
#define DEFAULT_GAP 4
// ============================================================
// Type ID and per-instance data
// ============================================================
static int32_t sTypeId = -1;
typedef struct {
int32_t wrappedH; // actual height from last layout pass (-1 = not yet computed)
} WrapBoxDataT;
// ============================================================
// widgetWrapBoxCalcMinSize
// ============================================================
void widgetWrapBoxCalcMinSize(WidgetT *w, const BitmapFontT *font) {
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t maxChildW = 0;
int32_t maxChildH = 0;
if (pad == 0) {
pad = DEFAULT_PAD;
}
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
widgetCalcMinSizeTree(c, font);
if (c->calcMinW > maxChildW) {
maxChildW = c->calcMinW;
}
if (c->calcMinH > maxChildH) {
maxChildH = c->calcMinH;
}
}
w->calcMinW = maxChildW + pad * 2;
WrapBoxDataT *d = (WrapBoxDataT *)w->data;
if (d && d->wrappedH > 0) {
w->calcMinH = d->wrappedH;
} else {
w->calcMinH = maxChildH + pad * 2;
}
}
// ============================================================
// widgetWrapBoxLayout
// ============================================================
//
// Three-pass layout:
// 1. Place children left-to-right with wrapping
// 2. Apply per-row alignment offsets
// 3. Recurse into child containers (so children see final positions)
void widgetWrapBoxLayout(WidgetT *w, const BitmapFontT *font) {
int32_t pad = wgtResolveSize(w->padding, 0, font->charWidth);
int32_t gap = wgtResolveSize(w->spacing, 0, font->charWidth);
if (pad == 0) {
pad = DEFAULT_PAD;
}
if (gap == 0) {
gap = DEFAULT_GAP;
}
int32_t availW = w->w - pad * 2;
int32_t baseX = w->x + pad;
// Pass 1: position children left-aligned with wrapping
int32_t curX = baseX;
int32_t curY = w->y + pad;
int32_t rowH = 0;
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (!c->visible) {
continue;
}
int32_t childW = c->calcMinW;
int32_t childH = c->calcMinH;
if (curX > baseX && (curX - baseX) + childW > availW) {
curX = baseX;
curY += rowH + gap;
rowH = 0;
}
c->x = curX;
c->y = curY;
c->w = childW;
c->h = childH;
if (childH > rowH) {
rowH = childH;
}
curX += childW + gap;
}
// Pass 2: apply alignment offsets per row
if (w->align == AlignCenterE || w->align == AlignEndE) {
WidgetT *rowStart = NULL;
int32_t rowY = -1;
int32_t rowEndX = 0;
for (WidgetT *c = w->firstChild; ; c = c ? c->nextSibling : NULL) {
bool newRow = false;
if (!c) {
newRow = (rowStart != NULL);
} else if (c->visible && c->y != rowY && rowStart != NULL) {
newRow = true;
}
if (newRow) {
int32_t rowW = rowEndX - baseX;
int32_t slack = availW - rowW;
if (slack > 0) {
int32_t offset = (w->align == AlignCenterE) ? slack / 2 : slack;
for (WidgetT *r = rowStart; r != c; r = r->nextSibling) {
if (r->visible && r->y == rowY) {
r->x += offset;
}
}
}
rowStart = NULL;
}
if (!c) {
break;
}
if (!c->visible) {
continue;
}
if (rowStart == NULL || c->y != rowY) {
rowStart = c;
rowY = c->y;
}
rowEndX = c->x + c->w;
}
}
// Pass 3: recurse into child containers AFTER alignment
// so children see their parent's final position.
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
if (c->visible) {
widgetLayoutChildren(c, font);
}
}
// Store actual wrapped height for subsequent measure passes.
int32_t usedH = (curY + rowH + pad) - w->y;
WrapBoxDataT *d = (WrapBoxDataT *)w->data;
if (d) {
d->wrappedH = usedH;
}
if (usedH > w->calcMinH) {
w->calcMinH = usedH;
}
}
// ============================================================
// widgetWrapBoxPaint
// ============================================================
void widgetWrapBoxPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
(void)w;
(void)disp;
(void)ops;
(void)font;
(void)colors;
}
// ============================================================
// DXE registration
// ============================================================
// ============================================================
// BASIC-facing accessors
// ============================================================
static int32_t wrapBoxGetAlign(const WidgetT *w) {
return (int32_t)w->align;
}
static void wrapBoxSetAlign(WidgetT *w, int32_t align) {
w->align = (WidgetAlignE)align;
wgtInvalidate(w);
}
static const char *sAlignNames[] = { "Left", "Center", "Right", NULL };
static const WgtPropDescT sProps[] = {
{ "Alignment", WGT_IFACE_ENUM, (void *)wrapBoxGetAlign, (void *)wrapBoxSetAlign, sAlignNames }
};
// ============================================================
// DXE registration
// ============================================================
static void wrapBoxDestroy(WidgetT *w) {
free(w->data);
}
static const WidgetClassT sClass = {
.version = WGT_CLASS_VERSION,
.flags = 0,
.handlers = {
[WGT_METHOD_PAINT] = (void *)widgetWrapBoxPaint,
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetWrapBoxCalcMinSize,
[WGT_METHOD_LAYOUT] = (void *)widgetWrapBoxLayout,
[WGT_METHOD_DESTROY] = (void *)wrapBoxDestroy,
}
};
static WidgetT *wrapBoxCreate(WidgetT *parent) {
if (!parent) {
return NULL;
}
WidgetT *w = widgetAlloc(parent, sTypeId);
if (w) {
WrapBoxDataT *d = (WrapBoxDataT *)calloc(1, sizeof(WrapBoxDataT));
if (d) {
d->wrappedH = -1;
}
w->data = d;
}
return w;
}
static const struct {
WidgetT *(*create)(WidgetT *parent);
} sApi = {
.create = wrapBoxCreate
};
static const WgtIfaceT sIface = {
.basName = "WrapBox",
.props = sProps,
.propCount = 1,
.methods = NULL,
.methodCount = 0,
.events = NULL,
.eventCount = 0,
.createSig = WGT_CREATE_PARENT,
.isContainer = true,
.defaultEvent = "Click",
.namePrefix = "WrapBox"
};
void wgtRegister(void) {
sTypeId = wgtRegisterClass(&sClass);
wgtRegisterApi("wrapbox", &sApi);
wgtRegisterIface("wrapbox", &sIface);
}

BIN
widgets/wrapBox/wrapbox.bmp (Stored with Git LFS) Normal file

Binary file not shown.

View file

@ -0,0 +1,5 @@
icon24 icon wrapbox.bmp
name text "WrapBox"
author text "DVX Project"
description text "Flow/wrap layout container"
version text "1.0"