Skip to main content

dioxus_nox_shell/
breakpoint.rs

1use dioxus::prelude::*;
2
3/// Viewport size category detected via `matchMedia` listeners.
4///
5/// The default value (before any JS event fires) is [`Medium`][ShellBreakpoint::Medium].
6#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
7pub enum ShellBreakpoint {
8    /// Viewport width < `compact_below` (default 640 px). Typical phones.
9    Compact,
10    /// Viewport width between `compact_below` and `expanded_above`. Typical tablets.
11    #[default]
12    Medium,
13    /// Viewport width >= `expanded_above` (default 1024 px). Typical desktops.
14    Expanded,
15}
16
17impl ShellBreakpoint {
18    /// Returns the lowercase string used for the `data-shell-breakpoint` attribute.
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            Self::Compact => "compact",
22            Self::Medium => "medium",
23            Self::Expanded => "expanded",
24        }
25    }
26
27    /// `true` when the viewport is in the compact (phone) range.
28    pub fn is_compact(&self) -> bool {
29        *self == Self::Compact
30    }
31
32    /// Alias for [`is_compact`][Self::is_compact] — reflects mobile-first terminology.
33    pub fn is_mobile(&self) -> bool {
34        self.is_compact()
35    }
36}
37
38/// How the sidebar behaves on compact (mobile) viewports.
39#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
40pub enum MobileSidebar {
41    /// Fixed-position overlay drawer (shadcn Sheet pattern). Default.
42    #[default]
43    Drawer,
44    /// Icon-only narrow strip alongside main content (Flutter NavigationRail style).
45    Rail,
46    /// Sidebar is removed entirely; consumers provide alternative navigation.
47    Hidden,
48}
49
50/// How the sidebar behaves on desktop (non-compact) viewports.
51#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
52pub enum DesktopSidebar {
53    /// Full-width sidebar. `toggle_sidebar` collapses it to zero width. Default.
54    #[default]
55    Full,
56    /// Permanent narrow icon-only rail (~56 px). Width never changes; `toggle_sidebar` is a no-op.
57    Rail,
58    /// Rail that expands into a full sidebar on toggle.
59    ///
60    /// `sidebar_visible = true` → full width; `false` → rail width.
61    /// `toggle_sidebar` switches between the two states.
62    Expandable,
63}
64
65/// Snap state for a persistent bottom sheet.
66#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67pub enum SheetSnap {
68    /// Sheet is hidden (0 height). Default.
69    #[default]
70    Hidden,
71    /// Sheet peeks at the bottom (~15–25% height).
72    Peek,
73    /// Sheet is half-height (~50%).
74    Half,
75    /// Sheet is fully expanded (~90%).
76    Full,
77}
78
79impl SheetSnap {
80    /// Returns the lowercase string used for the `data-shell-sheet-state` attribute.
81    pub fn as_str(&self) -> &'static str {
82        match self {
83            Self::Hidden => "hidden",
84            Self::Peek => "peek",
85            Self::Half => "half",
86            Self::Full => "full",
87        }
88    }
89
90    /// `true` when the sheet is visible at any snap point.
91    pub fn is_visible(&self) -> bool {
92        *self != Self::Hidden
93    }
94}
95
96/// Grouped breakpoint thresholds for [`AppShell`][crate::AppShell] and
97/// [`use_shell_breakpoint`].
98///
99/// Pass a custom value to override the defaults:
100/// ```rust,ignore
101/// AppShell {
102///     breakpoints: BreakpointConfig { compact_below: 480.0, expanded_above: 1280.0 },
103/// }
104/// ```
105#[derive(Clone, Copy, Debug, PartialEq)]
106pub struct BreakpointConfig {
107    /// Viewport width (px) below which the layout is considered compact. Default: 640.
108    pub compact_below: f64,
109    /// Viewport width (px) at or above which the layout is considered expanded. Default: 1024.
110    pub expanded_above: f64,
111}
112
113impl Default for BreakpointConfig {
114    fn default() -> Self {
115        Self {
116            compact_below: 640.0,
117            expanded_above: 1024.0,
118        }
119    }
120}
121
122/// Detects the viewport breakpoint via `matchMedia` listeners fed through
123/// [`document::eval`].
124///
125/// Returns a [`ReadSignal`] that updates reactively on resize events.
126/// The initial value is [`ShellBreakpoint::Medium`] until the first JS event
127/// fires on the next microtask after mount. When the eval channel closes
128/// (unsupported backends), the value falls back to `Medium`.
129///
130/// **Requires a JavaScript engine.** Supported on all WebView targets:
131/// web WASM, Wry desktop, iOS WKWebView, and Android WebView.
132pub fn use_shell_breakpoint(
133    compact_below: f64,
134    expanded_above: f64,
135) -> ReadSignal<ShellBreakpoint> {
136    use_shell_breakpoint_runtime(compact_below, expanded_above)
137}
138
139/// Private composite hook — encapsulates eval-channel logic so
140/// `use_shell_breakpoint` can remain a thin, stable public entry point.
141fn use_shell_breakpoint_runtime(
142    compact_below: f64,
143    expanded_above: f64,
144) -> ReadSignal<ShellBreakpoint> {
145    let mut bp = use_signal(|| ShellBreakpoint::Medium);
146    use_effect(move || {
147        spawn(async move {
148            let compact_max = compact_below - 1.0;
149            let js = format!(
150                r#"
151                function getBp() {{
152                    const w = window.innerWidth;
153                    if (w < {compact_below}) return "compact";
154                    if (w >= {expanded_above}) return "expanded";
155                    return "medium";
156                }}
157                dioxus.send(getBp());
158                const mqlC = window.matchMedia("(max-width: {compact_max}px)");
159                const mqlE = window.matchMedia("(min-width: {expanded_above}px)");
160                function onChange() {{ dioxus.send(getBp()); }}
161                mqlC.addEventListener("change", onChange);
162                mqlE.addEventListener("change", onChange);
163            "#
164            );
165            let mut eval = document::eval(&js);
166            while let Ok(v) = eval.recv::<String>().await {
167                bp.set(match v.as_str() {
168                    "compact" => ShellBreakpoint::Compact,
169                    "medium" => ShellBreakpoint::Medium,
170                    _ => ShellBreakpoint::Expanded,
171                });
172            }
173            // Eval channel closed — deterministic fallback for unsupported backends.
174            bp.set(ShellBreakpoint::Medium);
175        });
176    });
177    bp.into()
178}