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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
19pub enum NavOperation {
20 Push,
22 Pop,
24 Replace,
26}
27
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum NavMotion {
37 Animated,
38 Immediate,
39}
40
41#[derive(Clone, Copy, Debug, Eq, PartialEq)]
43pub enum NavStackEvent {
44 Pushed,
45 Popped,
46 Forwarded,
47 Replaced,
48 Cleared,
49}
50
51#[derive(Clone)]
55struct Transit {
56 outgoing: AnyView,
57 index: usize,
59 operation: NavOperation,
60 motion: NavMotion,
61}
62
63pub struct NavStackState {
82 history: History<NavEntry>,
83 transit: Option<Transit>,
84}
85
86#[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 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 pub fn current(&self) -> Option<&AnyView> {
126 self.history.current().map(|entry| &entry.view)
127 }
128
129 pub fn views(&self) -> impl ExactSizeIterator<Item = &AnyView> {
131 self.history.entries().map(|entry| &entry.view)
132 }
133
134 pub fn forward_views(&self) -> impl ExactSizeIterator<Item = &AnyView> {
137 self.history.forward_entries().map(|entry| &entry.view)
138 }
139
140 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 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 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 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 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 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 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 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#[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 pub fn transition(mut self, transition: Transition) -> Self {
317 self.transition = Some(transition);
318 self
319 }
320
321 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 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 if let Some(current) = ¤t {
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 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 .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#[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 pub fn index(&self) -> usize {
500 self.index
501 }
502
503 pub fn phase(&self) -> PresencePhase {
504 self.phase
505 }
506
507 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 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}