///|
/// Component builders for TUI
/// Provides ink-like API for building reactive UI trees
///|
/// ARIA role for accessibility
/// Based on WAI-ARIA 1.2 roles
pub(all) enum Role {
// Widget roles
Button
Checkbox
Link
Menuitem
Option
Radio
Slider
Switch
Tab
Textbox
// Composite widget roles
Listbox
Menu
Menubar
Radiogroup
Tablist
Tree
Treegrid
// Document structure roles
Article
Cell
Columnheader
Definition
Directory
Document
Group
Heading
Img
List
Listitem
Row
Rowgroup
Rowheader
Separator
Table
Term
// Landmark roles
Banner
Complementary
Contentinfo
Form
Main
Navigation
Region
Search
// Live region roles
Alert
Log
Marquee
Status
Timer
// Window roles
Alertdialog
Dialog
// Custom role
Custom(String)
} derive(Debug, Eq)
///|
fn[T : Debug] show_debug(value : T, logger : &Logger) -> Unit {
value.to_repr().output(logger)
}
///|
pub impl Show for Role with fn output(self : Role, logger : &Logger) -> Unit {
show_debug(self, logger)
}
///|
/// Role map (component ID -> role)
pub(all) struct RoleMap(Map[String, Role])
///|
pub fn RoleMap::new() -> RoleMap {
RoleMap(Map([]))
}
///|
pub fn RoleMap::set(self : RoleMap, id : String, role : Role) -> Unit {
self.0.set(id, role)
}
///|
pub fn RoleMap::get(self : RoleMap, id : String) -> Role? {
self.0.get(id)
}
///|
pub fn RoleMap::iter(self : RoleMap) -> Iter[(String, Role)] {
self.0.iter()
}
///|
/// Click handler type for component-level event handling
pub(all) struct ClickHandler(() -> Unit)
///|
/// Create a new click handler
pub fn ClickHandler::new(handler : () -> Unit) -> ClickHandler {
ClickHandler(handler)
}
///|
/// Invoke click handler
pub fn ClickHandler::invoke(self : ClickHandler) -> Unit {
let ClickHandler(handler) = self
handler()
}
///|
/// Click handlers map (component ID -> handler)
pub(all) struct ClickHandlers(Map[String, ClickHandler])
///|
pub fn ClickHandlers::new() -> ClickHandlers {
ClickHandlers(Map([]))
}
///|
pub fn ClickHandlers::set(
self : ClickHandlers,
id : String,
handler : ClickHandler,
) -> Unit {
self.0.set(id, handler)
}
///|
pub fn ClickHandlers::get(self : ClickHandlers, id : String) -> ClickHandler? {
self.0.get(id)
}
///|
pub fn ClickHandlers::iter(
self : ClickHandlers,
) -> Iter[(String, ClickHandler)] {
self.0.iter()
}
///|
/// Component represents a renderable UI element with its layout, style, and content
/// Note: Fields are internal implementation details. Use accessor methods for external access.
pub struct Component {
node : @node.Node
styles : RenderStyleMap
texts : TextContentMap
click_handlers : ClickHandlers
roles : RoleMap
}
// === Accessor methods for Component ===
///|
/// Get the component's ID
pub fn Component::id(self : Component) -> String {
self.node.id
}
///|
/// Get the layout node (internal use)
pub fn Component::get_node(self : Component) -> @node.Node {
self.node
}
///|
/// Get the styles map (internal use)
pub fn Component::get_styles(self : Component) -> RenderStyleMap {
self.styles
}
///|
/// Get the text content map (internal use)
pub fn Component::get_texts(self : Component) -> TextContentMap {
self.texts
}
///|
/// Get click handlers (internal use)
pub fn Component::get_click_handlers(self : Component) -> ClickHandlers {
self.click_handlers
}
///|
/// Get roles map (internal use)
pub fn Component::get_roles(self : Component) -> RoleMap {
self.roles
}
///|
/// Get text content by ID
pub fn Component::get_text(self : Component, id : String) -> String? {
self.texts.get(id)
}
///|
/// Check if component has a specific role
pub fn Component::get_role(self : Component, id : String) -> Role? {
self.roles.get(id)
}
// Global ID counter for unique component IDs
///|
let component_id_counter : Ref[Int] = { val: 0 }
///|
fn next_id() -> String {
let id = component_id_counter.val
component_id_counter.val = id + 1
"c" + id.to_string()
}
///|
/// Merge two components (for combining children)
fn merge_components(
comps : Array[Component],
) -> (RenderStyleMap, TextContentMap, ClickHandlers, RoleMap) {
let styles = RenderStyleMap::new()
let texts = TextContentMap::new()
let click_handlers = ClickHandlers::new()
let roles = RoleMap::new()
for comp in comps {
// Copy styles
for entry in comp.styles.0 {
styles.set(entry.0, entry.1)
}
// Copy texts
for entry in comp.texts.0 {
texts.set(entry.0, entry.1)
}
// Copy click handlers
for entry in comp.click_handlers.iter() {
click_handlers.set(entry.0, entry.1)
}
// Copy roles
for entry in comp.roles.iter() {
roles.set(entry.0, entry.1)
}
}
(styles, texts, click_handlers, roles)
}
///|
/// Create a box component
pub fn box(
children : Array[Component],
// ID
id? : String = "",
// Layout
width? : @values.Dimension = @values.Dimension::Auto,
height? : @values.Dimension = @values.Dimension::Auto,
min_width? : @values.Dimension = @values.Dimension::Auto,
min_height? : @values.Dimension = @values.Dimension::Auto,
max_width? : @values.Dimension = @values.Dimension::Auto,
max_height? : @values.Dimension = @values.Dimension::Auto,
flex_grow? : Double = 0.0,
flex_shrink? : Double = 1.0,
flex_basis? : @values.Dimension = @values.Dimension::Auto,
flex_direction? : @values.FlexDirection = @values.FlexDirection::Row,
flex_wrap? : @values.FlexWrap = @values.FlexWrap::NoWrap,
justify_content? : @values.Alignment = @values.Alignment::FlexStart,
align_items? : @values.Alignment = @values.Alignment::Stretch,
align_content? : @values.Alignment = @values.Alignment::Stretch,
gap? : Double = 0.0,
padding? : Double = 0.0,
padding_x? : Double = 0.0,
padding_y? : Double = 0.0,
margin? : Double = 0.0,
margin_x? : Double = 0.0,
margin_y? : Double = 0.0,
// Position (for overlay support)
position? : @values.Position = @values.Position::Static,
inset_top? : @values.Dimension = @values.Dimension::Auto,
inset_right? : @values.Dimension = @values.Dimension::Auto,
inset_bottom? : @values.Dimension = @values.Dimension::Auto,
inset_left? : @values.Dimension = @values.Dimension::Auto,
// Style
border? : BorderChars? = None,
border_color? : Color = Color::white(),
bg? : Color = Color::transparent(),
fg? : Color = Color::white(),
// Event handler
on_click? : (() -> Unit)? = None,
// Accessibility
role? : Role? = None,
) -> Component {
let actual_id = if id == "" { next_id() } else { id }
// Merge children's styles, texts, handlers, and roles
let (styles, texts, click_handlers, roles) = merge_components(children)
// Register click handler if provided
match on_click {
Some(handler) => click_handlers.set(actual_id, ClickHandler::new(handler))
None => ()
}
// Register role if provided
match role {
Some(r) => roles.set(actual_id, r)
None => ()
}
// Calculate padding (with x/y overrides)
let pad_left = if padding_x > 0.0 { padding_x } else { padding }
let pad_right = pad_left
let pad_top = if padding_y > 0.0 { padding_y } else { padding }
let pad_bottom = pad_top
// Calculate margin (with x/y overrides)
let margin_left = if margin_x > 0.0 { margin_x } else { margin }
let margin_right = margin_left
let margin_top = if margin_y > 0.0 { margin_y } else { margin }
let margin_bottom = margin_top
// Build style based on default with overrides
let style = @style.Style::default()
style.display = @values.Display::Flex
style.flex_direction = flex_direction
style.flex_wrap = flex_wrap
style.justify_content = justify_content
style.align_items = align_items
style.align_content = align_content
style.flex_grow = flex_grow
style.flex_shrink = flex_shrink
style.flex_basis = flex_basis
style.width = width
style.height = height
style.min_width = min_width
style.min_height = min_height
style.max_width = max_width
style.max_height = max_height
style.row_gap = @values.Dimension::Length(gap)
style.column_gap = @values.Dimension::Length(gap)
style.padding = @values.Rect::new(
@values.Dimension::Length(pad_top),
@values.Dimension::Length(pad_right),
@values.Dimension::Length(pad_bottom),
@values.Dimension::Length(pad_left),
)
style.margin = @values.Rect::new(
@values.Dimension::Length(margin_top),
@values.Dimension::Length(margin_right),
@values.Dimension::Length(margin_bottom),
@values.Dimension::Length(margin_left),
)
// Add border padding if border is present
if border is Some(_) {
style.border = @values.Rect::new(
@values.Dimension::Length(1.0),
@values.Dimension::Length(1.0),
@values.Dimension::Length(1.0),
@values.Dimension::Length(1.0),
)
}
// Position and inset for overlay support
style.position = position
style.inset = @values.Rect::new(
inset_top, inset_right, inset_bottom, inset_left,
)
// Build node
let child_nodes = children.map(fn(c) { c.node })
let node = @node.Node::new(actual_id, style, child_nodes)
// Add this box's style
styles.set(actual_id, {
fg,
bg,
bold: false,
underline: false,
border,
border_fg: border_color,
})
{ node, styles, texts, click_handlers, roles }
}
///|
/// Create a text component
pub fn text(
content : String,
fg? : Color = Color::white(),
bg? : Color = Color::transparent(),
bold? : Bool = false,
underline? : Bool = false,
) -> Component {
let id = next_id()
// Build style for text measurement
let style = @style.Style::default()
// Build node with measure function
let node = @node.Node::with_measure(id, style, text_measure_func(content))
// Create style and text maps
let styles = RenderStyleMap::new()
let texts = TextContentMap::new()
styles.set(id, {
fg,
bg,
bold,
underline,
border: None,
border_fg: Color::white(),
})
texts.set(id, content)
{
node,
styles,
texts,
click_handlers: ClickHandlers::new(),
roles: RoleMap::new(),
}
}
///|
/// Create a styled text (alias for text with labeled arguments)
pub fn styled_text(
content : String,
fg? : Color = Color::white(),
bg? : Color = Color::transparent(),
bold? : Bool = false,
underline? : Bool = false,
) -> Component {
text(content, fg~, bg~, bold~, underline~)
}
///|
/// Create a box with text content that wraps within the box's bounds
/// Unlike text(), this renders the text directly within the box's layout dimensions
pub fn text_box(
content : String,
// ID
id? : String = "",
// Size
width? : @values.Dimension = @values.Dimension::Auto,
height? : @values.Dimension = @values.Dimension::Auto,
min_width? : @values.Dimension = @values.Dimension::Auto,
min_height? : @values.Dimension = @values.Dimension::Auto,
max_width? : @values.Dimension = @values.Dimension::Auto,
max_height? : @values.Dimension = @values.Dimension::Auto,
// Flex properties
flex_grow? : Double = 0.0,
flex_shrink? : Double = 1.0,
flex_basis? : @values.Dimension = @values.Dimension::Auto,
// Padding & Margin
padding? : Double = 0.0,
padding_x? : Double = 0.0,
padding_y? : Double = 0.0,
margin? : Double = 0.0,
margin_x? : Double = 0.0,
margin_y? : Double = 0.0,
// Border
border? : BorderChars? = None,
border_color? : Color = Color::white(),
// Style
bg? : Color = Color::transparent(),
fg? : Color = Color::white(),
bold? : Bool = false,
underline? : Bool = false,
) -> Component {
let actual_id = if id == "" { next_id() } else { id }
// Build style
let style = @style.Style::default()
style.width = width
style.height = height
style.min_width = min_width
style.min_height = min_height
style.max_width = max_width
style.max_height = max_height
style.flex_grow = flex_grow
style.flex_shrink = flex_shrink
style.flex_basis = flex_basis
// Padding
let pad = padding
let pad_x = if padding_x > 0.0 { padding_x } else { pad }
let pad_y = if padding_y > 0.0 { padding_y } else { pad }
style.padding = @values.Rect::new(
@values.Dimension::Length(pad_y),
@values.Dimension::Length(pad_x),
@values.Dimension::Length(pad_y),
@values.Dimension::Length(pad_x),
)
// Margin
let mar = margin
let mar_x = if margin_x > 0.0 { margin_x } else { mar }
let mar_y = if margin_y > 0.0 { margin_y } else { mar }
style.margin = @values.Rect::new(
@values.Dimension::Length(mar_y),
@values.Dimension::Length(mar_x),
@values.Dimension::Length(mar_y),
@values.Dimension::Length(mar_x),
)
// Build node (no children, text rendered directly in this box)
let node = @node.Node::new(actual_id, style, [])
// Create style and text maps
let styles = RenderStyleMap::new()
let texts = TextContentMap::new()
styles.set(actual_id, {
fg,
bg,
bold,
underline,
border,
border_fg: border_color,
})
texts.set(actual_id, content)
{
node,
styles,
texts,
click_handlers: ClickHandlers::new(),
roles: RoleMap::new(),
}
}
///|
/// Spacer component - flexible empty space
pub fn spacer(flex_grow? : Double = 1.0) -> Component {
let id = next_id()
let style = @style.Style::default()
style.flex_grow = flex_grow
let node = @node.Node::new(id, style, [])
{
node,
styles: RenderStyleMap::new(),
texts: TextContentMap::new(),
click_handlers: ClickHandlers::new(),
roles: RoleMap::new(),
}
}