waterui_navigation/
lib.rs

1#![no_std]
2
3//! Navigation module for `WaterUI` framework.
4//!
5//! This module provides navigation components and utilities for building
6//! hierarchical user interfaces with navigation bars and links.
7extern crate alloc;
8
9/// Provides search functionality for navigation.
10pub mod search;
11pub mod tab;
12
13use alloc::{rc::Rc, vec::Vec};
14use core::{
15    cell::{Cell, RefCell},
16    fmt::Debug,
17};
18
19use nami::{
20    Computed,
21    collection::{Collection, List},
22};
23use waterui_color::Color;
24use waterui_controls::button;
25use waterui_core::{
26    AnyView, Environment, Metadata, Retain, View, env::use_env, handler::ViewBuilder,
27    impl_extractor, layout::StretchAxis, raw_view,
28};
29use waterui_text::Text;
30
31/// A view that combines a navigation bar with content.
32///
33/// The `NavigationView` contains a navigation bar with a title and other
34/// configuration options, along with the actual content to display.
35#[derive(Debug)]
36#[must_use]
37pub struct NavigationView {
38    /// The navigation bar for this view
39    pub bar: Bar,
40    /// The content to display in this view
41    pub content: AnyView,
42}
43
44/// A trait for handling custom navigation actions.
45/// For renderers to implement navigation handling.
46pub trait CustomNavigationController: 'static {
47    /// Pushes a new navigation view onto the stack.
48    /// # Arguments
49    /// * `content` - The navigation view to push
50    fn push(&mut self, content: NavigationView);
51    /// Pops the top navigation view off the stack.
52    fn pop(&mut self);
53}
54
55/// A receiver that handles navigation actions.
56/// For renderers to implement navigation handling.
57#[derive(Clone)]
58pub struct NavigationController(Rc<RefCell<dyn CustomNavigationController>>);
59
60impl_extractor!(NavigationController);
61
62impl Debug for NavigationController {
63    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
64        f.debug_struct("NavigationController").finish()
65    }
66}
67
68impl NavigationController {
69    /// Creates a new navigation receiver.
70    ///
71    /// # Arguments
72    ///
73    /// * `receiver` - An implementation of `CustomNavigationController`
74    pub fn new(receiver: impl CustomNavigationController) -> Self {
75        Self(Rc::new(RefCell::new(receiver)))
76    }
77
78    /// Pushes a new navigation view onto the stack.
79    ///
80    /// # Arguments
81    ///
82    /// * `content` - The navigation view to push
83    pub fn push(&self, content: NavigationView) {
84        self.0.borrow_mut().push(content);
85    }
86    /// Pops the top navigation view off the stack.
87    pub fn pop(&self) {
88        self.0.borrow_mut().pop();
89    }
90}
91
92raw_view!(NavigationView, StretchAxis::Both);
93
94/// Configuration for a navigation bar.
95///
96/// Represents the appearance and behavior of a navigation bar, including
97/// its title, color, and visibility.
98#[derive(Debug, Default)]
99pub struct Bar {
100    /// The title text displayed in the navigation bar
101    pub title: Text,
102    /// The background color of the navigation bar
103    pub color: Computed<Color>,
104    /// Whether the navigation bar is hidden
105    pub hidden: Computed<bool>,
106}
107
108/// A link that navigates to another view when activated.
109///
110/// The `NavigationLink` combines a label view with a function that creates
111/// the destination view when the link is activated.
112#[must_use]
113#[derive(Debug)]
114pub struct NavigationLink<Label, Content> {
115    /// The label view displayed for this link
116    pub label: Label,
117    /// A function that creates the destination view when the link is activated
118    pub content: Content,
119}
120impl<Label, Content> NavigationLink<Label, Content>
121where
122    Label: View,
123    Content: ViewBuilder<Output = NavigationView>,
124{
125    /// Creates a new navigation link.
126    ///
127    /// # Arguments
128    ///
129    /// * `label` - The label view to display for the link
130    /// * `content` - A function that creates the destination view
131    pub const fn new(label: Label, content: Content) -> Self {
132        Self { label, content }
133    }
134}
135
136/// A stack of navigation views.
137#[must_use]
138#[derive(Debug)]
139pub struct NavigationStack<T, F> {
140    root: AnyView, // Renderer requires to inject `NavigationController` to the root view's environment
141    path: T,
142    destination: F,
143}
144
145impl NavigationStack<(), ()> {
146    /// Creates a new navigation stack with the specified root view.
147    ///
148    /// # Arguments
149    /// * `root` - The root view of the navigation stack
150    pub fn new(root: impl View) -> Self {
151        Self {
152            root: AnyView::new(root),
153            path: (),
154            destination: (),
155        }
156    }
157
158    /// Consumes the navigation stack and returns its root view.
159    pub fn into_inner(self) -> AnyView {
160        self.root
161    }
162}
163
164impl<T> NavigationStack<NavigationPath<T>, ()> {
165    /// Creates a new navigation stack with the specified navigation path and root view.
166    ///
167    /// # Arguments
168    /// * `path` - The navigation path representing the current stack
169    /// * `root` - The root view of the navigation stack
170    pub fn with(path: NavigationPath<T>, root: impl View) -> Self {
171        Self {
172            root: AnyView::new(root),
173            path,
174            destination: (),
175        }
176    }
177
178    /// Sets the destination builder for the navigation stack.
179    ///
180    /// # Arguments
181    /// * `destination` - A function that creates a `NavigationView` from a path component
182    pub fn destination<F>(self, destination: F) -> NavigationStack<NavigationPath<T>, F>
183    where
184        F: 'static + Fn(T) -> NavigationView,
185    {
186        NavigationStack {
187            root: self.root,
188            path: self.path,
189            destination,
190        }
191    }
192}
193
194raw_view!(NavigationStack<(),()>, StretchAxis::Both);
195
196impl<T, F> View for NavigationStack<NavigationPath<T>, F>
197where
198    T: 'static + Clone + View,
199    F: 'static + Fn(T) -> NavigationView,
200{
201    fn body(self, _env: &Environment) -> impl View {
202        let path: NavigationPath<T> = self.path;
203        let destination = self.destination;
204        let root = self.root;
205        NavigationStack::new(use_env(move |receiver: NavigationController| {
206            let path = path.inner;
207            for component in &path {
208                receiver.push(destination(component));
209            }
210
211            let old_len = Cell::new(path.len());
212            #[allow(clippy::cast_possible_wrap)]
213            let guard = path.watch(.., move |slice| {
214                // list is a stack, only pop or push. So we only watch its length change
215                let slice = slice.into_value();
216                let len = slice.len();
217                let change = len as isize - old_len.get() as isize;
218                if change > 0 {
219                    // length increase, it has been pushed
220                    for item in slice.iter().skip(old_len.get()).take(len - old_len.get()) {
221                        receiver.push(destination(item.clone()));
222                    }
223                }
224                #[allow(clippy::cast_sign_loss)]
225                if change < 0 {
226                    //length decrease, it has been popped
227                    let pop_count = (-change) as usize;
228                    for _ in 0..pop_count {
229                        receiver.pop();
230                    }
231                }
232                old_len.set(len);
233            });
234
235            Metadata::new(root, Retain::new(guard))
236        }))
237    }
238}
239
240/// A path representing the current navigation stack.
241#[must_use]
242#[derive(Debug)]
243pub struct NavigationPath<T> {
244    inner: List<T>,
245}
246
247impl<T: 'static> From<Vec<T>> for NavigationPath<T> {
248    fn from(value: Vec<T>) -> Self {
249        Self {
250            inner: value.into(),
251        }
252    }
253}
254
255impl<T: 'static> FromIterator<T> for NavigationPath<T> {
256    fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
257        Self {
258            inner: List::from_iter(iter),
259        }
260    }
261}
262
263impl<T: 'static + Clone> Default for NavigationPath<T> {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268
269impl<T: 'static + Clone> NavigationPath<T> {
270    /// Creates a new, empty navigation path.
271    pub fn new() -> Self {
272        Self { inner: List::new() }
273    }
274
275    /// Pushes a new item onto the navigation path.
276    pub fn push(&mut self, value: T) {
277        self.inner.push(value);
278    }
279
280    /// Pops the top item from the navigation path.
281    pub fn pop(&self) {
282        let _ = self.inner.pop();
283    }
284
285    /// Pops `n` items from the navigation path.
286    pub fn pop_n(&self, n: usize) {
287        for _ in 0..n {
288            self.pop();
289        }
290    }
291
292    /// Returns an iterator over the items in the navigation path.
293    pub fn iter(&self) -> impl Iterator<Item = T> {
294        self.inner.iter()
295    }
296}
297
298impl<Label, Content> View for NavigationLink<Label, Content>
299where
300    Label: View,
301    Content: ViewBuilder<Output = NavigationView>,
302{
303    fn body(self, env: &waterui_core::Environment) -> impl View {
304        debug_assert!(
305            env.get::<NavigationController>().is_some(),
306            "NavigationLink used outside of a navigation context"
307        );
308
309        button(self.label).action(move |receiver: NavigationController| {
310            let content = (self.content).build();
311            receiver.push(content);
312        })
313    }
314}
315
316impl NavigationView {
317    /// Creates a new navigation view.
318    ///
319    /// # Arguments
320    ///
321    /// * `title` - The title to display in the navigation bar
322    /// * `content` - The content view to display
323    pub fn new(title: impl Into<Text>, content: impl View) -> Self {
324        let bar = Bar {
325            title: title.into(),
326            ..Default::default()
327        };
328
329        Self {
330            bar,
331            content: AnyView::new(content),
332        }
333    }
334}
335
336/// Convenience function to create a navigation view.
337///
338/// # Arguments
339///
340/// * `title` - The title to display in the navigation bar
341/// * `view` - The content view to display
342pub fn navigation(title: impl Into<Text>, view: impl View) -> NavigationView {
343    NavigationView::new(title, view)
344}