Skip to main content

cvkg_layout/
primitives.rs

1pub use cvkg_core::layout::EdgeInsets;
2use cvkg_core::{LayoutCache, LayoutView, Rect, Size, SizeProposal};
3
4/// A layout view that adds padding around its child.
5pub struct Padding {
6    pub insets: EdgeInsets,
7}
8
9impl Padding {
10    /// Creates a new Padding layout view with margins on each side.
11    pub fn new(insets: EdgeInsets) -> Self {
12        Self { insets }
13    }
14
15    /// Creates a Padding layout with uniform margin.
16    pub fn uniform(value: f32) -> Self {
17        Self {
18            insets: EdgeInsets::all(value),
19        }
20    }
21
22    /// Creates a Padding layout with symmetric horizontal and vertical margins.
23    pub fn symmetric(horizontal: f32, vertical: f32) -> Self {
24        Self {
25            insets: EdgeInsets {
26                top: vertical,
27                bottom: vertical,
28                leading: horizontal,
29                trailing: horizontal,
30            },
31        }
32    }
33}
34
35impl LayoutView for Padding {
36    fn size_that_fits(
37        &self,
38        proposal: SizeProposal,
39        subviews: &[&dyn LayoutView],
40        cache: &mut LayoutCache,
41    ) -> Size {
42        let inner_proposal = SizeProposal::new(
43            proposal
44                .width
45                .map(|w| (w - self.insets.leading - self.insets.trailing).max(0.0)),
46            proposal
47                .height
48                .map(|h| (h - self.insets.top - self.insets.bottom).max(0.0)),
49        );
50        let self_hash = self.view_hash();
51        let child_size = if subviews.is_empty() {
52            Size::ZERO
53        } else {
54            let child_hash = subviews[0].view_hash();
55            if self_hash != 0 && child_hash != 0 {
56                cache.register_parent(child_hash, self_hash);
57            }
58            crate::with_layout_cycle_guard(child_hash, Size::ZERO, || {
59                subviews[0].size_that_fits(inner_proposal, &[], cache)
60            })
61        };
62        Size {
63            width: child_size.width + self.insets.leading + self.insets.trailing,
64            height: child_size.height + self.insets.top + self.insets.bottom,
65        }
66    }
67
68    fn place_subviews(
69        &self,
70        bounds: Rect,
71        subviews: &mut [&mut dyn LayoutView],
72        cache: &mut LayoutCache,
73    ) {
74        let inner = Rect {
75            x: bounds.x + self.insets.leading,
76            y: bounds.y + self.insets.top,
77            width: (bounds.width - self.insets.leading - self.insets.trailing).max(0.0),
78            height: (bounds.height - self.insets.top - self.insets.bottom).max(0.0),
79        };
80        let self_hash = self.view_hash();
81        for child in subviews.iter_mut() {
82            let child_hash = child.view_hash();
83            if self_hash != 0 && child_hash != 0 {
84                cache.register_parent(child_hash, self_hash);
85            }
86            let is_visible = if let Some(viewport) = cache.viewport {
87                inner.intersects(&viewport)
88            } else {
89                true
90            };
91            if is_visible {
92                crate::with_layout_cycle_guard_void(child_hash, || {
93                    child.place_subviews(inner, &mut [], cache);
94                });
95            }
96        }
97    }
98}
99
100/// A layout view that respects safe area insets (notches, status bars).
101pub struct SafeArea {
102    pub edges: SafeAreaEdges,
103}
104
105/// Active safe-area edge constraints.
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
107pub struct SafeAreaEdges {
108    pub top: bool,
109    pub bottom: bool,
110    pub leading: bool,
111    pub trailing: bool,
112}
113
114impl Default for SafeAreaEdges {
115    fn default() -> Self {
116        Self {
117            top: true,
118            bottom: true,
119            leading: false,
120            trailing: false,
121        }
122    }
123}
124
125impl SafeArea {
126    /// Enables safe area on all four sides.
127    pub fn all() -> Self {
128        Self {
129            edges: SafeAreaEdges {
130                top: true,
131                bottom: true,
132                leading: true,
133                trailing: true,
134            },
135        }
136    }
137
138    /// Enables safe area vertical edges only (top and bottom).
139    pub fn vertical() -> Self {
140        Self {
141            edges: SafeAreaEdges::default(),
142        }
143    }
144
145    fn insets(&self) -> EdgeInsets {
146        EdgeInsets {
147            top: if self.edges.top { 44.0 } else { 0.0 },
148            bottom: if self.edges.bottom { 34.0 } else { 0.0 },
149            leading: 0.0,
150            trailing: 0.0,
151        }
152    }
153}
154
155impl LayoutView for SafeArea {
156    fn size_that_fits(
157        &self,
158        proposal: SizeProposal,
159        subviews: &[&dyn LayoutView],
160        cache: &mut LayoutCache,
161    ) -> Size {
162        Padding::new(self.insets()).size_that_fits(proposal, subviews, cache)
163    }
164
165    fn place_subviews(
166        &self,
167        bounds: Rect,
168        subviews: &mut [&mut dyn LayoutView],
169        cache: &mut LayoutCache,
170    ) {
171        Padding::new(self.insets()).place_subviews(bounds, subviews, cache);
172    }
173}
174
175/// Constrains a child to a specific aspect ratio.
176pub struct AspectRatio {
177    pub ratio: f32,
178}
179
180impl AspectRatio {
181    /// Creates a new AspectRatio.
182    pub fn new(ratio: f32) -> Self {
183        Self {
184            ratio: ratio.max(0.01),
185        }
186    }
187
188    /// Square aspect ratio (1.0).
189    pub fn square() -> Self {
190        Self::new(1.0)
191    }
192
193    /// Widescreen aspect ratio (16:9).
194    pub fn widescreen() -> Self {
195        Self::new(16.0 / 9.0)
196    }
197
198    /// Portrait aspect ratio (9:16).
199    pub fn portrait() -> Self {
200        Self::new(9.0 / 16.0)
201    }
202
203    fn fitted_size(&self, proposal: SizeProposal) -> Size {
204        let max_w = proposal.width.unwrap_or(f32::MAX);
205        let max_h = proposal.height.unwrap_or(f32::MAX);
206        let w = max_w;
207        let h = w / self.ratio;
208        if h <= max_h {
209            return Size {
210                width: w,
211                height: h,
212            };
213        }
214        Size {
215            width: max_h * self.ratio,
216            height: max_h,
217        }
218    }
219}
220
221impl LayoutView for AspectRatio {
222    fn size_that_fits(
223        &self,
224        proposal: SizeProposal,
225        subviews: &[&dyn LayoutView],
226        cache: &mut LayoutCache,
227    ) -> Size {
228        if subviews.is_empty() {
229            return self.fitted_size(proposal);
230        }
231        let self_hash = self.view_hash();
232        let child = subviews[0];
233        let child_hash = child.view_hash();
234        if self_hash != 0 && child_hash != 0 {
235            cache.register_parent(child_hash, self_hash);
236        }
237        let child_size = crate::with_layout_cycle_guard(child_hash, Size::ZERO, || {
238            child.size_that_fits(
239                SizeProposal::new(Some(f32::MAX), Some(f32::MAX)),
240                &[],
241                cache,
242            )
243        });
244        let intrinsic_ratio = child_size.width / child_size.height.max(0.01);
245        if (intrinsic_ratio - self.ratio).abs() < 0.01 {
246            return self.fitted_size(proposal);
247        }
248        let fit = self.fitted_size(proposal);
249        let child_w = fit.width.min(child_size.width);
250        let child_h = child_w / intrinsic_ratio;
251        let final_h = child_h.min(fit.height);
252        let final_w = final_h * intrinsic_ratio;
253        Size {
254            width: final_w,
255            height: final_h,
256        }
257    }
258
259    fn place_subviews(
260        &self,
261        bounds: Rect,
262        subviews: &mut [&mut dyn LayoutView],
263        cache: &mut LayoutCache,
264    ) {
265        let fit = self.fitted_size(SizeProposal::new(Some(bounds.width), Some(bounds.height)));
266        let x = bounds.x + (bounds.width - fit.width) * 0.5;
267        let y = bounds.y + (bounds.height - fit.height) * 0.0;
268        let inner = Rect {
269            x,
270            y,
271            width: fit.width,
272            height: fit.height,
273        };
274        let self_hash = self.view_hash();
275        for child in subviews.iter_mut() {
276            let child_hash = child.view_hash();
277            if self_hash != 0 && child_hash != 0 {
278                cache.register_parent(child_hash, self_hash);
279            }
280            let is_visible = if let Some(viewport) = cache.viewport {
281                inner.intersects(&viewport)
282            } else {
283                true
284            };
285            if is_visible {
286                crate::with_layout_cycle_guard_void(child_hash, || {
287                    child.place_subviews(inner, &mut [], cache);
288                });
289            }
290        }
291    }
292}