Several more minor widget fixes.

This commit is contained in:
Scott Duensing 2026-04-13 20:57:05 -05:00
parent 094b263c36
commit 7c1eb495e1
10 changed files with 59 additions and 45 deletions

View file

@ -6743,13 +6743,13 @@ static void onFormWinPaint(WindowT *win, RectT *dirtyArea) {
// Force measure + relayout only on structural changes (control
// add/remove/resize). Selection clicks just need repaint + overlay.
if (win->fullRepaint && win->widgetRoot) {
if (win->paintNeeded >= PAINT_FULL && win->widgetRoot) {
widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
win->widgetRoot->w = 0; // force layout pass
}
// Designer always needs full repaint (handles must be erased/redrawn)
win->fullRepaint = true;
win->paintNeeded = PAINT_FULL;
widgetOnPaint(win, dirtyArea);
// Then draw selection handles on top

View file

@ -373,7 +373,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Initial paint (dark background)
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
onPaint(sWin, &fullRect);
sWin->contentDirty = true;
sWin->iconNeedsRefresh = true;
return 0;
}

View file

@ -197,7 +197,7 @@ Central window object. Each window owns a persistent content backbuffer and rece
int32_t contentX, contentY, contentW, contentH Content area inset from frame
char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags
bool contentDirty true when contentBuf has changed
bool iconNeedsRefresh true when contentBuf changed (for minimized icon refresh)
bool needsPaint true until first onPaint call
int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize

View file

@ -1044,7 +1044,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (rWin->onPaint) {
RectT fullRect = {0, 0, rWin->contentW, rWin->contentH};
rWin->onPaint(rWin, &fullRect);
rWin->contentDirty = true;
rWin->iconNeedsRefresh = true;
}
dirtyListAdd(&ctx->dirty, rWin->x, rWin->y, rWin->w, rWin->h);
@ -3094,7 +3094,7 @@ static void pollWidgets(AppContextT *ctx) {
int32_t dirtyH = 0;
if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) {
win->contentDirty = true;
win->iconNeedsRefresh = true;
if (!win->minimized) {
int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
@ -3136,13 +3136,13 @@ static void refreshMinimizedIcons(AppContextT *ctx) {
continue;
}
if (!win->iconData && win->contentDirty) {
if (!win->iconData && win->iconNeedsRefresh) {
if (count >= ctx->iconRefreshIdx) {
int32_t ix;
int32_t iy;
wmMinimizedIconPos(d, iconIdx, &ix, &iy);
dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE);
win->contentDirty = false;
win->iconNeedsRefresh = false;
ctx->iconRefreshIdx = count + 1;
return;
}
@ -3192,7 +3192,7 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t
if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
// Dirty new position
@ -3693,7 +3693,7 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
}
@ -4351,7 +4351,7 @@ void dvxInvalidateWindow(AppContextT *ctx, WindowT *win) {
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
}
win->contentDirty = true;
win->iconNeedsRefresh = true;
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
}
@ -5081,22 +5081,19 @@ bool dvxUpdate(AppContextT *ctx) {
refreshMinimizedIcons(ctx);
}
// Flush deferred widget paints. wgtInvalidatePaint sets
// widgetPaintPending instead of calling dvxInvalidateWindow inline,
// so multiple invalidations per frame are batched into one tree walk.
// Also handles first-paint (fullRepaint) for newly created windows.
// Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
// (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
// batched into one paint — the highest level wins.
for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i];
if ((win->widgetPaintPending || win->fullRepaint) && win->onPaint) {
win->widgetPaintPending = false;
if (win->paintNeeded && win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
// fullRepaint is cleared by widgetOnPaint for widget windows.
// For raw-paint windows, clear it here so they don't repaint
// every frame forever.
win->fullRepaint = false;
win->contentDirty = true;
// widgetOnPaint clears paintNeeded for widget windows.
// For raw-paint windows, clear it here.
win->paintNeeded = PAINT_NONE;
win->iconNeedsRefresh = true;
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
}
}

View file

@ -465,6 +465,11 @@ typedef struct {
#define MIN_WINDOW_H 60
#define WM_MAX_FROM_SCREEN (-1) // use screen dimension as max (for maxW/maxH)
// Window paint states (for WindowT.paintNeeded)
#define PAINT_NONE 0 // no paint needed
#define PAINT_PARTIAL 1 // only dirty widgets (deferred wgtInvalidatePaint)
#define PAINT_FULL 2 // clear + relayout + repaint all (wgtInvalidate)
// Hit test region identifiers (returned via hitPart from wmHitTest)
#define HIT_CONTENT 0
#define HIT_TITLE 1
@ -506,9 +511,12 @@ typedef struct WindowT {
bool maximized;
bool resizable;
bool modal;
bool contentDirty; // true when contentBuf has changed since last icon refresh
bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones
bool widgetPaintPending; // deferred widget paint (set by wgtInvalidatePaint)
bool iconNeedsRefresh; // true when contentBuf has changed since last icon refresh
// Paint state: PAINT_NONE = idle, PAINT_PARTIAL = only dirty widgets,
// PAINT_FULL = clear background + relayout + repaint all.
// wgtInvalidatePaint sets PARTIAL, wgtInvalidate sets FULL.
// Higher values take priority (FULL > PARTIAL > NONE).
uint8_t paintNeeded; // 0=none, 1=partial, 2=full
int32_t maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width)
int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height)
// Pre-maximize geometry is saved so wmRestore() can put the window

View file

@ -1323,7 +1323,7 @@ WindowT *wmCreateWindow(WindowStackT *stack, DisplayT *d, const char *title, int
memset(win->contentBuf, 0xFF, bufSize);
}
win->fullRepaint = true;
win->paintNeeded = PAINT_FULL;
stack->windows[stack->count] = win;
stack->count++;
@ -1419,6 +1419,7 @@ void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouse
void wmDragEnd(WindowStackT *stack) {
stack->dragWindow = -1;
stack->dragActive = false;
}
@ -1620,7 +1621,7 @@ void wmDrawContent(DisplayT *d, const BlitOpsT *ops, WindowT *win, const RectT *
// 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
// pass is needed. iconNeedsRefresh tracks whether the content has changed
// since the last icon refresh.
//
// Icons are drawn before windows in the compositing loop (painter's
@ -1902,10 +1903,10 @@ void wmMaximize(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT
}
if (win->onPaint) {
win->fullRepaint = true;
win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
dirtyListAdd(dl, win->x, win->y, win->w, win->h);
@ -2420,10 +2421,10 @@ void wmResizeMove(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, int32_
}
if (win->onPaint) {
win->fullRepaint = true;
win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
// Always update dragOff to the clamped position so the next delta
@ -2474,10 +2475,10 @@ void wmRestore(WindowStackT *stack, DirtyListT *dl, const DisplayT *d, WindowT *
}
if (win->onPaint) {
win->fullRepaint = true;
win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect);
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
// Mark restored position dirty

View file

@ -301,7 +301,7 @@ static void widgetOnMouseInner(WindowT *win, WidgetT *root, int32_t x, int32_t y
int32_t rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY2;
int32_t rectW = win->contentW;
win->contentDirty = true;
win->iconNeedsRefresh = true;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
return;
}
@ -507,7 +507,7 @@ void widgetOnBlur(WindowT *win) {
// refresh its minimized icon thumbnail if needed.
void widgetOnFocus(WindowT *win) {
win->contentDirty = true;
win->iconNeedsRefresh = true;
}
@ -560,8 +560,8 @@ void widgetOnPaint(WindowT *win, RectT *dirtyArea) {
cd.clipW = win->contentW;
cd.clipH = win->contentH;
bool full = win->fullRepaint;
win->fullRepaint = false;
bool full = (win->paintNeeded >= PAINT_FULL);
win->paintNeeded = PAINT_NONE;
if (full) {
// Full repaint: clear background, relayout, paint everything

View file

@ -408,7 +408,7 @@ void wgtInvalidate(WidgetT *w) {
}
// Full repaint — layout changed, all widgets need redrawing
w->window->fullRepaint = true;
w->window->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, w->window);
}
@ -443,8 +443,10 @@ void wgtInvalidatePaint(WidgetT *w) {
// Defer the actual paint — it will happen once in the main loop
// before compositing, batching multiple invalidations into one
// tree walk instead of one per call.
w->window->widgetPaintPending = true;
// tree walk instead of one per call. Don't downgrade FULL to PARTIAL.
if (w->window->paintNeeded < PAINT_PARTIAL) {
w->window->paintNeeded = PAINT_PARTIAL;
}
}

View file

@ -1358,7 +1358,7 @@ img { max-width: 100%; }
int32_t contentX, contentY, contentW, contentH Content area inset from frame
char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags
bool contentDirty true when contentBuf has changed
bool iconNeedsRefresh true when contentBuf changed (for minimized icon refresh)
bool needsPaint true until first onPaint call
int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize

View file

@ -444,8 +444,8 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
}
}
// Tab headers -- clip to header area
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
// Tab headers -- clip to header area (tabH + 2 for the active tab's extra height)
int32_t headerLeft = w->x + 2 + (scroll ? TAB_ARROW_W : 0);
int32_t headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);
int32_t oldClipX = d->clipX;
@ -454,8 +454,14 @@ void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const B
int32_t oldClipH = d->clipH;
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
// Clear the header strip so scrolled-away tab pixels are erased
rectFill(d, ops, headerLeft, w->y, headerRight - headerLeft, tabH + 2, colors->contentBg);
// Clear the header strip so scrolled-away tab pixels are erased.
// Only clear above the content panel border (tabH, not tabH+2).
rectFill(d, ops, headerLeft, w->y, headerRight - headerLeft, tabH, colors->contentBg);
// Draw the content panel top border across the header area.
// Individual active tabs will erase it under themselves below.
drawHLine(d, ops, headerLeft, w->y + tabH, headerRight - headerLeft, colors->windowHighlight);
drawHLine(d, ops, headerLeft, w->y + tabH + 1, headerRight - headerLeft, colors->windowHighlight);
int32_t tabX = headerLeft - td->scrollOffset;
int32_t tabIdx = 0;