1use crate::TestSupportExt as _;
4use std::{rc::Rc, sync::Arc};
5
6use gpui::{
7 AnyElement, AnyView, App, Bounds, Context, Div, DragMoveEvent, Empty, EventEmitter,
8 FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Pixels,
9 Render, Stateful, Styled as _, WeakEntity, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::Placement;
13
14use super::{
15 active::ActiveTracker,
16 drag::{
17 AnyDrag, DragPanel, DropIndicator, DropPlaceholderBounds, DropTarget, ITEM_DRAG_SESSION_ID,
18 split_placement_at,
19 },
20 layout::{InsertTarget, NodeId, PanelId},
21 panel::PanelView,
22};
23
24#[non_exhaustive]
31pub enum TabGroupEvent {
32 Drop {
35 panel: PanelId,
36 source: NodeId,
37 target: InsertTarget,
38 },
39 DragDrop { item: AnyDrag, target: DropTarget },
41 ClosePanel { panel: PanelId },
43 ActiveChanged { ix: usize },
45 ZoomIn,
49 ZoomOut,
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
63pub struct TabGroupConstraints {
64 alone: bool,
65 dock_locked: bool,
66 collapsed: bool,
67 closable: bool,
68}
69
70impl TabGroupConstraints {
71 pub fn sealed() -> Self {
74 Self {
75 alone: true,
76 dock_locked: true,
77 collapsed: false,
78 closable: false,
79 }
80 }
81
82 pub fn in_split(alone: bool) -> Self {
86 Self {
87 alone,
88 dock_locked: false,
89 collapsed: false,
90 closable: true,
91 }
92 }
93
94 pub fn dock_locked(mut self, dock_locked: bool) -> Self {
96 self.dock_locked = dock_locked;
97 self
98 }
99
100 pub fn collapsed(mut self, collapsed: bool) -> Self {
102 self.collapsed = collapsed;
103 self
104 }
105
106 pub fn closable(mut self, closable: bool) -> Self {
109 self.closable = closable;
110 self
111 }
112
113 pub fn is_alone(&self) -> bool {
115 self.alone
116 }
117
118 pub fn is_locked(&self) -> bool {
125 self.dock_locked
126 }
127
128 pub fn is_collapsed(&self) -> bool {
129 self.collapsed
130 }
131
132 pub fn is_closable(&self) -> bool {
133 self.closable
134 }
135}
136
137pub struct TabGroup {
148 node: NodeId,
149 this: WeakEntity<Self>,
152 panels: Vec<Arc<dyn PanelView>>,
153 active_ix: usize,
154 zoomed: bool,
155 constraints: TabGroupConstraints,
156 focus_handle: FocusHandle,
157 active: ActiveTracker,
158 drop_indicator: Option<DropIndicator>,
159 renderer: Rc<dyn TabGroupRenderer>,
160}
161
162impl TabGroup {
163 pub(crate) fn new(node: NodeId, _window: &mut Window, cx: &mut Context<Self>) -> Self {
167 Self {
168 node,
169 this: cx.weak_entity(),
170 panels: Vec::new(),
171 active_ix: 0,
172 zoomed: false,
173 constraints: TabGroupConstraints::sealed(),
175 focus_handle: cx.focus_handle(),
176 active: ActiveTracker::default(),
177 drop_indicator: None,
178 renderer: Rc::new(BareTabGroup),
179 }
180 }
181
182 pub fn with_renderer(mut self, renderer: Rc<dyn TabGroupRenderer>) -> Self {
183 self.renderer = renderer;
184 self
185 }
186
187 pub fn node(&self) -> NodeId {
189 self.node
190 }
191
192 pub fn panels(&self) -> &[Arc<dyn PanelView>] {
193 &self.panels
194 }
195
196 pub fn active_ix(&self) -> usize {
197 self.active_ix
198 }
199
200 pub fn active_panel(&self, cx: &App) -> Option<Arc<dyn PanelView>> {
204 match self.panels.get(self.active_ix) {
205 Some(panel) if panel.visible(cx) => Some(panel.clone()),
206 Some(_) => self.visible_panels(cx).next(),
207 None => None,
208 }
209 }
210
211 pub fn is_zoomed(&self) -> bool {
212 self.zoomed
213 }
214
215 pub fn is_collapsed(&self) -> bool {
216 self.constraints.is_collapsed()
217 }
218
219 pub fn is_closable(&self, cx: &App) -> bool {
225 self.constraints.is_closable()
226 && self.draggable(cx)
227 && self
228 .active_panel(cx)
229 .is_some_and(|panel| panel.closable(cx))
230 }
231
232 pub fn select_tab(&mut self, ix: usize, window: &mut Window, cx: &mut Context<Self>) {
234 if ix >= self.panels.len() || ix == self.active_ix {
235 return;
236 }
237
238 self.active_ix = ix;
239 self.focus_active_panel(window, cx);
240 self.schedule_active_sync(window, cx);
241 cx.emit(TabGroupEvent::ActiveChanged { ix });
242 cx.notify();
243 }
244
245 pub fn close_panel(&mut self, panel: PanelId, cx: &mut Context<Self>) {
248 if !self.constraints.is_closable() {
249 return;
250 }
251 if !self.draggable(cx) {
253 return;
254 }
255
256 let closable = self
257 .panels
258 .iter()
259 .any(|candidate| candidate.panel_id(cx) == panel && candidate.closable(cx));
260 if !closable {
261 return;
262 }
263
264 cx.emit(TabGroupEvent::ClosePanel { panel });
265 cx.notify();
266 }
267
268 pub fn toggle_zoom(&mut self, window: &mut Window, cx: &mut Context<Self>) {
269 self.set_zoomed(!self.zoomed, window, cx);
270 }
271
272 pub fn context(&self, cx: &App) -> TabGroupContext {
274 let group = self.this.clone();
275
276 TabGroupContext {
277 node: self.node,
278 panels: self.panels.clone(),
279 active_panel: self.active_panel(cx),
280 active_ix: self.active_ix,
281 zoomed: self.zoomed,
282 collapsed: self.constraints.is_collapsed(),
283 closable: self.is_closable(cx),
284 locked: self.is_locked(),
285 draggable: self.draggable(cx),
286 droppable: self.droppable(),
287 drop_indicator: cx
290 .has_active_drag()
291 .then_some(self.drop_indicator)
292 .flatten(),
293 on_select_tab: {
294 let group = group.clone();
295 Rc::new(move |ix, window, cx| {
296 _ = group.update(cx, |group, cx| group.select_tab(ix, window, cx));
297 })
298 },
299 on_close: {
300 let group = group.clone();
301 Rc::new(move |panel, _, cx| {
302 _ = group.update(cx, |group, cx| group.close_panel(panel, cx));
303 })
304 },
305 on_toggle_zoom: {
306 let group = group.clone();
307 Rc::new(move |window, cx| {
308 _ = group.update(cx, |group, cx| group.toggle_zoom(window, cx));
309 })
310 },
311 on_drop_panel: {
312 let group = group.clone();
313 Rc::new(move |drag, ix, activate, _, cx| {
314 _ = group.update(cx, |group, cx| group.on_drop(&drag, ix, activate, cx));
315 })
316 },
317 on_drop_item: Rc::new(move |item, placement, _, cx| {
318 _ = group.update(cx, |group, cx| group.emit_drag_drop(&item, placement, cx));
319 }),
320 }
321 }
322}
323
324impl TabGroup {
328 pub(crate) fn sync_from_tree(
332 &mut self,
333 panels: Vec<Arc<dyn PanelView>>,
334 active_ix: usize,
335 window: &mut Window,
336 cx: &mut Context<Self>,
337 ) {
338 let group = cx.weak_entity();
339 let existing: Vec<PanelId> = self.panels.iter().map(|panel| panel.panel_id(cx)).collect();
340 for panel in panels.iter() {
341 if !existing.contains(&panel.panel_id(cx)) {
342 panel.on_added_to(group.clone(), window, cx);
343 }
344 }
345
346 self.panels = panels;
347 self.active_ix = active_ix.min(self.panels.len().saturating_sub(1));
348 self.schedule_active_sync(window, cx);
349 cx.notify();
350 }
351
352 pub(crate) fn set_constraints(
361 &mut self,
362 constraints: TabGroupConstraints,
363 window: &mut Window,
364 cx: &mut Context<Self>,
365 ) {
366 if self.constraints == constraints {
367 return;
368 }
369
370 let collapse_changed = self.constraints.is_collapsed() != constraints.is_collapsed();
373 self.constraints = constraints;
374 if collapse_changed {
375 self.schedule_active_sync(window, cx);
376 }
377 cx.notify();
378 }
379
380 pub(crate) fn set_zoomed(&mut self, zoomed: bool, window: &mut Window, cx: &mut Context<Self>) {
395 if self.zoomed == zoomed {
396 return;
397 }
398 let panel = self.active_panel(cx);
399 if zoomed && !panel.as_ref().is_some_and(|panel| panel.zoomable(cx)) {
400 return;
401 }
402
403 self.zoomed = zoomed;
404 cx.emit(if zoomed {
405 TabGroupEvent::ZoomIn
406 } else {
407 TabGroupEvent::ZoomOut
408 });
409
410 if let Some(panel) = panel {
414 cx.spawn_in(window, async move |_, cx| {
415 _ = cx.update(|window, cx| panel.set_zoomed(zoomed, window, cx));
416 })
417 .detach();
418 }
419 cx.notify();
420 }
421
422 pub(crate) fn last_notified_active(&self, panel: PanelId) -> Option<bool> {
425 self.active.last_notified(panel)
426 }
427
428 pub(crate) fn seed_active(&mut self, panel: PanelId, active: bool) {
431 self.active.seed(panel, active);
432 }
433}
434
435impl TabGroup {
436 fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
438 self.panels
439 .iter()
440 .filter(|panel| panel.visible(cx))
441 .cloned()
442 }
443
444 fn is_locked(&self) -> bool {
447 self.constraints.is_locked() || self.zoomed
448 }
449
450 fn is_last_panel(&self, cx: &App) -> bool {
454 self.constraints.is_alone() && self.visible_panels(cx).count() <= 1
455 }
456
457 fn draggable(&self, cx: &App) -> bool {
458 !self.is_locked() && !self.is_last_panel(cx)
459 }
460
461 fn droppable(&self) -> bool {
462 !self.is_locked()
463 }
464
465 fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
466 if let Some(panel) = self.active_panel(cx) {
467 panel.focus_handle(cx).focus(window, cx);
468 }
469 }
470
471 fn schedule_active_sync(&mut self, window: &mut Window, cx: &mut Context<Self>) {
475 if !self.active.schedule_sync() {
476 return;
477 }
478
479 cx.spawn_in(window, async move |group, cx| {
480 _ = cx.update(|window, cx| {
481 let Ok(changes) = group.update(cx, |group, cx| group.reconcile_active(cx)) else {
482 return;
483 };
484 for (panel, active) in changes {
487 panel.set_active(active, window, cx);
488 }
489 });
490 })
491 .detach();
492 }
493
494 fn reconcile_active(&mut self, cx: &App) -> Vec<(Arc<dyn PanelView>, bool)> {
498 self.active.sync_finished();
499
500 let ids: Vec<PanelId> = self.panels.iter().map(|panel| panel.panel_id(cx)).collect();
501 let displayed = match self.constraints.is_collapsed() {
502 true => None,
503 false => ids.get(self.active_ix).copied(),
504 };
505
506 self.active
507 .reconcile(&ids, displayed)
508 .into_iter()
509 .filter_map(|(id, active)| {
510 ids.iter()
511 .position(|candidate| *candidate == id)
512 .map(|ix| (self.panels[ix].clone(), active))
513 })
514 .collect()
515 }
516
517 fn on_panel_drag_move(
520 &mut self,
521 drag: &DragMoveEvent<DragPanel>,
522 _: &mut Window,
523 cx: &mut Context<Self>,
524 ) {
525 let bounds = drag.bounds;
526 if !bounds.contains(&drag.event.position) {
527 self.clear_drop_indicator(cx);
528 return;
529 }
530
531 let placement = split_placement_at(bounds, drag.event.position);
532 let dragged = drag.drag(cx);
533 let source = DropPlaceholderBounds::new(
535 drag.event.position - dragged.drag_offset() - bounds.origin,
536 dragged.preview_size(),
537 );
538
539 self.sync_drop_placeholder(bounds, placement, dragged.drag_session_id(), source, cx);
540 }
541
542 fn on_item_drag_move(
547 &mut self,
548 drag: &DragMoveEvent<AnyDrag>,
549 _: &mut Window,
550 cx: &mut Context<Self>,
551 ) {
552 let bounds = drag.bounds;
553 if !bounds.contains(&drag.event.position) {
554 self.clear_drop_indicator(cx);
555 return;
556 }
557
558 let placement = split_placement_at(bounds, drag.event.position);
559 let source = DropPlaceholderBounds::for_placement(bounds, placement);
560
561 self.sync_drop_placeholder(bounds, placement, ITEM_DRAG_SESSION_ID, source, cx);
562 }
563
564 fn sync_drop_placeholder(
565 &mut self,
566 bounds: Bounds<Pixels>,
567 placement: Option<Placement>,
568 drag_session_id: u64,
569 source: DropPlaceholderBounds,
570 cx: &mut Context<Self>,
571 ) {
572 let to = DropPlaceholderBounds::for_placement(bounds, placement);
573
574 let restart = self.drop_indicator.is_none_or(|indicator| {
575 indicator.drag_session_id() != drag_session_id || indicator.placement() != placement
576 });
577
578 let (from, epoch) = match (restart, self.drop_indicator) {
579 (false, Some(indicator)) => (indicator.from(), indicator.epoch()),
580 (_, Some(indicator)) => {
581 let from = match indicator.drag_session_id() == drag_session_id {
582 true => indicator.to(),
583 false => source,
584 };
585 (from, indicator.epoch().wrapping_add(1))
586 }
587 (_, None) => (source, 0),
588 };
589
590 self.drop_indicator = Some(DropIndicator::new(
591 bounds,
592 placement,
593 from,
594 to,
595 drag_session_id,
596 epoch,
597 ));
598 cx.notify();
599 }
600
601 fn clear_drop_indicator(&mut self, cx: &mut Context<Self>) {
602 if self.drop_indicator.take().is_some() {
603 cx.notify();
604 }
605 }
606
607 fn emit_drag_drop(
610 &mut self,
611 item: &AnyDrag,
612 placement: Option<Placement>,
613 cx: &mut Context<Self>,
614 ) {
615 self.drop_indicator = None;
616 cx.emit(TabGroupEvent::DragDrop {
617 item: item.clone(),
618 target: DropTarget::Group {
619 node: self.node,
620 placement,
621 },
622 });
623 cx.notify();
624 }
625
626 fn on_drop(
633 &mut self,
634 drag: &DragPanel,
635 ix: Option<usize>,
636 activate: bool,
637 cx: &mut Context<Self>,
638 ) {
639 let indicator = self.drop_indicator.take();
640 let placement = match ix {
641 Some(_) => None,
642 None => indicator.and_then(|indicator| indicator.placement()),
643 };
644
645 if drag.source() == self.node
649 && ix.is_none()
650 && (placement.is_none() || self.panels.len() == 1)
651 {
652 cx.notify();
653 return;
654 }
655
656 let target = match placement {
657 Some(placement) => InsertTarget::Split {
658 node: self.node,
659 placement,
660 size: None,
661 },
662 None => InsertTarget::Tabs {
663 node: self.node,
664 ix,
665 activate,
666 },
667 };
668
669 cx.emit(TabGroupEvent::Drop {
670 panel: drag.panel(),
671 source: drag.source(),
672 target,
673 });
674 cx.notify();
675 }
676}
677
678impl EventEmitter<TabGroupEvent> for TabGroup {}
679
680impl Focusable for TabGroup {
681 fn focus_handle(&self, cx: &App) -> FocusHandle {
682 match self.active_panel(cx) {
683 Some(panel) => panel.focus_handle(cx),
684 None => self.focus_handle.clone(),
685 }
686 }
687}
688
689impl Render for TabGroup {
690 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
691 let context = self.context(cx);
692 let renderer = self.renderer.clone();
693 let focus_handle = self.focus_handle(cx);
694 let droppable = context.droppable;
695 let indicator = context.drop_indicator;
696
697 renderer
698 .frame(&context, window, cx)
699 .test_support()
700 .flex()
710 .flex_col()
711 .size_full()
712 .overflow_hidden()
713 .track_focus(&focus_handle)
714 .tab_group()
715 .child(renderer.render_tab_bar(&context, window, cx))
716 .child(
717 renderer
718 .content_frame(&context, window, cx)
719 .test_support()
720 .flex()
724 .flex_col()
725 .when(!context.is_collapsed(), |this| this.flex_1())
726 .min_h(px(0.))
734 .overflow_hidden()
735 .when(droppable, |this| {
742 this.on_drag_move(cx.listener(Self::on_panel_drag_move))
743 .on_drop(cx.listener(|this, drag: &DragPanel, _, cx| {
744 this.on_drop(drag, None, true, cx)
745 }))
746 .on_drag_move(cx.listener(Self::on_item_drag_move))
747 .on_drop(cx.listener(|this, item: &AnyDrag, _, cx| {
748 let placement = this
749 .drop_indicator
750 .and_then(|indicator| indicator.placement());
751 this.emit_drag_drop(item, placement, cx);
752 }))
753 })
754 .map(|this| match context.active_panel.as_ref() {
755 Some(panel) => this.child(renderer.render_active_panel(
756 panel.view(),
757 &context,
758 window,
759 cx,
760 )),
761 None => this.children(renderer.render_empty(&context, window, cx)),
762 })
763 .when_some(indicator, |this, indicator| {
764 this.children(renderer.render_drop_indicator(indicator, window, cx))
765 }),
766 )
767 }
768}
769
770type SelectTabHandler = Rc<dyn Fn(usize, &mut Window, &mut App)>;
771type ClosePanelHandler = Rc<dyn Fn(PanelId, &mut Window, &mut App)>;
772type ToggleZoomHandler = Rc<dyn Fn(&mut Window, &mut App)>;
773type DropPanelHandler = Rc<dyn Fn(DragPanel, Option<usize>, bool, &mut Window, &mut App)>;
774type DropItemHandler = Rc<dyn Fn(AnyDrag, Option<Placement>, &mut Window, &mut App)>;
775
776#[derive(Clone)]
779pub struct TabGroupContext {
780 node: NodeId,
781 panels: Vec<Arc<dyn PanelView>>,
782 active_panel: Option<Arc<dyn PanelView>>,
783 active_ix: usize,
784 zoomed: bool,
785 collapsed: bool,
786 locked: bool,
787 draggable: bool,
788 droppable: bool,
789 closable: bool,
790 drop_indicator: Option<DropIndicator>,
791 on_select_tab: SelectTabHandler,
792 on_close: ClosePanelHandler,
793 on_toggle_zoom: ToggleZoomHandler,
794 on_drop_panel: DropPanelHandler,
795 on_drop_item: DropItemHandler,
796}
797
798impl TabGroupContext {
799 pub fn node(&self) -> NodeId {
802 self.node
803 }
804
805 pub fn panels(&self) -> &[Arc<dyn PanelView>] {
808 &self.panels
809 }
810
811 pub fn active_ix(&self) -> usize {
812 self.active_ix
813 }
814
815 pub fn active_panel(&self) -> Option<&Arc<dyn PanelView>> {
818 self.active_panel.as_ref()
819 }
820
821 pub fn drop_indicator(&self) -> Option<DropIndicator> {
822 self.drop_indicator
823 }
824
825 pub fn is_zoomed(&self) -> bool {
826 self.zoomed
827 }
828
829 pub fn is_collapsed(&self) -> bool {
830 self.collapsed
831 }
832
833 pub fn is_closable(&self) -> bool {
836 self.closable
837 }
838
839 pub fn is_locked(&self) -> bool {
840 self.locked
841 }
842
843 pub fn is_draggable(&self) -> bool {
844 self.draggable
845 }
846
847 pub fn is_droppable(&self) -> bool {
848 self.droppable
849 }
850
851 pub fn select_tab(&self, ix: usize, window: &mut Window, cx: &mut App) {
852 (self.on_select_tab)(ix, window, cx);
853 }
854
855 pub fn close(&self, panel: PanelId, window: &mut Window, cx: &mut App) {
856 (self.on_close)(panel, window, cx);
857 }
858
859 pub fn toggle_zoom(&self, window: &mut Window, cx: &mut App) {
860 (self.on_toggle_zoom)(window, cx);
861 }
862
863 pub fn drag_panel(&self, ix: usize, cx: &App) -> Option<DragPanel> {
866 self.panels
867 .get(ix)
868 .map(|panel| DragPanel::new(panel.panel_id(cx), self.node))
869 }
870
871 pub fn drop_panel(
874 &self,
875 drag: DragPanel,
876 ix: Option<usize>,
877 activate: bool,
878 window: &mut Window,
879 cx: &mut App,
880 ) {
881 (self.on_drop_panel)(drag, ix, activate, window, cx);
882 }
883
884 pub fn drop_item(
887 &self,
888 item: AnyDrag,
889 placement: Option<Placement>,
890 window: &mut Window,
891 cx: &mut App,
892 ) {
893 (self.on_drop_item)(item, placement, window, cx);
894 }
895}
896
897#[allow(unused_variables)]
904pub trait TabGroupRenderer: 'static {
905 fn frame(&self, group: &TabGroupContext, window: &mut Window, cx: &mut App) -> Stateful<Div> {
913 div().id("tab-group")
914 }
915
916 fn content_frame(
918 &self,
919 group: &TabGroupContext,
920 window: &mut Window,
921 cx: &mut App,
922 ) -> Stateful<Div> {
923 div().id("tab-group-content")
924 }
925
926 fn render_tab_bar(
927 &self,
928 group: &TabGroupContext,
929 window: &mut Window,
930 cx: &mut App,
931 ) -> AnyElement;
932
933 fn render_active_panel(
937 &self,
938 panel: AnyView,
939 group: &TabGroupContext,
940 window: &mut Window,
941 cx: &mut App,
942 ) -> AnyElement {
943 panel.into_any_element()
944 }
945
946 fn render_drop_indicator(
947 &self,
948 indicator: DropIndicator,
949 window: &mut Window,
950 cx: &mut App,
951 ) -> Option<AnyElement> {
952 None
953 }
954
955 fn render_empty(
956 &self,
957 group: &TabGroupContext,
958 window: &mut Window,
959 cx: &mut App,
960 ) -> Option<AnyElement> {
961 None
962 }
963}
964
965pub(crate) struct BareTabGroup;
967
968impl TabGroupRenderer for BareTabGroup {
969 fn render_tab_bar(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> AnyElement {
970 Empty.into_any_element()
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use std::cell::RefCell;
977
978 use gpui::{
979 AppContext as _, Entity, Modifiers, MouseButton, StatefulInteractiveElement as _,
980 TestAppContext, VisualTestContext, point, px, size,
981 };
982
983 use super::*;
984 use crate::dock::test_support::{
985 PanelSignal, build_group, drain, drain_active, log_of, panel_id,
986 };
987
988 fn group_node() -> NodeId {
990 NodeId::from_u64(1)
991 }
992
993 fn elsewhere() -> NodeId {
994 NodeId::from_u64(7)
995 }
996
997 fn content_bounds() -> Bounds<Pixels> {
998 Bounds {
999 origin: point(px(0.), px(0.)),
1000 size: size(px(400.), px(300.)),
1001 }
1002 }
1003
1004 fn record_events(
1007 group: &Entity<TabGroup>,
1008 cx: &mut VisualTestContext,
1009 ) -> Rc<RefCell<Vec<String>>> {
1010 let events: Rc<RefCell<Vec<String>>> = Rc::default();
1011 let sink = events.clone();
1012 cx.update(|_, cx| {
1013 cx.subscribe(group, move |_, event: &TabGroupEvent, _| {
1014 sink.borrow_mut().push(describe(event));
1015 })
1016 .detach();
1017 });
1018 events
1019 }
1020
1021 fn describe(event: &TabGroupEvent) -> String {
1022 match event {
1023 TabGroupEvent::Drop {
1024 panel,
1025 source,
1026 target,
1027 } => match target {
1028 InsertTarget::Tabs { node, ix, activate } => format!(
1029 "drop panel {} from {} into tabs {} at {:?} activate={}",
1030 panel.as_u64(),
1031 source.as_u64(),
1032 node.as_u64(),
1033 ix,
1034 activate
1035 ),
1036 InsertTarget::Split {
1037 node, placement, ..
1038 } => format!(
1039 "drop panel {} from {} split {} {}",
1040 panel.as_u64(),
1041 source.as_u64(),
1042 node.as_u64(),
1043 placement
1044 ),
1045 InsertTarget::Tile { .. } => "drop tile".into(),
1046 },
1047 TabGroupEvent::DragDrop { target, .. } => match target {
1048 DropTarget::Group { node, placement } => {
1049 format!("item onto {} at {:?}", node.as_u64(), placement)
1050 }
1051 DropTarget::Canvas => "item onto canvas".into(),
1052 },
1053 TabGroupEvent::ClosePanel { panel } => format!("close {}", panel.as_u64()),
1054 TabGroupEvent::ActiveChanged { ix } => format!("active {ix}"),
1055 TabGroupEvent::ZoomIn => "zoom in".into(),
1056 TabGroupEvent::ZoomOut => "zoom out".into(),
1057 }
1058 }
1059
1060 #[gpui::test]
1061 fn a_group_announces_its_first_panel_active(cx: &mut TestAppContext) {
1062 let log = log_of();
1063 let (group, panels, cx) = build_group(&log, &["a"], cx);
1064 cx.run_until_parked();
1065
1066 assert_eq!(drain_active(&log), vec![("a", true)]);
1067 let _ = (group, panels);
1068 }
1069
1070 #[gpui::test]
1071 fn selecting_another_tab_deactivates_then_activates(cx: &mut TestAppContext) {
1072 let log = log_of();
1073 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1074 cx.run_until_parked();
1075 drain_active(&log);
1076
1077 cx.update(|window, cx| {
1078 group.update(cx, |group, cx| group.select_tab(1, window, cx));
1079 });
1080 cx.run_until_parked();
1081
1082 assert_eq!(drain_active(&log), vec![("a", false), ("b", true)]);
1083 }
1084
1085 #[gpui::test]
1086 fn the_context_snapshots_the_group_state(cx: &mut TestAppContext) {
1087 let log = log_of();
1088 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1089
1090 let seen = cx.update(|_, cx| {
1091 group.update(cx, |group, cx| {
1092 let context = group.context(cx);
1093 (
1094 context.panels().len(),
1095 context.active_ix(),
1096 context.is_zoomed(),
1097 )
1098 })
1099 });
1100
1101 assert_eq!(seen, (2, 0, false));
1102 }
1103
1104 #[gpui::test]
1107 fn a_hidden_displayed_panel_is_still_the_active_one(cx: &mut TestAppContext) {
1108 let log = log_of();
1109 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1110 cx.update(|_, cx| {
1111 panels[0].update(cx, |panel, cx| panel.set_visible(false, cx));
1112 });
1113 cx.run_until_parked();
1114
1115 assert_eq!(drain_active(&log), vec![("a", true)]);
1116 assert_eq!(
1117 cx.update(|_, cx| group.read(cx).active_panel(cx).unwrap().panel_name(cx)),
1118 "b",
1119 "rendering falls back to the first visible panel"
1120 );
1121 }
1122
1123 #[gpui::test]
1126 fn a_panel_dropped_from_the_group_is_never_told_it_went_inactive(cx: &mut TestAppContext) {
1127 let log = log_of();
1128 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1129 cx.run_until_parked();
1130 drain(&log);
1131
1132 cx.update(|window, cx| {
1133 let remaining: Vec<Arc<dyn PanelView>> = vec![Arc::new(panels[1].clone())];
1134 group.update(cx, |group, cx| {
1135 group.sync_from_tree(remaining, 0, window, cx)
1136 });
1137 });
1138 cx.run_until_parked();
1139
1140 assert_eq!(drain_active(&log), vec![("b", true)]);
1141 }
1142
1143 #[gpui::test]
1144 fn selecting_a_tab_that_is_not_there_changes_nothing(cx: &mut TestAppContext) {
1145 let log = log_of();
1146 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1147 cx.run_until_parked();
1148 drain_active(&log);
1149 let events = record_events(&group, cx);
1150
1151 cx.update(|window, cx| {
1152 group.update(cx, |group, cx| group.select_tab(5, window, cx));
1153 });
1154 cx.run_until_parked();
1155
1156 assert_eq!(cx.update(|_, cx| group.read(cx).active_ix()), 0);
1157 assert!(events.borrow().is_empty());
1158 assert!(drain_active(&log).is_empty());
1159 }
1160
1161 #[gpui::test]
1162 fn a_locked_group_can_be_neither_dragged_nor_dropped_into(cx: &mut TestAppContext) {
1163 let log = log_of();
1164 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1165
1166 cx.update(|window, cx| {
1167 group.update(cx, |group, cx| {
1168 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1169 })
1170 });
1171 let unlocked = cx.update(|_, cx| {
1172 let context = group.read(cx).context(cx);
1173 (context.is_draggable(), context.is_droppable())
1174 });
1175 cx.update(|window, cx| {
1176 group.update(cx, |group, cx| {
1177 group.set_constraints(
1178 TabGroupConstraints::in_split(false).dock_locked(true),
1179 window,
1180 cx,
1181 )
1182 })
1183 });
1184 let locked = cx.update(|_, cx| {
1185 let context = group.read(cx).context(cx);
1186 (context.is_draggable(), context.is_droppable())
1187 });
1188
1189 assert_eq!(unlocked, (true, true));
1190 assert_eq!(locked, (false, false));
1191 }
1192
1193 #[gpui::test]
1196 fn a_zoomed_group_takes_no_drops(cx: &mut TestAppContext) {
1197 let log = log_of();
1198 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1199
1200 cx.update(|window, cx| {
1201 group.update(cx, |group, cx| {
1202 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1203 group.set_zoomed(true, window, cx);
1204 })
1205 });
1206
1207 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_droppable()));
1208 }
1209
1210 #[gpui::test]
1213 fn the_only_groups_last_visible_panel_cannot_be_dragged_out(cx: &mut TestAppContext) {
1214 let log = log_of();
1215 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1216
1217 cx.update(|window, cx| {
1218 group.update(cx, |group, cx| {
1219 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1220 })
1221 });
1222 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1223 cx.update(|window, cx| {
1224 group.update(cx, |group, cx| {
1225 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1226 })
1227 });
1228 let beside_others = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1229
1230 assert!(!alone);
1231 assert!(beside_others);
1232 }
1233
1234 #[gpui::test]
1235 fn a_panel_from_another_group_lands_as_one_move(cx: &mut TestAppContext) {
1236 let log = log_of();
1237 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1238 let events = record_events(&group, cx);
1239
1240 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1241 cx.update(|_, cx| {
1242 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1243 });
1244 cx.run_until_parked();
1245
1246 assert_eq!(
1247 *events.borrow(),
1248 vec!["drop panel 99 from 7 into tabs 1 at None activate=true"]
1249 );
1250 }
1251
1252 #[gpui::test]
1253 fn dropping_a_panel_back_onto_its_own_group_is_ignored(cx: &mut TestAppContext) {
1254 let log = log_of();
1255 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1256 let events = record_events(&group, cx);
1257
1258 let panel = panel_id(&panels[0], cx);
1259 let drag = DragPanel::new(panel, group_node());
1260 cx.update(|_, cx| {
1261 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1262 });
1263 cx.run_until_parked();
1264
1265 assert!(events.borrow().is_empty());
1266 }
1267
1268 #[gpui::test]
1269 fn a_hovering_split_turns_a_drop_into_a_split(cx: &mut TestAppContext) {
1270 let log = log_of();
1271 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1272 let events = record_events(&group, cx);
1273
1274 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1275 cx.update(|_, cx| {
1276 group.update(cx, |group, cx| {
1277 group.sync_drop_placeholder(
1278 content_bounds(),
1279 Some(Placement::Right),
1280 drag.drag_session_id(),
1281 DropPlaceholderBounds::for_placement(content_bounds(), None),
1282 cx,
1283 );
1284 group.on_drop(&drag, None, true, cx);
1285 });
1286 });
1287 cx.run_until_parked();
1288
1289 assert_eq!(*events.borrow(), vec!["drop panel 99 from 7 split 1 Right"]);
1290 }
1291
1292 #[gpui::test]
1295 fn a_tab_slot_overrides_the_hovering_split(cx: &mut TestAppContext) {
1296 let log = log_of();
1297 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1298 let events = record_events(&group, cx);
1299
1300 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1301 cx.update(|_, cx| {
1302 group.update(cx, |group, cx| {
1303 group.sync_drop_placeholder(
1304 content_bounds(),
1305 Some(Placement::Right),
1306 drag.drag_session_id(),
1307 DropPlaceholderBounds::for_placement(content_bounds(), None),
1308 cx,
1309 );
1310 group.on_drop(&drag, Some(0), true, cx);
1311 });
1312 });
1313 cx.run_until_parked();
1314
1315 assert_eq!(
1316 *events.borrow(),
1317 vec!["drop panel 99 from 7 into tabs 1 at Some(0) activate=true"]
1318 );
1319 }
1320
1321 #[gpui::test]
1324 fn a_lone_panel_cannot_split_out_of_its_own_group(cx: &mut TestAppContext) {
1325 let log = log_of();
1326 let (group, panels, cx) = build_group(&log, &["a"], cx);
1327 let events = record_events(&group, cx);
1328
1329 let drag = DragPanel::new(panel_id(&panels[0], cx), group_node());
1330 cx.update(|_, cx| {
1331 group.update(cx, |group, cx| {
1332 group.sync_drop_placeholder(
1333 content_bounds(),
1334 Some(Placement::Right),
1335 drag.drag_session_id(),
1336 DropPlaceholderBounds::for_placement(content_bounds(), None),
1337 cx,
1338 );
1339 group.on_drop(&drag, None, true, cx);
1340 });
1341 });
1342 cx.run_until_parked();
1343
1344 assert!(events.borrow().is_empty());
1345 }
1346
1347 #[gpui::test]
1348 fn the_placeholder_replays_only_when_the_target_moves(cx: &mut TestAppContext) {
1349 let log = log_of();
1350 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1351 let bounds = content_bounds();
1352 let source = DropPlaceholderBounds::new(point(px(10.), px(10.)), size(px(96.), px(30.)));
1353
1354 let sync = |placement: Option<Placement>, cx: &mut VisualTestContext| {
1355 cx.update(|_, cx| {
1356 group.update(cx, |group, cx| {
1357 group.sync_drop_placeholder(bounds, placement, 42, source, cx);
1358 group.drop_indicator.unwrap()
1359 })
1360 })
1361 };
1362
1363 let first = sync(Some(Placement::Left), cx);
1364 let again = sync(Some(Placement::Left), cx);
1365 let moved = sync(Some(Placement::Right), cx);
1366
1367 assert_eq!(first.epoch(), 0);
1368 assert_eq!(first.from(), source, "the first run flies in from the drag");
1369 assert_eq!(again.epoch(), 0, "an unchanged target keeps animating");
1370 assert_eq!(again.from(), source);
1371 assert_eq!(moved.epoch(), 1, "a moved target replays from where it was");
1372 assert_eq!(moved.from(), first.to());
1373 assert_eq!(
1374 moved.to(),
1375 DropPlaceholderBounds::for_placement(bounds, Some(Placement::Right))
1376 );
1377 }
1378
1379 #[gpui::test]
1382 fn the_indicator_is_withheld_when_no_drag_is_in_flight(cx: &mut TestAppContext) {
1383 let log = log_of();
1384 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1385
1386 let (stored, published) = cx.update(|_, cx| {
1387 group.update(cx, |group, cx| {
1388 group.sync_drop_placeholder(
1389 content_bounds(),
1390 Some(Placement::Top),
1391 42,
1392 DropPlaceholderBounds::for_placement(content_bounds(), None),
1393 cx,
1394 );
1395 (
1396 group.drop_indicator.is_some(),
1397 group.context(cx).drop_indicator().is_some(),
1398 )
1399 })
1400 });
1401
1402 assert!(stored);
1403 assert!(!published);
1404 }
1405
1406 #[gpui::test]
1407 fn a_host_drag_reports_the_group_and_the_edge_it_resolved(cx: &mut TestAppContext) {
1408 let log = log_of();
1409 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1410 let events = record_events(&group, cx);
1411
1412 cx.update(|_, cx| {
1413 group.update(cx, |group, cx| {
1414 group.emit_drag_drop(&AnyDrag::new(7u32), Some(Placement::Bottom), cx)
1415 });
1416 });
1417 cx.run_until_parked();
1418
1419 assert_eq!(*events.borrow(), vec!["item onto 1 at Some(Bottom)"]);
1420 }
1421
1422 #[gpui::test]
1423 fn closing_asks_the_container_rather_than_editing_the_group(cx: &mut TestAppContext) {
1424 let log = log_of();
1425 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1426 let events = record_events(&group, cx);
1427 let panel = panel_id(&panels[1], cx);
1428
1429 cx.update(|_, cx| {
1430 group.update(cx, |group, cx| {
1431 group.close_panel(panel, cx);
1432 group.close_panel(PanelId::from_u64(4242), cx);
1434 });
1435 });
1436 cx.run_until_parked();
1437
1438 assert_eq!(*events.borrow(), vec![format!("close {}", panel.as_u64())]);
1439 assert_eq!(
1440 cx.update(|_, cx| group.read(cx).panels().len()),
1441 2,
1442 "the group still holds both panels until the container edits the tree"
1443 );
1444 }
1445
1446 #[gpui::test]
1447 fn zooming_toggles_and_tells_the_displayed_panel(cx: &mut TestAppContext) {
1448 let log = log_of();
1449 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1450 let events = record_events(&group, cx);
1451
1452 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1453 cx.run_until_parked();
1454 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1455 cx.run_until_parked();
1456
1457 assert_eq!(*events.borrow(), vec!["zoom in", "zoom out"]);
1458 assert!(!cx.update(|_, cx| group.read(cx).is_zoomed()));
1459 assert_eq!(
1460 drain(&log)
1461 .into_iter()
1462 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
1463 .collect::<Vec<_>>(),
1464 vec![
1465 ("a", PanelSignal::Zoomed(true)),
1466 ("a", PanelSignal::Zoomed(false))
1467 ],
1468 "the old TabPanel sent this to itself, where the default no-op swallowed it"
1469 );
1470 }
1471
1472 #[gpui::test]
1476 fn a_drag_over_a_locked_group_resolves_nothing(cx: &mut TestAppContext) {
1477 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1478 cx.update(|window, cx| {
1479 group.update(cx, |group, cx| {
1480 group.set_constraints(
1481 TabGroupConstraints::in_split(false).dock_locked(true),
1482 window,
1483 cx,
1484 )
1485 })
1486 });
1487 cx.update(|window, cx| window.draw(cx).clear(cx));
1488
1489 drag_from_the_tab_into_the_content(cx);
1490
1491 assert!(
1492 cx.update(|_, cx| group.read(cx).drop_indicator.is_none()),
1493 "a locked group installs no drag-move listener, so nothing resolved"
1494 );
1495 }
1496
1497 #[gpui::test]
1500 fn a_group_the_container_sealed_shut_refuses_to_close(cx: &mut TestAppContext) {
1501 let log = log_of();
1502 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1503 let events = record_events(&group, cx);
1504 let panel = panel_id(&panels[0], cx);
1505
1506 cx.update(|window, cx| {
1507 group.update(cx, |group, cx| {
1508 group.set_constraints(
1509 TabGroupConstraints::in_split(false).closable(false),
1510 window,
1511 cx,
1512 );
1513 group.close_panel(panel, cx);
1514 })
1515 });
1516 cx.run_until_parked();
1517
1518 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_closable()));
1519 assert!(events.borrow().is_empty());
1520 }
1521
1522 #[gpui::test]
1528 fn the_only_groups_last_panel_is_not_closable(cx: &mut TestAppContext) {
1529 let log = log_of();
1530 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1531
1532 cx.update(|window, cx| {
1533 group.update(cx, |group, cx| {
1534 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1535 })
1536 });
1537 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1538
1539 cx.update(|window, cx| {
1540 group.update(cx, |group, cx| {
1541 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1542 })
1543 });
1544 let beside_a_sibling = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1545
1546 assert!(!alone);
1547 assert!(beside_a_sibling);
1548 }
1549
1550 #[gpui::test]
1553 fn collapsing_deactivates_the_displayed_panel_and_expanding_restores_it(
1554 cx: &mut TestAppContext,
1555 ) {
1556 let log = log_of();
1557 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1558 cx.run_until_parked();
1559 assert_eq!(drain_active(&log), vec![("a", true)]);
1560
1561 cx.update(|window, cx| {
1562 group.update(cx, |group, cx| {
1563 group.set_constraints(
1564 TabGroupConstraints::in_split(false).collapsed(true),
1565 window,
1566 cx,
1567 )
1568 })
1569 });
1570 cx.run_until_parked();
1571 assert_eq!(drain_active(&log), vec![("a", false)]);
1572
1573 cx.update(|window, cx| {
1574 group.update(cx, |group, cx| {
1575 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1576 })
1577 });
1578 cx.run_until_parked();
1579 assert_eq!(drain_active(&log), vec![("a", true)]);
1580 }
1581
1582 fn skin_content() -> Bounds<Pixels> {
1588 Bounds {
1589 origin: point(px(100.), px(50.)),
1590 size: size(px(400.), px(300.)),
1591 }
1592 }
1593
1594 struct RecordingRenderer {
1597 calls: Rc<RefCell<Vec<&'static str>>>,
1598 }
1599
1600 impl RecordingRenderer {
1601 fn saw(&self, call: &'static str) {
1602 self.calls.borrow_mut().push(call);
1603 }
1604 }
1605
1606 impl TabGroupRenderer for RecordingRenderer {
1607 fn frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1608 self.saw("frame");
1609 div().id("skin-frame").relative().size_full()
1610 }
1611
1612 fn content_frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1613 self.saw("content_frame");
1614 div()
1615 .id("skin-content")
1616 .absolute()
1617 .left(skin_content().origin.x)
1618 .top(skin_content().origin.y)
1619 .w(skin_content().size.width)
1620 .h(skin_content().size.height)
1621 }
1622
1623 fn render_tab_bar(
1624 &self,
1625 group: &TabGroupContext,
1626 _: &mut Window,
1627 cx: &mut App,
1628 ) -> AnyElement {
1629 self.saw("tab_bar");
1630 div()
1631 .id("skin-tab")
1632 .absolute()
1633 .left(px(0.))
1634 .top(px(0.))
1635 .w(px(80.))
1636 .h(px(24.))
1637 .when_some(group.drag_panel(0, cx), |this, drag| {
1638 this.on_drag(drag, |drag, offset, _, cx| {
1639 drag.set_drag_offset(offset);
1640 cx.new(|_| drag.clone())
1641 })
1642 })
1643 .into_any_element()
1644 }
1645
1646 fn render_active_panel(
1647 &self,
1648 panel: AnyView,
1649 _: &TabGroupContext,
1650 _: &mut Window,
1651 _: &mut App,
1652 ) -> AnyElement {
1653 self.saw("active_panel");
1654 panel.into_any_element()
1655 }
1656
1657 fn render_empty(
1658 &self,
1659 _: &TabGroupContext,
1660 _: &mut Window,
1661 _: &mut App,
1662 ) -> Option<AnyElement> {
1663 self.saw("empty");
1664 None
1665 }
1666
1667 fn render_drop_indicator(
1668 &self,
1669 _: DropIndicator,
1670 _: &mut Window,
1671 _: &mut App,
1672 ) -> Option<AnyElement> {
1673 self.saw("drop_indicator");
1674 None
1675 }
1676 }
1677
1678 fn build_skinned_group<'a>(
1679 names: &[&'static str],
1680 cx: &'a mut TestAppContext,
1681 ) -> (
1682 Entity<TabGroup>,
1683 Rc<RefCell<Vec<&'static str>>>,
1684 &'a mut VisualTestContext,
1685 ) {
1686 let calls: Rc<RefCell<Vec<&'static str>>> = Rc::default();
1687 let renderer = Rc::new(RecordingRenderer {
1688 calls: calls.clone(),
1689 });
1690 let (group, cx) = cx.add_window_view(|window, cx| {
1691 TabGroup::new(NodeId::from_u64(1), window, cx).with_renderer(renderer)
1692 });
1693
1694 let names = names.to_vec();
1695 let log = log_of();
1696 cx.update(|window, cx| {
1697 let views: Vec<Arc<dyn PanelView>> = names
1698 .iter()
1699 .map(|name| {
1700 Arc::new(crate::dock::test_support::TestPanel::logging(
1701 name, &log, cx,
1702 )) as _
1703 })
1704 .collect();
1705 group.update(cx, |group, cx| {
1706 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1707 group.sync_from_tree(views, 0, window, cx);
1708 });
1709 });
1710 cx.run_until_parked();
1711 calls.borrow_mut().clear();
1712
1713 (group, calls, cx)
1714 }
1715
1716 fn drag_from_the_tab_into_the_content(cx: &mut VisualTestContext) {
1719 cx.simulate_mouse_down(
1720 point(px(20.), px(10.)),
1721 MouseButton::Left,
1722 Modifiers::none(),
1723 );
1724 cx.simulate_mouse_move(
1725 point(px(30.), px(14.)),
1726 MouseButton::Left,
1727 Modifiers::none(),
1728 );
1729 cx.simulate_mouse_move(
1730 point(px(450.), px(200.)),
1731 MouseButton::Left,
1732 Modifiers::none(),
1733 );
1734 }
1735
1736 #[gpui::test]
1739 fn the_renderer_composes_frame_then_tab_bar_then_content(cx: &mut TestAppContext) {
1740 let (_group, calls, cx) = build_skinned_group(&["a"], cx);
1741
1742 cx.update(|window, cx| window.draw(cx).clear(cx));
1743
1744 assert_eq!(
1745 *calls.borrow(),
1746 vec!["frame", "tab_bar", "content_frame", "active_panel"],
1747 "the tab bar is a sibling of the content, not a child of it"
1748 );
1749 }
1750
1751 #[gpui::test]
1754 fn an_empty_group_asks_the_renderer_for_its_empty_state(cx: &mut TestAppContext) {
1755 let (_group, calls, cx) = build_skinned_group(&[], cx);
1756
1757 cx.update(|window, cx| window.draw(cx).clear(cx));
1758
1759 assert_eq!(
1760 *calls.borrow(),
1761 vec!["frame", "tab_bar", "content_frame", "empty"]
1762 );
1763 }
1764
1765 #[gpui::test]
1769 fn the_content_frame_is_what_a_drag_is_measured_against(cx: &mut TestAppContext) {
1770 let (group, calls, cx) = build_skinned_group(&["a", "b"], cx);
1771 cx.update(|window, cx| window.draw(cx).clear(cx));
1772
1773 drag_from_the_tab_into_the_content(cx);
1774
1775 let indicator = cx
1776 .update(|_, cx| group.read(cx).drop_indicator)
1777 .expect("the content frame's drag-move listener ran");
1778
1779 assert_eq!(
1780 indicator.bounds(),
1781 skin_content(),
1782 "measured against the content frame, not the full-size outer frame"
1783 );
1784 assert_eq!(indicator.placement(), Some(Placement::Right));
1785
1786 cx.update(|window, cx| window.draw(cx).clear(cx));
1787 assert!(
1788 calls.borrow().contains(&"drop_indicator"),
1789 "a published indicator is handed to the renderer to draw"
1790 );
1791 }
1792
1793 #[gpui::test]
1796 fn dropping_on_the_content_frame_reports_the_move(cx: &mut TestAppContext) {
1797 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1798 let events = record_events(&group, cx);
1799 cx.update(|window, cx| window.draw(cx).clear(cx));
1800
1801 drag_from_the_tab_into_the_content(cx);
1802 cx.simulate_mouse_up(
1803 point(px(450.), px(200.)),
1804 MouseButton::Left,
1805 Modifiers::none(),
1806 );
1807 cx.run_until_parked();
1808
1809 assert_eq!(events.borrow().len(), 1, "got {:?}", events.borrow());
1810 assert!(
1811 events.borrow()[0].ends_with("split 1 Right"),
1812 "got {:?}",
1813 events.borrow()
1814 );
1815 assert!(cx.update(|_, cx| group.read(cx).drop_indicator.is_none()));
1816 }
1817}