tessera_ui_basic_components/
column.rs1use derive_builder::Builder;
2use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition, place_node};
3use tessera_ui_macros::tessera;
4
5use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
6
7pub use crate::column_ui;
8
9#[derive(Builder, Clone, Debug)]
11#[builder(pattern = "owned")]
12pub struct ColumnArgs {
13 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
15 pub width: DimensionValue,
16 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
18 pub height: DimensionValue,
19 #[builder(default = "MainAxisAlignment::Start")]
21 pub main_axis_alignment: MainAxisAlignment,
22 #[builder(default = "CrossAxisAlignment::Start")]
24 pub cross_axis_alignment: CrossAxisAlignment,
25}
26
27impl Default for ColumnArgs {
28 fn default() -> Self {
29 ColumnArgsBuilder::default().build().unwrap()
30 }
31}
32
33pub struct ColumnItem {
35 pub weight: Option<f32>,
37 pub child: Box<dyn FnOnce() + Send + Sync>,
39}
40
41impl ColumnItem {
42 pub fn new(child: Box<dyn FnOnce() + Send + Sync>, weight: Option<f32>) -> Self {
44 ColumnItem { weight, child }
45 }
46
47 pub fn weighted(child: Box<dyn FnOnce() + Send + Sync>, weight: f32) -> Self {
49 ColumnItem {
50 weight: Some(weight),
51 child,
52 }
53 }
54}
55
56pub trait AsColumnItem {
58 fn into_column_item(self) -> ColumnItem;
59}
60
61impl AsColumnItem for ColumnItem {
62 fn into_column_item(self) -> ColumnItem {
63 self
64 }
65}
66
67impl<F: FnOnce() + Send + Sync + 'static> AsColumnItem for F {
69 fn into_column_item(self) -> ColumnItem {
70 ColumnItem {
71 weight: None,
72 child: Box::new(self),
73 }
74 }
75}
76
77impl<F: FnOnce() + Send + Sync + 'static> AsColumnItem for (F, f32) {
79 fn into_column_item(self) -> ColumnItem {
80 ColumnItem {
81 weight: Some(self.1),
82 child: Box::new(self.0),
83 }
84 }
85}
86
87#[tessera]
89pub fn column<const N: usize>(args: ColumnArgs, children_items_input: [impl AsColumnItem; N]) {
90 let children_items: [ColumnItem; N] =
91 children_items_input.map(|item_input| item_input.into_column_item());
92
93 let mut child_closures = Vec::with_capacity(N);
94 let mut child_weights = Vec::with_capacity(N);
95
96 for child_item in children_items {
97 child_closures.push(child_item.child);
98 child_weights.push(child_item.weight);
99 }
100
101 measure(Box::new(move |input| {
102 let column_intrinsic_constraint = Constraint::new(args.width, args.height);
103 let column_effective_constraint =
105 column_intrinsic_constraint.merge(input.parent_constraint);
106
107 let mut children_sizes = vec![None; N];
108 let mut max_child_width = Px(0);
109
110 let should_use_weight_for_height = match column_effective_constraint.height {
111 DimensionValue::Fixed(_) => true,
112 DimensionValue::Fill { max: Some(_), .. } => true,
113 DimensionValue::Wrap { max: Some(_), .. } => true,
114 _ => false,
115 };
116
117 if should_use_weight_for_height {
118 let available_height_for_children =
119 column_effective_constraint.height.get_max().unwrap();
120
121 let mut weighted_children_indices = Vec::new();
122 let mut unweighted_children_indices = Vec::new();
123 let mut total_weight_sum = 0.0f32;
124
125 for (i, weight_opt) in child_weights.iter().enumerate() {
126 if let Some(w) = weight_opt {
127 if *w > 0.0 {
128 weighted_children_indices.push(i);
129 total_weight_sum += w;
130 } else {
131 unweighted_children_indices.push(i);
132 }
133 } else {
134 unweighted_children_indices.push(i);
135 }
136 }
137
138 let mut total_height_of_unweighted_children = Px(0);
139 for &child_idx in &unweighted_children_indices {
140 let child_id = input.children_ids[child_idx];
141
142 let parent_offered_constraint_for_child = Constraint::new(
145 column_effective_constraint.width,
146 DimensionValue::Wrap {
147 min: None,
148 max: column_effective_constraint.height.get_max(),
149 },
150 );
151
152 let child_result = tessera_ui::measure_node(
154 child_id,
155 &parent_offered_constraint_for_child,
156 input.tree,
157 input.metadatas,
158 input.compute_resource_manager.clone(),
159 input.gpu,
160 )?;
161
162 children_sizes[child_idx] = Some(child_result);
163 total_height_of_unweighted_children += child_result.height;
164 max_child_width = max_child_width.max(child_result.width);
165 }
166
167 let remaining_height_for_weighted_children =
168 (available_height_for_children - total_height_of_unweighted_children).max(Px(0));
169 if total_weight_sum > 0.0 {
170 for &child_idx in &weighted_children_indices {
171 let child_weight = child_weights[child_idx].unwrap_or(0.0);
172 let allocated_height_for_child =
173 Px((remaining_height_for_weighted_children.0 as f32
174 * (child_weight / total_weight_sum)) as i32);
175 let child_id = input.children_ids[child_idx];
176
177 let parent_offered_constraint_for_child = Constraint::new(
180 column_effective_constraint.width,
181 DimensionValue::Fixed(allocated_height_for_child),
182 );
183
184 let child_result = tessera_ui::measure_node(
186 child_id,
187 &parent_offered_constraint_for_child,
188 input.tree,
189 input.metadatas,
190 input.compute_resource_manager.clone(),
191 input.gpu,
192 )?;
193
194 children_sizes[child_idx] = Some(child_result);
195 max_child_width = max_child_width.max(child_result.width);
196 }
197 }
198
199 let final_column_height = available_height_for_children;
200 let final_column_width = match column_effective_constraint.width {
202 DimensionValue::Fixed(w) => w,
203 DimensionValue::Fill { max: Some(w), .. } => w,
204 DimensionValue::Wrap { min, max } => {
205 let mut w = max_child_width;
206 if let Some(min_w) = min {
207 w = w.max(min_w);
208 }
209 if let Some(max_w) = max {
210 w = w.min(max_w);
211 }
212 w
213 }
214 _ => max_child_width, };
216
217 let total_measured_children_height: Px = children_sizes
218 .iter()
219 .filter_map(|size_opt| size_opt.as_ref().map(|s| s.height))
220 .fold(Px(0), |acc, height| acc + height);
221
222 place_children_with_alignment(
223 &children_sizes,
224 input.children_ids,
225 input.metadatas,
226 final_column_width,
227 final_column_height,
228 total_measured_children_height,
229 args.main_axis_alignment,
230 args.cross_axis_alignment,
231 N,
232 );
233
234 Ok(ComputedData {
235 width: final_column_width,
236 height: final_column_height,
237 })
238 } else {
239 let mut total_children_measured_height = Px(0);
241
242 for i in 0..N {
243 let child_id = input.children_ids[i];
244
245 let parent_offered_constraint_for_child = Constraint::new(
247 column_effective_constraint.width,
248 DimensionValue::Wrap {
249 min: None,
250 max: column_effective_constraint.height.get_max(),
251 },
252 );
253
254 let child_result = tessera_ui::measure_node(
256 child_id,
257 &parent_offered_constraint_for_child,
258 input.tree,
259 input.metadatas,
260 input.compute_resource_manager.clone(),
261 input.gpu,
262 )?;
263
264 children_sizes[i] = Some(child_result);
265 total_children_measured_height += child_result.height;
266 max_child_width = max_child_width.max(child_result.width);
267 }
268
269 let final_column_height = match column_effective_constraint.height {
271 DimensionValue::Fixed(h) => h,
272 DimensionValue::Fill { min, .. } => {
273 let mut h = total_children_measured_height;
275 if let Some(min_h) = min {
276 h = h.max(min_h);
277 }
278 h
279 }
280 DimensionValue::Wrap { min, max } => {
281 let mut h = total_children_measured_height;
282 if let Some(min_h) = min {
283 h = h.max(min_h);
284 }
285 if let Some(max_h) = max {
286 h = h.min(max_h);
287 }
288 h
289 }
290 };
291
292 let final_column_width = match column_effective_constraint.width {
293 DimensionValue::Fixed(w) => w,
294 DimensionValue::Fill { min, max } => {
295 let mut w = max_child_width;
296 if let Some(min_w) = min {
297 w = w.max(min_w);
298 }
299 if let Some(max_w) = max {
300 w = w.min(max_w);
301 }
302 else {
305 w = max_child_width;
306 }
307 w
308 }
309 DimensionValue::Wrap { min, max } => {
310 let mut w = max_child_width;
311 if let Some(min_w) = min {
312 w = w.max(min_w);
313 }
314 if let Some(max_w) = max {
315 w = w.min(max_w);
316 }
317 w
318 }
319 };
320
321 place_children_with_alignment(
322 &children_sizes,
323 input.children_ids,
324 input.metadatas,
325 final_column_width,
326 final_column_height,
327 total_children_measured_height,
328 args.main_axis_alignment,
329 args.cross_axis_alignment,
330 N,
331 );
332
333 Ok(ComputedData {
334 width: final_column_width,
335 height: final_column_height,
336 })
337 }
338 }));
339
340 for child_closure in child_closures {
341 child_closure();
342 }
343}
344
345fn place_children_with_alignment(
346 children_sizes: &[Option<ComputedData>],
347 children_ids: &[tessera_ui::NodeId],
348 metadatas: &tessera_ui::ComponentNodeMetaDatas,
349 final_column_width: Px,
350 final_column_height: Px,
351 total_children_height: Px,
352 main_axis_alignment: MainAxisAlignment,
353 cross_axis_alignment: CrossAxisAlignment,
354 child_count: usize,
355) {
356 let available_space = (final_column_height - total_children_height).max(Px(0));
357
358 let (mut current_y, spacing_between_children) = match main_axis_alignment {
359 MainAxisAlignment::Start => (Px(0), Px(0)),
360 MainAxisAlignment::Center => (available_space / 2, Px(0)),
361 MainAxisAlignment::End => (available_space, Px(0)),
362 MainAxisAlignment::SpaceEvenly => {
363 if child_count > 0 {
364 let s = available_space / (child_count as i32 + 1);
365 (s, s)
366 } else {
367 (Px(0), Px(0))
368 }
369 }
370 MainAxisAlignment::SpaceBetween => {
371 if child_count > 1 {
372 (Px(0), available_space / (child_count as i32 - 1))
373 } else if child_count == 1 {
374 (available_space / 2, Px(0))
375 } else {
376 (Px(0), Px(0))
377 }
378 }
379 MainAxisAlignment::SpaceAround => {
380 if child_count > 0 {
381 let s = available_space / (child_count as i32);
382 (s / 2, s)
383 } else {
384 (Px(0), Px(0))
385 }
386 }
387 };
388
389 for (i, child_size_opt) in children_sizes.iter().enumerate() {
390 if let Some(child_actual_size) = child_size_opt {
391 let child_id = children_ids[i];
392
393 let x_offset = match cross_axis_alignment {
394 CrossAxisAlignment::Start => Px(0),
395 CrossAxisAlignment::Center => {
396 (final_column_width - child_actual_size.width).max(Px(0)) / 2
397 }
398 CrossAxisAlignment::End => {
399 (final_column_width - child_actual_size.width).max(Px(0))
400 }
401 CrossAxisAlignment::Stretch => Px(0),
402 };
403
404 place_node(child_id, PxPosition::new(x_offset, current_y), metadatas);
405 current_y += child_actual_size.height;
406 if i < child_count - 1 {
407 current_y += spacing_between_children;
408 }
409 }
410 }
411}
412
413#[macro_export]
432macro_rules! column_ui {
433 ($args:expr $(, $child:expr)* $(,)?) => {
434 {
435 use $crate::column::AsColumnItem;
436 $crate::column::column($args, [
437 $(
438 $child.into_column_item()
439 ),*
440 ])
441 }
442 };
443}