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 constraints: self.constraints,
282 active_panel_closable: self.is_closable(cx),
283 draggable: self.draggable(cx),
284 droppable: self.droppable(),
285 drop_indicator: cx
288 .has_active_drag()
289 .then_some(self.drop_indicator)
290 .flatten(),
291 on_select_tab: {
292 let group = group.clone();
293 Rc::new(move |ix, window, cx| {
294 _ = group.update(cx, |group, cx| group.select_tab(ix, window, cx));
295 })
296 },
297 on_close: {
298 let group = group.clone();
299 Rc::new(move |panel, _, cx| {
300 _ = group.update(cx, |group, cx| group.close_panel(panel, cx));
301 })
302 },
303 on_toggle_zoom: {
304 let group = group.clone();
305 Rc::new(move |window, cx| {
306 _ = group.update(cx, |group, cx| group.toggle_zoom(window, cx));
307 })
308 },
309 on_drop_panel: {
310 let group = group.clone();
311 Rc::new(move |drag, ix, activate, _, cx| {
312 _ = group.update(cx, |group, cx| group.on_drop(&drag, ix, activate, cx));
313 })
314 },
315 on_drop_item: Rc::new(move |item, placement, _, cx| {
316 _ = group.update(cx, |group, cx| group.emit_drag_drop(&item, placement, cx));
317 }),
318 }
319 }
320}
321
322impl TabGroup {
326 pub(crate) fn sync_from_tree(
330 &mut self,
331 panels: Vec<Arc<dyn PanelView>>,
332 active_ix: usize,
333 window: &mut Window,
334 cx: &mut Context<Self>,
335 ) {
336 let group = cx.weak_entity();
337 let existing: Vec<PanelId> = self.panels.iter().map(|panel| panel.panel_id(cx)).collect();
338 for panel in panels.iter() {
339 if !existing.contains(&panel.panel_id(cx)) {
340 panel.on_added_to(group.clone(), window, cx);
341 }
342 }
343
344 self.panels = panels;
345 self.active_ix = active_ix.min(self.panels.len().saturating_sub(1));
346 self.schedule_active_sync(window, cx);
347 cx.notify();
348 }
349
350 pub(crate) fn set_constraints(
358 &mut self,
359 constraints: TabGroupConstraints,
360 window: &mut Window,
361 cx: &mut Context<Self>,
362 ) {
363 if self.constraints == constraints {
364 return;
365 }
366
367 let collapse_changed = self.constraints.is_collapsed() != constraints.is_collapsed();
370 self.constraints = constraints;
371 if collapse_changed {
372 self.schedule_active_sync(window, cx);
373 }
374 cx.notify();
375 }
376
377 pub(crate) fn set_zoomed(&mut self, zoomed: bool, window: &mut Window, cx: &mut Context<Self>) {
392 if self.zoomed == zoomed {
393 return;
394 }
395 let panel = self.active_panel(cx);
396 if zoomed && !panel.as_ref().is_some_and(|panel| panel.zoomable(cx)) {
397 return;
398 }
399
400 self.zoomed = zoomed;
401 cx.emit(if zoomed {
402 TabGroupEvent::ZoomIn
403 } else {
404 TabGroupEvent::ZoomOut
405 });
406
407 if let Some(panel) = panel {
411 cx.spawn_in(window, async move |_, cx| {
412 _ = cx.update(|window, cx| panel.set_zoomed(zoomed, window, cx));
413 })
414 .detach();
415 }
416 cx.notify();
417 }
418
419 pub(crate) fn last_notified_active(&self, panel: PanelId) -> Option<bool> {
422 self.active.last_notified(panel)
423 }
424
425 pub(crate) fn seed_active(&mut self, panel: PanelId, active: bool) {
428 self.active.seed(panel, active);
429 }
430}
431
432impl TabGroup {
433 fn visible_panels<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = Arc<dyn PanelView>> + 'a {
435 self.panels
436 .iter()
437 .filter(|panel| panel.visible(cx))
438 .cloned()
439 }
440
441 fn is_locked(&self) -> bool {
444 self.constraints.is_locked() || self.zoomed
445 }
446
447 fn is_last_panel(&self, cx: &App) -> bool {
451 self.constraints.is_alone() && self.visible_panels(cx).count() <= 1
452 }
453
454 fn draggable(&self, cx: &App) -> bool {
455 !self.is_locked() && !self.is_last_panel(cx)
456 }
457
458 fn droppable(&self) -> bool {
459 !self.is_locked()
460 }
461
462 fn focus_active_panel(&self, window: &mut Window, cx: &mut Context<Self>) {
463 if let Some(panel) = self.active_panel(cx) {
464 panel.focus_handle(cx).focus(window, cx);
465 }
466 }
467
468 fn schedule_active_sync(&mut self, window: &mut Window, cx: &mut Context<Self>) {
472 if !self.active.schedule_sync() {
473 return;
474 }
475
476 cx.spawn_in(window, async move |group, cx| {
477 _ = cx.update(|window, cx| {
478 let Ok(changes) = group.update(cx, |group, cx| group.reconcile_active(cx)) else {
479 return;
480 };
481 for (panel, active) in changes {
484 panel.set_active(active, window, cx);
485 }
486 });
487 })
488 .detach();
489 }
490
491 fn reconcile_active(&mut self, cx: &App) -> Vec<(Arc<dyn PanelView>, bool)> {
495 self.active.sync_finished();
496
497 let ids: Vec<PanelId> = self.panels.iter().map(|panel| panel.panel_id(cx)).collect();
498 let displayed = match self.constraints.is_collapsed() {
499 true => None,
500 false => ids.get(self.active_ix).copied(),
501 };
502
503 self.active
504 .reconcile(&ids, displayed)
505 .into_iter()
506 .filter_map(|(id, active)| {
507 ids.iter()
508 .position(|candidate| *candidate == id)
509 .map(|ix| (self.panels[ix].clone(), active))
510 })
511 .collect()
512 }
513
514 fn on_panel_drag_move(
517 &mut self,
518 drag: &DragMoveEvent<DragPanel>,
519 _: &mut Window,
520 cx: &mut Context<Self>,
521 ) {
522 let bounds = drag.bounds;
523 if !bounds.contains(&drag.event.position) {
524 self.clear_drop_indicator(cx);
525 return;
526 }
527
528 let placement = split_placement_at(bounds, drag.event.position);
529 let dragged = drag.drag(cx);
530 let source = DropPlaceholderBounds::new(
532 drag.event.position - dragged.drag_offset() - bounds.origin,
533 dragged.preview_size(),
534 );
535
536 self.sync_drop_placeholder(bounds, placement, dragged.drag_session_id(), source, cx);
537 }
538
539 fn on_item_drag_move(
544 &mut self,
545 drag: &DragMoveEvent<AnyDrag>,
546 _: &mut Window,
547 cx: &mut Context<Self>,
548 ) {
549 let bounds = drag.bounds;
550 if !bounds.contains(&drag.event.position) {
551 self.clear_drop_indicator(cx);
552 return;
553 }
554
555 let placement = split_placement_at(bounds, drag.event.position);
556 let source = DropPlaceholderBounds::for_placement(bounds, placement);
557
558 self.sync_drop_placeholder(bounds, placement, ITEM_DRAG_SESSION_ID, source, cx);
559 }
560
561 fn sync_drop_placeholder(
562 &mut self,
563 bounds: Bounds<Pixels>,
564 placement: Option<Placement>,
565 drag_session_id: u64,
566 source: DropPlaceholderBounds,
567 cx: &mut Context<Self>,
568 ) {
569 let to = DropPlaceholderBounds::for_placement(bounds, placement);
570
571 let restart = self.drop_indicator.is_none_or(|indicator| {
572 indicator.drag_session_id() != drag_session_id || indicator.placement() != placement
573 });
574
575 let (from, epoch) = match (restart, self.drop_indicator) {
576 (false, Some(indicator)) => (indicator.from(), indicator.epoch()),
577 (_, Some(indicator)) => {
578 let from = match indicator.drag_session_id() == drag_session_id {
579 true => indicator.to(),
580 false => source,
581 };
582 (from, indicator.epoch().wrapping_add(1))
583 }
584 (_, None) => (source, 0),
585 };
586
587 self.drop_indicator = Some(DropIndicator::new(
588 bounds,
589 placement,
590 from,
591 to,
592 drag_session_id,
593 epoch,
594 ));
595 cx.notify();
596 }
597
598 fn clear_drop_indicator(&mut self, cx: &mut Context<Self>) {
599 if self.drop_indicator.take().is_some() {
600 cx.notify();
601 }
602 }
603
604 fn emit_drag_drop(
607 &mut self,
608 item: &AnyDrag,
609 placement: Option<Placement>,
610 cx: &mut Context<Self>,
611 ) {
612 self.drop_indicator = None;
613 cx.emit(TabGroupEvent::DragDrop {
614 item: item.clone(),
615 target: DropTarget::new(self.node, placement),
616 });
617 cx.notify();
618 }
619
620 fn on_drop(
627 &mut self,
628 drag: &DragPanel,
629 ix: Option<usize>,
630 activate: bool,
631 cx: &mut Context<Self>,
632 ) {
633 let indicator = self.drop_indicator.take();
634 let placement = match ix {
635 Some(_) => None,
636 None => indicator.and_then(|indicator| indicator.placement()),
637 };
638
639 if drag.source() == self.node
643 && ix.is_none()
644 && (placement.is_none() || self.panels.len() == 1)
645 {
646 cx.notify();
647 return;
648 }
649
650 let target = match placement {
651 Some(placement) => InsertTarget::Split {
652 node: self.node,
653 placement,
654 size: None,
655 },
656 None => InsertTarget::Tabs {
657 node: self.node,
658 ix,
659 activate,
660 },
661 };
662
663 cx.emit(TabGroupEvent::Drop {
664 panel: drag.panel(),
665 source: drag.source(),
666 target,
667 });
668 cx.notify();
669 }
670}
671
672impl EventEmitter<TabGroupEvent> for TabGroup {}
673
674impl Focusable for TabGroup {
675 fn focus_handle(&self, cx: &App) -> FocusHandle {
676 match self.active_panel(cx) {
677 Some(panel) => panel.focus_handle(cx),
678 None => self.focus_handle.clone(),
679 }
680 }
681}
682
683impl Render for TabGroup {
684 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
685 let context = self.context(cx);
686 let renderer = self.renderer.clone();
687 let focus_handle = self.focus_handle(cx);
688 let droppable = context.droppable;
689 let indicator = context.drop_indicator;
690
691 renderer
692 .frame(&context, window, cx)
693 .test_support()
694 .flex()
704 .flex_col()
705 .size_full()
706 .overflow_hidden()
707 .track_focus(&focus_handle)
708 .tab_group()
709 .child(renderer.render_tab_bar(&context, window, cx))
710 .child(
711 renderer
712 .content_frame(&context, window, cx)
713 .test_support()
714 .flex()
718 .flex_col()
719 .when(!context.is_collapsed(), |this| this.flex_1())
720 .min_h(px(0.))
728 .overflow_hidden()
729 .when(droppable, |this| {
731 this.on_drag_move(cx.listener(Self::on_panel_drag_move))
732 .on_drop(cx.listener(|this, drag: &DragPanel, _, cx| {
733 this.on_drop(drag, None, true, cx)
734 }))
735 .on_drag_move(cx.listener(Self::on_item_drag_move))
736 .on_drop(cx.listener(|this, item: &AnyDrag, _, cx| {
737 let placement = this
738 .drop_indicator
739 .and_then(|indicator| indicator.placement());
740 this.emit_drag_drop(item, placement, cx);
741 }))
742 })
743 .map(|this| match context.active_panel.as_ref() {
744 Some(panel) => this.child(renderer.render_active_panel(
745 panel.view(),
746 &context,
747 window,
748 cx,
749 )),
750 None => this.children(renderer.render_empty(&context, window, cx)),
751 })
752 .when_some(indicator, |this, indicator| {
753 this.children(renderer.render_drop_indicator(indicator, window, cx))
754 }),
755 )
756 }
757}
758
759type SelectTabHandler = Rc<dyn Fn(usize, &mut Window, &mut App)>;
760type ClosePanelHandler = Rc<dyn Fn(PanelId, &mut Window, &mut App)>;
761type ToggleZoomHandler = Rc<dyn Fn(&mut Window, &mut App)>;
762type DropPanelHandler = Rc<dyn Fn(DragPanel, Option<usize>, bool, &mut Window, &mut App)>;
763type DropItemHandler = Rc<dyn Fn(AnyDrag, Option<Placement>, &mut Window, &mut App)>;
764
765#[derive(Clone)]
768pub struct TabGroupContext {
769 node: NodeId,
770 panels: Vec<Arc<dyn PanelView>>,
771 active_panel: Option<Arc<dyn PanelView>>,
772 active_ix: usize,
773 zoomed: bool,
774 constraints: TabGroupConstraints,
775 draggable: bool,
776 droppable: bool,
777 active_panel_closable: bool,
778 drop_indicator: Option<DropIndicator>,
779 on_select_tab: SelectTabHandler,
780 on_close: ClosePanelHandler,
781 on_toggle_zoom: ToggleZoomHandler,
782 on_drop_panel: DropPanelHandler,
783 on_drop_item: DropItemHandler,
784}
785
786impl TabGroupContext {
787 pub fn node(&self) -> NodeId {
790 self.node
791 }
792
793 pub fn panels(&self) -> &[Arc<dyn PanelView>] {
796 &self.panels
797 }
798
799 pub fn active_ix(&self) -> usize {
800 self.active_ix
801 }
802
803 pub fn active_panel(&self) -> Option<&Arc<dyn PanelView>> {
806 self.active_panel.as_ref()
807 }
808
809 pub fn drop_indicator(&self) -> Option<DropIndicator> {
810 self.drop_indicator
811 }
812
813 pub fn is_zoomed(&self) -> bool {
814 self.zoomed
815 }
816
817 pub fn is_collapsed(&self) -> bool {
818 self.constraints.is_collapsed()
819 }
820
821 pub fn is_closable(&self) -> bool {
823 self.active_panel_closable
824 }
825
826 pub fn is_panel_closable(&self, panel: PanelId, cx: &App) -> bool {
829 self.constraints.is_closable()
830 && self.draggable
831 && self
832 .panels
833 .iter()
834 .any(|candidate| candidate.panel_id(cx) == panel && candidate.closable(cx))
835 }
836
837 pub fn is_locked(&self) -> bool {
838 self.constraints.is_locked() || self.zoomed
839 }
840
841 pub fn is_draggable(&self) -> bool {
842 self.draggable
843 }
844
845 pub fn is_droppable(&self) -> bool {
846 self.droppable
847 }
848
849 pub fn select_tab(&self, ix: usize, window: &mut Window, cx: &mut App) {
850 (self.on_select_tab)(ix, window, cx);
851 }
852
853 pub fn close(&self, panel: PanelId, window: &mut Window, cx: &mut App) {
854 (self.on_close)(panel, window, cx);
855 }
856
857 pub fn toggle_zoom(&self, window: &mut Window, cx: &mut App) {
858 (self.on_toggle_zoom)(window, cx);
859 }
860
861 pub fn drag_panel(&self, ix: usize, cx: &App) -> Option<DragPanel> {
864 self.panels
865 .get(ix)
866 .map(|panel| DragPanel::new(panel.panel_id(cx), self.node))
867 }
868
869 pub fn drop_panel(
872 &self,
873 drag: DragPanel,
874 ix: Option<usize>,
875 activate: bool,
876 window: &mut Window,
877 cx: &mut App,
878 ) {
879 (self.on_drop_panel)(drag, ix, activate, window, cx);
880 }
881
882 pub fn drop_item(
885 &self,
886 item: AnyDrag,
887 placement: Option<Placement>,
888 window: &mut Window,
889 cx: &mut App,
890 ) {
891 (self.on_drop_item)(item, placement, window, cx);
892 }
893}
894
895#[allow(unused_variables)]
902pub trait TabGroupRenderer: 'static {
903 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 },
1046 TabGroupEvent::DragDrop { target, .. } => format!(
1047 "item onto {} at {:?}",
1048 target.node().as_u64(),
1049 target.placement()
1050 ),
1051 TabGroupEvent::ClosePanel { panel } => format!("close {}", panel.as_u64()),
1052 TabGroupEvent::ActiveChanged { ix } => format!("active {ix}"),
1053 TabGroupEvent::ZoomIn => "zoom in".into(),
1054 TabGroupEvent::ZoomOut => "zoom out".into(),
1055 }
1056 }
1057
1058 #[gpui::test]
1059 fn a_group_announces_its_first_panel_active(cx: &mut TestAppContext) {
1060 let log = log_of();
1061 let (group, panels, cx) = build_group(&log, &["a"], cx);
1062 cx.run_until_parked();
1063
1064 assert_eq!(drain_active(&log), vec![("a", true)]);
1065 let _ = (group, panels);
1066 }
1067
1068 #[gpui::test]
1069 fn selecting_another_tab_deactivates_then_activates(cx: &mut TestAppContext) {
1070 let log = log_of();
1071 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1072 cx.run_until_parked();
1073 drain_active(&log);
1074
1075 cx.update(|window, cx| {
1076 group.update(cx, |group, cx| group.select_tab(1, window, cx));
1077 });
1078 cx.run_until_parked();
1079
1080 assert_eq!(drain_active(&log), vec![("a", false), ("b", true)]);
1081 }
1082
1083 #[gpui::test]
1084 fn the_context_snapshots_the_group_state(cx: &mut TestAppContext) {
1085 let log = log_of();
1086 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1087
1088 let seen = cx.update(|_, cx| {
1089 group.update(cx, |group, cx| {
1090 let context = group.context(cx);
1091 (
1092 context.panels().len(),
1093 context.active_ix(),
1094 context.is_zoomed(),
1095 )
1096 })
1097 });
1098
1099 assert_eq!(seen, (2, 0, false));
1100 }
1101
1102 #[gpui::test]
1105 fn a_hidden_displayed_panel_is_still_the_active_one(cx: &mut TestAppContext) {
1106 let log = log_of();
1107 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1108 cx.update(|_, cx| {
1109 panels[0].update(cx, |panel, cx| panel.set_visible(false, cx));
1110 });
1111 cx.run_until_parked();
1112
1113 assert_eq!(drain_active(&log), vec![("a", true)]);
1114 assert_eq!(
1115 cx.update(|_, cx| group.read(cx).active_panel(cx).unwrap().panel_name(cx)),
1116 "b",
1117 "rendering falls back to the first visible panel"
1118 );
1119 }
1120
1121 #[gpui::test]
1124 fn a_panel_dropped_from_the_group_is_never_told_it_went_inactive(cx: &mut TestAppContext) {
1125 let log = log_of();
1126 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1127 cx.run_until_parked();
1128 drain(&log);
1129
1130 cx.update(|window, cx| {
1131 let remaining: Vec<Arc<dyn PanelView>> = vec![Arc::new(panels[1].clone())];
1132 group.update(cx, |group, cx| {
1133 group.sync_from_tree(remaining, 0, window, cx)
1134 });
1135 });
1136 cx.run_until_parked();
1137
1138 assert_eq!(drain_active(&log), vec![("b", true)]);
1139 }
1140
1141 #[gpui::test]
1142 fn selecting_a_tab_that_is_not_there_changes_nothing(cx: &mut TestAppContext) {
1143 let log = log_of();
1144 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1145 cx.run_until_parked();
1146 drain_active(&log);
1147 let events = record_events(&group, cx);
1148
1149 cx.update(|window, cx| {
1150 group.update(cx, |group, cx| group.select_tab(5, window, cx));
1151 });
1152 cx.run_until_parked();
1153
1154 assert_eq!(cx.update(|_, cx| group.read(cx).active_ix()), 0);
1155 assert!(events.borrow().is_empty());
1156 assert!(drain_active(&log).is_empty());
1157 }
1158
1159 #[gpui::test]
1160 fn a_locked_group_can_be_neither_dragged_nor_dropped_into(cx: &mut TestAppContext) {
1161 let log = log_of();
1162 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1163
1164 cx.update(|window, cx| {
1165 group.update(cx, |group, cx| {
1166 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1167 })
1168 });
1169 let unlocked = cx.update(|_, cx| {
1170 let context = group.read(cx).context(cx);
1171 (context.is_draggable(), context.is_droppable())
1172 });
1173 cx.update(|window, cx| {
1174 group.update(cx, |group, cx| {
1175 group.set_constraints(
1176 TabGroupConstraints::in_split(false).dock_locked(true),
1177 window,
1178 cx,
1179 )
1180 })
1181 });
1182 let locked = cx.update(|_, cx| {
1183 let context = group.read(cx).context(cx);
1184 (context.is_draggable(), context.is_droppable())
1185 });
1186
1187 assert_eq!(unlocked, (true, true));
1188 assert_eq!(locked, (false, false));
1189 }
1190
1191 #[gpui::test]
1194 fn a_zoomed_group_takes_no_drops(cx: &mut TestAppContext) {
1195 let log = log_of();
1196 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1197
1198 cx.update(|window, cx| {
1199 group.update(cx, |group, cx| {
1200 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1201 group.set_zoomed(true, window, cx);
1202 })
1203 });
1204
1205 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_droppable()));
1206 }
1207
1208 #[gpui::test]
1211 fn the_only_groups_last_visible_panel_cannot_be_dragged_out(cx: &mut TestAppContext) {
1212 let log = log_of();
1213 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1214
1215 cx.update(|window, cx| {
1216 group.update(cx, |group, cx| {
1217 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1218 })
1219 });
1220 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1221 cx.update(|window, cx| {
1222 group.update(cx, |group, cx| {
1223 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1224 })
1225 });
1226 let beside_others = cx.update(|_, cx| group.read(cx).context(cx).is_draggable());
1227
1228 assert!(!alone);
1229 assert!(beside_others);
1230 }
1231
1232 #[gpui::test]
1233 fn a_panel_from_another_group_lands_as_one_move(cx: &mut TestAppContext) {
1234 let log = log_of();
1235 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1236 let events = record_events(&group, cx);
1237
1238 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1239 cx.update(|_, cx| {
1240 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1241 });
1242 cx.run_until_parked();
1243
1244 assert_eq!(
1245 *events.borrow(),
1246 vec!["drop panel 99 from 7 into tabs 1 at None activate=true"]
1247 );
1248 }
1249
1250 #[gpui::test]
1251 fn dropping_a_panel_back_onto_its_own_group_is_ignored(cx: &mut TestAppContext) {
1252 let log = log_of();
1253 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1254 let events = record_events(&group, cx);
1255
1256 let panel = panel_id(&panels[0], cx);
1257 let drag = DragPanel::new(panel, group_node());
1258 cx.update(|_, cx| {
1259 group.update(cx, |group, cx| group.on_drop(&drag, None, true, cx));
1260 });
1261 cx.run_until_parked();
1262
1263 assert!(events.borrow().is_empty());
1264 }
1265
1266 #[gpui::test]
1267 fn a_hovering_split_turns_a_drop_into_a_split(cx: &mut TestAppContext) {
1268 let log = log_of();
1269 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1270 let events = record_events(&group, cx);
1271
1272 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1273 cx.update(|_, cx| {
1274 group.update(cx, |group, cx| {
1275 group.sync_drop_placeholder(
1276 content_bounds(),
1277 Some(Placement::Right),
1278 drag.drag_session_id(),
1279 DropPlaceholderBounds::for_placement(content_bounds(), None),
1280 cx,
1281 );
1282 group.on_drop(&drag, None, true, cx);
1283 });
1284 });
1285 cx.run_until_parked();
1286
1287 assert_eq!(*events.borrow(), vec!["drop panel 99 from 7 split 1 Right"]);
1288 }
1289
1290 #[gpui::test]
1293 fn a_tab_slot_overrides_the_hovering_split(cx: &mut TestAppContext) {
1294 let log = log_of();
1295 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1296 let events = record_events(&group, cx);
1297
1298 let drag = DragPanel::new(PanelId::from_u64(99), elsewhere());
1299 cx.update(|_, cx| {
1300 group.update(cx, |group, cx| {
1301 group.sync_drop_placeholder(
1302 content_bounds(),
1303 Some(Placement::Right),
1304 drag.drag_session_id(),
1305 DropPlaceholderBounds::for_placement(content_bounds(), None),
1306 cx,
1307 );
1308 group.on_drop(&drag, Some(0), true, cx);
1309 });
1310 });
1311 cx.run_until_parked();
1312
1313 assert_eq!(
1314 *events.borrow(),
1315 vec!["drop panel 99 from 7 into tabs 1 at Some(0) activate=true"]
1316 );
1317 }
1318
1319 #[gpui::test]
1322 fn a_lone_panel_cannot_split_out_of_its_own_group(cx: &mut TestAppContext) {
1323 let log = log_of();
1324 let (group, panels, cx) = build_group(&log, &["a"], cx);
1325 let events = record_events(&group, cx);
1326
1327 let drag = DragPanel::new(panel_id(&panels[0], cx), group_node());
1328 cx.update(|_, cx| {
1329 group.update(cx, |group, cx| {
1330 group.sync_drop_placeholder(
1331 content_bounds(),
1332 Some(Placement::Right),
1333 drag.drag_session_id(),
1334 DropPlaceholderBounds::for_placement(content_bounds(), None),
1335 cx,
1336 );
1337 group.on_drop(&drag, None, true, cx);
1338 });
1339 });
1340 cx.run_until_parked();
1341
1342 assert!(events.borrow().is_empty());
1343 }
1344
1345 #[gpui::test]
1346 fn the_placeholder_replays_only_when_the_target_moves(cx: &mut TestAppContext) {
1347 let log = log_of();
1348 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1349 let bounds = content_bounds();
1350 let source = DropPlaceholderBounds::new(point(px(10.), px(10.)), size(px(96.), px(30.)));
1351
1352 let sync = |placement: Option<Placement>, cx: &mut VisualTestContext| {
1353 cx.update(|_, cx| {
1354 group.update(cx, |group, cx| {
1355 group.sync_drop_placeholder(bounds, placement, 42, source, cx);
1356 group.drop_indicator.unwrap()
1357 })
1358 })
1359 };
1360
1361 let first = sync(Some(Placement::Left), cx);
1362 let again = sync(Some(Placement::Left), cx);
1363 let moved = sync(Some(Placement::Right), cx);
1364
1365 assert_eq!(first.epoch(), 0);
1366 assert_eq!(first.from(), source, "the first run flies in from the drag");
1367 assert_eq!(again.epoch(), 0, "an unchanged target keeps animating");
1368 assert_eq!(again.from(), source);
1369 assert_eq!(moved.epoch(), 1, "a moved target replays from where it was");
1370 assert_eq!(moved.from(), first.to());
1371 assert_eq!(
1372 moved.to(),
1373 DropPlaceholderBounds::for_placement(bounds, Some(Placement::Right))
1374 );
1375 }
1376
1377 #[gpui::test]
1380 fn the_indicator_is_withheld_when_no_drag_is_in_flight(cx: &mut TestAppContext) {
1381 let log = log_of();
1382 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1383
1384 let (stored, published) = cx.update(|_, cx| {
1385 group.update(cx, |group, cx| {
1386 group.sync_drop_placeholder(
1387 content_bounds(),
1388 Some(Placement::Top),
1389 42,
1390 DropPlaceholderBounds::for_placement(content_bounds(), None),
1391 cx,
1392 );
1393 (
1394 group.drop_indicator.is_some(),
1395 group.context(cx).drop_indicator().is_some(),
1396 )
1397 })
1398 });
1399
1400 assert!(stored);
1401 assert!(!published);
1402 }
1403
1404 #[gpui::test]
1405 fn a_host_drag_reports_the_group_and_the_edge_it_resolved(cx: &mut TestAppContext) {
1406 let log = log_of();
1407 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1408 let events = record_events(&group, cx);
1409
1410 cx.update(|_, cx| {
1411 group.update(cx, |group, cx| {
1412 group.emit_drag_drop(&AnyDrag::new(7u32), Some(Placement::Bottom), cx)
1413 });
1414 });
1415 cx.run_until_parked();
1416
1417 assert_eq!(*events.borrow(), vec!["item onto 1 at Some(Bottom)"]);
1418 }
1419
1420 #[gpui::test]
1421 fn closing_asks_the_container_rather_than_editing_the_group(cx: &mut TestAppContext) {
1422 let log = log_of();
1423 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1424 let events = record_events(&group, cx);
1425 let panel = panel_id(&panels[1], cx);
1426
1427 cx.update(|_, cx| {
1428 group.update(cx, |group, cx| {
1429 group.close_panel(panel, cx);
1430 group.close_panel(PanelId::from_u64(4242), cx);
1432 });
1433 });
1434 cx.run_until_parked();
1435
1436 assert_eq!(*events.borrow(), vec![format!("close {}", panel.as_u64())]);
1437 assert_eq!(
1438 cx.update(|_, cx| group.read(cx).panels().len()),
1439 2,
1440 "the group still holds both panels until the container edits the tree"
1441 );
1442 }
1443
1444 #[gpui::test]
1445 fn zooming_toggles_and_tells_the_displayed_panel(cx: &mut TestAppContext) {
1446 let log = log_of();
1447 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1448 let events = record_events(&group, cx);
1449
1450 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1451 cx.run_until_parked();
1452 cx.update(|window, cx| group.update(cx, |group, cx| group.toggle_zoom(window, cx)));
1453 cx.run_until_parked();
1454
1455 assert_eq!(*events.borrow(), vec!["zoom in", "zoom out"]);
1456 assert!(!cx.update(|_, cx| group.read(cx).is_zoomed()));
1457 assert_eq!(
1458 drain(&log)
1459 .into_iter()
1460 .filter(|(_, signal)| matches!(signal, PanelSignal::Zoomed(_)))
1461 .collect::<Vec<_>>(),
1462 vec![
1463 ("a", PanelSignal::Zoomed(true)),
1464 ("a", PanelSignal::Zoomed(false))
1465 ],
1466 "the old TabPanel sent this to itself, where the default no-op swallowed it"
1467 );
1468 }
1469
1470 #[gpui::test]
1474 fn a_drag_over_a_locked_group_resolves_nothing(cx: &mut TestAppContext) {
1475 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1476 cx.update(|window, cx| {
1477 group.update(cx, |group, cx| {
1478 group.set_constraints(
1479 TabGroupConstraints::in_split(false).dock_locked(true),
1480 window,
1481 cx,
1482 )
1483 })
1484 });
1485 cx.update(|window, cx| window.draw(cx).clear(cx));
1486
1487 drag_from_the_tab_into_the_content(cx);
1488
1489 assert!(
1490 cx.update(|_, cx| group.read(cx).drop_indicator.is_none()),
1491 "a locked group installs no drag-move listener, so nothing resolved"
1492 );
1493 }
1494
1495 #[gpui::test]
1498 fn a_group_the_container_sealed_shut_refuses_to_close(cx: &mut TestAppContext) {
1499 let log = log_of();
1500 let (group, panels, cx) = build_group(&log, &["a", "b"], cx);
1501 let events = record_events(&group, cx);
1502 let panel = panel_id(&panels[0], cx);
1503
1504 cx.update(|window, cx| {
1505 group.update(cx, |group, cx| {
1506 group.set_constraints(
1507 TabGroupConstraints::in_split(false).closable(false),
1508 window,
1509 cx,
1510 );
1511 group.close_panel(panel, cx);
1512 })
1513 });
1514 cx.run_until_parked();
1515
1516 assert!(!cx.update(|_, cx| group.read(cx).context(cx).is_closable()));
1517 assert!(events.borrow().is_empty());
1518 }
1519
1520 #[gpui::test]
1526 fn the_only_groups_last_panel_is_not_closable(cx: &mut TestAppContext) {
1527 let log = log_of();
1528 let (group, _panels, cx) = build_group(&log, &["a"], cx);
1529
1530 cx.update(|window, cx| {
1531 group.update(cx, |group, cx| {
1532 group.set_constraints(TabGroupConstraints::in_split(true), window, cx)
1533 })
1534 });
1535 let alone = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1536
1537 cx.update(|window, cx| {
1538 group.update(cx, |group, cx| {
1539 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1540 })
1541 });
1542 let beside_a_sibling = cx.update(|_, cx| group.read(cx).context(cx).is_closable());
1543
1544 assert!(!alone);
1545 assert!(beside_a_sibling);
1546 }
1547
1548 #[gpui::test]
1551 fn collapsing_deactivates_the_displayed_panel_and_expanding_restores_it(
1552 cx: &mut TestAppContext,
1553 ) {
1554 let log = log_of();
1555 let (group, _panels, cx) = build_group(&log, &["a", "b"], cx);
1556 cx.run_until_parked();
1557 assert_eq!(drain_active(&log), vec![("a", true)]);
1558
1559 cx.update(|window, cx| {
1560 group.update(cx, |group, cx| {
1561 group.set_constraints(
1562 TabGroupConstraints::in_split(false).collapsed(true),
1563 window,
1564 cx,
1565 )
1566 })
1567 });
1568 cx.run_until_parked();
1569 assert_eq!(drain_active(&log), vec![("a", false)]);
1570
1571 cx.update(|window, cx| {
1572 group.update(cx, |group, cx| {
1573 group.set_constraints(TabGroupConstraints::in_split(false), window, cx)
1574 })
1575 });
1576 cx.run_until_parked();
1577 assert_eq!(drain_active(&log), vec![("a", true)]);
1578 }
1579
1580 fn skin_content() -> Bounds<Pixels> {
1586 Bounds {
1587 origin: point(px(100.), px(50.)),
1588 size: size(px(400.), px(300.)),
1589 }
1590 }
1591
1592 struct RecordingRenderer {
1595 calls: Rc<RefCell<Vec<&'static str>>>,
1596 }
1597
1598 impl RecordingRenderer {
1599 fn saw(&self, call: &'static str) {
1600 self.calls.borrow_mut().push(call);
1601 }
1602 }
1603
1604 impl TabGroupRenderer for RecordingRenderer {
1605 fn frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1606 self.saw("frame");
1607 div().id("skin-frame").relative().size_full()
1608 }
1609
1610 fn content_frame(&self, _: &TabGroupContext, _: &mut Window, _: &mut App) -> Stateful<Div> {
1611 self.saw("content_frame");
1612 div()
1613 .id("skin-content")
1614 .absolute()
1615 .left(skin_content().origin.x)
1616 .top(skin_content().origin.y)
1617 .w(skin_content().size.width)
1618 .h(skin_content().size.height)
1619 }
1620
1621 fn render_tab_bar(
1622 &self,
1623 group: &TabGroupContext,
1624 _: &mut Window,
1625 cx: &mut App,
1626 ) -> AnyElement {
1627 self.saw("tab_bar");
1628 div()
1629 .id("skin-tab")
1630 .absolute()
1631 .left(px(0.))
1632 .top(px(0.))
1633 .w(px(80.))
1634 .h(px(24.))
1635 .when_some(group.drag_panel(0, cx), |this, drag| {
1636 this.on_drag(drag, |drag, offset, _, cx| {
1637 drag.set_drag_offset(offset);
1638 cx.new(|_| drag.clone())
1639 })
1640 })
1641 .into_any_element()
1642 }
1643
1644 fn render_active_panel(
1645 &self,
1646 panel: AnyView,
1647 _: &TabGroupContext,
1648 _: &mut Window,
1649 _: &mut App,
1650 ) -> AnyElement {
1651 self.saw("active_panel");
1652 panel.into_any_element()
1653 }
1654
1655 fn render_empty(
1656 &self,
1657 _: &TabGroupContext,
1658 _: &mut Window,
1659 _: &mut App,
1660 ) -> Option<AnyElement> {
1661 self.saw("empty");
1662 None
1663 }
1664
1665 fn render_drop_indicator(
1666 &self,
1667 _: DropIndicator,
1668 _: &mut Window,
1669 _: &mut App,
1670 ) -> Option<AnyElement> {
1671 self.saw("drop_indicator");
1672 None
1673 }
1674 }
1675
1676 fn build_skinned_group<'a>(
1677 names: &[&'static str],
1678 cx: &'a mut TestAppContext,
1679 ) -> (
1680 Entity<TabGroup>,
1681 Rc<RefCell<Vec<&'static str>>>,
1682 &'a mut VisualTestContext,
1683 ) {
1684 let calls: Rc<RefCell<Vec<&'static str>>> = Rc::default();
1685 let renderer = Rc::new(RecordingRenderer {
1686 calls: calls.clone(),
1687 });
1688 let (group, cx) = cx.add_window_view(|window, cx| {
1689 TabGroup::new(NodeId::from_u64(1), window, cx).with_renderer(renderer)
1690 });
1691
1692 let names = names.to_vec();
1693 let log = log_of();
1694 cx.update(|window, cx| {
1695 let views: Vec<Arc<dyn PanelView>> = names
1696 .iter()
1697 .map(|name| {
1698 Arc::new(crate::dock::test_support::TestPanel::logging(
1699 name, &log, cx,
1700 )) as _
1701 })
1702 .collect();
1703 group.update(cx, |group, cx| {
1704 group.set_constraints(TabGroupConstraints::in_split(false), window, cx);
1705 group.sync_from_tree(views, 0, window, cx);
1706 });
1707 });
1708 cx.run_until_parked();
1709 calls.borrow_mut().clear();
1710
1711 (group, calls, cx)
1712 }
1713
1714 fn drag_from_the_tab_into_the_content(cx: &mut VisualTestContext) {
1717 cx.simulate_mouse_down(
1718 point(px(20.), px(10.)),
1719 MouseButton::Left,
1720 Modifiers::none(),
1721 );
1722 cx.simulate_mouse_move(
1723 point(px(30.), px(14.)),
1724 MouseButton::Left,
1725 Modifiers::none(),
1726 );
1727 cx.simulate_mouse_move(
1728 point(px(450.), px(200.)),
1729 MouseButton::Left,
1730 Modifiers::none(),
1731 );
1732 }
1733
1734 #[gpui::test]
1737 fn the_renderer_composes_frame_then_tab_bar_then_content(cx: &mut TestAppContext) {
1738 let (_group, calls, cx) = build_skinned_group(&["a"], cx);
1739
1740 cx.update(|window, cx| window.draw(cx).clear(cx));
1741
1742 assert_eq!(
1743 *calls.borrow(),
1744 vec!["frame", "tab_bar", "content_frame", "active_panel"],
1745 "the tab bar is a sibling of the content, not a child of it"
1746 );
1747 }
1748
1749 #[gpui::test]
1752 fn an_empty_group_asks_the_renderer_for_its_empty_state(cx: &mut TestAppContext) {
1753 let (_group, calls, cx) = build_skinned_group(&[], cx);
1754
1755 cx.update(|window, cx| window.draw(cx).clear(cx));
1756
1757 assert_eq!(
1758 *calls.borrow(),
1759 vec!["frame", "tab_bar", "content_frame", "empty"]
1760 );
1761 }
1762
1763 #[gpui::test]
1767 fn the_content_frame_is_what_a_drag_is_measured_against(cx: &mut TestAppContext) {
1768 let (group, calls, cx) = build_skinned_group(&["a", "b"], cx);
1769 cx.update(|window, cx| window.draw(cx).clear(cx));
1770
1771 drag_from_the_tab_into_the_content(cx);
1772
1773 let indicator = cx
1774 .update(|_, cx| group.read(cx).drop_indicator)
1775 .expect("the content frame's drag-move listener ran");
1776
1777 assert_eq!(
1778 indicator.bounds(),
1779 skin_content(),
1780 "measured against the content frame, not the full-size outer frame"
1781 );
1782 assert_eq!(indicator.placement(), Some(Placement::Right));
1783
1784 cx.update(|window, cx| window.draw(cx).clear(cx));
1785 assert!(
1786 calls.borrow().contains(&"drop_indicator"),
1787 "a published indicator is handed to the renderer to draw"
1788 );
1789 }
1790
1791 #[gpui::test]
1794 fn dropping_on_the_content_frame_reports_the_move(cx: &mut TestAppContext) {
1795 let (group, _calls, cx) = build_skinned_group(&["a", "b"], cx);
1796 let events = record_events(&group, cx);
1797 cx.update(|window, cx| window.draw(cx).clear(cx));
1798
1799 drag_from_the_tab_into_the_content(cx);
1800 cx.simulate_mouse_up(
1801 point(px(450.), px(200.)),
1802 MouseButton::Left,
1803 Modifiers::none(),
1804 );
1805 cx.run_until_parked();
1806
1807 assert_eq!(events.borrow().len(), 1, "got {:?}", events.borrow());
1808 assert!(
1809 events.borrow()[0].ends_with("split 1 Right"),
1810 "got {:?}",
1811 events.borrow()
1812 );
1813 assert!(cx.update(|_, cx| group.read(cx).drop_indicator.is_none()));
1814 }
1815}