tessera_ui_basic_components/
row.rs1use derive_builder::Builder;
31use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, PxPosition, place_node};
32use tessera_ui_macros::tessera;
33
34use crate::alignment::{CrossAxisAlignment, MainAxisAlignment};
35
36pub use crate::row_ui;
37
38#[derive(Builder, Clone, Debug)]
40#[builder(pattern = "owned")]
41pub struct RowArgs {
42 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
44 pub width: DimensionValue,
45 #[builder(default = "DimensionValue::Wrap { min: None, max: None }")]
47 pub height: DimensionValue,
48 #[builder(default = "MainAxisAlignment::Start")]
50 pub main_axis_alignment: MainAxisAlignment,
51 #[builder(default = "CrossAxisAlignment::Start")]
53 pub cross_axis_alignment: CrossAxisAlignment,
54}
55
56impl Default for RowArgs {
57 fn default() -> Self {
58 RowArgsBuilder::default().build().unwrap()
59 }
60}
61
62pub struct RowItem {
64 pub weight: Option<f32>,
66 pub child: Box<dyn FnOnce() + Send + Sync>,
68}
69
70impl RowItem {
71 pub fn new(child: Box<dyn FnOnce() + Send + Sync>, weight: Option<f32>) -> Self {
73 RowItem { weight, child }
74 }
75
76 pub fn weighted(child: Box<dyn FnOnce() + Send + Sync>, weight: f32) -> Self {
78 RowItem {
79 weight: Some(weight),
80 child,
81 }
82 }
83}
84
85pub trait AsRowItem {
87 fn into_row_item(self) -> RowItem;
88}
89
90impl AsRowItem for RowItem {
91 fn into_row_item(self) -> RowItem {
92 self
93 }
94}
95
96impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for F {
98 fn into_row_item(self) -> RowItem {
99 RowItem {
100 weight: None,
101 child: Box::new(self),
102 }
103 }
104}
105
106impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for (F, f32) {
108 fn into_row_item(self) -> RowItem {
109 RowItem {
110 weight: Some(self.1),
111 child: Box::new(self.0),
112 }
113 }
114}
115
116#[tessera]
162pub fn row<const N: usize>(args: RowArgs, children_items_input: [impl AsRowItem; N]) {
163 let children_items: [RowItem; N] =
164 children_items_input.map(|item_input| item_input.into_row_item());
165
166 let mut child_closures = Vec::with_capacity(N);
167 let mut child_weights = Vec::with_capacity(N);
168
169 for child_item in children_items {
170 child_closures.push(child_item.child);
171 child_weights.push(child_item.weight);
172 }
173
174 measure(Box::new(move |input| {
175 let row_intrinsic_constraint = Constraint::new(args.width, args.height);
176 let row_effective_constraint = row_intrinsic_constraint.merge(input.parent_constraint);
178
179 let mut children_sizes = vec![None; N];
180 let mut max_child_height = Px(0);
181
182 let should_use_weight_for_width = match row_effective_constraint.width {
184 DimensionValue::Fixed(_) => true,
185 DimensionValue::Fill { max: Some(_), .. } => true,
186 DimensionValue::Wrap { max: Some(_), .. } => true,
187 _ => false,
188 };
189
190 if should_use_weight_for_width {
191 let available_width_for_children = row_effective_constraint.width.get_max().unwrap();
192
193 let mut weighted_children_indices = Vec::new();
194 let mut unweighted_children_indices = Vec::new();
195 let mut total_weight_sum = 0.0f32;
196
197 for (i, weight_opt) in child_weights.iter().enumerate() {
198 if let Some(w) = weight_opt {
199 if *w > 0.0 {
200 weighted_children_indices.push(i);
201 total_weight_sum += w;
202 } else {
203 unweighted_children_indices.push(i);
204 }
205 } else {
206 unweighted_children_indices.push(i);
207 }
208 }
209
210 let mut total_width_of_unweighted_children = Px(0);
211 for &child_idx in &unweighted_children_indices {
212 let child_id = input.children_ids[child_idx];
213
214 let parent_offered_constraint_for_child = Constraint::new(
216 DimensionValue::Wrap {
217 min: None,
218 max: row_effective_constraint.width.get_max(),
219 },
220 row_effective_constraint.height,
221 );
222
223 let child_result =
225 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
226
227 children_sizes[child_idx] = Some(child_result);
228 total_width_of_unweighted_children += child_result.width;
229 max_child_height = max_child_height.max(child_result.height);
230 }
231
232 let remaining_width_for_weighted_children =
233 (available_width_for_children - total_width_of_unweighted_children).max(Px(0));
234 if total_weight_sum > 0.0 {
235 for &child_idx in &weighted_children_indices {
236 let child_weight = child_weights[child_idx].unwrap_or(0.0);
237 let allocated_width_for_child =
238 Px((remaining_width_for_weighted_children.0 as f32
239 * (child_weight / total_weight_sum)) as i32);
240 let child_id = input.children_ids[child_idx];
241
242 let parent_offered_constraint_for_child = Constraint::new(
244 DimensionValue::Fixed(allocated_width_for_child),
245 row_effective_constraint.height,
246 );
247
248 let child_result =
250 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
251
252 children_sizes[child_idx] = Some(child_result);
253 max_child_height = max_child_height.max(child_result.height);
254 }
255 }
256
257 let final_row_width = available_width_for_children;
258 let final_row_height = match row_effective_constraint.height {
260 DimensionValue::Fixed(h) => h,
261 DimensionValue::Fill { max: Some(h), .. } => h,
262 DimensionValue::Wrap { min, max } => {
263 let mut h = max_child_height;
264 if let Some(min_h) = min {
265 h = h.max(min_h);
266 }
267 if let Some(max_h) = max {
268 h = h.min(max_h);
269 }
270 h
271 }
272 _ => max_child_height, };
274
275 let total_measured_children_width: Px = children_sizes
276 .iter()
277 .filter_map(|size_opt| size_opt.as_ref().map(|s| s.width))
278 .fold(Px(0), |acc, width| acc + width);
279
280 place_children_with_alignment(
281 &children_sizes,
282 input.children_ids,
283 input.metadatas,
284 final_row_width,
285 final_row_height,
286 total_measured_children_width,
287 args.main_axis_alignment,
288 args.cross_axis_alignment,
289 N,
290 );
291
292 Ok(ComputedData {
293 width: final_row_width,
294 height: final_row_height,
295 })
296 } else {
297 let mut total_children_measured_width = Px(0);
299
300 for i in 0..N {
301 let child_id = input.children_ids[i];
302
303 let parent_offered_constraint_for_child = Constraint::new(
305 match row_effective_constraint.width {
306 DimensionValue::Fixed(v) => DimensionValue::Wrap {
307 min: None,
308 max: Some(v),
309 },
310 DimensionValue::Fill { max, .. } => DimensionValue::Wrap { min: None, max },
311 DimensionValue::Wrap { max, .. } => DimensionValue::Wrap { min: None, max },
312 },
313 row_effective_constraint.height,
314 );
315
316 let child_result =
318 input.measure_child(child_id, &parent_offered_constraint_for_child)?;
319
320 children_sizes[i] = Some(child_result);
321 total_children_measured_width += child_result.width;
322 max_child_height = max_child_height.max(child_result.height);
323 }
324
325 let final_row_width = match row_effective_constraint.width {
327 DimensionValue::Fixed(w) => w,
328 DimensionValue::Fill { min, .. } => {
329 let mut w = input
332 .parent_constraint
333 .width
334 .get_max()
335 .unwrap_or(total_children_measured_width);
336 if let Some(min_w) = min {
337 w = w.max(min_w);
338 }
339 w
340 }
341 DimensionValue::Wrap { min, max } => {
342 let mut w = total_children_measured_width;
343 if let Some(min_w) = min {
344 w = w.max(min_w);
345 }
346 if let Some(max_w) = max {
347 w = w.min(max_w);
348 }
349 w
350 }
351 };
352
353 let final_row_height = match row_effective_constraint.height {
354 DimensionValue::Fixed(h) => h,
355 DimensionValue::Fill { min, max } => {
356 let mut h = max_child_height;
357 if let Some(min_h) = min {
358 h = h.max(min_h);
359 }
360 if let Some(max_h) = max {
361 h = h.min(max_h);
362 } else {
363 h = max_child_height;
364 }
365 h
366 }
367 DimensionValue::Wrap { min, max } => {
368 let mut h = max_child_height;
369 if let Some(min_h) = min {
370 h = h.max(min_h);
371 }
372 if let Some(max_h) = max {
373 h = h.min(max_h);
374 }
375 h
376 }
377 };
378
379 place_children_with_alignment(
380 &children_sizes,
381 input.children_ids,
382 input.metadatas,
383 final_row_width,
384 final_row_height,
385 total_children_measured_width,
386 args.main_axis_alignment,
387 args.cross_axis_alignment,
388 N,
389 );
390
391 Ok(ComputedData {
392 width: final_row_width,
393 height: final_row_height,
394 })
395 }
396 }));
397
398 for child_closure in child_closures {
399 child_closure();
400 }
401}
402
403fn place_children_with_alignment(
405 children_sizes: &[Option<ComputedData>],
406 children_ids: &[tessera_ui::NodeId],
407 metadatas: &tessera_ui::ComponentNodeMetaDatas,
408 final_row_width: Px,
409 final_row_height: Px,
410 total_children_width: Px,
411 main_axis_alignment: MainAxisAlignment,
412 cross_axis_alignment: CrossAxisAlignment,
413 child_count: usize,
414) {
415 let available_space = (final_row_width - total_children_width).max(Px(0));
416
417 let (mut current_x, spacing_between_children) = match main_axis_alignment {
419 MainAxisAlignment::Start => (Px(0), Px(0)),
420 MainAxisAlignment::Center => (available_space / 2, Px(0)),
421 MainAxisAlignment::End => (available_space, Px(0)),
422 MainAxisAlignment::SpaceEvenly => {
423 if child_count > 0 {
424 let s = available_space / (child_count as i32 + 1);
425 (s, s)
426 } else {
427 (Px(0), Px(0))
428 }
429 }
430 MainAxisAlignment::SpaceBetween => {
431 if child_count > 1 {
432 (Px(0), available_space / (child_count as i32 - 1))
433 } else if child_count == 1 {
434 (available_space / 2, Px(0))
435 } else {
436 (Px(0), Px(0))
437 }
438 }
439 MainAxisAlignment::SpaceAround => {
440 if child_count > 0 {
441 let s = available_space / (child_count as i32);
442 (s / 2, s)
443 } else {
444 (Px(0), Px(0))
445 }
446 }
447 };
448
449 for (i, child_size_opt) in children_sizes.iter().enumerate() {
450 if let Some(child_actual_size) = child_size_opt {
451 let child_id = children_ids[i];
452
453 let y_offset = match cross_axis_alignment {
455 CrossAxisAlignment::Start => Px(0),
456 CrossAxisAlignment::Center => {
457 (final_row_height - child_actual_size.height).max(Px(0)) / 2
458 }
459 CrossAxisAlignment::End => (final_row_height - child_actual_size.height).max(Px(0)),
460 CrossAxisAlignment::Stretch => Px(0),
461 };
462
463 place_node(child_id, PxPosition::new(current_x, y_offset), metadatas);
464 current_x += child_actual_size.width;
465 if i < child_count - 1 {
466 current_x += spacing_between_children;
467 }
468 }
469 }
470}
471
472#[macro_export]
492macro_rules! row_ui {
493 ($args:expr $(, $child:expr)* $(,)?) => {
494 {
495 use $crate::row::AsRowItem;
496 $crate::row::row($args, [
497 $(
498 $child.into_row_item()
499 ),*
500 ])
501 }
502 };
503}