1use std::cell::RefCell;
11use std::collections::HashMap;
12use std::rc::Rc;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15use teksilo_canvas::Size;
16use teksilo_core::signal::Signal;
17use teksilo_i18n::{LocalizedString, lit};
18use teksilo_settings::Versioned;
19use teksilo_tokens::Orientation;
20
21use crate::primitives::IconWidget;
22use crate::splitter::{PaneDescriptor, SplitterModel};
23use crate::toolbar::ToolbarItem;
24
25pub use super::geometry::{CornerOwners, DockCorner, DockSide};
26
27pub type DockIconFactory = Rc<dyn Fn() -> IconWidget>;
29
30pub type DockHeaderActionsFactory = Rc<dyn Fn(DockWidgetId) -> Vec<ToolbarItem>>;
41
42#[derive(
44 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
45)]
46pub struct DockWidgetId(pub u64);
47
48impl DockWidgetId {
49 pub fn fresh() -> Self {
51 static COUNTER: AtomicU64 = AtomicU64::new(1);
52 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
53 }
54 pub fn from_raw(v: u64) -> Self {
55 Self(v)
56 }
57 pub fn raw(self) -> u64 {
58 self.0
59 }
60}
61
62#[derive(
64 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
65)]
66pub struct DockTabId(pub u64);
67
68impl DockTabId {
69 pub fn fresh() -> Self {
70 static COUNTER: AtomicU64 = AtomicU64::new(1);
71 Self(COUNTER.fetch_add(1, Ordering::Relaxed))
72 }
73 pub fn from_raw(v: u64) -> Self {
74 Self(v)
75 }
76 pub fn raw(self) -> u64 {
77 self.0
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84pub enum TabPresentation {
85 Strip,
87 Rail,
89}
90
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93pub enum DockOpenMode {
94 Stack,
96 NewTab,
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct DockOpenLocation {
103 pub side: DockSide,
104 pub mode: DockOpenMode,
105}
106
107impl DockOpenLocation {
108 pub fn side(side: DockSide) -> Self {
110 Self {
111 side,
112 mode: DockOpenMode::Stack,
113 }
114 }
115 pub fn stack(mut self) -> Self {
117 self.mode = DockOpenMode::Stack;
118 self
119 }
120 pub fn new_tab(mut self) -> Self {
122 self.mode = DockOpenMode::NewTab;
123 self
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum DockRailItemSize {
130 Default = 0,
132 Compact = 1,
138 Labeled = 2,
141}
142
143impl DockRailItemSize {
144 fn from_usize(v: usize) -> Self {
145 match v {
146 1 => Self::Compact,
147 2 => Self::Labeled,
148 _ => Self::Default,
149 }
150 }
151
152 pub fn shows_label(self) -> bool {
155 matches!(self, Self::Labeled)
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum DockTabDisplay {
162 Text = 0,
164 Icon = 1,
166 IconText = 2,
168}
169
170impl DockTabDisplay {
171 fn from_usize(v: usize) -> Self {
172 match v {
173 1 => Self::Icon,
174 2 => Self::IconText,
175 _ => Self::Text,
176 }
177 }
178 pub fn shows_icon(self) -> bool {
180 matches!(self, Self::Icon | Self::IconText)
181 }
182 pub fn shows_text(self) -> bool {
184 matches!(self, Self::Text | Self::IconText)
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub struct DockPolicy {
200 pub allow_activity_drag: bool,
204 pub allow_dock_drag: bool,
207 pub allow_side_collapse: bool,
211 pub allow_activity_hide: bool,
213}
214
215impl Default for DockPolicy {
216 fn default() -> Self {
217 Self {
218 allow_activity_drag: true,
219 allow_dock_drag: true,
220 allow_side_collapse: true,
221 allow_activity_hide: true,
222 }
223 }
224}
225
226impl DockPolicy {
227 pub fn locked() -> Self {
230 Self {
231 allow_activity_drag: false,
232 allow_dock_drag: false,
233 allow_side_collapse: false,
234 allow_activity_hide: false,
235 }
236 }
237}
238
239pub(crate) struct DockWidgetMeta {
242 pub title: LocalizedString,
243 pub icon: Option<DockIconFactory>,
244 #[allow(dead_code)]
245 pub min_size: Option<Size>,
246 pub default: DockOpenLocation,
247 pub header_actions: Option<DockHeaderActionsFactory>,
250 pub show_header: bool,
254}
255
256#[derive(Clone)]
260pub(crate) struct DockTab {
261 pub id: DockTabId,
262 pub title: Option<LocalizedString>,
263 pub icon: Option<DockIconFactory>,
264 pub splitter: SplitterModel,
265 pub panes: Vec<DockWidgetId>,
267 pub hidden: bool,
270}
271
272pub(crate) struct SideState {
274 pub tabs: Vec<DockTab>,
275 pub selected_tab: usize,
276 pub presentation: TabPresentation,
277 pub rail_thickness: f32,
278 pub size: f32,
279 pub min_size: f32,
280 pub visible: bool,
281 pub enabled: bool,
285 pub visible_sig: Signal<bool>,
286 pub selected_tab_sig: Signal<usize>,
287 pub rail_size_sig: Signal<usize>,
291 pub tab_display_sig: Signal<usize>,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub struct DockLoc {
300 pub side: DockSide,
301 pub tab_idx: usize,
302 pub pane_idx: usize,
303}
304
305#[derive(Debug, Clone, Copy)]
308struct DetachOutcome {
309 loc: DockLoc,
310 removed_pane: bool,
311 removed_tab: bool,
312}
313
314impl DetachOutcome {
315 fn adjust_target(&self, side: DockSide, tab_idx: usize, pane_idx: usize) -> (usize, usize) {
319 if self.loc.side != side {
320 return (tab_idx, pane_idx);
321 }
322 if self.removed_tab {
323 let tab_idx = if self.loc.tab_idx < tab_idx {
325 tab_idx - 1
326 } else {
327 tab_idx
328 };
329 return (tab_idx, pane_idx);
330 }
331 if self.removed_pane && self.loc.tab_idx == tab_idx && self.loc.pane_idx < pane_idx {
332 return (tab_idx, pane_idx - 1);
333 }
334 (tab_idx, pane_idx)
335 }
336}
337
338pub(crate) fn side_orientation(side: DockSide) -> Orientation {
341 if side.is_horizontal_axis() {
342 Orientation::Vertical
343 } else {
344 Orientation::Horizontal
345 }
346}
347
348fn default_side_state(side: DockSide) -> SideState {
349 let (size, rail, min) = if side.is_horizontal_axis() {
350 (260.0, 0.0, 120.0)
351 } else {
352 (200.0, 0.0, 80.0)
353 };
354 SideState {
355 tabs: Vec::new(),
356 selected_tab: 0,
357 presentation: TabPresentation::Strip,
358 rail_thickness: rail,
359 size,
360 min_size: min,
361 visible: false,
362 enabled: true,
363 visible_sig: Signal::new(false),
364 selected_tab_sig: Signal::new(0),
365 rail_size_sig: Signal::new(DockRailItemSize::Default as usize),
366 tab_display_sig: Signal::new(DockTabDisplay::Text as usize),
367 }
368}
369
370struct Inner {
371 sides: HashMap<DockSide, SideState>,
372 docks: HashMap<DockWidgetId, DockWidgetMeta>,
373 locations: HashMap<DockWidgetId, DockLoc>,
374 open_sigs: HashMap<DockWidgetId, Signal<bool>>,
375 tab_hidden_sigs: HashMap<DockTabId, Signal<bool>>,
379 corners: CornerOwners,
380 policy: DockPolicy,
383 version: Signal<u64>,
386 geometry_version: Signal<u64>,
390 animate_next: bool,
391}
392
393#[derive(Clone)]
396pub(crate) struct DockTabView {
397 pub id: DockTabId,
398 pub title: Option<LocalizedString>,
399 pub icon: Option<DockIconFactory>,
400 pub splitter: SplitterModel,
401 pub panes: Vec<DockWidgetId>,
402 pub hidden: bool,
403}
404
405pub struct DockingModel(Rc<RefCell<Inner>>);
407
408impl Clone for DockingModel {
409 fn clone(&self) -> Self {
410 Self(self.0.clone())
411 }
412}
413
414impl std::fmt::Debug for DockingModel {
415 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
416 let inner = self.0.borrow();
417 f.debug_struct("DockingModel")
418 .field("docks", &inner.docks.len())
419 .field("open", &inner.locations.len())
420 .finish()
421 }
422}
423
424impl Default for DockingModel {
425 fn default() -> Self {
426 Self::new()
427 }
428}
429
430impl DockingModel {
431 pub fn new() -> Self {
433 let mut sides = HashMap::new();
434 for side in DockSide::ALL {
435 sides.insert(side, default_side_state(side));
436 }
437 Self(Rc::new(RefCell::new(Inner {
438 sides,
439 docks: HashMap::new(),
440 locations: HashMap::new(),
441 open_sigs: HashMap::new(),
442 tab_hidden_sigs: HashMap::new(),
443 corners: CornerOwners::default(),
444 policy: DockPolicy::default(),
445 version: Signal::new(0),
446 geometry_version: Signal::new(0),
447 animate_next: true,
448 })))
449 }
450
451 pub fn version(&self) -> Signal<u64> {
454 self.0.borrow().version.clone()
455 }
456
457 pub fn geometry_version(&self) -> Signal<u64> {
460 self.0.borrow().geometry_version.clone()
461 }
462
463 pub fn consume_animate_flag(&self) -> bool {
465 let mut inner = self.0.borrow_mut();
466 std::mem::replace(&mut inner.animate_next, true)
467 }
468
469 fn sync_derived(&self) {
474 let mut updates_b: Vec<(Signal<bool>, bool)> = Vec::new();
475 let mut updates_u: Vec<(Signal<usize>, usize)> = Vec::new();
476 {
477 let inner = self.0.borrow();
478 for st in inner.sides.values() {
479 updates_b.push((st.visible_sig.clone(), st.visible));
480 updates_u.push((st.selected_tab_sig.clone(), st.selected_tab));
481 }
482 for (id, sig) in &inner.open_sigs {
483 updates_b.push((sig.clone(), inner.locations.contains_key(id)));
484 }
485 for (tab_id, sig) in &inner.tab_hidden_sigs {
486 let hidden = inner
487 .sides
488 .values()
489 .any(|st| st.tabs.iter().any(|t| t.id == *tab_id && t.hidden));
490 updates_b.push((sig.clone(), hidden));
491 }
492 }
493 for (sig, val) in updates_b {
494 if sig.get() != val {
495 sig.set(val);
496 }
497 }
498 for (sig, val) in updates_u {
499 if sig.get() != val {
500 sig.set(val);
501 }
502 }
503 }
504
505 fn notify(&self) {
507 let version = self.0.borrow().version.clone();
508 version.set(version.get().wrapping_add(1));
509 self.sync_derived();
510 }
511
512 fn relayout(&self) {
515 let gv = self.0.borrow().geometry_version.clone();
516 gv.set(gv.get().wrapping_add(1));
517 self.sync_derived();
518 }
519
520 fn set_no_animate(&self) {
522 self.0.borrow_mut().animate_next = false;
523 }
524
525 pub(crate) fn register_meta(&self, id: DockWidgetId, meta: DockWidgetMeta) {
530 self.0.borrow_mut().docks.insert(id, meta);
531 }
532
533 pub fn is_registered(&self, id: DockWidgetId) -> bool {
535 self.0.borrow().docks.contains_key(&id)
536 }
537
538 pub fn set_side_rail(&self, side: DockSide, thickness: f32) {
544 {
545 let mut inner = self.0.borrow_mut();
546 if let Some(st) = inner.sides.get_mut(&side) {
547 st.rail_thickness = thickness.max(0.0);
548 st.presentation = if thickness > 0.0 {
549 TabPresentation::Rail
550 } else {
551 TabPresentation::Strip
552 };
553 }
554 }
555 self.notify();
556 }
557
558 pub fn set_side_size(&self, side: DockSide, size: f32) {
560 {
561 let mut inner = self.0.borrow_mut();
562 if let Some(st) = inner.sides.get_mut(&side) {
563 st.size = size.max(0.0);
564 }
565 }
566 self.relayout();
567 }
568
569 pub fn set_side_min_size(&self, side: DockSide, min: f32) {
571 {
572 let mut inner = self.0.borrow_mut();
573 if let Some(st) = inner.sides.get_mut(&side) {
574 st.min_size = min.max(0.0);
575 }
576 }
577 self.relayout();
578 }
579
580 pub fn set_policy(&self, policy: DockPolicy) {
585 let changed = {
586 let mut inner = self.0.borrow_mut();
587 let changed = inner.policy != policy;
588 inner.policy = policy;
589 changed
590 };
591 if changed {
592 self.notify();
593 }
594 }
595
596 pub fn policy(&self) -> DockPolicy {
599 self.0.borrow().policy
600 }
601
602 pub fn set_side_enabled(&self, side: DockSide, enabled: bool) {
607 let changed = {
608 let mut inner = self.0.borrow_mut();
609 match inner.sides.get_mut(&side) {
610 Some(st) if st.enabled != enabled => {
611 st.enabled = enabled;
612 true
613 }
614 _ => false,
615 }
616 };
617 if changed {
618 self.notify();
619 }
620 }
621
622 pub fn is_side_enabled(&self, side: DockSide) -> bool {
624 self.0.borrow().sides.get(&side).is_none_or(|s| s.enabled)
625 }
626
627 pub fn set_side_visible(&self, side: DockSide, visible: bool) {
629 self.set_side_visible_inner(side, visible, true);
630 }
631
632 pub fn set_side_visible_immediate(&self, side: DockSide, visible: bool) {
634 self.set_side_visible_inner(side, visible, false);
635 }
636
637 fn set_side_visible_inner(&self, side: DockSide, visible: bool, animate: bool) {
638 let changed = {
639 let mut inner = self.0.borrow_mut();
640 match inner.sides.get_mut(&side) {
641 Some(st) if st.visible != visible => {
642 st.visible = visible;
643 true
644 }
645 _ => false,
646 }
647 };
648 if changed {
649 if !animate {
650 self.set_no_animate();
651 }
652 self.relayout();
656 }
657 }
658
659 pub fn toggle_side_visible(&self, side: DockSide) {
661 let cur = self.is_side_visible(side);
662 self.set_side_visible(side, !cur);
663 }
664
665 pub fn select_tab(&self, side: DockSide, tab_idx: usize) {
668 {
669 let mut inner = self.0.borrow_mut();
670 if let Some(st) = inner.sides.get_mut(&side)
671 && tab_idx < st.tabs.len()
672 {
673 st.selected_tab = tab_idx;
674 }
675 }
676 self.sync_derived();
677 }
678
679 pub fn select_tab_by_id(&self, side: DockSide, tab_id: DockTabId) {
682 let idx = {
683 let inner = self.0.borrow();
684 inner
685 .sides
686 .get(&side)
687 .and_then(|st| st.tabs.iter().position(|t| t.id == tab_id))
688 };
689 if let Some(i) = idx {
690 self.select_tab(side, i);
691 }
692 }
693
694 pub fn set_tab_hidden(&self, tab_id: DockTabId, hidden: bool) {
701 let mut changed = false;
702 {
703 let mut inner = self.0.borrow_mut();
704 'outer: for st in inner.sides.values_mut() {
705 if let Some(ti) = st.tabs.iter().position(|t| t.id == tab_id) {
706 if st.tabs[ti].hidden == hidden {
707 break 'outer;
708 }
709 st.tabs[ti].hidden = hidden;
710 changed = true;
711 if hidden && st.selected_tab == ti {
719 st.selected_tab = Self::nearest_visible_tab(&st.tabs, ti + 1).unwrap_or(0);
720 }
721 break 'outer;
722 }
723 }
724 }
725 if changed {
726 self.notify();
727 }
728 }
729
730 pub fn is_tab_hidden(&self, tab_id: DockTabId) -> bool {
732 let inner = self.0.borrow();
733 inner
734 .sides
735 .values()
736 .any(|st| st.tabs.iter().any(|t| t.id == tab_id && t.hidden))
737 }
738
739 pub(crate) fn side_visible_tab_count(&self, side: DockSide) -> usize {
741 let inner = self.0.borrow();
742 inner
743 .sides
744 .get(&side)
745 .map(|st| st.tabs.iter().filter(|t| !t.hidden).count())
746 .unwrap_or(0)
747 }
748
749 pub(crate) fn rail_size_signal(&self, side: DockSide) -> Signal<usize> {
755 self.0
756 .borrow()
757 .sides
758 .get(&side)
759 .map(|st| st.rail_size_sig.clone())
760 .unwrap_or_else(|| Signal::new(0))
761 }
762
763 pub fn side_rail_size(&self, side: DockSide) -> DockRailItemSize {
765 DockRailItemSize::from_usize(self.rail_size_signal(side).get())
766 }
767
768 pub fn set_side_rail_size(&self, side: DockSide, size: DockRailItemSize) {
770 self.rail_size_signal(side).set(size as usize);
771 }
772
773 pub fn rail_size_mode_signal(&self, side: DockSide) -> Signal<DockRailItemSize> {
780 self.rail_size_signal(side)
781 .map(|v| DockRailItemSize::from_usize(*v))
782 }
783
784 pub(crate) fn tab_display_signal(&self, side: DockSide) -> Signal<usize> {
787 self.0
788 .borrow()
789 .sides
790 .get(&side)
791 .map(|st| st.tab_display_sig.clone())
792 .unwrap_or_else(|| Signal::new(0))
793 }
794
795 pub fn side_tab_display(&self, side: DockSide) -> DockTabDisplay {
797 DockTabDisplay::from_usize(self.tab_display_signal(side).get())
798 }
799
800 pub fn set_side_tab_display(&self, side: DockSide, display: DockTabDisplay) {
802 self.tab_display_signal(side).set(display as usize);
803 }
804
805 pub fn set_corner(&self, corner: DockCorner, owner: DockSide) {
807 {
808 let mut inner = self.0.borrow_mut();
809 inner.corners.set(corner, owner);
810 }
811 self.relayout();
812 }
813
814 fn detach(inner: &mut Inner, id: DockWidgetId) -> Option<DetachOutcome> {
823 let loc = inner.locations.remove(&id)?;
824 let mut removed_pane = false;
825 let mut removed_tab = false;
826 if let Some(st) = inner.sides.get_mut(&loc.side)
827 && let Some(tab) = st.tabs.get_mut(loc.tab_idx)
828 && loc.pane_idx < tab.panes.len()
829 {
830 removed_pane = true;
832 tab.panes.remove(loc.pane_idx);
833 tab.splitter.remove_pane(loc.pane_idx);
834 if tab.panes.is_empty() {
835 removed_tab = true;
836 st.tabs.remove(loc.tab_idx);
837 Self::fix_selection_after_remove(st, loc.tab_idx);
838 }
839 }
840 Self::reindex_side(inner, loc.side);
842 Some(DetachOutcome {
843 loc,
844 removed_pane,
845 removed_tab,
846 })
847 }
848
849 fn fix_selection_after_remove(st: &mut SideState, removed_idx: usize) {
858 if removed_idx < st.selected_tab {
859 st.selected_tab -= 1;
860 } else if st.selected_tab >= st.tabs.len() && !st.tabs.is_empty() {
861 st.selected_tab = st.tabs.len() - 1;
862 }
863 if st.tabs.is_empty() {
864 st.selected_tab = 0;
865 st.visible = false;
866 }
867 }
868
869 fn nearest_visible_tab(tabs: &[DockTab], from: usize) -> Option<usize> {
874 let n = tabs.len();
875 (from..n)
876 .find(|&j| !tabs[j].hidden)
877 .or_else(|| (0..from.min(n)).rev().find(|&j| !tabs[j].hidden))
878 }
879
880 fn reindex_side(inner: &mut Inner, side: DockSide) {
882 let entries: Vec<(DockWidgetId, DockLoc)> = {
883 let Some(st) = inner.sides.get(&side) else {
884 return;
885 };
886 let mut out = Vec::new();
887 for (ti, tab) in st.tabs.iter().enumerate() {
888 for (pi, dock) in tab.panes.iter().enumerate() {
889 out.push((
890 *dock,
891 DockLoc {
892 side,
893 tab_idx: ti,
894 pane_idx: pi,
895 },
896 ));
897 }
898 }
899 out
900 };
901 inner.locations.retain(|_, l| l.side != side);
903 for (id, loc) in entries {
904 inner.locations.insert(id, loc);
905 }
906 }
907
908 fn new_tab(side: DockSide, dock: DockWidgetId) -> DockTab {
909 DockTab {
910 id: DockTabId::fresh(),
911 title: None,
912 icon: None,
913 splitter: SplitterModel::new(1, side_orientation(side)),
914 panes: vec![dock],
915 hidden: false,
916 }
917 }
918
919 pub fn open_dock(&self, id: DockWidgetId, loc: DockOpenLocation) {
922 {
923 let mut inner = self.0.borrow_mut();
924 if !inner.docks.contains_key(&id) {
925 debug_assert!(false, "open_dock on unregistered dock {id:?}");
926 return;
927 }
928 if !Self::side_accepts(&inner, loc.side) {
929 return; }
931 if inner.locations.get(&id).map(|l| l.side) == Some(loc.side) {
935 return;
936 }
937 Self::detach(&mut inner, id);
938 Self::place(&mut inner, id, loc);
939 }
940 self.notify();
941 }
942
943 fn side_accepts(inner: &Inner, side: DockSide) -> bool {
946 inner.sides.get(&side).is_none_or(|s| s.enabled)
947 }
948
949 fn place(inner: &mut Inner, id: DockWidgetId, loc: DockOpenLocation) {
950 let side = loc.side;
951 let orientation = side_orientation(side);
952 if let Some(st) = inner.sides.get_mut(&side) {
953 st.visible = true;
954 match loc.mode {
955 DockOpenMode::NewTab => {
956 st.tabs.push(DockTab {
957 id: DockTabId::fresh(),
958 title: None,
959 icon: None,
960 splitter: SplitterModel::new(1, orientation),
961 panes: vec![id],
962 hidden: false,
963 });
964 st.selected_tab = st.tabs.len() - 1;
965 }
966 DockOpenMode::Stack => {
967 if st.tabs.is_empty() {
968 st.tabs.push(DockTab {
969 id: DockTabId::fresh(),
970 title: None,
971 icon: None,
972 splitter: SplitterModel::new(1, orientation),
973 panes: vec![id],
974 hidden: false,
975 });
976 st.selected_tab = 0;
977 } else {
978 let ti = st.selected_tab.min(st.tabs.len() - 1);
982 let tab = &mut st.tabs[ti];
983 let at = tab.panes.len();
984 tab.panes.push(id);
985 tab.splitter
986 .insert_pane(at, PaneDescriptor::new().stretch(1.0));
987 st.selected_tab = ti;
988 }
989 }
990 }
991 }
992 Self::reindex_side(inner, side);
993 }
994
995 pub fn promote_to_tab(&self, id: DockWidgetId, side: DockSide, at_tab: usize) {
997 {
998 let mut inner = self.0.borrow_mut();
999 if !inner.docks.contains_key(&id) || !Self::side_accepts(&inner, side) {
1000 return;
1001 }
1002 if let Some(loc) = inner.locations.get(&id)
1004 && loc.side == side
1005 && loc.tab_idx == at_tab
1006 && inner
1007 .sides
1008 .get(&side)
1009 .and_then(|st| st.tabs.get(at_tab))
1010 .map(|t| t.panes.len() == 1)
1011 .unwrap_or(false)
1012 {
1013 return;
1014 }
1015 let mut at_tab = at_tab;
1016 if let Some(outcome) = Self::detach(&mut inner, id) {
1017 (at_tab, _) = outcome.adjust_target(side, at_tab, 0);
1018 }
1019 let tab = Self::new_tab(side, id);
1020 if let Some(st) = inner.sides.get_mut(&side) {
1021 let at = at_tab.min(st.tabs.len());
1022 st.tabs.insert(at, tab);
1023 st.selected_tab = at;
1024 st.visible = true;
1025 }
1026 Self::reindex_side(&mut inner, side);
1027 }
1028 self.notify();
1029 }
1030
1031 pub fn split_into_tab(
1034 &self,
1035 id: DockWidgetId,
1036 side: DockSide,
1037 tab_idx: usize,
1038 pane_idx: usize,
1039 before: bool,
1040 ) {
1041 {
1042 let mut inner = self.0.borrow_mut();
1043 if !inner.docks.contains_key(&id) || !Self::side_accepts(&inner, side) {
1044 return;
1045 }
1046 if let Some(loc) = inner.locations.get(&id)
1049 && loc.side == side
1050 && loc.tab_idx == tab_idx
1051 && loc.pane_idx == pane_idx
1052 {
1053 return;
1054 }
1055 let (mut tab_idx, mut pane_idx) = (tab_idx, pane_idx);
1058 if let Some(outcome) = Self::detach(&mut inner, id) {
1059 (tab_idx, pane_idx) = outcome.adjust_target(side, tab_idx, pane_idx);
1060 }
1061 if let Some(st) = inner.sides.get_mut(&side)
1062 && let Some(tab) = st.tabs.get_mut(tab_idx)
1063 {
1064 let at = if before { pane_idx } else { pane_idx + 1 };
1065 let at = at.min(tab.panes.len());
1066 tab.panes.insert(at, id);
1067 tab.splitter
1068 .insert_pane(at, PaneDescriptor::new().stretch(1.0));
1069 st.visible = true;
1070 st.selected_tab = tab_idx;
1071 }
1072 Self::reindex_side(&mut inner, side);
1073 }
1074 self.notify();
1075 }
1076
1077 pub fn stack_into_tab(&self, id: DockWidgetId, side: DockSide, tab_idx: usize) {
1081 {
1082 let mut inner = self.0.borrow_mut();
1083 if !inner.docks.contains_key(&id) || !Self::side_accepts(&inner, side) {
1084 return;
1085 }
1086 if let Some(loc) = inner.locations.get(&id)
1088 && loc.side == side
1089 && loc.tab_idx == tab_idx
1090 && inner
1091 .sides
1092 .get(&side)
1093 .and_then(|st| st.tabs.get(tab_idx))
1094 .map(|t| t.panes.len() == 1)
1095 .unwrap_or(false)
1096 {
1097 return;
1098 }
1099 let mut tab_idx = tab_idx;
1100 if let Some(outcome) = Self::detach(&mut inner, id) {
1101 (tab_idx, _) = outcome.adjust_target(side, tab_idx, 0);
1102 }
1103 if let Some(st) = inner.sides.get_mut(&side)
1104 && let Some(tab) = st.tabs.get_mut(tab_idx)
1105 {
1106 let at = tab.panes.len();
1107 tab.panes.push(id);
1108 tab.splitter
1109 .insert_pane(at, PaneDescriptor::new().stretch(1.0));
1110 st.visible = true;
1111 st.selected_tab = tab_idx;
1112 }
1113 Self::reindex_side(&mut inner, side);
1114 }
1115 self.notify();
1116 }
1117
1118 pub fn move_dock(&self, id: DockWidgetId, loc: DockOpenLocation) {
1120 self.open_dock(id, loc);
1121 }
1122
1123 pub fn close_tab(&self, tab_id: DockTabId) {
1125 {
1126 let mut inner = self.0.borrow_mut();
1127 let mut found: Option<(DockSide, usize)> = None;
1128 for side in DockSide::ALL {
1129 if let Some(st) = inner.sides.get(&side)
1130 && let Some(ti) = st.tabs.iter().position(|t| t.id == tab_id)
1131 {
1132 found = Some((side, ti));
1133 break;
1134 }
1135 }
1136 let Some((side, ti)) = found else {
1137 return;
1138 };
1139 if let Some(st) = inner.sides.get_mut(&side) {
1140 st.tabs.remove(ti);
1141 Self::fix_selection_after_remove(st, ti);
1142 }
1143 Self::reindex_side(&mut inner, side);
1144 }
1145 self.notify();
1146 }
1147
1148 pub fn move_tab(&self, tab_id: DockTabId, target_side: DockSide, at_tab: usize) {
1151 {
1152 let mut inner = self.0.borrow_mut();
1153 if !Self::side_accepts(&inner, target_side) {
1154 return; }
1156 let mut found: Option<(DockSide, usize)> = None;
1158 for side in DockSide::ALL {
1159 if let Some(st) = inner.sides.get(&side)
1160 && let Some(ti) = st.tabs.iter().position(|t| t.id == tab_id)
1161 {
1162 found = Some((side, ti));
1163 break;
1164 }
1165 }
1166 let Some((src_side, src_ti)) = found else {
1167 return;
1168 };
1169 if src_side == target_side {
1170 if let Some(st) = inner.sides.get_mut(&src_side) {
1172 let tab = st.tabs.remove(src_ti);
1173 let at = at_tab.min(st.tabs.len());
1174 st.tabs.insert(at, tab);
1175 st.selected_tab = at;
1176 }
1177 Self::reindex_side(&mut inner, src_side);
1178 } else {
1179 let tab = {
1180 let st = inner.sides.get_mut(&src_side).unwrap();
1181 let tab = st.tabs.remove(src_ti);
1182 Self::fix_selection_after_remove(st, src_ti);
1183 tab
1184 };
1185 tab.splitter.set_orientation(side_orientation(target_side));
1187 if let Some(dst) = inner.sides.get_mut(&target_side) {
1188 let at = at_tab.min(dst.tabs.len());
1189 dst.tabs.insert(at, tab);
1190 dst.selected_tab = at;
1191 dst.visible = true;
1192 }
1193 Self::reindex_side(&mut inner, src_side);
1194 Self::reindex_side(&mut inner, target_side);
1195 }
1196 }
1197 self.notify();
1198 }
1199
1200 pub fn close_dock(&self, id: DockWidgetId) {
1202 {
1203 let mut inner = self.0.borrow_mut();
1204 Self::detach(&mut inner, id);
1205 }
1206 self.notify();
1207 }
1208
1209 pub fn toggle_dock(&self, id: DockWidgetId) {
1211 if self.is_dock_open(id) {
1212 self.close_dock(id);
1213 } else {
1214 let default = self.0.borrow().docks.get(&id).map(|m| m.default);
1215 if let Some(loc) = default {
1216 self.open_dock(id, loc);
1217 }
1218 }
1219 }
1220
1221 pub fn reveal_dock(&self, id: DockWidgetId) {
1223 let loc = self.0.borrow().locations.get(&id).copied();
1224 match loc {
1225 Some(loc) => {
1226 let changed = {
1234 let mut inner = self.0.borrow_mut();
1235 let tabs = inner
1236 .sides
1237 .get(&loc.side)
1238 .map_or(0, |st| st.tabs.len())
1239 .saturating_sub(1);
1240 match inner.sides.get_mut(&loc.side) {
1241 Some(st) => {
1242 let tab = loc.tab_idx.min(tabs);
1243 let changed = !st.visible || st.selected_tab != tab;
1244 st.visible = true;
1245 st.selected_tab = tab;
1246 changed
1247 }
1248 None => false,
1249 }
1250 };
1251 if changed {
1252 self.notify();
1253 }
1254 }
1255 None => {
1256 let default = self.0.borrow().docks.get(&id).map(|m| m.default);
1257 if let Some(loc) = default {
1258 self.open_dock(id, loc);
1259 }
1260 }
1261 }
1262 }
1263
1264 pub fn is_dock_open(&self, id: DockWidgetId) -> bool {
1267 self.0.borrow().locations.contains_key(&id)
1268 }
1269
1270 pub(crate) fn open_dock_ids(&self) -> Vec<DockWidgetId> {
1273 self.0.borrow().locations.keys().copied().collect()
1274 }
1275
1276 pub fn dock_location(&self, id: DockWidgetId) -> Option<DockLoc> {
1277 self.0.borrow().locations.get(&id).copied()
1278 }
1279
1280 pub fn dock_open_signal(&self, id: DockWidgetId) -> Signal<bool> {
1282 let mut inner = self.0.borrow_mut();
1283 let open = inner.locations.contains_key(&id);
1284 inner
1285 .open_sigs
1286 .entry(id)
1287 .or_insert_with(|| Signal::new(open))
1288 .clone()
1289 }
1290
1291 pub fn is_side_visible(&self, side: DockSide) -> bool {
1292 self.0.borrow().sides.get(&side).is_some_and(|s| s.visible)
1293 }
1294
1295 pub fn side_visible_signal(&self, side: DockSide) -> Signal<bool> {
1296 self.0
1297 .borrow()
1298 .sides
1299 .get(&side)
1300 .map(|s| s.visible_sig.clone())
1301 .unwrap_or_else(|| Signal::new(false))
1302 }
1303
1304 pub fn side_selected_tab_signal(&self, side: DockSide) -> Signal<usize> {
1305 self.0
1306 .borrow()
1307 .sides
1308 .get(&side)
1309 .map(|s| s.selected_tab_sig.clone())
1310 .unwrap_or_else(|| Signal::new(0))
1311 }
1312
1313 pub fn side_selected_tab(&self, side: DockSide) -> usize {
1314 self.0
1315 .borrow()
1316 .sides
1317 .get(&side)
1318 .map(|s| s.selected_tab)
1319 .unwrap_or(0)
1320 }
1321
1322 pub fn side_presentation(&self, side: DockSide) -> TabPresentation {
1323 self.0
1324 .borrow()
1325 .sides
1326 .get(&side)
1327 .map(|s| s.presentation)
1328 .unwrap_or(TabPresentation::Strip)
1329 }
1330
1331 pub fn side_size(&self, side: DockSide) -> f32 {
1332 self.0
1333 .borrow()
1334 .sides
1335 .get(&side)
1336 .map(|s| s.size)
1337 .unwrap_or(0.0)
1338 }
1339
1340 pub fn side_min_size(&self, side: DockSide) -> f32 {
1341 self.0
1342 .borrow()
1343 .sides
1344 .get(&side)
1345 .map(|s| s.min_size)
1346 .unwrap_or(0.0)
1347 }
1348
1349 pub fn side_rail_thickness(&self, side: DockSide) -> f32 {
1350 self.0
1351 .borrow()
1352 .sides
1353 .get(&side)
1354 .map(|s| {
1355 if matches!(s.presentation, TabPresentation::Rail) {
1356 s.rail_thickness
1357 } else {
1358 0.0
1359 }
1360 })
1361 .unwrap_or(0.0)
1362 }
1363
1364 pub fn side_has_rail(&self, side: DockSide) -> bool {
1365 self.side_rail_thickness(side) > 0.0
1366 }
1367
1368 pub fn corner_owner(&self, corner: DockCorner) -> DockSide {
1369 self.0.borrow().corners.owner(corner)
1370 }
1371
1372 pub(crate) fn corners(&self) -> CornerOwners {
1373 self.0.borrow().corners
1374 }
1375
1376 pub fn tab_count(&self, side: DockSide) -> usize {
1377 self.0
1378 .borrow()
1379 .sides
1380 .get(&side)
1381 .map(|s| s.tabs.len())
1382 .unwrap_or(0)
1383 }
1384
1385 pub(crate) fn side_append_index(&self, side: DockSide) -> usize {
1391 let inner = self.0.borrow();
1392 match inner.sides.get(&side) {
1393 Some(st) => st
1394 .tabs
1395 .iter()
1396 .rposition(|t| !t.hidden)
1397 .map(|i| i + 1)
1398 .unwrap_or(st.tabs.len()),
1399 None => 0,
1400 }
1401 }
1402
1403 pub(crate) fn dock_title(&self, id: DockWidgetId) -> Option<LocalizedString> {
1405 self.0.borrow().docks.get(&id).map(|m| m.title.clone())
1406 }
1407
1408 pub(crate) fn dock_icon(&self, id: DockWidgetId) -> Option<DockIconFactory> {
1409 self.0.borrow().docks.get(&id).and_then(|m| m.icon.clone())
1410 }
1411
1412 pub(crate) fn side_tabs(&self, side: DockSide) -> Vec<DockTabView> {
1414 let inner = self.0.borrow();
1415 let Some(st) = inner.sides.get(&side) else {
1416 return Vec::new();
1417 };
1418 st.tabs
1419 .iter()
1420 .map(|tab| DockTabView {
1421 id: tab.id,
1422 title: tab.title.clone(),
1423 icon: tab.icon.clone(),
1424 splitter: tab.splitter.clone(),
1425 panes: tab.panes.clone(),
1426 hidden: tab.hidden,
1427 })
1428 .collect()
1429 }
1430
1431 pub(crate) fn tab_docks(&self, tab_id: DockTabId) -> Vec<DockWidgetId> {
1433 let inner = self.0.borrow();
1434 for st in inner.sides.values() {
1435 if let Some(tab) = st.tabs.iter().find(|t| t.id == tab_id) {
1436 return tab.panes.clone();
1437 }
1438 }
1439 Vec::new()
1440 }
1441
1442 pub(crate) fn tab_view_by_id(&self, tab_id: DockTabId) -> Option<(DockSide, DockTabView)> {
1444 for side in DockSide::ALL {
1445 if let Some(view) = self.side_tabs(side).into_iter().find(|t| t.id == tab_id) {
1446 return Some((side, view));
1447 }
1448 }
1449 None
1450 }
1451
1452 pub fn tab_id_at(&self, side: DockSide, idx: usize) -> Option<DockTabId> {
1458 let inner = self.0.borrow();
1459 inner
1460 .sides
1461 .get(&side)
1462 .and_then(|st| st.tabs.get(idx).map(|t| t.id))
1463 }
1464
1465 pub(crate) fn primary_dock_of(view: &DockTabView) -> Option<DockWidgetId> {
1471 view.panes
1472 .iter()
1473 .enumerate()
1474 .find(|(i, _)| !view.splitter.is_collapsed(*i))
1475 .map(|(_, d)| *d)
1476 .or_else(|| view.panes.first().copied())
1477 }
1478
1479 pub(crate) fn activity_label(&self, view: &DockTabView) -> LocalizedString {
1482 view.title
1483 .clone()
1484 .or_else(|| Self::primary_dock_of(view).and_then(|d| self.dock_title(d)))
1485 .unwrap_or_else(|| lit!("Panel"))
1486 }
1487
1488 pub(crate) fn activity_icon(&self, view: &DockTabView) -> Option<DockIconFactory> {
1491 view.icon
1492 .clone()
1493 .or_else(|| Self::primary_dock_of(view).and_then(|d| self.dock_icon(d)))
1494 }
1495
1496 pub fn set_tab_title(&self, tab_id: DockTabId, title: Option<LocalizedString>) {
1502 let mut changed = false;
1503 {
1504 let mut inner = self.0.borrow_mut();
1505 'outer: for st in inner.sides.values_mut() {
1506 if let Some(tab) = st.tabs.iter_mut().find(|t| t.id == tab_id) {
1507 tab.title = title;
1508 changed = true;
1509 break 'outer;
1510 }
1511 }
1512 }
1513 if changed {
1514 self.notify();
1515 }
1516 }
1517
1518 pub fn tab_title(&self, tab_id: DockTabId) -> Option<LocalizedString> {
1521 let inner = self.0.borrow();
1522 inner
1523 .sides
1524 .values()
1525 .flat_map(|st| st.tabs.iter())
1526 .find(|t| t.id == tab_id)
1527 .and_then(|t| t.title.clone())
1528 }
1529
1530 pub fn activity_of(&self, dock_id: DockWidgetId) -> Option<DockTabId> {
1533 let inner = self.0.borrow();
1534 let loc = inner.locations.get(&dock_id)?;
1535 inner
1536 .sides
1537 .get(&loc.side)
1538 .and_then(|st| st.tabs.get(loc.tab_idx))
1539 .map(|t| t.id)
1540 }
1541
1542 pub fn set_dock_activity_title(
1545 &self,
1546 dock_id: DockWidgetId,
1547 title: impl Into<LocalizedString>,
1548 ) {
1549 if let Some(tab_id) = self.activity_of(dock_id) {
1550 self.set_tab_title(tab_id, Some(title.into()));
1551 }
1552 }
1553
1554 pub fn enabled_move_targets(&self, from: DockSide) -> Vec<DockSide> {
1559 DockSide::ALL
1560 .into_iter()
1561 .filter(|&s| s != from && self.is_side_enabled(s))
1562 .collect()
1563 }
1564
1565 pub(crate) fn tab_hidden_signal(&self, tab_id: DockTabId) -> Signal<bool> {
1570 let mut inner = self.0.borrow_mut();
1571 let hidden = inner
1572 .sides
1573 .values()
1574 .any(|st| st.tabs.iter().any(|t| t.id == tab_id && t.hidden));
1575 inner
1576 .tab_hidden_sigs
1577 .entry(tab_id)
1578 .or_insert_with(|| Signal::new(hidden))
1579 .clone()
1580 }
1581
1582 pub(crate) fn dock_header_actions(&self, id: DockWidgetId) -> Option<DockHeaderActionsFactory> {
1584 self.0
1585 .borrow()
1586 .docks
1587 .get(&id)
1588 .and_then(|m| m.header_actions.clone())
1589 }
1590
1591 pub(crate) fn dock_show_header(&self, id: DockWidgetId) -> bool {
1593 self.0
1594 .borrow()
1595 .docks
1596 .get(&id)
1597 .map(|m| m.show_header)
1598 .unwrap_or(false)
1599 }
1600
1601 pub fn export_state(&self) -> super::state::DockLayoutState {
1607 use super::state::*;
1608 let inner = self.0.borrow();
1609 let side_state = |side: DockSide| -> DockSideState {
1610 let Some(st) = inner.sides.get(&side) else {
1611 return DockSideState::default();
1612 };
1613 DockSideState {
1614 presentation: st.presentation,
1615 size_px: st.size,
1616 visible: st.visible,
1617 selected_tab: st.selected_tab,
1618 tabs: st
1619 .tabs
1620 .iter()
1621 .map(|tab| DockTabState {
1622 id: tab.id.raw(),
1623 splitter: tab.splitter.export_state(),
1624 panes: tab.panes.iter().map(|d| d.raw()).collect(),
1625 hidden: tab.hidden,
1626 })
1627 .collect(),
1628 rail_size: st
1629 .rail_size_sig
1630 .get()
1631 .min(DockRailItemSize::Labeled as usize),
1632 tab_display: st.tab_display_sig.get(),
1633 }
1634 };
1635 DockLayoutState {
1636 version: DockLayoutState::CURRENT_VERSION,
1637 leading: side_state(DockSide::Leading),
1638 trailing: side_state(DockSide::Trailing),
1639 top: side_state(DockSide::Top),
1640 bottom: side_state(DockSide::Bottom),
1641 corners: inner.corners,
1642 }
1643 }
1644
1645 pub fn import_state(&self, state: &super::state::DockLayoutState) {
1648 use super::state::*;
1649 {
1650 let mut inner = self.0.borrow_mut();
1651 inner.corners = state.corners;
1652 let known: std::collections::HashSet<u64> =
1653 inner.docks.keys().map(|k| k.raw()).collect();
1654
1655 let restore = |inner: &mut Inner, side: DockSide, dto: &DockSideState| {
1656 let orientation = side_orientation(side);
1657 let mut tabs: Vec<DockTab> = Vec::new();
1658 for tab_dto in &dto.tabs {
1659 let mut panes: Vec<DockWidgetId> = Vec::new();
1660 for dock in &tab_dto.panes {
1661 if known.contains(dock) {
1662 panes.push(DockWidgetId::from_raw(*dock));
1663 }
1664 }
1665 if !panes.is_empty() {
1666 let splitter = SplitterModel::new(panes.len(), orientation);
1667 if splitter.pane_count() == tab_dto.panes.len() {
1670 splitter.import_state(&tab_dto.splitter);
1671 }
1672 tabs.push(DockTab {
1673 id: DockTabId::from_raw(tab_dto.id),
1674 title: None,
1675 icon: None,
1676 splitter,
1677 panes,
1678 hidden: tab_dto.hidden,
1679 });
1680 }
1681 }
1682 let selected_tab = if tabs.is_empty() {
1688 0
1689 } else {
1690 let clamped = dto.selected_tab.min(tabs.len() - 1);
1691 Self::nearest_visible_tab(&tabs, clamped).unwrap_or(clamped)
1692 };
1693 if let Some(st) = inner.sides.get_mut(&side) {
1694 st.presentation = dto.presentation;
1695 st.size = dto.size_px;
1696 st.visible = dto.visible && !tabs.is_empty();
1697 st.selected_tab = selected_tab;
1698 st.tabs = tabs;
1699 st.rail_size_sig
1700 .set(dto.rail_size.min(DockRailItemSize::Labeled as usize));
1701 st.tab_display_sig.set(dto.tab_display.min(2));
1702 }
1703 };
1704
1705 restore(&mut inner, DockSide::Leading, &state.leading);
1706 restore(&mut inner, DockSide::Trailing, &state.trailing);
1707 restore(&mut inner, DockSide::Top, &state.top);
1708 restore(&mut inner, DockSide::Bottom, &state.bottom);
1709
1710 inner.locations.clear();
1711 for side in DockSide::ALL {
1712 Self::reindex_side(&mut inner, side);
1713 }
1714 }
1715 self.notify();
1716 }
1717}
1718
1719#[cfg(test)]
1720mod tests {
1721 use super::*;
1722 use teksilo_i18n::lit;
1723
1724 fn model() -> DockingModel {
1725 DockingModel::new()
1726 }
1727
1728 fn reg(m: &DockingModel, side: DockSide) -> DockWidgetId {
1729 let id = DockWidgetId::fresh();
1730 m.register_meta(
1731 id,
1732 DockWidgetMeta {
1733 title: lit!("Dock"),
1734 icon: None,
1735 min_size: None,
1736 default: DockOpenLocation::side(side),
1737 header_actions: None,
1738 show_header: false,
1739 },
1740 );
1741 id
1742 }
1743
1744 #[test]
1745 fn open_dock_places_and_shows_side() {
1746 let m = model();
1747 let id = reg(&m, DockSide::Leading);
1748 assert!(!m.is_side_visible(DockSide::Leading));
1749 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
1750 assert!(m.is_dock_open(id));
1751 assert!(m.is_side_visible(DockSide::Leading));
1752 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Leading);
1753 }
1754
1755 #[test]
1756 fn stack_adds_a_splitter_pane_to_one_tab() {
1757 let m = model();
1758 let a = reg(&m, DockSide::Leading);
1759 let b = reg(&m, DockSide::Leading);
1760 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1761 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1762 let tabs = m.side_tabs(DockSide::Leading);
1763 assert_eq!(tabs.len(), 1, "stacking shares one tab");
1764 assert_eq!(tabs[0].panes, vec![a, b], "stacking splits into two panes");
1765 assert_eq!(tabs[0].splitter.pane_count(), 2);
1766 }
1767
1768 #[test]
1769 fn new_tab_creates_a_second_tab() {
1770 let m = model();
1771 let a = reg(&m, DockSide::Bottom);
1772 let b = reg(&m, DockSide::Bottom);
1773 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
1774 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
1775 assert_eq!(m.tab_count(DockSide::Bottom), 2);
1776 }
1777
1778 #[test]
1779 fn single_location_invariant_no_duplicate() {
1780 let m = model();
1781 let id = reg(&m, DockSide::Leading);
1782 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
1783 m.open_dock(id, DockOpenLocation::side(DockSide::Trailing));
1784 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Trailing);
1786 assert!(
1787 !m.is_side_visible(DockSide::Leading),
1788 "leading emptied → hidden"
1789 );
1790 }
1791
1792 #[test]
1793 fn close_last_dock_hides_side() {
1794 let m = model();
1795 let id = reg(&m, DockSide::Trailing);
1796 m.open_dock(id, DockOpenLocation::side(DockSide::Trailing));
1797 m.close_dock(id);
1798 assert!(!m.is_dock_open(id));
1799 assert!(!m.is_side_visible(DockSide::Trailing));
1800 assert_eq!(m.tab_count(DockSide::Trailing), 0);
1801 }
1802
1803 #[test]
1804 fn close_one_of_two_stacked_panes_leaves_the_other() {
1805 let m = model();
1806 let a = reg(&m, DockSide::Leading);
1807 let b = reg(&m, DockSide::Leading);
1808 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1809 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1810 m.close_dock(b);
1811 let tabs = m.side_tabs(DockSide::Leading);
1813 assert_eq!(tabs[0].panes, vec![a]);
1814 assert_eq!(tabs[0].splitter.pane_count(), 1);
1815 }
1816
1817 #[test]
1818 fn toggle_dock_round_trips() {
1819 let m = model();
1820 let id = reg(&m, DockSide::Leading);
1821 m.toggle_dock(id);
1822 assert!(m.is_dock_open(id));
1823 m.toggle_dock(id);
1824 assert!(!m.is_dock_open(id));
1825 }
1826
1827 #[test]
1828 fn reveal_closed_dock_opens_on_default_side() {
1829 let m = model();
1830 let id = reg(&m, DockSide::Bottom);
1831 m.reveal_dock(id);
1832 assert!(m.is_dock_open(id));
1833 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Bottom);
1834 }
1835
1836 #[test]
1844 fn revealing_an_already_revealed_dock_does_not_notify() {
1845 let m = model();
1846 let id = reg(&m, DockSide::Bottom);
1847 m.reveal_dock(id);
1848 assert!(m.is_dock_open(id));
1849
1850 let settled = m.version().get();
1851 m.reveal_dock(id);
1852 assert_eq!(
1853 m.version().get(),
1854 settled,
1855 "nothing changed, so nothing downstream should be rebuilt"
1856 );
1857 }
1858
1859 #[test]
1861 fn revealing_a_hidden_dock_notifies() {
1862 let m = model();
1863 let id = reg(&m, DockSide::Bottom);
1864 m.reveal_dock(id);
1865 m.set_side_visible(DockSide::Bottom, false);
1866
1867 let settled = m.version().get();
1868 m.reveal_dock(id);
1869 assert_ne!(
1870 m.version().get(),
1871 settled,
1872 "it was hidden, so this is a change"
1873 );
1874 }
1875
1876 #[test]
1877 fn split_into_tab_adds_a_splitter_pane() {
1878 let m = model();
1879 let a = reg(&m, DockSide::Bottom);
1880 let b = reg(&m, DockSide::Bottom);
1881 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
1882 m.split_into_tab(b, DockSide::Bottom, 0, 0, false);
1883 let tabs = m.side_tabs(DockSide::Bottom);
1884 assert_eq!(tabs[0].panes.len(), 2, "split added a second pane");
1885 assert_eq!(tabs[0].splitter.pane_count(), 2);
1886 }
1887
1888 #[test]
1889 fn promote_to_tab_makes_its_own_tab() {
1890 let m = model();
1891 let a = reg(&m, DockSide::Leading);
1892 let b = reg(&m, DockSide::Leading);
1893 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1894 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1895 m.promote_to_tab(b, DockSide::Leading, 1);
1897 assert_eq!(m.tab_count(DockSide::Leading), 2);
1898 assert_eq!(m.dock_location(b).unwrap().tab_idx, 1);
1899 }
1900
1901 #[test]
1902 fn move_tab_relocates_whole_tab_and_reorients() {
1903 let m = model();
1904 let a = reg(&m, DockSide::Leading);
1905 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1906 let tab_id = m.side_tabs(DockSide::Leading)[0].id;
1907 let splitter = m.side_tabs(DockSide::Leading)[0].splitter.clone();
1909 assert_eq!(splitter.orientation(), Orientation::Vertical);
1910 m.move_tab(tab_id, DockSide::Bottom, 0);
1911 assert_eq!(m.dock_location(a).unwrap().side, DockSide::Bottom);
1912 assert!(!m.is_side_visible(DockSide::Leading));
1913 assert_eq!(
1914 splitter.orientation(),
1915 Orientation::Horizontal,
1916 "re-derived"
1917 );
1918 }
1919
1920 #[test]
1921 fn closing_a_tab_before_the_active_one_keeps_the_active_tab() {
1922 let m = model();
1926 let a = reg(&m, DockSide::Leading);
1927 let b = reg(&m, DockSide::Leading);
1928 let c = reg(&m, DockSide::Leading);
1929 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1930 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1931 m.open_dock(c, DockOpenLocation::side(DockSide::Leading).new_tab());
1932 let tab_a = m.side_tabs(DockSide::Leading)[0].id;
1933 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1934 m.select_tab(DockSide::Leading, 1);
1935 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
1936
1937 m.close_tab(tab_a);
1938
1939 assert_eq!(m.tab_count(DockSide::Leading), 2);
1940 assert_eq!(m.side_selected_tab(DockSide::Leading), 0, "B followed down");
1941 assert_eq!(
1942 m.side_tabs(DockSide::Leading)[0].id,
1943 tab_b,
1944 "the selected tab is still B, not C",
1945 );
1946 }
1947
1948 #[test]
1949 fn moving_a_tab_before_the_active_one_away_keeps_the_active_tab() {
1950 let m = model();
1954 let a = reg(&m, DockSide::Leading);
1955 let b = reg(&m, DockSide::Leading);
1956 let c = reg(&m, DockSide::Leading);
1957 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1958 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1959 m.open_dock(c, DockOpenLocation::side(DockSide::Leading).new_tab());
1960 let tab_a = m.side_tabs(DockSide::Leading)[0].id;
1961 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1962 m.select_tab(DockSide::Leading, 1);
1963
1964 m.move_tab(tab_a, DockSide::Trailing, 0);
1965
1966 assert_eq!(m.tab_count(DockSide::Leading), 2);
1967 assert_eq!(m.side_selected_tab(DockSide::Leading), 0, "B followed down");
1968 assert_eq!(m.side_tabs(DockSide::Leading)[0].id, tab_b);
1969 }
1970
1971 #[test]
1972 fn closing_the_last_active_tab_clamps_selection() {
1973 let m = model();
1975 let a = reg(&m, DockSide::Leading);
1976 let b = reg(&m, DockSide::Leading);
1977 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1978 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1979 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1980 m.select_tab(DockSide::Leading, 1);
1981
1982 m.close_tab(tab_b);
1983
1984 assert_eq!(m.tab_count(DockSide::Leading), 1);
1985 assert_eq!(m.side_selected_tab(DockSide::Leading), 0);
1986 }
1987
1988 #[test]
1989 fn version_bumps_on_mutation() {
1990 let m = model();
1991 let id = reg(&m, DockSide::Leading);
1992 let v = m.version();
1993 let before = v.get();
1994 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
1995 assert!(v.get() > before);
1996 }
1997
1998 #[test]
1999 fn dock_open_signal_and_side_visible_signal_track() {
2000 let m = model();
2001 let id = reg(&m, DockSide::Leading);
2002 let open = m.dock_open_signal(id);
2003 let vis = m.side_visible_signal(DockSide::Leading);
2004 assert!(!open.get());
2005 assert!(!vis.get());
2006 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
2007 assert!(open.get());
2008 assert!(vis.get());
2009 m.close_dock(id);
2010 assert!(!open.get());
2011 assert!(!vis.get());
2012 }
2013
2014 #[test]
2015 fn export_import_round_trips() {
2016 let m = model();
2017 let a = reg(&m, DockSide::Leading);
2018 let b = reg(&m, DockSide::Bottom);
2019 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2020 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom));
2021 let state = m.export_state();
2022
2023 let m2 = DockingModel::new();
2025 m2.register_meta(
2026 a,
2027 DockWidgetMeta {
2028 title: lit!("A"),
2029 icon: None,
2030 min_size: None,
2031 default: DockOpenLocation::side(DockSide::Leading),
2032 header_actions: None,
2033 show_header: false,
2034 },
2035 );
2036 m2.register_meta(
2037 b,
2038 DockWidgetMeta {
2039 title: lit!("B"),
2040 icon: None,
2041 min_size: None,
2042 default: DockOpenLocation::side(DockSide::Bottom),
2043 header_actions: None,
2044 show_header: false,
2045 },
2046 );
2047 m2.import_state(&state);
2048 assert!(m2.is_dock_open(a));
2049 assert!(m2.is_dock_open(b));
2050 assert_eq!(m2.dock_location(a).unwrap().side, DockSide::Leading);
2051 }
2052
2053 #[test]
2054 fn import_drops_unknown_dock_ids() {
2055 let m = model();
2056 let a = reg(&m, DockSide::Leading);
2057 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2058 let state = m.export_state();
2059 let m2 = DockingModel::new();
2061 m2.import_state(&state);
2062 assert!(!m2.is_dock_open(a), "unknown dock id dropped on import");
2063 assert!(!m2.is_side_visible(DockSide::Leading));
2064 }
2065
2066 #[test]
2069 fn split_a_dock_onto_its_own_pane_is_a_noop() {
2070 let m = model();
2073 let a = reg(&m, DockSide::Bottom);
2074 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
2075 let loc_before = m.dock_location(a).unwrap();
2076 let v = m.version().get();
2077
2078 m.split_into_tab(a, DockSide::Bottom, 0, 0, true);
2079 assert_eq!(m.version().get(), v, "no-op must not bump the version");
2080 assert_eq!(
2081 m.dock_location(a).unwrap(),
2082 loc_before,
2083 "location unchanged"
2084 );
2085 assert_eq!(
2086 m.side_tabs(DockSide::Bottom)[0].panes.len(),
2087 1,
2088 "still one pane"
2089 );
2090
2091 m.split_into_tab(a, DockSide::Bottom, 0, 0, false);
2092 assert_eq!(m.version().get(), v, "after-split self-drop also a no-op");
2093 assert_eq!(m.side_tabs(DockSide::Bottom)[0].panes.len(), 1);
2094 }
2095
2096 #[test]
2097 fn stack_a_dock_onto_its_own_sole_tab_is_a_noop() {
2098 let m = model();
2099 let a = reg(&m, DockSide::Leading);
2100 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2101 let v = m.version().get();
2102 m.stack_into_tab(a, DockSide::Leading, 0);
2103 assert_eq!(m.version().get(), v, "stacking a dock on itself is a no-op");
2104 let tabs = m.side_tabs(DockSide::Leading);
2105 assert_eq!(tabs[0].panes, vec![a], "still a single pane");
2106 }
2107
2108 #[test]
2109 fn stack_into_tab_appends_a_splitter_pane() {
2110 let m = model();
2111 let a = reg(&m, DockSide::Leading);
2112 let b = reg(&m, DockSide::Leading);
2113 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2114 m.stack_into_tab(b, DockSide::Leading, 0);
2115 let tabs = m.side_tabs(DockSide::Leading);
2116 assert_eq!(tabs.len(), 1, "stacked into the same tab");
2117 assert_eq!(tabs[0].panes, vec![a, b], "appended as a second pane");
2118 assert_eq!(tabs[0].splitter.pane_count(), 2);
2119 }
2120
2121 #[test]
2122 fn promote_a_dock_already_its_own_sole_tab_is_a_noop() {
2123 let m = model();
2124 let a = reg(&m, DockSide::Bottom);
2125 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2126 let v = m.version().get();
2127 m.promote_to_tab(a, DockSide::Bottom, 0);
2128 assert_eq!(
2129 m.version().get(),
2130 v,
2131 "promoting an already-sole tab is a no-op"
2132 );
2133 assert_eq!(m.tab_count(DockSide::Bottom), 1);
2134 }
2135
2136 #[test]
2137 fn split_targets_the_right_pane_after_an_earlier_pane_is_pruned() {
2138 let m = model();
2142 let a = reg(&m, DockSide::Bottom);
2143 let b = reg(&m, DockSide::Bottom);
2144 let c = reg(&m, DockSide::Bottom);
2145 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2146 m.split_into_tab(b, DockSide::Bottom, 0, 0, false); m.split_into_tab(c, DockSide::Bottom, 0, 1, false); m.split_into_tab(a, DockSide::Bottom, 0, 1, false); assert_eq!(
2152 m.side_tabs(DockSide::Bottom)[0].panes.len(),
2153 3,
2154 "still 3 panes"
2155 );
2156 assert_eq!(m.dock_location(b).unwrap().pane_idx, 0, "B leads");
2157 assert_eq!(m.dock_location(a).unwrap().pane_idx, 1, "A lands after B");
2158 assert_eq!(m.dock_location(c).unwrap().pane_idx, 2, "C trails");
2159 }
2160
2161 #[test]
2162 fn stack_into_a_later_tab_after_an_earlier_tab_is_pruned_keeps_the_dock() {
2163 let m = model();
2167 let a = reg(&m, DockSide::Bottom);
2168 let b = reg(&m, DockSide::Bottom);
2169 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2170 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2171 assert_eq!(m.tab_count(DockSide::Bottom), 2);
2172
2173 m.stack_into_tab(a, DockSide::Bottom, 1); assert!(m.is_dock_open(a), "A must not be lost");
2176 assert_eq!(m.tab_count(DockSide::Bottom), 1, "A's old tab was pruned");
2177 assert_eq!(
2178 m.side_tabs(DockSide::Bottom)[0].panes,
2179 vec![b, a],
2180 "A appended to B's tab"
2181 );
2182 }
2183
2184 #[test]
2187 fn set_tab_hidden_hides_and_restores_an_activity() {
2188 let m = model();
2189 let a = reg(&m, DockSide::Leading);
2190 let b = reg(&m, DockSide::Leading);
2191 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2192 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2193 let tabs = m.side_tabs(DockSide::Leading);
2194 let (ta, tb) = (tabs[0].id, tabs[1].id);
2195
2196 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 2);
2197 m.set_tab_hidden(ta, true);
2198 assert!(m.is_tab_hidden(ta));
2199 assert_eq!(
2200 m.side_visible_tab_count(DockSide::Leading),
2201 1,
2202 "hidden activity drops out of the visible count"
2203 );
2204 assert_eq!(m.tab_count(DockSide::Leading), 2);
2206 m.set_tab_hidden(ta, false);
2208 assert!(!m.is_tab_hidden(ta));
2209 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 2);
2210 let _ = tb;
2211 }
2212
2213 #[test]
2214 fn hiding_the_selected_tab_moves_selection_to_a_visible_one() {
2215 let m = model();
2216 let a = reg(&m, DockSide::Bottom);
2217 let b = reg(&m, DockSide::Bottom);
2218 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2219 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2220 let tabs = m.side_tabs(DockSide::Bottom);
2221 let (ta, tb) = (tabs[0].id, tabs[1].id);
2222
2223 m.select_tab_by_id(DockSide::Bottom, tb);
2224 assert_eq!(m.side_selected_tab(DockSide::Bottom), 1);
2225 m.set_tab_hidden(tb, true);
2227 assert_eq!(
2228 m.side_selected_tab(DockSide::Bottom),
2229 0,
2230 "selection moved off the hidden tab"
2231 );
2232 let _ = ta;
2233 }
2234
2235 #[test]
2236 fn select_tab_by_id_is_position_independent() {
2237 let m = model();
2238 let a = reg(&m, DockSide::Leading);
2239 let b = reg(&m, DockSide::Leading);
2240 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2241 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2242 let id_b = m.side_tabs(DockSide::Leading)[1].id;
2243 m.select_tab_by_id(DockSide::Leading, id_b);
2244 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
2245 }
2246
2247 #[test]
2248 fn rail_size_and_tab_display_round_trip_reactively() {
2249 use crate::docking::{DockRailItemSize, DockTabDisplay};
2250 let m = model();
2251 assert_eq!(
2253 m.side_rail_size(DockSide::Leading),
2254 DockRailItemSize::Default
2255 );
2256 assert_eq!(m.side_tab_display(DockSide::Leading), DockTabDisplay::Text);
2257
2258 let rail_sig = m.rail_size_signal(DockSide::Leading);
2260 let disp_sig = m.tab_display_signal(DockSide::Leading);
2261
2262 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Compact);
2263 assert_eq!(
2264 m.side_rail_size(DockSide::Leading),
2265 DockRailItemSize::Compact
2266 );
2267 assert_eq!(
2268 rail_sig.get(),
2269 1,
2270 "signal reflects the change (drives rebuild)"
2271 );
2272 assert!(!DockRailItemSize::Compact.shows_label());
2273
2274 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2276 assert_eq!(
2277 m.side_rail_size(DockSide::Leading),
2278 DockRailItemSize::Labeled
2279 );
2280 assert_eq!(rail_sig.get(), 2, "labeled mode drives a rebuild too");
2281 assert!(DockRailItemSize::Labeled.shows_label());
2282 assert!(!DockRailItemSize::Default.shows_label());
2283
2284 m.set_side_tab_display(DockSide::Leading, DockTabDisplay::IconText);
2285 assert_eq!(
2286 m.side_tab_display(DockSide::Leading),
2287 DockTabDisplay::IconText
2288 );
2289 assert_eq!(disp_sig.get(), 2);
2290 }
2291
2292 #[test]
2293 fn rail_size_mode_signal_tracks_the_mode() {
2294 use crate::docking::DockRailItemSize;
2295 let m = model();
2296 let sig = m.rail_size_mode_signal(DockSide::Leading);
2298 assert_eq!(sig.get(), DockRailItemSize::Default);
2299
2300 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Compact);
2301 assert_eq!(sig.get(), DockRailItemSize::Compact, "mode signal fires");
2302 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2303 assert_eq!(sig.get(), DockRailItemSize::Labeled);
2304 }
2305
2306 #[test]
2307 fn export_import_round_trips_hidden_and_display_prefs() {
2308 use crate::docking::{DockRailItemSize, DockTabDisplay};
2309 let m = model();
2310 let a = reg(&m, DockSide::Leading);
2311 let b = reg(&m, DockSide::Leading);
2312 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2313 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2314 let tb = m.side_tabs(DockSide::Leading)[1].id;
2315 m.set_tab_hidden(tb, true);
2316 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2317 m.set_side_tab_display(DockSide::Leading, DockTabDisplay::Icon);
2318
2319 let state = m.export_state();
2320
2321 let m2 = model();
2324 for id in [a, b] {
2325 m2.register_meta(
2326 id,
2327 DockWidgetMeta {
2328 title: lit!("Dock"),
2329 icon: None,
2330 min_size: None,
2331 default: DockOpenLocation::side(DockSide::Leading),
2332 header_actions: None,
2333 show_header: false,
2334 },
2335 );
2336 }
2337 m2.import_state(&state);
2338
2339 assert!(m2.is_tab_hidden(tb), "hidden activity restored");
2340 assert_eq!(
2341 m2.side_rail_size(DockSide::Leading),
2342 DockRailItemSize::Labeled
2343 );
2344 assert_eq!(m2.side_tab_display(DockSide::Leading), DockTabDisplay::Icon);
2345 }
2346
2347 #[test]
2350 fn hiding_the_only_visible_tab_resets_selection_to_zero() {
2351 let m = model();
2356 let a = reg(&m, DockSide::Leading);
2357 let b = reg(&m, DockSide::Leading);
2358 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2359 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2360 let (ta, tb) = {
2361 let t = m.side_tabs(DockSide::Leading);
2362 (t[0].id, t[1].id)
2363 };
2364 m.set_tab_hidden(ta, true);
2366 m.select_tab_by_id(DockSide::Leading, tb);
2367 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
2368 m.set_tab_hidden(tb, true);
2369 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 0);
2370 assert_eq!(
2371 m.side_selected_tab(DockSide::Leading),
2372 0,
2373 "selection resets to a coherent index when no tab is visible"
2374 );
2375 }
2376
2377 #[test]
2378 fn import_state_clamps_selection_off_a_hidden_tab() {
2379 let m = model();
2383 let a = reg(&m, DockSide::Bottom);
2384 let b = reg(&m, DockSide::Bottom);
2385 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2386 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2387 let tb = m.side_tabs(DockSide::Bottom)[1].id;
2390 m.set_tab_hidden(tb, true);
2391 let mut state = m.export_state();
2392 state.bottom.selected_tab = 1; let m2 = model();
2395 for id in [a, b] {
2396 m2.register_meta(
2397 id,
2398 DockWidgetMeta {
2399 title: lit!("Dock"),
2400 icon: None,
2401 min_size: None,
2402 default: DockOpenLocation::side(DockSide::Bottom),
2403 header_actions: None,
2404 show_header: false,
2405 },
2406 );
2407 }
2408 m2.import_state(&state);
2409 assert_eq!(
2410 m2.side_selected_tab(DockSide::Bottom),
2411 0,
2412 "selection clamped onto the visible tab, not the hidden one"
2413 );
2414 }
2415
2416 #[test]
2419 fn side_append_index_lands_after_last_visible_tab() {
2420 let m = model();
2421 let a = reg(&m, DockSide::Bottom);
2422 let b = reg(&m, DockSide::Bottom);
2423 let c = reg(&m, DockSide::Bottom);
2424 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2425 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2426 m.open_dock(c, DockOpenLocation::side(DockSide::Bottom).new_tab());
2427 let (tb, tc) = {
2429 let t = m.side_tabs(DockSide::Bottom);
2430 (t[1].id, t[2].id)
2431 };
2432 m.set_tab_hidden(tb, true);
2433 m.set_tab_hidden(tc, true);
2434 assert_eq!(m.side_append_index(DockSide::Bottom), 1);
2437 }
2438
2439 #[test]
2440 fn move_tab_appends_before_trailing_hidden_tabs() {
2441 let m = model();
2444 let a = reg(&m, DockSide::Leading);
2445 let b = reg(&m, DockSide::Leading);
2446 let mover = reg(&m, DockSide::Trailing);
2447 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2448 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2449 m.open_dock(mover, DockOpenLocation::side(DockSide::Trailing).new_tab());
2450 let tb = m.side_tabs(DockSide::Leading)[1].id;
2452 m.set_tab_hidden(tb, true);
2453 let mover_tab = m.side_tabs(DockSide::Trailing)[0].id;
2455 m.move_tab(
2456 mover_tab,
2457 DockSide::Leading,
2458 m.side_append_index(DockSide::Leading),
2459 );
2460 m.set_tab_hidden(tb, false);
2462 let order: Vec<_> = m
2463 .side_tabs(DockSide::Leading)
2464 .iter()
2465 .map(|t| t.panes[0])
2466 .collect();
2467 assert_eq!(order, vec![a, mover, b], "moved tab sits before hidden b");
2468 }
2469
2470 #[test]
2473 fn set_tab_title_overrides_the_derived_label() {
2474 let m = model();
2475 let a = reg(&m, DockSide::Leading);
2476 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2477 let tab = m.side_tabs(DockSide::Leading)[0].id;
2478 assert_eq!(m.activity_of(a), Some(tab));
2479 assert!(m.tab_title(tab).is_none());
2480 m.set_dock_activity_title(a, lit!("Source Control"));
2481 assert_eq!(
2482 m.tab_title(tab).unwrap().resolve_now(),
2483 "Source Control",
2484 "explicit activity title set via the dock id"
2485 );
2486 let view = m.side_tabs(DockSide::Leading)[0].clone();
2487 assert_eq!(m.activity_label(&view).resolve_now(), "Source Control");
2488 }
2489
2490 #[test]
2491 fn activity_label_follows_first_non_collapsed_pane() {
2492 let m = model();
2495 let a = DockWidgetId::fresh();
2496 let b = DockWidgetId::fresh();
2497 m.register_meta(
2498 a,
2499 DockWidgetMeta {
2500 title: lit!("Alpha"),
2501 icon: None,
2502 min_size: None,
2503 default: DockOpenLocation::side(DockSide::Leading),
2504 header_actions: None,
2505 show_header: false,
2506 },
2507 );
2508 m.register_meta(
2509 b,
2510 DockWidgetMeta {
2511 title: lit!("Beta"),
2512 icon: None,
2513 min_size: None,
2514 default: DockOpenLocation::side(DockSide::Leading),
2515 header_actions: None,
2516 show_header: false,
2517 },
2518 );
2519 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2520 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
2521 let view = m.side_tabs(DockSide::Leading)[0].clone();
2522 assert_eq!(m.activity_label(&view).resolve_now(), "Alpha");
2523 view.splitter.set_collapsed(0, true);
2525 let view = m.side_tabs(DockSide::Leading)[0].clone();
2526 assert_eq!(
2527 m.activity_label(&view).resolve_now(),
2528 "Beta",
2529 "label follows the first non-collapsed pane"
2530 );
2531 }
2532
2533 #[test]
2536 fn enabled_move_targets_excludes_self_and_disabled() {
2537 let m = model();
2538 assert_eq!(
2539 m.enabled_move_targets(DockSide::Leading),
2540 vec![DockSide::Trailing, DockSide::Top, DockSide::Bottom]
2541 );
2542 m.set_side_enabled(DockSide::Bottom, false);
2543 assert_eq!(
2544 m.enabled_move_targets(DockSide::Leading),
2545 vec![DockSide::Trailing, DockSide::Top],
2546 "a disabled side is never a move target"
2547 );
2548 m.set_side_enabled(DockSide::Top, false);
2549 m.set_side_enabled(DockSide::Trailing, false);
2550 assert!(
2551 m.enabled_move_targets(DockSide::Leading).is_empty(),
2552 "no enabled target → empty (the Move-to entry is then omitted)"
2553 );
2554 }
2555
2556 #[test]
2557 fn tab_hidden_signal_tracks_live_state() {
2558 let m = model();
2559 let a = reg(&m, DockSide::Leading);
2560 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2561 let tab = m.side_tabs(DockSide::Leading)[0].id;
2562 let sig = m.tab_hidden_signal(tab);
2563 assert!(!sig.get());
2564 m.set_tab_hidden(tab, true);
2565 assert!(sig.get(), "signal follows an external set_tab_hidden");
2566 m.set_tab_hidden(tab, false);
2567 assert!(!sig.get());
2568 }
2569}