1use super::adaptive::{WindowSizeClass, WindowWidthClass, sized_content};
12use super::pane_logic::*;
13use crate::{
14 color::ColorRole,
15 motion::{self, MotionToken},
16 theme::color,
17 tokens::typography::{BASE_PX, rem},
18};
19use gpui::{prelude::*, *};
20use std::rc::Rc;
21
22#[cfg(not(target_family = "wasm"))]
23use std::time::Instant;
24#[cfg(target_family = "wasm")]
25use web_time::Instant;
26
27const DELAYED_RATIO: f32 = 0.1;
29const HANDLE_TARGET: f32 = 48.;
32const HANDLE_BAR: (f32, f32) = (4., 48.);
34const HANDLE_BAR_ACTIVE: (f32, f32) = (12., 52.);
36const CLICK_SLOP: f32 = 3.;
38const HANDLE_BAR_ACTIVE_RADIUS: f32 = 12.;
40
41#[derive(Clone, Copy)]
42struct PaneRun {
43 from: PaneRect,
44 to: PaneRect,
45 delayed: bool,
46 started: Instant,
47}
48
49struct ScaffoldState {
51 value: Option<ThreePaneValue>,
52 size: (f32, f32),
53 targets: [Option<PaneRect>; 3],
54 runs: [Option<PaneRun>; 3],
55 first_pane_width: Option<f32>,
56 dragging: Option<f32>,
57 pressed_at: f32,
59
60 history_len: Option<usize>,
61 focus_stack: Vec<FocusHandle>,
62 pane_focus: [FocusHandle; 3],
63 handle_focus: FocusHandle,
64}
65
66impl ScaffoldState {
67 fn sample(&self, role: usize, now: Instant) -> (Option<PaneRect>, bool) {
68 let Some(run) = self.runs[role] else {
69 return (self.targets[role], false);
70 };
71 let curve = motion::curve(MotionToken::DefaultSpatial);
72 let total = curve.duration().as_secs_f32().max(f32::EPSILON);
73 let mut elapsed = now.saturating_duration_since(run.started).as_secs_f32() / total;
74 if run.delayed {
75 elapsed = (elapsed - DELAYED_RATIO) / (1. - DELAYED_RATIO);
76 }
77 if elapsed >= 1. {
78 return (Some(run.to), false);
79 }
80 let t = (curve.ease())(elapsed.max(0.));
81 (Some(run.from.lerp(&run.to, t)), true)
82 }
83}
84
85#[derive(gpui::IntoElement)]
94pub struct PaneScaffold<K: Clone + PartialEq + 'static> {
95 id: ElementId,
96 navigator: Entity<PaneNavigator<K>>,
97 order: [PaneRole; 3],
98 rule: PaneRule,
99 size_class: Option<WindowSizeClass>,
100 rtl: bool,
101 panes: [Option<(SharedString, AnyElement)>; 3],
102 preferred_widths: [Option<f32>; 3],
103 handle: Option<SharedString>,
104 anchors: Rc<[PaneAnchor]>,
105}
106
107impl<K: Clone + PartialEq + 'static> PaneScaffold<K> {
108 fn new(
109 id: impl Into<ElementId>,
110 navigator: &Entity<PaneNavigator<K>>,
111 order: [PaneRole; 3],
112 ) -> Self {
113 Self {
114 id: id.into(),
115 navigator: navigator.clone(),
116 order,
117 rule: PaneRule::default(),
118 size_class: None,
119 rtl: false,
120 panes: [None, None, None],
121 preferred_widths: [None; 3],
122 handle: None,
123 anchors: Rc::from(DEFAULT_PANE_ANCHORS),
124 }
125 }
126
127 pub fn list_detail(id: impl Into<ElementId>, navigator: &Entity<PaneNavigator<K>>) -> Self {
129 Self::new(id, navigator, LIST_DETAIL_ORDER)
130 }
131
132 pub fn supporting_pane(id: impl Into<ElementId>, navigator: &Entity<PaneNavigator<K>>) -> Self {
134 Self::new(id, navigator, SUPPORTING_PANE_ORDER)
135 }
136
137 pub fn pane(
139 mut self,
140 role: PaneRole,
141 label: impl Into<SharedString>,
142 content: impl IntoElement,
143 ) -> Self {
144 self.panes[role.index()] = Some((label.into(), content.into_any_element()));
145 self
146 }
147
148 pub fn rule(mut self, rule: PaneRule) -> Self {
150 self.rule = rule;
151 self
152 }
153
154 pub fn size_class(mut self, size_class: Option<WindowSizeClass>) -> Self {
158 self.size_class = size_class;
159 self
160 }
161
162 pub fn preferred_width(mut self, role: PaneRole, width: f32) -> Self {
164 self.preferred_widths[role.index()] = (width.is_finite() && width >= 0.).then_some(width);
165 self
166 }
167
168 pub fn rtl(mut self, rtl: bool) -> Self {
170 self.rtl = rtl;
171 self
172 }
173
174 pub fn drag_handle(mut self, label: impl Into<SharedString>) -> Self {
177 self.handle = Some(label.into());
178 self
179 }
180
181 pub fn anchors(mut self, anchors: impl IntoIterator<Item = PaneAnchor>) -> Self {
183 self.anchors = anchors.into_iter().collect();
184 self
185 }
186}
187
188const PANE_SELECTORS: [&str; 3] = ["pane-primary", "pane-secondary", "pane-tertiary"];
189
190fn settle_handle(
192 state: &Entity<ScaffoldState>,
193 position: f32,
194 spacer: f32,
195 window_notify: &mut App,
196) {
197 state.update(window_notify, |state, cx| {
198 state.first_pane_width = Some((position - (spacer / 2.).trunc()).max(0.));
199 state.dragging = None;
200 cx.notify();
201 });
202}
203
204impl<K: Clone + PartialEq + 'static> RenderOnce for PaneScaffold<K> {
205 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
206 let Self {
207 id,
208 navigator,
209 order,
210 rule,
211 size_class,
212 rtl,
213 panes,
214 preferred_widths,
215 handle,
216 anchors,
217 } = self;
218 let order = if rtl {
219 [order[2], order[1], order[0]]
220 } else {
221 order
222 };
223 let builder_id = id.clone();
224 div()
225 .id(id)
226 .size_full()
227 .overflow_hidden()
228 .child(sized_content(
229 (builder_id.clone(), "panes"),
230 move |bounds, window, cx| {
231 let size = bounds.size;
232 let (width, height) = (f32::from(size.width), f32::from(size.height));
233 let directive = pane_directive(
234 size_class.unwrap_or_else(|| WindowSizeClass::of_size(size)),
235 rule,
236 );
237 navigator.update(cx, |navigator, cx| {
238 if navigator.set_directive(directive) {
239 cx.notify();
240 }
241 });
242 let (value, strategies, history_len, destination) = {
243 let navigator = navigator.read(cx);
244 (
245 navigator.value(),
246 *navigator.strategies(),
247 navigator.history().len(),
248 navigator.current().map(|current| current.pane),
249 )
250 };
251 let state =
252 window.use_keyed_state((builder_id.clone(), "pane-state"), cx, |_, cx| {
253 ScaffoldState {
254 value: None,
255 size: (0., 0.),
256 targets: [None; 3],
257 runs: [None; 3],
258 first_pane_width: None,
259 dragging: None,
260 pressed_at: 0.,
261
262 history_len: None,
263 focus_stack: Vec::new(),
264 pane_focus: [
265 cx.focus_handle(),
266 cx.focus_handle(),
267 cx.focus_handle(),
268 ],
269 handle_focus: cx.focus_handle().tab_stop(true),
270 }
271 });
272 let now = cx.background_executor().now();
273 let reduced = cx.reduce_motion();
274 let (dragging, first_pane_width) = {
275 let state = state.read(cx);
276 (state.dragging, state.first_pane_width)
277 };
278 let layout = three_pane_layout(&PaneLayoutInput {
279 width,
280 height,
281 directive,
282 value,
283 order: &order,
284 strategies: &strategies,
285 present: [panes[0].is_some(), panes[1].is_some(), panes[2].is_some()],
286 preferred_widths,
287 dragging_offset: dragging,
288 first_pane_width,
289 });
290 state.update(cx, |state, _| {
291 let origin: [Option<PaneRect>; 3] =
292 std::array::from_fn(|role| state.sample(role, now).0);
293 let resized = state.size != (width, height);
294 match state.value {
295 Some(previous) if !resized && !reduced => {
297 if previous != value {
298 let motions = pane_motions(previous, value, &order);
299 let transitions = pane_transitions(
300 &motions,
301 &order,
302 &origin,
303 &layout.panes,
304 width,
305 );
306 for (role, transition) in transitions.iter().enumerate() {
307 state.runs[role] = transition.map(|transition| PaneRun {
308 from: transition.from,
309 to: transition.to,
310 delayed: transition.motion.delayed(),
311 started: now,
312 });
313 }
314 } else if dragging.is_some() {
315 state.runs = [None; 3];
316 } else {
317 for (role, from) in origin.iter().enumerate() {
318 if let (Some(from), Some(to)) = (*from, layout.panes[role])
319 && state.targets[role] != Some(to)
320 {
321 state.runs[role] = Some(PaneRun {
322 from,
323 to,
324 delayed: false,
325 started: now,
326 });
327 }
328 }
329 }
330 }
331 _ => state.runs = [None; 3],
332 }
333 state.value = Some(value);
334 state.size = (width, height);
335 state.targets = layout.panes;
336 });
337
338 let previous_len = state.read(cx).history_len;
340 if previous_len != Some(history_len) {
341 let target =
342 destination.map(|role| state.read(cx).pane_focus[role.index()].clone());
343 let grew = previous_len.is_some_and(|len| history_len > len);
344 let shrank = previous_len.is_some_and(|len| history_len < len);
345 let focused = window.focused(cx);
346 let restore = state.update(cx, |state, _| {
347 state.history_len = Some(history_len);
348 if grew {
349 state.focus_stack.extend(focused);
350 None
351 } else if shrank {
352 state.focus_stack.pop()
353 } else {
354 None
355 }
356 });
357 let next = if grew { target } else { restore.or(target) };
358 if let Some(next) = next {
359 window.defer(cx, move |window, cx| next.focus(window, cx));
360 }
361 }
362
363 let mut running = false;
364 let mut shown = [None; 3];
365 {
366 let current = state.read(cx);
367 for (slot, role) in order.into_iter().enumerate() {
368 let index = role.index();
369 let (rect, moving) = current.sample(index, now);
370 running |= moving;
371 let settled_hidden = layout.panes[index].is_none() && !moving;
373 shown[slot] = rect.filter(|_| !settled_hidden).map(|rect| (role, rect));
374 }
375 }
376
377 if !running {
378 let idle = state.read(cx).runs.iter().any(Option::is_some);
379 if idle {
380 state.update(cx, |state, _| state.runs = [None; 3]);
381 }
382 } else {
383 window.request_animation_frame();
384 }
385
386 let mut panes = panes;
387 let mut root = div().relative().size_full();
388 for (role, rect) in shown.into_iter().flatten() {
389 let Some((label, content)) = panes[role.index()].take() else {
390 continue;
391 };
392 let focus = state.read(cx).pane_focus[role.index()].clone();
393
394 if rect.width <= 0. {
395 continue;
396 }
397 let selector = PANE_SELECTORS[role.index()];
398 root = root.child(
399 div()
400 .id((builder_id.clone(), selector))
401 .debug_selector(move || selector.into())
402 .role(Role::Region)
403 .aria_label(label)
404 .track_focus(&focus)
405 .absolute()
406 .left(px(rect.x))
407 .top(px(rect.y))
408 .w(px(rect.width))
409 .h(px(rect.height))
410 .overflow_y_scroll()
411 .child(content),
412 );
413 }
414 if let (Some(label), Some(offset)) = (handle, layout.handle_offset) {
415 root = root.child(drag_handle(
416 (builder_id.clone(), "pane-handle"),
417 label,
418 offset,
419 bounds,
420 directive.horizontal_spacer,
421 anchors,
422 &state,
423 window,
424 cx,
425 ));
426 }
427 root.into_any_element()
428 },
429 ))
430 }
431}
432
433#[allow(clippy::too_many_arguments)]
436fn release_handle(
437 state: &Entity<ScaffoldState>,
438 anchors: &[PaneAnchor],
439 width: f32,
440 offset: f32,
441 spacer: f32,
442 x: f32,
443 cx: &mut App,
444) {
445 let current = state.read(cx);
446 if current.dragging.is_none() {
447 return;
448 }
449 let clicked = (x - current.pressed_at).abs() < CLICK_SLOP;
450 let position = if clicked {
451 next_pane_anchor(anchors, width, offset)
452 } else {
453 closest_pane_anchor(anchors, width, x)
454 };
455 settle_handle(state, position.unwrap_or(x), spacer, cx);
456}
457
458#[allow(clippy::too_many_arguments)]
460fn drag_handle(
461 id: impl Into<ElementId>,
462 label: SharedString,
463 offset: f32,
464 bounds: Bounds<Pixels>,
465 spacer: f32,
466 anchors: Rc<[PaneAnchor]>,
467 state: &Entity<ScaffoldState>,
468 window: &mut Window,
469 cx: &mut App,
470) -> AnyElement {
471 let width = f32::from(bounds.size.width);
472 let origin = bounds.origin.x;
473 let local = move |x: Pixels| f32::from(x - origin).clamp(0., width.max(0.));
474 let (dragged, focus) = {
475 let state = state.read(cx);
476 (state.dragging.is_some(), state.handle_focus.clone())
477 };
478 let target = f32::from(rem(HANDLE_TARGET).to_pixels(window.rem_size()));
479 let (bar_w, bar_h) = if dragged {
480 HANDLE_BAR_ACTIVE
481 } else {
482 HANDLE_BAR
483 };
484 let keys = {
485 let state = state.clone();
486 let anchors = anchors.clone();
487 move |event: &KeyDownEvent, _: &mut Window, cx: &mut App| {
488 let position = match event.keystroke.key.as_str() {
489 "left" => previous_pane_anchor(&anchors, width, offset),
490 "right" | "enter" => next_pane_anchor(&anchors, width, offset),
491 "home" => Some(0.),
492 "end" => Some(width),
493 _ => None,
494 };
495 if let Some(position) = position {
496 settle_handle(&state, position, spacer, cx);
497 cx.stop_propagation();
498 }
499 }
500 };
501 let press = {
502 let state = state.clone();
503 move |event: &MouseDownEvent, window: &mut Window, cx: &mut App| {
504 let focus = state.read(cx).handle_focus.clone();
505 focus.focus(window, cx);
506 state.update(cx, |state, cx| {
507 state.dragging = Some(offset);
508 state.pressed_at = local(event.position.x);
509 cx.notify();
510 });
511 cx.stop_propagation();
512 }
513 };
514 let up = {
517 let state = state.clone();
518 let anchors = anchors.clone();
519 move |event: &MouseUpEvent, _: &mut Window, cx: &mut App| {
520 release_handle(
521 &state,
522 &anchors,
523 width,
524 offset,
525 spacer,
526 local(event.position.x),
527 cx,
528 );
529 }
530 };
531 let tracking = dragged.then(|| {
533 let state = state.clone();
534 canvas(
535 |_, _, _| {},
536 move |_, _, window, _| {
537 let moving = state.clone();
538 window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
539 if phase == DispatchPhase::Capture
540 && event.dragging()
541 && moving.read(cx).dragging.is_some()
542 {
543 let x = local(event.position.x);
544 moving.update(cx, |state, cx| {
545 state.dragging = Some(x);
546 cx.notify();
547 });
548 }
549 });
550 window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
551 if phase == DispatchPhase::Capture {
552 release_handle(
553 &state,
554 &anchors,
555 width,
556 offset,
557 spacer,
558 local(event.position.x),
559 cx,
560 );
561 }
562 });
563 },
564 )
565 .absolute()
566 .inset_0()
567 });
568 div()
569 .absolute()
570 .inset_0()
571 .children(tracking)
572 .child(
573 div()
574 .id(id.into())
575 .debug_selector(|| "pane-drag-handle".into())
576 .role(Role::Splitter)
577 .aria_label(label)
578 .track_focus(&focus)
579 .absolute()
580 .left(px(offset - target / 2.))
581 .top(px((f32::from(bounds.size.height) - target) / 2.))
582 .size(px(target))
583 .flex()
584 .items_center()
585 .justify_center()
586 .cursor_col_resize()
587 .rounded_full()
588 .focus_visible(|ring| ring.border_2().border_color(color(ColorRole::SECONDARY)))
589 .on_key_down(keys)
590 .on_mouse_down(MouseButton::Left, press)
591 .on_mouse_up(MouseButton::Left, up)
592 .child(
593 div()
594 .w(rem(bar_w))
595 .h(rem(bar_h))
596 .when(!dragged, |bar| bar.rounded_full())
597 .when(dragged, |bar| bar.rounded(rem(HANDLE_BAR_ACTIVE_RADIUS)))
598 .bg(color(if dragged {
599 ColorRole::ON_SURFACE
600 } else {
601 ColorRole::OUTLINE
602 })),
603 ),
604 )
605 .into_any_element()
606}
607
608pub const FEED_MARGIN_COMPACT: f32 = 16.;
610pub const FEED_MARGIN: f32 = 24.;
612pub const FEED_GAP: f32 = 8.;
614pub const FEED_COLUMN_MIN_WIDTH: f32 = 240.;
616
617#[derive(Clone, Copy, Debug, PartialEq, Eq)]
619pub struct FeedMetrics {
620 pub columns: u16,
622 pub margin: u16,
624}
625
626pub fn feed_metrics(width: f32, design_px: f32) -> FeedMetrics {
629 if WindowWidthClass::of(width) == WindowWidthClass::Compact {
630 return FeedMetrics {
631 columns: 1,
632 margin: FEED_MARGIN_COMPACT as u16,
633 };
634 }
635 let inner = width - 2. * FEED_MARGIN * design_px;
636 let gap = FEED_GAP * design_px;
637 let columns = ((inner + gap) / (FEED_COLUMN_MIN_WIDTH * design_px + gap)).floor();
638 FeedMetrics {
639 columns: if columns.is_finite() {
640 columns.clamp(1., u16::MAX as f32) as u16
641 } else {
642 1
643 },
644 margin: FEED_MARGIN as u16,
645 }
646}
647
648#[derive(gpui::IntoElement)]
664pub struct FeedLayout {
665 id: ElementId,
666 items: Vec<(AnyElement, bool)>,
667}
668
669impl FeedLayout {
670 pub fn new(id: impl Into<ElementId>) -> Self {
672 Self {
673 id: id.into(),
674 items: Vec::new(),
675 }
676 }
677
678 pub fn item(mut self, item: impl IntoElement) -> Self {
680 self.items.push((item.into_any_element(), false));
681 self
682 }
683
684 pub fn wide_item(mut self, item: impl IntoElement) -> Self {
686 self.items.push((item.into_any_element(), true));
687 self
688 }
689}
690
691impl RenderOnce for FeedLayout {
692 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
693 let design_px = f32::from(window.rem_size()) / BASE_PX;
694 let initial = feed_metrics(f32::from(window.viewport_size().width), design_px);
695 let metrics = window.use_keyed_state((self.id.clone(), "feed-metrics"), cx, |_, _| initial);
696 let current = *metrics.read(cx);
697 let columns = current.columns.max(1);
698 div()
699 .id(self.id)
700 .debug_selector(|| "feed-layout".into())
701 .relative()
702 .w_full()
703 .grid()
704 .grid_cols(columns)
705 .gap(rem(FEED_GAP))
706 .px(rem(f32::from(current.margin)))
707 .child(
708 canvas(
709 move |bounds, _, cx| {
710 let measured = feed_metrics(f32::from(bounds.size.width), design_px);
711 metrics.update(cx, |metrics, cx| {
712 if *metrics != measured {
713 *metrics = measured;
714 cx.notify();
715 }
716 });
717 },
718 |_, _, _, _| {},
719 )
720 .absolute()
721 .inset_0(),
722 )
723 .children(self.items.into_iter().map(|(item, wide)| {
724 div()
725 .min_w_0()
726 .when(wide, |cell| cell.col_span(2.min(columns)))
727 .child(item)
728 }))
729 }
730}
731
732#[cfg(test)]
733mod tests {
734 use super::*;
735 use core::prelude::v1::test;
736
737 #[test]
738 fn the_feed_has_one_column_when_compact_then_as_many_as_fit() {
739 assert_eq!(
740 feed_metrics(360., 1.),
741 FeedMetrics {
742 columns: 1,
743 margin: 16
744 }
745 );
746 assert_eq!(
747 feed_metrics(599., 1.),
748 FeedMetrics {
749 columns: 1,
750 margin: 16
751 }
752 );
753 assert_eq!(feed_metrics(600., 1.).columns, 2);
755 assert_eq!(feed_metrics(600., 1.).margin, 24);
756 assert_eq!(feed_metrics(1000., 1.).columns, 3);
758 assert_eq!(feed_metrics(1000., 2.).columns, 1);
760 assert_eq!(feed_metrics(f32::NAN, 1.).columns, 1);
761 assert_eq!(feed_metrics(0., 1.).columns, 1);
762 }
763
764 struct Screen {
765 navigator: Entity<PaneNavigator<u32>>,
766 width: f32,
767 rtl: bool,
768 handle: bool,
769 }
770
771 impl Render for Screen {
772 fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
773 window.set_rem_size(px(BASE_PX));
774 let scaffold = PaneScaffold::list_detail("layout", &self.navigator)
775 .rtl(self.rtl)
776 .pane(PaneRole::Secondary, "Inbox", div().child("List"))
777 .pane(PaneRole::Primary, "Message", div().child("Detail"))
778 .pane(PaneRole::Tertiary, "Notes", div().child("Notes"));
779 div().w(px(self.width)).h(px(800.)).child(if self.handle {
780 scaffold.drag_handle("Resize panes")
781 } else {
782 scaffold
783 })
784 }
785 }
786
787 fn open(
788 cx: &mut gpui::TestAppContext,
789 width: f32,
790 reduced: bool,
791 ) -> (Entity<Screen>, &mut gpui::VisualTestContext) {
792 cx.update(|cx| cx.set_reduce_motion(reduced));
793 let (view, cx) = cx.add_window_view(move |_, cx| Screen {
794 navigator: cx.new(|_| PaneNavigator::list_detail()),
795 width,
796 rtl: false,
797 handle: false,
798 });
799 draw(cx);
800 (view, cx)
801 }
802
803 fn draw(cx: &mut gpui::VisualTestContext) {
804 for _ in 0..3 {
805 cx.update(|window, cx| {
806 window.refresh();
807 window.draw(cx).clear(cx);
808 });
809 cx.run_until_parked();
810 }
811 }
812
813 fn navigate(
814 view: &Entity<Screen>,
815 cx: &mut gpui::VisualTestContext,
816 change: impl FnOnce(&mut PaneNavigator<u32>),
817 ) {
818 cx.update(|_, cx| {
819 let navigator = view.read(cx).navigator.clone();
820 navigator.update(cx, |navigator, cx| {
821 change(navigator);
822 cx.notify();
823 });
824 view.update(cx, |_, cx| cx.notify());
825 });
826 draw(cx);
827 }
828
829 #[gpui::test]
830 fn the_scaffold_adapts_to_its_own_width_not_the_windows(cx: &mut gpui::TestAppContext) {
831 let (view, cx) = open(cx, 500., true);
832 assert!(cx.debug_bounds("pane-secondary").is_some(), "the list");
833 assert!(cx.debug_bounds("pane-primary").is_none());
834 cx.update(|_, cx| view.update(cx, |view, _| view.width = 1000.));
835 draw(cx);
836 let list = cx.debug_bounds("pane-secondary").unwrap();
837 let detail = cx.debug_bounds("pane-primary").unwrap();
838 assert_eq!(list.size.width, px(360.));
839 assert_eq!(detail.left() - list.right(), px(24.));
840 assert_eq!(detail.size.width, px(616.));
841 assert!(cx.debug_bounds("pane-tertiary").is_none());
842 cx.update(|_, cx| view.update(cx, |view, _| view.width = 1700.));
843 draw(cx);
844 assert!(cx.debug_bounds("pane-tertiary").is_some());
845 }
846
847 #[gpui::test]
848 fn one_pane_follows_the_destination_and_back_returns(cx: &mut gpui::TestAppContext) {
849 let (view, cx) = open(cx, 500., true);
850 navigate(&view, cx, |navigator| {
851 navigator.navigate_to(PaneRole::Primary, Some(7))
852 });
853 assert!(cx.debug_bounds("pane-primary").is_some());
854 assert!(cx.debug_bounds("pane-secondary").is_none());
855 navigate(&view, cx, |navigator| {
856 navigator.navigate_back(BackNavigationBehavior::default());
857 });
858 assert!(cx.debug_bounds("pane-secondary").is_some());
859 assert!(cx.debug_bounds("pane-primary").is_none());
860 }
861
862 #[gpui::test]
863 fn the_panes_slide_and_the_old_pane_stays_until_it_is_out(cx: &mut gpui::TestAppContext) {
864 let (view, cx) = open(cx, 500., false);
865 navigate(&view, cx, |navigator| {
866 navigator.navigate_to(PaneRole::Primary, None)
867 });
868 cx.executor()
871 .advance_clock(std::time::Duration::from_millis(60));
872 draw(cx);
873 let old = cx.debug_bounds("pane-secondary").expect("still leaving");
874 let new = cx.debug_bounds("pane-primary").expect("arriving");
875 assert!(old.left() < px(0.), "{:?}", old.left());
876 assert!(new.left() > px(0.), "{:?}", new.left());
877 cx.executor()
878 .advance_clock(std::time::Duration::from_secs(3));
879 draw(cx);
880 assert!(cx.debug_bounds("pane-secondary").is_none());
881 assert_eq!(cx.debug_bounds("pane-primary").unwrap().left(), px(0.));
882 }
883
884 #[gpui::test]
885 fn reduced_motion_moves_the_panes_at_once(cx: &mut gpui::TestAppContext) {
886 let (view, cx) = open(cx, 500., true);
887 navigate(&view, cx, |navigator| {
888 navigator.navigate_to(PaneRole::Primary, None)
889 });
890 assert!(cx.debug_bounds("pane-secondary").is_none());
891 assert_eq!(cx.debug_bounds("pane-primary").unwrap().left(), px(0.));
892 }
893
894 #[gpui::test]
895 fn right_to_left_mirrors_the_panes(cx: &mut gpui::TestAppContext) {
896 let (view, cx) = open(cx, 1000., true);
897 cx.update(|_, cx| view.update(cx, |view, _| view.rtl = true));
898 draw(cx);
899 let list = cx.debug_bounds("pane-secondary").unwrap();
900 let detail = cx.debug_bounds("pane-primary").unwrap();
901 assert!(list.left() > detail.left(), "the list is on the right");
902 assert_eq!(list.right(), px(1000.));
903 }
904
905 #[gpui::test]
906 fn focus_moves_to_the_new_pane_and_returns_on_back(cx: &mut gpui::TestAppContext) {
907 let (view, cx) = open(cx, 500., true);
908 navigate(&view, cx, |navigator| {
909 navigator.navigate_to(PaneRole::Primary, None)
910 });
911 let focused = cx.update(|window, cx| window.focused(cx));
912 assert!(focused.is_some(), "focus followed the destination");
913 navigate(&view, cx, |navigator| {
914 navigator.navigate_back(BackNavigationBehavior::default());
915 });
916 let back = cx.update(|window, cx| window.focused(cx));
918 assert!(back.is_some());
919 assert_ne!(back, focused);
920 }
921
922 #[gpui::test]
923 fn the_handle_drags_settles_on_an_anchor_and_takes_keys(cx: &mut gpui::TestAppContext) {
924 let (view, cx) = open(cx, 1000., true);
925 cx.update(|_, cx| view.update(cx, |view, _| view.handle = true));
926 draw(cx);
927 let handle = cx.debug_bounds("pane-drag-handle").unwrap().center();
928 assert_eq!(handle.x, px(372.));
929 cx.simulate_mouse_down(handle, MouseButton::Left, Modifiers::none());
930 draw(cx);
931 let target = point(px(520.), handle.y);
932 cx.simulate_mouse_move(target, MouseButton::Left, Modifiers::none());
933 draw(cx);
934 assert_eq!(
935 cx.debug_bounds("pane-drag-handle").unwrap().center().x,
936 px(520.)
937 );
938 cx.simulate_mouse_up(target, MouseButton::Left, Modifiers::none());
939 draw(cx);
940 assert_eq!(
942 cx.debug_bounds("pane-drag-handle").unwrap().center().x,
943 px(500.)
944 );
945 assert_eq!(
946 cx.debug_bounds("pane-secondary").unwrap().size.width,
947 px(488.)
948 );
949 let handle = cx.debug_bounds("pane-drag-handle").unwrap().center();
952 cx.simulate_click(handle, Modifiers::none());
953 draw(cx);
954 assert_eq!(
955 cx.debug_bounds("pane-drag-handle").unwrap().center().x,
956 px(760.)
957 );
958 cx.simulate_keystrokes("left");
959 draw(cx);
960 assert_eq!(
961 cx.debug_bounds("pane-drag-handle").unwrap().center().x,
962 px(500.)
963 );
964 cx.simulate_keystrokes("home");
965 draw(cx);
966 assert_eq!(
967 cx.debug_bounds("pane-primary").unwrap().size.width,
968 px(1000.)
969 );
970 }
971}