From 7c1eb495e1ca5fdab5d6aab7d0f8887318a6009a Mon Sep 17 00:00:00 2001 From: Scott Duensing Date: Mon, 13 Apr 2026 20:57:05 -0500 Subject: [PATCH] Several more minor widget fixes. --- apps/dvxbasic/ide/ideMain.c | 4 ++-- apps/imgview/imgview.c | 2 +- core/apiref.dhs | 2 +- core/dvxApp.c | 33 ++++++++++++--------------- core/dvxTypes.h | 14 +++++++++--- core/dvxWm.c | 17 +++++++------- core/widgetEvent.c | 8 +++---- core/widgetOps.c | 8 ++++--- docs/dvx_system_reference.html | 2 +- widgets/tabControl/widgetTabControl.c | 14 ++++++++---- 10 files changed, 59 insertions(+), 45 deletions(-) diff --git a/apps/dvxbasic/ide/ideMain.c b/apps/dvxbasic/ide/ideMain.c index 389d4b9..3a1fb93 100644 --- a/apps/dvxbasic/ide/ideMain.c +++ b/apps/dvxbasic/ide/ideMain.c @@ -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 diff --git a/apps/imgview/imgview.c b/apps/imgview/imgview.c index de76ad9..ea3dc41 100644 --- a/apps/imgview/imgview.c +++ b/apps/imgview/imgview.c @@ -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; } diff --git a/core/apiref.dhs b/core/apiref.dhs index 3d35f4e..0448e82 100644 --- a/core/apiref.dhs +++ b/core/apiref.dhs @@ -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 diff --git a/core/dvxApp.c b/core/dvxApp.c index 5b2b6b0..3ed5f55 100644 --- a/core/dvxApp.c +++ b/core/dvxApp.c @@ -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); } } diff --git a/core/dvxTypes.h b/core/dvxTypes.h index 0165596..4ff19e0 100644 --- a/core/dvxTypes.h +++ b/core/dvxTypes.h @@ -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 diff --git a/core/dvxWm.c b/core/dvxWm.c index d8d7b73..96faadc 100644 --- a/core/dvxWm.c +++ b/core/dvxWm.c @@ -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 diff --git a/core/widgetEvent.c b/core/widgetEvent.c index 44550bc..868057e 100644 --- a/core/widgetEvent.c +++ b/core/widgetEvent.c @@ -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 diff --git a/core/widgetOps.c b/core/widgetOps.c index db3d771..25b266a 100644 --- a/core/widgetOps.c +++ b/core/widgetOps.c @@ -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; + } } diff --git a/docs/dvx_system_reference.html b/docs/dvx_system_reference.html index 7e2fb47..30c6df7 100644 --- a/docs/dvx_system_reference.html +++ b/docs/dvx_system_reference.html @@ -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 diff --git a/widgets/tabControl/widgetTabControl.c b/widgets/tabControl/widgetTabControl.c index 4265468..57e0cc4 100644 --- a/widgets/tabControl/widgetTabControl.c +++ b/widgets/tabControl/widgetTabControl.c @@ -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;