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() {
1677 let splitter = SplitterModel::new(panes.len(), orientation);
1678 if splitter.pane_count() == tab_dto.panes.len() {
1681 splitter.import_state(&tab_dto.splitter);
1682 }
1683 tabs.push(DockTab {
1684 id: DockTabId::from_raw(tab_dto.id),
1685 title: None,
1686 icon: None,
1687 splitter,
1688 panes,
1689 hidden: tab_dto.hidden,
1690 });
1691 }
1692 }
1693 let selected_tab = if tabs.is_empty() {
1699 0
1700 } else {
1701 let clamped = dto.selected_tab.min(tabs.len() - 1);
1702 Self::nearest_visible_tab(&tabs, clamped).unwrap_or(clamped)
1703 };
1704 if let Some(st) = inner.sides.get_mut(&side) {
1705 st.presentation = dto.presentation;
1706 st.size = dto.size_px;
1707 st.visible = dto.visible && !tabs.is_empty();
1708 st.selected_tab = selected_tab;
1709 st.tabs = tabs;
1710 st.rail_size_sig
1711 .set(dto.rail_size.min(DockRailItemSize::Labeled as usize));
1712 st.tab_display_sig.set(dto.tab_display.min(2));
1713 }
1714 };
1715
1716 restore(&mut inner, DockSide::Leading, &state.leading);
1717 restore(&mut inner, DockSide::Trailing, &state.trailing);
1718 restore(&mut inner, DockSide::Top, &state.top);
1719 restore(&mut inner, DockSide::Bottom, &state.bottom);
1720
1721 inner.locations.clear();
1722 for side in DockSide::ALL {
1723 Self::reindex_side(&mut inner, side);
1724 }
1725 }
1726 self.notify();
1727 }
1728}
1729
1730#[cfg(test)]
1731mod tests {
1732 use super::*;
1733 use teksilo_i18n::lit;
1734
1735 fn model() -> DockingModel {
1736 DockingModel::new()
1737 }
1738
1739 fn reg(m: &DockingModel, side: DockSide) -> DockWidgetId {
1740 let id = DockWidgetId::fresh();
1741 m.register_meta(
1742 id,
1743 DockWidgetMeta {
1744 title: lit!("Dock"),
1745 icon: None,
1746 min_size: None,
1747 default: DockOpenLocation::side(side),
1748 header_actions: None,
1749 show_header: false,
1750 },
1751 );
1752 id
1753 }
1754
1755 #[test]
1756 fn open_dock_places_and_shows_side() {
1757 let m = model();
1758 let id = reg(&m, DockSide::Leading);
1759 assert!(!m.is_side_visible(DockSide::Leading));
1760 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
1761 assert!(m.is_dock_open(id));
1762 assert!(m.is_side_visible(DockSide::Leading));
1763 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Leading);
1764 }
1765
1766 #[test]
1767 fn stack_adds_a_splitter_pane_to_one_tab() {
1768 let m = model();
1769 let a = reg(&m, DockSide::Leading);
1770 let b = reg(&m, DockSide::Leading);
1771 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1772 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1773 let tabs = m.side_tabs(DockSide::Leading);
1774 assert_eq!(tabs.len(), 1, "stacking shares one tab");
1775 assert_eq!(tabs[0].panes, vec![a, b], "stacking splits into two panes");
1776 assert_eq!(tabs[0].splitter.pane_count(), 2);
1777 }
1778
1779 #[test]
1780 fn new_tab_creates_a_second_tab() {
1781 let m = model();
1782 let a = reg(&m, DockSide::Bottom);
1783 let b = reg(&m, DockSide::Bottom);
1784 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
1785 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
1786 assert_eq!(m.tab_count(DockSide::Bottom), 2);
1787 }
1788
1789 #[test]
1790 fn single_location_invariant_no_duplicate() {
1791 let m = model();
1792 let id = reg(&m, DockSide::Leading);
1793 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
1794 m.open_dock(id, DockOpenLocation::side(DockSide::Trailing));
1795 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Trailing);
1797 assert!(
1798 !m.is_side_visible(DockSide::Leading),
1799 "leading emptied → hidden"
1800 );
1801 }
1802
1803 #[test]
1804 fn close_last_dock_hides_side() {
1805 let m = model();
1806 let id = reg(&m, DockSide::Trailing);
1807 m.open_dock(id, DockOpenLocation::side(DockSide::Trailing));
1808 m.close_dock(id);
1809 assert!(!m.is_dock_open(id));
1810 assert!(!m.is_side_visible(DockSide::Trailing));
1811 assert_eq!(m.tab_count(DockSide::Trailing), 0);
1812 }
1813
1814 #[test]
1815 fn close_one_of_two_stacked_panes_leaves_the_other() {
1816 let m = model();
1817 let a = reg(&m, DockSide::Leading);
1818 let b = reg(&m, DockSide::Leading);
1819 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1820 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1821 m.close_dock(b);
1822 let tabs = m.side_tabs(DockSide::Leading);
1824 assert_eq!(tabs[0].panes, vec![a]);
1825 assert_eq!(tabs[0].splitter.pane_count(), 1);
1826 }
1827
1828 #[test]
1829 fn toggle_dock_round_trips() {
1830 let m = model();
1831 let id = reg(&m, DockSide::Leading);
1832 m.toggle_dock(id);
1833 assert!(m.is_dock_open(id));
1834 m.toggle_dock(id);
1835 assert!(!m.is_dock_open(id));
1836 }
1837
1838 #[test]
1839 fn reveal_closed_dock_opens_on_default_side() {
1840 let m = model();
1841 let id = reg(&m, DockSide::Bottom);
1842 m.reveal_dock(id);
1843 assert!(m.is_dock_open(id));
1844 assert_eq!(m.dock_location(id).unwrap().side, DockSide::Bottom);
1845 }
1846
1847 #[test]
1855 fn revealing_an_already_revealed_dock_does_not_notify() {
1856 let m = model();
1857 let id = reg(&m, DockSide::Bottom);
1858 m.reveal_dock(id);
1859 assert!(m.is_dock_open(id));
1860
1861 let settled = m.version().get();
1862 m.reveal_dock(id);
1863 assert_eq!(
1864 m.version().get(),
1865 settled,
1866 "nothing changed, so nothing downstream should be rebuilt"
1867 );
1868 }
1869
1870 #[test]
1872 fn revealing_a_hidden_dock_notifies() {
1873 let m = model();
1874 let id = reg(&m, DockSide::Bottom);
1875 m.reveal_dock(id);
1876 m.set_side_visible(DockSide::Bottom, false);
1877
1878 let settled = m.version().get();
1879 m.reveal_dock(id);
1880 assert_ne!(
1881 m.version().get(),
1882 settled,
1883 "it was hidden, so this is a change"
1884 );
1885 }
1886
1887 #[test]
1888 fn split_into_tab_adds_a_splitter_pane() {
1889 let m = model();
1890 let a = reg(&m, DockSide::Bottom);
1891 let b = reg(&m, DockSide::Bottom);
1892 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
1893 m.split_into_tab(b, DockSide::Bottom, 0, 0, false);
1894 let tabs = m.side_tabs(DockSide::Bottom);
1895 assert_eq!(tabs[0].panes.len(), 2, "split added a second pane");
1896 assert_eq!(tabs[0].splitter.pane_count(), 2);
1897 }
1898
1899 #[test]
1900 fn promote_to_tab_makes_its_own_tab() {
1901 let m = model();
1902 let a = reg(&m, DockSide::Leading);
1903 let b = reg(&m, DockSide::Leading);
1904 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1905 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
1906 m.promote_to_tab(b, DockSide::Leading, 1);
1908 assert_eq!(m.tab_count(DockSide::Leading), 2);
1909 assert_eq!(m.dock_location(b).unwrap().tab_idx, 1);
1910 }
1911
1912 #[test]
1913 fn move_tab_relocates_whole_tab_and_reorients() {
1914 let m = model();
1915 let a = reg(&m, DockSide::Leading);
1916 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1917 let tab_id = m.side_tabs(DockSide::Leading)[0].id;
1918 let splitter = m.side_tabs(DockSide::Leading)[0].splitter.clone();
1920 assert_eq!(splitter.orientation(), Orientation::Vertical);
1921 m.move_tab(tab_id, DockSide::Bottom, 0);
1922 assert_eq!(m.dock_location(a).unwrap().side, DockSide::Bottom);
1923 assert!(!m.is_side_visible(DockSide::Leading));
1924 assert_eq!(
1925 splitter.orientation(),
1926 Orientation::Horizontal,
1927 "re-derived"
1928 );
1929 }
1930
1931 #[test]
1932 fn closing_a_tab_before_the_active_one_keeps_the_active_tab() {
1933 let m = model();
1937 let a = reg(&m, DockSide::Leading);
1938 let b = reg(&m, DockSide::Leading);
1939 let c = reg(&m, DockSide::Leading);
1940 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1941 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1942 m.open_dock(c, DockOpenLocation::side(DockSide::Leading).new_tab());
1943 let tab_a = m.side_tabs(DockSide::Leading)[0].id;
1944 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1945 m.select_tab(DockSide::Leading, 1);
1946 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
1947
1948 m.close_tab(tab_a);
1949
1950 assert_eq!(m.tab_count(DockSide::Leading), 2);
1951 assert_eq!(m.side_selected_tab(DockSide::Leading), 0, "B followed down");
1952 assert_eq!(
1953 m.side_tabs(DockSide::Leading)[0].id,
1954 tab_b,
1955 "the selected tab is still B, not C",
1956 );
1957 }
1958
1959 #[test]
1960 fn moving_a_tab_before_the_active_one_away_keeps_the_active_tab() {
1961 let m = model();
1965 let a = reg(&m, DockSide::Leading);
1966 let b = reg(&m, DockSide::Leading);
1967 let c = reg(&m, DockSide::Leading);
1968 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1969 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1970 m.open_dock(c, DockOpenLocation::side(DockSide::Leading).new_tab());
1971 let tab_a = m.side_tabs(DockSide::Leading)[0].id;
1972 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1973 m.select_tab(DockSide::Leading, 1);
1974
1975 m.move_tab(tab_a, DockSide::Trailing, 0);
1976
1977 assert_eq!(m.tab_count(DockSide::Leading), 2);
1978 assert_eq!(m.side_selected_tab(DockSide::Leading), 0, "B followed down");
1979 assert_eq!(m.side_tabs(DockSide::Leading)[0].id, tab_b);
1980 }
1981
1982 #[test]
1983 fn closing_the_last_active_tab_clamps_selection() {
1984 let m = model();
1986 let a = reg(&m, DockSide::Leading);
1987 let b = reg(&m, DockSide::Leading);
1988 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
1989 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
1990 let tab_b = m.side_tabs(DockSide::Leading)[1].id;
1991 m.select_tab(DockSide::Leading, 1);
1992
1993 m.close_tab(tab_b);
1994
1995 assert_eq!(m.tab_count(DockSide::Leading), 1);
1996 assert_eq!(m.side_selected_tab(DockSide::Leading), 0);
1997 }
1998
1999 #[test]
2000 fn version_bumps_on_mutation() {
2001 let m = model();
2002 let id = reg(&m, DockSide::Leading);
2003 let v = m.version();
2004 let before = v.get();
2005 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
2006 assert!(v.get() > before);
2007 }
2008
2009 #[test]
2010 fn dock_open_signal_and_side_visible_signal_track() {
2011 let m = model();
2012 let id = reg(&m, DockSide::Leading);
2013 let open = m.dock_open_signal(id);
2014 let vis = m.side_visible_signal(DockSide::Leading);
2015 assert!(!open.get());
2016 assert!(!vis.get());
2017 m.open_dock(id, DockOpenLocation::side(DockSide::Leading));
2018 assert!(open.get());
2019 assert!(vis.get());
2020 m.close_dock(id);
2021 assert!(!open.get());
2022 assert!(!vis.get());
2023 }
2024
2025 #[test]
2026 fn export_import_round_trips() {
2027 let m = model();
2028 let a = reg(&m, DockSide::Leading);
2029 let b = reg(&m, DockSide::Bottom);
2030 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2031 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom));
2032 let state = m.export_state();
2033
2034 let m2 = DockingModel::new();
2036 m2.register_meta(
2037 a,
2038 DockWidgetMeta {
2039 title: lit!("A"),
2040 icon: None,
2041 min_size: None,
2042 default: DockOpenLocation::side(DockSide::Leading),
2043 header_actions: None,
2044 show_header: false,
2045 },
2046 );
2047 m2.register_meta(
2048 b,
2049 DockWidgetMeta {
2050 title: lit!("B"),
2051 icon: None,
2052 min_size: None,
2053 default: DockOpenLocation::side(DockSide::Bottom),
2054 header_actions: None,
2055 show_header: false,
2056 },
2057 );
2058 m2.import_state(&state);
2059 assert!(m2.is_dock_open(a));
2060 assert!(m2.is_dock_open(b));
2061 assert_eq!(m2.dock_location(a).unwrap().side, DockSide::Leading);
2062 }
2063
2064 #[test]
2065 fn import_drops_unknown_dock_ids() {
2066 let m = model();
2067 let a = reg(&m, DockSide::Leading);
2068 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2069 let state = m.export_state();
2070 let m2 = DockingModel::new();
2072 m2.import_state(&state);
2073 assert!(!m2.is_dock_open(a), "unknown dock id dropped on import");
2074 assert!(!m2.is_side_visible(DockSide::Leading));
2075 }
2076
2077 #[test]
2080 fn split_a_dock_onto_its_own_pane_is_a_noop() {
2081 let m = model();
2084 let a = reg(&m, DockSide::Bottom);
2085 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom));
2086 let loc_before = m.dock_location(a).unwrap();
2087 let v = m.version().get();
2088
2089 m.split_into_tab(a, DockSide::Bottom, 0, 0, true);
2090 assert_eq!(m.version().get(), v, "no-op must not bump the version");
2091 assert_eq!(
2092 m.dock_location(a).unwrap(),
2093 loc_before,
2094 "location unchanged"
2095 );
2096 assert_eq!(
2097 m.side_tabs(DockSide::Bottom)[0].panes.len(),
2098 1,
2099 "still one pane"
2100 );
2101
2102 m.split_into_tab(a, DockSide::Bottom, 0, 0, false);
2103 assert_eq!(m.version().get(), v, "after-split self-drop also a no-op");
2104 assert_eq!(m.side_tabs(DockSide::Bottom)[0].panes.len(), 1);
2105 }
2106
2107 #[test]
2108 fn stack_a_dock_onto_its_own_sole_tab_is_a_noop() {
2109 let m = model();
2110 let a = reg(&m, DockSide::Leading);
2111 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2112 let v = m.version().get();
2113 m.stack_into_tab(a, DockSide::Leading, 0);
2114 assert_eq!(m.version().get(), v, "stacking a dock on itself is a no-op");
2115 let tabs = m.side_tabs(DockSide::Leading);
2116 assert_eq!(tabs[0].panes, vec![a], "still a single pane");
2117 }
2118
2119 #[test]
2120 fn stack_into_tab_appends_a_splitter_pane() {
2121 let m = model();
2122 let a = reg(&m, DockSide::Leading);
2123 let b = reg(&m, DockSide::Leading);
2124 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2125 m.stack_into_tab(b, DockSide::Leading, 0);
2126 let tabs = m.side_tabs(DockSide::Leading);
2127 assert_eq!(tabs.len(), 1, "stacked into the same tab");
2128 assert_eq!(tabs[0].panes, vec![a, b], "appended as a second pane");
2129 assert_eq!(tabs[0].splitter.pane_count(), 2);
2130 }
2131
2132 #[test]
2133 fn promote_a_dock_already_its_own_sole_tab_is_a_noop() {
2134 let m = model();
2135 let a = reg(&m, DockSide::Bottom);
2136 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2137 let v = m.version().get();
2138 m.promote_to_tab(a, DockSide::Bottom, 0);
2139 assert_eq!(
2140 m.version().get(),
2141 v,
2142 "promoting an already-sole tab is a no-op"
2143 );
2144 assert_eq!(m.tab_count(DockSide::Bottom), 1);
2145 }
2146
2147 #[test]
2148 fn split_targets_the_right_pane_after_an_earlier_pane_is_pruned() {
2149 let m = model();
2153 let a = reg(&m, DockSide::Bottom);
2154 let b = reg(&m, DockSide::Bottom);
2155 let c = reg(&m, DockSide::Bottom);
2156 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2157 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!(
2163 m.side_tabs(DockSide::Bottom)[0].panes.len(),
2164 3,
2165 "still 3 panes"
2166 );
2167 assert_eq!(m.dock_location(b).unwrap().pane_idx, 0, "B leads");
2168 assert_eq!(m.dock_location(a).unwrap().pane_idx, 1, "A lands after B");
2169 assert_eq!(m.dock_location(c).unwrap().pane_idx, 2, "C trails");
2170 }
2171
2172 #[test]
2173 fn stack_into_a_later_tab_after_an_earlier_tab_is_pruned_keeps_the_dock() {
2174 let m = model();
2178 let a = reg(&m, DockSide::Bottom);
2179 let b = reg(&m, DockSide::Bottom);
2180 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2181 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2182 assert_eq!(m.tab_count(DockSide::Bottom), 2);
2183
2184 m.stack_into_tab(a, DockSide::Bottom, 1); assert!(m.is_dock_open(a), "A must not be lost");
2187 assert_eq!(m.tab_count(DockSide::Bottom), 1, "A's old tab was pruned");
2188 assert_eq!(
2189 m.side_tabs(DockSide::Bottom)[0].panes,
2190 vec![b, a],
2191 "A appended to B's tab"
2192 );
2193 }
2194
2195 #[test]
2198 fn set_tab_hidden_hides_and_restores_an_activity() {
2199 let m = model();
2200 let a = reg(&m, DockSide::Leading);
2201 let b = reg(&m, DockSide::Leading);
2202 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2203 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2204 let tabs = m.side_tabs(DockSide::Leading);
2205 let (ta, tb) = (tabs[0].id, tabs[1].id);
2206
2207 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 2);
2208 m.set_tab_hidden(ta, true);
2209 assert!(m.is_tab_hidden(ta));
2210 assert_eq!(
2211 m.side_visible_tab_count(DockSide::Leading),
2212 1,
2213 "hidden activity drops out of the visible count"
2214 );
2215 assert_eq!(m.tab_count(DockSide::Leading), 2);
2217 m.set_tab_hidden(ta, false);
2219 assert!(!m.is_tab_hidden(ta));
2220 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 2);
2221 let _ = tb;
2222 }
2223
2224 #[test]
2225 fn hiding_the_selected_tab_moves_selection_to_a_visible_one() {
2226 let m = model();
2227 let a = reg(&m, DockSide::Bottom);
2228 let b = reg(&m, DockSide::Bottom);
2229 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2230 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2231 let tabs = m.side_tabs(DockSide::Bottom);
2232 let (ta, tb) = (tabs[0].id, tabs[1].id);
2233
2234 m.select_tab_by_id(DockSide::Bottom, tb);
2235 assert_eq!(m.side_selected_tab(DockSide::Bottom), 1);
2236 m.set_tab_hidden(tb, true);
2238 assert_eq!(
2239 m.side_selected_tab(DockSide::Bottom),
2240 0,
2241 "selection moved off the hidden tab"
2242 );
2243 let _ = ta;
2244 }
2245
2246 #[test]
2247 fn select_tab_by_id_is_position_independent() {
2248 let m = model();
2249 let a = reg(&m, DockSide::Leading);
2250 let b = reg(&m, DockSide::Leading);
2251 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2252 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2253 let id_b = m.side_tabs(DockSide::Leading)[1].id;
2254 m.select_tab_by_id(DockSide::Leading, id_b);
2255 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
2256 }
2257
2258 #[test]
2259 fn rail_size_and_tab_display_round_trip_reactively() {
2260 use crate::docking::{DockRailItemSize, DockTabDisplay};
2261 let m = model();
2262 assert_eq!(
2264 m.side_rail_size(DockSide::Leading),
2265 DockRailItemSize::Default
2266 );
2267 assert_eq!(m.side_tab_display(DockSide::Leading), DockTabDisplay::Text);
2268
2269 let rail_sig = m.rail_size_signal(DockSide::Leading);
2271 let disp_sig = m.tab_display_signal(DockSide::Leading);
2272
2273 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Compact);
2274 assert_eq!(
2275 m.side_rail_size(DockSide::Leading),
2276 DockRailItemSize::Compact
2277 );
2278 assert_eq!(
2279 rail_sig.get(),
2280 1,
2281 "signal reflects the change (drives rebuild)"
2282 );
2283 assert!(!DockRailItemSize::Compact.shows_label());
2284
2285 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2287 assert_eq!(
2288 m.side_rail_size(DockSide::Leading),
2289 DockRailItemSize::Labeled
2290 );
2291 assert_eq!(rail_sig.get(), 2, "labeled mode drives a rebuild too");
2292 assert!(DockRailItemSize::Labeled.shows_label());
2293 assert!(!DockRailItemSize::Default.shows_label());
2294
2295 m.set_side_tab_display(DockSide::Leading, DockTabDisplay::IconText);
2296 assert_eq!(
2297 m.side_tab_display(DockSide::Leading),
2298 DockTabDisplay::IconText
2299 );
2300 assert_eq!(disp_sig.get(), 2);
2301 }
2302
2303 #[test]
2304 fn rail_size_mode_signal_tracks_the_mode() {
2305 use crate::docking::DockRailItemSize;
2306 let m = model();
2307 let sig = m.rail_size_mode_signal(DockSide::Leading);
2309 assert_eq!(sig.get(), DockRailItemSize::Default);
2310
2311 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Compact);
2312 assert_eq!(sig.get(), DockRailItemSize::Compact, "mode signal fires");
2313 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2314 assert_eq!(sig.get(), DockRailItemSize::Labeled);
2315 }
2316
2317 #[test]
2318 fn export_import_round_trips_hidden_and_display_prefs() {
2319 use crate::docking::{DockRailItemSize, DockTabDisplay};
2320 let m = model();
2321 let a = reg(&m, DockSide::Leading);
2322 let b = reg(&m, DockSide::Leading);
2323 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2324 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2325 let tb = m.side_tabs(DockSide::Leading)[1].id;
2326 m.set_tab_hidden(tb, true);
2327 m.set_side_rail_size(DockSide::Leading, DockRailItemSize::Labeled);
2328 m.set_side_tab_display(DockSide::Leading, DockTabDisplay::Icon);
2329
2330 let state = m.export_state();
2331
2332 let m2 = model();
2335 for id in [a, b] {
2336 m2.register_meta(
2337 id,
2338 DockWidgetMeta {
2339 title: lit!("Dock"),
2340 icon: None,
2341 min_size: None,
2342 default: DockOpenLocation::side(DockSide::Leading),
2343 header_actions: None,
2344 show_header: false,
2345 },
2346 );
2347 }
2348 m2.import_state(&state);
2349
2350 assert!(m2.is_tab_hidden(tb), "hidden activity restored");
2351 assert_eq!(
2352 m2.side_rail_size(DockSide::Leading),
2353 DockRailItemSize::Labeled
2354 );
2355 assert_eq!(m2.side_tab_display(DockSide::Leading), DockTabDisplay::Icon);
2356 }
2357
2358 #[test]
2361 fn hiding_the_only_visible_tab_resets_selection_to_zero() {
2362 let m = model();
2367 let a = reg(&m, DockSide::Leading);
2368 let b = reg(&m, DockSide::Leading);
2369 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2370 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2371 let (ta, tb) = {
2372 let t = m.side_tabs(DockSide::Leading);
2373 (t[0].id, t[1].id)
2374 };
2375 m.set_tab_hidden(ta, true);
2377 m.select_tab_by_id(DockSide::Leading, tb);
2378 assert_eq!(m.side_selected_tab(DockSide::Leading), 1);
2379 m.set_tab_hidden(tb, true);
2380 assert_eq!(m.side_visible_tab_count(DockSide::Leading), 0);
2381 assert_eq!(
2382 m.side_selected_tab(DockSide::Leading),
2383 0,
2384 "selection resets to a coherent index when no tab is visible"
2385 );
2386 }
2387
2388 #[test]
2389 fn import_state_clamps_selection_off_a_hidden_tab() {
2390 let m = model();
2394 let a = reg(&m, DockSide::Bottom);
2395 let b = reg(&m, DockSide::Bottom);
2396 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2397 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2398 let tb = m.side_tabs(DockSide::Bottom)[1].id;
2401 m.set_tab_hidden(tb, true);
2402 let mut state = m.export_state();
2403 state.bottom.selected_tab = 1; let m2 = model();
2406 for id in [a, b] {
2407 m2.register_meta(
2408 id,
2409 DockWidgetMeta {
2410 title: lit!("Dock"),
2411 icon: None,
2412 min_size: None,
2413 default: DockOpenLocation::side(DockSide::Bottom),
2414 header_actions: None,
2415 show_header: false,
2416 },
2417 );
2418 }
2419 m2.import_state(&state);
2420 assert_eq!(
2421 m2.side_selected_tab(DockSide::Bottom),
2422 0,
2423 "selection clamped onto the visible tab, not the hidden one"
2424 );
2425 }
2426
2427 #[test]
2430 fn side_append_index_lands_after_last_visible_tab() {
2431 let m = model();
2432 let a = reg(&m, DockSide::Bottom);
2433 let b = reg(&m, DockSide::Bottom);
2434 let c = reg(&m, DockSide::Bottom);
2435 m.open_dock(a, DockOpenLocation::side(DockSide::Bottom).new_tab());
2436 m.open_dock(b, DockOpenLocation::side(DockSide::Bottom).new_tab());
2437 m.open_dock(c, DockOpenLocation::side(DockSide::Bottom).new_tab());
2438 let (tb, tc) = {
2440 let t = m.side_tabs(DockSide::Bottom);
2441 (t[1].id, t[2].id)
2442 };
2443 m.set_tab_hidden(tb, true);
2444 m.set_tab_hidden(tc, true);
2445 assert_eq!(m.side_append_index(DockSide::Bottom), 1);
2448 }
2449
2450 #[test]
2451 fn move_tab_appends_before_trailing_hidden_tabs() {
2452 let m = model();
2455 let a = reg(&m, DockSide::Leading);
2456 let b = reg(&m, DockSide::Leading);
2457 let mover = reg(&m, DockSide::Trailing);
2458 m.open_dock(a, DockOpenLocation::side(DockSide::Leading).new_tab());
2459 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).new_tab());
2460 m.open_dock(mover, DockOpenLocation::side(DockSide::Trailing).new_tab());
2461 let tb = m.side_tabs(DockSide::Leading)[1].id;
2463 m.set_tab_hidden(tb, true);
2464 let mover_tab = m.side_tabs(DockSide::Trailing)[0].id;
2466 m.move_tab(
2467 mover_tab,
2468 DockSide::Leading,
2469 m.side_append_index(DockSide::Leading),
2470 );
2471 m.set_tab_hidden(tb, false);
2473 let order: Vec<_> = m
2474 .side_tabs(DockSide::Leading)
2475 .iter()
2476 .map(|t| t.panes[0])
2477 .collect();
2478 assert_eq!(order, vec![a, mover, b], "moved tab sits before hidden b");
2479 }
2480
2481 #[test]
2484 fn set_tab_title_overrides_the_derived_label() {
2485 let m = model();
2486 let a = reg(&m, DockSide::Leading);
2487 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2488 let tab = m.side_tabs(DockSide::Leading)[0].id;
2489 assert_eq!(m.activity_of(a), Some(tab));
2490 assert!(m.tab_title(tab).is_none());
2491 m.set_dock_activity_title(a, lit!("Source Control"));
2492 assert_eq!(
2493 m.tab_title(tab).unwrap().resolve_now(),
2494 "Source Control",
2495 "explicit activity title set via the dock id"
2496 );
2497 let view = m.side_tabs(DockSide::Leading)[0].clone();
2498 assert_eq!(m.activity_label(&view).resolve_now(), "Source Control");
2499 }
2500
2501 #[test]
2502 fn activity_label_follows_first_non_collapsed_pane() {
2503 let m = model();
2506 let a = DockWidgetId::fresh();
2507 let b = DockWidgetId::fresh();
2508 m.register_meta(
2509 a,
2510 DockWidgetMeta {
2511 title: lit!("Alpha"),
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.register_meta(
2520 b,
2521 DockWidgetMeta {
2522 title: lit!("Beta"),
2523 icon: None,
2524 min_size: None,
2525 default: DockOpenLocation::side(DockSide::Leading),
2526 header_actions: None,
2527 show_header: false,
2528 },
2529 );
2530 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2531 m.open_dock(b, DockOpenLocation::side(DockSide::Leading).stack());
2532 let view = m.side_tabs(DockSide::Leading)[0].clone();
2533 assert_eq!(m.activity_label(&view).resolve_now(), "Alpha");
2534 view.splitter.set_collapsed(0, true);
2536 let view = m.side_tabs(DockSide::Leading)[0].clone();
2537 assert_eq!(
2538 m.activity_label(&view).resolve_now(),
2539 "Beta",
2540 "label follows the first non-collapsed pane"
2541 );
2542 }
2543
2544 #[test]
2547 fn enabled_move_targets_excludes_self_and_disabled() {
2548 let m = model();
2549 assert_eq!(
2550 m.enabled_move_targets(DockSide::Leading),
2551 vec![DockSide::Trailing, DockSide::Top, DockSide::Bottom]
2552 );
2553 m.set_side_enabled(DockSide::Bottom, false);
2554 assert_eq!(
2555 m.enabled_move_targets(DockSide::Leading),
2556 vec![DockSide::Trailing, DockSide::Top],
2557 "a disabled side is never a move target"
2558 );
2559 m.set_side_enabled(DockSide::Top, false);
2560 m.set_side_enabled(DockSide::Trailing, false);
2561 assert!(
2562 m.enabled_move_targets(DockSide::Leading).is_empty(),
2563 "no enabled target → empty (the Move-to entry is then omitted)"
2564 );
2565 }
2566
2567 #[test]
2568 fn tab_hidden_signal_tracks_live_state() {
2569 let m = model();
2570 let a = reg(&m, DockSide::Leading);
2571 m.open_dock(a, DockOpenLocation::side(DockSide::Leading));
2572 let tab = m.side_tabs(DockSide::Leading)[0].id;
2573 let sig = m.tab_hidden_signal(tab);
2574 assert!(!sig.get());
2575 m.set_tab_hidden(tab, true);
2576 assert!(sig.get(), "signal follows an external set_tab_hidden");
2577 m.set_tab_hidden(tab, false);
2578 assert!(!sig.get());
2579 }
2580}