use std::cell::RefCell;
use std::rc::Rc;
use rnk::core::Dimension;
use rnk::hooks::{HookContext, with_hooks};
use rnk::prelude::*;
#[derive(Clone, Debug)]
struct TodoItem {
id: usize,
text: String,
completed: bool,
created_at: String,
}
#[derive(Clone, Debug)]
struct AppState {
todos: Vec<TodoItem>,
selected_index: usize,
show_help: bool,
show_completed: bool,
status_message: String,
}
impl Default for AppState {
fn default() -> Self {
Self {
todos: vec![
TodoItem {
id: 1,
text: "Learn Rust".into(),
completed: true,
created_at: "2024-01-01".into(),
},
TodoItem {
id: 2,
text: "Build tink framework".into(),
completed: true,
created_at: "2024-01-02".into(),
},
TodoItem {
id: 3,
text: "Create complex example".into(),
completed: false,
created_at: "2024-01-03".into(),
},
TodoItem {
id: 4,
text: "Write documentation".into(),
completed: false,
created_at: "2024-01-04".into(),
},
TodoItem {
id: 5,
text: "Publish to crates.io".into(),
completed: false,
created_at: "2024-01-05".into(),
},
],
selected_index: 0,
show_help: false,
show_completed: true,
status_message: "Welcome to Tink Todo!".into(),
}
}
}
fn render_header() -> Element {
Box::new()
.width(Dimension::Percent(100.0))
.padding_x(2.0)
.padding_y(1.0)
.border_style(BorderStyle::Round)
.border_color(Color::Cyan)
.flex_direction(FlexDirection::Column)
.align_items(AlignItems::Center)
.child(
Text::new("========================================")
.color(Color::Cyan)
.into_element(),
)
.child(
Text::new(" TINK TODO APPLICATION ")
.color(Color::White)
.bold()
.into_element(),
)
.child(
Text::new("========================================")
.color(Color::Cyan)
.into_element(),
)
.into_element()
}
fn render_stats(state: &AppState) -> Element {
let total = state.todos.len();
let completed = state.todos.iter().filter(|t| t.completed).count();
let pending = total - completed;
let percentage = (completed * 100).checked_div(total).unwrap_or(0);
let bar_width = 20;
let filled = (bar_width * completed) / total.max(1);
let empty = bar_width - filled;
let progress_bar = format!(
"[{}{}] {}%",
"=".repeat(filled),
"-".repeat(empty),
percentage
);
Box::new()
.border_style(BorderStyle::Round)
.border_top_color(Color::Green)
.border_bottom_color(Color::Green)
.border_left_color(Color::Yellow)
.border_right_color(Color::Yellow)
.padding(1)
.margin_bottom(1.0)
.flex_direction(FlexDirection::Column)
.child(
Text::new(" Statistics ")
.color(Color::Yellow)
.bold()
.underline()
.into_element(),
)
.child(Newline::new().into_element())
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.child(Text::new("Total: ").color(Color::White).into_element())
.child(
Text::new(format!("{}", total))
.color(Color::Cyan)
.bold()
.into_element(),
)
.into_element(),
)
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.child(Text::new("Completed: ").color(Color::White).into_element())
.child(
Text::new(format!("{}", completed))
.color(Color::Green)
.bold()
.into_element(),
)
.into_element(),
)
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.child(Text::new("Pending: ").color(Color::White).into_element())
.child(
Text::new(format!("{}", pending))
.color(Color::Red)
.bold()
.into_element(),
)
.into_element(),
)
.child(Newline::new().into_element())
.child(
Text::new(progress_bar)
.color(if percentage >= 80 {
Color::Green
} else if percentage >= 50 {
Color::Yellow
} else {
Color::Red
})
.into_element(),
)
.into_element()
}
fn render_todo_item(item: &TodoItem, is_selected: bool, index: usize) -> Element {
let checkbox = if item.completed { "[x]" } else { "[ ]" };
let status_color = if item.completed {
Color::Green
} else {
Color::White
};
let mut container = Box::new()
.flex_direction(FlexDirection::Row)
.padding_x(1.0)
.width(Dimension::Percent(100.0));
if is_selected {
container = container
.background(Color::Ansi256(236))
.border_style(BorderStyle::Single)
.border_color(Color::Cyan);
}
let mut text_component = Text::new(&item.text).color(status_color);
if item.completed {
text_component = text_component.italic().strikethrough();
}
container
.child(
Text::new(format!("{:2}. ", index + 1))
.color(Color::Ansi256(240))
.into_element(),
)
.child(
Text::new(checkbox)
.color(if item.completed {
Color::Green
} else {
Color::Ansi256(240)
})
.bold()
.into_element(),
)
.child(Text::new(" ").into_element())
.child(text_component.into_element())
.child(Spacer::new().into_element())
.child(
Text::new(&item.created_at)
.color(Color::Ansi256(240))
.into_element(),
)
.into_element()
}
fn render_todo_list(state: &AppState) -> Element {
let filtered_todos: Vec<_> = if state.show_completed {
state.todos.iter().collect()
} else {
state.todos.iter().filter(|t| !t.completed).collect()
};
let mut list = Box::new()
.flex_direction(FlexDirection::Column)
.border_style(BorderStyle::Round)
.border_color(Color::Blue)
.padding(1)
.flex_grow(1.0)
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.margin_bottom(1.0)
.child(
Text::new(" Todo List ")
.color(Color::Blue)
.bold()
.underline()
.into_element(),
)
.child(Spacer::new().into_element())
.child(
Text::new(if state.show_completed {
"[Show All]"
} else {
"[Hide Done]"
})
.color(Color::Ansi256(240))
.into_element(),
)
.into_element(),
);
if filtered_todos.is_empty() {
list = list.child(
Box::new()
.justify_content(JustifyContent::Center)
.padding(2)
.child(
Text::new("No todos yet! Press 'a' to add one.")
.color(Color::Ansi256(240))
.italic()
.into_element(),
)
.into_element(),
);
} else {
for (display_idx, item) in filtered_todos.iter().enumerate() {
let actual_idx = state
.todos
.iter()
.position(|t| t.id == item.id)
.unwrap_or(0);
let is_selected = actual_idx == state.selected_index;
list = list.child(render_todo_item(item, is_selected, display_idx));
}
}
list.into_element()
}
fn render_status_bar(state: &AppState) -> Element {
Box::new()
.width(Dimension::Percent(100.0))
.flex_direction(FlexDirection::Row)
.padding_x(1.0)
.background(Color::Ansi256(236))
.child(
Text::new(&state.status_message)
.color(Color::White)
.into_element(),
)
.child(Spacer::new().into_element())
.child(
Text::new("Press 'h' for help | 'q' to quit")
.color(Color::Ansi256(240))
.into_element(),
)
.into_element()
}
fn render_help_popup(show: bool) -> Element {
if !show {
return Box::new().hidden().into_element();
}
Box::new()
.position_absolute()
.top(5.0)
.left(10.0)
.width(50)
.border_style(BorderStyle::Double)
.border_color(Color::Magenta)
.background(Color::Ansi256(234))
.padding(2)
.flex_direction(FlexDirection::Column)
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.child(Spacer::new().into_element())
.child(
Text::new("Keyboard Shortcuts")
.color(Color::Magenta)
.bold()
.into_element(),
)
.child(Spacer::new().into_element())
.into_element(),
)
.child(Newline::new().into_element())
.child(render_help_row("j / Down", "Move down"))
.child(render_help_row("k / Up", "Move up"))
.child(render_help_row("Enter", "Toggle completion"))
.child(render_help_row("a", "Add new todo"))
.child(render_help_row("d", "Delete selected"))
.child(render_help_row("c", "Toggle show completed"))
.child(render_help_row("h", "Toggle this help"))
.child(render_help_row("q / Esc", "Quit application"))
.child(Newline::new().into_element())
.child(
Box::new()
.justify_content(JustifyContent::Center)
.child(
Text::new("Press 'h' to close")
.color(Color::Ansi256(240))
.italic()
.into_element(),
)
.into_element(),
)
.into_element()
}
fn render_help_row(key: &str, desc: &str) -> Element {
Box::new()
.flex_direction(FlexDirection::Row)
.padding_x(1.0)
.child(
Box::new()
.width(12)
.child(Text::new(key).color(Color::Cyan).bold().into_element())
.into_element(),
)
.child(Text::new(desc).color(Color::White).into_element())
.into_element()
}
fn render_quick_actions() -> Element {
Box::new()
.border_style(BorderStyle::Round)
.border_color(Color::Magenta)
.padding(1)
.flex_direction(FlexDirection::Column)
.child(
Text::new(" Quick Actions ")
.color(Color::Magenta)
.bold()
.underline()
.into_element(),
)
.child(Newline::new().into_element())
.child(
Text::new(" [a] Add Todo")
.color(Color::Green)
.into_element(),
)
.child(Text::new(" [d] Delete").color(Color::Red).into_element())
.child(
Text::new(" [c] Filter")
.color(Color::Yellow)
.into_element(),
)
.child(Text::new(" [h] Help").color(Color::Cyan).into_element())
.into_element()
}
fn render_transform_demo() -> Element {
Transform::new(|s| s.to_uppercase())
.child(
Text::new("this text is transformed to uppercase")
.color(Color::Ansi256(245))
.into_element(),
)
.into_element()
}
fn render_app(state: &AppState) -> Element {
Box::new()
.width(Dimension::Percent(100.0))
.height(Dimension::Percent(100.0))
.flex_direction(FlexDirection::Column)
.padding(1)
.child(render_header())
.child(Newline::new().into_element())
.child(
Box::new()
.flex_direction(FlexDirection::Row)
.flex_grow(1.0)
.child(
Box::new()
.width(30)
.flex_direction(FlexDirection::Column)
.child(render_stats(state))
.child(render_quick_actions())
.child(Newline::new().into_element())
.child(render_transform_demo())
.into_element(),
)
.child(Box::new().width(2).into_element())
.child(
Box::new()
.flex_grow(1.0)
.flex_direction(FlexDirection::Column)
.child(render_todo_list(state))
.into_element(),
)
.into_element(),
)
.child(render_status_bar(state))
.child(render_help_popup(state.show_help))
.into_element()
}
fn render_static_log(messages: &[String]) -> Element {
Static::new(messages.to_vec(), |msg, i| {
Text::new(format!("[LOG {}] {}", i + 1, msg))
.color(Color::Ansi256(240))
.into_element()
})
.into_element()
}
fn main() {
println!("\x1b[2J\x1b[H"); println!("Tink Todo App - Comprehensive Demo\n");
let state = AppState::default();
let action_log = vec![
"Application started".to_string(),
"Loaded 5 todos from memory".to_string(),
];
let ctx = Rc::new(RefCell::new(HookContext::new()));
let element = with_hooks(ctx.clone(), || {
let app_state = use_signal(|| state.clone());
let log = use_signal(|| action_log.clone());
Box::new()
.flex_direction(FlexDirection::Column)
.child(render_static_log(&log.get()))
.child(render_app(&app_state.get()))
.into_element()
});
let output = rnk::render_to_string_auto(&element);
print!("{}", output);
println!("\n\n--- Demo Complete ---");
println!("This example demonstrates ALL tink features:");
println!(" - Flexbox layout (row/column, justify, align, gap)");
println!(" - Styled text (colors, bold, italic, underline, strikethrough)");
println!(" - Borders with per-side colors (Round, Single, Double)");
println!(" - use_signal for reactive state management");
println!(" - Static component for persistent output");
println!(" - Transform component for text transformation");
println!(" - position: absolute for popup overlay");
println!(" - display: none for conditional rendering");
println!(" - Spacer and Newline components");
println!(" - Background colors");
println!(" - Dimension::Percent for responsive sizing");
}