Widgets Guide
This guide covers the widget system in depth.
Widget Trait
Every widget implements the Widget trait:
use horizon_lattice::widget::{Widget, WidgetBase, PaintContext};
use horizon_lattice::widget::SizeHint;
use horizon_lattice::widget::events::WidgetEvent;
use horizon_lattice_core::{Object, ObjectId};
pub trait WidgetDefinition {
fn widget_base(&self) -> &WidgetBase;
fn widget_base_mut(&mut self) -> &mut WidgetBase;
fn size_hint(&self) -> SizeHint { SizeHint::default() }
fn paint(&self, ctx: &mut PaintContext<'_>) {}
fn event(&mut self, event: &mut WidgetEvent) -> bool { false }
}
Size Hints
Size hints tell layouts what size a widget prefers:
use horizon_lattice::widget::SizeHint;
use horizon_lattice::render::Size;
// Create a simple size hint with preferred dimensions
let hint = SizeHint::from_dimensions(100.0, 30.0);
assert_eq!(hint.preferred, Size::new(100.0, 30.0));
// Add minimum and maximum constraints
let constrained = SizeHint::from_dimensions(100.0, 30.0)
.with_minimum_dimensions(50.0, 20.0)
.with_maximum_dimensions(200.0, 50.0);
assert_eq!(constrained.minimum, Some(Size::new(50.0, 20.0)));
assert_eq!(constrained.maximum, Some(Size::new(200.0, 50.0)));
// Create a fixed size (cannot grow or shrink)
let fixed = SizeHint::fixed(Size::new(100.0, 100.0));
assert!(fixed.is_fixed());
Size Policies
Size policies control how widgets grow and shrink:
use horizon_lattice::widget::{SizePolicy, SizePolicyPair};
// Fixed - cannot resize
let fixed = SizePolicyPair::fixed();
assert!(!fixed.horizontal.can_grow());
assert!(!fixed.horizontal.can_shrink());
// Preferred - can grow or shrink, prefers hint size
let preferred = SizePolicyPair::preferred();
assert!(preferred.horizontal.can_grow());
assert!(preferred.horizontal.can_shrink());
// Expanding - actively wants more space
let expanding = SizePolicyPair::expanding();
assert!(expanding.horizontal.wants_to_grow());
// Custom policy with stretch factor
let stretched = SizePolicyPair::new(SizePolicy::Expanding, SizePolicy::Fixed)
.with_horizontal_stretch(2); // Gets 2x extra space compared to stretch=1
assert_eq!(stretched.horizontal_stretch, 2);
Widget Lifecycle
new()- Create widget with WidgetBase- Configure properties and connect signals
- Add to parent/layout
show()is called (inherited from parent)paint()called when visibleevent()called for input- Widget dropped when parent is destroyed
Creating Custom Widgets
Here’s a conceptual example of creating a custom progress bar widget:
use horizon_lattice::widget::{Widget, WidgetBase, PaintContext};
use horizon_lattice::widget::SizeHint;
use horizon_lattice::widget::events::WidgetEvent;
use horizon_lattice::render::{Color, Rect};
use horizon_lattice_core::{Object, ObjectId};
struct ProgressBar {
base: WidgetBase,
value: f32, // 0.0 to 1.0
color: Color,
}
impl ProgressBar {
pub fn new() -> Self {
Self {
base: WidgetBase::new::<Self>(),
value: 0.0,
color: Color::from_rgb8(52, 152, 219),
}
}
pub fn set_value(&mut self, value: f32) {
self.value = value.clamp(0.0, 1.0);
self.base.update(); // Request repaint
}
pub fn value(&self) -> f32 {
self.value
}
}
impl Object for ProgressBar {
fn object_id(&self) -> ObjectId {
self.base.object_id()
}
}
impl Widget for ProgressBar {
fn widget_base(&self) -> &WidgetBase { &self.base }
fn widget_base_mut(&mut self) -> &mut WidgetBase { &mut self.base }
fn size_hint(&self) -> SizeHint {
SizeHint::from_dimensions(200.0, 20.0)
.with_minimum_dimensions(50.0, 10.0)
}
fn paint(&self, ctx: &mut PaintContext<'_>) {
let rect = ctx.rect();
// Background
ctx.renderer().fill_rect(rect, Color::from_rgb8(200, 200, 200));
// Progress fill
let fill_width = rect.width() * self.value;
let fill_rect = Rect::new(0.0, 0.0, fill_width, rect.height());
ctx.renderer().fill_rect(fill_rect, self.color);
}
}
Size Hint Examples
Different widgets have different size hint patterns:
use horizon_lattice::widget::SizeHint;
use horizon_lattice::render::Size;
// Label - prefers text size, can't shrink below it
fn label_size_hint(text_width: f32, text_height: f32) -> SizeHint {
SizeHint::from_dimensions(text_width, text_height)
.with_minimum_dimensions(text_width, text_height)
}
// Button - has padding around content
fn button_size_hint(content_width: f32, content_height: f32) -> SizeHint {
let padding = 16.0;
SizeHint::from_dimensions(content_width + padding, content_height + padding)
.with_minimum_dimensions(60.0, 30.0)
}
// Text input - can expand horizontally
fn text_input_size_hint() -> SizeHint {
SizeHint::from_dimensions(150.0, 30.0)
.with_minimum_dimensions(50.0, 30.0)
}
Geometry Methods
Widgets provide methods to query and set their geometry:
use horizon_lattice::render::{Point, Rect, Size};
// Simulating widget geometry operations
let geometry = Rect::new(10.0, 20.0, 100.0, 50.0);
// Position (relative to parent)
let pos = geometry.origin;
assert_eq!(pos, Point::new(10.0, 20.0));
// Size
let size = geometry.size;
assert_eq!(size, Size::new(100.0, 50.0));
// Local rect (always at origin 0,0)
let local_rect = Rect::new(0.0, 0.0, size.width, size.height);
assert_eq!(local_rect.origin, Point::new(0.0, 0.0));
// Check if a point is inside the local rect
let point = Point::new(50.0, 25.0);
assert!(local_rect.contains(point));
let outside = Point::new(150.0, 25.0);
assert!(!local_rect.contains(outside));
Coordinate Mapping
Map points between widget-local and parent coordinate systems:
use horizon_lattice::render::Point;
// Widget at position (10, 20)
let widget_pos = Point::new(10.0, 20.0);
// Point in widget-local coordinates
let local_point = Point::new(5.0, 5.0);
// Map to parent coordinates
let parent_point = Point::new(
local_point.x + widget_pos.x,
local_point.y + widget_pos.y,
);
assert_eq!(parent_point, Point::new(15.0, 25.0));
// Map from parent back to local
let back_to_local = Point::new(
parent_point.x - widget_pos.x,
parent_point.y - widget_pos.y,
);
assert_eq!(back_to_local, local_point);
Visibility and Enabled State
Control widget visibility and interaction:
// Visibility concepts
let mut visible = true;
let mut enabled = true;
// Hide a widget
visible = false;
// Disable a widget (grayed out, can't interact)
enabled = false;
// Check effective state (considering parent hierarchy)
// If parent is hidden, child is effectively hidden too
fn is_effectively_visible(self_visible: bool, parent_visible: bool) -> bool {
self_visible && parent_visible
}
assert!(!is_effectively_visible(true, false)); // Parent hidden
assert!(!is_effectively_visible(false, true)); // Self hidden
assert!(is_effectively_visible(true, true)); // Both visible
Focus Policy
Control how widgets receive keyboard focus:
use horizon_lattice::widget::FocusPolicy;
// NoFocus - widget cannot receive focus (e.g., labels)
let no_focus = FocusPolicy::NoFocus;
// TabFocus - focus via Tab key only (e.g., read-only controls)
let tab_focus = FocusPolicy::TabFocus;
// ClickFocus - focus via mouse click only
let click_focus = FocusPolicy::ClickFocus;
// StrongFocus - focus via both Tab and click (e.g., buttons, text fields)
let strong_focus = FocusPolicy::StrongFocus;
Repaint Requests
Request widget repainting when content changes:
use horizon_lattice::render::Rect;
// Full repaint - entire widget needs redrawing
fn request_full_repaint(needs_repaint: &mut bool) {
*needs_repaint = true;
}
// Partial repaint - only a region needs redrawing
fn request_partial_repaint(dirty_region: &mut Option<Rect>, new_dirty: Rect) {
*dirty_region = Some(match dirty_region {
Some(existing) => existing.union(&new_dirty),
None => new_dirty,
});
}
let mut dirty = None;
request_partial_repaint(&mut dirty, Rect::new(0.0, 0.0, 50.0, 50.0));
request_partial_repaint(&mut dirty, Rect::new(40.0, 40.0, 50.0, 50.0));
// Dirty region is now the union of both rects
let combined = dirty.unwrap();
assert_eq!(combined.origin.x, 0.0);
assert_eq!(combined.origin.y, 0.0);
Signals and Properties
Widgets use signals to notify of changes:
use horizon_lattice_core::{Signal, Property};
// Create signals for widget state changes
let visible_changed: Signal<bool> = Signal::new();
let geometry_changed: Signal<(f32, f32, f32, f32)> = Signal::new();
// Connect to signals
visible_changed.connect(|&visible| {
println!("Visibility changed to: {}", visible);
});
// Emit when state changes
visible_changed.emit(false);
// Properties with automatic change notification
let value: Property<f32> = Property::new(0.0);
// Get the current value
assert_eq!(value.get(), 0.0);
// Set returns true if value changed
assert!(value.set(0.5));
assert!(!value.set(0.5)); // Same value, returns false
Built-in Widgets
See the Widget Catalog for all available widgets including:
- Basic: Label, PushButton, CheckBox, RadioButton
- Input: LineEdit, TextEdit, SpinBox, Slider
- Containers: Frame, GroupBox, ScrollArea, TabWidget
- Display: ProgressBar, StatusBar
- Dialogs: MessageBox, FileDialog, ColorDialog