1use std::cell::RefCell;
37use std::rc::Rc;
38
39use serde::{Deserialize, Serialize};
40use teksilo_core::signal::Signal;
41use teksilo_settings::Versioned;
42use teksilo_tokens::Orientation;
43
44pub const SPLITTER_GUTTER_THICKNESS: f32 = 6.0;
47pub const SPLITTER_MIN_PANE_SIZE: f32 = 96.0;
49pub const SPLITTER_KEYBOARD_STEP: f32 = 24.0;
51pub const SPLITTER_SNAP_OFFSET: f32 = 30.0;
53
54#[derive(Debug, Clone)]
63pub struct PaneDescriptor {
64 pub initial_size: Option<f32>,
67 pub min_size: f32,
69 pub max_size: Option<f32>,
71 pub stretch: f32,
74 pub collapsible: bool,
79 pub collapsed: bool,
81 pub collapsed_size: f32,
87 pub visible: bool,
93}
94
95impl Default for PaneDescriptor {
96 fn default() -> Self {
97 Self {
98 initial_size: None,
99 min_size: SPLITTER_MIN_PANE_SIZE,
100 max_size: None,
101 stretch: 1.0,
102 collapsible: false,
103 collapsed: false,
104 collapsed_size: 0.0,
105 visible: true,
106 }
107 }
108}
109
110impl PaneDescriptor {
111 pub fn new() -> Self {
112 Self::default()
113 }
114 pub fn size(mut self, size: f32) -> Self {
115 self.initial_size = Some(size);
116 self
117 }
118 pub fn min_size(mut self, min: f32) -> Self {
119 self.min_size = min;
120 self
121 }
122 pub fn max_size(mut self, max: f32) -> Self {
123 self.max_size = Some(max);
124 self
125 }
126 pub fn stretch(mut self, stretch: f32) -> Self {
127 self.stretch = stretch;
128 self
129 }
130 pub fn collapsible(mut self, collapsible: bool) -> Self {
131 self.collapsible = collapsible;
132 self
133 }
134 pub fn collapsed(mut self, collapsed: bool) -> Self {
135 self.collapsed = collapsed;
136 self
137 }
138 pub fn collapsed_size(mut self, px: f32) -> Self {
141 self.collapsed_size = px.max(0.0);
142 self
143 }
144 pub fn visible(mut self, visible: bool) -> Self {
145 self.visible = visible;
146 self
147 }
148}
149
150#[derive(Debug, Clone)]
155struct PaneEntry {
156 stored_size: f32,
157 min_size: f32,
158 max_size: Option<f32>,
159 stretch: f32,
160 collapsible: bool,
161 collapsed: bool,
162 collapsed_size: f32,
163 visible: bool,
164}
165
166impl PaneEntry {
167 fn from_descriptor(d: &PaneDescriptor, fallback_size: f32) -> Self {
168 let min = d.min_size.max(0.0);
169 let max = d.max_size.map(|m| m.max(min));
172 let stored = d.initial_size.unwrap_or(fallback_size).max(0.0);
173 Self {
174 stored_size: stored,
175 min_size: min,
176 max_size: max,
177 stretch: d.stretch.max(0.0),
178 collapsible: d.collapsible,
179 collapsed: d.collapsed,
180 collapsed_size: d.collapsed_size.max(0.0),
181 visible: d.visible,
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq)]
189pub struct PaneSnapshot {
190 pub stored_size: f32,
191 pub min_size: f32,
192 pub max_size: Option<f32>,
193 pub stretch: f32,
194 pub collapsed: bool,
195 pub collapsed_size: f32,
196 pub visible: bool,
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
208pub struct PaneState {
209 pub stored_size: f32,
210 pub collapsed: bool,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
219pub struct SplitterState {
220 #[serde(default = "default_version")]
221 pub version: u32,
222 #[serde(default)]
223 pub panes: Vec<PaneState>,
224}
225
226fn default_version() -> u32 {
227 SplitterState::CURRENT_VERSION
228}
229
230impl Default for SplitterState {
231 fn default() -> Self {
232 Self {
233 version: SplitterState::CURRENT_VERSION,
234 panes: Vec::new(),
235 }
236 }
237}
238
239impl Versioned for SplitterState {
240 const CURRENT_VERSION: u32 = 1;
241 fn version(&self) -> u32 {
242 self.version
243 }
244 fn set_version(&mut self, v: u32) {
245 self.version = v;
246 }
247}
248
249struct SplitterModelInner {
254 panes: Vec<PaneEntry>,
255 orientation: Orientation,
256 gutter_thickness: f32,
257 keyboard_step_px: f32,
258 snap_offset: f32,
259 version: Signal<u64>,
260 animate_next_collapse: bool,
264}
265
266pub struct SplitterModel(Rc<RefCell<SplitterModelInner>>);
269
270impl Clone for SplitterModel {
271 fn clone(&self) -> Self {
272 Self(self.0.clone())
273 }
274}
275
276impl std::fmt::Debug for SplitterModel {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 match self.0.try_borrow() {
279 Ok(inner) => f
280 .debug_struct("SplitterModel")
281 .field("handles", &Rc::strong_count(&self.0))
282 .field("panes", &inner.panes.len())
283 .field("orientation", &inner.orientation)
284 .finish(),
285 Err(_) => f
286 .debug_struct("SplitterModel")
287 .field("handles", &Rc::strong_count(&self.0))
288 .field("panes", &"<borrowed>")
289 .finish(),
290 }
291 }
292}
293
294impl SplitterModel {
295 pub fn new(n: usize, orientation: Orientation) -> Self {
299 let panes = (0..n)
300 .map(|_| PaneEntry::from_descriptor(&PaneDescriptor::default(), 0.0))
301 .collect();
302 Self::from_inner(panes, orientation)
303 }
304
305 pub fn from_panes(panes: Vec<PaneDescriptor>, orientation: Orientation) -> Self {
307 let entries = panes
308 .iter()
309 .map(|d| PaneEntry::from_descriptor(d, d.initial_size.unwrap_or(0.0)))
310 .collect();
311 Self::from_inner(entries, orientation)
312 }
313
314 fn from_inner(panes: Vec<PaneEntry>, orientation: Orientation) -> Self {
315 Self(Rc::new(RefCell::new(SplitterModelInner {
316 panes,
317 orientation,
318 gutter_thickness: SPLITTER_GUTTER_THICKNESS,
319 keyboard_step_px: SPLITTER_KEYBOARD_STEP,
320 snap_offset: SPLITTER_SNAP_OFFSET,
321 version: Signal::new(0),
322 animate_next_collapse: true,
323 })))
324 }
325
326 pub fn handle_count(&self) -> usize {
328 Rc::strong_count(&self.0)
329 }
330
331 fn bump_version(&self) {
334 let version = self.0.borrow().version.clone();
337 version.set(version.get().wrapping_add(1));
338 }
339
340 pub fn set_stored_size(&self, index: usize, size: f32) {
343 {
344 let mut inner = self.0.borrow_mut();
345 let Some(p) = inner.panes.get_mut(index) else {
346 return;
347 };
348 p.stored_size = size.max(0.0);
349 }
350 self.bump_version();
351 }
352
353 pub fn set_stored_size_silent(&self, index: usize, size: f32) {
358 let mut inner = self.0.borrow_mut();
359 if let Some(p) = inner.panes.get_mut(index) {
360 p.stored_size = size.max(0.0);
361 }
362 }
363
364 pub fn set_pair_sizes(&self, index: usize, size_a: f32, size_b: f32) {
368 {
369 let mut inner = self.0.borrow_mut();
370 if index + 1 >= inner.panes.len() {
371 return;
372 }
373 inner.panes[index].stored_size = size_a.max(0.0);
374 inner.panes[index + 1].stored_size = size_b.max(0.0);
375 }
376 self.bump_version();
377 }
378
379 pub fn set_min_size(&self, index: usize, min: f32) {
380 {
381 let mut inner = self.0.borrow_mut();
382 let Some(p) = inner.panes.get_mut(index) else {
383 return;
384 };
385 p.min_size = min.max(0.0);
386 if let Some(m) = p.max_size {
388 p.max_size = Some(m.max(p.min_size));
389 }
390 }
391 self.bump_version();
392 }
393
394 pub fn set_max_size(&self, index: usize, max: Option<f32>) {
395 {
396 let mut inner = self.0.borrow_mut();
397 let Some(p) = inner.panes.get_mut(index) else {
398 return;
399 };
400 p.max_size = max.map(|m| m.max(p.min_size));
401 }
402 self.bump_version();
403 }
404
405 pub fn set_stretch(&self, index: usize, stretch: f32) {
406 {
407 let mut inner = self.0.borrow_mut();
408 let Some(p) = inner.panes.get_mut(index) else {
409 return;
410 };
411 p.stretch = stretch.max(0.0);
412 }
413 self.bump_version();
414 }
415
416 pub fn set_collapsible(&self, index: usize, collapsible: bool) {
417 {
418 let mut inner = self.0.borrow_mut();
419 let Some(p) = inner.panes.get_mut(index) else {
420 return;
421 };
422 p.collapsible = collapsible;
423 }
424 self.bump_version();
425 }
426
427 pub fn set_collapsed(&self, index: usize, collapsed: bool) {
432 self.set_collapsed_inner(index, collapsed, true);
433 }
434
435 pub fn set_collapsed_immediate(&self, index: usize, collapsed: bool) {
438 self.set_collapsed_inner(index, collapsed, false);
439 }
440
441 pub fn toggle_collapsed(&self, index: usize) {
443 let current = self.is_collapsed(index);
444 self.set_collapsed(index, !current);
445 }
446
447 pub fn set_collapsed_size(&self, index: usize, px: f32) {
451 let mut inner = self.0.borrow_mut();
452 if let Some(p) = inner.panes.get_mut(index) {
453 p.collapsed_size = px.max(0.0);
454 }
455 }
456
457 fn set_collapsed_inner(&self, index: usize, collapsed: bool, animate: bool) {
458 {
459 let mut inner = self.0.borrow_mut();
460 let Some(p) = inner.panes.get_mut(index) else {
461 return;
462 };
463 if p.collapsed == collapsed {
464 return; }
466 p.collapsed = collapsed;
467 inner.animate_next_collapse = animate;
468 }
469 self.bump_version();
470 }
471
472 pub fn set_pane_visible(&self, index: usize, visible: bool) {
478 {
479 let mut inner = self.0.borrow_mut();
480 let Some(p) = inner.panes.get_mut(index) else {
481 return;
482 };
483 if p.visible == visible {
484 return;
485 }
486 p.visible = visible;
487 inner.animate_next_collapse = true;
488 }
489 self.bump_version();
490 }
491
492 pub fn is_pane_visible(&self, index: usize) -> bool {
493 self.0
494 .borrow()
495 .panes
496 .get(index)
497 .map(|p| p.visible)
498 .unwrap_or(false)
499 }
500
501 pub fn consume_animate_flag(&self) -> bool {
505 let mut inner = self.0.borrow_mut();
506 let f = inner.animate_next_collapse;
507 inner.animate_next_collapse = true;
508 f
509 }
510
511 pub fn insert_pane(&self, index: usize, desc: PaneDescriptor) {
520 {
521 let mut inner = self.0.borrow_mut();
522 let idx = index.min(inner.panes.len());
523 let fallback = if inner.panes.is_empty() {
524 SPLITTER_MIN_PANE_SIZE
525 } else {
526 inner.panes.iter().map(|p| p.stored_size).sum::<f32>() / inner.panes.len() as f32
527 };
528 inner
529 .panes
530 .insert(idx, PaneEntry::from_descriptor(&desc, fallback));
531 }
532 self.bump_version();
533 }
534
535 pub fn remove_pane(&self, index: usize) {
538 {
539 let mut inner = self.0.borrow_mut();
540 if index >= inner.panes.len() {
541 return;
542 }
543 inner.panes.remove(index);
544 }
545 self.bump_version();
546 }
547
548 pub fn replace_pane_desc(&self, index: usize, desc: PaneDescriptor) {
551 {
552 let mut inner = self.0.borrow_mut();
553 let Some(p) = inner.panes.get_mut(index) else {
554 return;
555 };
556 let fallback = p.stored_size;
557 *p = PaneEntry::from_descriptor(&desc, fallback);
558 }
559 self.bump_version();
560 }
561
562 pub fn set_gutter_thickness(&self, thickness: f32) {
565 {
566 self.0.borrow_mut().gutter_thickness = thickness.max(1.0);
567 }
568 self.bump_version();
569 }
570
571 pub fn set_snap_offset(&self, offset: f32) {
572 {
573 self.0.borrow_mut().snap_offset = offset.max(0.0);
574 }
575 self.bump_version();
576 }
577
578 pub fn set_keyboard_step_px(&self, step: f32) {
579 {
580 self.0.borrow_mut().keyboard_step_px = step.max(1.0);
581 }
582 self.bump_version();
583 }
584
585 pub fn set_orientation(&self, orientation: Orientation) {
586 {
587 self.0.borrow_mut().orientation = orientation;
588 }
589 self.bump_version();
590 }
591
592 pub fn pane_count(&self) -> usize {
595 self.0.borrow().panes.len()
596 }
597 pub fn stored_size(&self, index: usize) -> f32 {
598 self.0
599 .borrow()
600 .panes
601 .get(index)
602 .map(|p| p.stored_size)
603 .unwrap_or(0.0)
604 }
605 pub fn min_size(&self, index: usize) -> f32 {
606 self.0
607 .borrow()
608 .panes
609 .get(index)
610 .map(|p| p.min_size)
611 .unwrap_or(0.0)
612 }
613 pub fn max_size(&self, index: usize) -> Option<f32> {
614 self.0.borrow().panes.get(index).and_then(|p| p.max_size)
615 }
616 pub fn stretch(&self, index: usize) -> f32 {
617 self.0
618 .borrow()
619 .panes
620 .get(index)
621 .map(|p| p.stretch)
622 .unwrap_or(0.0)
623 }
624 pub fn is_collapsible(&self, index: usize) -> bool {
625 self.0
626 .borrow()
627 .panes
628 .get(index)
629 .map(|p| p.collapsible)
630 .unwrap_or(false)
631 }
632 pub fn collapsed_size(&self, index: usize) -> f32 {
635 self.0
636 .borrow()
637 .panes
638 .get(index)
639 .map(|p| p.collapsed_size)
640 .unwrap_or(0.0)
641 }
642 pub fn is_collapsed(&self, index: usize) -> bool {
643 self.0
644 .borrow()
645 .panes
646 .get(index)
647 .map(|p| p.collapsed)
648 .unwrap_or(false)
649 }
650 pub fn orientation(&self) -> Orientation {
651 self.0.borrow().orientation
652 }
653 pub fn gutter_thickness(&self) -> f32 {
654 self.0.borrow().gutter_thickness
655 }
656 pub fn snap_offset(&self) -> f32 {
657 self.0.borrow().snap_offset
658 }
659 pub fn keyboard_step_px(&self) -> f32 {
660 self.0.borrow().keyboard_step_px
661 }
662
663 pub fn version(&self) -> Signal<u64> {
666 self.0.borrow().version.clone()
667 }
668
669 pub fn pane_snapshots(&self) -> Vec<PaneSnapshot> {
671 self.0
672 .borrow()
673 .panes
674 .iter()
675 .map(|p| PaneSnapshot {
676 stored_size: p.stored_size,
677 min_size: p.min_size,
678 max_size: p.max_size,
679 stretch: p.stretch,
680 collapsed: p.collapsed,
681 collapsed_size: p.collapsed_size,
682 visible: p.visible,
683 })
684 .collect()
685 }
686
687 pub fn export_state(&self) -> SplitterState {
692 let inner = self.0.borrow();
693 SplitterState {
694 version: SplitterState::CURRENT_VERSION,
695 panes: inner
696 .panes
697 .iter()
698 .map(|p| PaneState {
699 stored_size: p.stored_size,
700 collapsed: p.collapsed,
701 })
702 .collect(),
703 }
704 }
705
706 pub fn import_state(&self, state: &SplitterState) -> bool {
711 let ok = {
712 let mut inner = self.0.borrow_mut();
713 if state.panes.len() != inner.panes.len() {
714 false
715 } else {
716 for (p, s) in inner.panes.iter_mut().zip(&state.panes) {
717 p.stored_size = s.stored_size.max(0.0);
718 p.collapsed = s.collapsed;
719 }
720 inner.animate_next_collapse = false;
721 true
722 }
723 };
724 if ok {
725 self.bump_version();
726 }
727 ok
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use super::*;
734
735 #[test]
736 fn clone_shares_state() {
737 let a = SplitterModel::new(3, Orientation::Horizontal);
738 let b = a.clone();
739 assert_eq!(b.pane_count(), 3);
740 a.set_stored_size(0, 200.0);
741 assert_eq!(b.stored_size(0), 200.0);
742 assert_eq!(a.handle_count(), 2);
743 }
744
745 #[test]
746 fn version_bumps_on_mutation() {
747 let m = SplitterModel::new(2, Orientation::Horizontal);
748 let v = m.version();
749 let v0 = v.get();
750 m.set_stored_size(0, 150.0);
751 assert_ne!(v.get(), v0);
752 let v1 = v.get();
754 m.set_collapsed(0, false); assert_eq!(v.get(), v1);
756 }
757
758 #[test]
759 fn export_import_round_trips() {
760 let m = SplitterModel::new(3, Orientation::Horizontal);
761 m.set_stored_size(0, 120.0);
762 m.set_stored_size(1, 340.0);
763 m.set_collapsed(2, true);
764 let state = m.export_state();
765
766 let restored = SplitterModel::new(3, Orientation::Horizontal);
767 assert!(restored.import_state(&state));
768 assert_eq!(restored.stored_size(0), 120.0);
769 assert_eq!(restored.stored_size(1), 340.0);
770 assert!(restored.is_collapsed(2));
771 }
772
773 #[test]
774 fn import_rejects_pane_count_mismatch() {
775 let m = SplitterModel::new(3, Orientation::Horizontal);
776 let state = m.export_state();
777 let two = SplitterModel::new(2, Orientation::Horizontal);
778 assert!(!two.import_state(&state));
779 }
780
781 #[test]
782 fn insert_remove_change_count() {
783 let m = SplitterModel::new(2, Orientation::Horizontal);
784 m.insert_pane(1, PaneDescriptor::new().size(100.0));
785 assert_eq!(m.pane_count(), 3);
786 assert_eq!(m.stored_size(1), 100.0);
787 m.remove_pane(0);
788 assert_eq!(m.pane_count(), 2);
789 }
790
791 #[test]
792 fn max_size_enforced_ge_min() {
793 let m = SplitterModel::from_panes(
794 vec![PaneDescriptor::new().min_size(200.0).max_size(100.0)],
795 Orientation::Horizontal,
796 );
797 assert_eq!(m.max_size(0), Some(200.0));
799 }
800
801 #[test]
802 fn animate_flag_consumed_and_resets() {
803 let m = SplitterModel::new(2, Orientation::Horizontal);
804 m.set_collapsed_immediate(0, true);
805 assert!(!m.consume_animate_flag()); assert!(m.consume_animate_flag());
808 }
809}