teksilo_widgets/primitives/
max_size.rs1use teksilo_canvas::{Rect, Size, SizeProposal};
27use teksilo_core::accessibility::AccessNodeBuilder;
28use teksilo_core::signal::Prop;
29use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
30use teksilo_core::widget_id::WidgetId;
31
32#[derive(Debug)]
36pub struct MaxSize {
37 child_id: Option<WidgetId>,
38 pending_child: Option<PendingChild>,
39 max_width: Option<Prop<f32>>,
40 max_height: Option<Prop<f32>>,
41}
42
43impl MaxSize {
44 pub fn new(width: f32, height: f32) -> Self {
46 Self {
47 child_id: None,
48 pending_child: None,
49 max_width: Some(Prop::Static(width)),
50 max_height: Some(Prop::Static(height)),
51 }
52 }
53
54 pub fn width(width: f32) -> Self {
56 Self {
57 child_id: None,
58 pending_child: None,
59 max_width: Some(Prop::Static(width)),
60 max_height: None,
61 }
62 }
63
64 pub fn height(height: f32) -> Self {
66 Self {
67 child_id: None,
68 pending_child: None,
69 max_width: None,
70 max_height: Some(Prop::Static(height)),
71 }
72 }
73
74 pub fn max_width(mut self, state: impl Into<Prop<f32>>) -> Self {
76 self.max_width = Some(state.into());
77 self
78 }
79
80 pub fn max_height(mut self, state: impl Into<Prop<f32>>) -> Self {
82 self.max_height = Some(state.into());
83 self
84 }
85
86 pub fn child_id(mut self, id: WidgetId) -> Self {
88 self.pending_child = Some(PendingChild::Id(id));
89 self
90 }
91
92 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
94 self.pending_child = Some(PendingChild::Deferred(Box::new(widget)));
95 self
96 }
97}
98
99impl Widget for MaxSize {
100 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
101 if let Some(pending) = self.pending_child.take() {
102 self.child_id = Some(match pending {
103 PendingChild::Id(id) => id,
104 PendingChild::Deferred(w) => ctx.add_boxed(w),
105 });
106 }
107 let self_id = ctx.self_id();
108 let registry = ctx.binding_registry();
109 if let Some(ref w) = self.max_width {
110 w.register_if_bound(
111 self_id,
112 registry,
113 teksilo_core::binding::BindingLevel::Relayout,
114 );
115 }
116 if let Some(ref h) = self.max_height {
117 h.register_if_bound(
118 self_id,
119 registry,
120 teksilo_core::binding::BindingLevel::Relayout,
121 );
122 }
123 self.child_id.into_iter().collect()
124 }
125
126 fn layout_response(
127 &self,
128 proposal: SizeProposal,
129 ctx: &LayoutContext,
130 ) -> teksilo_core::widget::LayoutResponse {
131 let max_w = self.max_width.as_ref().map(|r| r.get());
132 let max_h = self.max_height.as_ref().map(|r| r.get());
133
134 let clamped_proposal = SizeProposal {
135 width: match (proposal.width, max_w) {
136 (Some(w), Some(max)) => Some(w.min(max)),
137 (None, Some(max)) => Some(max),
138 (w, None) => w,
139 },
140 height: match (proposal.height, max_h) {
141 (Some(h), Some(max)) => Some(h.min(max)),
142 (None, Some(max)) => Some(max),
143 (h, None) => h,
144 },
145 };
146
147 let child_size = self
148 .child_id
149 .and_then(|id| ctx.child_size(id, clamped_proposal))
150 .unwrap_or(Size::ZERO);
151
152 let w = match max_w {
153 Some(max) => child_size.width.min(max),
154 None => child_size.width,
155 };
156 let h = match max_h {
157 Some(max) => child_size.height.min(max),
158 None => child_size.height,
159 };
160 Size::new(w, h).into()
161 }
162
163 fn place_children(
164 &self,
165 bounds: Rect,
166 _proposal: SizeProposal,
167 children: &mut [WidgetPlacement],
168 _ctx: &LayoutContext,
169 ) {
170 for child in children.iter_mut() {
171 child.origin = bounds.origin();
172 child.size = bounds.size();
173 }
174 }
175
176 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
177
178 fn clips_children(&self) -> bool {
179 self.max_width.is_some() || self.max_height.is_some()
180 }
181
182 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
183 builder.set_hidden();
184 }
185
186 fn children(&self) -> Vec<WidgetId> {
187 self.child_id.into_iter().collect()
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use teksilo_core::signal::Signal;
195 use teksilo_core::widget_tree::WidgetTree;
196
197 #[derive(Debug)]
198 struct FixedLeaf(f32, f32);
199 impl Widget for FixedLeaf {
200 fn layout_response(
201 &self,
202 _proposal: SizeProposal,
203 _ctx: &LayoutContext,
204 ) -> teksilo_core::widget::LayoutResponse {
205 Size::new(self.0, self.1).into()
206 }
207 }
208
209 #[test]
210 fn clamps_large_child_to_maximum() {
211 let mut tree = WidgetTree::new();
212 let child = tree.add(FixedLeaf(800.0, 600.0));
213 let max = tree.add(MaxSize::new(400.0, 300.0).child_id(child));
214 tree.layout(SizeProposal::unspecified());
215
216 let mb = tree.bounds(max);
217 assert!((mb.width - 400.0).abs() < 0.01);
218 assert!((mb.height - 300.0).abs() < 0.01);
219 }
220
221 #[test]
222 fn small_child_is_not_clamped() {
223 let mut tree = WidgetTree::new();
224 let child = tree.add(FixedLeaf(100.0, 50.0));
225 let max = tree.add(MaxSize::new(400.0, 300.0).child_id(child));
226 tree.layout(SizeProposal::unspecified());
227
228 let mb = tree.bounds(max);
229 assert!((mb.width - 100.0).abs() < 0.01);
230 assert!((mb.height - 50.0).abs() < 0.01);
231 }
232
233 #[test]
234 fn max_width_only() {
235 let mut tree = WidgetTree::new();
236 let child = tree.add(FixedLeaf(800.0, 50.0));
237 let max = tree.add(MaxSize::width(400.0).child_id(child));
238 tree.layout(SizeProposal::unspecified());
239
240 let mb = tree.bounds(max);
241 assert!((mb.width - 400.0).abs() < 0.01);
242 assert!((mb.height - 50.0).abs() < 0.01);
243 }
244
245 #[test]
246 fn max_width_dynamic() {
247 let max_w = Signal::new(400.0_f32);
248 let mut tree = WidgetTree::new();
249 let child = tree.add(FixedLeaf(800.0, 50.0));
250 let max = tree.add(
251 MaxSize::width(9999.0)
252 .max_width(max_w.clone())
253 .child_id(child),
254 );
255 tree.layout(SizeProposal::unspecified());
256 assert!((tree.bounds(max).width - 400.0).abs() < 0.01);
257
258 max_w.set(200.0);
259 tree.layout(SizeProposal::unspecified());
260 assert!((tree.bounds(max).width - 200.0).abs() < 0.01);
261 }
262}