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 // Force measure + relayout only on structural changes (control
// add/remove/resize). Selection clicks just need repaint + overlay. // 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); widgetCalcMinSizeTree(win->widgetRoot, &sAc->font);
win->widgetRoot->w = 0; // force layout pass win->widgetRoot->w = 0; // force layout pass
} }
// Designer always needs full repaint (handles must be erased/redrawn) // Designer always needs full repaint (handles must be erased/redrawn)
win->fullRepaint = true; win->paintNeeded = PAINT_FULL;
widgetOnPaint(win, dirtyArea); widgetOnPaint(win, dirtyArea);
// Then draw selection handles on top // Then draw selection handles on top

View file

@ -373,7 +373,7 @@ int32_t appMain(DxeAppContextT *ctx) {
// Initial paint (dark background) // Initial paint (dark background)
RectT fullRect = {0, 0, sWin->contentW, sWin->contentH}; RectT fullRect = {0, 0, sWin->contentW, sWin->contentH};
onPaint(sWin, &fullRect); onPaint(sWin, &fullRect);
sWin->contentDirty = true; sWin->iconNeedsRefresh = true;
return 0; 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 int32_t contentX, contentY, contentW, contentH Content area inset from frame
char title[MAX_TITLE_LEN] Window title text (max 128 chars) char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags 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 bool needsPaint true until first onPaint call
int32_t maxW, maxH Maximum dimensions int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize

View file

@ -1044,7 +1044,7 @@ static void dispatchEvents(AppContextT *ctx) {
if (rWin->onPaint) { if (rWin->onPaint) {
RectT fullRect = {0, 0, rWin->contentW, rWin->contentH}; RectT fullRect = {0, 0, rWin->contentW, rWin->contentH};
rWin->onPaint(rWin, &fullRect); rWin->onPaint(rWin, &fullRect);
rWin->contentDirty = true; rWin->iconNeedsRefresh = true;
} }
dirtyListAdd(&ctx->dirty, rWin->x, rWin->y, rWin->w, rWin->h); dirtyListAdd(&ctx->dirty, rWin->x, rWin->y, rWin->w, rWin->h);
@ -3094,7 +3094,7 @@ static void pollWidgets(AppContextT *ctx) {
int32_t dirtyH = 0; int32_t dirtyH = 0;
if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) { if (wclsQuickRepaint(w, &dirtyY, &dirtyH) > 0) {
win->contentDirty = true; win->iconNeedsRefresh = true;
if (!win->minimized) { if (!win->minimized) {
int32_t scrollY = win->vScroll ? win->vScroll->value : 0; int32_t scrollY = win->vScroll ? win->vScroll->value : 0;
@ -3136,13 +3136,13 @@ static void refreshMinimizedIcons(AppContextT *ctx) {
continue; continue;
} }
if (!win->iconData && win->contentDirty) { if (!win->iconData && win->iconNeedsRefresh) {
if (count >= ctx->iconRefreshIdx) { if (count >= ctx->iconRefreshIdx) {
int32_t ix; int32_t ix;
int32_t iy; int32_t iy;
wmMinimizedIconPos(d, iconIdx, &ix, &iy); wmMinimizedIconPos(d, iconIdx, &ix, &iy);
dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE); dirtyListAdd(&ctx->dirty, ix, iy, ICON_TOTAL_SIZE, ICON_TOTAL_SIZE);
win->contentDirty = false; win->iconNeedsRefresh = false;
ctx->iconRefreshIdx = count + 1; ctx->iconRefreshIdx = count + 1;
return; return;
} }
@ -3192,7 +3192,7 @@ static void repositionWindow(AppContextT *ctx, WindowT *win, int32_t x, int32_t
if (win->onPaint) { if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
win->contentDirty = true; win->iconNeedsRefresh = true;
} }
// Dirty new position // Dirty new position
@ -3693,7 +3693,7 @@ int32_t dvxChangeVideoMode(AppContextT *ctx, int32_t requestedW, int32_t request
if (win->onPaint) { if (win->onPaint) {
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); 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_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
} }
win->contentDirty = true; win->iconNeedsRefresh = true;
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h);
} }
@ -5081,22 +5081,19 @@ bool dvxUpdate(AppContextT *ctx) {
refreshMinimizedIcons(ctx); refreshMinimizedIcons(ctx);
} }
// Flush deferred widget paints. wgtInvalidatePaint sets // Flush deferred paints. paintNeeded is set by wgtInvalidatePaint
// widgetPaintPending instead of calling dvxInvalidateWindow inline, // (PARTIAL) or wgtInvalidate (FULL). Multiple calls per frame are
// so multiple invalidations per frame are batched into one tree walk. // batched into one paint — the highest level wins.
// Also handles first-paint (fullRepaint) for newly created windows.
for (int32_t i = 0; i < ctx->stack.count; i++) { for (int32_t i = 0; i < ctx->stack.count; i++) {
WindowT *win = ctx->stack.windows[i]; WindowT *win = ctx->stack.windows[i];
if ((win->widgetPaintPending || win->fullRepaint) && win->onPaint) { if (win->paintNeeded && win->onPaint) {
win->widgetPaintPending = false;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect)); WIN_CALLBACK(ctx, win, win->onPaint(win, &fullRect));
// fullRepaint is cleared by widgetOnPaint for widget windows. // widgetOnPaint clears paintNeeded for widget windows.
// For raw-paint windows, clear it here so they don't repaint // For raw-paint windows, clear it here.
// every frame forever. win->paintNeeded = PAINT_NONE;
win->fullRepaint = false; win->iconNeedsRefresh = true;
win->contentDirty = true;
dirtyListAdd(&ctx->dirty, win->x, win->y, win->w, win->h); 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 MIN_WINDOW_H 60
#define WM_MAX_FROM_SCREEN (-1) // use screen dimension as max (for maxW/maxH) #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) // Hit test region identifiers (returned via hitPart from wmHitTest)
#define HIT_CONTENT 0 #define HIT_CONTENT 0
#define HIT_TITLE 1 #define HIT_TITLE 1
@ -506,9 +511,12 @@ typedef struct WindowT {
bool maximized; bool maximized;
bool resizable; bool resizable;
bool modal; bool modal;
bool contentDirty; // true when contentBuf has changed since last icon refresh bool iconNeedsRefresh; // true when contentBuf has changed since last icon refresh
bool fullRepaint; // true = clear + repaint all widgets; false = only dirty ones // Paint state: PAINT_NONE = idle, PAINT_PARTIAL = only dirty widgets,
bool widgetPaintPending; // deferred widget paint (set by wgtInvalidatePaint) // 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 maxW; // maximum width (WM_MAX_FROM_SCREEN = use screen width)
int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height) int32_t maxH; // maximum height (WM_MAX_FROM_SCREEN = use screen height)
// Pre-maximize geometry is saved so wmRestore() can put the window // 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); memset(win->contentBuf, 0xFF, bufSize);
} }
win->fullRepaint = true; win->paintNeeded = PAINT_FULL;
stack->windows[stack->count] = win; stack->windows[stack->count] = win;
stack->count++; stack->count++;
@ -1419,6 +1419,7 @@ void wmDragBegin(WindowStackT *stack, int32_t idx, int32_t mouseX, int32_t mouse
void wmDragEnd(WindowStackT *stack) { void wmDragEnd(WindowStackT *stack) {
stack->dragWindow = -1; 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 // The live thumbnail (option 2) shows miniature window contents in the
// icon/task view, giving a quick preview of each minimized window. The thumbnail // icon/task view, giving a quick preview of each minimized window. The thumbnail
// is rendered from the existing content buffer, so no extra rendering // 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. // since the last icon refresh.
// //
// Icons are drawn before windows in the compositing loop (painter's // 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) { if (win->onPaint) {
win->fullRepaint = true; win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->iconNeedsRefresh = true;
} }
dirtyListAdd(dl, win->x, win->y, win->w, win->h); 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) { if (win->onPaint) {
win->fullRepaint = true; win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->iconNeedsRefresh = true;
} }
// Always update dragOff to the clamped position so the next delta // 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) { if (win->onPaint) {
win->fullRepaint = true; win->paintNeeded = PAINT_FULL;
RectT fullRect = {0, 0, win->contentW, win->contentH}; RectT fullRect = {0, 0, win->contentW, win->contentH};
win->onPaint(win, &fullRect); win->onPaint(win, &fullRect);
win->contentDirty = true; win->iconNeedsRefresh = true;
} }
// Mark restored position dirty // 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 rectX = win->x + win->contentX;
int32_t rectY = win->y + win->contentY + dirtyY - scrollY2; int32_t rectY = win->y + win->contentY + dirtyY - scrollY2;
int32_t rectW = win->contentW; int32_t rectW = win->contentW;
win->contentDirty = true; win->iconNeedsRefresh = true;
dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH); dirtyListAdd(&ctx->dirty, rectX, rectY, rectW, dirtyH);
return; return;
} }
@ -507,7 +507,7 @@ void widgetOnBlur(WindowT *win) {
// refresh its minimized icon thumbnail if needed. // refresh its minimized icon thumbnail if needed.
void widgetOnFocus(WindowT *win) { 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.clipW = win->contentW;
cd.clipH = win->contentH; cd.clipH = win->contentH;
bool full = win->fullRepaint; bool full = (win->paintNeeded >= PAINT_FULL);
win->fullRepaint = false; win->paintNeeded = PAINT_NONE;
if (full) { if (full) {
// Full repaint: clear background, relayout, paint everything // 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 // Full repaint — layout changed, all widgets need redrawing
w->window->fullRepaint = true; w->window->paintNeeded = PAINT_FULL;
dvxInvalidateWindow(ctx, w->window); dvxInvalidateWindow(ctx, w->window);
} }
@ -443,8 +443,10 @@ void wgtInvalidatePaint(WidgetT *w) {
// Defer the actual paint — it will happen once in the main loop // Defer the actual paint — it will happen once in the main loop
// before compositing, batching multiple invalidations into one // before compositing, batching multiple invalidations into one
// tree walk instead of one per call. // tree walk instead of one per call. Don't downgrade FULL to PARTIAL.
w->window->widgetPaintPending = true; 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 int32_t contentX, contentY, contentW, contentH Content area inset from frame
char title[MAX_TITLE_LEN] Window title text (max 128 chars) char title[MAX_TITLE_LEN] Window title text (max 128 chars)
bool visible, focused, minimized, maximized, resizable, modal Window state flags 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 bool needsPaint true until first onPaint call
int32_t maxW, maxH Maximum dimensions int32_t maxW, maxH Maximum dimensions
int32_t preMaxX, preMaxY, preMaxW, preMaxH Saved geometry before maximize 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 // 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 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 headerRight = scroll ? (w->x + w->w - TAB_ARROW_W) : (w->x + w->w);
int32_t oldClipX = d->clipX; 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; int32_t oldClipH = d->clipH;
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2); setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
// Clear the header strip so scrolled-away tab pixels are erased // Clear the header strip so scrolled-away tab pixels are erased.
rectFill(d, ops, headerLeft, w->y, headerRight - headerLeft, tabH + 2, colors->contentBg); // 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 tabX = headerLeft - td->scrollOffset;
int32_t tabIdx = 0; int32_t tabIdx = 0;