// widgetSplitter.c -- Splitter (draggable divider between two panes) // // A container that divides its area between exactly two child widgets // with a draggable divider bar between them. The divider position // (dividerPos) is stored as a pixel offset from the leading edge // (left for vertical, top for horizontal). // // Architecture: the splitter is a special container (has its own // layout function) that manually positions its two children and the // divider bar. It does NOT use the generic box layout. Children are // clipped to their respective panes during painting to prevent // overflow into the other pane. // // The divider bar (SPLITTER_BAR_W = 5px) is drawn as a raised bevel // with a "gripper" pattern -- small embossed 2x2 bumps arranged // in a line centered on the bar. This provides visual feedback that // the bar is draggable, following the Win3.1/Motif convention. // // Drag state: divider dragging stores the clicked splitter widget // and the mouse offset within the bar in globals (sDragSplitter, // sDragSplitStart). The event loop handles mouse-move during drag // by computing the new divider position from mouse coordinates and // calling widgetSplitterClampPos to enforce minimum pane sizes. // // Minimum pane sizes come from children's calcMinW/H, with a floor // of SPLITTER_MIN_PANE (20px) to prevent panes from collapsing to // nothing. The clamp also ensures the divider can't be dragged past // where the second pane would violate its minimum. #include "widgetInternal.h" // ============================================================ // Prototypes // ============================================================ static WidgetT *spFirstChild(WidgetT *w); static WidgetT *spSecondChild(WidgetT *w); // ============================================================ // spFirstChild -- get first visible child // ============================================================ // These helpers skip invisible children so the splitter works // correctly even if one pane is hidden. The splitter only cares // about the first two visible children; any additional children // are ignored (though this shouldn't happen in practice). static WidgetT *spFirstChild(WidgetT *w) { for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->visible) { return c; } } return NULL; } // ============================================================ // spSecondChild -- get second visible child // ============================================================ static WidgetT *spSecondChild(WidgetT *w) { int32_t n = 0; for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { if (c->visible) { n++; if (n == 2) { return c; } } } return NULL; } // ============================================================ // widgetSplitterClampPos -- clamp divider to child minimums // ============================================================ void widgetSplitterClampPos(WidgetT *w, int32_t *pos) { WidgetT *c1 = spFirstChild(w); WidgetT *c2 = spSecondChild(w); bool vert = w->as.splitter.vertical; int32_t totalSize = vert ? w->w : w->h; int32_t minFirst = c1 ? (vert ? c1->calcMinW : c1->calcMinH) : SPLITTER_MIN_PANE; int32_t minSecond = c2 ? (vert ? c2->calcMinW : c2->calcMinH) : SPLITTER_MIN_PANE; if (minFirst < SPLITTER_MIN_PANE) { minFirst = SPLITTER_MIN_PANE; } if (minSecond < SPLITTER_MIN_PANE) { minSecond = SPLITTER_MIN_PANE; } int32_t maxPos = totalSize - SPLITTER_BAR_W - minSecond; if (maxPos < minFirst) { maxPos = minFirst; } *pos = clampInt(*pos, minFirst, maxPos); } // ============================================================ // widgetSplitterCalcMinSize // ============================================================ void widgetSplitterCalcMinSize(WidgetT *w, const BitmapFontT *font) { // Recursively measure children for (WidgetT *c = w->firstChild; c; c = c->nextSibling) { widgetCalcMinSizeTree(c, font); } WidgetT *c1 = spFirstChild(w); WidgetT *c2 = spSecondChild(w); int32_t m1w = c1 ? c1->calcMinW : 0; int32_t m1h = c1 ? c1->calcMinH : 0; int32_t m2w = c2 ? c2->calcMinW : 0; int32_t m2h = c2 ? c2->calcMinH : 0; if (w->as.splitter.vertical) { w->calcMinW = m1w + m2w + SPLITTER_BAR_W; w->calcMinH = DVX_MAX(m1h, m2h); } else { w->calcMinW = DVX_MAX(m1w, m2w); w->calcMinH = m1h + m2h + SPLITTER_BAR_W; } } // ============================================================ // widgetSplitterLayout // ============================================================ // Layout assigns each child its full pane area, then recurses into // child layouts. The first child gets [0, dividerPos) and the second // gets [dividerPos + SPLITTER_BAR_W, end). The divider position is // clamped before use to respect minimum pane sizes, even if the // user hasn't dragged yet (dividerPos=0 from construction gets // clamped to minFirst). void widgetSplitterLayout(WidgetT *w, const BitmapFontT *font) { WidgetT *c1 = spFirstChild(w); WidgetT *c2 = spSecondChild(w); int32_t pos = w->as.splitter.dividerPos; widgetSplitterClampPos(w, &pos); w->as.splitter.dividerPos = pos; if (w->as.splitter.vertical) { // Left pane if (c1) { c1->x = w->x; c1->y = w->y; c1->w = pos; c1->h = w->h; widgetLayoutChildren(c1, font); } // Right pane if (c2) { c2->x = w->x + pos + SPLITTER_BAR_W; c2->y = w->y; c2->w = w->w - pos - SPLITTER_BAR_W; c2->h = w->h; if (c2->w < 0) { c2->w = 0; } widgetLayoutChildren(c2, font); } } else { // Top pane if (c1) { c1->x = w->x; c1->y = w->y; c1->w = w->w; c1->h = pos; widgetLayoutChildren(c1, font); } // Bottom pane if (c2) { c2->x = w->x; c2->y = w->y + pos + SPLITTER_BAR_W; c2->w = w->w; c2->h = w->h - pos - SPLITTER_BAR_W; if (c2->h < 0) { c2->h = 0; } widgetLayoutChildren(c2, font); } } } // ============================================================ // widgetSplitterOnMouse // ============================================================ // Mouse handling first checks if the click is on the divider bar. // If so, it starts a drag. If not, it recursively hit-tests into // children and forwards the event. This manual hit-test forwarding // is needed because the splitter has WCLASS_NO_HIT_RECURSE (the // generic hit-test would find the splitter but not recurse into its // clipped children). void widgetSplitterOnMouse(WidgetT *hit, WidgetT *root, int32_t vx, int32_t vy) { int32_t pos = hit->as.splitter.dividerPos; // Check if click is on the divider bar bool onDivider; if (hit->as.splitter.vertical) { int32_t barX = hit->x + pos; onDivider = (vx >= barX && vx < barX + SPLITTER_BAR_W); } else { int32_t barY = hit->y + pos; onDivider = (vy >= barY && vy < barY + SPLITTER_BAR_W); } if (onDivider) { // Start dragging sDragSplitter = hit; if (hit->as.splitter.vertical) { sDragSplitStart = vx - hit->x - pos; } else { sDragSplitStart = vy - hit->y - pos; } return; } // Forward click to child widgets WidgetT *child = NULL; for (WidgetT *c = hit->firstChild; c; c = c->nextSibling) { WidgetT *ch = widgetHitTest(c, vx, vy); if (ch) { child = ch; } } if (child && child->enabled && child->wclass && child->wclass->onMouse) { if (sFocusedWidget && sFocusedWidget != child) { sFocusedWidget->focused = false; } child->wclass->onMouse(child, root, vx, vy); if (child->focused) { sFocusedWidget = child; } } wgtInvalidatePaint(hit); } // ============================================================ // widgetSplitterPaint // ============================================================ // Paint clips each child to its pane area, preventing overflow across // the divider. The clip rect is saved/restored around each child's // paint. The divider bar is painted last (on top) so it's always // visible even if a child overflows. // // The gripper bumps use the classic embossed-dot technique: a // highlight pixel at (x,y) and a shadow pixel at (x+1,y+1) create // a tiny raised bump. 11 bumps spaced 3px apart center vertically // (or horizontally) on the bar. This is purely decorative but // provides the expected visual affordance for "draggable". void widgetSplitterPaint(WidgetT *w, DisplayT *d, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) { int32_t pos = w->as.splitter.dividerPos; // Paint first child with clip rect WidgetT *c1 = spFirstChild(w); WidgetT *c2 = spSecondChild(w); int32_t oldClipX = d->clipX; int32_t oldClipY = d->clipY; int32_t oldClipW = d->clipW; int32_t oldClipH = d->clipH; if (c1) { if (w->as.splitter.vertical) { setClipRect(d, w->x, w->y, pos, w->h); } else { setClipRect(d, w->x, w->y, w->w, pos); } widgetPaintOne(c1, d, ops, font, colors); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); } if (c2) { if (w->as.splitter.vertical) { setClipRect(d, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h); } else { setClipRect(d, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W); } widgetPaintOne(c2, d, ops, font, colors); setClipRect(d, oldClipX, oldClipY, oldClipW, oldClipH); } // Draw divider bar -- raised bevel BevelStyleT bevel = BEVEL_RAISED(colors, 1); if (w->as.splitter.vertical) { int32_t barX = w->x + pos; drawBevel(d, ops, barX, w->y, SPLITTER_BAR_W, w->h, &bevel); // Gripper -- row of embossed 2x2 bumps centered vertically int32_t gx = barX + 1; int32_t midY = w->y + w->h / 2; int32_t count = 5; for (int32_t i = -count; i <= count; i++) { int32_t dy = midY + i * 3; drawHLine(d, ops, gx, dy, 2, colors->windowHighlight); drawHLine(d, ops, gx + 1, dy + 1, 2, colors->windowShadow); } } else { int32_t barY = w->y + pos; drawBevel(d, ops, w->x, barY, w->w, SPLITTER_BAR_W, &bevel); // Gripper -- row of embossed 2x2 bumps centered horizontally int32_t gy = barY + 1; int32_t midX = w->x + w->w / 2; int32_t count = 5; for (int32_t i = -count; i <= count; i++) { int32_t dx = midX + i * 3; drawHLine(d, ops, dx, gy, 1, colors->windowHighlight); drawHLine(d, ops, dx + 1, gy, 1, colors->windowShadow); drawHLine(d, ops, dx, gy + 1, 1, colors->windowHighlight); drawHLine(d, ops, dx + 1, gy + 1, 1, colors->windowShadow); } } } // ============================================================ // wgtSplitter // ============================================================ WidgetT *wgtSplitter(WidgetT *parent, bool vertical) { WidgetT *w = widgetAlloc(parent, WidgetSplitterE); if (w) { w->as.splitter.vertical = vertical; w->as.splitter.dividerPos = 0; w->weight = 100; } return w; } // ============================================================ // wgtSplitterGetPos // ============================================================ int32_t wgtSplitterGetPos(const WidgetT *w) { return w->as.splitter.dividerPos; } // ============================================================ // wgtSplitterSetPos // ============================================================ void wgtSplitterSetPos(WidgetT *w, int32_t pos) { w->as.splitter.dividerPos = pos; wgtInvalidate(w); }