teksilo_widgets/primitives/
wrap.rs1use teksilo_canvas::{Point, Rect, Size, SizeProposal};
29use teksilo_core::accessibility::AccessNodeBuilder;
30use teksilo_core::signal::Prop;
31use teksilo_core::widget::{LayoutContext, PaintContext, PendingChild, Widget, WidgetPlacement};
32use teksilo_core::widget_id::WidgetId;
33
34#[derive(Debug)]
36pub struct Wrap {
37 spacing: Prop<f32>,
38 line_spacing: Prop<f32>,
39 child_ids: Vec<WidgetId>,
40 pending: Vec<PendingChild>,
41}
42
43impl Wrap {
44 pub fn new() -> Self {
46 Self {
47 spacing: Prop::Static(0.0),
48 line_spacing: Prop::Static(0.0),
49 child_ids: Vec::new(),
50 pending: Vec::new(),
51 }
52 }
53
54 pub fn spacing(mut self, spacing: impl Into<Prop<f32>>) -> Self {
57 self.spacing = spacing.into();
58 self
59 }
60
61 pub fn line_spacing(mut self, spacing: impl Into<Prop<f32>>) -> Self {
64 self.line_spacing = spacing.into();
65 self
66 }
67
68 pub fn add_child(mut self, id: WidgetId) -> Self {
70 self.pending.push(PendingChild::Id(id));
71 self
72 }
73
74 pub fn child(mut self, widget: impl Widget + 'static) -> Self {
76 self.pending.push(PendingChild::Deferred(Box::new(widget)));
77 self
78 }
79
80 pub fn children(mut self, iter: impl IntoIterator<Item = impl Widget + 'static>) -> Self {
82 for widget in iter {
83 self.pending.push(PendingChild::Deferred(Box::new(widget)));
84 }
85 self
86 }
87
88 pub fn child_opt(mut self, widget: Option<impl Widget + 'static>) -> Self {
90 if let Some(w) = widget {
91 self.pending.push(PendingChild::Deferred(Box::new(w)));
92 }
93 self
94 }
95
96 fn compute_layout(
99 &self,
100 available_width: f32,
101 children: &[WidgetId],
102 ctx: &LayoutContext,
103 ) -> (Vec<Size>, Vec<bool>) {
104 let child_proposal = SizeProposal::unspecified();
105 let mut sizes = Vec::with_capacity(children.len());
106 let mut line_breaks = vec![false; children.len()];
107 let mut x = 0.0_f32;
108 let spacing = self.spacing.get();
109
110 for (i, &child_id) in children.iter().enumerate() {
111 let size = ctx
112 .child_size(child_id, child_proposal)
113 .unwrap_or(Size::ZERO);
114 sizes.push(size);
115
116 if i > 0 {
117 let needed = x + spacing + size.width;
118 if needed > available_width {
119 line_breaks[i] = true;
120 x = size.width;
121 } else {
122 x += spacing + size.width;
123 }
124 } else {
125 x = size.width;
126 }
127 }
128 (sizes, line_breaks)
129 }
130}
131
132impl Default for Wrap {
133 fn default() -> Self {
134 Self::new()
135 }
136}
137
138impl Widget for Wrap {
139 fn layout_response(
140 &self,
141 proposal: SizeProposal,
142 ctx: &LayoutContext,
143 ) -> teksilo_core::widget::LayoutResponse {
144 if self.child_ids.is_empty() {
145 return (proposal.resolve(0.0, 0.0)).into();
146 }
147
148 let available_width = proposal.width.unwrap_or(f32::MAX);
149 let (sizes, line_breaks) = self.compute_layout(available_width, &self.child_ids, ctx);
150
151 let spacing = self.spacing.get();
152 let line_spacing = self.line_spacing.get();
153 let mut max_line_width = 0.0_f32;
154 let mut line_width = 0.0_f32;
155 let mut line_height = 0.0_f32;
156 let mut total_height = 0.0_f32;
157 let mut line_count = 0;
158
159 for (i, size) in sizes.iter().enumerate() {
160 if line_breaks[i] || i == 0 {
161 if i > 0 {
162 max_line_width = max_line_width.max(line_width);
163 total_height += line_height;
164 line_count += 1;
165 }
166 line_width = size.width;
167 line_height = size.height;
168 } else {
169 line_width += spacing + size.width;
170 line_height = line_height.max(size.height);
171 }
172 }
173 max_line_width = max_line_width.max(line_width);
175 total_height += line_height;
176 line_count += 1;
177
178 let total_line_gap = line_spacing * (line_count as f32 - 1.0).max(0.0);
179 Size::new(max_line_width, total_height + total_line_gap).into()
180 }
181
182 fn place_children(
183 &self,
184 bounds: Rect,
185 _proposal: SizeProposal,
186 children: &mut [WidgetPlacement],
187 ctx: &LayoutContext,
188 ) {
189 if children.is_empty() {
190 return;
191 }
192
193 let active_ids: Vec<WidgetId> = children.iter().map(|c| c.id).collect();
194 let (sizes, line_breaks) = self.compute_layout(bounds.width, &active_ids, ctx);
195 let spacing = self.spacing.get();
196 let line_spacing = self.line_spacing.get();
197 let mut x = bounds.x;
198 let mut y = bounds.y;
199 let mut line_height = 0.0_f32;
200
201 for (i, child) in children.iter_mut().enumerate() {
202 if i >= sizes.len() {
203 break;
204 }
205 if line_breaks[i] {
206 y += line_height + line_spacing;
207 x = bounds.x;
208 line_height = 0.0;
209 }
210
211 child.origin = Point::new(x, y);
212 child.size = sizes[i];
213 line_height = line_height.max(sizes[i].height);
214
215 x += sizes[i].width + spacing;
216 }
217 }
218
219 fn paint(&self, _bounds: Rect, _canvas: &mut teksilo_canvas::Canvas, _ctx: &PaintContext) {}
220
221 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
222 builder.set_role(teksilo_core::accesskit::Role::GenericContainer);
223 }
224
225 fn children(&self) -> Vec<WidgetId> {
226 self.child_ids.clone()
227 }
228
229 fn build(&mut self, ctx: &mut teksilo_core::build_context::BuildContext) -> Vec<WidgetId> {
230 let pending = std::mem::take(&mut self.pending);
231 if !pending.is_empty() {
232 self.child_ids = pending
233 .into_iter()
234 .map(|child| match child {
235 PendingChild::Id(id) => id,
236 PendingChild::Deferred(w) => ctx.add_boxed(w),
237 })
238 .collect();
239 }
240 let self_id = ctx.self_id();
241 let registry = ctx.binding_registry();
242 self.spacing.register_if_bound(
243 self_id,
244 registry,
245 teksilo_core::binding::BindingLevel::Relayout,
246 );
247 self.line_spacing.register_if_bound(
248 self_id,
249 registry,
250 teksilo_core::binding::BindingLevel::Relayout,
251 );
252 self.child_ids.clone()
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use teksilo_core::widget_tree::WidgetTree;
260
261 #[derive(Debug)]
262 struct FixedLeaf(f32, f32);
263 impl Widget for FixedLeaf {
264 fn layout_response(
265 &self,
266 _proposal: SizeProposal,
267 _ctx: &LayoutContext,
268 ) -> teksilo_core::widget::LayoutResponse {
269 Size::new(self.0, self.1).into()
270 }
271 }
272
273 #[test]
274 fn single_line_no_wrap() {
275 let mut tree = WidgetTree::new();
276 let a = tree.add(FixedLeaf(40.0, 20.0));
277 let b = tree.add(FixedLeaf(40.0, 20.0));
278 let _wrap = tree.add(Wrap::new().spacing(10.0).add_child(a).add_child(b));
279 tree.layout(SizeProposal::exact(200.0, 100.0));
280
281 assert!((tree.bounds(a).y - 0.0).abs() < 0.01);
282 assert!((tree.bounds(b).y - 0.0).abs() < 0.01);
283 assert!((tree.bounds(b).x - 50.0).abs() < 0.01); }
285
286 #[test]
287 fn wraps_to_next_line() {
288 let mut tree = WidgetTree::new();
289 let a = tree.add(FixedLeaf(80.0, 20.0));
290 let b = tree.add(FixedLeaf(80.0, 20.0));
291 let c = tree.add(FixedLeaf(80.0, 20.0));
292 let _wrap = tree.add(
293 Wrap::new()
294 .spacing(10.0)
295 .line_spacing(5.0)
296 .add_child(a)
297 .add_child(b)
298 .add_child(c),
299 );
300 tree.layout(SizeProposal::exact(200.0, 200.0));
301
302 assert!((tree.bounds(a).y - 0.0).abs() < 0.01);
304 assert!((tree.bounds(b).y - 0.0).abs() < 0.01);
305 assert!((tree.bounds(c).y - 25.0).abs() < 0.01); assert!((tree.bounds(c).x - 0.0).abs() < 0.01); }
309
310 #[test]
311 fn intrinsic_height_accounts_for_wrapping() {
312 let mut tree = WidgetTree::new();
313 let a = tree.add(FixedLeaf(60.0, 20.0));
314 let b = tree.add(FixedLeaf(60.0, 30.0));
315 let c = tree.add(FixedLeaf(60.0, 20.0));
316 let wrap = tree.add(
317 Wrap::new()
318 .spacing(10.0)
319 .line_spacing(5.0)
320 .add_child(a)
321 .add_child(b)
322 .add_child(c),
323 );
324 tree.layout(SizeProposal {
325 width: Some(140.0),
326 height: None,
327 });
328
329 let wb = tree.bounds(wrap);
333 assert!((wb.height - 55.0).abs() < 0.01);
334 }
335
336 #[test]
337 fn empty_wrap() {
338 let mut tree = WidgetTree::new();
339 let _wrap = tree.add(Wrap::new());
340 tree.layout(SizeProposal::exact(200.0, 100.0));
341 }
343
344 #[test]
345 fn dormant_child_does_not_misalign_placement() {
346 let mut tree = WidgetTree::new();
347 let a = tree.add(FixedLeaf(40.0, 20.0));
348 let b = tree.add(FixedLeaf(50.0, 25.0));
349 let c = tree.add(FixedLeaf(60.0, 30.0));
350 let _wrap = tree.add(
351 Wrap::new()
352 .spacing(10.0)
353 .add_child(a)
354 .add_child(b)
355 .add_child(c),
356 );
357 tree.layout(SizeProposal::exact(300.0, 100.0));
358
359 assert!((tree.bounds(b).x - 50.0).abs() < 0.01);
361 assert!((tree.bounds(b).width - 50.0).abs() < 0.01);
362 assert!((tree.bounds(c).x - 110.0).abs() < 0.01);
363 assert!((tree.bounds(c).width - 60.0).abs() < 0.01);
364
365 tree.set_dormant(a);
367 tree.layout(SizeProposal::exact(300.0, 100.0));
368
369 assert!((tree.bounds(b).x - 0.0).abs() < 0.01);
371 assert!((tree.bounds(b).width - 50.0).abs() < 0.01);
372 assert!((tree.bounds(b).height - 25.0).abs() < 0.01);
373 assert!((tree.bounds(c).x - 60.0).abs() < 0.01); assert!((tree.bounds(c).width - 60.0).abs() < 0.01);
376 assert!((tree.bounds(c).height - 30.0).abs() < 0.01);
377 }
378}