// 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) { if (!w || w->type != WidgetTabControlE) { return 0; } return w->as.tabControl.activeTab; } // ============================================================ // wgtTabControlSetActive // ============================================================ void wgtTabControlSetActive(WidgetT *w, int32_t idx) { if (!w || w->type != WidgetTabControlE) { return; } w->as.tabControl.activeTab = idx; } // ============================================================ // 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++; } }