Skip to main content

gpui_base/
nav_stack.rs

1use std::{rc::Rc, time::Duration};
2
3use gpui::{
4    AnyElement, AnyView, App, Context, Div, ElementId, Entity, EventEmitter,
5    InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, StyleRefinement, Styled,
6    Window, div, prelude::FluentBuilder as _,
7};
8
9use crate::{
10    History, StyledExt as _,
11    motion::{Presence, PresencePhase, Transition},
12};
13
14/// What a running transition is doing, in Qt's terms.
15///
16/// The operation decides paint order and lets a renderer move a pushed view
17/// differently from a popped one.
18#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum NavOperation {
20    /// A view was pushed over the previous top.
21    Push,
22    /// The top was popped, revealing the view below.
23    Pop,
24    /// The top was swapped for another view.
25    Replace,
26}
27
28/// Whether one change runs the [`NavStack`]'s transition, as UIKit's
29/// `animated:` and Qt's `StackView.Immediate` decide per call.
30///
31/// `Immediate` switches views on the spot even when the element has a
32/// transition, which is what restoring a stack at launch or jumping to a
33/// page from a command wants. A `NavStack` without a transition is always
34/// immediate, whatever is passed here.
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum NavMotion {
37    Animated,
38    Immediate,
39}
40
41/// Emitted by [`NavStackState`] after the stack changed.
42#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum NavStackEvent {
44    Pushed,
45    Popped,
46    Forwarded,
47    Replaced,
48    Cleared,
49}
50
51/// The view leaving the stack, kept mounted until its exit transition
52/// finishes. The element samples that transition as a [`Presence`] keyed by
53/// the view, so an interrupted change reverses from where it is.
54#[derive(Clone)]
55struct Transit {
56    outgoing: AnyView,
57    /// The position the outgoing view had on the stack.
58    index: usize,
59    operation: NavOperation,
60    motion: NavMotion,
61}
62
63/// A stack of views, one visible at a time, with the pages popped off it
64/// kept for `forward`.
65///
66/// This is SwiftUI's `NavigationStack`, Qt's `StackView` and WinUI's
67/// `Frame`: navigation between pages. Underneath it is a [`History`] of
68/// views from the root through the current page. A popped page becomes a
69/// forward entry until a push discards that forward branch, which is what
70/// WinUI's `BackStack` and `ForwardStack` do.
71///
72/// The stack owns which view is current and the lifecycle of a change: after
73/// a push, pop or replace, the outgoing view stays mounted until the
74/// [`NavStack`]'s transition finishes, so the application can animate it.
75/// The views themselves, and what a transition looks like, belong to the
76/// application.
77///
78/// `pop` keeps the root, as Qt's `StackView` and UIKit's navigation controller
79/// do; `clear` is the way to empty the stack. A back button is shown when
80/// `depth() > 1`, a forward button when `forward_views()` is not empty.
81pub struct NavStackState {
82    history: History<NavEntry>,
83    transit: Option<Transit>,
84}
85
86/// A view on the stack. A page pushed twice is two separate entries.
87#[derive(Clone)]
88struct NavEntry {
89    view: AnyView,
90}
91
92impl NavEntry {
93    fn new(view: impl Into<AnyView>) -> Self {
94        Self { view: view.into() }
95    }
96}
97
98impl EventEmitter<NavStackEvent> for NavStackState {}
99
100impl Default for NavStackState {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl NavStackState {
107    pub fn new() -> Self {
108        Self {
109            history: History::new(),
110            transit: None,
111        }
112    }
113
114    /// The number of views on the stack.
115    pub fn depth(&self) -> usize {
116        self.history.entries().len()
117    }
118
119    pub fn is_empty(&self) -> bool {
120        self.history.entries().len() == 0
121    }
122
123    /// The view on top of the stack, which is the one shown once any
124    /// transition has finished.
125    pub fn current(&self) -> Option<&AnyView> {
126        self.history.current().map(|entry| &entry.view)
127    }
128
129    /// Every view on the stack, root first.
130    pub fn views(&self) -> impl ExactSizeIterator<Item = &AnyView> {
131        self.history.entries().map(|entry| &entry.view)
132    }
133
134    /// The views popped since the last push, nearest first: the one
135    /// `forward` would bring back is the first.
136    pub fn forward_views(&self) -> impl ExactSizeIterator<Item = &AnyView> {
137        self.history.forward_entries().map(|entry| &entry.view)
138    }
139
140    /// Pushes `view` on top of the stack and discards the forward views.
141    ///
142    /// Into an empty stack this is immediate, like Qt's `initialItem`. Over
143    /// an existing top it starts a [`NavOperation::Push`] transition, unless
144    /// `motion` is [`NavMotion::Immediate`].
145    pub fn push(&mut self, view: impl Into<AnyView>, motion: NavMotion, cx: &mut Context<Self>) {
146        let outgoing = self.top();
147        self.history.push(NavEntry::new(view));
148        self.finish(
149            outgoing,
150            NavOperation::Push,
151            motion,
152            NavStackEvent::Pushed,
153            cx,
154        );
155    }
156
157    /// Pops the top view and returns it, starting a [`NavOperation::Pop`]
158    /// transition to the view below. The view waits in `forward_views`.
159    ///
160    /// The root is never popped: this returns `None` at a depth of one or
161    /// less.
162    pub fn pop(&mut self, motion: NavMotion, cx: &mut Context<Self>) -> Option<AnyView> {
163        if self.depth() <= 1 {
164            return None;
165        }
166        let popped = self.top()?;
167        self.history.back()?;
168        self.finish(
169            Some(popped.clone()),
170            NavOperation::Pop,
171            motion,
172            NavStackEvent::Popped,
173            cx,
174        );
175        Some(popped.0)
176    }
177
178    /// Pops every view above the root in one [`NavOperation::Pop`]
179    /// transition from the previous top, and returns them root-side first.
180    pub fn pop_to_root(&mut self, motion: NavMotion, cx: &mut Context<Self>) -> Vec<AnyView> {
181        let outgoing = self.top();
182        let mut popped = Vec::new();
183        while self.depth() > 1 {
184            let Some(view) = self.current().cloned() else {
185                break;
186            };
187            if self.history.back().is_none() {
188                break;
189            }
190            popped.push(view);
191        }
192        if popped.is_empty() {
193            return popped;
194        }
195        self.finish(
196            outgoing,
197            NavOperation::Pop,
198            motion,
199            NavStackEvent::Popped,
200            cx,
201        );
202        popped.reverse();
203        popped
204    }
205
206    /// Brings back the most recently popped view, starting a
207    /// [`NavOperation::Push`] transition over the current top, and returns
208    /// it. `None` when nothing has been popped since the last push.
209    pub fn forward(&mut self, motion: NavMotion, cx: &mut Context<Self>) -> Option<AnyView> {
210        let outgoing = self.top();
211        let view = self.history.forward()?.view;
212        self.finish(
213            outgoing,
214            NavOperation::Push,
215            motion,
216            NavStackEvent::Forwarded,
217            cx,
218        );
219        Some(view)
220    }
221
222    /// Swaps the top view for `view` and returns the one replaced, starting a
223    /// [`NavOperation::Replace`] transition. The forward views are kept. On
224    /// an empty stack this is a push.
225    pub fn replace(
226        &mut self,
227        view: impl Into<AnyView>,
228        motion: NavMotion,
229        cx: &mut Context<Self>,
230    ) -> Option<AnyView> {
231        let Some(replaced) = self.top() else {
232            self.push(view, motion, cx);
233            return None;
234        };
235        self.history.replace_current(NavEntry::new(view));
236        self.finish(
237            Some(replaced.clone()),
238            NavOperation::Replace,
239            motion,
240            NavStackEvent::Replaced,
241            cx,
242        );
243        Some(replaced.0)
244    }
245
246    /// Empties the stack and the forward views immediately, abandoning any
247    /// running transition.
248    pub fn clear(&mut self, cx: &mut Context<Self>) {
249        self.history.clear();
250        self.transit = None;
251        cx.emit(NavStackEvent::Cleared);
252        cx.notify();
253    }
254
255    /// The top view with its position, before an operation moves it.
256    fn top(&self) -> Option<(AnyView, usize)> {
257        let index = self.depth().checked_sub(1)?;
258        self.current().cloned().map(|view| (view, index))
259    }
260
261    /// Records the change: `outgoing` stays mounted until its exit
262    /// transition finishes, or not at all for an immediate change. A change
263    /// already in transit is superseded, so at most one outgoing view is ever
264    /// mounted; the element reverses its presence from wherever it is.
265    fn finish(
266        &mut self,
267        outgoing: Option<(AnyView, usize)>,
268        operation: NavOperation,
269        motion: NavMotion,
270        event: NavStackEvent,
271        cx: &mut Context<Self>,
272    ) {
273        self.transit = outgoing.map(|(outgoing, index)| Transit {
274            outgoing,
275            index,
276            operation,
277            motion,
278        });
279        cx.emit(event);
280        cx.notify();
281    }
282}
283
284type ItemRenderer = Rc<dyn Fn(NavPage, &mut Window, &mut App) -> AnyElement>;
285
286/// An unstyled host for a [`NavStackState`].
287///
288/// The container is positioned so that the two views of a transition can
289/// overlap; each mounted view is handed to the `item` renderer as a
290/// [`NavPage`] that already fills the container. Everything else — size,
291/// clipping, background, and how a transition moves — is the application's.
292///
293/// Without a `transition` the stack switches views immediately, as it also
294/// does under reduced motion.
295#[derive(IntoElement)]
296pub struct NavStack {
297    base: Div,
298    style: StyleRefinement,
299    state: Entity<NavStackState>,
300    transition: Option<Transition>,
301    render_item: Option<ItemRenderer>,
302}
303
304impl NavStack {
305    pub fn new(state: &Entity<NavStackState>) -> Self {
306        Self {
307            base: div(),
308            style: StyleRefinement::default(),
309            state: state.clone(),
310            transition: None,
311            render_item: None,
312        }
313    }
314
315    /// The timing every push, pop and replace runs under.
316    pub fn transition(mut self, transition: Transition) -> Self {
317        self.transition = Some(transition);
318        self
319    }
320
321    /// Renders each mounted view. The item is already positioned to fill the
322    /// container; refine it to move or fade the view by its phase and
323    /// progress, then return it.
324    pub fn item(
325        mut self,
326        render: impl Fn(NavPage, &mut Window, &mut App) -> AnyElement + 'static,
327    ) -> Self {
328        self.render_item = Some(Rc::new(render));
329        self
330    }
331}
332
333impl Styled for NavStack {
334    fn style(&mut self) -> &mut StyleRefinement {
335        &mut self.style
336    }
337}
338
339impl RenderOnce for NavStack {
340    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
341        let (current, depth, transit) = {
342            let state = self.state.read(cx);
343            (
344                state.current().cloned(),
345                state.depth(),
346                state.transit.clone(),
347            )
348        };
349        let immediate = cx.reduce_motion()
350            || self.transition.is_none()
351            || transit
352                .as_ref()
353                .is_some_and(|transit| transit.motion == NavMotion::Immediate);
354        let transition = if immediate {
355            Transition::new(Duration::ZERO)
356        } else {
357            self.transition
358                .clone()
359                .unwrap_or_else(|| Transition::new(Duration::ZERO))
360        };
361
362        // The outgoing view's presence is the change's clock: its exit runs
363        // 1 → 0, reversing if the change is interrupted, and both pages of
364        // the change read their progress from it.
365        let change = transit.and_then(|transit| {
366            let sample = Presence::new(page_id(&transit.outgoing), false)
367                .transition(transition.clone())
368                .sample(window, cx);
369            if sample.should_render() {
370                Some((transit, 1.0 - sample.progress))
371            } else {
372                self.state.update(cx, |state, _| state.transit = None);
373                None
374            }
375        });
376
377        // The current view's presence is sampled too, so a view brought back
378        // by `forward` or revealed by `pop` starts its next exit from present.
379        // With nothing changing it settles on the spot; the root does not
380        // animate in.
381        if let Some(current) = &current {
382            let transition = if change.is_some() {
383                transition
384            } else {
385                Transition::new(Duration::ZERO)
386            };
387            Presence::new(page_id(current), true)
388                .transition(transition)
389                .sample(window, cx);
390        }
391
392        let mut items = Vec::with_capacity(3);
393        if let Some(current) = current {
394            let index = depth - 1;
395            match change {
396                Some((transit, progress)) => {
397                    let current = NavPage::new(
398                        current,
399                        index,
400                        PresencePhase::Entering,
401                        Some(transit.operation),
402                        progress,
403                    );
404                    let outgoing = NavPage::new(
405                        transit.outgoing,
406                        transit.index,
407                        PresencePhase::Exiting,
408                        Some(transit.operation),
409                        progress,
410                    );
411                    // A pushed or replacing view paints over what it covers; a
412                    // popped view paints over what it reveals.
413                    match transit.operation {
414                        NavOperation::Push | NavOperation::Replace => {
415                            items.push(outgoing);
416                            items.push(current);
417                        }
418                        NavOperation::Pop => {
419                            items.push(current);
420                            items.push(outgoing);
421                        }
422                    }
423                }
424                None => items.push(NavPage::new(
425                    current,
426                    index,
427                    PresencePhase::Present,
428                    None,
429                    1.0,
430                )),
431            }
432        }
433        let changing = items.len() > 1;
434
435        let render_item = self.render_item;
436        self.base
437            .relative()
438            .refine_style(&self.style)
439            .children(items.into_iter().map(|item| match &render_item {
440                Some(render) => render(item, window, cx),
441                None => item.into_any_element(),
442            }))
443            // Neither page takes pointer input while the change runs: the
444            // outgoing one is on its way out, and the incoming one is not yet
445            // where it will be.
446            .when(changing, |this| {
447                this.child(div().absolute().inset_0().occlude())
448            })
449    }
450}
451
452fn page_id(view: &AnyView) -> ElementId {
453    ("nav-stack", view.entity_id()).into()
454}
455
456/// One mounted view of a [`NavStack`], handed to the item renderer.
457///
458/// The item fills its container. Its readers describe where the view is in
459/// the change that is running, so the renderer can move it: `phase` says
460/// whether it is arriving, settled, or leaving; `operation` says which
461/// change; `progress` runs from `0.0` to `1.0` over the transition, already
462/// eased, and is shared by both views of one change.
463#[derive(IntoElement)]
464pub struct NavPage {
465    base: Div,
466    style: StyleRefinement,
467    view: AnyView,
468    index: usize,
469    phase: PresencePhase,
470    operation: Option<NavOperation>,
471    progress: f32,
472}
473
474impl NavPage {
475    fn new(
476        view: AnyView,
477        index: usize,
478        phase: PresencePhase,
479        operation: Option<NavOperation>,
480        progress: f32,
481    ) -> Self {
482        Self {
483            base: div(),
484            style: StyleRefinement::default(),
485            view,
486            index,
487            phase,
488            operation,
489            progress,
490        }
491    }
492
493    pub fn view(&self) -> &AnyView {
494        &self.view
495    }
496
497    /// The view's position on the stack, root first. A view on its way out
498    /// keeps the position it had.
499    pub fn index(&self) -> usize {
500        self.index
501    }
502
503    pub fn phase(&self) -> PresencePhase {
504        self.phase
505    }
506
507    /// The change in progress, or `None` once the stack has settled.
508    pub fn operation(&self) -> Option<NavOperation> {
509        self.operation
510    }
511
512    pub fn progress(&self) -> f32 {
513        self.progress
514    }
515}
516
517impl Styled for NavPage {
518    fn style(&mut self) -> &mut StyleRefinement {
519        &mut self.style
520    }
521}
522
523impl RenderOnce for NavPage {
524    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
525        self.base
526            .absolute()
527            .inset_0()
528            .child(self.view)
529            .refine_style(&self.style)
530    }
531}
532
533#[cfg(test)]
534mod tests {
535    use std::{cell::RefCell, time::Duration};
536
537    use gpui::{AppContext as _, Render, TestAppContext};
538
539    use super::*;
540
541    struct Page;
542
543    impl Render for Page {
544        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
545            div()
546        }
547    }
548
549    fn page(cx: &mut TestAppContext) -> AnyView {
550        cx.new(|_| Page).into()
551    }
552
553    fn stack(cx: &mut TestAppContext) -> (Entity<NavStackState>, Rc<RefCell<Vec<NavStackEvent>>>) {
554        let stack = cx.new(|_| NavStackState::new());
555        let events = Rc::new(RefCell::new(Vec::new()));
556        cx.update({
557            let events = events.clone();
558            let stack = stack.clone();
559            move |cx| {
560                cx.subscribe(&stack, move |_, event: &NavStackEvent, _| {
561                    events.borrow_mut().push(*event);
562                })
563                .detach();
564            }
565        });
566        (stack, events)
567    }
568
569    #[gpui::test]
570    fn push_and_pop_keep_the_root(cx: &mut TestAppContext) {
571        let (stack, events) = stack(cx);
572        let (root, second) = (page(cx), page(cx));
573
574        stack.update(cx, |stack, cx| {
575            stack.push(root.clone(), NavMotion::Animated, cx)
576        });
577        assert!(stack.read_with(cx, |stack, _| stack.transit.is_none()));
578
579        stack.update(cx, |stack, cx| {
580            stack.push(second.clone(), NavMotion::Animated, cx)
581        });
582        stack.read_with(cx, |stack, _| {
583            assert_eq!(stack.depth(), 2);
584            assert_eq!(stack.current(), Some(&second));
585            let transit = stack
586                .transit
587                .as_ref()
588                .expect("push over a view transitions");
589            assert_eq!(transit.operation, NavOperation::Push);
590            assert_eq!(transit.outgoing, root);
591        });
592
593        let popped = stack.update(cx, |stack, cx| stack.pop(NavMotion::Animated, cx));
594        assert_eq!(popped, Some(second.clone()));
595        stack.read_with(cx, |stack, _| {
596            assert_eq!(stack.views().collect::<Vec<_>>(), [&root]);
597            let transit = stack.transit.as_ref().expect("pop transitions");
598            assert_eq!(transit.operation, NavOperation::Pop);
599            assert_eq!(transit.outgoing, second);
600            assert_eq!(transit.index, 1, "the popped view keeps its position");
601        });
602
603        assert_eq!(
604            stack.update(cx, |stack, cx| stack.pop(NavMotion::Animated, cx)),
605            None
606        );
607        assert_eq!(stack.read_with(cx, |stack, _| stack.depth()), 1);
608        assert_eq!(
609            &*events.borrow(),
610            &[
611                NavStackEvent::Pushed,
612                NavStackEvent::Pushed,
613                NavStackEvent::Popped
614            ]
615        );
616    }
617
618    #[gpui::test]
619    fn pop_to_root_returns_everything_above_it(cx: &mut TestAppContext) {
620        let (stack, _) = stack(cx);
621        let pages: Vec<AnyView> = (0..3).map(|_| page(cx)).collect();
622        for view in &pages {
623            stack.update(cx, |stack, cx| {
624                stack.push(view.clone(), NavMotion::Animated, cx)
625            });
626        }
627
628        assert_eq!(
629            stack.update(cx, |stack, cx| stack.pop_to_root(NavMotion::Animated, cx)),
630            pages[1..]
631        );
632        stack.read_with(cx, |stack, _| {
633            assert_eq!(stack.views().cloned().collect::<Vec<_>>(), pages[..1]);
634            let transit = stack.transit.as_ref().expect("pop_to_root transitions");
635            assert_eq!(transit.outgoing, pages[2]);
636            assert_eq!(transit.index, 2, "the previous top keeps its position");
637        });
638        assert!(
639            stack
640                .update(cx, |stack, cx| stack.pop_to_root(NavMotion::Animated, cx))
641                .is_empty()
642        );
643    }
644
645    #[gpui::test]
646    fn replace_swaps_the_top_and_pushes_into_an_empty_stack(cx: &mut TestAppContext) {
647        let (stack, events) = stack(cx);
648        let (first, second) = (page(cx), page(cx));
649
650        assert_eq!(
651            stack.update(cx, |stack, cx| stack.replace(
652                first.clone(),
653                NavMotion::Animated,
654                cx
655            )),
656            None
657        );
658        assert_eq!(
659            stack.update(cx, |stack, cx| stack.replace(
660                second.clone(),
661                NavMotion::Animated,
662                cx
663            )),
664            Some(first.clone())
665        );
666        stack.read_with(cx, |stack, _| {
667            assert_eq!(stack.views().collect::<Vec<_>>(), [&second]);
668            let transit = stack.transit.as_ref().expect("replace transitions");
669            assert_eq!(transit.operation, NavOperation::Replace);
670            assert_eq!(transit.outgoing, first);
671            assert_eq!(
672                transit.index, 0,
673                "the replaced view sat where the new one sits"
674            );
675        });
676
677        stack.update(cx, |stack, cx| stack.clear(cx));
678        stack.read_with(cx, |stack, _| {
679            assert!(stack.is_empty());
680            assert!(stack.transit.is_none());
681        });
682        assert_eq!(
683            &*events.borrow(),
684            &[
685                NavStackEvent::Pushed,
686                NavStackEvent::Replaced,
687                NavStackEvent::Cleared
688            ]
689        );
690    }
691
692    #[gpui::test]
693    fn popped_views_wait_for_forward_until_the_next_push(cx: &mut TestAppContext) {
694        let (stack, events) = stack(cx);
695        let pages: Vec<AnyView> = (0..3).map(|_| page(cx)).collect();
696        for view in &pages {
697            stack.update(cx, |stack, cx| {
698                stack.push(view.clone(), NavMotion::Animated, cx)
699            });
700        }
701        assert!(
702            stack
703                .update(cx, |stack, cx| stack.forward(NavMotion::Animated, cx))
704                .is_none()
705        );
706
707        stack.update(cx, |stack, cx| {
708            stack.pop(NavMotion::Animated, cx);
709            stack.pop(NavMotion::Animated, cx);
710        });
711        stack.read_with(cx, |stack, _| {
712            assert_eq!(stack.depth(), 1);
713            assert_eq!(
714                stack.forward_views().cloned().collect::<Vec<_>>(),
715                pages[1..]
716            );
717        });
718
719        let brought_back = stack.update(cx, |stack, cx| stack.forward(NavMotion::Animated, cx));
720        assert_eq!(brought_back, Some(pages[1].clone()));
721        stack.read_with(cx, |stack, _| {
722            assert_eq!(stack.current(), Some(&pages[1]));
723            let transit = stack
724                .transit
725                .as_ref()
726                .expect("forward transitions like a push");
727            assert_eq!(transit.operation, NavOperation::Push);
728            assert_eq!(transit.outgoing, pages[0]);
729            assert_eq!(stack.forward_views().len(), 1);
730        });
731
732        let fresh = page(cx);
733        stack.update(cx, |stack, cx| stack.push(fresh, NavMotion::Animated, cx));
734        assert_eq!(
735            stack.read_with(cx, |stack, _| stack.forward_views().len()),
736            0
737        );
738        assert_eq!(events.borrow().last(), Some(&NavStackEvent::Pushed));
739        assert!(events.borrow().contains(&NavStackEvent::Forwarded));
740    }
741
742    #[gpui::test]
743    fn an_immediate_change_records_its_motion_and_supersedes_the_running_one(
744        cx: &mut TestAppContext,
745    ) {
746        let (stack, events) = stack(cx);
747        let (root, second, third) = (page(cx), page(cx), page(cx));
748        stack.update(cx, |stack, cx| {
749            stack.push(root, NavMotion::Animated, cx);
750            stack.push(second.clone(), NavMotion::Animated, cx);
751            stack.push(third.clone(), NavMotion::Immediate, cx);
752        });
753        stack.read_with(cx, |stack, _| {
754            assert_eq!(stack.current(), Some(&third));
755            let transit = stack.transit.as_ref().expect("the change is recorded");
756            assert_eq!(transit.motion, NavMotion::Immediate);
757            assert_eq!(transit.outgoing, second, "the running push was superseded");
758        });
759        assert_eq!(
760            stack.update(cx, |stack, cx| stack.pop(NavMotion::Immediate, cx)),
761            Some(third.clone())
762        );
763        stack.read_with(cx, |stack, _| {
764            let transit = stack.transit.as_ref().unwrap();
765            assert_eq!(transit.motion, NavMotion::Immediate);
766            assert_eq!(transit.outgoing, third);
767        });
768        assert_eq!(events.borrow().len(), 4);
769    }
770
771    struct Host {
772        stack: Entity<NavStackState>,
773    }
774
775    impl Render for Host {
776        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
777            NavStack::new(&self.stack)
778                .size_full()
779                .transition(Transition::new(Duration::from_millis(200)))
780        }
781    }
782
783    #[gpui::test]
784    fn the_outgoing_view_is_dropped_once_its_exit_has_run(cx: &mut TestAppContext) {
785        let stack = cx.new(|_| NavStackState::new());
786        let (root, second) = (page(cx), page(cx));
787        let (_, cx) = cx.add_window_view({
788            let stack = stack.clone();
789            move |_, _| Host { stack }
790        });
791        stack.update(cx, |stack, cx| {
792            stack.push(root, NavMotion::Immediate, cx);
793            stack.push(second, NavMotion::Animated, cx);
794        });
795
796        cx.update(|window, cx| window.draw(cx).clear(cx));
797        assert!(stack.read_with(cx, |stack, _| stack.transit.is_some()));
798
799        cx.executor().advance_clock(Duration::from_millis(100));
800        cx.update(|window, cx| window.draw(cx).clear(cx));
801        assert!(stack.read_with(cx, |stack, _| stack.transit.is_some()));
802
803        cx.executor().advance_clock(Duration::from_millis(150));
804        cx.update(|window, cx| window.draw(cx).clear(cx));
805        assert!(stack.read_with(cx, |stack, _| stack.transit.is_none()));
806
807        // An immediate change is gone after the frame that draws it.
808        stack.update(cx, |stack, cx| {
809            stack.pop(NavMotion::Immediate, cx);
810        });
811        cx.update(|window, cx| window.draw(cx).clear(cx));
812        assert!(stack.read_with(cx, |stack, _| stack.transit.is_none()));
813    }
814
815    #[gpui::test]
816    fn a_new_operation_replaces_the_running_transition(cx: &mut TestAppContext) {
817        let (stack, _) = stack(cx);
818        let pages: Vec<AnyView> = (0..3).map(|_| page(cx)).collect();
819        for view in &pages {
820            stack.update(cx, |stack, cx| {
821                stack.push(view.clone(), NavMotion::Animated, cx)
822            });
823        }
824        stack.update(cx, |stack, cx| {
825            stack.pop(NavMotion::Animated, cx);
826        });
827        stack.read_with(cx, |stack, _| {
828            let transit = stack.transit.as_ref().unwrap();
829            assert_eq!(transit.operation, NavOperation::Pop);
830            assert_eq!(transit.outgoing, pages[2]);
831        });
832    }
833}