leptos_use/
use_breakpoints.rs

1use crate::{use_media_query, use_window};
2use leptos::logging::error;
3use leptos::prelude::*;
4use leptos::reactive::wrappers::read::Signal;
5use paste::paste;
6use std::collections::HashMap;
7use std::fmt::Debug;
8use std::hash::Hash;
9
10/// Reactive viewport breakpoints.
11///
12/// ## Demo
13///
14/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_breakpoints)
15///
16/// ## Usage
17///
18/// ```
19/// # use leptos::prelude::*;
20/// # use leptos_use::{use_breakpoints, BreakpointsTailwind, breakpoints_tailwind};
21/// #
22/// # #[component]
23/// # fn Demo() -> impl IntoView {
24/// #
25/// let screen_width = use_breakpoints(breakpoints_tailwind());
26///
27/// use BreakpointsTailwind::*;
28///
29/// let sm_and_larger = screen_width.ge(Sm);
30/// let larger_than_sm = screen_width.gt(Sm);
31/// let lg_and_smaller = screen_width.le(Lg);
32/// let smaller_than_lg = screen_width.lt(Lg);
33/// #
34/// # view! { }
35/// # }
36/// ```
37///
38/// ## Breakpoints
39///
40/// There are many predefined breakpoints for major UI frameworks. The following are provided.
41///
42/// * [`breakpoints_tailwind`]
43/// * [`breakpoints_bootstrap_v5`]
44/// * [`breakpoints_material`]
45/// * [`breakpoints_ant_design`]
46/// * [`breakpoints_quasar`]
47/// * [`breakpoints_semantic`]
48/// * [`breakpoints_master_css`]
49///
50/// You can also provide your own breakpoints.
51///
52/// ```
53/// # use std::collections::HashMap;
54/// use leptos::prelude::*;
55/// # use leptos_use::use_breakpoints;
56/// #
57/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58/// enum MyBreakpoints {
59///     Tablet,
60///     Laptop,
61///     Desktop,
62/// }
63///
64/// fn my_breakpoints() -> HashMap<MyBreakpoints, u32> {
65///     use MyBreakpoints::*;
66///
67///     HashMap::from([
68///         (Tablet, 640),
69///         (Laptop, 1024),
70///         (Desktop, 1280),
71///     ])
72/// }
73///
74/// #[component]
75/// fn Demo() -> impl IntoView {
76///     let screen_width = use_breakpoints(my_breakpoints());
77///
78///     use MyBreakpoints::*;
79///
80///     let laptop = screen_width.between(Laptop, Desktop);
81///
82///     view! { }
83/// }
84/// ```
85///
86/// ## Non-reactive methods
87///
88/// For every reactive method there is also a non-reactive variant that is prefixed with `is_`
89///
90/// ```
91/// # use leptos::prelude::*;
92/// # use leptos_use::{use_breakpoints, BreakpointsTailwind, breakpoints_tailwind};
93/// #
94/// # #[component]
95/// # fn Demo() -> impl IntoView {
96/// #
97/// let screen_width = use_breakpoints(breakpoints_tailwind());
98///
99/// use BreakpointsTailwind::*;
100///
101/// let sm_and_larger = screen_width.is_ge(Sm);
102/// let larger_than_sm = screen_width.is_gt(Sm);
103/// let lg_and_smaller = screen_width.is_le(Lg);
104/// let smaller_than_lg = screen_width.is_lt(Lg);
105/// #
106/// # view! { }
107/// # }
108/// ```
109///
110/// ## Server-Side Rendering
111///
112/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
113///
114/// Since internally this uses [`fn@crate::use_media_query`], which returns always `false` on the server,
115/// the returned methods also will return `false`.
116pub fn use_breakpoints<K: Eq + Hash + Debug + Clone + Send + Sync>(
117    breakpoints: HashMap<K, u32>,
118) -> UseBreakpointsReturn<K> {
119    UseBreakpointsReturn { breakpoints }
120}
121
122/// Return type of [`use_breakpoints`]
123#[derive(Clone)]
124pub struct UseBreakpointsReturn<K: Eq + Hash + Debug + Clone + Send + Sync> {
125    breakpoints: HashMap<K, u32>,
126}
127
128macro_rules! query_suffix {
129    (>) => {
130        ".1"
131    };
132    (<) => {
133        ".9"
134    };
135    (=) => {
136        ""
137    };
138}
139
140macro_rules! value_expr {
141    ($v:ident, >) => {
142        $v
143    };
144    ($v:ident, <) => {
145        $v - 1
146    };
147    ($v:ident, =) => {
148        $v
149    };
150}
151
152macro_rules! format_media_query {
153    ($cmp:tt, $suffix:tt, $v:ident) => {
154        format!(
155            "({}-width: {}{}px)",
156            $cmp,
157            value_expr!($v, $suffix),
158            query_suffix!($suffix)
159        )
160    };
161}
162
163macro_rules! impl_cmp_reactively {
164    (   #[$attr:meta]
165        $fn:ident, $cmp:tt, $suffix:tt) => {
166        paste! {
167            // Reactive check if
168            #[$attr]
169            pub fn $fn(&self, key: K) -> Signal<bool> {
170                if let Some(value) = self.breakpoints.get(&key) {
171                    use_media_query(format_media_query!($cmp, $suffix, value))
172                } else {
173                    self.not_found_signal(key)
174                }
175            }
176
177            // Static check if
178            #[$attr]
179            pub fn [<is_ $fn>](&self, key: K) -> bool {
180                if let Some(value) = self.breakpoints.get(&key) {
181                    Self::match_(&format_media_query!($cmp, $suffix, value))
182                } else {
183                    self.not_found(key)
184                }
185            }
186        }
187    };
188}
189
190impl<K> UseBreakpointsReturn<K>
191where
192    K: Eq + Hash + Debug + Clone + Send + Sync + 'static,
193{
194    fn match_(query: &str) -> bool {
195        if let Ok(Some(query_list)) = use_window().match_media(query) {
196            return query_list.matches();
197        }
198
199        false
200    }
201
202    fn not_found_signal(&self, key: K) -> Signal<bool> {
203        error!("Breakpoint \"{:?}\" not found", key);
204        Signal::derive(|| false)
205    }
206
207    fn not_found(&self, key: K) -> bool {
208        error!("Breakpoint \"{:?}\" not found", key);
209        false
210    }
211
212    impl_cmp_reactively!(
213        /// `[screen size]` > `key`
214        gt, "min", >
215    );
216    impl_cmp_reactively!(
217        /// `[screen size]` >= `key`
218        ge, "min", =
219    );
220    impl_cmp_reactively!(
221        /// `[screen size]` < `key`
222        lt, "max", <
223    );
224    impl_cmp_reactively!(
225        /// `[screen size]` <= `key`
226        le, "max", =
227    );
228
229    fn between_media_query(min: &u32, max: &u32) -> String {
230        format!("(min-width: {min}px) and (max-width: {}.9px)", max - 1)
231    }
232
233    /// Reactive check if `min_key` <= `[screen size]` <= `max_key`
234    pub fn between(&self, min_key: K, max_key: K) -> Signal<bool> {
235        if let Some(min) = self.breakpoints.get(&min_key) {
236            if let Some(max) = self.breakpoints.get(&max_key) {
237                use_media_query(Self::between_media_query(min, max))
238            } else {
239                self.not_found_signal(max_key)
240            }
241        } else {
242            self.not_found_signal(min_key)
243        }
244    }
245
246    /// Static check if `min_key` <= `[screen size]` <= `max_key`
247    pub fn is_between(&self, min_key: K, max_key: K) -> bool {
248        if let Some(min) = self.breakpoints.get(&min_key) {
249            if let Some(max) = self.breakpoints.get(&max_key) {
250                Self::match_(&Self::between_media_query(min, max))
251            } else {
252                self.not_found(max_key)
253            }
254        } else {
255            self.not_found(min_key)
256        }
257    }
258
259    /// Reactive Vec of all breakpoints that fulfill `[screen size]` >= `key`
260    pub fn current(&self) -> Signal<Vec<K>> {
261        let breakpoints = self.breakpoints.clone();
262        let keys: Vec<_> = breakpoints.keys().cloned().collect();
263
264        let ge = move |key: &K| {
265            let value = breakpoints
266                .get(key)
267                .expect("only used with keys() from the HashMap");
268
269            use_media_query(format_media_query!("min", =, value))
270        };
271
272        let signals: Vec<_> = keys.iter().map(ge.clone()).collect();
273
274        Signal::derive(move || {
275            keys.iter()
276                .cloned()
277                .zip(signals.iter().cloned())
278                .filter_map(|(key, signal)| signal.get().then_some(key))
279                .collect::<Vec<_>>()
280        })
281    }
282}
283
284/// Breakpoint keys for Tailwind V2
285///
286/// See <https://tailwindcss.com/docs/breakpoints>
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288pub enum BreakpointsTailwind {
289    Sm,
290    Md,
291    Lg,
292    Xl,
293    Xxl,
294}
295
296/// Breakpoint definitions for Tailwind V2
297///
298/// See <https://tailwindcss.com/docs/breakpoints>
299pub fn breakpoints_tailwind() -> HashMap<BreakpointsTailwind, u32> {
300    HashMap::from([
301        (BreakpointsTailwind::Sm, 640),
302        (BreakpointsTailwind::Md, 768),
303        (BreakpointsTailwind::Lg, 1024),
304        (BreakpointsTailwind::Xl, 1280),
305        (BreakpointsTailwind::Xxl, 1536),
306    ])
307}
308
309/// Breakpoint keys for Bootstrap V5
310///
311/// See <https://getbootstrap.com/docs/5.0/layout/breakpoints>
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
313pub enum BreakpointsBootstrapV5 {
314    Sm,
315    Md,
316    Lg,
317    Xl,
318    Xxl,
319}
320
321/// Breakpoint definitions for Bootstrap V5
322///
323/// <https://getbootstrap.com/docs/5.0/layout/breakpoints>
324pub fn breakpoints_bootstrap_v5() -> HashMap<BreakpointsBootstrapV5, u32> {
325    HashMap::from([
326        (BreakpointsBootstrapV5::Sm, 576),
327        (BreakpointsBootstrapV5::Md, 768),
328        (BreakpointsBootstrapV5::Lg, 992),
329        (BreakpointsBootstrapV5::Xl, 1200),
330        (BreakpointsBootstrapV5::Xxl, 1400),
331    ])
332}
333
334/// Breakpoint keys for Material UI V5
335///
336/// See <https://mui.com/material-ui/customization/breakpoints/>
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub enum BreakpointsMaterial {
339    Xs,
340    Sm,
341    Md,
342    Lg,
343    Xl,
344}
345
346/// Breakpoint definitions for Material UI V5
347///
348/// See <https://mui.com/material-ui/customization/breakpoints/>
349pub fn breakpoints_material() -> HashMap<BreakpointsMaterial, u32> {
350    HashMap::from([
351        (BreakpointsMaterial::Xs, 1),
352        (BreakpointsMaterial::Sm, 600),
353        (BreakpointsMaterial::Md, 900),
354        (BreakpointsMaterial::Lg, 1200),
355        (BreakpointsMaterial::Xl, 1536),
356    ])
357}
358
359/// Breakpoint keys for Ant Design
360///
361/// See <https://ant.design/components/layout/#breakpoint-width>
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
363pub enum BreakpointsAntDesign {
364    Xs,
365    Sm,
366    Md,
367    Lg,
368    Xl,
369    Xxl,
370}
371
372/// Breakpoint definitions for Ant Design
373///
374/// See <https://ant.design/components/layout/#breakpoint-width>
375pub fn breakpoints_ant_design() -> HashMap<BreakpointsAntDesign, u32> {
376    HashMap::from([
377        (BreakpointsAntDesign::Xs, 480),
378        (BreakpointsAntDesign::Sm, 576),
379        (BreakpointsAntDesign::Md, 768),
380        (BreakpointsAntDesign::Lg, 992),
381        (BreakpointsAntDesign::Xl, 1200),
382        (BreakpointsAntDesign::Xxl, 1600),
383    ])
384}
385
386/// Breakpoint keys for Quasar V2
387///
388/// See <https://quasar.dev/style/breakpoints>
389#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
390pub enum BreakpointsQuasar {
391    Xs,
392    Sm,
393    Md,
394    Lg,
395    Xl,
396}
397
398/// Breakpoint definitions for Quasar V2
399///
400/// See <https://quasar.dev/style/breakpoints>
401pub fn breakpoints_quasar() -> HashMap<BreakpointsQuasar, u32> {
402    HashMap::from([
403        (BreakpointsQuasar::Xs, 1),
404        (BreakpointsQuasar::Sm, 600),
405        (BreakpointsQuasar::Md, 1024),
406        (BreakpointsQuasar::Lg, 1440),
407        (BreakpointsQuasar::Xl, 1920),
408    ])
409}
410
411/// Breakpoint keys for Semantic UI
412///
413/// See <https://semantic-ui.com/elements/container.html>
414#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
415pub enum BreakpointsSemantic {
416    Mobile,
417    Tablet,
418    SmallMonitor,
419    LargeMonitor,
420}
421
422/// Breakpoint definitions for Semantic UI
423///
424/// See <https://semantic-ui.com/elements/container.html>
425pub fn breakpoints_semantic() -> HashMap<BreakpointsSemantic, u32> {
426    HashMap::from([
427        (BreakpointsSemantic::Mobile, 1),
428        (BreakpointsSemantic::Tablet, 768),
429        (BreakpointsSemantic::SmallMonitor, 992),
430        (BreakpointsSemantic::LargeMonitor, 1200),
431    ])
432}
433
434/// Breakpoint keys for Master CSS
435///
436/// See <https://docs.master.co/css/breakpoints>
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
438pub enum BreakpointsMasterCss {
439    Xxxs,
440    Xxs,
441    Xs,
442    Sm,
443    Md,
444    Lg,
445    Xl,
446    Xxl,
447    Xxxl,
448    Xxxxl,
449}
450
451/// Breakpoint definitions for Master CSS
452///
453/// See <https://docs.master.co/css/breakpoints>
454pub fn breakpoints_master_css() -> HashMap<BreakpointsMasterCss, u32> {
455    HashMap::from([
456        (BreakpointsMasterCss::Xxxs, 360),
457        (BreakpointsMasterCss::Xxs, 480),
458        (BreakpointsMasterCss::Xs, 600),
459        (BreakpointsMasterCss::Sm, 768),
460        (BreakpointsMasterCss::Md, 1024),
461        (BreakpointsMasterCss::Lg, 1280),
462        (BreakpointsMasterCss::Xl, 1440),
463        (BreakpointsMasterCss::Xxl, 1600),
464        (BreakpointsMasterCss::Xxxl, 1920),
465        (BreakpointsMasterCss::Xxxxl, 2560),
466    ])
467}