Skip to main content

dioxus_nox_shell/
context.rs

1use crate::ShellLayout;
2use crate::breakpoint::{DesktopSidebar, MobileSidebar, SheetSnap, ShellBreakpoint};
3use dioxus::prelude::*;
4
5/// Reactive context shared across the shell tree.
6///
7/// Provided by `AppShell` via `use_context_provider`. Access it from any
8/// descendant component with [`use_shell_context`].
9#[derive(Clone, Copy)]
10pub struct ShellContext {
11    /// Current layout mode, reactive via `Signal`.
12    pub layout: Signal<ShellLayout>,
13    /// Current viewport breakpoint, read-only reactive signal.
14    pub breakpoint: ReadSignal<ShellBreakpoint>,
15    /// Whether the desktop sidebar is in its expanded state (`true`) or not (`false`).
16    ///
17    /// For `Full`: `true` = visible at full width, `false` = collapsed to zero.
18    /// For `Expandable`: `true` = full width, `false` = rail width.
19    /// For `Rail`: ignored (rail is always visible at its fixed width).
20    pub sidebar_visible: Signal<bool>,
21    /// Whether the mobile overlay sidebar is open.
22    pub sidebar_mobile_open: Signal<bool>,
23    /// Mobile sidebar variant (Drawer, Rail, or Hidden).
24    pub mobile_sidebar: ReadSignal<MobileSidebar>,
25    /// Desktop sidebar variant (Full, Rail, or Expandable).
26    pub desktop_sidebar: ReadSignal<DesktopSidebar>,
27    /// Stack navigation depth. Starts at 1 (root screen).
28    pub stack_depth: Signal<u32>,
29    /// Whether the full-screen modal is currently presented.
30    pub modal_open: Signal<bool>,
31    /// Whether the search overlay is currently active.
32    pub search_active: Signal<bool>,
33    /// Current snap position of the persistent bottom sheet.
34    pub sheet_snap: Signal<SheetSnap>,
35    /// Callback fired when modal state changes (controlled-mode support).
36    pub(crate) on_modal_change: Signal<Option<EventHandler<bool>>>,
37    /// Callback fired when search active state changes (controlled-mode support).
38    pub(crate) on_search_change: Signal<Option<EventHandler<bool>>>,
39}
40
41impl ShellContext {
42    /// `true` when the current breakpoint is compact (phone-sized viewport).
43    pub fn is_mobile(&self) -> bool {
44        (self.breakpoint)().is_compact()
45    }
46
47    /// Toggles the appropriate sidebar state based on the current breakpoint.
48    ///
49    /// - On mobile: toggles `sidebar_mobile_open` (overlay open/closed)
50    /// - On desktop `Full` / `Expandable`: toggles `sidebar_visible`
51    /// - On desktop `Rail`: no-op (rail is always visible)
52    ///
53    /// Takes `&self` because [`Signal`] has interior mutability.
54    pub fn toggle_sidebar(&self) {
55        if self.is_mobile() {
56            let mut mob = self.sidebar_mobile_open;
57            mob.set(!(self.sidebar_mobile_open)());
58        } else if (self.desktop_sidebar)() != DesktopSidebar::Rail {
59            let mut vis = self.sidebar_visible;
60            vis.set(!(self.sidebar_visible)());
61        }
62    }
63
64    /// Returns the `data-shell-sidebar-state` attribute value for the root element.
65    ///
66    /// | Context | Value |
67    /// |---------|-------|
68    /// | Mobile open | `"open"` |
69    /// | Mobile closed | `"closed"` |
70    /// | Desktop expanded | `"expanded"` |
71    /// | Desktop `Full` collapsed | `"collapsed"` |
72    /// | Desktop `Rail` (always) | `"rail"` |
73    /// | Desktop `Expandable` collapsed | `"rail"` |
74    pub fn sidebar_state(&self) -> &'static str {
75        if self.is_mobile() {
76            if (self.sidebar_mobile_open)() {
77                "open"
78            } else {
79                "closed"
80            }
81        } else {
82            match (self.desktop_sidebar)() {
83                DesktopSidebar::Rail => "rail",
84                DesktopSidebar::Full => {
85                    if (self.sidebar_visible)() {
86                        "expanded"
87                    } else {
88                        "collapsed"
89                    }
90                }
91                DesktopSidebar::Expandable => {
92                    if (self.sidebar_visible)() {
93                        "expanded"
94                    } else {
95                        "rail"
96                    }
97                }
98            }
99        }
100    }
101
102    // ── Stack navigation ──────────────────────────────────────────────────────
103
104    /// Pushes a new screen onto the stack (increments depth by 1).
105    pub fn push_stack(&self) {
106        let mut s = self.stack_depth;
107        s.set((self.stack_depth)() + 1);
108    }
109
110    /// Pops the top screen from the stack (decrements depth by 1, minimum 1).
111    pub fn pop_stack(&self) {
112        let d = (self.stack_depth)();
113        if d > 1 {
114            let mut s = self.stack_depth;
115            s.set(d - 1);
116        }
117    }
118
119    /// Resets the stack to the root screen (depth 1).
120    pub fn reset_stack(&self) {
121        let mut s = self.stack_depth;
122        s.set(1);
123    }
124
125    /// `true` when there is at least one screen above the root to pop back to.
126    pub fn can_go_back(&self) -> bool {
127        (self.stack_depth)() > 1
128    }
129
130    // ── Full-screen modal ─────────────────────────────────────────────────────
131
132    /// Presents the full-screen modal.
133    pub fn open_modal(&self) {
134        let mut m = self.modal_open;
135        m.set(true);
136        if let Some(cb) = (self.on_modal_change)() {
137            cb.call(true);
138        }
139    }
140
141    /// Dismisses the full-screen modal.
142    pub fn close_modal(&self) {
143        let mut m = self.modal_open;
144        m.set(false);
145        if let Some(cb) = (self.on_modal_change)() {
146            cb.call(false);
147        }
148    }
149
150    /// Toggles the full-screen modal between presented and dismissed.
151    pub fn toggle_modal(&self) {
152        let next = !(self.modal_open)();
153        let mut m = self.modal_open;
154        m.set(next);
155        if let Some(cb) = (self.on_modal_change)() {
156            cb.call(next);
157        }
158    }
159
160    // ── Search overlay ────────────────────────────────────────────────────────
161
162    /// Activates the search overlay.
163    pub fn open_search(&self) {
164        let mut s = self.search_active;
165        s.set(true);
166        if let Some(cb) = (self.on_search_change)() {
167            cb.call(true);
168        }
169    }
170
171    /// Deactivates the search overlay.
172    pub fn close_search(&self) {
173        let mut s = self.search_active;
174        s.set(false);
175        if let Some(cb) = (self.on_search_change)() {
176            cb.call(false);
177        }
178    }
179
180    /// Toggles the search overlay between active and inactive.
181    pub fn toggle_search(&self) {
182        let next = !(self.search_active)();
183        let mut s = self.search_active;
184        s.set(next);
185        if let Some(cb) = (self.on_search_change)() {
186            cb.call(next);
187        }
188    }
189
190    // ── Bottom sheet ──────────────────────────────────────────────────────────
191
192    /// Sets the bottom sheet to the given snap position.
193    pub fn set_sheet_snap(&self, snap: SheetSnap) {
194        let mut s = self.sheet_snap;
195        s.set(snap);
196    }
197}
198
199/// Access [`ShellContext`] from any descendant of [`AppShell`].
200///
201/// # Panics
202///
203/// Panics if called outside an `AppShell` tree (no context provided).
204pub fn use_shell_context() -> ShellContext {
205    use_context::<ShellContext>()
206}