tessera_ui_basic_components/
column.rs1use derive_builder::Builder;
16use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition, place_node};
17use tessera_ui_macros::tessera;
18
19use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
20
21pub use crate::column_ui;
22
23#[derive(Builder, Clone, Debug)]
25#[builder(pattern = "owned")]
26pub struct ColumnArgs {
27 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
29 pub width: DimensionValue,
30 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
32 pub height: DimensionValue,
33 #[builder(default = "MainAxisAlignment::Start")]
35 pub main_axis_alignment: MainAxisAlignment,
36 #[builder(default = "CrossAxisAlignment::Start")]
38 pub cross_axis_alignment: CrossAxisAlignment,
39}
40
41impl Default for ColumnArgs {
42 fn default() -> Self {
43 ColumnArgsBuilder::default().build().unwrap()
44 }
45}
46
47pub struct ColumnItem {
49 pub weight: Option<f32>,
51 pub child: Box<dyn FnOnce() + Send + Sync>,
53}
54
55impl ColumnItem {
56 pub fn new(child: Box<dyn FnOnce() + Send + Sync>, weight: Option<f32>) -> Self {
58 ColumnItem { weight, child }
59 }
60
61 pub fn weighted(child: Box<dyn FnOnce() + Send + Sync>, weight: f32) -> Self {
63 ColumnItem {
64 weight: Some(weight),
65 child,
66 }
67 }
68}
69
70pub trait AsColumnItem {
72 fn into_column_item(self) -> ColumnItem;
73}
74
75impl AsColumnItem for ColumnItem {
76 fn into_column_item(self) -> ColumnItem {
77 self
78 }
79}
80
81impl<F: FnOnce() + Send + Sync + 'static> AsColumnItem for F {
83 fn into_column_item(self) -> ColumnItem {
84 ColumnItem {
85 weight: None,
86 child: Box::new(self),
87 }
88 }
89}
90
91impl<F: FnOnce() + Send + Sync + 'static> AsColumnItem for (F, f32) {
93 fn into_column_item(self) -> ColumnItem {
94 ColumnItem {
95 weight: Some(self.1),
96 child: Box::new(self.0),
97 }
98 }
99}
100
101#[tessera]
139pub fn column<const N: usize>(args: ColumnArgs, children_items_input: [impl AsColumnItem; N]) {
140 let children_items: [ColumnItem; N] =
141 children_items_input.map(|item_input| item_input.into_column_item());
142
143 let mut child_closures = Vec::with_capacity(N);
144 let mut child_weights = Vec::with_capacity(N);
145
146 for child_item in children_items {
147 child_closures.push(child_item.child);
148 child_weights.push(child_item.weight);
149 }
150
151 measure(Box::new(move |input| {
152 let column_intrinsic_constraint = Constraint::new(args.width, args.height);
153 let column_effective_constraint =
155 column_intrinsic_constraint.merge(input.parent_constraint);
156
157 let mut children_sizes = vec![None; N];
158 let mut max_child_width = Px(0);
159
160 let should_use_weight_for_height = match column_effective_constraint.height {
161 DimensionValue::Fixed(_) => true,
162 DimensionValue::Fill { max: Some(_), .. } => true,
163 DimensionValue::Wrap { max: Some(_), .. } => true,
164 _ => false,
165 };
166
167 if should_use_weight_for_height {
168 let available_height_for_children =
169 column_effective_constraint.height.get_max().unwrap();
170
171 let mut weighted_children_indices = Vec::new();
172 let mut unweighted_children_indices = Vec::new();
173 let mut total_weight_sum = 0.0f32;
174
175 for (i, weight_opt) in child_weights.iter().enumerate() {
176 if let Some(w) = weight_opt {
177 if *w > 0.0 {
178 weighted_children_indices.push(i);
179 total_weight_sum += w;
180 } else {
181 unweighted_children_indices.push(i);
182 }
183 } else {
184 unweighted_children_indices.push(i);
185 }
186 }
187
188 let mut total_height_of_unweighted_children = Px(0);
189 for &child_idx in &unweighted_children_indices {
190 let Some(child_id) = input.children_ids.get(child_idx).copied() else {
191 continue;
192 };
193
194 let parent_offered_constraint_for_child = Constraint::new(
197 column_effective_constraint.width,
198 DimensionValue::Wrap {
199 min: None,
200 max: column_effective_constraint.height.get_max(),
201 },
202 );
203
204 let child_result =
206 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
207
208 children_sizes[child_idx] = Some(child_result);
209 total_height_of_unweighted_children += child_result.height;
210 max_child_width = max_child_width.max(child_result.width);
211 }
212
213 let remaining_height_for_weighted_children =
214 (available_height_for_children - total_height_of_unweighted_children).max(Px(0));
215 if total_weight_sum > 0.0 {
216 for &child_idx in &weighted_children_indices {
217 let child_weight = child_weights[child_idx].unwrap_or(0.0);
218 let allocated_height_for_child =
219 Px((remaining_height_for_weighted_children.0 as f32
220 * (child_weight / total_weight_sum)) as i32);
221 let child_id = input.children_ids[child_idx];
222
223 let parent_offered_constraint_for_child = Constraint::new(
226 column_effective_constraint.width,
227 DimensionValue::Fixed(allocated_height_for_child),
228 );
229
230 let child_result =
232 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
233
234 children_sizes[child_idx] = Some(child_result);
235 max_child_width = max_child_width.max(child_result.width);
236 }
237 }
238
239 let total_measured_children_height: Px = children_sizes
240 .iter()
241 .filter_map(|size_opt| size_opt.as_ref().map(|s| s.height))
242 .fold(Px(0), |acc, height| acc + height);
243
244 let final_column_height = match column_effective_constraint.height {
245 DimensionValue::Fixed(h) => h,
246 DimensionValue::Fill { max: Some(h), .. } => h,
247 DimensionValue::Wrap { min, max } => {
248 let mut h = total_measured_children_height;
249 if let Some(min_h) = min {
250 h = h.max(min_h);
251 }
252 if let Some(max_h) = max {
253 h = h.min(max_h);
254 }
255 h
256 }
257 _ => available_height_for_children,
258 };
259
260 let final_column_width = match column_effective_constraint.width {
262 DimensionValue::Fixed(w) => w,
263 DimensionValue::Fill { max: Some(w), .. } => w,
264 DimensionValue::Wrap { min, max } => {
265 let mut w = max_child_width;
266 if let Some(min_w) = min {
267 w = w.max(min_w);
268 }
269 if let Some(max_w) = max {
270 w = w.min(max_w);
271 }
272 w
273 }
274 _ => max_child_width, };
276
277 place_children_with_alignment(
278 &children_sizes,
279 input.children_ids,
280 input.metadatas,
281 final_column_width,
282 final_column_height,
283 total_measured_children_height,
284 args.main_axis_alignment,
285 args.cross_axis_alignment,
286 N,
287 );
288
289 Ok(ComputedData {
290 width: final_column_width,
291 height: final_column_height,
292 })
293 } else {
294 let mut total_children_measured_height = Px(0);
296
297 for i in 0..N {
298 let child_id = input.children_ids[i];
299
300 let parent_offered_constraint_for_child = Constraint::new(
302 column_effective_constraint.width,
303 DimensionValue::Wrap {
304 min: None,
305 max: column_effective_constraint.height.get_max(),
306 },
307 );
308
309 let child_result =
311 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
312
313 children_sizes[i] = Some(child_result);
314 total_children_measured_height += child_result.height;
315 max_child_width = max_child_width.max(child_result.width);
316 }
317
318 let final_column_height = match column_effective_constraint.height {
320 DimensionValue::Fixed(h) => h,
321 DimensionValue::Fill { min, .. } => {
322 let mut h = input
324 .parent_constraint
325 .height
326 .get_max()
327 .unwrap_or(total_children_measured_height);
328 if let Some(min_h) = min {
329 h = h.max(min_h);
330 }
331 h
332 }
333 DimensionValue::Wrap { min, max } => {
334 let mut h = total_children_measured_height;
335 if let Some(min_h) = min {
336 h = h.max(min_h);
337 }
338 if let Some(max_h) = max {
339 h = h.min(max_h);
340 }
341 h
342 }
343 };
344
345 let final_column_width = match column_effective_constraint.width {
346 DimensionValue::Fixed(w) => w,
347 DimensionValue::Fill { min, max } => {
348 let mut w = max_child_width;
349 if let Some(min_w) = min {
350 w = w.max(min_w);
351 }
352 if let Some(max_w) = max {
353 w = w.min(max_w);
354 }
355 else {
358 w = max_child_width;
359 }
360 w
361 }
362 DimensionValue::Wrap { min, max } => {
363 let mut w = max_child_width;
364 if let Some(min_w) = min {
365 w = w.max(min_w);
366 }
367 if let Some(max_w) = max {
368 w = w.min(max_w);
369 }
370 w
371 }
372 };
373
374 place_children_with_alignment(
375 &children_sizes,
376 input.children_ids,
377 input.metadatas,
378 final_column_width,
379 final_column_height,
380 total_children_measured_height,
381 args.main_axis_alignment,
382 args.cross_axis_alignment,
383 N,
384 );
385
386 Ok(ComputedData {
387 width: final_column_width,
388 height: final_column_height,
389 })
390 }
391 }));
392
393 for child_closure in child_closures {
394 child_closure();
395 }
396}
397
398fn place_children_with_alignment(
399 children_sizes: &[Option<ComputedData>],
400 children_ids: &[tessera_ui::NodeId],
401 metadatas: &tessera_ui::ComponentNodeMetaDatas,
402 final_column_width: Px,
403 final_column_height: Px,
404 total_children_height: Px,
405 main_axis_alignment: MainAxisAlignment,
406 cross_axis_alignment: CrossAxisAlignment,
407 child_count: usize,
408) {
409 let available_space = (final_column_height - total_children_height).max(Px(0));
410
411 let (mut current_y, spacing_between_children) = match main_axis_alignment {
412 MainAxisAlignment::Start => (Px(0), Px(0)),
413 MainAxisAlignment::Center => (available_space / 2, Px(0)),
414 MainAxisAlignment::End => (available_space, Px(0)),
415 MainAxisAlignment::SpaceEvenly => {
416 if child_count > 0 {
417 let s = available_space / (child_count as i32 + 1);
418 (s, s)
419 } else {
420 (Px(0), Px(0))
421 }
422 }
423 MainAxisAlignment::SpaceBetween => {
424 if child_count > 1 {
425 (Px(0), available_space / (child_count as i32 - 1))
426 } else if child_count == 1 {
427 (available_space / 2, Px(0))
428 } else {
429 (Px(0), Px(0))
430 }
431 }
432 MainAxisAlignment::SpaceAround => {
433 if child_count > 0 {
434 let s = available_space / (child_count as i32);
435 (s / 2, s)
436 } else {
437 (Px(0), Px(0))
438 }
439 }
440 };
441
442 for (i, child_size_opt) in children_sizes.iter().enumerate() {
443 if let Some(child_actual_size) = child_size_opt {
444 let child_id = children_ids[i];
445
446 let x_offset = match cross_axis_alignment {
447 CrossAxisAlignment::Start => Px(0),
448 CrossAxisAlignment::Center => {
449 (final_column_width - child_actual_size.width).max(Px(0)) / 2
450 }
451 CrossAxisAlignment::End => {
452 (final_column_width - child_actual_size.width).max(Px(0))
453 }
454 CrossAxisAlignment::Stretch => Px(0),
455 };
456
457 place_node(child_id, PxPosition::new(x_offset, current_y), metadatas);
458 current_y += child_actual_size.height;
459 if i < child_count - 1 {
460 current_y += spacing_between_children;
461 }
462 }
463 }
464}
465
466#[macro_export]
485macro_rules! column_ui {
486 ($args:expr $(, $child:expr)* $(,)?) => {
487 {
488 use $crate::column::AsColumnItem;
489 $crate::column::column($args, [
490 $(
491 $child.into_column_item()
492 ),*
493 ])
494 }
495 };
496}