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}