teksilo_widgets/primitives/
aspect_ratio.rs1use teksilo_canvas::{Point, Rect, Size, SizeProposal};
31use teksilo_core::accessibility::AccessNodeBuilder;
32use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
33use teksilo_core::widget_id::WidgetId;
34
35#[derive(Debug)]
37pub struct AspectRatio {
38 ratio: f32,
40 child_id: Option<WidgetId>,
41 pending_child: Option<PendingChild>,
42}
43
44impl AspectRatio {
45 pub fn new(ratio: f32) -> Self {
47 Self {
48 ratio: ratio.max(f32::EPSILON),
49 child_id: None,
50 pending_child: None,
51 }
52 }
53
54 pub fn widescreen() -> Self {
56 Self::new(16.0 / 9.0)
57 }
58
59 pub fn square() -> Self {
61 Self::new(1.0)
62 }
63
64 pub fn child(mut self, widget: impl teksilo_core::IntoTeksiChild) -> Self {
67 self.pending_child = Some(teksilo_core::IntoTeksiChild::into_pending(widget));
68 self
69 }
70 pub fn child_opt(self, widget: Option<impl teksilo_core::IntoTeksiChild>) -> Self {
77 match widget {
78 Some(w) => self.child(w),
79 None => self,
80 }
81 }
82
83 fn constrain(&self, width: Option<f32>, height: Option<f32>) -> Size {
85 match (width, height) {
86 (Some(w), Some(h)) => {
87 let h_from_w = w / self.ratio;
89 let w_from_h = h * self.ratio;
90 if h_from_w <= h {
91 Size::new(w, h_from_w)
92 } else {
93 Size::new(w_from_h, h)
94 }
95 }
96 (Some(w), None) => Size::new(w, w / self.ratio),
97 (None, Some(h)) => Size::new(h * self.ratio, h),
98 (None, None) => Size::new(0.0, 0.0),
99 }
100 }
101}
102
103impl Widget for AspectRatio {
104 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
105 if let Some(pending) = self.pending_child.take() {
106 self.child_id = Some(match pending {
107 PendingChild::Id(id) => id,
108 PendingChild::Deferred(w) => ctx.add_boxed(w),
109 });
110 }
111 self.child_id.into_iter().collect()
112 }
113
114 fn layout_response(
115 &self,
116 proposal: SizeProposal,
117 _ctx: &LayoutContext,
118 ) -> teksilo_core::widget::LayoutResponse {
119 self.constrain(proposal.width, proposal.height).into()
120 }
121
122 fn place_children(
123 &self,
124 bounds: Rect,
125 _proposal: SizeProposal,
126 children: &mut [WidgetPlacement],
127 _ctx: &LayoutContext,
128 ) {
129 for child in children.iter_mut() {
130 child.origin = Point::new(bounds.x, bounds.y);
131 child.size = Size::new(bounds.width, bounds.height);
132 }
133 }
134
135 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
136
137 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
138 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
143 }
144
145 fn children(&self) -> Vec<WidgetId> {
146 self.child_id.into_iter().collect()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use teksilo_core::widget_tree::WidgetTree;
154
155 #[derive(Debug)]
156 struct FixedLeaf(f32, f32);
157 impl Widget for FixedLeaf {
158 fn layout_response(
159 &self,
160 _proposal: SizeProposal,
161 _ctx: &LayoutContext,
162 ) -> teksilo_core::widget::LayoutResponse {
163 Size::new(self.0, self.1).into()
164 }
165 }
166
167 #[test]
168 fn aspect_ratio_constrains_by_width() {
169 let mut tree = WidgetTree::new();
170 let child = tree.add(FixedLeaf(100.0, 100.0));
171 let ar = tree.add(AspectRatio::new(2.0).child(child)); tree.layout(SizeProposal {
173 width: Some(200.0),
174 height: None,
175 });
176
177 let b = tree.bounds(ar);
178 assert!((b.width - 200.0).abs() < 0.01);
179 assert!((b.height - 100.0).abs() < 0.01); }
181
182 #[test]
183 fn aspect_ratio_constrains_by_height() {
184 let mut tree = WidgetTree::new();
185 let child = tree.add(FixedLeaf(100.0, 100.0));
186 let ar = tree.add(AspectRatio::new(2.0).child(child)); tree.layout(SizeProposal {
188 width: None,
189 height: Some(100.0),
190 });
191
192 let b = tree.bounds(ar);
193 assert!((b.width - 200.0).abs() < 0.01);
197 assert!((b.height - 100.0).abs() < 0.01);
198 }
199
200 #[test]
201 fn square_aspect_ratio() {
202 let mut tree = WidgetTree::new();
203 let child = tree.add(FixedLeaf(50.0, 50.0));
204 let ar = tree.add(AspectRatio::square().child(child));
205 tree.layout(SizeProposal {
206 width: None,
207 height: Some(100.0),
208 });
209
210 let b = tree.bounds(ar);
211 assert!((b.width - 100.0).abs() < 0.01);
212 assert!((b.height - 100.0).abs() < 0.01);
213 }
214
215 #[test]
216 fn width_only_proposal() {
217 let ar = AspectRatio::new(4.0 / 3.0);
218 let theme = teksilo_core::presets::intui::light();
219 let ctx = LayoutContext::for_testing(&theme);
220 let size = ar
221 .layout_response(
222 SizeProposal {
223 width: Some(400.0),
224 height: None,
225 },
226 &ctx,
227 )
228 .size;
229 assert!((size.width - 400.0).abs() < 0.01);
230 assert!((size.height - 300.0).abs() < 0.01); }
232}