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