tessera_ui_basic_components/
row.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::row_ui;
8
9#[derive(Builder, Clone, Debug)]
11#[builder(pattern = "owned")]
12pub struct RowArgs {
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 RowArgs {
28 fn default() -> Self {
29 RowArgsBuilder::default().build().unwrap()
30 }
31}
32
33pub struct RowItem {
35 pub weight: Option<f32>,
37 pub child: Box<dyn FnOnce() + Send + Sync>,
39}
40
41impl RowItem {
42 pub fn new(child: Box<dyn FnOnce() + Send + Sync>, weight: Option<f32>) -> Self {
44 RowItem { weight, child }
45 }
46
47 pub fn weighted(child: Box<dyn FnOnce() + Send + Sync>, weight: f32) -> Self {
49 RowItem {
50 weight: Some(weight),
51 child,
52 }
53 }
54}
55
56pub trait AsRowItem {
58 fn into_row_item(self) -> RowItem;
59}
60
61impl AsRowItem for RowItem {
62 fn into_row_item(self) -> RowItem {
63 self
64 }
65}
66
67impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for F {
69 fn into_row_item(self) -> RowItem {
70 RowItem {
71 weight: None,
72 child: Box::new(self),
73 }
74 }
75}
76
77impl<F: FnOnce() + Send + Sync + 'static> AsRowItem for (F, f32) {
79 fn into_row_item(self) -> RowItem {
80 RowItem {
81 weight: Some(self.1),
82 child: Box::new(self.0),
83 }
84 }
85}
86
87#[tessera]
89pub fn row<const N: usize>(args: RowArgs, children_items_input: [impl AsRowItem; N]) {
90 let children_items: [RowItem; N] =
91 children_items_input.map(|item_input| item_input.into_row_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 row_intrinsic_constraint = Constraint::new(args.width, args.height);
103 let row_effective_constraint = row_intrinsic_constraint.merge(input.parent_constraint);
105
106 let mut children_sizes = vec![None; N];
107 let mut max_child_height = Px(0);
108
109 let should_use_weight_for_width = match row_effective_constraint.width {
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_width {
118 let available_width_for_children = row_effective_constraint.width.get_max().unwrap();
119
120 let mut weighted_children_indices = Vec::new();
121 let mut unweighted_children_indices = Vec::new();
122 let mut total_weight_sum = 0.0f32;
123
124 for (i, weight_opt) in child_weights.iter().enumerate() {
125 if let Some(w) = weight_opt {
126 if *w > 0.0 {
127 weighted_children_indices.push(i);
128 total_weight_sum += w;
129 } else {
130 unweighted_children_indices.push(i);
131 }
132 } else {
133 unweighted_children_indices.push(i);
134 }
135 }
136
137 let mut total_width_of_unweighted_children = Px(0);
138 for &child_idx in &unweighted_children_indices {
139 let Some(child_id) = input.children_ids.get(child_idx).copied() else {
140 continue;
141 };
142
143 let parent_offered_constraint_for_child = Constraint::new(
145 DimensionValue::Wrap {
146 min: None,
147 max: row_effective_constraint.width.get_max(),
148 },
149 row_effective_constraint.height,
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_width_of_unweighted_children += child_result.width;
164 max_child_height = max_child_height.max(child_result.height);
165 }
166
167 let remaining_width_for_weighted_children =
168 (available_width_for_children - total_width_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_width_for_child =
173 Px((remaining_width_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(
179 DimensionValue::Fixed(allocated_width_for_child),
180 row_effective_constraint.height,
181 );
182
183 let child_result = tessera_ui::measure_node(
185 child_id,
186 &parent_offered_constraint_for_child,
187 input.tree,
188 input.metadatas,
189 input.compute_resource_manager.clone(),
190 input.gpu,
191 )?;
192
193 children_sizes[child_idx] = Some(child_result);
194 max_child_height = max_child_height.max(child_result.height);
195 }
196 }
197
198 let final_row_width = available_width_for_children;
199 let final_row_height = match row_effective_constraint.height {
201 DimensionValue::Fixed(h) => h,
202 DimensionValue::Fill { max: Some(h), .. } => h,
203 DimensionValue::Wrap { min, max } => {
204 let mut h = max_child_height;
205 if let Some(min_h) = min {
206 h = h.max(min_h);
207 }
208 if let Some(max_h) = max {
209 h = h.min(max_h);
210 }
211 h
212 }
213 _ => max_child_height, };
215
216 let total_measured_children_width: Px = children_sizes
217 .iter()
218 .filter_map(|size_opt| size_opt.as_ref().map(|s| s.width))
219 .fold(Px(0), |acc, width| acc + width);
220
221 place_children_with_alignment(
222 &children_sizes,
223 input.children_ids,
224 input.metadatas,
225 final_row_width,
226 final_row_height,
227 total_measured_children_width,
228 args.main_axis_alignment,
229 args.cross_axis_alignment,
230 N,
231 );
232
233 Ok(ComputedData {
234 width: final_row_width,
235 height: final_row_height,
236 })
237 } else {
238 let mut total_children_measured_width = Px(0);
240
241 for i in 0..N {
242 let child_id = input.children_ids[i];
243
244 let parent_offered_constraint_for_child = Constraint::new(
246 match row_effective_constraint.width {
247 DimensionValue::Fixed(v) => DimensionValue::Wrap {
248 min: None,
249 max: Some(v),
250 },
251 DimensionValue::Fill { max, .. } => DimensionValue::Wrap { min: None, max },
252 DimensionValue::Wrap { max, .. } => DimensionValue::Wrap { min: None, max },
253 },
254 row_effective_constraint.height,
255 );
256
257 let child_result = tessera_ui::measure_node(
259 child_id,
260 &parent_offered_constraint_for_child,
261 input.tree,
262 input.metadatas,
263 input.compute_resource_manager.clone(),
264 input.gpu,
265 )?;
266
267 children_sizes[i] = Some(child_result);
268 total_children_measured_width += child_result.width;
269 max_child_height = max_child_height.max(child_result.height);
270 }
271
272 let final_row_width = match row_effective_constraint.width {
274 DimensionValue::Fixed(w) => w,
275 DimensionValue::Fill { min, .. } => {
276 let mut w = total_children_measured_width;
278 if let Some(min_w) = min {
279 w = w.max(min_w);
280 }
281 w
282 }
283 DimensionValue::Wrap { min, max } => {
284 let mut w = total_children_measured_width;
285 if let Some(min_w) = min {
286 w = w.max(min_w);
287 }
288 if let Some(max_w) = max {
289 w = w.min(max_w);
290 }
291 w
292 }
293 };
294
295 let final_row_height = match row_effective_constraint.height {
296 DimensionValue::Fixed(h) => h,
297 DimensionValue::Fill { min, max } => {
298 let mut h = max_child_height;
299 if let Some(min_h) = min {
300 h = h.max(min_h);
301 }
302 if let Some(max_h) = max {
303 h = h.min(max_h);
304 } else {
305 h = max_child_height;
306 }
307 h
308 }
309 DimensionValue::Wrap { min, max } => {
310 let mut h = max_child_height;
311 if let Some(min_h) = min {
312 h = h.max(min_h);
313 }
314 if let Some(max_h) = max {
315 h = h.min(max_h);
316 }
317 h
318 }
319 };
320
321 place_children_with_alignment(
322 &children_sizes,
323 input.children_ids,
324 input.metadatas,
325 final_row_width,
326 final_row_height,
327 total_children_measured_width,
328 args.main_axis_alignment,
329 args.cross_axis_alignment,
330 N,
331 );
332
333 Ok(ComputedData {
334 width: final_row_width,
335 height: final_row_height,
336 })
337 }
338 }));
339
340 for child_closure in child_closures {
341 child_closure();
342 }
343}
344
345fn place_children_with_alignment(
347 children_sizes: &[Option<ComputedData>],
348 children_ids: &[tessera_ui::NodeId],
349 metadatas: &tessera_ui::ComponentNodeMetaDatas,
350 final_row_width: Px,
351 final_row_height: Px,
352 total_children_width: Px,
353 main_axis_alignment: MainAxisAlignment,
354 cross_axis_alignment: CrossAxisAlignment,
355 child_count: usize,
356) {
357 let available_space = (final_row_width - total_children_width).max(Px(0));
358
359 let (mut current_x, spacing_between_children) = match main_axis_alignment {
361 MainAxisAlignment::Start => (Px(0), Px(0)),
362 MainAxisAlignment::Center => (available_space / 2, Px(0)),
363 MainAxisAlignment::End => (available_space, Px(0)),
364 MainAxisAlignment::SpaceEvenly => {
365 if child_count > 0 {
366 let s = available_space / (child_count as i32 + 1);
367 (s, s)
368 } else {
369 (Px(0), Px(0))
370 }
371 }
372 MainAxisAlignment::SpaceBetween => {
373 if child_count > 1 {
374 (Px(0), available_space / (child_count as i32 - 1))
375 } else if child_count == 1 {
376 (available_space / 2, Px(0))
377 } else {
378 (Px(0), Px(0))
379 }
380 }
381 MainAxisAlignment::SpaceAround => {
382 if child_count > 0 {
383 let s = available_space / (child_count as i32);
384 (s / 2, s)
385 } else {
386 (Px(0), Px(0))
387 }
388 }
389 };
390
391 for (i, child_size_opt) in children_sizes.iter().enumerate() {
392 if let Some(child_actual_size) = child_size_opt {
393 let child_id = children_ids[i];
394
395 let y_offset = match cross_axis_alignment {
397 CrossAxisAlignment::Start => Px(0),
398 CrossAxisAlignment::Center => {
399 (final_row_height - child_actual_size.height).max(Px(0)) / 2
400 }
401 CrossAxisAlignment::End => (final_row_height - child_actual_size.height).max(Px(0)),
402 CrossAxisAlignment::Stretch => Px(0),
403 };
404
405 place_node(child_id, PxPosition::new(current_x, y_offset), metadatas);
406 current_x += child_actual_size.width;
407 if i < child_count - 1 {
408 current_x += spacing_between_children;
409 }
410 }
411 }
412}
413
414#[macro_export]
434macro_rules! row_ui {
435 ($args:expr $(, $child:expr)* $(,)?) => {
436 {
437 use $crate::row::AsRowItem;
438 $crate::row::row($args, [
439 $(
440 $child.into_row_item()
441 ),*
442 ])
443 }
444 };
445}