1use std::{rc::Rc, sync::Arc};
4
5use gpui::{
6 AnyElement, AnyView, App, Bounds, Context, Div, DragMoveEvent, Empty, EventEmitter,
7 FocusHandle, Focusable, InteractiveElement as _, IntoElement, ParentElement as _, Pixels,
8 Render, Stateful, Styled as _, WeakEntity, Window, div, prelude::FluentBuilder as _, px,
9};
10
11use crate::Placement;
12
13use super::{
14 active::ActiveTracker,
15 drag::{
16 AnyDrag, DragPanel, DropIndicator, DropPlaceholderBounds, DropTarget, ITEM_DRAG_SESSION_ID,
17 split_placement_at,
18 },
19 layout::{InsertTarget, NodeId, PanelId},
20 panel::PanelView,
21};
22
23#[non_exhaustive]
30pub enum TabGroupEvent {
31 Drop {
34 panel: PanelId,
35 source: NodeId,
36 target: InsertTarget,
37 },
38 DragDrop { item: AnyDrag, target: DropTarget },
40 ClosePanel { panel: PanelId },
42 ActiveChanged { ix: usize },
44 ZoomIn,
48 ZoomOut,
50}
51
52#[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(
360 &mut self,
361 constraints: TabGroupConstraints,
362 window: &mut Window,
363 cx: &mut Context<Self>,
364 ) {
365 if self.constraints == constraints {
366 return;
367 }
368
369 let collapse_changed = self.constraints.is_collapsed() != constraints.is_collapsed();
372 self.constraints = constraints;
373 if collapse_changed {
374 self.schedule_active_sync(window, cx);
375 }
376 cx.notify();
377 }
378
379 pub(crate) fn set_zoomed(&mut self, zoomed: bool, window: &mut Window, cx: &mut Context<Self>) {
394 if self.zoomed == zoomed {
395 return;
396 }
397 let panel = self.active_panel(cx);
398 if zoomed && !panel.as_ref().is_some_and(|panel| panel.zoomable(cx)) {
399 return;
400 }
401
402 self.zoomed = zoomed;
403 cx.emit(if zoomed {
404 TabGroupEvent::ZoomIn
405 } else {
406 TabGroupEvent::ZoomOut
407 });
408
409 if let Some(panel) = panel {
413 cx.spawn_in(window, async move |_, cx| {
414 _ = cx.update(|window, cx| panel.set_zoomed(zoomed, window, cx));
415 })
416 .detach();
417 }
418 cx.notify();
419 }
420
421 pub(crate) fn last_notified_active(&self, panel: PanelId) -> Option<bool> {
424 self.active.last_notified(panel)
425 }
426
427 pub(crate) fn seed_active(&mut self, panel: PanelId, active: bool) {
430 self.active.seed(panel, active);
431 }
432}
433
434impl TabGroup {
435 fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
437 self.panels
438 .iter()
439 .filter(|panel| panel.visible(cx))
440 .cloned()
441 }
442
443 fn is_locked(&self) -> bool {
446 self.constraints.is_locked() || self.zoomed
447 }
448
449 fn is_last_panel(&self, cx: &App) -> bool {
453 self.constraints.is_alone() && self.visible_panels(cx).count() <= 1
454 }
455
456 fn draggable(&self, cx: &App) -> bool {
457 !self.is_locked() && !self.is_last_panel(cx)
458 }
459
460 fn droppable(&self) -> bool {
461 !self.is_locked()
462 }
463
464 fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
465 if let Some(panel) = self.active_panel(cx) {
466 panel.focus_handle(cx).focus(window, cx);
467 }
468 }
469
470 fn schedule_active_sync(&mut self, window: &mut Window, cx: &mut Context<Self>) {
474 if !self.active.schedule_sync() {
475 return;
476 }
477
478 cx.spawn_in(window, async move |group, cx| {
479 _ = cx.update(|window, cx| {
480 let Ok(changes) = group.update(cx, |group, cx| group.reconcile_active(cx)) else {
481 return;
482 };
483 for (panel, active) in changes {
486 panel.set_active(active, window, cx);
487 }
488 });
489 })
490 .detach();
491 }
492
493 fn reconcile_active(&mut self, cx: &App) -> Vec<(Arc<dyn PanelView>, bool)> {
497 self.active.sync_finished();
498
499 let ids: Vec<PanelId> = self.panels.iter().map(|panel| panel.panel_id(cx)).collect();
500 let displayed = match self.constraints.is_collapsed() {
501 true => None,
502 false => ids.get(self.active_ix).copied(),
503 };
504
505 self.active
506 .reconcile(&ids, displayed)
507 .into_iter()
508 .filter_map(|(id, active)| {
509 ids.iter()
510 .position(|candidate| *candidate == id)
511 .map(|ix| (self.panels[ix].clone(), active))
512 })
513 .collect()
514 }
515
516 fn on_panel_drag_move(
519 &mut self,
520 drag: &DragMoveEvent<DragPanel>,
521 _: &mut Window,
522 cx: &mut Context<Self>,
523 ) {
524 let bounds = drag.bounds;
525 if !bounds.contains(&drag.event.position) {
526 self.clear_drop_indicator(cx);
527 return;
528 }
529
530 let placement = split_placement_at(bounds, drag.event.position);
531 let dragged = drag.drag(cx);
532 let source = DropPlaceholderBounds::new(
534 drag.event.position - dragged.drag_offset() - bounds.origin,
535 dragged.preview_size(),
536 );
537
538 self.sync_drop_placeholder(bounds, placement, dragged.drag_session_id(), source, cx);
539 }
540
541 fn on_item_drag_move(
546 &mut self,
547 drag: &DragMoveEvent<AnyDrag>,
548 _: &mut Window,
549 cx: &mut Context<Self>,
550 ) {
551 let bounds = drag.bounds;
552 if !bounds.contains(&drag.event.position) {
553 self.clear_drop_indicator(cx);
554 return;
555 }
556
557 let placement = split_placement_at(bounds, drag.event.position);
558 let source = DropPlaceholderBounds::for_placement(bounds, placement);
559
560 self.sync_drop_placeholder(bounds, placement, ITEM_DRAG_SESSION_ID, source, cx);
561 }
562
563 fn sync_drop_placeholder(
564 &mut self,
565 bounds: Bounds<Pixels>,
566 placement: Option<Placement>,
567 drag_session_id: u64,
568 source: DropPlaceholderBounds,
569 cx: &mut Context<Self>,
570 ) {
571 let to = DropPlaceholderBounds::for_placement(bounds, placement);
572
573 let restart = self.drop_indicator.is_none_or(|indicator| {
574 indicator.drag_session_id() != drag_session_id || indicator.placement() != placement
575 });
576
577 let (from, epoch) = match (restart, self.drop_indicator) {
578 (false, Some(indicator)) => (indicator.from(), indicator.epoch()),
579 (_, Some(indicator)) => {
580 let from = match indicator.drag_session_id() == drag_session_id {
581 true => indicator.to(),
582 false => source,
583 };
584 (from, indicator.epoch().wrapping_add(1))
585 }
586 (_, None) => (source, 0),
587 };
588
589 self.drop_indicator = Some(DropIndicator::new(
590 bounds,
591 placement,
592 from,
593 to,
594 drag_session_id,
595 epoch,
596 ));
597 cx.notify();
598 }
599
600 fn clear_drop_indicator(&mut self, cx: &mut Context<Self>) {
601 if self.drop_indicator.take().is_some() {
602 cx.notify();
603 }
604 }
605
606 fn emit_drag_drop(
609 &mut self,
610 item: &AnyDrag,
611 placement: Option<Placement>,
612 cx: &mut Context<Self>,
613 ) {
614 self.drop_indicator = None;
615 cx.emit(TabGroupEvent::DragDrop {
616 item: item.clone(),
617 target: DropTarget::Group {
618 node: self.node,
619 placement,
620 },
621 });
622 cx.notify();
623 }
624
625 fn on_drop(
632 &mut self,
633 drag: &DragPanel,
634 ix: Option<usize>,
635 activate: bool,
636 cx: &mut Context<Self>,
637 ) {
638 let indicator = self.drop_indicator.take();
639 let placement = match ix {
640 Some(_) => None,
641 None => indicator.and_then(|indicator| indicator.placement()),
642 };
643
644 if drag.source() == self.node
648 && ix.is_none()
649 && (placement.is_none() || self.panels.len() == 1)
650 {
651 cx.notify();
652 return;
653 }
654
655 let target = match placement {
656 Some(placement) => InsertTarget::Split {
657 node: self.node,
658 placement,
659 size: None,
660 },
661 None => InsertTarget::Tabs {
662 node: self.node,
663 ix,
664 activate,
665 },
666 };
667
668 cx.emit(TabGroupEvent::Drop {
669 panel: drag.panel(),
670 source: drag.source(),
671 target,
672 });
673 cx.notify();
674 }
675}
676
677impl EventEmitter<TabGroupEvent> for TabGroup {}
678
679impl Focusable for TabGroup {
680 fn focus_handle(&self, cx: &App) -> FocusHandle {
681 match self.active_panel(cx) {
682 Some(panel) => panel.focus_handle(cx),
683 None => self.focus_handle.clone(),
684 }
685 }
686}
687
688impl Render for TabGroup {
689 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
690 let context = self.context(cx);
691 let renderer = self.renderer.clone();
692 let focus_handle = self.focus_handle(cx);
693 let droppable = context.droppable;
694 let indicator = context.drop_indicator;
695
696 renderer
697 .frame(&context, window, cx)
698 .flex()
708 .flex_col()
709 .size_full()
710 .overflow_hidden()
711 .track_focus(&focus_handle)
712 .tab_group()
713 .child(renderer.render_tab_bar(&context, window, cx))
714 .child(
715 renderer
716 .content_frame(&context, window, cx)
717 .flex()
721 .flex_col()
722 .when(!context.is_collapsed(), |this| this.flex_1())
723 .min_h(px(0.))
731 .overflow_hidden()
732 .when(droppable, |this| {
739 this.on_drag_move(cx.listener(Self::on_panel_drag_move))
740 .on_drop(cx.listener(|this, drag: &DragPanel, _, cx| {
741 this.on_drop(drag, None, true, cx)
742 }))
743 .on_drag_move(cx.listener(Self::on_item_drag_move))
744 .on_drop(cx.listener(|this, item: &AnyDrag, _, cx| {
745 let placement = this
746 .drop_indicator
747 .and_then(|indicator| indicator.placement());
748 this.emit_drag_drop(item, placement, cx);
749 }))
750 })
751 .map(|this| match context.active_panel.as_ref() {
752 Some(panel) => this.child(renderer.render_active_panel(
753 panel.view(),
754 &context,
755 window,
756 cx,
757 )),
758 None => this.children(renderer.render_empty(&context, window, cx)),
759 })
760 .when_some(indicator, |this, indicator| {
761 this.children(renderer.render_drop_indicator(indicator, window, cx))
762 }),
763 )
764 }
765}
766
767type SelectTabHandler = Rc<dyn Fn(usize, &mut Window, &mut App)>;
768type ClosePanelHandler = Rc<dyn Fn(PanelId, &mut Window, &mut App)>;
769type ToggleZoomHandler = Rc<dyn Fn(&mut Window, &mut App)>;
770type DropPanelHandler = Rc<dyn Fn(DragPanel, Option<usize>, bool, &mut Window, &mut App)>;
771type DropItemHandler = Rc<dyn Fn(AnyDrag, Option<Placement>, &mut Window, &mut App)>;
772
773#[derive(Clone)]
776pub struct TabGroupContext {
777 node: NodeId,
778 panels: Vec<Arc<dyn PanelView>>,
779 active_panel: Option<Arc<dyn PanelView>>,
780 active_ix: usize,
781 zoomed: bool,
782 collapsed: bool,
783 locked: bool,
784 draggable: bool,
785 droppable: bool,
786 closable: bool,
787 drop_indicator: Option<DropIndicator>,
788 on_select_tab: SelectTabHandler,
789 on_close: ClosePanelHandler,
790 on_toggle_zoom: ToggleZoomHandler,
791 on_drop_panel: DropPanelHandler,
792 on_drop_item: DropItemHandler,
793}
794
795impl TabGroupContext {
796 pub fn node(&self) -> NodeId {
799 self.node
800 }
801
802 pub fn panels(&self) -> &[Arc<dyn PanelView>] {
805 &self.panels
806 }
807
808 pub fn active_ix(&self) -> usize {
809 self.active_ix
810 }
811
812 pub fn active_panel(&self) -> Option<&Arc<dyn PanelView>> {
815 self.active_panel.as_ref()
816 }
817
818 pub fn drop_indicator(&self) -> Option<DropIndicator> {
819 self.drop_indicator
820 }
821
822 pub fn is_zoomed(&self) -> bool {
823 self.zoomed
824 }
825
826 pub fn is_collapsed(&self) -> bool {
827 self.collapsed
828 }
829
830 pub fn is_closable(&self) -> bool {
833 self.closable
834 }
835
836 pub fn is_locked(&self) -> bool {
837 self.locked
838 }
839
840 pub fn is_draggable(&self) -> bool {
841 self.draggable
842 }
843
844 pub fn is_droppable(&self) -> bool {
845 self.droppable
846 }
847
848 pub fn select_tab(&self, ix: usize, window: &mut Window, cx: &mut App) {
849 (self.on_select_tab)(ix, window, cx);
850 }
851
852 pub fn close(&self, panel: PanelId, window: &mut Window, cx: &mut App) {
853 (self.on_close)(panel, window, cx);
854 }
855
856 pub fn toggle_zoom(&self, window: &mut Window, cx: &mut App) {
857 (self.on_toggle_zoom)(window, cx);
858 }
859
860 pub fn drag_panel(&self, ix: usize, cx: &App) -> Option<DragPanel> {
863 self.panels
864 .get(ix)
865 .map(|panel| DragPanel::new(panel.panel_id(cx), self.node))
866 }
867
868 pub fn drop_panel(
871 &self,
872 drag: DragPanel,
873 ix: Option<usize>,
874 activate: bool,
875 window: &mut Window,
876 cx: &mut App,
877 ) {
878 (self.on_drop_panel)(drag, ix, activate, window, cx);
879 }
880
881 pub fn drop_item(
884 &self,
885 item: AnyDrag,
886 placement: Option<Placement>,
887 window: &mut Window,
888 cx: &mut App,
889 ) {
890 (self.on_drop_item)(item, placement, window, cx);
891 }
892}
893
894#[allow(unused_variables)]
901pub trait TabGroupRenderer: 'static {
902 fn frame(&self, group: &TabGroupContext, window: &mut Window, cx: &mut App) -> Stateful<Div> {
910 div().id("tab-group")
911 }
912
913 fn content_frame(
915 &self,
916 group: &TabGroupContext,
917 window: &mut Window,
918 cx: &mut App,
919 ) -> Stateful<Div> {
920 div().id("tab-group-content")
921 }
922
923 fn render_tab_bar(
924 &self,
925 group: &TabGroupContext,
926 window: &mut Window,
927 cx: &mut App,
928 ) -> AnyElement;
929
930 fn render_active_panel(
934 &self,
935 panel: AnyView,
936 group: &TabGroupContext,
937 window: &mut Window,
938 cx: &mut App,
939 ) -> AnyElement {
940 panel.into_any_element()
941 }
942
943 fn render_drop_indicator(
944 &self,
945 indicator: DropIndicator,
946 window: &mut Window,
947 cx: &mut App,
948 ) -> Option<AnyElement> {
949 None
950 }
951
952 fn render_empty(
953 &self,
954 group: &TabGroupContext,
955 window: &mut Window,
956 cx: &mut App,
957 ) -> Option<AnyElement> {
958 None
959 }
960}
961
962pub(crate) struct BareTabGroup;
964
965impl TabGroupRenderer for BareTabGroup {
966 fn render_tab_bar(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> AnyElement {
967 Empty.into_any_element()
968 }
969}
970
971#[cfg(test)]
972mod tests {
973 use std::cell::RefCell;
974
975 use gpui::{
976 AppContext as _, Entity, Modifiers, MouseButton, StatefulInteractiveElement as _,
977 TestAppContext, VisualTestContext, point, px, size,
978 };
979
980 use super::*;
981 use crate::dock::test_support::{
982 PanelSignal, build_group, drain, drain_active, log_of, panel_id,
983 };
984
985 fn group_node() -> NodeId {
987 NodeId::from_u64(1)
988 }
989
990 fn elsewhere() -> NodeId {
991 NodeId::from_u64(7)
992 }
993
994 fn content_bounds() -> Bounds<Pixels> {
995 Bounds {
996 origin: point(px(0.), px(0.)),
997 size: size(px(400.), px(300.)),
998 }
999 }
1000
1001 fn record_events(
1004 group: &Entity<TabGroup>,
1005 cx: &mut VisualTestContext,
1006 ) -> Rc<RefCell<Vec<String>>> {
1007 let events: Rc<RefCell<Vec<String>>> = Rc::default();
1008 let sink = events.clone();
1009 cx.update(|_, cx| {
1010 cx.subscribe(group, move |_, event: &TabGroupEvent, _| {
1011 sink.borrow_mut().push(describe(event));
1012 })
1013 .detach();
1014 });
1015 events
1016 }
1017
1018 fn describe(event: &TabGroupEvent) -> String {
1019 match event {
1020 TabGroupEvent::Drop {
1021 panel,
1022 source,
1023 target,
1024 } => match target {
1025 InsertTarget::Tabs { node, ix, activate } => format!(
1026 "drop panel {} from {} into tabs {} at {:?} activate={}",
1027 panel.as_u64(),
1028 source.as_u64(),
1029 node.as_u64(),
1030 ix,
1031 activate
1032 ),
1033 InsertTarget::Split {
1034 node, placement, ..
1035 } => format!(
1036 "drop panel {} from {} split {} {}",
1037 panel.as_u64(),
1038 source.as_u64(),
1039 node.as_u64(),
1040 placement
1041 ),
1042 InsertTarget::Tile { .. } => "drop tile".into(),
1043 },
1044 TabGroupEvent::DragDrop { target, .. } => match target {
1045 DropTarget::Group { node, placement } => {
1046 format!("item onto {} at {:?}", node.as_u64(), placement)
1047 }
1048 DropTarget::Canvas => "item onto canvas".into(),
1049 },
1050 TabGroupEvent::ClosePanel { panel } => format!("close {}", panel.as_u64()),
1051 TabGroupEvent::ActiveChanged { ix } => format!("active {ix}"),
1052 TabGroupEvent::ZoomIn => "zoom in".into(),
1053 TabGroupEvent::ZoomOut => "zoom out".into(),
1054 }
1055 }
1056
1057 #[gpui::test]
1058 fn a_group_announces_its_first_panel_active(cx: &mut TestAppContext) {
1059 let log = log_of();
1060 let (group, panels, cx) = build_group(&log, &["a"], cx);
1061 cx.run_until_parked();
1062
1063 assert_eq!(drain_active(&log), vec![("a", true)]);
1064 let _ = (group, panels);
1065 }
1066
1067 #[gpui::test]
1068 fn selecting_another_tab_deactivates_then_activates(cx: &mut TestAppContext) {
1069 let log = log_of();
1070 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1071 cx.run_until_parked();
1072 drain_active(&log);
1073
1074 cx.update(|window, cx| {
1075 group.update(cx, |group, cx| group.select_tab(1, window, cx));
1076 });
1077 cx.run_until_parked();
1078
1079 assert_eq!(drain_active(&log), vec![("a", false), ("b", true)]);
1080 }
1081
1082 #[gpui::test]
1083 fn the_context_snapshots_the_group_state(cx: &mut TestAppContext) {
1084 let log = log_of();
1085 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1086
1087 let seen = cx.update(|_, cx| {
1088 group.update(cx, |group, cx| {
1089 let context = group.context(cx);
1090 (
1091 context.panels().len(),
1092 context.active_ix(),
1093 context.is_zoomed(),
1094 )
1095 })
1096 });
1097
1098 assert_eq!(seen, (2, 0, false));
1099 }
1100
1101 #[gpui::test]
1104 fn a_hidden_displayed_panel_is_still_the_active_one(cx: &mut TestAppContext) {
1105 let log = log_of();
1106 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1107 cx.update(|_, cx| {
1108 panels[0].update(cx, |panel, cx| panel.set_visible(false, cx));
1109 });
1110 cx.run_until_parked();
1111
1112 assert_eq!(drain_active(&log), vec![("a", true)]);
1113 assert_eq!(
1114 cx.update(|_, cx| group.read(cx).active_panel(cx).unwrap().panel_name(cx)),
1115 "b",
1116 "rendering falls back to the first visible panel"
1117 );
1118 }
1119
1120 #[gpui::test]
1123 fn a_panel_dropped_from_the_group_is_never_told_it_went_inactive(cx: &mut TestAppContext) {
1124 let log = log_of();
1125 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1126 cx.run_until_parked();
1127 drain(&log);
1128
1129 cx.update(|window, cx| {
1130 let remaining: Vec<Arc<dyn PanelView>> = vec![Arc::new(panels[1].clone())];
1131 group.update(cx, |group, cx| {
1132 group.sync_from_tree(remaining, 0, window, cx)
1133 });
1134 });
1135 cx.run_until_parked();
1136
1137 assert_eq!(drain_active(&log), vec![("b", true)]);
1138 }
1139
1140 #[gpui::test]
1141 fn selecting_a_tab_that_is_not_there_changes_nothing(cx: &mut TestAppContext) {
1142 let log = log_of();
1143 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1144 cx.run_until_parked();
1145 drain_active(&log);
1146 let events = record_events(&group, cx);
1147
1148 cx.update(|window, cx| {
1149 group.update(cx, |group, cx| group.select_tab(5, window, cx));
1150 });
1151 cx.run_until_parked();
1152
1153 assert_eq!(cx.update(|_, cx| group.read(cx).active_ix()), 0);
1154 assert!(events.borrow().is_empty());
1155 assert!(drain_active(&log).is_empty());
1156 }
1157
1158 #[gpui::test]
1159 fn a_locked_group_can_be_neither_dragged_nor_dropped_into(cx: &mut TestAppContext) {
1160 let log = log_of();
1161 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1162
1163 cx.update(|window, cx| {
1164 group.update(cx, |group, cx| {
1165 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1166 })
1167 });
1168 let unlocked = cx.update(|_, cx| {
1169 let context = group.read(cx).context(cx);
1170 (context.is_draggable(), context.is_droppable())
1171 });
1172 cx.update(|window, cx| {
1173 group.update(cx, |group, cx| {
1174 group.set_constraints(
1175 TabGroupConstraints::in_split(false).dock_locked(true),
1176 window,
1177 cx,
1178 )
1179 })
1180 });
1181 let locked = cx.update(|_, cx| {
1182 let context = group.read(cx).context(cx);
1183 (context.is_draggable(), context.is_droppable())
1184 });
1185
1186 assert_eq!(unlocked, (true, true));
1187 assert_eq!(locked, (false, false));
1188 }
1189
1190 #[gpui::test]
1193 fn a_zoomed_group_takes_no_drops(cx: &mut TestAppContext) {
1194 let log = log_of();
1195 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1196
1197 cx.update(|window, cx| {
1198 group.update(cx, |group, cx| {
1199 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1200 group.set_zoomed(true, window, cx);
1201 })
1202 });
1203
1204 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_droppable()));
1205 }
1206
1207 #[gpui::test]
1210 fn the_only_groups_last_visible_panel_cannot_be_dragged_out(cx: &mut TestAppContext) {
1211 let log = log_of();
1212 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1213
1214 cx.update(|window, cx| {
1215 group.update(cx, |group, cx| {
1216 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1217 })
1218 });
1219 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1220 cx.update(|window, cx| {
1221 group.update(cx, |group, cx| {
1222 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1223 })
1224 });
1225 let beside_others = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1226
1227 assert!(!alone);
1228 assert!(beside_others);
1229 }
1230
1231 #[gpui::test]
1232 fn a_panel_from_another_group_lands_as_one_move(cx: &mut TestAppContext) {
1233 let log = log_of();
1234 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1235 let events = record_events(&group, cx);
1236
1237 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1238 cx.update(|_, cx| {
1239 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1240 });
1241 cx.run_until_parked();
1242
1243 assert_eq!(
1244 *events.borrow(),
1245 vec!["drop panel 99 from 7 into tabs 1 at None activate=true"]
1246 );
1247 }
1248
1249 #[gpui::test]
1250 fn dropping_a_panel_back_onto_its_own_group_is_ignored(cx: &mut TestAppContext) {
1251 let log = log_of();
1252 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1253 let events = record_events(&group, cx);
1254
1255 let panel = panel_id(&panels[0], cx);
1256 let drag = DragPanel::new(panel, group_node());
1257 cx.update(|_, cx| {
1258 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1259 });
1260 cx.run_until_parked();
1261
1262 assert!(events.borrow().is_empty());
1263 }
1264
1265 #[gpui::test]
1266 fn a_hovering_split_turns_a_drop_into_a_split(cx: &mut TestAppContext) {
1267 let log = log_of();
1268 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1269 let events = record_events(&group, cx);
1270
1271 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1272 cx.update(|_, cx| {
1273 group.update(cx, |group, cx| {
1274 group.sync_drop_placeholder(
1275 content_bounds(),
1276 Some(Placement::Right),
1277 drag.drag_session_id(),
1278 DropPlaceholderBounds::for_placement(content_bounds(), None),
1279 cx,
1280 );
1281 group.on_drop(&drag, None, true, cx);
1282 });
1283 });
1284 cx.run_until_parked();
1285
1286 assert_eq!(*events.borrow(), vec!["drop panel 99 from 7 split 1 Right"]);
1287 }
1288
1289 #[gpui::test]
1292 fn a_tab_slot_overrides_the_hovering_split(cx: &mut TestAppContext) {
1293 let log = log_of();
1294 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1295 let events = record_events(&group, cx);
1296
1297 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1298 cx.update(|_, cx| {
1299 group.update(cx, |group, cx| {
1300 group.sync_drop_placeholder(
1301 content_bounds(),
1302 Some(Placement::Right),
1303 drag.drag_session_id(),
1304 DropPlaceholderBounds::for_placement(content_bounds(), None),
1305 cx,
1306 );
1307 group.on_drop(&drag, Some(0), true, cx);
1308 });
1309 });
1310 cx.run_until_parked();
1311
1312 assert_eq!(
1313 *events.borrow(),
1314 vec!["drop panel 99 from 7 into tabs 1 at Some(0) activate=true"]
1315 );
1316 }
1317
1318 #[gpui::test]
1321 fn a_lone_panel_cannot_split_out_of_its_own_group(cx: &mut TestAppContext) {
1322 let log = log_of();
1323 let (group, panels, cx) = build_group(&log, &["a"], cx);
1324 let events = record_events(&group, cx);
1325
1326 let drag = DragPanel::new(panel_id(&panels[0], cx), group_node());
1327 cx.update(|_, cx| {
1328 group.update(cx, |group, cx| {
1329 group.sync_drop_placeholder(
1330 content_bounds(),
1331 Some(Placement::Right),
1332 drag.drag_session_id(),
1333 DropPlaceholderBounds::for_placement(content_bounds(), None),
1334 cx,
1335 );
1336 group.on_drop(&drag, None, true, cx);
1337 });
1338 });
1339 cx.run_until_parked();
1340
1341 assert!(events.borrow().is_empty());
1342 }
1343
1344 #[gpui::test]
1345 fn the_placeholder_replays_only_when_the_target_moves(cx: &mut TestAppContext) {
1346 let log = log_of();
1347 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1348 let bounds = content_bounds();
1349 let source = DropPlaceholderBounds::new(point(px(10.), px(10.)), size(px(96.), px(30.)));
1350
1351 let sync = |placement: Option<Placement>, cx: &mut VisualTestContext| {
1352 cx.update(|_, cx| {
1353 group.update(cx, |group, cx| {
1354 group.sync_drop_placeholder(bounds, placement, 42, source, cx);
1355 group.drop_indicator.unwrap()
1356 })
1357 })
1358 };
1359
1360 let first = sync(Some(Placement::Left), cx);
1361 let again = sync(Some(Placement::Left), cx);
1362 let moved = sync(Some(Placement::Right), cx);
1363
1364 assert_eq!(first.epoch(), 0);
1365 assert_eq!(first.from(), source, "the first run flies in from the drag");
1366 assert_eq!(again.epoch(), 0, "an unchanged target keeps animating");
1367 assert_eq!(again.from(), source);
1368 assert_eq!(moved.epoch(), 1, "a moved target replays from where it was");
1369 assert_eq!(moved.from(), first.to());
1370 assert_eq!(
1371 moved.to(),
1372 DropPlaceholderBounds::for_placement(bounds, Some(Placement::Right))
1373 );
1374 }
1375
1376 #[gpui::test]
1379 fn the_indicator_is_withheld_when_no_drag_is_in_flight(cx: &mut TestAppContext) {
1380 let log = log_of();
1381 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1382
1383 let (stored, published) = cx.update(|_, cx| {
1384 group.update(cx, |group, cx| {
1385 group.sync_drop_placeholder(
1386 content_bounds(),
1387 Some(Placement::Top),
1388 42,
1389 DropPlaceholderBounds::for_placement(content_bounds(), None),
1390 cx,
1391 );
1392 (
1393 group.drop_indicator.is_some(),
1394 group.context(cx).drop_indicator().is_some(),
1395 )
1396 })
1397 });
1398
1399 assert!(stored);
1400 assert!(!published);
1401 }
1402
1403 #[gpui::test]
1404 fn a_host_drag_reports_the_group_and_the_edge_it_resolved(cx: &mut TestAppContext) {
1405 let log = log_of();
1406 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1407 let events = record_events(&group, cx);
1408
1409 cx.update(|_, cx| {
1410 group.update(cx, |group, cx| {
1411 group.emit_drag_drop(&AnyDrag::new(7u32), Some(Placement::Bottom), cx)
1412 });
1413 });
1414 cx.run_until_parked();
1415
1416 assert_eq!(*events.borrow(), vec!["item onto 1 at Some(Bottom)"]);
1417 }
1418
1419 #[gpui::test]
1420 fn closing_asks_the_container_rather_than_editing_the_group(cx: &mut TestAppContext) {
1421 let log = log_of();
1422 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1423 let events = record_events(&group, cx);
1424 let panel = panel_id(&panels[1], cx);
1425
1426 cx.update(|_, cx| {
1427 group.update(cx, |group, cx| {
1428 group.close_panel(panel, cx);
1429 group.close_panel(PanelId::from_u64(4242), cx);
1431 });
1432 });
1433 cx.run_until_parked();
1434
1435 assert_eq!(*events.borrow(), vec![format!("close {}", panel.as_u64())]);
1436 assert_eq!(
1437 cx.update(|_, cx| group.read(cx).panels().len()),
1438 2,
1439 "the group still holds both panels until the container edits the tree"
1440 );
1441 }
1442
1443 #[gpui::test]
1444 fn zooming_toggles_and_tells_the_displayed_panel(cx: &mut TestAppContext) {
1445 let log = log_of();
1446 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1447 let events = record_events(&group, cx);
1448
1449 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1450 cx.run_until_parked();
1451 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1452 cx.run_until_parked();
1453
1454 assert_eq!(*events.borrow(), vec!["zoom in", "zoom out"]);
1455 assert!(!cx.update(|_, cx| group.read(cx).is_zoomed()));
1456 assert_eq!(
1457 drain(&log)
1458 .into_iter()
1459 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
1460 .collect::<Vec<_>>(),
1461 vec![
1462 ("a", PanelSignal::Zoomed(true)),
1463 ("a", PanelSignal::Zoomed(false))
1464 ],
1465 "the old TabPanel sent this to itself, where the default no-op swallowed it"
1466 );
1467 }
1468
1469 #[gpui::test]
1473 fn a_drag_over_a_locked_group_resolves_nothing(cx: &mut TestAppContext) {
1474 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1475 cx.update(|window, cx| {
1476 group.update(cx, |group, cx| {
1477 group.set_constraints(
1478 TabGroupConstraints::in_split(false).dock_locked(true),
1479 window,
1480 cx,
1481 )
1482 })
1483 });
1484 cx.update(|window, cx| window.draw(cx).clear(cx));
1485
1486 drag_from_the_tab_into_the_content(cx);
1487
1488 assert!(
1489 cx.update(|_, cx| group.read(cx).drop_indicator.is_none()),
1490 "a locked group installs no drag-move listener, so nothing resolved"
1491 );
1492 }
1493
1494 #[gpui::test]
1497 fn a_group_the_container_sealed_shut_refuses_to_close(cx: &mut TestAppContext) {
1498 let log = log_of();
1499 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1500 let events = record_events(&group, cx);
1501 let panel = panel_id(&panels[0], cx);
1502
1503 cx.update(|window, cx| {
1504 group.update(cx, |group, cx| {
1505 group.set_constraints(
1506 TabGroupConstraints::in_split(false).closable(false),
1507 window,
1508 cx,
1509 );
1510 group.close_panel(panel, cx);
1511 })
1512 });
1513 cx.run_until_parked();
1514
1515 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_closable()));
1516 assert!(events.borrow().is_empty());
1517 }
1518
1519 #[gpui::test]
1525 fn the_only_groups_last_panel_is_not_closable(cx: &mut TestAppContext) {
1526 let log = log_of();
1527 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1528
1529 cx.update(|window, cx| {
1530 group.update(cx, |group, cx| {
1531 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1532 })
1533 });
1534 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1535
1536 cx.update(|window, cx| {
1537 group.update(cx, |group, cx| {
1538 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1539 })
1540 });
1541 let beside_a_sibling = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1542
1543 assert!(!alone);
1544 assert!(beside_a_sibling);
1545 }
1546
1547 #[gpui::test]
1550 fn collapsing_deactivates_the_displayed_panel_and_expanding_restores_it(
1551 cx: &mut TestAppContext,
1552 ) {
1553 let log = log_of();
1554 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1555 cx.run_until_parked();
1556 assert_eq!(drain_active(&log), vec![("a", true)]);
1557
1558 cx.update(|window, cx| {
1559 group.update(cx, |group, cx| {
1560 group.set_constraints(
1561 TabGroupConstraints::in_split(false).collapsed(true),
1562 window,
1563 cx,
1564 )
1565 })
1566 });
1567 cx.run_until_parked();
1568 assert_eq!(drain_active(&log), vec![("a", false)]);
1569
1570 cx.update(|window, cx| {
1571 group.update(cx, |group, cx| {
1572 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1573 })
1574 });
1575 cx.run_until_parked();
1576 assert_eq!(drain_active(&log), vec![("a", true)]);
1577 }
1578
1579 fn skin_content() -> Bounds<Pixels> {
1585 Bounds {
1586 origin: point(px(100.), px(50.)),
1587 size: size(px(400.), px(300.)),
1588 }
1589 }
1590
1591 struct RecordingRenderer {
1594 calls: Rc<RefCell<Vec<&'static str>>>,
1595 }
1596
1597 impl RecordingRenderer {
1598 fn saw(&self, call: &'static str) {
1599 self.calls.borrow_mut().push(call);
1600 }
1601 }
1602
1603 impl TabGroupRenderer for RecordingRenderer {
1604 fn frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1605 self.saw("frame");
1606 div().id("skin-frame").relative().size_full()
1607 }
1608
1609 fn content_frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1610 self.saw("content_frame");
1611 div()
1612 .id("skin-content")
1613 .absolute()
1614 .left(skin_content().origin.x)
1615 .top(skin_content().origin.y)
1616 .w(skin_content().size.width)
1617 .h(skin_content().size.height)
1618 }
1619
1620 fn render_tab_bar(
1621 &self,
1622 group: &TabGroupContext,
1623 _: &mut Window,
1624 cx: &mut App,
1625 ) -> AnyElement {
1626 self.saw("tab_bar");
1627 div()
1628 .id("skin-tab")
1629 .absolute()
1630 .left(px(0.))
1631 .top(px(0.))
1632 .w(px(80.))
1633 .h(px(24.))
1634 .when_some(group.drag_panel(0, cx), |this, drag| {
1635 this.on_drag(drag, |drag, offset, _, cx| {
1636 drag.set_drag_offset(offset);
1637 cx.new(|_| drag.clone())
1638 })
1639 })
1640 .into_any_element()
1641 }
1642
1643 fn render_active_panel(
1644 &self,
1645 panel: AnyView,
1646 _: &TabGroupContext,
1647 _: &mut Window,
1648 _: &mut App,
1649 ) -> AnyElement {
1650 self.saw("active_panel");
1651 panel.into_any_element()
1652 }
1653
1654 fn render_empty(
1655 &self,
1656 _: &TabGroupContext,
1657 _: &mut Window,
1658 _: &mut App,
1659 ) -> Option<AnyElement> {
1660 self.saw("empty");
1661 None
1662 }
1663
1664 fn render_drop_indicator(
1665 &self,
1666 _: DropIndicator,
1667 _: &mut Window,
1668 _: &mut App,
1669 ) -> Option<AnyElement> {
1670 self.saw("drop_indicator");
1671 None
1672 }
1673 }
1674
1675 fn build_skinned_group<'a>(
1676 names: &[&'static str],
1677 cx: &'a mut TestAppContext,
1678 ) -> (
1679 Entity<TabGroup>,
1680 Rc<RefCell<Vec<&'static str>>>,
1681 &'a mut VisualTestContext,
1682 ) {
1683 let calls: Rc<RefCell<Vec<&'static str>>> = Rc::default();
1684 let renderer = Rc::new(RecordingRenderer {
1685 calls: calls.clone(),
1686 });
1687 let (group, cx) = cx.add_window_view(|window, cx| {
1688 TabGroup::new(NodeId::from_u64(1), window, cx).with_renderer(renderer)
1689 });
1690
1691 let names = names.to_vec();
1692 let log = log_of();
1693 cx.update(|window, cx| {
1694 let views: Vec<Arc<dyn PanelView>> = names
1695 .iter()
1696 .map(|name| {
1697 Arc::new(crate::dock::test_support::TestPanel::logging(
1698 name, &log, cx,
1699 )) as _
1700 })
1701 .collect();
1702 group.update(cx, |group, cx| {
1703 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1704 group.sync_from_tree(views, 0, window, cx);
1705 });
1706 });
1707 cx.run_until_parked();
1708 calls.borrow_mut().clear();
1709
1710 (group, calls, cx)
1711 }
1712
1713 fn drag_from_the_tab_into_the_content(cx: &mut VisualTestContext) {
1716 cx.simulate_mouse_down(
1717 point(px(20.), px(10.)),
1718 MouseButton::Left,
1719 Modifiers::none(),
1720 );
1721 cx.simulate_mouse_move(
1722 point(px(30.), px(14.)),
1723 MouseButton::Left,
1724 Modifiers::none(),
1725 );
1726 cx.simulate_mouse_move(
1727 point(px(450.), px(200.)),
1728 MouseButton::Left,
1729 Modifiers::none(),
1730 );
1731 }
1732
1733 #[gpui::test]
1736 fn the_renderer_composes_frame_then_tab_bar_then_content(cx: &mut TestAppContext) {
1737 let (_group, calls, cx) = build_skinned_group(&["a"], cx);
1738
1739 cx.update(|window, cx| window.draw(cx).clear(cx));
1740
1741 assert_eq!(
1742 *calls.borrow(),
1743 vec!["frame", "tab_bar", "content_frame", "active_panel"],
1744 "the tab bar is a sibling of the content, not a child of it"
1745 );
1746 }
1747
1748 #[gpui::test]
1751 fn an_empty_group_asks_the_renderer_for_its_empty_state(cx: &mut TestAppContext) {
1752 let (_group, calls, cx) = build_skinned_group(&[], cx);
1753
1754 cx.update(|window, cx| window.draw(cx).clear(cx));
1755
1756 assert_eq!(
1757 *calls.borrow(),
1758 vec!["frame", "tab_bar", "content_frame", "empty"]
1759 );
1760 }
1761
1762 #[gpui::test]
1766 fn the_content_frame_is_what_a_drag_is_measured_against(cx: &mut TestAppContext) {
1767 let (group, calls, cx) = build_skinned_group(&["a", "b"], cx);
1768 cx.update(|window, cx| window.draw(cx).clear(cx));
1769
1770 drag_from_the_tab_into_the_content(cx);
1771
1772 let indicator = cx
1773 .update(|_, cx| group.read(cx).drop_indicator)
1774 .expect("the content frame's drag-move listener ran");
1775
1776 assert_eq!(
1777 indicator.bounds(),
1778 skin_content(),
1779 "measured against the content frame, not the full-size outer frame"
1780 );
1781 assert_eq!(indicator.placement(), Some(Placement::Right));
1782
1783 cx.update(|window, cx| window.draw(cx).clear(cx));
1784 assert!(
1785 calls.borrow().contains(&"drop_indicator"),
1786 "a published indicator is handed to the renderer to draw"
1787 );
1788 }
1789
1790 #[gpui::test]
1793 fn dropping_on_the_content_frame_reports_the_move(cx: &mut TestAppContext) {
1794 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1795 let events = record_events(&group, cx);
1796 cx.update(|window, cx| window.draw(cx).clear(cx));
1797
1798 drag_from_the_tab_into_the_content(cx);
1799 cx.simulate_mouse_up(
1800 point(px(450.), px(200.)),
1801 MouseButton::Left,
1802 Modifiers::none(),
1803 );
1804 cx.run_until_parked();
1805
1806 assert_eq!(events.borrow().len(), 1, "got {:?}", events.borrow());
1807 assert!(
1808 events.borrow()[0].ends_with("split 1 Right"),
1809 "got {:?}",
1810 events.borrow()
1811 );
1812 assert!(cx.update(|_, cx| group.read(cx).drop_indicator.is_none()));
1813 }
1814}