///|
/// Render TuiNode to crater Layout and CharBuffer
///|
/// Grid area placement info (package-visible for render_typed.mbt)
pub(all) struct GridAreaInfo {
column : Int
row : Int
column_span : Int
row_span : Int
}
///|
/// Render context holding styles and handlers
pub struct RenderContext {
styles : Map[String, @core.RenderStyle]
texts : Map[String, String]
handlers : Map[String, (TuiEvent) -> Unit]
roles : Map[String, @core.Role]
mut id_counter : Int
// Stack of grid area maps (for nested grids)
area_stack : Array[Map[String, GridAreaInfo]]
}
///|
/// Create a new render context
pub fn RenderContext::new() -> RenderContext {
{
styles: {},
texts: {},
handlers: {},
roles: {},
id_counter: 0,
area_stack: [],
}
}
///|
/// Parse grid-template-areas and build area map
fn parse_grid_template_areas(
areas : Array[String],
) -> Map[String, GridAreaInfo] {
let result : Map[String, GridAreaInfo] = {}
for row_idx, row_str in areas {
let cells = row_str.split(" ").filter(fn(s) { s.length() > 0 }).collect()
for col_idx, name_view in cells {
let name = name_view.to_owned()
if name == "." {
continue // Skip empty cells
}
match result.get(name) {
Some(info) => {
// Extend existing area
let new_col_span = col_idx + 1 - info.column + 1
let new_row_span = row_idx + 1 - info.row + 1
result[name] = {
column: info.column,
row: info.row,
column_span: if new_col_span > info.column_span {
new_col_span
} else {
info.column_span
},
row_span: if new_row_span > info.row_span {
new_row_span
} else {
info.row_span
},
}
}
None =>
// New area - 1-indexed
result[name] = {
column: col_idx + 1,
row: row_idx + 1,
column_span: 1,
row_span: 1,
}
}
}
}
result
}
///|
/// Generate a unique ID
fn RenderContext::gen_id(self : RenderContext) -> String {
self.id_counter = self.id_counter + 1
"v" + self.id_counter.to_string()
}
///|
/// Visual style accumulator (used during attribute processing)
priv struct VisualStyle {
mut fg : @core.Color?
mut bg : @core.Color?
mut bold : Bool
mut underline : Bool
mut border : @core.BorderChars?
mut border_color : @core.Color?
}
///|
fn VisualStyle::default() -> VisualStyle {
{
fg: None,
bg: None,
bold: false,
underline: false,
border: None,
border_color: None,
}
}
///|
/// Convert TuiNode to crater Node
pub fn to_crater_node(ctx : RenderContext, node : TuiNode) -> @node.Node {
match node {
@luna.Node::Text(content) => {
let id = ctx.gen_id()
ctx.texts[id] = content
let style = @style.Style::default()
@node.Node::with_measure(id, style, @core.text_measure_func(content))
}
@luna.Node::DynamicText(getter) => {
let id = ctx.gen_id()
let content = getter()
ctx.texts[id] = content
let style = @style.Style::default()
@node.Node::with_measure(id, style, @core.text_measure_func(content))
}
@luna.Node::Element(el) => convert_element(ctx, el)
@luna.Node::Fragment(children) => {
// Fragment becomes a column container
let id = ctx.gen_id()
let crater_children = children.map(fn(c) { to_crater_node(ctx, c) })
let style = @style.Style::default()
style.flex_direction = @values.FlexDirection::Column
@node.Node::new(id, style, crater_children)
}
@luna.Node::Show(condition~, child~) =>
if condition() {
to_crater_node(ctx, child())
} else {
// Empty node
let id = ctx.gen_id()
let style = @style.Style::default()
@node.Node::new(id, style, [])
}
@luna.Node::For(render~) => {
let items = render()
let id = ctx.gen_id()
let style = @style.Style::default()
style.flex_direction = @values.FlexDirection::Column
let crater_children = items.map(fn(item) { to_crater_node(ctx, item) })
@node.Node::new(id, style, crater_children)
}
@luna.Node::Component(render~) => to_crater_node(ctx, render())
// Ignore SSR-specific nodes
_ => {
let id = ctx.gen_id()
let style = @style.Style::default()
@node.Node::new(id, style, [])
}
}
}
///|
/// Convert VElement to crater Node (type-safe with TuiAttrValue)
fn convert_element(
ctx : RenderContext,
el : @luna.VElement[TuiEvent, TuiAttrValue],
) -> @node.Node {
// Get or generate ID and check for grid template areas
let mut id = ""
let mut grid_areas : Array[String]? = None
let mut grid_area_name : String? = None
for attr in el.attrs {
match attr {
("id", @luna.Attr::VStatic(TuiAttrValue::Id(v))) => id = v
("id", @luna.Attr::VDynamic(getter)) =>
match getter() {
TuiAttrValue::Id(v) => id = v
_ => ()
}
(
"grid_template_areas",
@luna.Attr::VStatic(TuiAttrValue::GridTemplateAreas(v)),
) => grid_areas = Some(v)
("grid_area", @luna.Attr::VStatic(TuiAttrValue::GridArea(v))) =>
grid_area_name = Some(v)
_ => ()
}
}
if id.length() == 0 {
id = ctx.gen_id()
}
// Create style
let style = @style.Style::default()
style.display = @values.Display::Flex
// Track visual style
let visual = VisualStyle::default()
// Process attributes using typed values
for attr in el.attrs {
let (_, value) = attr
match value {
@luna.Attr::VStatic(typed_value) => {
apply_a11y_attr(ctx, id, typed_value)
apply_attr(style, visual, typed_value)
}
@luna.Attr::VDynamic(getter) => {
let typed_value = getter()
apply_a11y_attr(ctx, id, typed_value)
apply_attr(style, visual, typed_value)
}
@luna.Attr::VHandler(handler) => ctx.handlers[id] = handler.get_callback()
}
}
// Resolve grid area from parent context
match grid_area_name {
Some(name) =>
if ctx.area_stack.length() > 0 {
let current_map = ctx.area_stack[ctx.area_stack.length() - 1]
match current_map.get(name) {
Some(info) => {
style.grid_column = {
start: @values.GridPlacement::Line(info.column),
end: @values.GridPlacement::Span(info.column_span),
}
style.grid_row = {
start: @values.GridPlacement::Line(info.row),
end: @values.GridPlacement::Span(info.row_span),
}
}
None => ()
}
}
None => ()
}
// Store style if not default
if visual.fg is Some(_) ||
visual.bg is Some(_) ||
visual.bold ||
visual.underline ||
visual.border is Some(_) {
ctx.styles[id] = {
fg: visual.fg.unwrap_or(@core.Color::white()),
bg: visual.bg.unwrap_or(@core.Color::transparent()),
bold: visual.bold,
underline: visual.underline,
border: visual.border,
border_fg: visual.border_color.unwrap_or(@core.Color::white()),
}
}
// Handle special tags
match el.tag {
"text" => {
// Text wrapper - extract text content from children
let mut text_content = ""
for child in el.children {
match child {
@luna.Node::Text(content) => text_content = content
@luna.Node::DynamicText(getter) => text_content = getter()
_ => ()
}
}
ctx.texts[id] = text_content
@node.Node::with_measure(id, style, @core.text_measure_func(text_content))
}
"spacer" => {
// Spacer has flex_grow by default
if style.flex_grow == 0.0 {
style.flex_grow = 1.0
}
@node.Node::new(id, style, [])
}
_ => {
// Push grid area map if this is a grid container
match grid_areas {
Some(areas) => ctx.area_stack.push(parse_grid_template_areas(areas))
None => ()
}
// Convert children
let crater_children = el.children.map(fn(c) { to_crater_node(ctx, c) })
// Pop grid area map
match grid_areas {
Some(_) => {
let _ = ctx.area_stack.pop()
}
None => ()
}
@node.Node::new(id, style, crater_children)
}
}
}
///|
/// Apply typed attribute value to style and visual properties
fn apply_attr(
style : @style.Style,
visual : VisualStyle,
attr : TuiAttrValue,
) -> Unit {
match attr {
TuiAttrValue::Direction(v) => style.flex_direction = v.to_flex_direction()
TuiAttrValue::Width(v) => style.width = v.to_dimension()
TuiAttrValue::Height(v) => style.height = v.to_dimension()
TuiAttrValue::MinWidth(v) => style.min_width = v.to_dimension()
TuiAttrValue::MinHeight(v) => style.min_height = v.to_dimension()
TuiAttrValue::FlexGrow(v) => style.flex_grow = v
TuiAttrValue::FlexShrink(v) => style.flex_shrink = v
TuiAttrValue::Gap(v) => {
style.row_gap = @values.Dimension::Length(v)
style.column_gap = @values.Dimension::Length(v)
}
// Display
TuiAttrValue::Display(v) => style.display = v.to_display()
// Grid container
TuiAttrValue::GridTemplateColumns(v) => style.grid_template_columns = v
TuiAttrValue::GridTemplateRows(v) => style.grid_template_rows = v
TuiAttrValue::GridAutoFlow(v) =>
style.grid_auto_flow = v.to_grid_auto_flow()
// Grid item
TuiAttrValue::GridColumn(v) =>
style.grid_column = {
start: @values.GridPlacement::Line(v),
end: @values.GridPlacement::Auto,
}
TuiAttrValue::GridRow(v) =>
style.grid_row = {
start: @values.GridPlacement::Line(v),
end: @values.GridPlacement::Auto,
}
TuiAttrValue::GridColumnSpan(v) =>
style.grid_column = {
start: style.grid_column.start,
end: @values.GridPlacement::Span(v),
}
TuiAttrValue::GridRowSpan(v) =>
style.grid_row = {
start: style.grid_row.start,
end: @values.GridPlacement::Span(v),
}
TuiAttrValue::Padding(v) => {
let len = @values.Dimension::Length(v)
style.padding = @values.Rect::new(len, len, len, len)
}
TuiAttrValue::PaddingX(v) => {
let len = @values.Dimension::Length(v)
let current = style.padding
style.padding = @values.Rect::new(current.top, len, current.bottom, len)
}
TuiAttrValue::PaddingY(v) => {
let len = @values.Dimension::Length(v)
let current = style.padding
style.padding = @values.Rect::new(len, current.right, len, current.left)
}
TuiAttrValue::Margin(v) => {
let len = @values.Dimension::Length(v)
style.margin = @values.Rect::new(len, len, len, len)
}
TuiAttrValue::MarginX(v) => {
let len = @values.Dimension::Length(v)
let current = style.margin
style.margin = @values.Rect::new(current.top, len, current.bottom, len)
}
TuiAttrValue::MarginY(v) => {
let len = @values.Dimension::Length(v)
let current = style.margin
style.margin = @values.Rect::new(len, current.right, len, current.left)
}
TuiAttrValue::Justify(v) => style.justify_content = v.to_alignment()
TuiAttrValue::Align(v) => style.align_items = v.to_alignment()
TuiAttrValue::Border(v) => {
visual.border = v.to_border_chars()
match v.to_border_chars() {
Some(_) => {
let one = @values.Dimension::Length(1.0)
style.border = @values.Rect::new(one, one, one, one)
}
None => ()
}
}
TuiAttrValue::BorderColor(v) => visual.border_color = Some(v.to_color())
TuiAttrValue::Fg(v) => visual.fg = Some(v.to_color())
TuiAttrValue::Bg(v) => visual.bg = Some(v.to_color())
TuiAttrValue::Bold(v) => visual.bold = v
TuiAttrValue::Underline(v) => visual.underline = v
TuiAttrValue::Id(_) => ()
// Accessibility: TabIndex is metadata, doesn't affect layout
TuiAttrValue::TabIndex(_) => ()
TuiAttrValue::Role(_) => ()
// Grid template areas: handled in convert_element for context passing
TuiAttrValue::GridTemplateAreas(_) => ()
// Grid area: resolved in convert_element_with_area
TuiAttrValue::GridArea(_) => ()
}
}
///|
fn apply_a11y_attr(
ctx : RenderContext,
id : String,
attr : TuiAttrValue,
) -> Unit {
match attr {
TuiAttrValue::Role(role) => ctx.roles[id] = role
_ => ()
}
}