Skip to main content

sequoia_view/
split_panel.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Constraints, Edges, Length, Rect, Size};
5use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
6use sequoia_layout::{Axis, Indents, LayoutCtx, LayoutStyle};
7use sequoia_reactive::Signal;
8use sequoia_style::{BoxStyle, SplitPanelStyle, VisualState};
9
10use crate::any::AnyView;
11use crate::runtime::{ElementId, Runtime};
12use crate::view::{EventCtx, PaintCtx, View};
13use crate::views::Unit;
14
15const DEFAULT_HANDLE: f32 = 7.0;
16
17const MIN_HANDLE: f32 = 3.0;
18
19const LINE_IDLE: f32 = 1.0;
20
21const LINE_ACTIVE: f32 = 2.0;
22
23const GRAB_PAD: f32 = 6.0;
24
25const IDLE_COLOR: Color = Color::rgb8(0x75, 0x75, 0x75);
26
27const ACTIVE_COLOR: Color = Color::rgb8(0x0D, 0x99, 0xFF);
28
29#[derive(Clone, Copy)]
30struct SplitDrag {
31    origin: f32,
32    fraction: f32,
33}
34
35fn split_lengths(
36    total: f32,
37    handle: f32,
38    fraction: f32,
39    min_first: f32,
40    min_second: f32,
41) -> (f32, f32, f32) {
42    let frac = if fraction.is_finite() {
43        fraction.clamp(0.0, 1.0)
44    } else {
45        0.5
46    };
47    let hw = if handle.is_finite() {
48        handle.max(0.0)
49    } else {
50        DEFAULT_HANDLE
51    };
52    let avail = (total - hw).max(0.0);
53    let max_first = (avail - min_second.max(0.0)).max(0.0);
54    let first = (avail * frac).clamp(min_first.max(0.0).min(max_first), max_first);
55    let second = (avail - first).max(0.0);
56    (first, first, second)
57}
58
59#[derive(Clone)]
60pub struct SplitPanel {
61    fraction: Signal<f32>,
62    first: Option<AnyView>,
63    second: Option<AnyView>,
64    axis: Axis,
65    handle_width: Option<f32>,
66    min_first: f32,
67    min_second: f32,
68    handle: Option<Color>,
69    handle_active: Option<Color>,
70    style: Option<SplitPanelStyle>,
71    layout: LayoutStyle,
72    deco: BoxStyle,
73    drag: Rc<Cell<Option<SplitDrag>>>,
74}
75
76impl SplitPanel {
77    pub fn new(fraction: Signal<f32>) -> Self {
78        Self {
79            fraction,
80            first: None,
81            second: None,
82            axis: Axis::Horizontal,
83            handle_width: None,
84            min_first: 0.0,
85            min_second: 0.0,
86            handle: None,
87            handle_active: None,
88            style: None,
89            layout: LayoutStyle {
90                width: Length::Fill(1.0),
91                height: Length::Fill(1.0),
92                ..LayoutStyle::default()
93            },
94            deco: BoxStyle::new(),
95            drag: Rc::new(Cell::new(None)),
96        }
97    }
98
99    pub fn first<V: View + 'static>(mut self, child: V) -> Self {
100        self.first = Some(AnyView::new(child));
101        self
102    }
103
104    pub fn second<V: View + 'static>(mut self, child: V) -> Self {
105        self.second = Some(AnyView::new(child));
106        self
107    }
108
109    pub fn axis(mut self, axis: Axis) -> Self {
110        self.axis = axis;
111        self
112    }
113
114    pub fn horizontal(self) -> Self {
115        self.axis(Axis::Horizontal)
116    }
117
118    pub fn vertical(self) -> Self {
119        self.axis(Axis::Vertical)
120    }
121
122    pub fn handle_width(mut self, width: f32) -> Self {
123        self.handle_width = Some(if width.is_finite() {
124            width.max(MIN_HANDLE)
125        } else {
126            DEFAULT_HANDLE
127        });
128        self
129    }
130
131    pub fn min_first(mut self, min: f32) -> Self {
132        self.min_first = if min.is_finite() { min.max(0.0) } else { 0.0 };
133        self
134    }
135
136    pub fn min_second(mut self, min: f32) -> Self {
137        self.min_second = if min.is_finite() { min.max(0.0) } else { 0.0 };
138        self
139    }
140
141    pub fn min_pane(mut self, min: f32) -> Self {
142        let min = if min.is_finite() { min.max(0.0) } else { 0.0 };
143        self.min_first = min;
144        self.min_second = min;
145        self
146    }
147
148    pub fn handle_color(mut self, color: Color) -> Self {
149        self.handle = Some(color);
150        self
151    }
152
153    pub fn handle_active_color(mut self, color: Color) -> Self {
154        self.handle_active = Some(color);
155        self
156    }
157
158    pub fn style(mut self, style: SplitPanelStyle) -> Self {
159        self.style = Some(style);
160        self
161    }
162
163    pub fn fraction_value(&self) -> f32 {
164        let f = self.fraction.get_untracked();
165        if f.is_finite() {
166            f.clamp(0.0, 1.0)
167        } else {
168            0.5
169        }
170    }
171
172    fn resolved_handle_width(&self) -> f32 {
173        let style = self.style.unwrap_or_default();
174        self.handle_width
175            .or(style.handle_width)
176            .unwrap_or(DEFAULT_HANDLE)
177    }
178
179    fn resolved_handle(&self) -> Color {
180        let style = self.style.unwrap_or_default();
181        self.handle.or(style.handle).unwrap_or(IDLE_COLOR)
182    }
183
184    fn resolved_handle_active(&self) -> Color {
185        let style = self.style.unwrap_or_default();
186        self.handle_active
187            .or(style.handle_active)
188            .unwrap_or(ACTIVE_COLOR)
189    }
190
191    fn axis_pos(&self, x: f32, y: f32) -> f32 {
192        match self.axis {
193            Axis::Horizontal => x,
194            Axis::Vertical => y,
195        }
196    }
197
198    fn content_rect(&self, rect: Rect) -> Rect {
199        let pad = self.layout.padding;
200        Rect::from_xywh(
201            rect.left() + pad.left,
202            rect.top() + pad.top,
203            (rect.size.width - pad.horizontal_sum()).max(0.0),
204            (rect.size.height - pad.vertical_sum()).max(0.0),
205        )
206    }
207
208    fn handle_range(&self, content: Rect) -> (f32, f32) {
209        let total = match self.axis {
210            Axis::Horizontal => content.size.width,
211            Axis::Vertical => content.size.height,
212        };
213        let (_, off, _) = split_lengths(
214            total,
215            self.resolved_handle_width(),
216            self.fraction_value(),
217            self.min_first,
218            self.min_second,
219        );
220        let start = match self.axis {
221            Axis::Horizontal => content.left(),
222            Axis::Vertical => content.top(),
223        };
224        (start + off, start + off + self.resolved_handle_width())
225    }
226
227    pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
228        self.layout = self.layout.padding(padding);
229        self
230    }
231
232    pub fn pad(self, padding: impl Into<Edges>) -> Self {
233        self.padding(padding)
234    }
235
236    pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
237        self.layout = self.layout.padding_xy(x, y);
238        self
239    }
240
241    pub fn padding_x(mut self, x: f32) -> Self {
242        self.layout = self.layout.padding_x(x);
243        self
244    }
245
246    pub fn padding_y(mut self, y: f32) -> Self {
247        self.layout = self.layout.padding_y(y);
248        self
249    }
250
251    pub fn padding_left(mut self, v: f32) -> Self {
252        self.layout = self.layout.padding_left(v);
253        self
254    }
255
256    pub fn padding_right(mut self, v: f32) -> Self {
257        self.layout = self.layout.padding_right(v);
258        self
259    }
260
261    pub fn padding_top(mut self, v: f32) -> Self {
262        self.layout = self.layout.padding_top(v);
263        self
264    }
265
266    pub fn padding_bottom(mut self, v: f32) -> Self {
267        self.layout = self.layout.padding_bottom(v);
268        self
269    }
270
271    pub fn width(mut self, width: impl Into<Length>) -> Self {
272        self.layout = self.layout.width(width.into());
273        self
274    }
275
276    pub fn height(mut self, height: impl Into<Length>) -> Self {
277        self.layout = self.layout.height(height.into());
278        self
279    }
280
281    pub fn size(mut self, width: impl Into<Length>, height: impl Into<Length>) -> Self {
282        self.layout = self.layout.width(width.into()).height(height.into());
283        self
284    }
285
286    pub fn min_width(mut self, v: f32) -> Self {
287        self.layout = self.layout.min_width(v);
288        self
289    }
290
291    pub fn max_width(mut self, v: f32) -> Self {
292        self.layout = self.layout.max_width(v);
293        self
294    }
295
296    pub fn min_height(mut self, v: f32) -> Self {
297        self.layout = self.layout.min_height(v);
298        self
299    }
300
301    pub fn max_height(mut self, v: f32) -> Self {
302        self.layout = self.layout.max_height(v);
303        self
304    }
305
306    pub fn overflow(mut self, overflow: sequoia_layout::Overflow) -> Self {
307        self.layout = self.layout.overflow(overflow);
308        self
309    }
310
311    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
312        self.layout = self.layout.margin(margin);
313        self
314    }
315
316    pub fn margin_xy(mut self, x: f32, y: f32) -> Self {
317        self.layout = self.layout.margin_xy(x, y);
318        self
319    }
320
321    pub fn margin_x(mut self, x: f32) -> Self {
322        self.layout = self.layout.margin_x(x);
323        self
324    }
325
326    pub fn margin_y(mut self, y: f32) -> Self {
327        self.layout = self.layout.margin_y(y);
328        self
329    }
330
331    pub fn margin_left(mut self, v: f32) -> Self {
332        self.layout = self.layout.margin_left(v);
333        self
334    }
335
336    pub fn margin_right(mut self, v: f32) -> Self {
337        self.layout = self.layout.margin_right(v);
338        self
339    }
340
341    pub fn margin_top(mut self, v: f32) -> Self {
342        self.layout = self.layout.margin_top(v);
343        self
344    }
345
346    pub fn margin_bottom(mut self, v: f32) -> Self {
347        self.layout = self.layout.margin_bottom(v);
348        self
349    }
350
351    pub fn indent_top(mut self, v: f32) -> Self {
352        self.layout = self.layout.indent_top(v);
353        self
354    }
355
356    pub fn indent_bottom(mut self, v: f32) -> Self {
357        self.layout = self.layout.indent_bottom(v);
358        self
359    }
360
361    pub fn indent_left(mut self, v: f32) -> Self {
362        self.layout = self.layout.indent_left(v);
363        self
364    }
365
366    pub fn indent_right(mut self, v: f32) -> Self {
367        self.layout = self.layout.indent_right(v);
368        self
369    }
370
371    pub fn clear_indents(mut self) -> Self {
372        self.layout = self.layout.clear_indents();
373        self
374    }
375
376    pub fn indent(mut self, indents: Indents) -> Self {
377        self.layout = self.layout.indent(indents);
378        self
379    }
380
381    pub fn indent_all(mut self, v: f32) -> Self {
382        self.layout = self.layout.indent_all(v);
383        self
384    }
385
386    pub fn indent_xy(mut self, x: f32, y: f32) -> Self {
387        self.layout = self.layout.indent_xy(x, y);
388        self
389    }
390
391    pub fn indent_axes(mut self, horizontal: f32, vertical: f32) -> Self {
392        self.layout = self.layout.indent_axes(horizontal, vertical);
393        self
394    }
395
396    pub fn flex(mut self, weight: f32) -> Self {
397        self.layout = self.layout.flex(weight);
398        self
399    }
400
401    pub fn flex_shrink(mut self, shrink: f32) -> Self {
402        self.layout = self.layout.flex_shrink(shrink);
403        self
404    }
405
406    pub fn flex_basis(mut self, basis: Option<Length>) -> Self {
407        self.layout = self.layout.flex_basis(basis);
408        self
409    }
410
411    pub fn align_self(mut self, align: Option<sequoia_layout::Align>) -> Self {
412        self.layout = self.layout.align_self(align);
413        self
414    }
415
416    pub fn expand(self) -> Self {
417        self.flex(1.0)
418    }
419
420    pub fn fill_width(mut self) -> Self {
421        self.layout = self.layout.width(Length::Fill(1.0));
422        self
423    }
424
425    pub fn fill_height(mut self) -> Self {
426        self.layout = self.layout.height(Length::Fill(1.0));
427        self
428    }
429
430    pub fn fill_size(self) -> Self {
431        self.fill_width().fill_height()
432    }
433
434    pub fn fit(mut self) -> Self {
435        self.layout = self.layout.fit();
436        self
437    }
438
439    pub fn hit_testable(mut self, on: bool) -> Self {
440        self.layout = self.layout.hit_testable(on);
441        self
442    }
443
444    pub fn position(mut self, position: sequoia_layout::Position) -> Self {
445        self.layout = self.layout.position(position);
446        self
447    }
448
449    pub fn absolute(mut self) -> Self {
450        self.layout = self.layout.absolute();
451        self
452    }
453
454    pub fn background(mut self, color: Color) -> Self {
455        self.deco = self.deco.background(color);
456        self
457    }
458
459    pub fn border(mut self, width: f32, color: Color) -> Self {
460        self.deco = self.deco.border(width, color);
461        self
462    }
463
464    pub fn radius(mut self, radius: f32) -> Self {
465        self.deco = self.deco.radius(radius);
466        self
467    }
468
469    pub fn box_style(mut self, style: BoxStyle) -> Self {
470        self.deco = style;
471        self
472    }
473}
474
475impl View for SplitPanel {
476    fn body(&self) -> impl View + 'static {
477        Unit
478    }
479    fn children_override(&self) -> Option<Vec<AnyView>> {
480        let _ = self.fraction.get();
481        let mut out = Vec::with_capacity(2);
482        if let Some(f) = &self.first {
483            out.push(f.clone());
484        }
485        if let Some(s) = &self.second {
486            out.push(s.clone());
487        }
488        Some(out)
489    }
490    fn layout_style(&self) -> LayoutStyle {
491        self.layout
492    }
493    fn captures_pointer(&self) -> bool {
494        false
495    }
496    fn has_visual_state(&self) -> bool {
497        true
498    }
499    fn cursor_icon(&self, state: VisualState) -> CursorIcon {
500        if self.drag.get().is_some() || state.pressed || state.hovered {
501            match self.axis {
502                Axis::Horizontal => CursorIcon::ColResize,
503                Axis::Vertical => CursorIcon::RowResize,
504            }
505        } else {
506            CursorIcon::Default
507        }
508    }
509    fn arrange_override(
510        &self,
511        rt: &Runtime,
512        el: ElementId,
513        rect: Rect,
514        ctx: &mut LayoutCtx,
515    ) -> bool {
516        let kids = rt.children(el);
517        if kids.is_empty() {
518            return true;
519        }
520        let content = self.content_rect(rect);
521        let hw = self.resolved_handle_width();
522        let (total_main, cross) = match self.axis {
523            Axis::Horizontal => (content.size.width, content.size.height),
524            Axis::Vertical => (content.size.height, content.size.width),
525        };
526        let has_first = self.first.is_some();
527        let has_second = self.second.is_some();
528        let mut slots: Vec<(ElementId, Rect)> = Vec::with_capacity(2);
529        if has_first && has_second && kids.len() >= 2 {
530            let (a, off, b) = split_lengths(
531                total_main,
532                hw,
533                self.fraction_value(),
534                self.min_first,
535                self.min_second,
536            );
537            let (ra, rb) = match self.axis {
538                Axis::Horizontal => (
539                    Rect::from_xywh(content.left(), content.top(), a, cross),
540                    Rect::from_xywh(content.left() + off + hw, content.top(), b, cross),
541                ),
542                Axis::Vertical => (
543                    Rect::from_xywh(content.left(), content.top(), cross, a),
544                    Rect::from_xywh(content.left(), content.top() + off + hw, cross, b),
545                ),
546            };
547            slots.push((kids[0], ra));
548            slots.push((kids[1], rb));
549        } else {
550            let whole = match self.axis {
551                Axis::Horizontal => {
552                    Rect::from_xywh(content.left(), content.top(), total_main, cross)
553                }
554                Axis::Vertical => Rect::from_xywh(content.left(), content.top(), cross, total_main),
555            };
556            slots.push((kids[0], whole));
557        }
558        for (child, slot) in slots {
559            let max = Size::new(slot.size.width.max(0.0), slot.size.height.max(0.0));
560            let _ = rt.measure_node(
561                child,
562                Constraints {
563                    min: Size::ZERO,
564                    max,
565                },
566                ctx,
567            );
568            rt.arrange_node(child, slot, ctx);
569        }
570        true
571    }
572    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
573        if ctx.phase != EventPhase::Target {
574            return;
575        }
576        match event {
577            Event::Pointer(PointerEvent::Down {
578                button: PointerButton::Left,
579                pos,
580                ..
581            }) => {
582                let content = self.content_rect(ctx.rect);
583                let (lo, hi) = self.handle_range(content);
584                let p = self.axis_pos(pos.x, pos.y);
585
586                if p >= lo - GRAB_PAD && p <= hi + GRAB_PAD {
587                    self.drag.set(Some(SplitDrag {
588                        origin: p,
589                        fraction: self.fraction_value(),
590                    }));
591                    ctx.capture_pointer();
592                    ctx.handled();
593                }
594            }
595            Event::Pointer(PointerEvent::Move { pos, .. }) => {
596                if let Some(st) = self.drag.get() {
597                    let content = self.content_rect(ctx.rect);
598                    let total = match self.axis {
599                        Axis::Horizontal => content.size.width,
600                        Axis::Vertical => content.size.height,
601                    };
602                    let avail = (total - self.resolved_handle_width()).max(0.0);
603                    if avail > 0.0 {
604                        let p = self.axis_pos(pos.x, pos.y);
605                        let next = st.fraction + (p - st.origin) / avail;
606                        self.fraction.set(next.clamp(0.0, 1.0));
607                    }
608                    ctx.handled();
609                }
610            }
611            Event::Pointer(PointerEvent::Up {
612                button: PointerButton::Left,
613                ..
614            }) => {
615                self.drag.set(None);
616            }
617            Event::Pointer(PointerEvent::Cancel) => {
618                self.drag.set(None);
619            }
620            _ => {}
621        }
622    }
623    fn paint_self(&self, ctx: &mut PaintCtx) {
624        ctx.paint_box(self.deco);
625        let content = self.content_rect(ctx.rect);
626        if !self.first.is_some() || !self.second.is_some() {
627            return;
628        }
629        let (lo, _) = self.handle_range(content);
630        let hw = self.resolved_handle_width();
631        if hw <= 0.0 {
632            return;
633        }
634        let active = self.drag.get().is_some() || ctx.state.pressed || ctx.state.hovered;
635        let (w, c) = if active {
636            (LINE_ACTIVE, self.resolved_handle_active())
637        } else {
638            (LINE_IDLE, self.resolved_handle())
639        };
640        match self.axis {
641            Axis::Horizontal => {
642                let cx = lo + hw * 0.5;
643                ctx.quad(
644                    Rect::from_xywh(cx - w * 0.5, content.top(), w, content.size.height),
645                    c,
646                    0.0,
647                );
648            }
649            Axis::Vertical => {
650                let cy = lo + hw * 0.5;
651                ctx.quad(
652                    Rect::from_xywh(content.left(), cy - w * 0.5, content.size.width, w),
653                    c,
654                    0.0,
655                );
656            }
657        }
658    }
659}
660
661#[cfg(test)]
662mod tests {
663    use super::*;
664
665    #[test]
666    fn even_split_without_minimums() {
667        let (a, off, b) = split_lengths(400.0, 8.0, 0.5, 0.0, 0.0);
668        assert_eq!((a, off, b), (196.0, 196.0, 196.0));
669    }
670
671    #[test]
672    fn fraction_moves_divider() {
673        let (a, off, b) = split_lengths(400.0, 8.0, 0.25, 0.0, 0.0);
674        assert_eq!((a, off, b), (98.0, 98.0, 294.0));
675    }
676
677    #[test]
678    fn minimums_clamp_without_panic() {
679        let (a, _, b) = split_lengths(400.0, 8.0, 0.0, 100.0, 0.0);
680        assert_eq!((a, b), (100.0, 292.0));
681
682        let (a, _, b) = split_lengths(100.0, 0.0, 0.5, 80.0, 80.0);
683        assert_eq!((a, b), (20.0, 80.0));
684    }
685
686    #[test]
687    fn degenerate_inputs_collapse() {
688        assert_eq!(split_lengths(4.0, 8.0, 0.5, 0.0, 0.0), (0.0, 0.0, 0.0));
689        assert_eq!(split_lengths(400.0, 8.0, f32::NAN, 0.0, 0.0).0, 196.0);
690        assert_eq!(split_lengths(0.0, 8.0, 0.5, 0.0, 0.0), (0.0, 0.0, 0.0));
691    }
692
693    #[test]
694    fn fraction_value_sanitizes() {
695        let s = SplitPanel::new(Signal::new(2.0));
696        assert_eq!(s.fraction_value(), 1.0);
697        let s = SplitPanel::new(Signal::new(f32::NAN));
698        assert_eq!(s.fraction_value(), 0.5);
699    }
700}