502 lines
14 KiB
C
502 lines
14 KiB
C
// widgetTabControl.c — TabControl and TabPage widgets
|
|
|
|
#include "widgetInternal.h"
|
|
|
|
#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);
|
|
|
|
|
|
// ============================================================
|
|
// tabClosePopup — close any open dropdown/combobox popup
|
|
// ============================================================
|
|
|
|
static void tabClosePopup(void) {
|
|
if (sOpenPopup) {
|
|
if (sOpenPopup->type == WidgetDropdownE) {
|
|
sOpenPopup->as.dropdown.open = false;
|
|
} else if (sOpenPopup->type == WidgetComboBoxE) {
|
|
sOpenPopup->as.comboBox.open = false;
|
|
}
|
|
|
|
sOpenPopup = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// tabEnsureVisible — scroll so active tab is visible
|
|
// ============================================================
|
|
|
|
static void tabEnsureVisible(WidgetT *w, const BitmapFontT *font) {
|
|
if (!tabNeedScroll(w, font)) {
|
|
w->as.tabControl.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 != WidgetTabPageE) {
|
|
continue;
|
|
}
|
|
|
|
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
|
|
|
if (tabIdx == w->as.tabControl.activeTab) {
|
|
int32_t tabLeft = tabX - w->as.tabControl.scrollOffset;
|
|
int32_t tabRight = tabLeft + tw;
|
|
|
|
if (tabLeft < 0) {
|
|
w->as.tabControl.scrollOffset += tabLeft;
|
|
} else if (tabRight > headerW) {
|
|
w->as.tabControl.scrollOffset += tabRight - headerW;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
|
|
// Clamp
|
|
int32_t totalW = tabHeaderTotalW(w, font);
|
|
int32_t maxOff = totalW - headerW;
|
|
|
|
if (maxOff < 0) {
|
|
maxOff = 0;
|
|
}
|
|
|
|
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.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 == WidgetTabPageE) {
|
|
total += textWidthAccel(font, c->as.tabPage.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);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTabControl
|
|
// ============================================================
|
|
|
|
WidgetT *wgtTabControl(WidgetT *parent) {
|
|
WidgetT *w = widgetAlloc(parent, WidgetTabControlE);
|
|
|
|
if (w) {
|
|
w->as.tabControl.activeTab = 0;
|
|
w->as.tabControl.scrollOffset = 0;
|
|
w->weight = 100;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTabControlGetActive
|
|
// ============================================================
|
|
|
|
int32_t wgtTabControlGetActive(const WidgetT *w) {
|
|
VALIDATE_WIDGET(w, WidgetTabControlE, 0);
|
|
|
|
return w->as.tabControl.activeTab;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTabControlSetActive
|
|
// ============================================================
|
|
|
|
void wgtTabControlSetActive(WidgetT *w, int32_t idx) {
|
|
VALIDATE_WIDGET_VOID(w, WidgetTabControlE);
|
|
|
|
w->as.tabControl.activeTab = idx;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// wgtTabPage
|
|
// ============================================================
|
|
|
|
WidgetT *wgtTabPage(WidgetT *parent, const char *title) {
|
|
WidgetT *w = widgetAlloc(parent, WidgetTabPageE);
|
|
|
|
if (w) {
|
|
w->as.tabPage.title = title;
|
|
w->accelKey = accelParse(title);
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTabControlCalcMinSize
|
|
// ============================================================
|
|
|
|
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 != WidgetTabPageE) {
|
|
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;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTabControlLayout
|
|
// ============================================================
|
|
|
|
void widgetTabControlLayout(WidgetT *w, const BitmapFontT *font) {
|
|
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 != WidgetTabPageE) {
|
|
continue;
|
|
}
|
|
|
|
c->x = contentX;
|
|
c->y = contentY;
|
|
c->w = contentW;
|
|
c->h = contentH;
|
|
|
|
if (idx == w->as.tabControl.activeTab) {
|
|
c->visible = true;
|
|
widgetLayoutChildren(c, font);
|
|
} else {
|
|
c->visible = false;
|
|
}
|
|
|
|
idx++;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTabControlOnKey
|
|
// ============================================================
|
|
|
|
void widgetTabControlOnKey(WidgetT *w, int32_t key, int32_t mod) {
|
|
(void)mod;
|
|
|
|
int32_t tabCount = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type == WidgetTabPageE) {
|
|
tabCount++;
|
|
}
|
|
}
|
|
|
|
if (tabCount <= 1) {
|
|
return;
|
|
}
|
|
|
|
int32_t active = w->as.tabControl.activeTab;
|
|
|
|
if (key == (0x4D | 0x100)) {
|
|
active = (active + 1) % tabCount;
|
|
} else if (key == (0x4B | 0x100)) {
|
|
active = (active - 1 + tabCount) % tabCount;
|
|
} else if (key == (0x47 | 0x100)) {
|
|
active = 0;
|
|
} else if (key == (0x4F | 0x100)) {
|
|
active = tabCount - 1;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if (active != w->as.tabControl.activeTab) {
|
|
tabClosePopup();
|
|
w->as.tabControl.activeTab = active;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTabControlOnMouse
|
|
// ============================================================
|
|
|
|
void widgetTabControlOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) {
|
|
hit->focused = true;
|
|
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) {
|
|
hit->as.tabControl.scrollOffset -= font->charWidth * 4;
|
|
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.scrollOffset, 0, maxOff);
|
|
wgtInvalidatePaint(hit);
|
|
return;
|
|
}
|
|
|
|
// Right arrow
|
|
if (vx >= hit->x + hit->w - TAB_ARROW_W && vx < hit->x + hit->w) {
|
|
hit->as.tabControl.scrollOffset += font->charWidth * 4;
|
|
hit->as.tabControl.scrollOffset = clampInt(hit->as.tabControl.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 - hit->as.tabControl.scrollOffset;
|
|
int32_t tabIdx = 0;
|
|
|
|
for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != WidgetTabPageE) {
|
|
continue;
|
|
}
|
|
|
|
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
|
|
|
if (vx >= tabX && vx < tabX + tw && vx >= headerLeft) {
|
|
if (tabIdx != hit->as.tabControl.activeTab) {
|
|
tabClosePopup();
|
|
hit->as.tabControl.activeTab = tabIdx;
|
|
|
|
if (hit->onChange) {
|
|
hit->onChange(hit);
|
|
}
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetTabControlPaint
|
|
// ============================================================
|
|
|
|
void widgetTabControlPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
int32_t tabH = font->charHeight + TAB_PAD_V * 2;
|
|
bool scroll = tabNeedScroll(w, font);
|
|
|
|
// 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;
|
|
}
|
|
|
|
w->as.tabControl.scrollOffset = clampInt(w->as.tabControl.scrollOffset, 0, maxOff);
|
|
|
|
uint32_t fg = colors->contentFg;
|
|
BevelStyleT btnBevel = BEVEL_RAISED(colors, 1);
|
|
|
|
// Left arrow button
|
|
drawBevel(d, ops, w->x, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
|
{
|
|
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, fg);
|
|
}
|
|
}
|
|
|
|
// Right arrow button
|
|
int32_t rx = w->x + w->w - TAB_ARROW_W;
|
|
drawBevel(d, ops, rx, w->y, TAB_ARROW_W, tabH, &btnBevel);
|
|
{
|
|
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, fg);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tab headers — clip to header area
|
|
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;
|
|
int32_t oldClipY = d->clipY;
|
|
int32_t oldClipW = d->clipW;
|
|
int32_t oldClipH = d->clipH;
|
|
setClipRect(d, headerLeft, w->y, headerRight - headerLeft, tabH + 2);
|
|
|
|
int32_t tabX = headerLeft - w->as.tabControl.scrollOffset;
|
|
int32_t tabIdx = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != WidgetTabPageE) {
|
|
continue;
|
|
}
|
|
|
|
int32_t tw = textWidthAccel(font, c->as.tabPage.title) + TAB_PAD_H * 2;
|
|
bool isActive = (tabIdx == w->as.tabControl.activeTab);
|
|
int32_t ty = isActive ? w->y : w->y + 2;
|
|
int32_t th = isActive ? tabH + 2 : 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 + 2, ty + 2, tw - 4, th - 2, tabFace);
|
|
|
|
// Top edge
|
|
drawHLine(d, ops, tabX + 2, ty, tw - 4, colors->windowHighlight);
|
|
drawHLine(d, ops, tabX + 2, ty + 1, tw - 4, colors->windowHighlight);
|
|
|
|
// Left edge
|
|
drawVLine(d, ops, tabX, ty + 2, th - 2, colors->windowHighlight);
|
|
drawVLine(d, ops, tabX + 1, ty + 2, th - 2, colors->windowHighlight);
|
|
|
|
// Right edge
|
|
drawVLine(d, ops, tabX + tw - 1, ty + 2, th - 2, colors->windowShadow);
|
|
drawVLine(d, ops, tabX + tw - 2, ty + 2, th - 2, colors->windowShadow);
|
|
|
|
if (isActive) {
|
|
// Erase panel top border under active tab
|
|
rectFill(d, ops, tabX + 2, w->y + tabH, tw - 4, 2, 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, c->as.tabPage.title, colors->contentFg, tabFace, true);
|
|
|
|
if (isActive && w->focused) {
|
|
drawFocusRect(d, ops, tabX + 3, ty + 3, tw - 6, th - 4, colors->contentFg);
|
|
}
|
|
}
|
|
|
|
tabX += tw;
|
|
tabIdx++;
|
|
}
|
|
|
|
setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
|
|
// Paint only active tab page's children
|
|
tabIdx = 0;
|
|
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
if (c->type != WidgetTabPageE) {
|
|
continue;
|
|
}
|
|
|
|
if (tabIdx == w->as.tabControl.activeTab) {
|
|
for (WidgetT *gc = c->firstChild; gc; gc = gc->nextSibling) {
|
|
widgetPaintOne(gc, d, ops, font, colors);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
tabIdx++;
|
|
}
|
|
}
|