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 {
476 let mut inner = self.0.borrow_mut();
477 let Some(p) = inner.panes.get_mut(index) else {
478 return;
479 };
480 if p.visible == visible {
481 return;
482 }
483 p.visible = visible;
484 inner.animate_next_collapse = true;
485 }
486 self.bump_version();
487 }
488
489 pub fn is_pane_visible(&self, index: usize) -> bool {
490 self.0
491 .borrow()
492 .panes
493 .get(index)
494 .map(|p| p.visible)
495 .unwrap_or(false)
496 }
497
498 pub fn consume_animate_flag(&self) -> bool {
502 let mut inner = self.0.borrow_mut();
503 let f = inner.animate_next_collapse;
504 inner.animate_next_collapse = true;
505 f
506 }
507
508 pub fn insert_pane(&self, index: usize, desc: PaneDescriptor) {
517 {
518 let mut inner = self.0.borrow_mut();
519 let idx = index.min(inner.panes.len());
520 let fallback = if inner.panes.is_empty() {
521 SPLITTER_MIN_PANE_SIZE
522 } else {
523 inner.panes.iter().map(|p| p.stored_size).sum::<f32>() / inner.panes.len() as f32
524 };
525 inner
526 .panes
527 .insert(idx, PaneEntry::from_descriptor(&desc, fallback));
528 }
529 self.bump_version();
530 }
531
532 pub fn remove_pane(&self, index: usize) {
535 {
536 let mut inner = self.0.borrow_mut();
537 if index >= inner.panes.len() {
538 return;
539 }
540 inner.panes.remove(index);
541 }
542 self.bump_version();
543 }
544
545 pub fn replace_pane_desc(&self, index: usize, desc: PaneDescriptor) {
548 {
549 let mut inner = self.0.borrow_mut();
550 let Some(p) = inner.panes.get_mut(index) else {
551 return;
552 };
553 let fallback = p.stored_size;
554 *p = PaneEntry::from_descriptor(&desc, fallback);
555 }
556 self.bump_version();
557 }
558
559 pub fn set_gutter_thickness(&self, thickness: f32) {
562 {
563 self.0.borrow_mut().gutter_thickness = thickness.max(1.0);
564 }
565 self.bump_version();
566 }
567
568 pub fn set_snap_offset(&self, offset: f32) {
569 {
570 self.0.borrow_mut().snap_offset = offset.max(0.0);
571 }
572 self.bump_version();
573 }
574
575 pub fn set_keyboard_step_px(&self, step: f32) {
576 {
577 self.0.borrow_mut().keyboard_step_px = step.max(1.0);
578 }
579 self.bump_version();
580 }
581
582 pub fn set_orientation(&self, orientation: Orientation) {
583 {
584 self.0.borrow_mut().orientation = orientation;
585 }
586 self.bump_version();
587 }
588
589 pub fn pane_count(&self) -> usize {
592 self.0.borrow().panes.len()
593 }
594 pub fn stored_size(&self, index: usize) -> f32 {
595 self.0
596 .borrow()
597 .panes
598 .get(index)
599 .map(|p| p.stored_size)
600 .unwrap_or(0.0)
601 }
602 pub fn min_size(&self, index: usize) -> f32 {
603 self.0
604 .borrow()
605 .panes
606 .get(index)
607 .map(|p| p.min_size)
608 .unwrap_or(0.0)
609 }
610 pub fn max_size(&self, index: usize) -> Option<f32> {
611 self.0.borrow().panes.get(index).and_then(|p| p.max_size)
612 }
613 pub fn stretch(&self, index: usize) -> f32 {
614 self.0
615 .borrow()
616 .panes
617 .get(index)
618 .map(|p| p.stretch)
619 .unwrap_or(0.0)
620 }
621 pub fn is_collapsible(&self, index: usize) -> bool {
622 self.0
623 .borrow()
624 .panes
625 .get(index)
626 .map(|p| p.collapsible)
627 .unwrap_or(false)
628 }
629 pub fn collapsed_size(&self, index: usize) -> f32 {
632 self.0
633 .borrow()
634 .panes
635 .get(index)
636 .map(|p| p.collapsed_size)
637 .unwrap_or(0.0)
638 }
639 pub fn is_collapsed(&self, index: usize) -> bool {
640 self.0
641 .borrow()
642 .panes
643 .get(index)
644 .map(|p| p.collapsed)
645 .unwrap_or(false)
646 }
647 pub fn orientation(&self) -> Orientation {
648 self.0.borrow().orientation
649 }
650 pub fn gutter_thickness(&self) -> f32 {
651 self.0.borrow().gutter_thickness
652 }
653 pub fn snap_offset(&self) -> f32 {
654 self.0.borrow().snap_offset
655 }
656 pub fn keyboard_step_px(&self) -> f32 {
657 self.0.borrow().keyboard_step_px
658 }
659
660 pub fn version(&self) -> Signal<u64> {
663 self.0.borrow().version.clone()
664 }
665
666 pub fn pane_snapshots(&self) -> Vec<PaneSnapshot> {
668 self.0
669 .borrow()
670 .panes
671 .iter()
672 .map(|p| PaneSnapshot {
673 stored_size: p.stored_size,
674 min_size: p.min_size,
675 max_size: p.max_size,
676 stretch: p.stretch,
677 collapsed: p.collapsed,
678 collapsed_size: p.collapsed_size,
679 visible: p.visible,
680 })
681 .collect()
682 }
683
684 pub fn export_state(&self) -> SplitterState {
689 let inner = self.0.borrow();
690 SplitterState {
691 version: SplitterState::CURRENT_VERSION,
692 panes: inner
693 .panes
694 .iter()
695 .map(|p| PaneState {
696 stored_size: p.stored_size,
697 collapsed: p.collapsed,
698 })
699 .collect(),
700 }
701 }
702
703 pub fn import_state(&self, state: &SplitterState) -> bool {
708 let ok = {
709 let mut inner = self.0.borrow_mut();
710 if state.panes.len() != inner.panes.len() {
711 false
712 } else {
713 for (p, s) in inner.panes.iter_mut().zip(&state.panes) {
714 p.stored_size = s.stored_size.max(0.0);
715 p.collapsed = s.collapsed;
716 }
717 inner.animate_next_collapse = false;
718 true
719 }
720 };
721 if ok {
722 self.bump_version();
723 }
724 ok
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn clone_shares_state() {
734 let a = SplitterModel::new(3, Orientation::Horizontal);
735 let b = a.clone();
736 assert_eq!(b.pane_count(), 3);
737 a.set_stored_size(0, 200.0);
738 assert_eq!(b.stored_size(0), 200.0);
739 assert_eq!(a.handle_count(), 2);
740 }
741
742 #[test]
743 fn version_bumps_on_mutation() {
744 let m = SplitterModel::new(2, Orientation::Horizontal);
745 let v = m.version();
746 let v0 = v.get();
747 m.set_stored_size(0, 150.0);
748 assert_ne!(v.get(), v0);
749 let v1 = v.get();
751 m.set_collapsed(0, false); assert_eq!(v.get(), v1);
753 }
754
755 #[test]
756 fn export_import_round_trips() {
757 let m = SplitterModel::new(3, Orientation::Horizontal);
758 m.set_stored_size(0, 120.0);
759 m.set_stored_size(1, 340.0);
760 m.set_collapsed(2, true);
761 let state = m.export_state();
762
763 let restored = SplitterModel::new(3, Orientation::Horizontal);
764 assert!(restored.import_state(&state));
765 assert_eq!(restored.stored_size(0), 120.0);
766 assert_eq!(restored.stored_size(1), 340.0);
767 assert!(restored.is_collapsed(2));
768 }
769
770 #[test]
771 fn import_rejects_pane_count_mismatch() {
772 let m = SplitterModel::new(3, Orientation::Horizontal);
773 let state = m.export_state();
774 let two = SplitterModel::new(2, Orientation::Horizontal);
775 assert!(!two.import_state(&state));
776 }
777
778 #[test]
779 fn insert_remove_change_count() {
780 let m = SplitterModel::new(2, Orientation::Horizontal);
781 m.insert_pane(1, PaneDescriptor::new().size(100.0));
782 assert_eq!(m.pane_count(), 3);
783 assert_eq!(m.stored_size(1), 100.0);
784 m.remove_pane(0);
785 assert_eq!(m.pane_count(), 2);
786 }
787
788 #[test]
789 fn max_size_enforced_ge_min() {
790 let m = SplitterModel::from_panes(
791 vec![PaneDescriptor::new().min_size(200.0).max_size(100.0)],
792 Orientation::Horizontal,
793 );
794 assert_eq!(m.max_size(0), Some(200.0));
796 }
797
798 #[test]
799 fn animate_flag_consumed_and_resets() {
800 let m = SplitterModel::new(2, Orientation::Horizontal);
801 m.set_collapsed_immediate(0, true);
802 assert!(!m.consume_animate_flag()); assert!(m.consume_animate_flag());
805 }
806}