// 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); }