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#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Responsive {
60    /// Optional stable identity for diagnostics and retained child identity.
61    pub id: Option<WidgetId>,
62    /// Width source used to evaluate the responsive cases.
63    pub query: ResponsiveQuery,
64    /// Ordered cases; the first matching case wins.
65    pub cases: Vec<ResponsiveCase>,
66    /// Child rendered when no case matches.
67    pub fallback: Widget,
68}
69
70impl Responsive {
71    /// Creates a responsive switch with a required fallback branch.
72    pub fn new(fallback: impl Into<Widget>) -> Self {
73        Self {
74            id: None,
75            query: ResponsiveQuery::Viewport,
76            cases: Vec::new(),
77            fallback: fallback.into(),
78        }
79    }
80
81    /// Sets an explicit identity for deterministic inspection and diffing.
82    pub fn id(mut self, id: WidgetId) -> Self {
83        self.id = Some(id);
84        self
85    }
86
87    /// Appends an ordered responsive case.
88    pub fn case(mut self, case: ResponsiveCase) -> Self {
89        self.cases.push(case);
90        self
91    }
92
93    /// Selects viewport or parent-container width evaluation.
94    pub fn query(mut self, query: ResponsiveQuery) -> Self {
95        self.query = query;
96        self
97    }
98
99    /// Evaluates cases against the immediate parent constraints.
100    pub fn container_query(self) -> Self {
101        self.query(ResponsiveQuery::Container)
102    }
103}
104
105impl InternalLower for Responsive {
106    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
107        let id = self.id.unwrap_or_else(|| cx.next_node_id());
108        cx.push_scope(id);
109        let mut children = Vec::with_capacity(self.cases.len() + 1);
110        for case in &self.cases {
111            children.push(case.child.lower(cx));
112        }
113        children.push(self.fallback.lower(cx));
114        cx.pop_scope();
115
116        let mut builder = InternalIrBuilder::new(
117            id,
118            Op::Layout(LayoutOp::Responsive {
119                query: self.query,
120                cases: self.cases.iter().map(ResponsiveCase::condition).collect(),
121            }),
122        );
123        for child in children {
124            builder.add_child(child);
125        }
126        builder.build(cx)
127    }
128}