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}