// 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) {
            if (d->activeTab != idx) {
                tabClosePopup();
                d->activeTab = idx;

                // Fire the tab control's Change handler, matching the
                // mouse and arrow-key tab-switch paths.
                if (w->parent->onChange) {
                    w->parent->onChange(w->parent);
                }

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