1use std::sync::Arc;
2
3use fret_core::{Px, Rect};
4use fret_ui::element::{AnyElement, LayoutQueryRegionProps};
5use fret_ui::{ElementContext, GlobalElementId, Invalidation, UiHost};
6
7use crate::{IntoUiElement, collect_children};
8
9pub mod tailwind {
14 use fret_core::Px;
15
16 pub const SM: Px = Px(640.0);
17 pub const MD: Px = Px(768.0);
18 pub const LG: Px = Px(1024.0);
19 pub const XL: Px = Px(1280.0);
20 pub const XXL: Px = Px(1536.0);
21}
22
23#[derive(Debug, Clone, Copy)]
24pub struct ContainerQueryHysteresis {
25 pub up: Px,
26 pub down: Px,
27}
28
29impl Default for ContainerQueryHysteresis {
30 fn default() -> Self {
31 Self {
34 up: Px(8.0),
35 down: Px(8.0),
36 }
37 }
38}
39
40#[derive(Debug, Default, Clone, Copy)]
41struct ContainerBreakpointsState {
42 active_index: usize,
44 initialized: bool,
45}
46
47#[derive(Debug, Default, Clone, Copy)]
48struct ContainerWidthAtLeastState {
49 active: bool,
50 initialized: bool,
51}
52
53fn container_breakpoints_init_active_index<T: Copy>(width: Px, breakpoints: &[(Px, T)]) -> usize {
54 let mut active_index = 0;
55 for (i, (min_width, _)) in breakpoints.iter().enumerate() {
56 if width.0 >= min_width.0 {
57 active_index = i + 1;
58 }
59 }
60 active_index
61}
62
63fn container_breakpoints_apply_hysteresis<T: Copy>(
64 width: Px,
65 breakpoints: &[(Px, T)],
66 hysteresis: ContainerQueryHysteresis,
67 mut active_index: usize,
68) -> usize {
69 loop {
70 if active_index >= breakpoints.len() {
71 break;
72 }
73 let next_min_width = breakpoints[active_index].0;
74 if width.0 >= next_min_width.0 + hysteresis.up.0 {
75 active_index = active_index.saturating_add(1);
76 continue;
77 }
78 break;
79 }
80
81 loop {
82 if active_index == 0 {
83 break;
84 }
85 let cur_min_width = breakpoints[active_index - 1].0;
86 if width.0 < cur_min_width.0 - hysteresis.down.0 {
87 active_index = active_index.saturating_sub(1);
88 continue;
89 }
90 break;
91 }
92
93 active_index
94}
95
96fn container_width_at_least_init(width: Px, threshold: Px) -> bool {
97 width.0 >= threshold.0
98}
99
100fn container_width_at_least_apply_hysteresis(
101 width: Px,
102 threshold: Px,
103 hysteresis: ContainerQueryHysteresis,
104 active: bool,
105) -> bool {
106 if !active && width.0 >= threshold.0 + hysteresis.up.0 {
107 return true;
108 }
109 if active && width.0 < threshold.0 - hysteresis.down.0 {
110 return false;
111 }
112 active
113}
114
115#[track_caller]
120pub fn container_query_region_with_id<H, I, T>(
121 cx: &mut ElementContext<'_, H>,
122 name: impl Into<Arc<str>>,
123 mut props: LayoutQueryRegionProps,
124 f: impl FnOnce(&mut ElementContext<'_, H>, GlobalElementId) -> I,
125) -> AnyElement
126where
127 H: UiHost,
128 I: IntoIterator<Item = T>,
129 T: IntoUiElement<H>,
130{
131 props.name = Some(name.into());
132 cx.layout_query_region_with_id(props, move |cx, id| {
133 let items = f(cx, id);
134 collect_children(cx, items)
135 })
136}
137
138#[track_caller]
139pub fn container_query_region<H, I, T>(
140 cx: &mut ElementContext<'_, H>,
141 name: impl Into<Arc<str>>,
142 props: LayoutQueryRegionProps,
143 f: impl FnOnce(&mut ElementContext<'_, H>) -> I,
144) -> AnyElement
145where
146 H: UiHost,
147 I: IntoIterator<Item = T>,
148 T: IntoUiElement<H>,
149{
150 container_query_region_with_id(cx, name, props, |cx, _id| f(cx))
151}
152
153#[track_caller]
167pub fn container_breakpoints<H, T: Copy>(
168 cx: &mut ElementContext<'_, H>,
169 region: GlobalElementId,
170 invalidation: Invalidation,
171 base: T,
172 breakpoints: &[(Px, T)],
173 hysteresis: ContainerQueryHysteresis,
174) -> T
175where
176 H: UiHost,
177{
178 let rect: Option<Rect> = cx.layout_query_bounds(region, invalidation);
179 let Some(width) = rect.map(|r| r.size.width) else {
180 return base;
181 };
182
183 cx.slot_state(ContainerBreakpointsState::default, |st| {
184 if !st.initialized {
185 st.active_index = container_breakpoints_init_active_index(width, breakpoints);
186 st.initialized = true;
187 }
188
189 st.active_index =
190 container_breakpoints_apply_hysteresis(width, breakpoints, hysteresis, st.active_index);
191
192 if st.active_index == 0 {
193 return base;
194 }
195 breakpoints
196 .get(st.active_index - 1)
197 .map(|(_, v)| *v)
198 .unwrap_or(base)
199 })
200}
201
202#[track_caller]
210pub fn container_width_at_least<H: UiHost>(
211 cx: &mut ElementContext<'_, H>,
212 region: GlobalElementId,
213 invalidation: Invalidation,
214 default_when_unknown: bool,
215 threshold: Px,
216 hysteresis: ContainerQueryHysteresis,
217) -> bool {
218 let rect: Option<Rect> = cx.layout_query_bounds(region, invalidation);
219 let Some(width) = rect.map(|r| r.size.width) else {
220 return default_when_unknown;
221 };
222
223 cx.slot_state(ContainerWidthAtLeastState::default, |st| {
224 if !st.initialized {
225 st.active = container_width_at_least_init(width, threshold);
226 st.initialized = true;
227 }
228
229 st.active =
230 container_width_at_least_apply_hysteresis(width, threshold, hysteresis, st.active);
231 st.active
232 })
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn container_breakpoints_hysteresis_transitions() {
241 let breakpoints = &[
242 (tailwind::SM, 1usize),
243 (tailwind::MD, 2usize),
244 (tailwind::LG, 3usize),
245 ];
246 let hysteresis = ContainerQueryHysteresis {
247 up: Px(8.0),
248 down: Px(8.0),
249 };
250
251 let mut active_index = container_breakpoints_init_active_index(Px(700.0), breakpoints);
253 assert_eq!(active_index, 1);
254
255 active_index = container_breakpoints_apply_hysteresis(
257 Px(770.0),
258 breakpoints,
259 hysteresis,
260 active_index,
261 );
262 assert_eq!(active_index, 1);
263 active_index = container_breakpoints_apply_hysteresis(
264 Px(776.0),
265 breakpoints,
266 hysteresis,
267 active_index,
268 );
269 assert_eq!(active_index, 2);
270
271 active_index = container_breakpoints_apply_hysteresis(
273 Px(762.0),
274 breakpoints,
275 hysteresis,
276 active_index,
277 );
278 assert_eq!(active_index, 2);
279 active_index = container_breakpoints_apply_hysteresis(
280 Px(759.0),
281 breakpoints,
282 hysteresis,
283 active_index,
284 );
285 assert_eq!(active_index, 1);
286
287 active_index = container_breakpoints_apply_hysteresis(
289 Px(2000.0),
290 breakpoints,
291 hysteresis,
292 active_index,
293 );
294 assert_eq!(active_index, 3);
295 }
296
297 #[test]
298 fn container_width_at_least_hysteresis_transitions() {
299 let threshold = tailwind::MD;
300 let hysteresis = ContainerQueryHysteresis::default();
301
302 let mut active = container_width_at_least_init(Px(700.0), threshold);
303 assert!(!active);
304
305 active =
307 container_width_at_least_apply_hysteresis(Px(770.0), threshold, hysteresis, active);
308 assert!(!active);
309 active =
310 container_width_at_least_apply_hysteresis(Px(776.0), threshold, hysteresis, active);
311 assert!(active);
312
313 active =
315 container_width_at_least_apply_hysteresis(Px(762.0), threshold, hysteresis, active);
316 assert!(active);
317 active =
318 container_width_at_least_apply_hysteresis(Px(759.0), threshold, hysteresis, active);
319 assert!(!active);
320 }
321}