Building Complex Layouts
OSUI provides foundational primitives for rendering text and components, allowing you to compose them into complex layouts. While OSUI doesn't include a built-in layout engine (like flexbox or grid), it exposes the DrawContext and geometric types (Point, Area, Size) that enable you to manually position elements or build your own layout components.
The Rendering Pipeline
At its core, OSUI's rendering works by accumulating DrawInstructions into a DrawContext. The engine then takes this DrawContext and executes the instructions to draw to the terminal.
View: A component returns aView, which is essentially a closure that takes a mutableDrawContextand adds drawing instructions to it.DrawContext: This is the canvas for your component. It has anarea(the total space available to the current component) and anallocatedarea (the space currently used by drawing instructions within that component).DrawInstruction: The actual commands to draw, likeText,View(for child components), orChild(for nestedDrawContexts).
Core Rendering Primitives
These types, found in the osui::render module, are essential for manual layout.
-
PointRepresents a position
(x, y)in terminal coordinates.xis column,yis row.pub struct Point {
pub x: u16,
pub y: u16,
} -
SizeRepresents
widthandheightin terminal columns and rows.pub struct Size {
pub width: u16,
pub height: u16,
} -
AreaCombines
PointandSizeto define a rectangular region.pub struct Area {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
DrawContext: Your Drawing Canvas
Inside a View closure, you receive a mutable DrawContext. This is how you interact with the rendering system.
use osui::prelude::*;
use std::sync::Arc;
#[component]
fn MyCustomLayout(cx: &Arc<Context>, children: &Rsx) -> View {
let children_view = children.view(&cx); // Convert Rsx children to a View
Arc::new(move |ctx: &mut DrawContext| {
// `ctx.area` gives you the total available space for this component.
let available_width = ctx.area.width;
let available_height = ctx.area.height;
// --- Manual layout example: Two columns ---
// Allocate space for the left column
let left_col_area = ctx.allocate(
ctx.area.x,
ctx.area.y,
available_width / 2,
available_height,
);
// Draw some text in the left column
ctx.draw_text(
Point { x: left_col_area.x + 1, y: left_col_area.y + 1 },
"Left Panel",
);
ctx.draw_text(
Point { x: left_col_area.x + 1, y: left_col_area.y + 2 },
&format!("Available: {}x{}", left_col_area.width, left_col_area.height),
);
// Allocate space for the right column
let right_col_area = ctx.allocate(
ctx.area.x + available_width / 2, // Start x at half width
ctx.area.y,
available_width / 2,
available_height,
);
// Draw the children (passed to MyCustomLayout) into the right column
// This effectively "moves" the children's rendering into this specific area.
ctx.draw_view(
right_col_area, // Children will render relative to this new area
children_view.clone(),
);
// Draw more text in the right column
ctx.draw_text(
Point { x: right_col_area.x + 1, y: right_col_area.y + 1 },
"Right Panel (Children Area)",
);
})
}
#[component]
fn App(cx: &Arc<Context>) -> View {
rsx! {
MyCustomLayout {
rsx! {
"Hello from the child content!"
"This text should appear in the right panel."
}
}
}.view(&cx)
}
pub fn main() {
let engine = Console::new();
engine.run(App {}).expect("Failed to run app");
}
Key DrawContext Methods:
ctx.area: TheAreathat the current component has been allocated by its parent. All drawing coordinates are relative toctx.area.xandctx.area.y.ctx.allocate(x, y, width, height): This method marks a region withinctx.areaas "used". It takes coordinates relative toctx.area's top-left corner (0,0 of its own space) and returns a newArearepresenting the allocated sub-region. It also updatesctx.allocatedto be the union of all allocations so far within thisDrawContext.ctx.draw_text(point, text): Adds aTextinstruction.pointis relative toctx.area.ctx.draw_view(area, view): Adds aViewinstruction. This is how you tell the renderer to draw a child component (or anotherView) within a specificarea. Theareahere is also relative toctx.area. The child view will then receive thisareaas its ownctx.area.
Building a Simple Layout Component
The MyCustomLayout component above demonstrates a basic two-column layout. You can create more sophisticated layout components by:
- Calculating Sub-Regions: Based on
ctx.area.widthandctx.area.height, divide the space into logical sub-regions (e.g., header, footer, sidebar, main content). - Allocating Space: Use
ctx.allocate()to define these sub-regions. - Drawing Content:
- For static text or background elements, use
ctx.draw_text(). - For child components, call
child_rsx.view(&cx)to get theirView, and then usectx.draw_view(sub_area, child_view)to render them in their designated space.
- For static text or background elements, use
Tips for Layouts:
- Relative Positioning: Always think of
PointandAreacoordinates as being relative to thectx.areaof the currentViewbeing rendered. TheConsoleengine handles translating these relative coordinates to absolute terminal coordinates. - No Overlapping: Be mindful of overlapping areas. If you draw two things to the same
Point, the last one drawn will overwrite the first. OSUI does not automatically manage Z-ordering. - Responsive Design: Consider how your layouts will adapt to different terminal sizes. You can use
ctx.area.widthandctx.area.heightto make calculations dynamic. - Composition: Layout components can themselves be children of other layout components, allowing you to build complex nested structures.
While implementing a full layout system like CSS Flexbox is beyond the scope of OSUI's core, these primitives empower you to craft highly customized and visually rich terminal interfaces by manually managing space and component placement.