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