1use derive_builder::Builder;
7use tessera_ui::{
8 ComputedData, Constraint, DimensionValue, MeasureInput, MeasurementError, NodeId, Px,
9 PxPosition, tessera,
10};
11
12use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
13
14#[derive(Builder, Clone, Debug)]
16#[builder(pattern = "owned")]
17pub struct ColumnArgs {
18 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
20 pub width: DimensionValue,
21 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
23 pub height: DimensionValue,
24 #[builder(default = "MainAxisAlignment::Start")]
26 pub main_axis_alignment: MainAxisAlignment,
27 #[builder(default = "CrossAxisAlignment::Start")]
29 pub cross_axis_alignment: CrossAxisAlignment,
30}
31
32impl Default for ColumnArgs {
33 fn default() -> Self {
34 ColumnArgsBuilder::default()
35 .build()
36 .expect("builder construction failed")
37 }
38}
39
40pub struct ColumnScope<'a> {
42 child_closures: &'a mut Vec<Box<dyn FnOnce() + Send + Sync>>,
43 child_weights: &'a mut Vec<Option<f32>>,
44}
45
46impl<'a> ColumnScope<'a> {
47 pub fn child<F>(&mut self, child_closure: F)
49 where
50 F: FnOnce() + Send + Sync + 'static,
51 {
52 self.child_closures.push(Box::new(child_closure));
53 self.child_weights.push(None);
54 }
55
56 pub fn child_weighted<F>(&mut self, child_closure: F, weight: f32)
58 where
59 F: FnOnce() + Send + Sync + 'static,
60 {
61 self.child_closures.push(Box::new(child_closure));
62 self.child_weights.push(Some(weight));
63 }
64}
65
66#[tessera]
93pub fn column<F>(args: ColumnArgs, scope_config: F)
94where
95 F: FnOnce(&mut ColumnScope),
96{
97 let mut child_closures: Vec<Box<dyn FnOnce() + Send + Sync>> = Vec::new();
98 let mut child_weights: Vec<Option<f32>> = Vec::new();
99
100 {
101 let mut scope = ColumnScope {
102 child_closures: &mut child_closures,
103 child_weights: &mut child_weights,
104 };
105 scope_config(&mut scope);
106 }
107
108 let n = child_closures.len();
109
110 measure(Box::new(
111 move |input| -> Result<ComputedData, MeasurementError> {
112 assert_eq!(
113 input.children_ids.len(),
114 n,
115 "Mismatch between children defined in scope and runtime children count"
116 );
117
118 let column_intrinsic_constraint = Constraint::new(args.width, args.height);
119 let column_effective_constraint =
120 column_intrinsic_constraint.merge(input.parent_constraint);
121
122 let mut children_sizes = vec![None; n];
123 let mut max_child_width = Px(0);
124
125 let has_weighted_children = child_weights.iter().any(|w| w.unwrap_or(0.0) > 0.0);
126 let should_use_weight_for_height = has_weighted_children
127 && matches!(
128 column_effective_constraint.height,
129 DimensionValue::Fixed(_)
130 | DimensionValue::Fill { max: Some(_), .. }
131 | DimensionValue::Wrap { max: Some(_), .. }
132 );
133
134 let (final_column_width, final_column_height, total_measured_children_height) =
135 if should_use_weight_for_height {
136 measure_weighted_column(
137 input,
138 &args,
139 &child_weights,
140 &column_effective_constraint,
141 &mut children_sizes,
142 &mut max_child_width,
143 )?
144 } else {
145 measure_unweighted_column(
146 input,
147 &args,
148 &column_effective_constraint,
149 &mut children_sizes,
150 &mut max_child_width,
151 )?
152 };
153
154 place_children_with_alignment(&PlaceChildrenArgs {
155 children_sizes: &children_sizes,
156 children_ids: input.children_ids,
157 input,
158 final_column_width,
159 final_column_height,
160 total_children_height: total_measured_children_height,
161 main_axis_alignment: args.main_axis_alignment,
162 cross_axis_alignment: args.cross_axis_alignment,
163 child_count: n,
164 });
165
166 Ok(ComputedData {
167 width: final_column_width,
168 height: final_column_height,
169 })
170 },
171 ));
172
173 for child_closure in child_closures {
174 child_closure();
175 }
176}
177
178struct PlaceChildrenArgs<'a> {
180 children_sizes: &'a [Option<ComputedData>],
181 children_ids: &'a [NodeId],
182 input: &'a MeasureInput<'a>,
183 final_column_width: Px,
184 final_column_height: Px,
185 total_children_height: Px,
186 main_axis_alignment: MainAxisAlignment,
187 cross_axis_alignment: CrossAxisAlignment,
188 child_count: usize,
189}
190
191fn classify_children(child_weights: &[Option<f32>]) -> (Vec<usize>, Vec<usize>, f32) {
193 let mut weighted_indices = Vec::new();
194 let mut unweighted_indices = Vec::new();
195 let mut total_weight = 0.0;
196 for (i, weight_opt) in child_weights.iter().enumerate() {
197 if let Some(w) = weight_opt {
198 if *w > 0.0 {
199 weighted_indices.push(i);
200 total_weight += w;
201 } else {
202 unweighted_indices.push(i);
203 }
204 } else {
205 unweighted_indices.push(i);
206 }
207 }
208 (weighted_indices, unweighted_indices, total_weight)
209}
210
211fn measure_unweighted_children_for_column(
214 input: &MeasureInput,
215 indices: &[usize],
216 children_sizes: &mut [Option<ComputedData>],
217 max_child_width: &mut Px,
218 column_effective_constraint: &Constraint,
219) -> Result<Px, MeasurementError> {
220 let mut total = Px(0);
221
222 let parent_offered_constraint_for_child = Constraint::new(
223 column_effective_constraint.width,
224 DimensionValue::Wrap {
225 min: None,
226 max: column_effective_constraint.height.get_max(),
227 },
228 );
229
230 let children_to_measure: Vec<_> = indices
231 .iter()
232 .map(|&child_idx| {
233 (
234 input.children_ids[child_idx],
235 parent_offered_constraint_for_child,
236 )
237 })
238 .collect();
239
240 let children_results = input.measure_children(children_to_measure)?;
241
242 for &child_idx in indices {
243 let child_id = input.children_ids[child_idx];
244 if let Some(child_result) = children_results.get(&child_id) {
245 children_sizes[child_idx] = Some(*child_result);
246 total += child_result.height;
247 *max_child_width = (*max_child_width).max(child_result.width);
248 }
249 }
250
251 Ok(total)
252}
253
254struct WeightedColumnMeasureContext<'a> {
256 input: &'a MeasureInput<'a>,
257 children_sizes: &'a mut [Option<ComputedData>],
258 max_child_width: &'a mut Px,
259 column_effective_constraint: &'a Constraint,
260 child_weights: &'a [Option<f32>],
261}
262
263fn measure_weighted_children_for_column(
264 ctx: WeightedColumnMeasureContext<'_>,
265 weighted_indices: &[usize],
266 remaining_height: Px,
267 total_weight: f32,
268) -> Result<(), MeasurementError> {
269 if total_weight <= 0.0 {
270 return Ok(());
271 }
272
273 let children_to_measure: Vec<_> = weighted_indices
274 .iter()
275 .map(|&child_idx| {
276 let child_weight = ctx.child_weights[child_idx].unwrap_or(0.0);
277 let allocated_height =
278 Px((remaining_height.0 as f32 * (child_weight / total_weight)) as i32);
279 let child_id = ctx.input.children_ids[child_idx];
280 let parent_offered_constraint_for_child = Constraint::new(
281 ctx.column_effective_constraint.width,
282 DimensionValue::Fixed(allocated_height),
283 );
284 (child_id, parent_offered_constraint_for_child)
285 })
286 .collect();
287
288 let children_results = ctx.input.measure_children(children_to_measure)?;
289
290 for &child_idx in weighted_indices {
291 let child_id = ctx.input.children_ids[child_idx];
292 if let Some(child_result) = children_results.get(&child_id) {
293 ctx.children_sizes[child_idx] = Some(*child_result);
294 *ctx.max_child_width = (*ctx.max_child_width).max(child_result.width);
295 }
296 }
297
298 Ok(())
299}
300
301fn calculate_final_column_height(
302 column_effective_constraint: &Constraint,
303 measured_children_height: Px,
304) -> Px {
305 match column_effective_constraint.height {
306 DimensionValue::Fixed(h) => h,
307 DimensionValue::Fill { min, max } => {
308 if let Some(max) = max {
309 if let Some(min) = min {
310 max.max(min)
311 } else {
312 max
313 }
314 } else {
315 panic!(
316 "Seems that you are trying to use Fill without max in a non-infinite parent constraint. This is not supported. Parent constraint: {column_effective_constraint:?}"
317 );
318 }
319 }
320 DimensionValue::Wrap { min, max } => {
321 let mut h = measured_children_height;
322 if let Some(min_h) = min {
323 h = h.max(min_h);
324 }
325 if let Some(max_h) = max {
326 h = h.min(max_h);
327 }
328 h
329 }
330 }
331}
332
333fn calculate_final_column_width(
334 column_effective_constraint: &Constraint,
335 max_child_width: Px,
336 parent_constraint: &Constraint,
337) -> Px {
338 match column_effective_constraint.width {
339 DimensionValue::Fixed(w) => w,
340 DimensionValue::Fill { min, max } => {
341 if let Some(max) = max {
342 if let Some(min) = min {
343 max.max(min)
344 } else {
345 max
346 }
347 } else {
348 panic!(
349 "Seems that you are trying to use Fill without max in a non-infinite parent constraint. This is not supported. Parent constraint: {parent_constraint:?}"
350 );
351 }
352 }
353 DimensionValue::Wrap { min, max } => {
354 let mut w = max_child_width;
355 if let Some(min_w) = min {
356 w = w.max(min_w);
357 }
358 if let Some(max_w) = max {
359 w = w.min(max_w);
360 }
361 w
362 }
363 }
364}
365
366fn measure_weighted_column(
369 input: &MeasureInput,
370 _args: &ColumnArgs,
371 child_weights: &[Option<f32>],
372 column_effective_constraint: &Constraint,
373 children_sizes: &mut [Option<ComputedData>],
374 max_child_width: &mut Px,
375) -> Result<(Px, Px, Px), MeasurementError> {
376 let available_height_for_children = column_effective_constraint
377 .height
378 .get_max()
379 .expect("Column height Fill expected with finite max constraint");
380
381 let (weighted_children_indices, unweighted_children_indices, total_weight_sum) =
382 classify_children(child_weights);
383
384 let total_height_of_unweighted_children = measure_unweighted_children_for_column(
385 input,
386 &unweighted_children_indices,
387 children_sizes,
388 max_child_width,
389 column_effective_constraint,
390 )?;
391
392 let remaining_height_for_weighted_children =
393 (available_height_for_children - total_height_of_unweighted_children).max(Px(0));
394
395 measure_weighted_children_for_column(
396 WeightedColumnMeasureContext {
397 input,
398 children_sizes,
399 max_child_width,
400 column_effective_constraint,
401 child_weights,
402 },
403 &weighted_children_indices,
404 remaining_height_for_weighted_children,
405 total_weight_sum,
406 )?;
407
408 let total_measured_children_height: Px = children_sizes
409 .iter()
410 .filter_map(|s| s.as_ref().map(|s| s.height))
411 .fold(Px(0), |acc, h| acc + h);
412
413 let final_column_height =
414 calculate_final_column_height(column_effective_constraint, total_measured_children_height);
415 let final_column_width = calculate_final_column_width(
416 column_effective_constraint,
417 *max_child_width,
418 input.parent_constraint,
419 );
420
421 Ok((
422 final_column_width,
423 final_column_height,
424 total_measured_children_height,
425 ))
426}
427
428fn measure_unweighted_column(
429 input: &MeasureInput,
430 _args: &ColumnArgs,
431 column_effective_constraint: &Constraint,
432 children_sizes: &mut [Option<ComputedData>],
433 max_child_width: &mut Px,
434) -> Result<(Px, Px, Px), MeasurementError> {
435 let n = children_sizes.len();
436 let mut total_children_measured_height = Px(0);
437
438 let parent_offered_constraint_for_child = Constraint::new(
439 column_effective_constraint.width,
440 DimensionValue::Wrap {
441 min: None,
442 max: column_effective_constraint.height.get_max(),
443 },
444 );
445
446 let children_to_measure: Vec<_> = input
447 .children_ids
448 .iter()
449 .map(|&child_id| (child_id, parent_offered_constraint_for_child))
450 .collect();
451
452 let children_results = input.measure_children(children_to_measure)?;
453
454 for (i, &child_id) in input.children_ids.iter().enumerate().take(n) {
455 if let Some(child_result) = children_results.get(&child_id) {
456 children_sizes[i] = Some(*child_result);
457 total_children_measured_height += child_result.height;
458 *max_child_width = (*max_child_width).max(child_result.width);
459 }
460 }
461
462 let final_column_height =
463 calculate_final_column_height(column_effective_constraint, total_children_measured_height);
464 let final_column_width = calculate_final_column_width(
465 column_effective_constraint,
466 *max_child_width,
467 input.parent_constraint,
468 );
469 Ok((
470 final_column_width,
471 final_column_height,
472 total_children_measured_height,
473 ))
474}
475
476fn place_children_with_alignment(args: &PlaceChildrenArgs) {
485 let (mut current_y, spacing_between_children) = calculate_main_axis_layout_for_column(
486 args.final_column_height,
487 args.total_children_height,
488 args.main_axis_alignment,
489 args.child_count,
490 );
491
492 for (i, child_size_opt) in args.children_sizes.iter().enumerate() {
493 if let Some(child_actual_size) = child_size_opt {
494 let child_id = args.children_ids[i];
495 let x_offset = calculate_cross_axis_offset_for_column(
496 child_actual_size,
497 args.final_column_width,
498 args.cross_axis_alignment,
499 );
500 args.input
501 .place_child(child_id, PxPosition::new(x_offset, current_y));
502 current_y += child_actual_size.height;
503 if i < args.child_count - 1 {
504 current_y += spacing_between_children;
505 }
506 }
507 }
508}
509
510fn calculate_main_axis_layout_for_column(
511 final_column_height: Px,
512 total_children_height: Px,
513 main_axis_alignment: MainAxisAlignment,
514 child_count: usize,
515) -> (Px, Px) {
516 let available_space = (final_column_height - total_children_height).max(Px(0));
517 match main_axis_alignment {
518 MainAxisAlignment::Start => (Px(0), Px(0)),
519 MainAxisAlignment::Center => (available_space / 2, Px(0)),
520 MainAxisAlignment::End => (available_space, Px(0)),
521 MainAxisAlignment::SpaceEvenly => {
522 if child_count > 0 {
523 let s = available_space / (child_count as i32 + 1);
524 (s, s)
525 } else {
526 (Px(0), Px(0))
527 }
528 }
529 MainAxisAlignment::SpaceBetween => {
530 if child_count > 1 {
531 (Px(0), available_space / (child_count as i32 - 1))
532 } else if child_count == 1 {
533 (available_space / 2, Px(0))
534 } else {
535 (Px(0), Px(0))
536 }
537 }
538 MainAxisAlignment::SpaceAround => {
539 if child_count > 0 {
540 let s = available_space / (child_count as i32);
541 (s / 2, s)
542 } else {
543 (Px(0), Px(0))
544 }
545 }
546 }
547}
548
549fn calculate_cross_axis_offset_for_column(
550 child_actual_size: &ComputedData,
551 final_column_width: Px,
552 cross_axis_alignment: CrossAxisAlignment,
553) -> Px {
554 match cross_axis_alignment {
555 CrossAxisAlignment::Start => Px(0),
556 CrossAxisAlignment::Center => (final_column_width - child_actual_size.width).max(Px(0)) / 2,
557 CrossAxisAlignment::End => (final_column_width - child_actual_size.width).max(Px(0)),
558 CrossAxisAlignment::Stretch => Px(0),
559 }
560}