// ═══════════════════════════════════════════════════════════════════
// Splitter primitives — runtime-resizable dividers
// ═══════════════════════════════════════════════════════════════════
//
// Slint's `@children` slot can only appear ONCE per component, so
// a single "two-slot split view" component is structurally
// awkward. Instead this file ships the primitive — the divider
// handle with drag logic — and lets the caller arrange the two
// panels via a regular HorizontalLayout / VerticalLayout.
//
// Two helpers:
//
// NeonSplitterHandle — col-resize divider (4px wide).
// NeonVSplitterHandle — row-resize divider (4px tall).
//
// Mounting pattern (horizontal split):
//
// in-out property <length> left-w: 240px;
// HorizontalLayout {
// spacing: 0px;
// Rectangle { width: root.left-w; /* left content */ }
// NeonSplitterHandle {
// position <=> root.left-w;
// bound-min: 120px;
// bound-max: parent.width - 200px;
// }
// Rectangle { horizontal-stretch: 1; /* right content */ }
// }
//
// Vertical split: same shape with VerticalLayout +
// NeonVSplitterHandle. The host owns the position; the handle
// mutates it via drag.
//
// Why a primitive instead of a "two-children" component:
// • Avoids the `@children` once-per-component limit cleanly.
// • Lets the caller decide how to compose the panels (extra
// layers, custom backgrounds, scroll containers, etc.).
// • Drop-in compatible with std HorizontalLayout / VerticalLayout
// so designers don't need to learn a new layout idiom.
//
// The previous version's bugs (hardcoded 600px upper bound,
// per-event grab reset jitter) are gone here — the drag stores
// grab-x and start-pos at pointer-down and computes a cumulative
// delta on each `moved` event.
//
// ═══════════════════════════════════════════════════════════════════
import { Theme } from "../theme.slint";
// ── Horizontal handle — vertical bar, col-resize cursor ─────────────
export component NeonSplitterHandle inherits Rectangle {
/// Two-way bound to the leading panel's width. The handle
/// mutates this on drag; the caller binds it to whatever
/// state owns the split layout.
in-out property <length> position;
/// Hard bounds applied during drag. `bound-max` typically
/// uses `parent.width - <trailing min>` so the trailing panel
/// never collapses below its minimum.
in property <length> bound-min: 80px;
in property <length> bound-max: 4000px;
in property <length> thickness: 4px;
in property <color> idle-color: Theme.border;
in property <color> active-color: Theme.neon-cyan;
callback dragged;
width: thickness;
background: drag.has-hover || drag.pressed
? root.active-color
: root.idle-color;
animate background { duration: Theme.transition-fast; }
drag := TouchArea {
mouse-cursor: col-resize;
property <length> grab-x: 0px;
property <length> start-pos: 0px;
property <bool> dragging: false;
pointer-event(e) => {
if (e.kind == PointerEventKind.down
&& e.button == PointerEventButton.left)
{
self.grab-x = self.mouse-x;
self.start-pos = root.position;
self.dragging = true;
}
if (e.kind == PointerEventKind.up) {
self.dragging = false;
}
}
moved => {
if (self.dragging) {
root.position = clamp(
self.start-pos + self.mouse-x - self.grab-x,
root.bound-min,
max(root.bound-min, root.bound-max),
);
root.dragged();
}
}
}
}
// ── Vertical handle — horizontal bar, row-resize cursor ─────────────
export component NeonVSplitterHandle inherits Rectangle {
in-out property <length> position;
in property <length> bound-min: 60px;
in property <length> bound-max: 4000px;
in property <length> thickness: 4px;
in property <color> idle-color: Theme.border;
in property <color> active-color: Theme.neon-cyan;
callback dragged;
height: thickness;
background: drag.has-hover || drag.pressed
? root.active-color
: root.idle-color;
animate background { duration: Theme.transition-fast; }
drag := TouchArea {
mouse-cursor: row-resize;
property <length> grab-y: 0px;
property <length> start-pos: 0px;
property <bool> dragging: false;
pointer-event(e) => {
if (e.kind == PointerEventKind.down
&& e.button == PointerEventButton.left)
{
self.grab-y = self.mouse-y;
self.start-pos = root.position;
self.dragging = true;
}
if (e.kind == PointerEventKind.up) {
self.dragging = false;
}
}
moved => {
if (self.dragging) {
root.position = clamp(
self.start-pos + self.mouse-y - self.grab-y,
root.bound-min,
max(root.bound-min, root.bound-max),
);
root.dragged();
}
}
}
}
// ═══════════════════════════════════════════════════════════════════
// NeonSplitView — backwards-compatible single-slot wrapper
// ═══════════════════════════════════════════════════════════════════
//
// Kept so existing call sites that import `NeonSplitView` keep
// compiling. The `@children` slot is the LEADING panel; the
// trailing area is left empty for the host to fill via siblings
// outside this component, or just unused. NEW code should prefer
// composing manually with NeonSplitterHandle (see the file
// header pattern).
//
// ═══════════════════════════════════════════════════════════════════
export component NeonSplitView inherits Rectangle {
in-out property <length> split-position: 240px;
in property <length> min-leading: 80px;
in property <length> min-trailing: 80px;
in property <length> divider-thickness: 4px;
in property <color> divider-color: Theme.border;
callback split-changed;
HorizontalLayout {
spacing: 0px;
Rectangle {
width: clamp(
root.split-position,
root.min-leading,
max(root.min-leading,
root.width - root.divider-thickness - root.min-trailing),
);
clip: true;
VerticalLayout {
padding: 0px;
spacing: 0px;
@children
}
}
NeonSplitterHandle {
position <=> root.split-position;
bound-min: root.min-leading;
bound-max: max(root.min-leading,
root.width - root.divider-thickness - root.min-trailing);
thickness: root.divider-thickness;
idle-color: root.divider-color;
dragged => { root.split-changed(); }
}
Rectangle {
horizontal-stretch: 1;
clip: true;
}
}
}