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(mut self, widget: impl teksilo_core::IntoTeksiChild) -> Self {
88 self.pending_child = Some(teksilo_core::IntoTeksiChild::into_pending(widget));
89 self
90 }
91 pub fn child_opt(self, widget: Option<impl teksilo_core::IntoTeksiChild>) -> Self {
98 match widget {
99 Some(w) => self.child(w),
100 None => self,
101 }
102 }
103}
104
105impl Widget for MaxSize {
106 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
107 if let Some(pending) = self.pending_child.take() {
108 self.child_id = Some(match pending {
109 PendingChild::Id(id) => id,
110 PendingChild::Deferred(w) => ctx.add_boxed(w),
111 });
112 }
113 let self_id = ctx.self_id();
114 let registry = ctx.binding_registry();
115 if let Some(ref w) = self.max_width {
116 w.register_if_bound(
117 self_id,
118 registry,
119 teksilo_core::binding::BindingLevel::Relayout,
120 );
121 }
122 if let Some(ref h) = self.max_height {
123 h.register_if_bound(
124 self_id,
125 registry,
126 teksilo_core::binding::BindingLevel::Relayout,
127 );
128 }
129 self.child_id.into_iter().collect()
130 }
131
132 fn layout_response(
133 &self,
134 proposal: SizeProposal,
135 ctx: &LayoutContext,
136 ) -> teksilo_core::widget::LayoutResponse {
137 let max_w = self.max_width.as_ref().map(|r| r.get());
138 let max_h = self.max_height.as_ref().map(|r| r.get());
139
140 let clamped_proposal = SizeProposal {
141 width: match (proposal.width, max_w) {
142 (Some(w), Some(max)) => Some(w.min(max)),
143 (None, Some(max)) => Some(max),
144 (w, None) => w,
145 },
146 height: match (proposal.height, max_h) {
147 (Some(h), Some(max)) => Some(h.min(max)),
148 (None, Some(max)) => Some(max),
149 (h, None) => h,
150 },
151 };
152
153 let child_size = self
154 .child_id
155 .and_then(|id| ctx.child_size(id, clamped_proposal))
156 .unwrap_or(Size::ZERO);
157
158 let w = match max_w {
159 Some(max) => child_size.width.min(max),
160 None => child_size.width,
161 };
162 let h = match max_h {
163 Some(max) => child_size.height.min(max),
164 None => child_size.height,
165 };
166 Size::new(w, h).into()
167 }
168
169 fn place_children(
170 &self,
171 bounds: Rect,
172 _proposal: SizeProposal,
173 children: &mut [WidgetPlacement],
174 _ctx: &LayoutContext,
175 ) {
176 for child in children.iter_mut() {
177 child.origin = bounds.origin();
178 child.size = bounds.size();
179 }
180 }
181
182 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
183
184 fn clips_children(&self) -> bool {
185 self.max_width.is_some() || self.max_height.is_some()
186 }
187
188 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
189 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
194 }
195
196 fn children(&self) -> Vec<WidgetId> {
197 self.child_id.into_iter().collect()
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use teksilo_core::signal::Signal;
205 use teksilo_core::widget_tree::WidgetTree;
206
207 #[derive(Debug)]
208 struct FixedLeaf(f32, f32);
209 impl Widget for FixedLeaf {
210 fn layout_response(
211 &self,
212 _proposal: SizeProposal,
213 _ctx: &LayoutContext,
214 ) -> teksilo_core::widget::LayoutResponse {
215 Size::new(self.0, self.1).into()
216 }
217 }
218
219 #[test]
220 fn clamps_large_child_to_maximum() {
221 let mut tree = WidgetTree::new();
222 let child = tree.add(FixedLeaf(800.0, 600.0));
223 let max = tree.add(MaxSize::new(400.0, 300.0).child(child));
224 tree.layout(SizeProposal::unspecified());
225
226 let mb = tree.bounds(max);
227 assert!((mb.width - 400.0).abs() < 0.01);
228 assert!((mb.height - 300.0).abs() < 0.01);
229 }
230
231 #[test]
232 fn small_child_is_not_clamped() {
233 let mut tree = WidgetTree::new();
234 let child = tree.add(FixedLeaf(100.0, 50.0));
235 let max = tree.add(MaxSize::new(400.0, 300.0).child(child));
236 tree.layout(SizeProposal::unspecified());
237
238 let mb = tree.bounds(max);
239 assert!((mb.width - 100.0).abs() < 0.01);
240 assert!((mb.height - 50.0).abs() < 0.01);
241 }
242
243 #[test]
244 fn max_width_only() {
245 let mut tree = WidgetTree::new();
246 let child = tree.add(FixedLeaf(800.0, 50.0));
247 let max = tree.add(MaxSize::width(400.0).child(child));
248 tree.layout(SizeProposal::unspecified());
249
250 let mb = tree.bounds(max);
251 assert!((mb.width - 400.0).abs() < 0.01);
252 assert!((mb.height - 50.0).abs() < 0.01);
253 }
254
255 #[test]
256 fn max_width_dynamic() {
257 let max_w = Signal::new(400.0_f32);
258 let mut tree = WidgetTree::new();
259 let child = tree.add(FixedLeaf(800.0, 50.0));
260 let max = tree.add(MaxSize::width(9999.0).max_width(max_w.clone()).child(child));
261 tree.layout(SizeProposal::unspecified());
262 assert!((tree.bounds(max).width - 400.0).abs() < 0.01);
263
264 max_w.set(200.0);
265 tree.layout(SizeProposal::unspecified());
266 assert!((tree.bounds(max).width - 200.0).abs() < 0.01);
267 }
268}