use std::rc::Rc;
pub type Callback = Rc<dyn Fn()>;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Justify {
#[default]
Start,
End,
Center,
SpaceBetween,
SpaceAround,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Align {
Start,
End,
Center,
#[default]
Stretch,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LayoutMode {
#[default]
Flex,
}
pub type SelectCallback = Rc<dyn Fn(usize)>;
pub type ChangeCallback = Rc<dyn Fn(String)>;
pub type ToggleCallback = Rc<dyn Fn(bool)>;
pub type CursorChangeCallback = Rc<dyn Fn(usize, usize)>;
pub type CursorPosCallback = Rc<dyn Fn(usize)>;
pub type TreePath = Vec<usize>;
pub type TreeSelectCallback = Rc<dyn Fn(TreePath)>;
pub type TreeActivateCallback = Rc<dyn Fn(TreePath)>;
pub type SortCallback = Rc<dyn Fn(usize, bool)>;
pub type RowActivateCallback = Rc<dyn Fn(usize)>;
pub type CommandCallback = Rc<dyn Fn(&'static str)>;
pub type CanvasDrawCallback = Rc<dyn Fn(&mut crate::canvas::DrawContext)>;
#[derive(Clone)]
pub enum View {
Text(TextNode),
VStack(VStackNode),
HStack(HStackNode),
Button(ButtonNode),
Box(BoxNode),
Spacer(SpacerNode),
List(ListNode),
TextInput(TextInputNode),
TextArea(TextAreaNode),
Checkbox(CheckboxNode),
RadioGroup(RadioGroupNode),
Modal(ModalNode),
Split(SplitNode),
Tabs(TabsNode),
Tree(TreeNode),
Table(TableNode),
ProgressBar(ProgressBarNode),
StatusBar(StatusBarNode),
CommandPalette(CommandPaletteNode),
MenuBar(MenuBarNode),
ToastContainer(ToastContainerNode),
Form(FormNode),
FormField(FormFieldNode),
Canvas(CanvasNode),
Image(ImageNode),
Terminal(TerminalNode),
Empty,
}
impl std::fmt::Debug for View {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
View::Text(n) => f.debug_tuple("Text").field(n).finish(),
View::VStack(n) => f.debug_tuple("VStack").field(n).finish(),
View::HStack(n) => f.debug_tuple("HStack").field(n).finish(),
View::Button(n) => f
.debug_struct("Button")
.field("label", &n.label)
.field("on_press", &"<callback>")
.finish(),
View::Box(n) => f.debug_tuple("Box").field(n).finish(),
View::Spacer(n) => f.debug_tuple("Spacer").field(n).finish(),
View::List(n) => f
.debug_struct("List")
.field("items", &n.items.len())
.field("selected", &n.selected)
.finish(),
View::TextInput(n) => f
.debug_struct("TextInput")
.field("value", &n.value)
.finish(),
View::TextArea(n) => f
.debug_struct("TextArea")
.field("value", &n.value)
.field("cursor", &(n.cursor_line, n.cursor_col))
.finish(),
View::Checkbox(n) => f
.debug_struct("Checkbox")
.field("checked", &n.checked)
.field("label", &n.label)
.finish(),
View::RadioGroup(n) => f
.debug_struct("RadioGroup")
.field("selected", &n.selected)
.field("options", &n.options)
.finish(),
View::Modal(n) => f
.debug_struct("Modal")
.field("visible", &n.visible)
.field("title", &n.title)
.finish(),
View::Split(n) => f
.debug_struct("Split")
.field("orientation", &n.orientation)
.field("ratio", &n.ratio)
.finish(),
View::Tabs(n) => f
.debug_struct("Tabs")
.field("tabs", &n.tabs)
.field("active", &n.active)
.finish(),
View::Tree(n) => f
.debug_struct("Tree")
.field("items", &n.items.len())
.field("selected", &n.selected)
.finish(),
View::Table(n) => f
.debug_struct("Table")
.field("columns", &n.columns.len())
.field("rows", &n.rows.len())
.field("selected", &n.selected)
.finish(),
View::ProgressBar(n) => f
.debug_struct("ProgressBar")
.field("value", &n.value)
.field("label", &n.label)
.finish(),
View::StatusBar(n) => f
.debug_struct("StatusBar")
.field("left", &n.left)
.field("center", &n.center)
.field("right", &n.right)
.finish(),
View::CommandPalette(n) => f
.debug_struct("CommandPalette")
.field("visible", &n.visible)
.field("query", &n.query)
.field("selected", &n.selected)
.finish(),
View::MenuBar(n) => f
.debug_struct("MenuBar")
.field("menus", &n.menus.len())
.field("active_menu", &n.active_menu)
.finish(),
View::ToastContainer(n) => f
.debug_struct("ToastContainer")
.field("toasts", &n.toasts.len())
.finish(),
View::Form(n) => f
.debug_struct("Form")
.field("children", &n.children.len())
.finish(),
View::FormField(n) => f
.debug_struct("FormField")
.field("name", &n.name)
.field("label", &n.label)
.finish(),
View::Canvas(n) => f
.debug_struct("Canvas")
.field("width", &n.pixel_width)
.field("height", &n.pixel_height)
.finish(),
View::Image(n) => f
.debug_struct("Image")
.field("has_data", &n.source.is_some())
.finish(),
View::Terminal(n) => f
.debug_struct("Terminal")
.field("rows", &n.rows)
.field("cols", &n.cols)
.field("border", &n.border)
.finish(),
View::Empty => write!(f, "Empty"),
}
}
}
impl View {
pub fn text(content: impl Into<String>) -> Self {
View::Text(TextNode {
content: content.into(),
color: None,
bg_color: None,
bold: false,
italic: false,
underline: false,
dim: false,
})
}
pub fn styled_text(content: impl Into<String>) -> TextBuilder {
TextBuilder::new(content)
}
pub fn vstack() -> VStackBuilder {
VStackBuilder::new()
}
pub fn hstack() -> HStackBuilder {
HStackBuilder::new()
}
pub fn button() -> ButtonBuilder {
ButtonBuilder::new()
}
pub fn boxed() -> BoxBuilder {
BoxBuilder::new()
}
pub fn spacer() -> Self {
View::Spacer(SpacerNode { flex: 1, height: 0 })
}
pub fn spacer_flex(flex: u16) -> Self {
View::Spacer(SpacerNode { flex, height: 0 })
}
pub fn gap(height: u16) -> Self {
View::Spacer(SpacerNode { flex: 0, height })
}
pub fn list() -> ListBuilder {
ListBuilder::new()
}
pub fn text_input() -> TextInputBuilder {
TextInputBuilder::new()
}
pub fn checkbox() -> CheckboxBuilder {
CheckboxBuilder::new()
}
pub fn radio_group() -> RadioGroupBuilder {
RadioGroupBuilder::new()
}
pub fn text_area() -> TextAreaBuilder {
TextAreaBuilder::new()
}
pub fn modal() -> ModalBuilder {
ModalBuilder::new()
}
pub fn split() -> SplitBuilder {
SplitBuilder::new()
}
pub fn tabs() -> TabsBuilder {
TabsBuilder::new()
}
pub fn tree() -> TreeBuilder {
TreeBuilder::new()
}
pub fn table() -> TableBuilder {
TableBuilder::new()
}
pub fn progress_bar() -> ProgressBarBuilder {
ProgressBarBuilder::new()
}
pub fn status_bar() -> StatusBarBuilder {
StatusBarBuilder::new()
}
pub fn command_palette() -> CommandPaletteBuilder {
CommandPaletteBuilder::new()
}
pub fn menu_bar() -> MenuBarBuilder {
MenuBarBuilder::new()
}
pub fn toast_container() -> ToastContainerBuilder {
ToastContainerBuilder::new()
}
pub fn form() -> FormBuilder {
FormBuilder::new()
}
pub fn form_field(name: impl Into<String>) -> FormFieldBuilder {
FormFieldBuilder::new(name)
}
pub fn canvas() -> CanvasBuilder {
CanvasBuilder::new()
}
pub fn image() -> ImageBuilder {
ImageBuilder::new()
}
pub fn terminal() -> TerminalBuilder {
TerminalBuilder::new()
}
pub fn empty() -> Self {
View::Empty
}
pub fn is_focusable(&self) -> bool {
match self {
View::Button(_) => true,
View::Box(node) => node.scroll,
View::List(_) => true,
View::TextInput(_) => true,
View::TextArea(_) => true,
View::Checkbox(_) => true,
View::RadioGroup(_) => true, View::Split(_) => false, View::Tabs(_) => true, View::Tree(_) => true, View::Table(_) => true, View::CommandPalette(_) => true, View::MenuBar(_) => true, View::FormField(_) => true, View::Terminal(_) => true, _ => false,
}
}
pub fn flex(&self) -> u16 {
match self {
View::Box(n) => n.flex,
View::Spacer(n) => n.flex,
_ => 0,
}
}
pub fn min_height(&self) -> Option<u16> {
match self {
View::Box(n) => n.min_height,
_ => None,
}
}
pub fn max_height(&self) -> Option<u16> {
match self {
View::Box(n) => n.max_height,
_ => None,
}
}
pub fn min_width(&self) -> Option<u16> {
match self {
View::Box(n) => n.min_width,
_ => None,
}
}
pub fn max_width(&self) -> Option<u16> {
match self {
View::Box(n) => n.max_width,
_ => None,
}
}
pub fn intrinsic_height(&self) -> Option<u16> {
match self {
View::Text(n) => Some(n.content.lines().count().max(1) as u16),
View::Button(_) => Some(1),
View::Box(n) => {
let border = if n.border { 2 } else { 0 };
let padding = n.padding * 2;
let inner = n
.child
.as_ref()
.and_then(|c| c.intrinsic_height())
.unwrap_or(0);
Some(inner + border + padding)
}
View::VStack(n) => {
if n.children.is_empty() {
return Some(0);
}
let spacing = if n.children.len() > 1 {
n.spacing * (n.children.len() as u16 - 1)
} else {
0
};
let children_height: u16 =
n.children.iter().filter_map(|c| c.intrinsic_height()).sum();
Some(children_height + spacing)
}
View::HStack(n) => {
n.children
.iter()
.filter_map(|c| c.intrinsic_height())
.max()
.or(Some(1))
}
View::List(n) => Some(n.items.len().max(1) as u16),
View::TextInput(_) => Some(1),
View::TextArea(n) => Some(n.rows),
View::Checkbox(_) => Some(1),
View::RadioGroup(n) => Some(n.options.len() as u16), View::Modal(_) => None, View::Spacer(n) => {
if n.flex == 0 {
Some(n.height) } else {
None }
}
View::Split(_) => None, View::Tabs(_) => None, View::Tree(_) => None, View::Table(_) => None, View::ProgressBar(_) => Some(1), View::StatusBar(_) => Some(1), View::CommandPalette(_) => None, View::MenuBar(_) => Some(1), View::ToastContainer(_) => None, View::Form(n) => {
let children_height: u16 =
n.children.iter().filter_map(|c| c.intrinsic_height()).sum();
Some(children_height)
}
View::FormField(n) => {
let base_height = 2u16; let error_height = if n.error.is_some() { 1 } else { 0 };
Some(base_height + error_height)
}
View::Canvas(n) => {
Some((n.pixel_height / 20).max(1))
}
View::Image(n) => {
n.cell_height.or(Some(5))
}
View::Terminal(n) => {
let border = if n.border { 2 } else { 0 };
Some(n.rows as u16 + border)
}
View::Empty => Some(0),
}
}
pub fn intrinsic_width(&self) -> Option<u16> {
match self {
View::Text(n) => {
let max_line_width = n.content.lines().map(|l| l.len()).max().unwrap_or(0);
Some(max_line_width as u16)
}
View::Button(n) => {
Some(n.label.len() as u16 + 4)
}
View::Box(n) => {
let border = if n.border { 2 } else { 0 };
let padding = n.padding * 2;
let inner = n
.child
.as_ref()
.and_then(|c| c.intrinsic_width())
.unwrap_or(0);
Some(inner + border + padding)
}
View::VStack(n) => {
n.children
.iter()
.filter_map(|c| c.intrinsic_width())
.max()
.or(Some(1))
}
View::HStack(n) => {
if n.children.is_empty() {
return Some(0);
}
let spacing = if n.children.len() > 1 {
n.spacing * (n.children.len() as u16 - 1)
} else {
0
};
let children_width: u16 =
n.children.iter().filter_map(|c| c.intrinsic_width()).sum();
Some(children_width + spacing)
}
View::List(n) => {
let max_item = n.items.iter().map(|i| i.len()).max().unwrap_or(0);
Some(max_item as u16 + 2)
}
View::TextInput(_) => {
None
}
View::TextArea(_) => {
None
}
View::Checkbox(n) => {
Some(n.label.len() as u16 + 4)
}
View::RadioGroup(n) => {
let max_option = n.options.iter().map(|o| o.len()).max().unwrap_or(0);
Some(max_option as u16 + 4)
}
View::Modal(_) => None, View::Spacer(n) => {
if n.flex == 0 {
Some(0) } else {
None }
}
View::Split(_) => None, View::Tabs(_) => None, View::Tree(_) => None, View::Table(_) => None, View::ProgressBar(n) => {
let label_width = n.label.as_ref().map(|l| l.len() + 1).unwrap_or(0);
let bar_width = n.width.unwrap_or(10) as usize;
let percentage_width = if n.show_percentage { 5 } else { 0 };
Some((label_width + bar_width + percentage_width) as u16)
}
View::StatusBar(n) => {
let left_width = n.left.len();
let center_width = n.center.as_ref().map(|c| c.len()).unwrap_or(0);
let right_width = n.right.as_ref().map(|r| r.len()).unwrap_or(0);
let spacing = if center_width > 0 || right_width > 0 {
2
} else {
0
};
Some((left_width + center_width + right_width + spacing) as u16)
}
View::CommandPalette(_) => None, View::MenuBar(n) => {
let labels_width: usize = n.menus.iter().map(|m| m.label.len() + 3).sum(); Some(labels_width as u16)
}
View::ToastContainer(_) => None, View::Form(n) => {
n.children.iter().filter_map(|c| c.intrinsic_width()).max()
}
View::FormField(n) => {
let label_width = n.label.len() as u16;
let input_width = 20u16; Some(label_width.max(input_width))
}
View::Canvas(n) => {
Some((n.pixel_width / 10).max(1))
}
View::Image(n) => {
n.cell_width.or(Some(10))
}
View::Terminal(n) => {
let border = if n.border { 2 } else { 0 };
Some(n.cols as u16 + border)
}
View::Empty => Some(0),
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Orientation {
#[default]
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TabPosition {
#[default]
Top,
Bottom,
}
#[derive(Debug, Clone)]
pub struct TextNode {
pub content: String,
pub color: Option<crossterm::style::Color>,
pub bg_color: Option<crossterm::style::Color>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub dim: bool,
}
#[derive(Debug, Clone)]
pub struct VStackNode {
pub children: Vec<View>,
pub spacing: u16,
pub justify: Justify,
pub align: Align,
pub layout_mode: LayoutMode,
}
#[derive(Debug, Clone)]
pub struct HStackNode {
pub children: Vec<View>,
pub spacing: u16,
pub justify: Justify,
pub align: Align,
pub layout_mode: LayoutMode,
}
#[derive(Debug, Clone)]
pub struct BoxNode {
pub child: Option<std::boxed::Box<View>>,
pub border: bool,
pub padding: u16,
pub flex: u16,
pub scroll: bool,
pub auto_scroll_bottom: bool,
pub focusable: bool,
pub min_width: Option<u16>,
pub max_width: Option<u16>,
pub min_height: Option<u16>,
pub max_height: Option<u16>,
}
#[derive(Debug, Clone)]
pub struct SpacerNode {
pub flex: u16,
pub height: u16,
}
#[derive(Clone)]
pub struct ButtonNode {
pub label: String,
pub on_press: Option<Callback>,
}
#[derive(Clone)]
pub struct ListNode {
pub items: Vec<String>,
pub selected: usize,
pub on_select: Option<SelectCallback>,
}
#[derive(Clone)]
pub struct TextInputNode {
pub value: String,
pub placeholder: String,
pub on_change: Option<ChangeCallback>,
pub on_cursor_change: Option<CursorPosCallback>,
pub on_submit: Option<Callback>,
pub on_key_up: Option<Callback>,
pub on_key_down: Option<Callback>,
pub cursor_pos: usize,
pub focused: bool,
}
#[derive(Clone)]
pub struct TextAreaNode {
pub value: String,
pub placeholder: String,
pub on_change: Option<ChangeCallback>,
pub on_cursor_change: Option<CursorChangeCallback>,
pub cursor_line: usize,
pub cursor_col: usize,
pub rows: u16,
pub wrap_width: Option<u16>,
}
#[derive(Clone)]
pub struct CheckboxNode {
pub checked: bool,
pub label: String,
pub on_toggle: Option<ToggleCallback>,
}
#[derive(Clone)]
pub struct RadioGroupNode {
pub options: Vec<String>,
pub selected: usize,
pub label: Option<String>,
pub on_change: Option<SelectCallback>,
}
#[derive(Clone)]
pub struct ModalNode {
pub visible: bool,
pub title: String,
pub child: Option<std::boxed::Box<View>>,
pub on_dismiss: Option<Callback>,
pub width_percent: u16,
pub height_percent: u16,
}
#[derive(Clone)]
pub struct SplitNode {
pub orientation: Orientation,
pub first: std::boxed::Box<View>,
pub second: std::boxed::Box<View>,
pub ratio: f32,
pub min_first: Option<u16>,
pub min_second: Option<u16>,
pub show_divider: bool,
}
#[derive(Clone)]
pub struct TabsNode {
pub tabs: Vec<String>,
pub children: Vec<View>,
pub active: usize,
pub on_change: Option<SelectCallback>,
pub position: TabPosition,
}
#[derive(Clone, Debug)]
pub struct TreeItem {
pub label: String,
pub icon: Option<String>,
pub children: Vec<TreeItem>,
pub expanded: bool,
}
impl TreeItem {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
icon: None,
children: Vec::new(),
expanded: false,
}
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn child(mut self, child: TreeItem) -> Self {
self.children.push(child);
self
}
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
pub fn is_leaf(&self) -> bool {
self.children.is_empty()
}
}
#[derive(Clone)]
pub struct TreeNode {
pub items: Vec<TreeItem>,
pub selected: TreePath,
pub on_select: Option<TreeSelectCallback>,
pub on_activate: Option<TreeActivateCallback>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum TextAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum ColumnWidth {
#[default]
Auto,
Fixed(u16),
Flex(u16),
}
#[derive(Debug, Clone)]
pub struct TableColumn {
pub header: String,
pub width: ColumnWidth,
pub sortable: bool,
pub align: TextAlign,
}
impl TableColumn {
pub fn new(header: impl Into<String>) -> Self {
Self {
header: header.into(),
width: ColumnWidth::Auto,
sortable: false,
align: TextAlign::Left,
}
}
pub fn width(mut self, width: ColumnWidth) -> Self {
self.width = width;
self
}
pub fn sortable(mut self, sortable: bool) -> Self {
self.sortable = sortable;
self
}
pub fn align(mut self, align: TextAlign) -> Self {
self.align = align;
self
}
}
#[derive(Clone)]
pub struct TableNode {
pub columns: Vec<TableColumn>,
pub rows: Vec<Vec<String>>,
pub selected: usize,
pub sort: Option<(usize, bool)>,
pub on_select: Option<SelectCallback>,
pub on_sort: Option<SortCallback>,
pub on_activate: Option<RowActivateCallback>,
}
#[derive(Clone)]
pub struct ProgressBarNode {
pub value: f32,
pub label: Option<String>,
pub show_percentage: bool,
pub width: Option<u16>,
pub filled_char: char,
pub empty_char: char,
}
#[derive(Clone)]
pub struct StatusBarNode {
pub left: String,
pub center: Option<String>,
pub right: Option<String>,
pub bg_color: Option<crossterm::style::Color>,
pub fg_color: Option<crossterm::style::Color>,
}
#[derive(Debug, Default)]
pub struct VStackBuilder {
children: Vec<View>,
spacing: u16,
justify: Justify,
align: Align,
layout_mode: LayoutMode,
}
impl VStackBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn child(mut self, view: View) -> Self {
self.children.push(view);
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn justify(mut self, justify: Justify) -> Self {
self.justify = justify;
self
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn layout_mode(mut self, mode: LayoutMode) -> Self {
self.layout_mode = mode;
self
}
pub fn build(self) -> View {
View::VStack(VStackNode {
children: self.children,
spacing: self.spacing,
justify: self.justify,
align: self.align,
layout_mode: self.layout_mode,
})
}
}
#[derive(Debug, Default)]
pub struct HStackBuilder {
children: Vec<View>,
spacing: u16,
justify: Justify,
align: Align,
layout_mode: LayoutMode,
}
impl HStackBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn child(mut self, view: View) -> Self {
self.children.push(view);
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn justify(mut self, justify: Justify) -> Self {
self.justify = justify;
self
}
pub fn align(mut self, align: Align) -> Self {
self.align = align;
self
}
pub fn layout_mode(mut self, mode: LayoutMode) -> Self {
self.layout_mode = mode;
self
}
pub fn build(self) -> View {
View::HStack(HStackNode {
children: self.children,
spacing: self.spacing,
justify: self.justify,
align: self.align,
layout_mode: self.layout_mode,
})
}
}
#[derive(Default)]
pub struct ButtonBuilder {
label: String,
on_press: Option<Callback>,
}
impl ButtonBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn on_press(mut self, callback: impl Fn() + 'static) -> Self {
self.on_press = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Button(ButtonNode {
label: self.label,
on_press: self.on_press,
})
}
}
#[derive(Default)]
pub struct BoxBuilder {
child: Option<View>,
border: bool,
padding: u16,
flex: u16,
scroll: bool,
auto_scroll_bottom: bool,
focusable: Option<bool>,
min_width: Option<u16>,
max_width: Option<u16>,
min_height: Option<u16>,
max_height: Option<u16>,
}
impl BoxBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn child(mut self, view: View) -> Self {
self.child = Some(view);
self
}
pub fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn padding(mut self, padding: u16) -> Self {
self.padding = padding;
self
}
pub fn flex(mut self, flex: u16) -> Self {
self.flex = flex;
self
}
pub fn scroll(mut self, scroll: bool) -> Self {
self.scroll = scroll;
self
}
pub fn auto_scroll_bottom(mut self, auto_scroll: bool) -> Self {
self.auto_scroll_bottom = auto_scroll;
self
}
pub fn focusable(mut self, focusable: bool) -> Self {
self.focusable = Some(focusable);
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = Some(width);
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = Some(width);
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = Some(height);
self
}
pub fn max_height(mut self, height: u16) -> Self {
self.max_height = Some(height);
self
}
pub fn build(self) -> View {
let default_focusable = self.scroll || self.auto_scroll_bottom;
View::Box(BoxNode {
child: self.child.map(std::boxed::Box::new),
border: self.border,
padding: self.padding,
flex: self.flex,
scroll: self.scroll,
auto_scroll_bottom: self.auto_scroll_bottom,
focusable: self.focusable.unwrap_or(default_focusable),
min_width: self.min_width,
max_width: self.max_width,
min_height: self.min_height,
max_height: self.max_height,
})
}
}
#[derive(Default)]
pub struct ListBuilder {
items: Vec<String>,
selected: usize,
on_select: Option<SelectCallback>,
}
impl ListBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn items(mut self, items: Vec<String>) -> Self {
self.items = items;
self
}
pub fn selected(mut self, selected: usize) -> Self {
self.selected = selected;
self
}
pub fn on_select(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_select = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::List(ListNode {
items: self.items,
selected: self.selected,
on_select: self.on_select,
})
}
}
#[derive(Default)]
pub struct TextInputBuilder {
value: String,
placeholder: String,
on_change: Option<ChangeCallback>,
on_cursor_change: Option<CursorPosCallback>,
on_submit: Option<Callback>,
on_key_up: Option<Callback>,
on_key_down: Option<Callback>,
cursor_pos: usize,
focused: bool,
}
impl TextInputBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self.cursor_pos = self.value.len();
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
self.on_change = Some(Rc::new(callback));
self
}
pub fn on_cursor_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_cursor_change = Some(Rc::new(callback));
self
}
pub fn on_submit(mut self, callback: impl Fn() + 'static) -> Self {
self.on_submit = Some(Rc::new(callback));
self
}
pub fn on_key_up(mut self, callback: impl Fn() + 'static) -> Self {
self.on_key_up = Some(Rc::new(callback));
self
}
pub fn on_key_down(mut self, callback: impl Fn() + 'static) -> Self {
self.on_key_down = Some(Rc::new(callback));
self
}
pub fn cursor(mut self, pos: usize) -> Self {
self.cursor_pos = pos;
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn build(self) -> View {
View::TextInput(TextInputNode {
value: self.value.clone(),
placeholder: self.placeholder,
on_change: self.on_change,
on_cursor_change: self.on_cursor_change,
on_submit: self.on_submit,
on_key_up: self.on_key_up,
on_key_down: self.on_key_down,
cursor_pos: self.cursor_pos.min(self.value.len()),
focused: self.focused,
})
}
}
#[derive(Default)]
pub struct CheckboxBuilder {
checked: bool,
label: String,
on_toggle: Option<ToggleCallback>,
}
impl CheckboxBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn on_toggle(mut self, callback: impl Fn(bool) + 'static) -> Self {
self.on_toggle = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Checkbox(CheckboxNode {
checked: self.checked,
label: self.label,
on_toggle: self.on_toggle,
})
}
}
#[derive(Default)]
pub struct RadioGroupBuilder {
options: Vec<String>,
selected: usize,
label: Option<String>,
on_change: Option<SelectCallback>,
}
impl RadioGroupBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn options(mut self, options: Vec<impl Into<String>>) -> Self {
self.options = options.into_iter().map(|s| s.into()).collect();
self
}
pub fn option(mut self, option: impl Into<String>) -> Self {
self.options.push(option.into());
self
}
pub fn selected(mut self, selected: usize) -> Self {
self.selected = selected;
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn on_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_change = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::RadioGroup(RadioGroupNode {
options: self.options,
selected: self.selected,
label: self.label,
on_change: self.on_change,
})
}
}
#[derive(Debug, Default)]
pub struct TextBuilder {
content: String,
color: Option<crossterm::style::Color>,
bg_color: Option<crossterm::style::Color>,
bold: bool,
italic: bool,
underline: bool,
dim: bool,
}
impl TextBuilder {
pub fn new(content: impl Into<String>) -> Self {
Self {
content: content.into(),
..Default::default()
}
}
pub fn color(mut self, color: crossterm::style::Color) -> Self {
self.color = Some(color);
self
}
pub fn bg(mut self, color: crossterm::style::Color) -> Self {
self.bg_color = Some(color);
self
}
pub fn bold(mut self) -> Self {
self.bold = true;
self
}
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
pub fn dim(mut self) -> Self {
self.dim = true;
self
}
pub fn build(self) -> View {
View::Text(TextNode {
content: self.content,
color: self.color,
bg_color: self.bg_color,
bold: self.bold,
italic: self.italic,
underline: self.underline,
dim: self.dim,
})
}
}
#[derive(Default)]
pub struct TextAreaBuilder {
value: String,
placeholder: String,
on_change: Option<ChangeCallback>,
on_cursor_change: Option<CursorChangeCallback>,
cursor_line: usize,
cursor_col: usize,
rows: u16,
wrap_width: Option<u16>,
}
impl TextAreaBuilder {
pub fn new() -> Self {
Self {
rows: 5, ..Default::default()
}
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
self.on_change = Some(Rc::new(callback));
self
}
pub fn on_cursor_change(mut self, callback: impl Fn(usize, usize) + 'static) -> Self {
self.on_cursor_change = Some(Rc::new(callback));
self
}
pub fn cursor_line(mut self, line: usize) -> Self {
self.cursor_line = line;
self
}
pub fn cursor_col(mut self, col: usize) -> Self {
self.cursor_col = col;
self
}
pub fn rows(mut self, rows: u16) -> Self {
self.rows = rows;
self
}
pub fn wrap_width(mut self, width: u16) -> Self {
self.wrap_width = Some(width);
self
}
pub fn build(self) -> View {
View::TextArea(TextAreaNode {
value: self.value,
placeholder: self.placeholder,
on_change: self.on_change,
on_cursor_change: self.on_cursor_change,
cursor_line: self.cursor_line,
cursor_col: self.cursor_col,
rows: self.rows,
wrap_width: self.wrap_width,
})
}
}
#[derive(Default)]
pub struct ModalBuilder {
visible: bool,
title: String,
child: Option<View>,
on_dismiss: Option<Callback>,
width_percent: u16,
height_percent: u16,
}
impl ModalBuilder {
pub fn new() -> Self {
Self {
width_percent: 60,
height_percent: 50,
..Default::default()
}
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = title.into();
self
}
pub fn child(mut self, view: View) -> Self {
self.child = Some(view);
self
}
pub fn on_dismiss(mut self, callback: impl Fn() + 'static) -> Self {
self.on_dismiss = Some(Rc::new(callback));
self
}
pub fn width(mut self, percent: u16) -> Self {
self.width_percent = percent.min(100);
self
}
pub fn height(mut self, percent: u16) -> Self {
self.height_percent = percent.min(100);
self
}
pub fn build(self) -> View {
View::Modal(ModalNode {
visible: self.visible,
title: self.title,
child: self.child.map(std::boxed::Box::new),
on_dismiss: self.on_dismiss,
width_percent: self.width_percent,
height_percent: self.height_percent,
})
}
}
#[derive(Default)]
pub struct SplitBuilder {
orientation: Orientation,
first: Option<View>,
second: Option<View>,
ratio: f32,
min_first: Option<u16>,
min_second: Option<u16>,
show_divider: bool,
}
impl SplitBuilder {
pub fn new() -> Self {
Self {
ratio: 0.5, show_divider: true,
..Default::default()
}
}
pub fn horizontal(mut self) -> Self {
self.orientation = Orientation::Horizontal;
self
}
pub fn vertical(mut self) -> Self {
self.orientation = Orientation::Vertical;
self
}
pub fn first(mut self, view: View) -> Self {
self.first = Some(view);
self
}
pub fn second(mut self, view: View) -> Self {
self.second = Some(view);
self
}
pub fn ratio(mut self, ratio: f32) -> Self {
self.ratio = ratio.clamp(0.0, 1.0);
self
}
pub fn min_first(mut self, min: u16) -> Self {
self.min_first = Some(min);
self
}
pub fn min_second(mut self, min: u16) -> Self {
self.min_second = Some(min);
self
}
pub fn show_divider(mut self, show: bool) -> Self {
self.show_divider = show;
self
}
pub fn build(self) -> View {
View::Split(SplitNode {
orientation: self.orientation,
first: std::boxed::Box::new(self.first.unwrap_or(View::Empty)),
second: std::boxed::Box::new(self.second.unwrap_or(View::Empty)),
ratio: self.ratio,
min_first: self.min_first,
min_second: self.min_second,
show_divider: self.show_divider,
})
}
}
#[derive(Default)]
pub struct TabsBuilder {
tabs: Vec<String>,
children: Vec<View>,
active: usize,
on_change: Option<SelectCallback>,
position: TabPosition,
}
impl TabsBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn tab(mut self, label: impl Into<String>, content: View) -> Self {
self.tabs.push(label.into());
self.children.push(content);
self
}
pub fn active(mut self, index: usize) -> Self {
self.active = index;
self
}
pub fn on_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_change = Some(Rc::new(callback));
self
}
pub fn position(mut self, position: TabPosition) -> Self {
self.position = position;
self
}
pub fn build(self) -> View {
View::Tabs(TabsNode {
tabs: self.tabs,
children: self.children,
active: self.active,
on_change: self.on_change,
position: self.position,
})
}
}
#[derive(Default)]
pub struct TreeBuilder {
items: Vec<TreeItem>,
selected: TreePath,
on_select: Option<TreeSelectCallback>,
on_activate: Option<TreeActivateCallback>,
}
impl TreeBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn items(mut self, items: Vec<TreeItem>) -> Self {
self.items = items;
self
}
pub fn item(mut self, item: TreeItem) -> Self {
self.items.push(item);
self
}
pub fn selected(mut self, path: TreePath) -> Self {
self.selected = path;
self
}
pub fn on_select(mut self, callback: impl Fn(TreePath) + 'static) -> Self {
self.on_select = Some(Rc::new(callback));
self
}
pub fn on_activate(mut self, callback: impl Fn(TreePath) + 'static) -> Self {
self.on_activate = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Tree(TreeNode {
items: self.items,
selected: self.selected,
on_select: self.on_select,
on_activate: self.on_activate,
})
}
}
#[derive(Default)]
pub struct TableBuilder {
columns: Vec<TableColumn>,
rows: Vec<Vec<String>>,
selected: usize,
sort: Option<(usize, bool)>,
on_select: Option<SelectCallback>,
on_sort: Option<SortCallback>,
on_activate: Option<RowActivateCallback>,
}
impl TableBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn column(mut self, header: impl Into<String>) -> Self {
self.columns.push(TableColumn::new(header));
self
}
pub fn column_with(mut self, column: TableColumn) -> Self {
self.columns.push(column);
self
}
pub fn rows(mut self, rows: Vec<Vec<String>>) -> Self {
self.rows = rows;
self
}
pub fn row(mut self, row: Vec<String>) -> Self {
self.rows.push(row);
self
}
pub fn selected(mut self, index: usize) -> Self {
self.selected = index;
self
}
pub fn sort(mut self, sort: Option<(usize, bool)>) -> Self {
self.sort = sort;
self
}
pub fn sort_by(mut self, column: usize, ascending: bool) -> Self {
self.sort = Some((column, ascending));
self
}
pub fn on_select(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_select = Some(Rc::new(callback));
self
}
pub fn on_sort(mut self, callback: impl Fn(usize, bool) + 'static) -> Self {
self.on_sort = Some(Rc::new(callback));
self
}
pub fn on_activate(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_activate = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Table(TableNode {
columns: self.columns,
rows: self.rows,
selected: self.selected,
sort: self.sort,
on_select: self.on_select,
on_sort: self.on_sort,
on_activate: self.on_activate,
})
}
}
#[derive(Debug, Clone)]
pub struct ProgressBarBuilder {
value: f32,
label: Option<String>,
show_percentage: bool,
width: Option<u16>,
filled_char: char,
empty_char: char,
}
impl Default for ProgressBarBuilder {
fn default() -> Self {
Self {
value: 0.0,
label: None,
show_percentage: true,
width: None,
filled_char: 'â–ˆ',
empty_char: 'â–‘',
}
}
}
impl ProgressBarBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn value(mut self, value: f32) -> Self {
self.value = value.clamp(0.0, 1.0);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn show_percentage(mut self, show: bool) -> Self {
self.show_percentage = show;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn filled_char(mut self, ch: char) -> Self {
self.filled_char = ch;
self
}
pub fn empty_char(mut self, ch: char) -> Self {
self.empty_char = ch;
self
}
pub fn build(self) -> View {
View::ProgressBar(ProgressBarNode {
value: self.value,
label: self.label,
show_percentage: self.show_percentage,
width: self.width,
filled_char: self.filled_char,
empty_char: self.empty_char,
})
}
}
#[derive(Debug, Clone, Default)]
pub struct StatusBarBuilder {
left: String,
center: Option<String>,
right: Option<String>,
bg_color: Option<crossterm::style::Color>,
fg_color: Option<crossterm::style::Color>,
}
impl StatusBarBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn left(mut self, content: impl Into<String>) -> Self {
self.left = content.into();
self
}
pub fn center(mut self, content: impl Into<String>) -> Self {
self.center = Some(content.into());
self
}
pub fn right(mut self, content: impl Into<String>) -> Self {
self.right = Some(content.into());
self
}
pub fn bg(mut self, color: crossterm::style::Color) -> Self {
self.bg_color = Some(color);
self
}
pub fn fg(mut self, color: crossterm::style::Color) -> Self {
self.fg_color = Some(color);
self
}
pub fn build(self) -> View {
View::StatusBar(StatusBarNode {
left: self.left,
center: self.center,
right: self.right,
bg_color: self.bg_color,
fg_color: self.fg_color,
})
}
}
#[derive(Clone)]
pub struct PaletteCommand {
pub id: &'static str,
pub label: String,
pub shortcut: Option<String>,
pub category: Option<String>,
}
impl PaletteCommand {
pub fn new(id: &'static str, label: impl Into<String>) -> Self {
Self {
id,
label: label.into(),
shortcut: None,
category: None,
}
}
pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
self.shortcut = Some(shortcut.into());
self
}
pub fn category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
}
#[derive(Clone)]
pub struct CommandPaletteNode {
pub visible: bool,
pub query: String,
pub commands: Vec<PaletteCommand>,
pub selected: usize,
pub on_query_change: Option<ChangeCallback>,
pub on_select: Option<CommandCallback>,
pub on_dismiss: Option<Callback>,
pub width_percent: u16,
pub height_percent: u16,
}
#[derive(Default)]
pub struct CommandPaletteBuilder {
visible: bool,
query: String,
commands: Vec<PaletteCommand>,
selected: usize,
on_query_change: Option<ChangeCallback>,
on_select: Option<CommandCallback>,
on_dismiss: Option<Callback>,
width_percent: u16,
height_percent: u16,
}
impl CommandPaletteBuilder {
pub fn new() -> Self {
Self {
width_percent: 50,
height_percent: 60,
..Default::default()
}
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn query(mut self, query: impl Into<String>) -> Self {
self.query = query.into();
self
}
pub fn commands(mut self, commands: Vec<PaletteCommand>) -> Self {
self.commands = commands;
self
}
pub fn command(mut self, command: PaletteCommand) -> Self {
self.commands.push(command);
self
}
pub fn selected(mut self, selected: usize) -> Self {
self.selected = selected;
self
}
pub fn on_query_change(mut self, callback: impl Fn(String) + 'static) -> Self {
self.on_query_change = Some(Rc::new(callback));
self
}
pub fn on_select(mut self, callback: impl Fn(&'static str) + 'static) -> Self {
self.on_select = Some(Rc::new(callback));
self
}
pub fn on_dismiss(mut self, callback: impl Fn() + 'static) -> Self {
self.on_dismiss = Some(Rc::new(callback));
self
}
pub fn width(mut self, percent: u16) -> Self {
self.width_percent = percent.min(100);
self
}
pub fn height(mut self, percent: u16) -> Self {
self.height_percent = percent.min(100);
self
}
pub fn build(self) -> View {
View::CommandPalette(CommandPaletteNode {
visible: self.visible,
query: self.query,
commands: self.commands,
selected: self.selected,
on_query_change: self.on_query_change,
on_select: self.on_select,
on_dismiss: self.on_dismiss,
width_percent: self.width_percent,
height_percent: self.height_percent,
})
}
}
#[derive(Clone)]
pub struct Menu {
pub label: String,
pub items: Vec<MenuItemNode>,
}
impl Menu {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
items: Vec::new(),
}
}
pub fn item(mut self, item: MenuItemNode) -> Self {
self.items.push(item);
self
}
pub fn command(self, id: &'static str, label: impl Into<String>) -> Self {
self.item(MenuItemNode::Command {
id,
label: label.into(),
shortcut: None,
})
}
pub fn command_with_shortcut(
self,
id: &'static str,
label: impl Into<String>,
shortcut: impl Into<String>,
) -> Self {
self.item(MenuItemNode::Command {
id,
label: label.into(),
shortcut: Some(shortcut.into()),
})
}
pub fn separator(self) -> Self {
self.item(MenuItemNode::Separator)
}
}
#[derive(Clone)]
pub enum MenuItemNode {
Command {
id: &'static str,
label: String,
shortcut: Option<String>,
},
Separator,
}
#[derive(Clone)]
pub struct MenuBarNode {
pub menus: Vec<Menu>,
pub active_menu: Option<usize>,
pub highlighted_menu: usize,
pub selected_item: usize,
pub on_select: Option<CommandCallback>,
pub on_menu_change: Option<SelectCallback>,
pub on_highlight_change: Option<SelectCallback>,
pub on_item_change: Option<SelectCallback>,
}
#[derive(Default)]
pub struct MenuBarBuilder {
menus: Vec<Menu>,
active_menu: Option<usize>,
highlighted_menu: usize,
selected_item: usize,
on_select: Option<CommandCallback>,
on_menu_change: Option<SelectCallback>,
on_highlight_change: Option<SelectCallback>,
on_item_change: Option<SelectCallback>,
}
impl MenuBarBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn menu(mut self, menu: Menu) -> Self {
self.menus.push(menu);
self
}
pub fn active_menu(mut self, index: Option<usize>) -> Self {
self.active_menu = index;
self
}
pub fn highlighted_menu(mut self, index: usize) -> Self {
self.highlighted_menu = index;
self
}
pub fn selected_item(mut self, index: usize) -> Self {
self.selected_item = index;
self
}
pub fn on_select(mut self, callback: impl Fn(&'static str) + 'static) -> Self {
self.on_select = Some(Rc::new(callback));
self
}
pub fn on_menu_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_menu_change = Some(Rc::new(callback));
self
}
pub fn on_highlight_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_highlight_change = Some(Rc::new(callback));
self
}
pub fn on_item_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
self.on_item_change = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::MenuBar(MenuBarNode {
menus: self.menus,
active_menu: self.active_menu,
highlighted_menu: self.highlighted_menu,
selected_item: self.selected_item,
on_select: self.on_select,
on_menu_change: self.on_menu_change,
on_highlight_change: self.on_highlight_change,
on_item_change: self.on_item_change,
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ToastPosition {
TopRight,
TopLeft,
#[default]
BottomRight,
BottomLeft,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum ToastLevelView {
#[default]
Info,
Success,
Warning,
Error,
}
#[derive(Clone)]
pub struct ToastItem {
pub message: String,
pub level: ToastLevelView,
pub progress: f32,
}
#[derive(Clone)]
pub struct ToastContainerNode {
pub toasts: Vec<ToastItem>,
pub position: ToastPosition,
pub max_visible: usize,
pub width: u16,
}
#[derive(Default)]
pub struct ToastContainerBuilder {
toasts: Vec<ToastItem>,
position: ToastPosition,
max_visible: usize,
width: u16,
}
impl ToastContainerBuilder {
pub fn new() -> Self {
Self {
toasts: Vec::new(),
position: ToastPosition::BottomRight,
max_visible: 5,
width: 40,
}
}
pub fn toasts(mut self, toasts: Vec<ToastItem>) -> Self {
self.toasts = toasts;
self
}
pub fn from_queue(mut self, queue: &crate::toast::ToastQueue) -> Self {
let toasts = queue.collect();
self.toasts = toasts
.into_iter()
.map(|t| {
let progress = t.remaining_fraction();
let level = match t.level {
crate::toast::ToastLevel::Info => ToastLevelView::Info,
crate::toast::ToastLevel::Success => ToastLevelView::Success,
crate::toast::ToastLevel::Warning => ToastLevelView::Warning,
crate::toast::ToastLevel::Error => ToastLevelView::Error,
};
ToastItem {
message: t.message,
level,
progress,
}
})
.collect();
self
}
pub fn position(mut self, position: ToastPosition) -> Self {
self.position = position;
self
}
pub fn max_visible(mut self, max: usize) -> Self {
self.max_visible = max;
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width;
self
}
pub fn build(self) -> View {
View::ToastContainer(ToastContainerNode {
toasts: self.toasts,
position: self.position,
max_visible: self.max_visible,
width: self.width,
})
}
}
pub type FormSubmitCallback = Rc<dyn Fn(std::collections::HashMap<String, String>)>;
#[derive(Clone)]
pub struct FormNode {
pub children: Vec<View>,
pub on_submit: Option<FormSubmitCallback>,
pub spacing: u16,
}
#[derive(Default)]
pub struct FormBuilder {
children: Vec<View>,
on_submit: Option<FormSubmitCallback>,
spacing: u16,
}
impl FormBuilder {
pub fn new() -> Self {
Self {
spacing: 1,
..Default::default()
}
}
pub fn child(mut self, view: View) -> Self {
self.children.push(view);
self
}
pub fn spacing(mut self, spacing: u16) -> Self {
self.spacing = spacing;
self
}
pub fn on_submit(
mut self,
callback: impl Fn(std::collections::HashMap<String, String>) + 'static,
) -> Self {
self.on_submit = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Form(FormNode {
children: self.children,
on_submit: self.on_submit,
spacing: self.spacing,
})
}
}
#[derive(Clone)]
pub struct FormFieldNode {
pub name: String,
pub label: String,
pub value: String,
pub placeholder: String,
pub error: Option<String>,
pub password: bool,
pub on_change: Option<ChangeCallback>,
pub on_blur: Option<Callback>,
pub cursor_pos: usize,
}
#[derive(Default)]
pub struct FormFieldBuilder {
name: String,
label: String,
value: String,
placeholder: String,
error: Option<String>,
password: bool,
on_change: Option<ChangeCallback>,
on_blur: Option<Callback>,
cursor_pos: usize,
}
impl FormFieldBuilder {
pub fn new(name: impl Into<String>) -> Self {
let name = name.into();
Self {
label: name.clone(),
name,
..Default::default()
}
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
pub fn value(mut self, value: impl Into<String>) -> Self {
self.value = value.into();
self.cursor_pos = self.value.len();
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn error(mut self, error: Option<String>) -> Self {
self.error = error;
self
}
pub fn password(mut self, password: bool) -> Self {
self.password = password;
self
}
pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
self.on_change = Some(Rc::new(callback));
self
}
pub fn on_blur(mut self, callback: impl Fn() + 'static) -> Self {
self.on_blur = Some(Rc::new(callback));
self
}
pub fn cursor(mut self, pos: usize) -> Self {
self.cursor_pos = pos;
self
}
pub fn build(self) -> View {
View::FormField(FormFieldNode {
name: self.name,
label: self.label,
value: self.value.clone(),
placeholder: self.placeholder,
error: self.error,
password: self.password,
on_change: self.on_change,
on_blur: self.on_blur,
cursor_pos: self.cursor_pos.min(self.value.len()),
})
}
}
#[derive(Clone)]
pub struct CanvasNode {
pub pixel_width: u16,
pub pixel_height: u16,
pub on_draw: Option<CanvasDrawCallback>,
pub id: u32,
}
pub struct CanvasBuilder {
pixel_width: u16,
pixel_height: u16,
on_draw: Option<CanvasDrawCallback>,
id: u32,
}
impl Default for CanvasBuilder {
fn default() -> Self {
use std::sync::atomic::{AtomicU32, Ordering};
static NEXT_ID: AtomicU32 = AtomicU32::new(1);
Self {
pixel_width: 100,
pixel_height: 50,
on_draw: None,
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
}
}
}
impl CanvasBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn width(mut self, width: u16) -> Self {
self.pixel_width = width;
self
}
pub fn height(mut self, height: u16) -> Self {
self.pixel_height = height;
self
}
pub fn on_draw<F>(mut self, callback: F) -> Self
where
F: Fn(&mut crate::canvas::DrawContext) + 'static,
{
self.on_draw = Some(Rc::new(callback));
self
}
pub fn id(mut self, id: u32) -> Self {
self.id = id;
self
}
pub fn build(self) -> View {
View::Canvas(CanvasNode {
pixel_width: self.pixel_width,
pixel_height: self.pixel_height,
on_draw: self.on_draw,
id: self.id,
})
}
}
#[derive(Clone)]
pub struct ImageNode {
pub source: Option<crate::image::ImageSource>,
pub id: u32,
pub cell_width: Option<u16>,
pub cell_height: Option<u16>,
pub alt: Option<String>,
}
pub struct ImageBuilder {
source: Option<crate::image::ImageSource>,
id: u32,
cell_width: Option<u16>,
cell_height: Option<u16>,
alt: Option<String>,
}
impl Default for ImageBuilder {
fn default() -> Self {
Self {
source: None,
id: crate::image::next_image_id(),
cell_width: None,
cell_height: None,
alt: None,
}
}
}
impl ImageBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn data(mut self, bytes: &[u8]) -> Self {
self.source = Some(crate::image::ImageSource::Data(bytes.to_vec()));
if let Some((w, h)) = crate::image::detect_image_dimensions(bytes) {
let (cw, ch) = crate::image::pixels_to_cells(w, h);
if self.cell_width.is_none() {
self.cell_width = Some(cw);
}
if self.cell_height.is_none() {
self.cell_height = Some(ch);
}
}
self
}
pub fn file(mut self, path: impl Into<String>) -> Self {
self.source = Some(crate::image::ImageSource::File(path.into()));
self
}
pub fn width(mut self, cells: u16) -> Self {
self.cell_width = Some(cells);
self
}
pub fn height(mut self, cells: u16) -> Self {
self.cell_height = Some(cells);
self
}
pub fn id(mut self, id: u32) -> Self {
self.id = id;
self
}
pub fn alt(mut self, text: impl Into<String>) -> Self {
self.alt = Some(text.into());
self
}
pub fn build(self) -> View {
View::Image(ImageNode {
source: self.source,
id: self.id,
cell_width: self.cell_width,
cell_height: self.cell_height,
alt: self.alt,
})
}
}
#[derive(Clone)]
pub struct TerminalNode {
pub handle: crate::terminal_state::TerminalHandle,
pub rows: usize,
pub cols: usize,
pub border: bool,
pub title: Option<String>,
pub on_exit: Option<Callback>,
}
pub struct TerminalBuilder {
handle: Option<crate::terminal_state::TerminalHandle>,
rows: usize,
cols: usize,
border: bool,
title: Option<String>,
on_exit: Option<Callback>,
}
impl Default for TerminalBuilder {
fn default() -> Self {
Self {
handle: None,
rows: 24,
cols: 80,
border: true,
title: Some("Terminal".to_string()),
on_exit: None,
}
}
}
impl TerminalBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn handle(mut self, handle: crate::terminal_state::TerminalHandle) -> Self {
self.handle = Some(handle);
self
}
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows;
self
}
pub fn cols(mut self, cols: usize) -> Self {
self.cols = cols;
self
}
pub fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn on_exit(mut self, callback: impl Fn() + 'static) -> Self {
self.on_exit = Some(Rc::new(callback));
self
}
pub fn build(self) -> View {
View::Terminal(TerminalNode {
handle: self.handle.expect("Terminal requires a handle (from cx.use_terminal())"),
rows: self.rows,
cols: self.cols,
border: self.border,
title: self.title,
on_exit: self.on_exit,
})
}
}