Skip to main content

tessera_components/
segmented_buttons.rs

1//! Material Design segmented buttons for compact selections.
2//!
3//! ## Usage
4//!
5//! Switch between views or filters with a connected control.
6
7use tessera_ui::{
8    AccessibilityActionHandler, AccessibilityNode, AxisConstraint, Callback, Color, ComputedData,
9    Constraint, Dp, FocusDirection, FocusManager, FocusState, FocusTraversalPolicy, LayoutResult,
10    MeasurementError, Modifier, Px, PxPosition, RenderSlotWith, SemanticsModifierNode,
11    accesskit::{Orientation, Role, Toggled},
12    layout::{MeasureScope, layout},
13    modifier::{FocusModifierExt as _, ModifierCapabilityExt as _},
14    provide_context, remember, tessera, use_context,
15};
16
17use crate::{
18    alignment::{Alignment, CrossAxisAlignment},
19    icon::icon as icon_component,
20    layout_flex::cross_axis_child_constraint,
21    material_icons::filled,
22    modifier::{InteractionState, ModifierExt as _, Padding},
23    painter::Painter,
24    row::row,
25    shape_def::{RoundedCorner, Shape},
26    spacer::spacer,
27    surface::{SurfaceStyle, surface},
28    text::text,
29    theme::{MaterialAlpha, MaterialColorScheme, MaterialTheme, TextStyle},
30};
31
32const SEGMENTED_ICON_SPACING: Dp = Dp(8.0);
33
34#[cfg(all(test, not(target_family = "wasm")))]
35mod tests;
36
37/// Selection and keyboard activation behavior for a segmented choice.
38#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub enum SegmentedButtonSelectionMode {
40    /// Moving focus selects one choice in the group.
41    Single,
42    /// Each choice toggles independently on activation.
43    #[default]
44    Multiple,
45}
46
47struct SegmentedGroupSemantics {
48    single_choice: bool,
49}
50
51impl SemanticsModifierNode for SegmentedGroupSemantics {
52    fn apply(
53        &self,
54        node: &mut AccessibilityNode,
55        _handler: &mut Option<AccessibilityActionHandler>,
56    ) {
57        node.role = Some(if self.single_choice {
58            Role::RadioGroup
59        } else {
60            Role::Group
61        });
62        node.orientation = Some(Orientation::Horizontal);
63        node.merge_descendants = true;
64    }
65}
66
67/// Color values for segmented buttons in different states.
68#[derive(Clone, PartialEq, Copy, Debug)]
69pub struct SegmentedButtonColors {
70    /// Container color when enabled and active.
71    pub active_container_color: Color,
72    /// Content color when enabled and active.
73    pub active_content_color: Color,
74    /// Border color when enabled and active.
75    pub active_border_color: Color,
76    /// Container color when enabled and inactive.
77    pub inactive_container_color: Color,
78    /// Content color when enabled and inactive.
79    pub inactive_content_color: Color,
80    /// Border color when enabled and inactive.
81    pub inactive_border_color: Color,
82    /// Container color when disabled and active.
83    pub disabled_active_container_color: Color,
84    /// Content color when disabled and active.
85    pub disabled_active_content_color: Color,
86    /// Border color when disabled and active.
87    pub disabled_active_border_color: Color,
88    /// Container color when disabled and inactive.
89    pub disabled_inactive_container_color: Color,
90    /// Content color when disabled and inactive.
91    pub disabled_inactive_content_color: Color,
92    /// Border color when disabled and inactive.
93    pub disabled_inactive_border_color: Color,
94}
95
96impl SegmentedButtonColors {
97    fn container_color(self, enabled: bool, active: bool) -> Color {
98        match (enabled, active) {
99            (true, true) => self.active_container_color,
100            (true, false) => self.inactive_container_color,
101            (false, true) => self.disabled_active_container_color,
102            (false, false) => self.disabled_inactive_container_color,
103        }
104    }
105
106    fn content_color(self, enabled: bool, active: bool) -> Color {
107        match (enabled, active) {
108            (true, true) => self.active_content_color,
109            (true, false) => self.inactive_content_color,
110            (false, true) => self.disabled_active_content_color,
111            (false, false) => self.disabled_inactive_content_color,
112        }
113    }
114
115    fn border_color(self, enabled: bool, active: bool) -> Color {
116        match (enabled, active) {
117            (true, true) => self.active_border_color,
118            (true, false) => self.inactive_border_color,
119            (false, true) => self.disabled_active_border_color,
120            (false, false) => self.disabled_inactive_border_color,
121        }
122    }
123}
124
125/// Defaults for segmented buttons.
126pub struct SegmentedButtonDefaults;
127
128impl SegmentedButtonDefaults {
129    /// Minimum height for segmented buttons.
130    pub const HEIGHT: Dp = Dp(40.0);
131    /// Default border width.
132    pub const BORDER_WIDTH: Dp = Dp(1.0);
133    /// Default icon size.
134    pub const ICON_SIZE: Dp = Dp(18.0);
135    /// Default content padding.
136    pub const CONTENT_PADDING: Padding = Padding::symmetric(Dp(12.0), Dp(8.0));
137
138    /// Default shape for segmented buttons.
139    pub fn shape() -> Shape {
140        Shape::CAPSULE
141    }
142
143    /// Build the item shape for the button at `index` with `count` items.
144    pub fn item_shape(index: usize, count: usize, base_shape: Shape) -> Shape {
145        if count <= 1 {
146            return base_shape;
147        }
148
149        let Shape::RoundedRectangle {
150            top_left,
151            top_right,
152            bottom_right,
153            bottom_left,
154        } = base_shape
155        else {
156            return base_shape;
157        };
158
159        let is_first = index == 0;
160        let is_last = index + 1 == count;
161
162        Shape::RoundedRectangle {
163            top_left: if is_first {
164                top_left
165            } else {
166                RoundedCorner::ZERO
167            },
168            top_right: if is_last {
169                top_right
170            } else {
171                RoundedCorner::ZERO
172            },
173            bottom_right: if is_last {
174                bottom_right
175            } else {
176                RoundedCorner::ZERO
177            },
178            bottom_left: if is_first {
179                bottom_left
180            } else {
181                RoundedCorner::ZERO
182            },
183        }
184    }
185
186    /// Default colors derived from the current theme.
187    pub fn colors(scheme: &MaterialColorScheme) -> SegmentedButtonColors {
188        let scheme = scheme.clone();
189        let disabled_content = scheme
190            .on_surface
191            .with_alpha(MaterialAlpha::DISABLED_CONTENT);
192        let disabled_border = scheme.outline.with_alpha(MaterialAlpha::DISABLED_CONTAINER);
193
194        SegmentedButtonColors {
195            active_container_color: scheme.secondary_container,
196            active_content_color: scheme.on_secondary_container,
197            active_border_color: scheme.outline,
198            inactive_container_color: Color::TRANSPARENT,
199            inactive_content_color: scheme.on_surface,
200            inactive_border_color: scheme.outline,
201            disabled_active_container_color: scheme.secondary_container,
202            disabled_active_content_color: disabled_content,
203            disabled_active_border_color: disabled_border,
204            disabled_inactive_container_color: Color::TRANSPARENT,
205            disabled_inactive_content_color: disabled_content,
206            disabled_inactive_border_color: disabled_border,
207        }
208    }
209}
210
211/// # segmented_button
212///
213/// Render a segmented button item with optional icon and label.
214///
215/// ## Usage
216///
217/// Use inside segmented button rows to represent selectable options.
218/// Prefer connected [`crate::button_groups::button_groups`] for Expressive
219/// interfaces.
220///
221/// ## Parameters
222///
223/// - `selected` — whether the segment is selected.
224/// - `enabled` — whether the segment is enabled.
225/// - `label` — label shown inside the segment.
226/// - `icon` — optional icon displayed before the label.
227/// - `selected_icon` — optional replacement for the selected checkmark.
228/// - `show_selected_icon` — whether selection replaces the leading icon with a
229///   checkmark; defaults to true.
230/// - `modifier` — layout and behavior modifiers applied to the segment.
231/// - `shape` — shape of the segment.
232/// - `colors` — optional color overrides for the segment.
233/// - `border_width` — border width around the segment.
234/// - `min_height` — minimum visual height; defaults to 40dp while retaining the
235///   minimum interactive target.
236/// - `content_padding` — internal padding for the segment content.
237/// - `on_click` — optional click callback.
238/// - `accessibility_label` — optional accessibility label.
239/// - `accessibility_description` — optional accessibility description.
240///
241/// ## Examples
242///
243/// ```
244/// use tessera_components::segmented_buttons::{
245///     SegmentedButtonDefaults, segmented_button, single_choice_segmented_button_row,
246/// };
247/// use tessera_components::theme::{MaterialTheme, material_theme};
248/// use tessera_ui::{remember, tessera};
249///
250/// #[tessera]
251/// fn demo() {
252///     material_theme()
253///         .theme(|| MaterialTheme::default())
254///         .child(|| {
255///             let selected = remember(|| 0usize);
256///             single_choice_segmented_button_row().content(move |mode| {
257///                 segmented_button().selection_mode(mode)
258///                     .label("List")
259///                     .selected(selected.get() == 0)
260///                     .shape(SegmentedButtonDefaults::item_shape(
261///                         0,
262///                         2,
263///                         SegmentedButtonDefaults::shape(),
264///                     ))
265///                     .on_click(|| {});
266///                 segmented_button().selection_mode(mode)
267///                     .label("Grid")
268///                     .selected(selected.get() == 1)
269///                     .shape(SegmentedButtonDefaults::item_shape(
270///                         1,
271///                         2,
272///                         SegmentedButtonDefaults::shape(),
273///                     ))
274///                     .on_click(|| {});
275///             });
276///         });
277/// }
278///
279/// # tessera_ui::testing::layout_test(demo).run();
280/// ```
281#[tessera]
282pub fn segmented_button(
283    selection_mode: Option<SegmentedButtonSelectionMode>,
284    selected: Option<bool>,
285    enabled: Option<bool>,
286    #[prop(into)] label: Option<String>,
287    #[prop(into)] icon: Option<Painter>,
288    #[prop(into)] selected_icon: Option<Painter>,
289    show_selected_icon: Option<bool>,
290    modifier: Option<Modifier>,
291    shape: Option<Shape>,
292    colors: Option<SegmentedButtonColors>,
293    border_width: Option<Dp>,
294    min_height: Option<Dp>,
295    content_padding: Option<Padding>,
296    on_click: Option<Callback>,
297    #[prop(into)] accessibility_label: Option<String>,
298    #[prop(into)] accessibility_description: Option<String>,
299) {
300    let selected = selected.unwrap_or(false);
301    let label = label.unwrap_or_default();
302    let enabled = enabled.unwrap_or(true);
303    let modifier = modifier.unwrap_or_default();
304    let shape = shape.unwrap_or_else(SegmentedButtonDefaults::shape);
305    let single_choice = selection_mode.unwrap_or_default() == SegmentedButtonSelectionMode::Single;
306    let icon = if selected && show_selected_icon.unwrap_or(true) {
307        Some(selected_icon.unwrap_or_else(|| filled::CHECK_SVG.into()))
308    } else {
309        icon
310    };
311    let interaction_state = remember(InteractionState::new);
312    if (!enabled || on_click.is_none()) && interaction_state.get() != InteractionState::default() {
313        interaction_state.set(InteractionState::default());
314    }
315    let theme = use_context::<MaterialTheme>()
316        .expect("MaterialTheme must be provided")
317        .get();
318    let typography = theme.typography;
319    let colors = colors.unwrap_or_else(|| SegmentedButtonDefaults::colors(&theme.color_scheme));
320    let container_color = colors.container_color(enabled, selected);
321    let content_color = colors.content_color(enabled, selected);
322    let border_color = colors.border_color(enabled, selected);
323    let border_width = border_width.unwrap_or(SegmentedButtonDefaults::BORDER_WIDTH);
324    let content_padding = content_padding.unwrap_or(SegmentedButtonDefaults::CONTENT_PADDING);
325
326    let surface_style = if border_width.0 > 0.0 {
327        SurfaceStyle::FilledOutlined {
328            fill_color: container_color,
329            border_color,
330            border_width,
331        }
332    } else {
333        SurfaceStyle::Filled {
334            color: container_color,
335        }
336    };
337
338    let mut modifier = modifier.size_in(
339        Some(Dp(58.0)),
340        None,
341        Some(min_height.unwrap_or(SegmentedButtonDefaults::HEIGHT)),
342        None,
343    );
344    if enabled
345        && single_choice
346        && let Some(on_click) = on_click
347    {
348        let is_selected = selected;
349        modifier = modifier
350            .focusable()
351            .preferred_tab_stop(selected)
352            .on_focus_event(move |focus_state: FocusState| {
353                let directional = matches!(
354                    FocusManager::current().focus_change_direction(),
355                    Some(FocusDirection::Left | FocusDirection::Right)
356                );
357                if focus_state.is_focused()
358                    && directional
359                    && !is_selected
360                    && !interaction_state.with(|state| state.is_pressed())
361                {
362                    on_click.call();
363                }
364            });
365    }
366
367    let has_on_click = on_click.is_some();
368    let accessibility_label =
369        accessibility_label.or_else(|| (!label.is_empty()).then(|| label.clone()));
370
371    surface()
372        .modifier(modifier)
373        .style(surface_style)
374        .shape(shape)
375        .content_alignment(Alignment::Center)
376        .content_color(content_color)
377        .enabled(enabled)
378        .interaction_state(interaction_state)
379        .ripple_color(content_color)
380        .on_click_optional(on_click)
381        .accessibility_role(if single_choice {
382            Role::RadioButton
383        } else {
384            Role::CheckBox
385        })
386        .accessibility_toggled(if selected {
387            Toggled::True
388        } else {
389            Toggled::False
390        })
391        .accessibility_focusable(has_on_click && enabled)
392        .accessibility_label_optional(accessibility_label)
393        .accessibility_description_optional(accessibility_description)
394        .child(move || {
395            let leading_icon = icon.clone();
396            let padding = content_padding;
397            let label_outer = label.clone();
398            provide_context(
399                || typography.label_large,
400                move || {
401                    let inherited =
402                        use_context::<TextStyle>().expect("segmented button text style");
403                    if inherited.get() != typography.label_large {
404                        inherited.set(typography.label_large);
405                    }
406                    let leading_icon = leading_icon.clone();
407                    let label_outer = label_outer.clone();
408                    layout()
409                        .modifier(Modifier::new().padding(padding))
410                        .child(move || {
411                            let leading_icon = leading_icon.clone();
412                            let label = label_outer.clone();
413                            let label_offset = if leading_icon.is_none() {
414                                Dp::from(
415                                    Px::ZERO
416                                        - (Px::from(SegmentedButtonDefaults::ICON_SIZE)
417                                            + Px::from(SEGMENTED_ICON_SPACING))
418                                            / 2,
419                                )
420                            } else {
421                                Dp(0.0)
422                            };
423                            row()
424                                .cross_axis_alignment(CrossAxisAlignment::Center)
425                                .children(move || {
426                                    if let Some(icon_content) = leading_icon.clone() {
427                                        icon_component()
428                                            .painter(icon_content)
429                                            .size(SegmentedButtonDefaults::ICON_SIZE);
430                                    } else {
431                                        spacer().modifier(
432                                            Modifier::new()
433                                                .width(SegmentedButtonDefaults::ICON_SIZE),
434                                        );
435                                    }
436
437                                    if !label.is_empty() {
438                                        spacer().modifier(
439                                            Modifier::new().width(SEGMENTED_ICON_SPACING),
440                                        );
441                                        {
442                                            text(label.clone()).modifier(
443                                                Modifier::new().offset(label_offset, Dp(0.0)),
444                                            );
445                                        };
446                                    }
447                                });
448                        });
449                },
450            );
451        });
452}
453
454/// # single_choice_segmented_button_row
455///
456/// Lay out segmented buttons for single-selection groups.
457///
458/// ## Usage
459///
460/// Use when only one option should be active at a time.
461///
462/// ## Parameters
463///
464/// - `modifier` — modifiers applied to the row container.
465/// - `overlap` — overlap amount between adjacent segments.
466/// - `cross_axis_alignment` — cross-axis alignment for segments.
467/// - `equal_width` — whether segments should share equal width.
468/// - `content` — row content slot.
469///
470/// ## Examples
471///
472/// ```
473/// use tessera_components::segmented_buttons::{
474///     SegmentedButtonDefaults, segmented_button, single_choice_segmented_button_row,
475/// };
476/// use tessera_components::theme::{MaterialTheme, material_theme};
477/// use tessera_ui::{remember, tessera};
478///
479/// #[tessera]
480/// fn demo() {
481///     material_theme()
482///         .theme(|| MaterialTheme::default())
483///         .child(|| {
484///             let selected = remember(|| 0usize);
485///             single_choice_segmented_button_row().content(move |mode| {
486///                 segmented_button().selection_mode(mode)
487///                     .label("Day")
488///                     .selected(selected.get() == 0)
489///                     .shape(SegmentedButtonDefaults::item_shape(
490///                         0,
491///                         2,
492///                         SegmentedButtonDefaults::shape(),
493///                     ))
494///                     .on_click(|| {});
495///                 segmented_button().selection_mode(mode)
496///                     .label("Week")
497///                     .selected(selected.get() == 1)
498///                     .shape(SegmentedButtonDefaults::item_shape(
499///                         1,
500///                         2,
501///                         SegmentedButtonDefaults::shape(),
502///                     ))
503///                     .on_click(|| {});
504///             });
505///         });
506/// }
507///
508/// # tessera_ui::testing::layout_test(demo).run();
509/// ```
510#[tessera]
511pub fn single_choice_segmented_button_row(
512    modifier: Option<Modifier>,
513    overlap: Option<Dp>,
514    cross_axis_alignment: Option<CrossAxisAlignment>,
515    equal_width: Option<bool>,
516    content: Option<RenderSlotWith<SegmentedButtonSelectionMode>>,
517) {
518    let (modifier, layout_policy, content) = segmented_button_row_parts(
519        modifier,
520        overlap,
521        cross_axis_alignment,
522        equal_width,
523        content,
524    );
525    let modifier = modifier
526        .focus_group()
527        .push_semantics(SegmentedGroupSemantics {
528            single_choice: true,
529        })
530        .focus_traversal_policy(
531            FocusTraversalPolicy::horizontal()
532                .wrap(true)
533                .single_tab_stop(true),
534        );
535    layout().modifier(modifier).child(move || {
536        let content = content;
537        layout().policy(layout_policy.clone()).child(move || {
538            let content = content;
539            if let Some(content) = content {
540                content.render(SegmentedButtonSelectionMode::Single);
541            }
542        });
543    });
544}
545
546/// # multi_choice_segmented_button_row
547///
548/// Lay out segmented buttons for multi-selection groups.
549///
550/// ## Usage
551///
552/// Use when multiple options can be active simultaneously.
553///
554/// ## Parameters
555///
556/// - `modifier` — modifiers applied to the row container.
557/// - `overlap` — overlap amount between adjacent segments.
558/// - `cross_axis_alignment` — cross-axis alignment for segments.
559/// - `equal_width` — whether segments should share equal width.
560/// - `content` — row content slot.
561///
562/// ## Examples
563///
564/// ```
565/// use tessera_components::segmented_buttons::{
566///     SegmentedButtonDefaults, multi_choice_segmented_button_row, segmented_button,
567/// };
568/// use tessera_components::theme::{MaterialTheme, material_theme};
569/// use tessera_ui::{remember, tessera};
570///
571/// #[tessera]
572/// fn demo() {
573///     material_theme()
574///         .theme(|| MaterialTheme::default())
575///         .child(|| {
576///             let selected = remember(|| [true, false]);
577///             multi_choice_segmented_button_row().content(move |mode| {
578///                 segmented_button().selection_mode(mode)
579///                     .label("Email")
580///                     .selected(selected.get()[0])
581///                     .shape(SegmentedButtonDefaults::item_shape(
582///                         0,
583///                         2,
584///                         SegmentedButtonDefaults::shape(),
585///                     ))
586///                     .on_click(|| {});
587///                 segmented_button().selection_mode(mode)
588///                     .label("Push")
589///                     .selected(selected.get()[1])
590///                     .shape(SegmentedButtonDefaults::item_shape(
591///                         1,
592///                         2,
593///                         SegmentedButtonDefaults::shape(),
594///                     ))
595///                     .on_click(|| {});
596///             });
597///         });
598/// }
599///
600/// # tessera_ui::testing::layout_test(demo).run();
601/// ```
602#[tessera]
603pub fn multi_choice_segmented_button_row(
604    modifier: Option<Modifier>,
605    overlap: Option<Dp>,
606    cross_axis_alignment: Option<CrossAxisAlignment>,
607    equal_width: Option<bool>,
608    content: Option<RenderSlotWith<SegmentedButtonSelectionMode>>,
609) {
610    let (modifier, layout_policy, content) = segmented_button_row_parts(
611        modifier,
612        overlap,
613        cross_axis_alignment,
614        equal_width,
615        content,
616    );
617    let modifier = modifier
618        .focus_group()
619        .push_semantics(SegmentedGroupSemantics {
620            single_choice: false,
621        })
622        .focus_traversal_policy(FocusTraversalPolicy::horizontal().wrap(true));
623    layout().modifier(modifier).child(move || {
624        let content = content;
625        layout().policy(layout_policy.clone()).child(move || {
626            let content = content;
627            if let Some(content) = content {
628                content.render(SegmentedButtonSelectionMode::Multiple);
629            }
630        });
631    });
632}
633
634fn segmented_button_row_parts(
635    modifier: Option<Modifier>,
636    overlap: Option<Dp>,
637    cross_axis_alignment: Option<CrossAxisAlignment>,
638    equal_width: Option<bool>,
639    content: Option<RenderSlotWith<SegmentedButtonSelectionMode>>,
640) -> (
641    Modifier,
642    SegmentedButtonRowLayout,
643    Option<RenderSlotWith<SegmentedButtonSelectionMode>>,
644) {
645    let modifier = modifier.unwrap_or_default();
646    let overlap = Px::from(overlap.unwrap_or(SegmentedButtonDefaults::BORDER_WIDTH)).max(Px::ZERO);
647    let cross_axis_alignment = cross_axis_alignment.unwrap_or(CrossAxisAlignment::Center);
648    let equal_width = equal_width.unwrap_or(true);
649    let modifier = modifier.size_in(None, None, Some(SegmentedButtonDefaults::HEIGHT), None);
650    let layout_policy = SegmentedButtonRowLayout {
651        overlap,
652        cross_axis_alignment,
653        equal_width,
654    };
655    (modifier, layout_policy, content)
656}
657
658#[derive(Clone, PartialEq)]
659struct SegmentedButtonRowLayout {
660    overlap: Px,
661    cross_axis_alignment: CrossAxisAlignment,
662    equal_width: bool,
663}
664
665impl tessera_ui::LayoutModifierNode for SegmentedButtonRowLayout {
666    fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
667        let mut result = LayoutResult::default();
668        let children = input.children();
669        if children.is_empty() {
670            return Ok(result.with_size(ComputedData::ZERO));
671        }
672
673        let row_constraint = *input.parent_constraint().as_ref();
674        let child_constraint = Constraint::new(
675            AxisConstraint::new(Px::ZERO, row_constraint.width.resolve_max()),
676            cross_axis_child_constraint(row_constraint.height, self.cross_axis_alignment),
677        );
678
679        let mut children_sizes = vec![None; children.len()];
680        let mut total_width = Px::ZERO;
681        let mut max_width = Px::ZERO;
682        let mut max_height = Px::ZERO;
683        for (index, &child) in children.iter().enumerate() {
684            let child_result = child.measure(input, &child_constraint)?;
685            children_sizes[index] = Some(child_result.size());
686            total_width += child_result.width;
687            max_width = max_width.max(child_result.width);
688            max_height = max_height.max(child_result.height);
689        }
690
691        if self.equal_width && max_width > Px::ZERO {
692            let count = children.len() as i32;
693            let available = row_constraint.width.resolve_max();
694            let overlap = self
695                .overlap
696                .min(max_width)
697                .min(available.unwrap_or(max_width));
698            let target = row_constraint
699                .width
700                .clamp(max_width * count - overlap * (count - 1));
701            let budget = target + overlap * (count - 1);
702            let (width, remainder) = (budget / count, budget.0 % count);
703            total_width = Px::ZERO;
704            max_height = Px::ZERO;
705
706            for (index, &child) in children.iter().enumerate() {
707                let child_width = width + Px(i32::from((index as i32) < remainder));
708                let equal_constraint = Constraint::new(
709                    child_width,
710                    cross_axis_child_constraint(row_constraint.height, self.cross_axis_alignment),
711                );
712                let child_result = child.measure(input, &equal_constraint)?;
713                children_sizes[index] = Some(child_result.size());
714                total_width += child_result.width;
715                max_height = max_height.max(child_result.height);
716            }
717        }
718
719        let min_child_width = children_sizes
720            .iter()
721            .filter_map(|size| size.map(|size| size.width))
722            .min()
723            .unwrap_or(Px::ZERO);
724        let overlap = if children.len() > 1 {
725            self.overlap.min(min_child_width)
726        } else {
727            Px::ZERO
728        };
729        let total_width_with_overlap = if children.len() > 1 {
730            total_width - overlap * (children.len() as i32 - 1)
731        } else {
732            total_width
733        };
734        let total_width_with_overlap = total_width_with_overlap.max(Px::ZERO);
735        let final_width = row_constraint.width.clamp(total_width_with_overlap);
736        let final_height = row_constraint.height.clamp(max_height);
737
738        let mut current_x = Px::ZERO;
739        for (index, child_size_opt) in children_sizes.iter().enumerate() {
740            if let Some(child_size) = child_size_opt {
741                let child = children[index];
742                let y_offset = crate::layout_flex::cross_axis_offset(
743                    child_size.height,
744                    final_height,
745                    self.cross_axis_alignment,
746                );
747                result.place_child(child, PxPosition::from_f32_xy(current_x.0 as f32, y_offset));
748                current_x += child_size.width;
749                if index < children.len() - 1 {
750                    current_x -= overlap;
751                }
752            }
753        }
754
755        Ok(result.with_size(ComputedData {
756            width: final_width,
757            height: final_height,
758        }))
759    }
760}