765 lines
24 KiB
C
765 lines
24 KiB
C
// The MIT License (MIT)
|
|
//
|
|
// Copyright (C) 2026 Scott Duensing
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to
|
|
// deal in the Software without restriction, including without limitation the
|
|
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
|
// sell copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
// IN THE SOFTWARE.
|
|
|
|
#define DVX_WIDGET_IMPL
|
|
// widgetTabControl.c -- TabControl and TabPage widgets
|
|
//
|
|
// Two-level architecture: TabControlE is the container holding
|
|
// selection state and rendering the tab header strip. TabPageE
|
|
// children act as invisible sub-containers, each holding the content
|
|
// widgets for that page. Only the active page's children are visible
|
|
// and receive layout.
|
|
//
|
|
// Tab header rendering: each tab is a manually-drawn chrome piece
|
|
// (not a button widget) with top/left/right edges in highlight/shadow.
|
|
// The active tab is 2px taller than inactive tabs, extending down to
|
|
// overlap the content panel's top border -- this creates the classic
|
|
// "folder tab" illusion where the active tab appears connected to the
|
|
// panel below it. The panel's top border is erased under the active
|
|
// tab to complete the effect.
|
|
//
|
|
// Scrolling tab headers: when the total tab header width exceeds the
|
|
// available space, left/right arrow buttons appear and the header area
|
|
// becomes a clipped scrolling region. The scrollOffset tracks how many
|
|
// pixels the tab strip has scrolled. tabEnsureVisible() auto-scrolls
|
|
// to keep the active tab visible after keyboard navigation.
|
|
//
|
|
// Tab switching closes any open dropdown/combobox popup before
|
|
// switching, because the popup's owning widget may be on the
|
|
// now-hidden page and would become orphaned visually.
|
|
//
|
|
// Layout: all tab pages are positioned at the same content area
|
|
// coordinates, but only the active page has visible=true. This means
|
|
// widgetLayoutChildren is only called for the active page, saving
|
|
// layout computation for hidden pages. When switching tabs, the old
|
|
// page becomes invisible and the new page becomes visible + relaid out.
|
|
|
|
#include "dvxWgtP.h"
|
|
|
|
#define TAB_PAD_H 8
|
|
#define TAB_PAD_V 4
|
|
#define TAB_BORDER 2
|
|
// Visual "lift" of the active tab (pixels). Inactive tabs are drawn
|
|
// TAB_ACTIVE_LIFT pixels lower so the active tab appears foremost.
|
|
#define TAB_ACTIVE_LIFT 2
|
|
// Tab interior inset used by rectFill/drawVLine when painting tab body.
|
|
#define TAB_EDGE_INSET 2
|
|
// Focus-rect inset inside the active tab.
|
|
#define TAB_FOCUS_INSET 3
|
|
|
|
static int32_t sTabControlTypeId = -1;
|
|
static int32_t sTabPageTypeId = -1;
|
|
|
|
typedef struct {
|
|
int32_t activeTab;
|
|
int32_t scrollOffset;
|
|
} TabControlDataT;
|
|
|
|
typedef struct {
|
|
char *title;
|
|
} TabPageDataT;
|
|
|
|
#define TAB_ARROW_W 16
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static void tabClosePopup(void);
|
|
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font);
|
|
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font);
|
|
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font);
|
|
WidgetT *wgtTabControl(WidgetT *parent);
|
|
int32_t wgtTabControlGetActive(const WidgetT *w);
|
|
void wgtTabControlSetActive(WidgetT *w, int32_t idx);
|
|
WidgetT *wgtTabPage(WidgetT *parent, const char *title);
|
|
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font);
|
|
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font);
|
|
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod);
|
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy);
|
|
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
|
|
void widgetTabPageAccelActivate(WidgetT *w, WidgetT *root);
|
|
void widgetTabPageDestroy(WidgetT *w);
|
|
void widgetTabPageSetText(WidgetT *w, const char *text);
|
|
|
|
|
|
// tabClosePopup -- close any open dropdown/combobox popup
|
|
static void tabClosePopup(void) {
|
|
if (sOpenPopup) {
|
|
wclsClosePopup(sOpenPopup);
|
|
sOpenPopup = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// tabEnsureVisible -- scroll so active tab is visible
|
|
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
|
|
TabControlDataT *d = (TabControlDataT *)w->data;
|
|
|
|
if (!tabNeedScroll(w, font)) {
|
|
d->scrollOffset = 0;
|
|
return;
|
|
}
|
|
|
|
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
|
|
|
if (headerW < 1) {
|
|
return;
|
|
}
|
|
|
|
// Find start and end X of the active tab
|
|
int32_t tabX = 0;
|
|
int32_t tabIdx = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
TabPageDataT *pd = (TabPageDataT *)c->data;
|
|
int32_t tw = textWidthAccel(font, pd->title) + TAB_PAD_H * 2;
|
|
|
|
if (tabIdx == d->activeTab) {
|
|
int32_t tabLeft = tabX - d->scrollOffset;
|
|
int32_t tabRight = tabLeft + tw;
|
|
|
|
if (tabLeft < 0) {
|
|
d->scrollOffset += tabLeft;
|
|
} else if (tabRight > headerW) {
|
|
d->scrollOffset += tabRight - headerW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
|
|
// Clamp
|
|
int32_t totalW = tabHeaderTotalW(w, font);
|
|
int32_t maxOff = totalW - headerW;
|
|
|
|
if (maxOff < 0) {
|
|
maxOff = 0;
|
|
}
|
|
|
|
d->scrollOffset = clampInt(d->scrollOffset, 0, maxOff);
|
|
}
|
|
|
|
|
|
// tabHeaderTotalW -- total width of all tab headers
|
|
static int32_t tabHeaderTotalW(const WidgetT *w, const BitmapFontT *font) {
|
|
int32_t total = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTabPageTypeId) {
|
|
TabPageDataT *pd = (TabPageDataT *)c->data;
|
|
total += textWidthAccel(font, pd->title) + TAB_PAD_H * 2;
|
|
}
|
|
}
|
|
|
|
return total;
|
|
}
|
|
|
|
|
|
// tabNeedScroll -- do tab headers overflow?
|
|
static bool tabNeedScroll(const WidgetT *w, const BitmapFontT *font) {
|
|
int32_t totalW = tabHeaderTotalW(w, font);
|
|
return totalW > (w->w - 4);
|
|
}
|
|
|
|
|
|
WidgetT *wgtTabControl(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, sTabControlTypeId);
|
|
|
|
if (!w) {
|
|
return NULL;
|
|
}
|
|
|
|
TabControlDataT *d = calloc(1, sizeof(TabControlDataT));
|
|
|
|
if (!d) {
|
|
widgetAllocRollback(w);
|
|
return NULL;
|
|
}
|
|
|
|
d->activeTab = 0;
|
|
d->scrollOffset = 0;
|
|
w->data = d;
|
|
w->weight = WGT_WEIGHT_FILL;
|
|
return w;
|
|
}
|
|
|
|
|
|
int32_t wgtTabControlGetActive(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, sTabControlTypeId, 0);
|
|
TabControlDataT *d = (TabControlDataT *)w->data;
|
|
|
|
return d->activeTab;
|
|
}
|
|
|
|
|
|
void wgtTabControlSetActive(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, sTabControlTypeId);
|
|
TabControlDataT *d = (TabControlDataT *)w->data;
|
|
|
|
d->activeTab = idx;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
|
|
WidgetT *w = widgetAlloc(parent, sTabPageTypeId);
|
|
|
|
if (!w) {
|
|
return NULL;
|
|
}
|
|
|
|
TabPageDataT *d = calloc(1, sizeof(TabPageDataT));
|
|
|
|
if (!d) {
|
|
widgetAllocRollback(w);
|
|
return NULL;
|
|
}
|
|
|
|
d->title = strdup(title ? title : "");
|
|
w->data = d;
|
|
w->accelKey = accelParse(d->title);
|
|
return w;
|
|
}
|
|
|
|
|
|
void widgetTabPageDestroy(WidgetT *w) {
|
|
TabPageDataT *d = (TabPageDataT *)w->data;
|
|
|
|
if (d) {
|
|
free(d->title);
|
|
free(d);
|
|
w->data = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
void widgetTabPageSetText(WidgetT *w, const char *text) {
|
|
TabPageDataT *d = (TabPageDataT *)w->data;
|
|
|
|
if (d) {
|
|
free(d->title);
|
|
d->title = strdup(text ? text : "");
|
|
w->accelKey = accelParse(d->title);
|
|
}
|
|
}
|
|
|
|
|
|
// Min size: tab header height + the maximum min size across ALL pages
|
|
// (not just the active one). This ensures the tab control reserves
|
|
// enough space for the largest page, preventing resize flicker when
|
|
// switching tabs. Children are recursively measured.
|
|
void widgetTabControlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
|
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
|
int32_t maxPageW = 0;
|
|
int32_t maxPageH = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
widgetCalcMinSizeTree(c, font);
|
|
maxPageW = DVX_MAX(maxPageW, c->calcMinW);
|
|
maxPageH = DVX_MAX(maxPageH, c->calcMinH);
|
|
}
|
|
|
|
w->calcMinW = maxPageW + TAB_BORDER * 2;
|
|
w->calcMinH = tabH + maxPageH + TAB_BORDER * 2;
|
|
}
|
|
|
|
|
|
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
|
|
TabControlDataT *d = (TabControlDataT *)w->data;
|
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
|
int32_t contentX = w->x + TAB_BORDER;
|
|
int32_t contentY = w->y + tabH + TAB_BORDER;
|
|
int32_t contentW = w->w - TAB_BORDER * 2;
|
|
int32_t contentH = w->h - tabH - TAB_BORDER * 2;
|
|
|
|
if (contentW < 0) { contentW = 0; }
|
|
if (contentH < 0) { contentH = 0; }
|
|
|
|
tabEnsureVisible(w, font);
|
|
|
|
int32_t idx = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
c->x = contentX;
|
|
c->y = contentY;
|
|
c->w = contentW;
|
|
c->h = contentH;
|
|
|
|
if (idx == d->activeTab) {
|
|
c->visible = true;
|
|
widgetLayoutChildren(c, font);
|
|
} else {
|
|
c->visible = false;
|
|
}
|
|
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
|
|
// Keyboard navigation: Left/Right cycle through tabs with wrapping
|
|
// (modular arithmetic). Home/End jump to first/last tab. The tab
|
|
// control only handles these keys when it has focus -- if a child
|
|
// widget inside the active page has focus, keys go there instead.
|
|
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
TabControlDataT *d = (TabControlDataT *)w->data;
|
|
|
|
int32_t tabCount = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == sTabPageTypeId) {
|
|
tabCount++;
|
|
}
|
|
}
|
|
|
|
if (tabCount <= 1) {
|
|
return;
|
|
}
|
|
|
|
int32_t active = d->activeTab;
|
|
|
|
if (key == KEY_RIGHT) {
|
|
active = (active + 1) % tabCount;
|
|
} else if (key == KEY_LEFT) {
|
|
active = (active - 1 + tabCount) % tabCount;
|
|
} else if (key == KEY_HOME) {
|
|
active = 0;
|
|
} else if (key == KEY_END) {
|
|
active = tabCount - 1;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if (active != d->activeTab) {
|
|
tabClosePopup();
|
|
d->activeTab = active;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
}
|
|
|
|
|
|
// Mouse clicks in the tab header area walk the tab list computing
|
|
// accumulated X positions to find which tab was clicked. Only clicks
|
|
// in the header strip (top tabH pixels) are handled here -- clicks
|
|
// on the content area go through normal child hit-testing. Scroll
|
|
// arrow clicks adjust scrollOffset by 4 character widths at a time.
|
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
sFocusedWidget = hit;
|
|
TabControlDataT *d = (TabControlDataT *)hit->data;
|
|
AppContextT *ctx = (AppContextT *)root->userData;
|
|
const BitmapFontT *font = &ctx->font;
|
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
|
|
|
// Only handle clicks in the tab header area
|
|
if (vy < hit->y || vy >= hit->y + tabH) {
|
|
return;
|
|
}
|
|
|
|
bool scroll = tabNeedScroll(hit, font);
|
|
|
|
// Check scroll arrow clicks
|
|
if (scroll) {
|
|
int32_t totalW = tabHeaderTotalW(hit, font);
|
|
int32_t headerW = hit->w - TAB_ARROW_W * 2 - 4;
|
|
int32_t maxOff = totalW - headerW;
|
|
|
|
if (maxOff < 0) {
|
|
maxOff = 0;
|
|
}
|
|
|
|
// Left arrow
|
|
if (vx >= hit->x && vx < hit->x + TAB_ARROW_W) {
|
|
d->scrollOffset -= font->charWidth * 4;
|
|
d->scrollOffset = clampInt(d->scrollOffset, 0, maxOff);
|
|
wgtInvalidatePaint(hit);
|
|
return;
|
|
}
|
|
|
|
// Right arrow
|
|
if (vx >= hit->x + hit->w - TAB_ARROW_W && vx < hit->x + hit->w) {
|
|
d->scrollOffset += font->charWidth * 4;
|
|
d->scrollOffset = clampInt(d->scrollOffset, 0, maxOff);
|
|
wgtInvalidatePaint(hit);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Click on tab header
|
|
int32_t headerLeft = hit->x + 2 + (scroll ? TAB_ARROW_W : 0);
|
|
int32_t tabX = headerLeft - d->scrollOffset;
|
|
int32_t tabIdx = 0;
|
|
|
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
TabPageDataT *pd = (TabPageDataT *)c->data;
|
|
int32_t tw = textWidthAccel(font, pd->title) + TAB_PAD_H * 2;
|
|
|
|
if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) {
|
|
if (tabIdx != d->activeTab) {
|
|
tabClosePopup();
|
|
d->activeTab = tabIdx;
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
|
|
wgtInvalidate(hit);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
}
|
|
|
|
|
|
// Paint order: content panel first (raised bevel below the tab strip),
|
|
// then scroll arrows if needed, then tab headers in a clipped region,
|
|
// then the active page's children. Tab headers are painted with a clip
|
|
// rect so partially-scrolled tabs at the edges are cleanly truncated.
|
|
//
|
|
// The active tab is drawn 2px taller (extending from w->y instead of
|
|
// w->y+2) and erases the panel's top border beneath it (2px of
|
|
// contentBg), creating the visual connection between tab and panel.
|
|
// Inactive tabs sit 2px lower and draw a bottom border to separate
|
|
// them from the panel.
|
|
//
|
|
// Only the active page's children are painted (WCLASS_PAINTS_CHILDREN
|
|
// flag means the generic paint won't descend into tab control children).
|
|
// This is critical for performance on 486 -- we skip painting all
|
|
// hidden pages entirely.
|
|
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
TabControlDataT *td = (TabControlDataT *)w->data;
|
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
|
bool scroll = tabNeedScroll(w, font);
|
|
bool selfDirty = w->paintDirty;
|
|
|
|
if (!selfDirty) {
|
|
goto paintChildren;
|
|
}
|
|
|
|
// Content panel
|
|
BevelStyleT panelBevel;
|
|
panelBevel.highlight = colors->windowHighlight;
|
|
panelBevel.shadow = colors->windowShadow;
|
|
panelBevel.face = colors->contentBg;
|
|
panelBevel.width = 2;
|
|
drawBevel(d, ops, w->x, w->y + tabH, w->w, w->h - tabH, &panelBevel);
|
|
|
|
// Scroll arrows
|
|
if (scroll) {
|
|
int32_t totalW = tabHeaderTotalW(w, font);
|
|
int32_t headerW = w->w - TAB_ARROW_W * 2 - 4;
|
|
int32_t maxOff = totalW - headerW;
|
|
|
|
if (maxOff < 0) {
|
|
maxOff = 0;
|
|
}
|
|
|
|
td->scrollOffset = clampInt(td->scrollOffset, 0, maxOff);
|
|
|
|
bool canScrollLeft = (td->scrollOffset > 0);
|
|
bool canScrollRight = (td->scrollOffset < maxOff);
|
|
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
|
|
|
// Left arrow button
|
|
drawBevel(d, ops, w->x, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
|
{
|
|
uint32_t arrowFg = canScrollLeft ? colors->contentFg : colors->windowShadow;
|
|
int32_t cx = w->x + TAB_ARROW_W / 2;
|
|
int32_t cy = w->y + tabH / 2;
|
|
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawVLine(d, ops, cx - 2 + i, cy - i, 1 + i * 2, arrowFg);
|
|
}
|
|
}
|
|
|
|
// Right arrow button
|
|
int32_t rx = w->x + w->w - TAB_ARROW_W;
|
|
drawBevel(d, ops, rx, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
|
{
|
|
uint32_t arrowFg = canScrollRight ? colors->contentFg : colors->windowShadow;
|
|
int32_t cx = rx + TAB_ARROW_W / 2;
|
|
int32_t cy = w->y + tabH / 2;
|
|
|
|
for (int32_t i = 0; i < 4; i++) {
|
|
drawVLine(d, ops, cx + 2 - i, cy - i, 1 + i * 2, arrowFg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tab headers -- clip to header area (tabH + TAB_ACTIVE_LIFT for the active tab's extra height)
|
|
int32_t headerLeft = w->x + TAB_EDGE_INSET + (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;
|
|
int32_t oldClipY = d->clipY;
|
|
int32_t oldClipW = d->clipW;
|
|
int32_t oldClipH = d->clipH;
|
|
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + TAB_ACTIVE_LIFT);
|
|
|
|
// 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;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
TabPageDataT *pd = (TabPageDataT *)c->data;
|
|
int32_t tw = textWidthAccel(font, pd->title) + TAB_PAD_H * 2;
|
|
bool isActive = (tabIdx == td->activeTab);
|
|
int32_t ty = isActive ? w->y : w->y + TAB_ACTIVE_LIFT;
|
|
int32_t th = isActive ? tabH + TAB_ACTIVE_LIFT : tabH;
|
|
uint32_t tabFace = isActive ? colors->contentBg : colors->windowFace;
|
|
|
|
// Only draw tabs that are at least partially visible
|
|
if (tabX + tw > headerLeft && tabX < headerRight) {
|
|
// Fill tab background
|
|
rectFill(d, ops, tabX + TAB_EDGE_INSET, ty + TAB_EDGE_INSET, tw - 2 * TAB_EDGE_INSET, th - TAB_EDGE_INSET, tabFace);
|
|
|
|
// Top edge
|
|
drawHLine(d, ops, tabX + TAB_EDGE_INSET, ty, tw - 2 * TAB_EDGE_INSET, colors->windowHighlight);
|
|
drawHLine(d, ops, tabX + TAB_EDGE_INSET, ty + 1, tw - 2 * TAB_EDGE_INSET, colors->windowHighlight);
|
|
|
|
// Left edge
|
|
drawVLine(d, ops, tabX, ty + TAB_EDGE_INSET, th - TAB_EDGE_INSET, colors->windowHighlight);
|
|
drawVLine(d, ops, tabX + 1, ty + TAB_EDGE_INSET, th - TAB_EDGE_INSET, colors->windowHighlight);
|
|
|
|
// Right edge
|
|
drawVLine(d, ops, tabX + tw - 1, ty + TAB_EDGE_INSET, th - TAB_EDGE_INSET, colors->windowShadow);
|
|
drawVLine(d, ops, tabX + tw - 2, ty + TAB_EDGE_INSET, th - TAB_EDGE_INSET, colors->windowShadow);
|
|
|
|
if (isActive) {
|
|
// Erase panel top border under active tab
|
|
rectFill(d, ops, tabX + TAB_EDGE_INSET, w->y + tabH, tw - 2 * TAB_EDGE_INSET, TAB_EDGE_INSET, colors->contentBg);
|
|
} else {
|
|
// Bottom edge for inactive tab
|
|
drawHLine(d, ops, tabX, ty + th - 1, tw, colors->windowShadow);
|
|
drawHLine(d, ops, tabX + 1, ty + th - 2, tw - 2, colors->windowShadow);
|
|
}
|
|
|
|
// Tab label
|
|
int32_t labelY = ty + TAB_PAD_V;
|
|
|
|
if (!isActive) {
|
|
labelY++;
|
|
}
|
|
|
|
drawTextAccel(d, ops, font, tabX + TAB_PAD_H, labelY, pd->title, colors->contentFg, tabFace, true);
|
|
|
|
if (isActive && w == sFocusedWidget) {
|
|
drawFocusRect(d, ops, tabX + TAB_FOCUS_INSET, ty + TAB_FOCUS_INSET, tw - 2 * TAB_FOCUS_INSET, th - TAB_FOCUS_INSET - 1, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
|
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
|
|
paintChildren:
|
|
;
|
|
// Paint only active tab page's children
|
|
int32_t activeIdx = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
if (activeIdx == td->activeTab) {
|
|
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
|
|
widgetPaintOne(gc, d, ops, font, colors);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
activeIdx++;
|
|
}
|
|
}
|
|
|
|
|
|
void widgetTabPageAccelActivate(WidgetT *w, WidgetT *root) {
|
|
(void)root;
|
|
|
|
if (!w->parent || w->parent->type != sTabControlTypeId) {
|
|
return;
|
|
}
|
|
|
|
TabControlDataT *d = (TabControlDataT *)w->parent->data;
|
|
|
|
// Find our index among sibling tab pages
|
|
int32_t idx = 0;
|
|
|
|
for (WidgetT *c = w->parent->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != sTabPageTypeId) {
|
|
continue;
|
|
}
|
|
|
|
if (c == w) {
|
|
tabClosePopup();
|
|
d->activeTab = idx;
|
|
wgtInvalidate(w->parent);
|
|
return;
|
|
}
|
|
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
|
|
static const WidgetClassT sClassTabControl = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_FOCUSABLE | WCLASS_PAINTS_CHILDREN,
|
|
.handlers = {
|
|
[WGT_METHOD_PAINT] = (void *)widgetTabControlPaint,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetTabControlCalcMinSize,
|
|
[WGT_METHOD_LAYOUT] = (void *)widgetTabControlLayout,
|
|
[WGT_METHOD_ON_MOUSE] = (void *)widgetTabControlOnMouse,
|
|
[WGT_METHOD_ON_KEY] = (void *)widgetTabControlOnKey,
|
|
}
|
|
};
|
|
|
|
static const WidgetClassT sClassTabPage = {
|
|
.version = WGT_CLASS_VERSION,
|
|
.flags = WCLASS_ACCEL_WHEN_HIDDEN,
|
|
.handlers = {
|
|
[WGT_METHOD_ON_ACCEL_ACTIVATE] = (void *)widgetTabPageAccelActivate,
|
|
[WGT_METHOD_CALC_MIN_SIZE] = (void *)widgetCalcMinSizeBox,
|
|
[WGT_METHOD_LAYOUT] = (void *)widgetLayoutBox,
|
|
[WGT_METHOD_SET_TEXT] = (void *)widgetTabPageSetText,
|
|
[WGT_METHOD_DESTROY] = (void *)widgetTabPageDestroy,
|
|
}
|
|
};
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent);
|
|
WidgetT *(*page)(WidgetT *parent, const char *title);
|
|
void (*setActive)(WidgetT *w, int32_t idx);
|
|
int32_t (*getActive)(const WidgetT *w);
|
|
} sApi = {
|
|
.create = wgtTabControl,
|
|
.page = wgtTabPage,
|
|
.setActive = wgtTabControlSetActive,
|
|
.getActive = wgtTabControlGetActive
|
|
};
|
|
|
|
static const WgtPropDescT sProps[] = {
|
|
{ "TabIndex", WGT_IFACE_INT, (void *)wgtTabControlGetActive, (void *)wgtTabControlSetActive, NULL }
|
|
};
|
|
|
|
// BASIC wrapper: AddPage creates a tab page and returns nothing.
|
|
// The page is accessible by index via SetActive.
|
|
static void basAddPage(WidgetT *w, const char *title) {
|
|
wgtTabPage(w, title);
|
|
}
|
|
|
|
static const WgtMethodDescT sMethods[] = {
|
|
{ "AddPage", WGT_SIG_STR, (void *)basAddPage },
|
|
{ "GetActive", WGT_SIG_RET_INT, (void *)wgtTabControlGetActive },
|
|
{ "SetActive", WGT_SIG_INT, (void *)wgtTabControlSetActive },
|
|
};
|
|
|
|
static const WgtIfaceT sIface = {
|
|
.basName = "TabStrip",
|
|
.props = sProps,
|
|
.propCount = 1,
|
|
.methods = sMethods,
|
|
.methodCount = 3,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT,
|
|
.isContainer = true,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
// TabPage: authored in .frm as Begin TabPage name ... End inside a
|
|
// TabStrip. Caption sets the title string. The page is a container
|
|
// so children nest inside it (one page per Begin/End block).
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent, const char *title);
|
|
} sTabPageApi = {
|
|
.create = wgtTabPage
|
|
};
|
|
|
|
static const WgtIfaceT sTabPageIface = {
|
|
.basName = "TabPage",
|
|
.props = NULL,
|
|
.propCount = 0,
|
|
.methods = NULL,
|
|
.methodCount = 0,
|
|
.events = NULL,
|
|
.eventCount = 0,
|
|
.createSig = WGT_CREATE_PARENT_TEXT,
|
|
.isContainer = true,
|
|
.defaultEvent = "Click"
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTabControlTypeId = wgtRegisterClass(&sClassTabControl);
|
|
sTabPageTypeId = wgtRegisterClass(&sClassTabPage);
|
|
wgtRegisterApi("tabcontrol", &sApi);
|
|
wgtRegisterIface("tabcontrol", &sIface);
|
|
wgtRegisterApi("tabpage", &sTabPageApi);
|
|
wgtRegisterIface("tabpage", &sTabPageIface);
|
|
}
|