530 lines
16 KiB
C
530 lines
16 KiB
C
#define DVX_WIDGET_IMPL
|
|
// 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 "dvxWidgetPlugin.h"
|
|
|
|
#define SPLITTER_BAR_W 5
|
|
#define SPLITTER_MIN_PANE 20
|
|
|
|
static int32_t sTypeId = -1;
|
|
|
|
typedef struct {
|
|
int32_t dividerPos;
|
|
bool vertical;
|
|
} SplitterDataT;
|
|
|
|
|
|
// ============================================================
|
|
// Prototypes
|
|
// ============================================================
|
|
|
|
static WidgetT *spFirstChild(WidgetT *w);
|
|
static WidgetT *spSecondChild(WidgetT *w);
|
|
static void widgetSplitterDestroy(WidgetT *w);
|
|
static void widgetSplitterScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY);
|
|
|
|
|
|
// ============================================================
|
|
// 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) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
WidgetT *c1 = spFirstChild(w);
|
|
WidgetT *c2 = spSecondChild(w);
|
|
bool vert = d->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;
|
|
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
|
|
if (d->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) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
WidgetT *c1 = spFirstChild(w);
|
|
WidgetT *c2 = spSecondChild(w);
|
|
int32_t pos = d->dividerPos;
|
|
|
|
widgetSplitterClampPos(w, &pos);
|
|
d->dividerPos = pos;
|
|
|
|
if (d->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) {
|
|
SplitterDataT *d = (SplitterDataT *)hit->data;
|
|
int32_t pos = d->dividerPos;
|
|
|
|
// Check if click is on the divider bar
|
|
bool onDivider;
|
|
|
|
if (d->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 (d->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 *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
int32_t pos = d->dividerPos;
|
|
|
|
// Paint first child with clip rect
|
|
WidgetT *c1 = spFirstChild(w);
|
|
WidgetT *c2 = spSecondChild(w);
|
|
|
|
int32_t oldClipX = disp->clipX;
|
|
int32_t oldClipY = disp->clipY;
|
|
int32_t oldClipW = disp->clipW;
|
|
int32_t oldClipH = disp->clipH;
|
|
|
|
if (c1) {
|
|
if (d->vertical) {
|
|
setClipRect(disp, w->x, w->y, pos, w->h);
|
|
} else {
|
|
setClipRect(disp, w->x, w->y, w->w, pos);
|
|
}
|
|
|
|
widgetPaintOne(c1, disp, ops, font, colors);
|
|
setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
}
|
|
|
|
if (c2) {
|
|
if (d->vertical) {
|
|
setClipRect(disp, w->x + pos + SPLITTER_BAR_W, w->y, w->w - pos - SPLITTER_BAR_W, w->h);
|
|
} else {
|
|
setClipRect(disp, w->x, w->y + pos + SPLITTER_BAR_W, w->w, w->h - pos - SPLITTER_BAR_W);
|
|
}
|
|
|
|
widgetPaintOne(c2, disp, ops, font, colors);
|
|
setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);
|
|
}
|
|
|
|
// Draw divider bar -- raised bevel
|
|
BevelStyleT bevel = BEVEL_RAISED(colors, 1);
|
|
|
|
if (d->vertical) {
|
|
int32_t barX = w->x + pos;
|
|
drawBevel(disp, 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(disp, ops, gx, dy, 2, colors->windowHighlight);
|
|
drawHLine(disp, ops, gx + 1, dy + 1, 2, colors->windowShadow);
|
|
}
|
|
} else {
|
|
int32_t barY = w->y + pos;
|
|
drawBevel(disp, 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(disp, ops, dx, gy, 1, colors->windowHighlight);
|
|
drawHLine(disp, ops, dx + 1, gy, 1, colors->windowShadow);
|
|
drawHLine(disp, ops, dx, gy + 1, 1, colors->windowHighlight);
|
|
drawHLine(disp, ops, dx + 1, gy + 1, 1, colors->windowShadow);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSplitterGetCursorShape
|
|
// ============================================================
|
|
|
|
int32_t widgetSplitterGetCursorShape(const WidgetT *w, int32_t vx, int32_t vy) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
|
|
// During active drag, always show resize cursor
|
|
if (sDragSplitter == w) {
|
|
return d->vertical ? CURSOR_RESIZE_H : CURSOR_RESIZE_V;
|
|
}
|
|
|
|
int32_t pos = d->dividerPos;
|
|
|
|
if (d->vertical) {
|
|
int32_t barX = w->x + pos;
|
|
|
|
if (vx >= barX && vx < barX + SPLITTER_BAR_W) {
|
|
return CURSOR_RESIZE_H;
|
|
}
|
|
} else {
|
|
int32_t barY = w->y + pos;
|
|
|
|
if (vy >= barY && vy < barY + SPLITTER_BAR_W) {
|
|
return CURSOR_RESIZE_V;
|
|
}
|
|
}
|
|
|
|
// Not on our divider -- check children (handles nested splitters
|
|
// and other widgets with cursor shapes like ListView column borders)
|
|
for (WidgetT *c = w->firstChild; c; c = c->nextSibling) {
|
|
WidgetT *child = widgetHitTest(c, vx, vy);
|
|
|
|
if (child && child->wclass && child->wclass->getCursorShape) {
|
|
int32_t shape = child->wclass->getCursorShape(child, vx, vy);
|
|
|
|
if (shape > 0) {
|
|
return shape;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSplitterDestroy
|
|
// ============================================================
|
|
|
|
static void widgetSplitterDestroy(WidgetT *w) {
|
|
free(w->data);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// widgetSplitterScrollDragUpdate
|
|
// ============================================================
|
|
|
|
static void widgetSplitterScrollDragUpdate(WidgetT *w, int32_t orient, int32_t dragOff, int32_t mouseX, int32_t mouseY) {
|
|
(void)orient;
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
int32_t pos;
|
|
|
|
if (d->vertical) {
|
|
pos = mouseX - w->x - dragOff;
|
|
} else {
|
|
pos = mouseY - w->y - dragOff;
|
|
}
|
|
|
|
widgetSplitterClampPos(w, &pos);
|
|
|
|
if (pos != d->dividerPos) {
|
|
d->dividerPos = pos;
|
|
|
|
if (w->onChange) {
|
|
w->onChange(w);
|
|
}
|
|
|
|
wgtInvalidate(w);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
static const WidgetClassT sClassSplitter = {
|
|
.flags = WCLASS_PAINTS_CHILDREN | WCLASS_NO_HIT_RECURSE,
|
|
.paint = widgetSplitterPaint,
|
|
.paintOverlay = NULL,
|
|
.calcMinSize = widgetSplitterCalcMinSize,
|
|
.layout = widgetSplitterLayout,
|
|
.onMouse = widgetSplitterOnMouse,
|
|
.onKey = NULL,
|
|
.destroy = widgetSplitterDestroy,
|
|
.getText = NULL,
|
|
.setText = NULL,
|
|
.getCursorShape = widgetSplitterGetCursorShape,
|
|
.scrollDragUpdate = widgetSplitterScrollDragUpdate
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// Widget creation functions
|
|
// ============================================================
|
|
|
|
|
|
WidgetT *wgtSplitter(WidgetT *parent, bool vertical) {
|
|
WidgetT *w = widgetAlloc(parent, sTypeId);
|
|
|
|
if (w) {
|
|
SplitterDataT *d = (SplitterDataT *)calloc(1, sizeof(SplitterDataT));
|
|
d->vertical = vertical;
|
|
d->dividerPos = 0;
|
|
w->data = d;
|
|
w->weight = 100;
|
|
}
|
|
|
|
return w;
|
|
}
|
|
|
|
|
|
int32_t wgtSplitterGetPos(const WidgetT *w) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
return d->dividerPos;
|
|
}
|
|
|
|
|
|
void wgtSplitterSetPos(WidgetT *w, int32_t pos) {
|
|
SplitterDataT *d = (SplitterDataT *)w->data;
|
|
d->dividerPos = pos;
|
|
wgtInvalidate(w);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// DXE registration
|
|
// ============================================================
|
|
|
|
|
|
static const struct {
|
|
WidgetT *(*create)(WidgetT *parent, bool vertical);
|
|
void (*setPos)(WidgetT *w, int32_t pos);
|
|
int32_t (*getPos)(const WidgetT *w);
|
|
} sApi = {
|
|
.create = wgtSplitter,
|
|
.setPos = wgtSplitterSetPos,
|
|
.getPos = wgtSplitterGetPos
|
|
};
|
|
|
|
void wgtRegister(void) {
|
|
sTypeId = wgtRegisterClass(&sClassSplitter);
|
|
wgtRegisterApi("splitter", &sApi);
|
|
}
|