Skip to main content

fission_core/ui/widgets/
responsive.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::ui::Widget;
4use fission_ir::{
5    op::{ResponsiveCondition, ResponsiveQuery},
6    LayoutOp, Op, WidgetId,
7};
8use serde::{Deserialize, Serialize};
9
10/// A declarative responsive branch selected for a width range.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ResponsiveCase {
13    /// Inclusive minimum width for this branch.
14    pub min_width: Option<f32>,
15    /// Exclusive maximum width for this branch.
16    pub max_width: Option<f32>,
17    /// Child rendered when the width falls inside this branch.
18    pub child: Widget,
19}
20
21impl ResponsiveCase {
22    /// Matches widths greater than or equal to `min_width`.
23    pub fn min_width(min_width: f32, child: impl Into<Widget>) -> Self {
24        Self {
25            min_width: Some(min_width),
26            max_width: None,
27            child: child.into(),
28        }
29    }
30
31    /// Matches widths below `max_width`.
32    pub fn max_width(max_width: f32, child: impl Into<Widget>) -> Self {
33        Self {
34            min_width: None,
35            max_width: Some(max_width),
36            child: child.into(),
37        }
38    }
39
40    /// Matches an inclusive lower and exclusive upper width range.
41    pub fn between(min_width: f32, max_width: f32, child: impl Into<Widget>) -> Self {
42        Self {
43            min_width: Some(min_width),
44            max_width: Some(max_width),
45            child: child.into(),
46        }
47    }
48
49    fn condition(&self) -> ResponsiveCondition {
50        ResponsiveCondition {
51            min_width: self.min_width,
52            max_width: self.max_width,
53        }
54    }
55}
56
57/// Selects a widget branch using declarative viewport or container breakpoints.
58///
59/// Prefer `Responsive` over manually reading `view.viewport_size()` when the
60/// rule is only "render this child below or above this width". Keep viewport
61/// reads for cases that also depend on app state, environment values, or
62/// measured geometry.
63///
64/// # Example
65///
66/// ```rust,ignore
67/// Responsive::new(DesktopShell)
68///     .id(WidgetId::explicit("mail.responsive"))
69///     .case(ResponsiveCase::max_width(900.0, PhoneShell))
70///     .case(ResponsiveCase::between(900.0, 1200.0, TabletShell))
71///     .into()
72/// ```
73///
74/// Every branch is lowered, even though layout displays only the selected one.
75/// Build each branch independently rather than cloning a `Widget` value, and
76/// keep explicit widget IDs unique across the complete responsive tree.
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct Responsive {
79    /// Optional stable identity for diagnostics and retained child identity.
80    pub id: Option<WidgetId>,
81    /// Width source used to evaluate the responsive cases.
82    pub query: ResponsiveQuery,
83    /// Ordered cases; the first matching case wins.
84    pub cases: Vec<ResponsiveCase>,
85    /// Child rendered when no case matches.
86    pub fallback: Widget,
87}
88
89impl Responsive {
90    /// Creates a responsive switch with a required fallback branch.
91    pub fn new(fallback: impl Into<Widget>) -> Self {
92        Self {
93            id: None,
94            query: ResponsiveQuery::Viewport,
95            cases: Vec::new(),
96            fallback: fallback.into(),
97        }
98    }
99
100    /// Sets an explicit identity for deterministic inspection and diffing.
101    pub fn id(mut self, id: WidgetId) -> Self {
102        self.id = Some(id);
103        self
104    }
105
106    /// Appends an ordered responsive case.
107    pub fn case(mut self, case: ResponsiveCase) -> Self {
108        self.cases.push(case);
109        self
110    }
111
112    /// Selects viewport or parent-container width evaluation.
113    pub fn query(mut self, query: ResponsiveQuery) -> Self {
114        self.query = query;
115        self
116    }
117
118    /// Evaluates cases against the immediate parent constraints.
119    pub fn container_query(self) -> Self {
120        self.query(ResponsiveQuery::Container)
121    }
122}
123
124impl InternalLower for Responsive {
125    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
126        let id = self.id.unwrap_or_else(|| cx.next_node_id());
127        cx.push_scope(id);
128        let mut children = Vec::with_capacity(self.cases.len() + 1);
129        for case in &self.cases {
130            children.push(case.child.lower(cx));
131        }
132        children.push(self.fallback.lower(cx));
133        cx.pop_scope();
134
135        let mut builder = InternalIrBuilder::new(
136            id,
137            Op::Layout(LayoutOp::Responsive {
138                query: self.query,
139                cases: self.cases.iter().map(ResponsiveCase::condition).collect(),
140            }),
141        );
142        for child in children {
143            builder.add_child(child);
144        }
145        builder.build(cx)
146    }
147}