1use std::cell::RefCell;
34use std::rc::Rc;
35
36use serde::{Deserialize, Serialize};
37use teksilo_core::signal::Signal;
38use teksilo_settings::Versioned;
39use teksilo_tokens::Orientation;
40
41pub const SPLITTER_GUTTER_THICKNESS: f32 = 6.0;
44pub const SPLITTER_MIN_PANE_SIZE: f32 = 96.0;
46pub const SPLITTER_KEYBOARD_STEP: f32 = 24.0;
48pub const SPLITTER_SNAP_OFFSET: f32 = 30.0;
50
51#[derive(Debug, Clone)]
60pub struct PaneDescriptor {
61 pub initial_size: Option<f32>,
64 pub min_size: f32,
66 pub max_size: Option<f32>,
68 pub stretch: f32,
71 pub collapsible: bool,
76 pub collapsed: bool,
78 pub collapsed_size: f32,
84 pub visible: bool,
90}
91
92impl Default for PaneDescriptor {
93 fn default() -> Self {
94 Self {
95 initial_size: None,
96 min_size: SPLITTER_MIN_PANE_SIZE,
97 max_size: None,
98 stretch: 1.0,
99 collapsible: false,
100 collapsed: false,
101 collapsed_size: 0.0,
102 visible: true,
103 }
104 }
105}
106
107impl PaneDescriptor {
108 pub fn new() -> Self {
109 Self::default()
110 }
111 pub fn size(mut self, size: f32) -> Self {
112 self.initial_size = Some(size);
113 self
114 }
115 pub fn min_size(mut self, min: f32) -> Self {
116 self.min_size = min;
117 self
118 }
119 pub fn max_size(mut self, max: f32) -> Self {
120 self.max_size = Some(max);
121 self
122 }
123 pub fn stretch(mut self, stretch: f32) -> Self {
124 self.stretch = stretch;
125 self
126 }
127 pub fn collapsible(mut self, collapsible: bool) -> Self {
128 self.collapsible = collapsible;
129 self
130 }
131 pub fn collapsed(mut self, collapsed: bool) -> Self {
132 self.collapsed = collapsed;
133 self
134 }
135 pub fn collapsed_size(mut self, px: f32) -> Self {
138 self.collapsed_size = px.max(0.0);
139 self
140 }
141 pub fn visible(mut self, visible: bool) -> Self {
142 self.visible = visible;
143 self
144 }
145}
146
147#[derive(Debug, Clone)]
152struct PaneEntry {
153 stored_size: f32,
154 min_size: f32,
155 max_size: Option<f32>,
156 stretch: f32,
157 collapsible: bool,
158 collapsed: bool,
159 collapsed_size: f32,
160 visible: bool,
161}
162
163impl PaneEntry {
164 fn from_descriptor(d: &PaneDescriptor, fallback_size: f32) -> Self {
165 let min = d.min_size.max(0.0);
166 let max = d.max_size.map(|m| m.max(min));
169 let stored = d.initial_size.unwrap_or(fallback_size).max(0.0);
170 Self {
171 stored_size: stored,
172 min_size: min,
173 max_size: max,
174 stretch: d.stretch.max(0.0),
175 collapsible: d.collapsible,
176 collapsed: d.collapsed,
177 collapsed_size: d.collapsed_size.max(0.0),
178 visible: d.visible,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq)]
186pub struct PaneSnapshot {
187 pub stored_size: f32,
188 pub min_size: f32,
189 pub max_size: Option<f32>,
190 pub stretch: f32,
191 pub collapsed: bool,
192 pub collapsed_size: f32,
193 pub visible: bool,
194}
195
196#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
205pub struct PaneState {
206 pub stored_size: f32,
207 pub collapsed: bool,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216pub struct SplitterState {
217 #[serde(default = "default_version")]
218 pub version: u32,
219 #[serde(default)]
220 pub panes: Vec<PaneState>,
221}
222
223fn default_version() -> u32 {
224 SplitterState::CURRENT_VERSION
225}
226
227impl Default for SplitterState {
228 fn default() -> Self {
229 Self {
230 version: SplitterState::CURRENT_VERSION,
231 panes: Vec::new(),
232 }
233 }
234}
235
236impl Versioned for SplitterState {
237 const CURRENT_VERSION: u32 = 1;
238 fn version(&self) -> u32 {
239 self.version
240 }
241 fn set_version(&mut self, v: u32) {
242 self.version = v;
243 }
244}
245
246struct SplitterModelInner {
251 panes: Vec<PaneEntry>,
252 orientation: Orientation,
253 gutter_thickness: f32,
254 keyboard_step_px: f32,
255 snap_offset: f32,
256 version: Signal<u64>,
257 animate_next_collapse: bool,
261}
262
263pub struct SplitterModel(Rc<RefCell<SplitterModelInner>>);
266
267impl Clone for SplitterModel {
268 fn clone(&self) -> Self {
269 Self(self.0.clone())
270 }
271}
272
273impl std::fmt::Debug for SplitterModel {
274 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
275 match self.0.try_borrow() {
276 Ok(inner) => f
277 .debug_struct("SplitterModel")
278 .field("handles", &Rc::strong_count(&self.0))
279 .field("panes", &inner.panes.len())
280 .field("orientation", &inner.orientation)
281 .finish(),
282 Err(_) => f
283 .debug_struct("SplitterModel")
284 .field("handles", &Rc::strong_count(&self.0))
285 .field("panes", &"<borrowed>")
286 .finish(),
287 }
288 }
289}
290
291impl SplitterModel {
292 pub fn new(n: usize, orientation: Orientation) -> Self {
296 let panes = (0..n)
297 .map(|_| PaneEntry::from_descriptor(&PaneDescriptor::default(), 0.0))
298 .collect();
299 Self::from_inner(panes, orientation)
300 }
301
302 pub fn from_panes(panes: Vec<PaneDescriptor>, orientation: Orientation) -> Self {
304 let entries = panes
305 .iter()
306 .map(|d| PaneEntry::from_descriptor(d, d.initial_size.unwrap_or(0.0)))
307 .collect();
308 Self::from_inner(entries, orientation)
309 }
310
311 fn from_inner(panes: Vec<PaneEntry>, orientation: Orientation) -> Self {
312 Self(Rc::new(RefCell::new(SplitterModelInner {
313 panes,
314 orientation,
315 gutter_thickness: SPLITTER_GUTTER_THICKNESS,
316 keyboard_step_px: SPLITTER_KEYBOARD_STEP,
317 snap_offset: SPLITTER_SNAP_OFFSET,
318 version: Signal::new(0),
319 animate_next_collapse: true,
320 })))
321 }
322
323 pub fn handle_count(&self) -> usize {
325 Rc::strong_count(&self.0)
326 }
327
328 fn bump_version(&self) {
331 let version = self.0.borrow().version.clone();
334 version.set(version.get().wrapping_add(1));
335 }
336
337 pub fn set_stored_size(&self, index: usize, size: f32) {
340 {
341 let mut inner = self.0.borrow_mut();
342 let Some(p) = inner.panes.get_mut(index) else {
343 return;
344 };
345 p.stored_size = size.max(0.0);
346 }
347 self.bump_version();
348 }
349
350 pub fn set_stored_size_silent(&self, index: usize, size: f32) {
355 let mut inner = self.0.borrow_mut();
356 if let Some(p) = inner.panes.get_mut(index) {
357 p.stored_size = size.max(0.0);
358 }
359 }
360
361 pub fn set_pair_sizes(&self, index: usize, size_a: f32, size_b: f32) {
365 {
366 let mut inner = self.0.borrow_mut();
367 if index + 1 >= inner.panes.len() {
368 return;
369 }
370 inner.panes[index].stored_size = size_a.max(0.0);
371 inner.panes[index + 1].stored_size = size_b.max(0.0);
372 }
373 self.bump_version();
374 }
375
376 pub fn set_min_size(&self, index: usize, min: f32) {
377 {
378 let mut inner = self.0.borrow_mut();
379 let Some(p) = inner.panes.get_mut(index) else {
380 return;
381 };
382 p.min_size = min.max(0.0);
383 if let Some(m) = p.max_size {
385 p.max_size = Some(m.max(p.min_size));
386 }
387 }
388 self.bump_version();
389 }
390
391 pub fn set_max_size(&self, index: usize, max: Option<f32>) {
392 {
393 let mut inner = self.0.borrow_mut();
394 let Some(p) = inner.panes.get_mut(index) else {
395 return;
396 };
397 p.max_size = max.map(|m| m.max(p.min_size));
398 }
399 self.bump_version();
400 }
401
402 pub fn set_stretch(&self, index: usize, stretch: f32) {
403 {
404 let mut inner = self.0.borrow_mut();
405 let Some(p) = inner.panes.get_mut(index) else {
406 return;
407 };
408 p.stretch = stretch.max(0.0);
409 }
410 self.bump_version();
411 }
412
413 pub fn set_collapsible(&self, index: usize, collapsible: bool) {
414 {
415 let mut inner = self.0.borrow_mut();
416 let Some(p) = inner.panes.get_mut(index) else {
417 return;
418 };
419 p.collapsible = collapsible;
420 }
421 self.bump_version();
422 }
423
424 pub fn set_collapsed(&self, index: usize, collapsed: bool) {
429 self.set_collapsed_inner(index, collapsed, true);
430 }
431
432 pub fn set_collapsed_immediate(&self, index: usize, collapsed: bool) {
435 self.set_collapsed_inner(index, collapsed, false);
436 }
437
438 pub fn toggle_collapsed(&self, index: usize) {
440 let current = self.is_collapsed(index);
441 self.set_collapsed(index, !current);
442 }
443
444 pub fn set_collapsed_size(&self, index: usize, px: f32) {
448 let mut inner = self.0.borrow_mut();
449 if let Some(p) = inner.panes.get_mut(index) {
450 p.collapsed_size = px.max(0.0);
451 }
452 }
453
454 fn set_collapsed_inner(&self, index: usize, collapsed: bool, animate: bool) {
455 {
456 let mut inner = self.0.borrow_mut();
457 let Some(p) = inner.panes.get_mut(index) else {
458 return;
459 };
460 if p.collapsed == collapsed {
461 return; }
463 p.collapsed = collapsed;
464 inner.animate_next_collapse = animate;
465 }
466 self.bump_version();
467 }
468
469 pub fn set_pane_visible(&self, index: usize, visible: bool) {
475 self.set_pane_visible_inner(index, visible, true);
476 }
477
478 pub fn set_pane_visible_immediate(&self, index: usize, visible: bool) {
483 self.set_pane_visible_inner(index, visible, false);
484 }
485
486 fn set_pane_visible_inner(&self, index: usize, visible: bool, animate: bool) {
487 {
488 let mut inner = self.0.borrow_mut();
489 let Some(p) = inner.panes.get_mut(index) else {
490 return;
491 };
492 if p.visible == visible {
493 return;
494 }
495 p.visible = visible;
496 inner.animate_next_collapse = animate;
497 }
498 self.bump_version();
499 }
500
501 pub fn is_pane_visible(&self, index: usize) -> bool {
502 self.0
503 .borrow()
504 .panes
505 .get(index)
506 .map(|p| p.visible)
507 .unwrap_or(false)
508 }
509
510 pub fn consume_animate_flag(&self) -> bool {
514 let mut inner = self.0.borrow_mut();
515 let f = inner.animate_next_collapse;
516 inner.animate_next_collapse = true;
517 f
518 }
519
520 pub fn insert_pane(&self, index: usize, desc: PaneDescriptor) {
529 {
530 let mut inner = self.0.borrow_mut();
531 let idx = index.min(inner.panes.len());
532 let fallback = if inner.panes.is_empty() {
533 SPLITTER_MIN_PANE_SIZE
534 } else {
535 inner.panes.iter().map(|p| p.stored_size).sum::<f32>() / inner.panes.len() as f32
536 };
537 inner
538 .panes
539 .insert(idx, PaneEntry::from_descriptor(&desc, fallback));
540 }
541 self.bump_version();
542 }
543
544 pub fn remove_pane(&self, index: usize) {
547 {
548 let mut inner = self.0.borrow_mut();
549 if index >= inner.panes.len() {
550 return;
551 }
552 inner.panes.remove(index);
553 }
554 self.bump_version();
555 }
556
557 pub fn replace_pane_desc(&self, index: usize, desc: PaneDescriptor) {
560 {
561 let mut inner = self.0.borrow_mut();
562 let Some(p) = inner.panes.get_mut(index) else {
563 return;
564 };
565 let fallback = p.stored_size;
566 *p = PaneEntry::from_descriptor(&desc, fallback);
567 }
568 self.bump_version();
569 }
570
571 pub fn set_gutter_thickness(&self, thickness: f32) {
574 {
575 self.0.borrow_mut().gutter_thickness = thickness.max(1.0);
576 }
577 self.bump_version();
578 }
579
580 pub fn set_snap_offset(&self, offset: f32) {
581 {
582 self.0.borrow_mut().snap_offset = offset.max(0.0);
583 }
584 self.bump_version();
585 }
586
587 pub fn set_keyboard_step_px(&self, step: f32) {
588 {
589 self.0.borrow_mut().keyboard_step_px = step.max(1.0);
590 }
591 self.bump_version();
592 }
593
594 pub fn set_orientation(&self, orientation: Orientation) {
595 {
596 self.0.borrow_mut().orientation = orientation;
597 }
598 self.bump_version();
599 }
600
601 pub fn pane_count(&self) -> usize {
604 self.0.borrow().panes.len()
605 }
606 pub fn stored_size(&self, index: usize) -> f32 {
607 self.0
608 .borrow()
609 .panes
610 .get(index)
611 .map(|p| p.stored_size)
612 .unwrap_or(0.0)
613 }
614 pub fn min_size(&self, index: usize) -> f32 {
615 self.0
616 .borrow()
617 .panes
618 .get(index)
619 .map(|p| p.min_size)
620 .unwrap_or(0.0)
621 }
622 pub fn max_size(&self, index: usize) -> Option<f32> {
623 self.0.borrow().panes.get(index).and_then(|p| p.max_size)
624 }
625 pub fn stretch(&self, index: usize) -> f32 {
626 self.0
627 .borrow()
628 .panes
629 .get(index)
630 .map(|p| p.stretch)
631 .unwrap_or(0.0)
632 }
633 pub fn is_collapsible(&self, index: usize) -> bool {
634 self.0
635 .borrow()
636 .panes
637 .get(index)
638 .map(|p| p.collapsible)
639 .unwrap_or(false)
640 }
641 pub fn collapsed_size(&self, index: usize) -> f32 {
644 self.0
645 .borrow()
646 .panes
647 .get(index)
648 .map(|p| p.collapsed_size)
649 .unwrap_or(0.0)
650 }
651 pub fn is_collapsed(&self, index: usize) -> bool {
652 self.0
653 .borrow()
654 .panes
655 .get(index)
656 .map(|p| p.collapsed)
657 .unwrap_or(false)
658 }
659 pub fn orientation(&self) -> Orientation {
660 self.0.borrow().orientation
661 }
662 pub fn gutter_thickness(&self) -> f32 {
663 self.0.borrow().gutter_thickness
664 }
665 pub fn snap_offset(&self) -> f32 {
666 self.0.borrow().snap_offset
667 }
668 pub fn keyboard_step_px(&self) -> f32 {
669 self.0.borrow().keyboard_step_px
670 }
671
672 pub fn version(&self) -> Signal<u64> {
675 self.0.borrow().version.clone()
676 }
677
678 pub fn pane_snapshots(&self) -> Vec<PaneSnapshot> {
680 self.0
681 .borrow()
682 .panes
683 .iter()
684 .map(|p| PaneSnapshot {
685 stored_size: p.stored_size,
686 min_size: p.min_size,
687 max_size: p.max_size,
688 stretch: p.stretch,
689 collapsed: p.collapsed,
690 collapsed_size: p.collapsed_size,
691 visible: p.visible,
692 })
693 .collect()
694 }
695
696 pub fn export_state(&self) -> SplitterState {
701 let inner = self.0.borrow();
702 SplitterState {
703 version: SplitterState::CURRENT_VERSION,
704 panes: inner
705 .panes
706 .iter()
707 .map(|p| PaneState {
708 stored_size: p.stored_size,
709 collapsed: p.collapsed,
710 })
711 .collect(),
712 }
713 }
714
715 pub fn import_state(&self, state: &SplitterState) -> bool {
720 let ok = {
721 let mut inner = self.0.borrow_mut();
722 if state.panes.len() != inner.panes.len() {
723 false
724 } else {
725 for (p, s) in inner.panes.iter_mut().zip(&state.panes) {
726 p.stored_size = s.stored_size.max(0.0);
727 p.collapsed = s.collapsed;
728 }
729 inner.animate_next_collapse = false;
730 true
731 }
732 };
733 if ok {
734 self.bump_version();
735 }
736 ok
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn clone_shares_state() {
746 let a = SplitterModel::new(3, Orientation::Horizontal);
747 let b = a.clone();
748 assert_eq!(b.pane_count(), 3);
749 a.set_stored_size(0, 200.0);
750 assert_eq!(b.stored_size(0), 200.0);
751 assert_eq!(a.handle_count(), 2);
752 }
753
754 #[test]
755 fn version_bumps_on_mutation() {
756 let m = SplitterModel::new(2, Orientation::Horizontal);
757 let v = m.version();
758 let v0 = v.get();
759 m.set_stored_size(0, 150.0);
760 assert_ne!(v.get(), v0);
761 let v1 = v.get();
763 m.set_collapsed(0, false); assert_eq!(v.get(), v1);
765 }
766
767 #[test]
768 fn export_import_round_trips() {
769 let m = SplitterModel::new(3, Orientation::Horizontal);
770 m.set_stored_size(0, 120.0);
771 m.set_stored_size(1, 340.0);
772 m.set_collapsed(2, true);
773 let state = m.export_state();
774
775 let restored = SplitterModel::new(3, Orientation::Horizontal);
776 assert!(restored.import_state(&state));
777 assert_eq!(restored.stored_size(0), 120.0);
778 assert_eq!(restored.stored_size(1), 340.0);
779 assert!(restored.is_collapsed(2));
780 }
781
782 #[test]
783 fn import_rejects_pane_count_mismatch() {
784 let m = SplitterModel::new(3, Orientation::Horizontal);
785 let state = m.export_state();
786 let two = SplitterModel::new(2, Orientation::Horizontal);
787 assert!(!two.import_state(&state));
788 }
789
790 #[test]
791 fn insert_remove_change_count() {
792 let m = SplitterModel::new(2, Orientation::Horizontal);
793 m.insert_pane(1, PaneDescriptor::new().size(100.0));
794 assert_eq!(m.pane_count(), 3);
795 assert_eq!(m.stored_size(1), 100.0);
796 m.remove_pane(0);
797 assert_eq!(m.pane_count(), 2);
798 }
799
800 #[test]
801 fn max_size_enforced_ge_min() {
802 let m = SplitterModel::from_panes(
803 vec![PaneDescriptor::new().min_size(200.0).max_size(100.0)],
804 Orientation::Horizontal,
805 );
806 assert_eq!(m.max_size(0), Some(200.0));
808 }
809
810 #[test]
811 fn animate_flag_consumed_and_resets() {
812 let m = SplitterModel::new(2, Orientation::Horizontal);
813 m.set_collapsed_immediate(0, true);
814 assert!(!m.consume_animate_flag()); assert!(m.consume_animate_flag());
817 }
818}