rust_widgets 2.7.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Box layout manager — arranges items in a single row or column.
use super::{Layout, LayoutConstraints, LayoutContext, Orientation, SizePolicy};
use crate::compat::{Any, Vec};
use crate::core::{ObjectId, Rect};
#[derive(Debug)]
struct BoxLayoutItem {
    widget_id: Option<ObjectId>,
    stretch: u32,
    constraints: LayoutConstraints,
    policy: SizePolicy,
}
/// Linear layout that arranges items in one direction.
#[derive(Debug)]
pub struct BoxLayout {
    orientation: Orientation,
    spacing: u32,
    margin: u32,
    items: Vec<BoxLayoutItem>,
}
impl BoxLayout {
    /// Create a box layout with orientation, spacing and margin.
    pub fn new(orientation: Orientation, spacing: u32, margin: u32) -> Self {
        Self { orientation, spacing, margin, items: Vec::new() }
    }
    /// Returns layout orientation.
    pub fn orientation(&self) -> Orientation {
        self.orientation
    }
    /// Returns inter-item spacing.
    pub fn spacing(&self) -> u32 {
        self.spacing
    }
    /// Updates inter-item spacing.
    pub fn set_spacing(&mut self, spacing: u32) {
        self.spacing = spacing;
    }
    /// Returns outer margin.
    pub fn margin(&self) -> u32 {
        self.margin
    }
    /// Updates outer margin.
    pub fn set_margin(&mut self, margin: u32) {
        self.margin = margin;
    }
    /// Returns number of managed items (widgets + spacers).
    pub fn item_count(&self) -> usize {
        self.items.len()
    }
    /// Adds an empty spacer item with the provided stretch factor.
    pub fn add_spacer(&mut self, stretch: u32) {
        self.items.push(BoxLayoutItem {
            widget_id: None,
            stretch: stretch.max(1),
            constraints: LayoutConstraints::new(0, None),
            policy: SizePolicy::Expanding,
        });
    }
    /// Sets size constraints for an existing widget item.
    pub fn set_constraints(&mut self, widget_id: ObjectId, constraints: LayoutConstraints) {
        if let Some(item) = self.items.iter_mut().find(|item| item.widget_id == Some(widget_id)) {
            item.constraints = constraints;
        }
    }
    /// Sets size policy for an existing widget item.
    pub fn set_size_policy(&mut self, widget_id: ObjectId, policy: SizePolicy) {
        if let Some(item) = self.items.iter_mut().find(|item| item.widget_id == Some(widget_id)) {
            item.policy = policy;
        }
    }
    /// Splits `primary` pixels across the items, honouring each item's constraints.
    ///
    /// # The two invariants this must not break
    ///
    /// 1. `sum(assigned) <= primary` — children that together need more than the parent
    ///    must not be placed partly outside it. Overflow here is visible as a control
    ///    painted over its neighbour, and it is reachable from the public
    ///    `set_constraints` API, so it cannot be left to the caller to avoid.
    /// 2. Each item's `min` is honoured *when the space can satisfy all of them*. When it
    ///    cannot — two 80px minima in a 100px row — no assignment satisfies both, so the
    ///    shortfall is distributed proportionally to the minima instead of being applied
    ///    inconsistently (the previous single-pass shrink loop reduced some items below
    ///    their minimum while leaving others at it, so the result depended on item order).
    fn allocate_major_lengths(&self, primary: u32) -> Vec<u32> {
        if self.items.is_empty() {
            return Vec::new();
        }
        let total_stretch: u32 = self.items.iter().map(|item| item.stretch).sum::<u32>().max(1);
        let mut assigned = Vec::with_capacity(self.items.len());
        for item in &self.items {
            let mut major = if item.policy == SizePolicy::Fixed {
                item.constraints.max.unwrap_or(item.constraints.min)
            } else {
                primary.saturating_mul(item.stretch) / total_stretch
            };
            major = major.max(item.constraints.min);
            if let Some(max) = item.constraints.max {
                major = major.min(max.max(item.constraints.min));
            }
            assigned.push(major);
        }

        // `min` is a hard floor only while the parent can pay for every floor. When the
        // floors alone exceed `primary`, they are scaled down proportionally: every item
        // then falls short by the same fraction, which is the only order-independent
        // answer, and invariant 1 is restored before the grow/shrink passes run.
        let total_min: u32 = self.items.iter().map(|item| item.constraints.min).sum();
        if total_min > primary {
            let budget = primary;
            let mut scaled = Vec::with_capacity(self.items.len());
            let mut consumed = 0u32;
            for (index, item) in self.items.iter().enumerate() {
                // The last item takes the remainder rather than its own rounded share, so
                // the pieces always add up to exactly `budget`.
                let share = if index + 1 == self.items.len() {
                    budget.saturating_sub(consumed)
                } else {
                    (budget.saturating_mul(item.constraints.min) / total_min.max(1))
                        .min(budget.saturating_sub(consumed))
                };
                consumed = consumed.saturating_add(share);
                scaled.push(share);
            }
            return scaled;
        }

        let mut total_assigned: u32 = assigned.iter().sum();
        while total_assigned < primary {
            let mut grew = false;
            for (index, item) in self.items.iter().enumerate() {
                if total_assigned >= primary {
                    break;
                }
                let max_allowed =
                    item.constraints.max.unwrap_or(u32::MAX).max(item.constraints.min);
                if assigned[index] < max_allowed {
                    assigned[index] = assigned[index].saturating_add(1);
                    total_assigned = total_assigned.saturating_add(1);
                    grew = true;
                }
            }
            if !grew {
                break;
            }
        }
        while total_assigned > primary {
            let mut shrank = false;
            for (index, _item) in self.items.iter().enumerate().rev() {
                if total_assigned <= primary {
                    break;
                }
                let min_allowed = self.items[index].constraints.min;
                if assigned[index] > min_allowed {
                    assigned[index] = assigned[index].saturating_sub(1);
                    total_assigned = total_assigned.saturating_sub(1);
                    shrank = true;
                }
            }
            if !shrank {
                // Nothing is above its minimum and the total is still too large, which
                // can now only happen if a `max` below the summed minima was pinned above
                // its own minimum. Reducing from the largest allocation keeps the sum
                // inside `primary` instead of returning an overflowing vector.
                let Some((largest_index, _)) = assigned
                    .iter()
                    .enumerate()
                    .filter(|(_, value)| **value > 0)
                    .max_by_key(|(_, value)| **value)
                else {
                    break;
                };
                assigned[largest_index] = assigned[largest_index].saturating_sub(1);
                total_assigned = total_assigned.saturating_sub(1);
            }
        }
        assigned
    }
}
impl Layout for BoxLayout {
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }

    fn update_with_context(
        &self,
        rect: Rect,
        context: &LayoutContext,
        widgets: &mut dyn FnMut(ObjectId, Rect),
    ) {
        if self.items.is_empty() {
            return;
        }
        // Spacing follows the **larger** of the layout scale and the text scale.
        //
        // `LayoutContext::font_scale` is the device's text-size preference, and the two are
        // separate facts: a HiDPI screen needs more logical spacing, and a device whose text is set
        // larger needs more room between controls even at the same DPI. Taking the maximum is the
        // conservative reading — a control whose font grew but whose padding did not would have its
        // text touching its own border, which is the defect the field exists to let a layout avoid.
        //
        // The field had no reader at all before this, so a 2x text preference grew the glyphs (via
        // the theme's font token) and left every gap at its nominal size.
        let scale = context.layout_scale.max(context.font_scale);
        let scaled_spacing = (self.spacing as f32 * scale).round() as u32;
        let scaled_margin = (self.margin as f32 * scale).round() as u32;
        let gaps = (self.items.len().saturating_sub(1)) as u32;
        let primary = match self.orientation {
            Orientation::Horizontal => rect.width,
            Orientation::Vertical => rect.height,
        }
        .saturating_sub(scaled_margin * 2)
        .saturating_sub(gaps * scaled_spacing);
        let majors = self.allocate_major_lengths(primary);
        let mut cursor_x = rect.x + scaled_margin as i32;
        let mut cursor_y = rect.y + scaled_margin as i32;
        for (index, item) in self.items.iter().enumerate() {
            let major = majors.get(index).copied().unwrap_or(0);
            let child_rect = match self.orientation {
                Orientation::Horizontal => Rect::new(
                    cursor_x,
                    cursor_y,
                    major,
                    rect.height.saturating_sub(scaled_margin * 2),
                ),
                Orientation::Vertical => Rect::new(
                    cursor_x,
                    cursor_y,
                    rect.width.saturating_sub(scaled_margin * 2),
                    major,
                ),
            };
            if let Some(widget_id) = item.widget_id {
                // Grown to the device class's minimum touch area, as the flex layout does — the two
                // must agree or the same controls would be addressable in one container and not the
                // other. The cursor advances by the *allocated* major length either way, so growing
                // a child cannot push its siblings around.
                widgets(
                    widget_id,
                    crate::layout::types::grow_to_min_touch_size(
                        child_rect,
                        context.min_touch_size,
                    ),
                );
            }
            match self.orientation {
                Orientation::Horizontal => cursor_x += (major + scaled_spacing) as i32,
                Orientation::Vertical => cursor_y += (major + scaled_spacing) as i32,
            }
        }
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
    fn child_ids(&self) -> Vec<ObjectId> {
        self.items.iter().filter_map(|item| item.widget_id).collect()
    }
    fn has_child(&self, id: ObjectId) -> bool {
        self.items.iter().any(|item| item.widget_id == Some(id))
    }
    fn clear(&mut self) {
        self.items.clear();
    }
    fn add_widget(&mut self, widget_id: ObjectId, stretch: u32) {
        self.items.push(BoxLayoutItem {
            widget_id: Some(widget_id),
            stretch: stretch.max(1),
            constraints: LayoutConstraints::new(0, None),
            policy: SizePolicy::Expanding,
        });
    }
    fn remove_widget(&mut self, widget_id: ObjectId) {
        self.items.retain(|item| item.widget_id != Some(widget_id));
    }
    fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
        if self.items.is_empty() {
            return;
        }
        let gaps = (self.items.len().saturating_sub(1)) as u32;
        let primary = match self.orientation {
            Orientation::Horizontal => rect.width,
            Orientation::Vertical => rect.height,
        }
        .saturating_sub(self.margin * 2)
        .saturating_sub(gaps * self.spacing);
        let majors = self.allocate_major_lengths(primary);
        let mut cursor_x = rect.x + self.margin as i32;
        let mut cursor_y = rect.y + self.margin as i32;
        for (index, item) in self.items.iter().enumerate() {
            let major = majors.get(index).copied().unwrap_or(0);
            let child_rect = match self.orientation {
                Orientation::Horizontal => Rect::new(
                    cursor_x,
                    cursor_y,
                    major,
                    rect.height.saturating_sub(self.margin * 2),
                ),
                Orientation::Vertical => {
                    Rect::new(cursor_x, cursor_y, rect.width.saturating_sub(self.margin * 2), major)
                }
            };
            if let Some(widget_id) = item.widget_id {
                widgets(widget_id, child_rect);
            }
            match self.orientation {
                Orientation::Horizontal => cursor_x += (major + self.spacing) as i32,
                Orientation::Vertical => cursor_y += (major + self.spacing) as i32,
            }
        }
    }
}