Layouts Guide
Layouts automatically arrange child widgets within a container.
Layout Algorithm
Layouts use a two-pass algorithm:
- Measure pass: Query each child’s
size_hint()and size policy - Arrange pass: Assign positions and sizes to children
Content Margins
All layouts support content margins - spacing between the layout’s content and its edges:
use horizon_lattice::widget::layout::ContentMargins;
// Create uniform margins (same on all sides)
let uniform = ContentMargins::uniform(10.0);
assert_eq!(uniform.left, 10.0);
assert_eq!(uniform.top, 10.0);
assert_eq!(uniform.right, 10.0);
assert_eq!(uniform.bottom, 10.0);
// Create symmetric margins (horizontal/vertical)
let symmetric = ContentMargins::symmetric(20.0, 10.0);
assert_eq!(symmetric.left, 20.0);
assert_eq!(symmetric.right, 20.0);
assert_eq!(symmetric.top, 10.0);
assert_eq!(symmetric.bottom, 10.0);
// Create custom margins
let custom = ContentMargins::new(5.0, 10.0, 15.0, 20.0);
assert_eq!(custom.horizontal(), 20.0); // left + right
assert_eq!(custom.vertical(), 30.0); // top + bottom
LayoutKind Enum
The LayoutKind enum provides a unified interface for all layout types:
use horizon_lattice::widget::layout::LayoutKind;
// Create different layout types
let hbox = LayoutKind::horizontal();
let vbox = LayoutKind::vertical();
let grid = LayoutKind::grid();
let form = LayoutKind::form();
let stack = LayoutKind::stack();
let flow = LayoutKind::flow();
let anchor = LayoutKind::anchor();
// All layouts share a common interface
let mut layout = LayoutKind::vertical();
assert_eq!(layout.item_count(), 0);
assert!(layout.is_empty());
BoxLayout (HBox and VBox)
Arrange widgets horizontally or vertically:
use horizon_lattice::widget::layout::{BoxLayout, ContentMargins, Orientation};
// Create a horizontal layout
let mut hbox = BoxLayout::horizontal();
hbox.set_spacing(10.0); // Space between widgets
hbox.set_content_margins(ContentMargins::uniform(8.0)); // Outer margins
assert_eq!(hbox.spacing(), 10.0);
assert_eq!(hbox.orientation(), Orientation::Horizontal);
// Create a vertical layout
let mut vbox = BoxLayout::vertical();
vbox.set_spacing(5.0);
assert_eq!(vbox.orientation(), Orientation::Vertical);
Adding Items to Layouts
Layouts can contain widgets, spacers, and nested layouts:
use horizon_lattice::widget::layout::{LayoutKind, LayoutItem, SpacerItem, SpacerType};
use horizon_lattice::render::Size;
let mut layout = LayoutKind::vertical();
// Add a fixed spacer (takes a specific amount of space)
let fixed_spacer = LayoutItem::Spacer(SpacerItem::fixed(Size::new(0.0, 20.0)));
layout.add_item(fixed_spacer);
// Add an expanding spacer (fills available space)
let expanding_spacer = LayoutItem::Spacer(SpacerItem::new(
Size::ZERO,
SpacerType::Expanding,
));
layout.add_item(expanding_spacer);
assert_eq!(layout.item_count(), 2);
GridLayout
Arrange widgets in rows and columns:
use horizon_lattice::widget::layout::GridLayout;
let mut grid = GridLayout::new();
// Set spacing between cells
grid.set_spacing(10.0);
grid.set_horizontal_spacing(15.0); // Override horizontal only
grid.set_vertical_spacing(5.0); // Override vertical only
// Set column stretch factors (column 1 expands more)
grid.set_column_stretch(0, 0); // Column 0: no stretch
grid.set_column_stretch(1, 1); // Column 1: stretch factor 1
// Set row stretch
grid.set_row_stretch(0, 0); // Row 0: no stretch
grid.set_row_stretch(1, 2); // Row 1: stretch factor 2
// Set minimum column width
grid.set_column_minimum_width(0, 100.0);
FormLayout
Convenient layout for label-field pairs:
use horizon_lattice::widget::layout::{FormLayout, RowWrapPolicy, FieldGrowthPolicy};
let mut form = FormLayout::new();
// Configure form behavior
form.set_row_wrap_policy(RowWrapPolicy::WrapLongRows);
form.set_field_growth_policy(FieldGrowthPolicy::ExpandingFieldsGrow);
// Set spacing
form.set_horizontal_spacing(10.0);
form.set_vertical_spacing(8.0);
// The form automatically aligns labels and fields
// Labels go in the left column, fields in the right
StackLayout
Stack widgets on top of each other (only one visible at a time):
use horizon_lattice::widget::layout::{StackLayout, StackSizeMode};
let mut stack = StackLayout::new();
// Configure how the stack calculates its size
stack.set_size_mode(StackSizeMode::CurrentWidgetSize); // Size based on current widget
// or
stack.set_size_mode(StackSizeMode::MaximumSize); // Size based on largest widget
// Set the current index (which widget is visible)
stack.set_current_index(0);
assert_eq!(stack.current_index(), 0);
FlowLayout
Arrange widgets in a flowing pattern (like text wrapping):
use horizon_lattice::widget::layout::FlowLayout;
let mut flow = FlowLayout::new();
// Set spacing between items
flow.set_spacing(10.0);
flow.set_horizontal_spacing(15.0); // Between items in a row
flow.set_vertical_spacing(8.0); // Between rows
AnchorLayout
Position widgets relative to each other or the container:
use horizon_lattice::widget::layout::{AnchorLayout, Anchor, AnchorLine, AnchorTarget};
use horizon_lattice_core::ObjectId;
let mut anchor = AnchorLayout::new();
// Anchors connect widget edges to targets
// For example: widget's left edge to parent's left edge plus margin
let left_anchor = Anchor {
line: AnchorLine::Left,
target: AnchorTarget::Parent,
target_line: AnchorLine::Left,
margin: 10.0,
};
// Center horizontally in parent
let center_anchor = Anchor {
line: AnchorLine::HorizontalCenter,
target: AnchorTarget::Parent,
target_line: AnchorLine::HorizontalCenter,
margin: 0.0,
};
Nested Layouts
Layouts can be nested for complex UIs:
use horizon_lattice::widget::layout::{LayoutKind, BoxLayout, ContentMargins};
// Main vertical layout
let mut main = LayoutKind::vertical();
// Header as a horizontal layout
let mut header = BoxLayout::horizontal();
header.set_spacing(10.0);
header.set_content_margins(ContentMargins::uniform(5.0));
// Convert to LayoutKind for nesting
let header_kind = LayoutKind::from(header);
// In a real app, you would add the header layout as an item
// to the main layout
Layout Invalidation
Layouts track when they need recalculation:
use horizon_lattice::widget::layout::LayoutKind;
let mut layout = LayoutKind::vertical();
// Check if layout needs recalculation
if layout.needs_recalculation() {
println!("Layout needs to be recalculated");
}
// Invalidate to force recalculation
layout.invalidate();
assert!(layout.needs_recalculation());
Layout Geometry
Set and query layout geometry:
use horizon_lattice::widget::layout::LayoutKind;
use horizon_lattice::render::Rect;
let mut layout = LayoutKind::vertical();
// Set the layout's bounding rectangle
let rect = Rect::new(10.0, 20.0, 300.0, 400.0);
layout.set_geometry(rect);
// Query the geometry
let geom = layout.geometry();
assert_eq!(geom.origin.x, 10.0);
assert_eq!(geom.origin.y, 20.0);
assert_eq!(geom.size.width, 300.0);
assert_eq!(geom.size.height, 400.0);
Custom Layout Implementation
Creating a custom layout involves implementing the Layout trait and optionally using LayoutBase to handle common functionality.
Architecture Overview
Custom layouts in Horizon Lattice follow this architecture:
- Layout trait: Defines 21 methods for item management, size calculation, geometry, and invalidation
- LayoutBase: A helper struct that provides common functionality (item storage, margins, spacing, caching)
- LayoutItem: Enum wrapping widgets, spacers, or nested layouts
- Two-pass algorithm: Collection (size hints) followed by distribution (positioning)
Step-by-Step: Creating a Centered Layout
Let’s build a CenteredLayout that centers a single widget within its bounds.
Step 1: Define the Struct
use horizon_lattice::widget::layout::{Layout, LayoutBase, LayoutItem, ContentMargins};
use horizon_lattice::widget::geometry::{SizeHint, SizePolicyPair};
use horizon_lattice::widget::dispatcher::WidgetAccess;
use horizon_lattice_core::ObjectId;
use horizon_lattice_render::{Rect, Size};
/// A layout that centers its single child widget.
#[derive(Debug, Clone)]
pub struct CenteredLayout {
/// Delegate common functionality to LayoutBase.
base: LayoutBase,
}
impl CenteredLayout {
/// Create a new centered layout.
pub fn new() -> Self {
Self {
base: LayoutBase::new(),
}
}
}
impl Default for CenteredLayout {
fn default() -> Self {
Self::new()
}
}
Step 2: Implement the Layout Trait
The Layout trait requires implementing methods across several categories:
Item Management - Delegate to LayoutBase:
impl Layout for CenteredLayout {
fn add_item(&mut self, item: LayoutItem) {
// Only allow one item for centering
if self.base.is_empty() {
self.base.add_item(item);
}
}
fn insert_item(&mut self, index: usize, item: LayoutItem) {
if self.base.is_empty() && index == 0 {
self.base.insert_item(index, item);
}
}
fn remove_item(&mut self, index: usize) -> Option<LayoutItem> {
self.base.remove_item(index)
}
fn remove_widget(&mut self, widget: ObjectId) -> bool {
self.base.remove_widget(widget)
}
fn item_count(&self) -> usize {
self.base.item_count()
}
fn item_at(&self, index: usize) -> Option<&LayoutItem> {
self.base.item_at(index)
}
fn item_at_mut(&mut self, index: usize) -> Option<&mut LayoutItem> {
self.base.item_at_mut(index)
}
fn clear(&mut self) {
self.base.clear();
}
// ... continued below
}
Size Hints - Calculate based on the child:
impl Layout for CenteredLayout {
// ... item management above
fn size_hint<S: WidgetAccess>(&self, storage: &S) -> SizeHint {
// Return cached hint if available
if let Some(cached) = self.base.cached_size_hint() {
return cached;
}
// Get child's size hint (if we have a child)
let child_hint = if let Some(item) = self.base.item_at(0) {
self.base.get_item_size_hint(storage, item)
} else {
SizeHint::default()
};
// Add margins
let margins = self.base.content_margins();
SizeHint {
preferred: Size::new(
child_hint.preferred.width + margins.horizontal(),
child_hint.preferred.height + margins.vertical(),
),
minimum: child_hint.minimum.map(|s| Size::new(
s.width + margins.horizontal(),
s.height + margins.vertical(),
)),
maximum: child_hint.maximum.map(|s| Size::new(
s.width + margins.horizontal(),
s.height + margins.vertical(),
)),
}
}
fn minimum_size<S: WidgetAccess>(&self, storage: &S) -> Size {
self.size_hint(storage).effective_minimum()
}
fn size_policy(&self) -> SizePolicyPair {
SizePolicyPair::default()
}
// ... continued below
}
Geometry - Delegate to LayoutBase:
impl Layout for CenteredLayout {
// ... size hints above
fn geometry(&self) -> Rect {
self.base.geometry()
}
fn set_geometry(&mut self, rect: Rect) {
self.base.set_geometry(rect);
}
fn content_margins(&self) -> ContentMargins {
self.base.content_margins()
}
fn set_content_margins(&mut self, margins: ContentMargins) {
self.base.set_content_margins(margins);
}
fn spacing(&self) -> f32 {
self.base.spacing()
}
fn set_spacing(&mut self, spacing: f32) {
self.base.set_spacing(spacing);
}
// ... continued below
}
Layout Calculation - The core algorithm:
impl Layout for CenteredLayout {
// ... geometry above
fn calculate<S: WidgetAccess>(&mut self, storage: &S, _available: Size) -> Size {
let content_rect = self.base.content_rect();
if let Some(item) = self.base.item_at(0) {
// Get the child's preferred size
let hint = self.base.get_item_size_hint(storage, item);
// Constrain to available space
let child_width = hint.preferred.width.min(content_rect.width());
let child_height = hint.preferred.height.min(content_rect.height());
// Calculate centered position
let x = content_rect.origin.x + (content_rect.width() - child_width) / 2.0;
let y = content_rect.origin.y + (content_rect.height() - child_height) / 2.0;
// Store the calculated geometry
self.base.set_item_geometry(0, Rect::new(x, y, child_width, child_height));
}
// Cache size hint for performance
let hint = self.size_hint(storage);
self.base.set_cached_size_hint(hint);
self.base.mark_valid();
self.base.geometry().size
}
fn apply<S: WidgetAccess>(&self, storage: &mut S) {
// Apply calculated geometry to the widget
if let Some(item) = self.base.item_at(0) {
if let Some(geometry) = self.base.item_geometry(0) {
LayoutBase::apply_item_geometry(storage, item, geometry);
}
}
}
// ... continued below
}
Invalidation and Ownership - Delegate to LayoutBase:
impl Layout for CenteredLayout {
// ... calculate and apply above
fn invalidate(&mut self) {
self.base.invalidate();
}
fn needs_recalculation(&self) -> bool {
self.base.needs_recalculation()
}
fn parent_widget(&self) -> Option<ObjectId> {
self.base.parent_widget()
}
fn set_parent_widget(&mut self, parent: Option<ObjectId>) {
self.base.set_parent_widget(parent);
}
}
Using LayoutBase Helpers
LayoutBase provides several helper methods for implementing layouts:
use horizon_lattice::widget::layout::LayoutBase;
// In your calculate() implementation:
fn calculate<S: WidgetAccess>(&mut self, storage: &S, available: Size) -> Size {
// Get the content area (geometry minus margins)
let content = self.base.content_rect();
// Iterate over items and check visibility
for (i, item) in self.base.items().iter().enumerate() {
// Skip hidden widgets
if !self.base.is_item_visible(storage, item) {
continue;
}
// Get size hint for this item
let hint = self.base.get_item_size_hint(storage, item);
// Get size policy for this item
let policy = self.base.get_item_size_policy(storage, item);
// Calculate position and store it
let rect = Rect::new(/* your calculation */);
self.base.set_item_geometry(i, rect);
}
// Count visible items (for spacing calculations)
let visible_count = self.base.visible_item_count(storage);
self.base.mark_valid();
available
}
Space Distribution
For layouts that distribute space among multiple items, use LayoutBase::distribute_space:
use horizon_lattice::widget::geometry::{SizeHint, SizePolicy};
// Collect item information: (size_hint, policy, stretch_factor)
let items: Vec<(SizeHint, SizePolicy, u8)> = /* gather from items */;
// Calculate totals
let total_hint: f32 = items.iter().map(|(h, _, _)| h.preferred.width).sum();
let total_min: f32 = items.iter().map(|(h, _, _)| h.effective_minimum().width).sum();
let total_max: f32 = items.iter().map(|(h, _, _)| h.effective_maximum().width).sum();
// Distribute available space
let sizes = LayoutBase::distribute_space(
&items,
available_width, // Total available space
total_hint, // Sum of preferred sizes
total_min, // Sum of minimum sizes
total_max, // Sum of maximum sizes
);
// sizes[i] is the width to assign to item i
RTL (Right-to-Left) Support
For horizontal layouts, support RTL text direction:
fn calculate<S: WidgetAccess>(&mut self, storage: &S, available: Size) -> Size {
let content = self.base.content_rect();
let mut x_pos: f32 = 0.0;
for (i, item) in self.base.items().iter().enumerate() {
let item_width = /* calculated width */;
// Mirror x position for RTL layouts
let x = self.base.mirror_x(x_pos, item_width, content.width());
let rect = Rect::new(
content.origin.x + x,
content.origin.y,
item_width,
content.height(),
);
self.base.set_item_geometry(i, rect);
x_pos += item_width + self.base.spacing();
}
self.base.mark_valid();
available
}
Height-for-Width Layouts
Some layouts (like flow layouts) need to adjust height based on available width:
impl Layout for FlowingLayout {
fn has_height_for_width(&self) -> bool {
true
}
fn height_for_width<S: WidgetAccess>(&self, storage: &S, width: f32) -> Option<f32> {
// Calculate how many rows needed at this width
let mut current_x: f32 = 0.0;
let mut current_row_height: f32 = 0.0;
let mut total_height: f32 = 0.0;
let content_width = width - self.base.content_margins().horizontal();
for item in self.base.items() {
let hint = self.base.get_item_size_hint(storage, item);
let item_width = hint.preferred.width;
let item_height = hint.preferred.height;
if current_x + item_width > content_width && current_x > 0.0 {
// Wrap to next row
total_height += current_row_height + self.base.spacing();
current_x = 0.0;
current_row_height = 0.0;
}
current_row_height = current_row_height.max(item_height);
current_x += item_width + self.base.spacing();
}
total_height += current_row_height;
Some(total_height + self.base.content_margins().vertical())
}
}
Complete Example: Diagonal Layout
Here’s a complete custom layout that arranges items diagonally:
use horizon_lattice::widget::layout::{Layout, LayoutBase, LayoutItem, ContentMargins};
use horizon_lattice::widget::geometry::{SizeHint, SizePolicyPair};
use horizon_lattice::widget::dispatcher::WidgetAccess;
use horizon_lattice_core::ObjectId;
use horizon_lattice_render::{Rect, Size};
/// Arranges items diagonally from top-left to bottom-right.
#[derive(Debug, Clone)]
pub struct DiagonalLayout {
base: LayoutBase,
/// Horizontal offset per item.
x_offset: f32,
/// Vertical offset per item.
y_offset: f32,
}
impl DiagonalLayout {
pub fn new(x_offset: f32, y_offset: f32) -> Self {
Self {
base: LayoutBase::new(),
x_offset,
y_offset,
}
}
}
impl Layout for DiagonalLayout {
// Item management - delegate to base
fn add_item(&mut self, item: LayoutItem) { self.base.add_item(item); }
fn insert_item(&mut self, index: usize, item: LayoutItem) { self.base.insert_item(index, item); }
fn remove_item(&mut self, index: usize) -> Option<LayoutItem> { self.base.remove_item(index) }
fn remove_widget(&mut self, widget: ObjectId) -> bool { self.base.remove_widget(widget) }
fn item_count(&self) -> usize { self.base.item_count() }
fn item_at(&self, index: usize) -> Option<&LayoutItem> { self.base.item_at(index) }
fn item_at_mut(&mut self, index: usize) -> Option<&mut LayoutItem> { self.base.item_at_mut(index) }
fn clear(&mut self) { self.base.clear(); }
fn size_hint<S: WidgetAccess>(&self, storage: &S) -> SizeHint {
let margins = self.base.content_margins();
let visible_count = self.base.visible_item_count(storage);
let mut max_width: f32 = 0.0;
let mut max_height: f32 = 0.0;
for (i, item) in self.base.items().iter().enumerate() {
if !self.base.is_item_visible(storage, item) { continue; }
let hint = self.base.get_item_size_hint(storage, item);
let x_end = (i as f32) * self.x_offset + hint.preferred.width;
let y_end = (i as f32) * self.y_offset + hint.preferred.height;
max_width = max_width.max(x_end);
max_height = max_height.max(y_end);
}
SizeHint::new(Size::new(
max_width + margins.horizontal(),
max_height + margins.vertical(),
))
}
fn minimum_size<S: WidgetAccess>(&self, storage: &S) -> Size {
self.size_hint(storage).effective_minimum()
}
fn size_policy(&self) -> SizePolicyPair { SizePolicyPair::default() }
// Geometry - delegate to base
fn geometry(&self) -> Rect { self.base.geometry() }
fn set_geometry(&mut self, rect: Rect) { self.base.set_geometry(rect); }
fn content_margins(&self) -> ContentMargins { self.base.content_margins() }
fn set_content_margins(&mut self, margins: ContentMargins) { self.base.set_content_margins(margins); }
fn spacing(&self) -> f32 { self.base.spacing() }
fn set_spacing(&mut self, spacing: f32) { self.base.set_spacing(spacing); }
fn calculate<S: WidgetAccess>(&mut self, storage: &S, available: Size) -> Size {
let content = self.base.content_rect();
let mut visible_index = 0;
for (i, item) in self.base.items().iter().enumerate() {
if !self.base.is_item_visible(storage, item) {
self.base.set_item_geometry(i, Rect::ZERO);
continue;
}
let hint = self.base.get_item_size_hint(storage, item);
let x = content.origin.x + (visible_index as f32) * self.x_offset;
let y = content.origin.y + (visible_index as f32) * self.y_offset;
self.base.set_item_geometry(i, Rect::new(
x,
y,
hint.preferred.width.min(content.width()),
hint.preferred.height.min(content.height()),
));
visible_index += 1;
}
self.base.mark_valid();
available
}
fn apply<S: WidgetAccess>(&self, storage: &mut S) {
for (i, item) in self.base.items().iter().enumerate() {
if let Some(geometry) = self.base.item_geometry(i) {
LayoutBase::apply_item_geometry(storage, item, geometry);
}
}
}
// Invalidation - delegate to base
fn invalidate(&mut self) { self.base.invalidate(); }
fn needs_recalculation(&self) -> bool { self.base.needs_recalculation() }
fn parent_widget(&self) -> Option<ObjectId> { self.base.parent_widget() }
fn set_parent_widget(&mut self, parent: Option<ObjectId>) { self.base.set_parent_widget(parent); }
}
Best Practices for Custom Layouts
- Always use LayoutBase - It handles caching, invalidation, and common operations
- Mark layout valid after calculation - Call
self.base.mark_valid()at the end ofcalculate() - Skip hidden items - Use
is_item_visible()to skip hidden widgets - Cache size hints - Use
set_cached_size_hint()for performance - Handle empty layouts - Return early if
item_count() == 0 - Respect size policies - Use
get_item_size_policy()to determine if items can grow/shrink - Account for margins - Use
content_rect()to get the area inside margins - Test with RTL - If horizontal, test with RTL text direction
Default Layout Constants
The layout system provides sensible defaults:
use horizon_lattice::widget::layout::{DEFAULT_SPACING, DEFAULT_MARGINS};
// Default spacing between items
assert_eq!(DEFAULT_SPACING, 6.0);
// Default content margins
assert_eq!(DEFAULT_MARGINS.left, 9.0);
assert_eq!(DEFAULT_MARGINS.top, 9.0);
assert_eq!(DEFAULT_MARGINS.right, 9.0);
assert_eq!(DEFAULT_MARGINS.bottom, 9.0);
Best Practices
- Use appropriate layouts - VBox/HBox for linear arrangements, Grid for tables, Form for input forms
- Set size policies - Help layouts make better decisions about space distribution
- Use stretch factors - Control how extra space is distributed between widgets
- Nest layouts - Combine simple layouts for complex UIs rather than using one complex layout
- Set minimum sizes - Prevent layouts from shrinking widgets too small
- Use spacers - Add flexible space to push widgets apart or fill gaps
See the Layout Reference for all layout types.