Intrinsic Sizing
FrankenTUI supports intrinsic sizing: widgets can report their natural
dimensions based on content, enabling content-aware layouts with
Constraint::FitContent.
Overview
Traditional TUI layouts use fixed or percentage-based sizing. Intrinsic sizing adds content-awareness:
| Approach | Example | Use Case |
|---|---|---|
| Fixed | Constraint::Fixed(20) | Static chrome, borders |
| Percentage | Constraint::Percentage(50.0) | Proportional splits |
| Fill | Constraint::Fill | Remaining space |
| FitContent | Constraint::FitContent | Content-aware sizing |
Core Types
SizeConstraints
Captures the full sizing semantics for a widget:
pub struct SizeConstraints {
pub min: Size, // Minimum usable size
pub preferred: Size, // Ideal size for content
pub max: Option<Size>, // Maximum useful size (None = unbounded)
}Invariants:
min.width <= preferred.width <= max.map_or(∞, |m| m.width)min.height <= preferred.height <= max.map_or(∞, |m| m.height)
Constructors:
SizeConstraints::ZERO— Default for fill-available widgetsSizeConstraints::exact(size)— Fixed size (min = preferred = max)SizeConstraints::at_least(min, preferred)— Minimum with unbounded max
MeasurableWidget Trait
Widgets implement this to report intrinsic dimensions:
pub trait MeasurableWidget {
/// Measure the widget given available space.
fn measure(&self, available: Size) -> SizeConstraints {
SizeConstraints::ZERO // Default: fill available
}
/// Does this widget have content-dependent sizing?
fn has_intrinsic_size(&self) -> bool {
false // Default: no
}
}Widget Author Guide
When to Implement MeasurableWidget
Implement intrinsic sizing when your widget has natural dimensions:
| Widget Type | Has Intrinsic Size? | Example |
|---|---|---|
| Label/Text | Yes | Width = text length |
| Button | Yes | Width = label + padding |
| Icon | Yes | Usually 1x1 or 2x1 |
| Paragraph | Yes | Wrapped text dimensions |
| Table | Yes | Column widths from content |
| Container | Maybe | Sum of children |
| Canvas/Chart | No | Fills available space |
Implementation Checklist
- Calculate min: Smallest size before content clips
- Calculate preferred: Size that best displays content
- Calculate max: Maximum useful size (None if unbounded)
- Return true for
has_intrinsic_size()
Example: Simple Label
use ftui_core::geometry::Size;
use ftui_widgets::{MeasurableWidget, SizeConstraints};
struct Label {
text: String,
}
impl MeasurableWidget for Label {
fn measure(&self, _available: Size) -> SizeConstraints {
let width = self.text.len() as u16;
SizeConstraints {
min: Size::new(1, 1), // At least show something
preferred: Size::new(width, 1), // Full text on one line
max: Some(Size::new(width, 1)), // No benefit from extra space
}
}
fn has_intrinsic_size(&self) -> bool {
true
}
}Example: Wrapping Text
impl MeasurableWidget for Paragraph {
fn measure(&self, available: Size) -> SizeConstraints {
// Calculate wrapped line count at available width
let lines = self.wrap_lines(available.width);
let max_line_width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
let height = lines.len() as u16;
SizeConstraints {
min: Size::new(1, 1),
preferred: Size::new(max_line_width, height),
max: None, // Can use extra space for padding
}
}
fn has_intrinsic_size(&self) -> bool {
true
}
}Example: Container with Children
impl MeasurableWidget for HorizontalStack {
fn measure(&self, available: Size) -> SizeConstraints {
let mut total_width = 0u16;
let mut max_height = 0u16;
let mut min_width = 0u16;
let mut min_height = 0u16;
for child in &self.children {
let c = child.measure(available);
total_width = total_width.saturating_add(c.preferred.width);
max_height = max_height.max(c.preferred.height);
min_width = min_width.saturating_add(c.min.width);
min_height = min_height.max(c.min.height);
}
SizeConstraints {
min: Size::new(min_width, min_height),
preferred: Size::new(total_width, max_height),
max: None,
}
}
fn has_intrinsic_size(&self) -> bool {
self.children.iter().any(|c| c.has_intrinsic_size())
}
}Implementation Requirements
- Monotonicity:
min <= preferred <= max - Purity: Same inputs → same outputs (no side effects)
- Performance: O(content_length) worst case
- Min Constancy:
minshould not depend onavailable
Layout Migration Guide
Before: Static Layout
// Old approach: fixed widths
let chunks = Flex::horizontal()
.constraints([
Constraint::Fixed(20), // Sidebar always 20 cols
Constraint::Fill, // Content fills rest
])
.split(area);After: Content-Aware Layout
// New approach: fit to content
let chunks = Flex::horizontal()
.constraints([
Constraint::FitContent, // Sidebar fits its content
Constraint::Fill, // Content fills rest
])
.split(area);
// The layout system calls sidebar.measure() to determine widthResponsive Patterns
Adaptive Sidebar
Sidebar collapses to icons when space is tight:
impl MeasurableWidget for AdaptiveSidebar {
fn measure(&self, available: Size) -> SizeConstraints {
if available.width < 60 {
// Icon-only mode
SizeConstraints::exact(Size::new(4, available.height))
} else {
// Full labels
SizeConstraints {
min: Size::new(4, 1),
preferred: Size::new(20, available.height),
max: Some(Size::new(30, available.height)),
}
}
}
}Flexible Cards
Cards switch between horizontal and vertical layout:
fn layout_cards(area: Rect) -> Vec<Rect> {
if area.width >= 60 {
// Side-by-side
Flex::horizontal()
.constraints([Constraint::FitContent, Constraint::FitContent])
.split(area)
} else {
// Stacked
Flex::vertical()
.constraints([Constraint::FitContent, Constraint::FitContent])
.split(area)
}
}Constraint Types Reference
| Constraint | Behavior | When to Use |
|---|---|---|
Fixed(n) | Exactly n cells | Borders, separators |
Percentage(p) | p% of available | Proportional splits |
Ratio(n, d) | n/d of available | Precise ratios |
Fill | All remaining space | Main content areas |
FitContent | Widget’s preferred size | Content-aware sizing |
Testing Intrinsic Sizing
Unit Test Template
#[test]
fn widget_measure_invariants() {
let widget = MyWidget::new("content");
let available = Size::new(100, 50);
let c = widget.measure(available);
// Invariant: min <= preferred <= max
assert!(c.min.width <= c.preferred.width);
assert!(c.min.height <= c.preferred.height);
if let Some(max) = c.max {
assert!(c.preferred.width <= max.width);
assert!(c.preferred.height <= max.height);
}
}
#[test]
fn widget_measure_is_pure() {
let widget = MyWidget::new("content");
let available = Size::new(100, 50);
let c1 = widget.measure(available);
let c2 = widget.measure(available);
assert_eq!(c1, c2, "measure() must be pure");
}
#[test]
fn widget_min_is_constant() {
let widget = MyWidget::new("content");
let c1 = widget.measure(Size::new(100, 50));
let c2 = widget.measure(Size::new(200, 100));
assert_eq!(c1.min, c2.min, "min should not depend on available");
}Property Tests
See crates/ftui-widgets/src/measurable.rs for comprehensive property-based
tests using proptest.
The demo showcase ships a live example at
crates/ftui-demo-showcase/src/screens/intrinsic_sizing.rs. Run the full
gallery with cargo run -p ftui-demo-showcase and pick the intrinsic sizing
screen.
Related Work
- Paragraph:
crates/ftui-widgets/src/paragraph.rsimplementsMeasurableWidget. - Block:
crates/ftui-widgets/src/block.rscalculates chrome dimensions. - List:
crates/ftui-widgets/src/list.rsmeasures item content.
See Also
API-level reference for MeasurableWidget, SizeConstraints, and the
FitContent constraint.
How constraint-based layout composes with intrinsic sizing.
Flex & GridBreakpoint-driven layout that pairs with FitContent.
The 80+ widgets and which of them implement MeasurableWidget.