Skip to main content

telex/
view.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4use crate::widget::Widget;
5
6/// Callback type for event handlers (no arguments).
7pub type Callback = Rc<dyn Fn()>;
8
9/// Alignment along the main axis (justify).
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub enum Justify {
12    /// Items at the start (default).
13    #[default]
14    Start,
15    /// Items at the end.
16    End,
17    /// Items centered.
18    Center,
19    /// Items spread with space between them.
20    SpaceBetween,
21    /// Items spread with space around them.
22    SpaceAround,
23}
24
25/// Alignment along the cross axis (align).
26#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
27pub enum Align {
28    /// Items at the start.
29    Start,
30    /// Items at the end.
31    End,
32    /// Items centered.
33    Center,
34    /// Items stretch to fill (default).
35    #[default]
36    Stretch,
37}
38
39/// Layout mode for stack containers.
40///
41/// This enum allows switching between different layout algorithms.
42/// Currently only Flex is implemented, but this provides the hook
43/// for future layout experiments (e.g., percentage-based layouts).
44#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
45pub enum LayoutMode {
46    /// Flex-based layout (default).
47    /// Children with flex > 0 share remaining space proportionally.
48    /// Children with flex = 0 use their intrinsic/min size.
49    #[default]
50    Flex,
51    // Future: Percent - children specify exact percentages
52    // Future: Grid - CSS grid-like layout
53}
54
55/// Callback type for selection events (receives selected index).
56pub type SelectCallback = Rc<dyn Fn(usize)>;
57
58/// Callback type for text change events (receives new text).
59pub type ChangeCallback = Rc<dyn Fn(String)>;
60
61/// Callback type for toggle events (receives new state).
62pub type ToggleCallback = Rc<dyn Fn(bool)>;
63
64/// Callback type for cursor position change events (receives line, column).
65pub type CursorChangeCallback = Rc<dyn Fn(usize, usize)>;
66
67/// Callback type for cursor position change events in single-line inputs (receives position).
68pub type CursorPosCallback = Rc<dyn Fn(usize)>;
69
70/// Path to a node in a tree (indices at each level).
71pub type TreePath = Vec<usize>;
72
73/// Callback type for tree selection events (receives path to selected item).
74pub type TreeSelectCallback = Rc<dyn Fn(TreePath)>;
75
76/// Callback type for tree activation events (receives path to activated item).
77pub type TreeActivateCallback = Rc<dyn Fn(TreePath)>;
78
79/// Callback type for table sort events (receives column index and ascending flag).
80pub type SortCallback = Rc<dyn Fn(usize, bool)>;
81
82/// Callback type for table row activation events (receives row index).
83pub type RowActivateCallback = Rc<dyn Fn(usize)>;
84
85/// Callback type for command execution events (receives command ID).
86pub type CommandCallback = Rc<dyn Fn(&'static str)>;
87
88/// Callback type for canvas drawing (receives mutable draw context).
89pub type CanvasDrawCallback = Rc<dyn Fn(&mut crate::canvas::DrawContext)>;
90
91/// Callback type for slider value changes.
92pub type SliderCallback = Rc<dyn Fn(f64)>;
93
94/// The core view type - a node in the UI tree.
95#[derive(Clone)]
96pub enum View {
97    /// A text node displaying a string.
98    Text(TextNode),
99    /// A vertical stack of child views.
100    VStack(VStackNode),
101    /// A horizontal stack of child views.
102    HStack(HStackNode),
103    /// A clickable button.
104    Button(ButtonNode),
105    /// A container with optional border, padding, and flex sizing.
106    Box(BoxNode),
107    /// Flexible space that expands to fill available space.
108    Spacer(SpacerNode),
109    /// A selectable list of items.
110    List(ListNode),
111    /// A single-line text input.
112    TextInput(TextInputNode),
113    /// A multi-line text area.
114    TextArea(TextAreaNode),
115    /// A checkbox (toggle).
116    Checkbox(CheckboxNode),
117    /// A group of radio buttons (mutually exclusive options).
118    RadioGroup(RadioGroupNode),
119    /// A modal dialog overlay.
120    Modal(ModalNode),
121    /// A split pane container with two resizable panels.
122    Split(SplitNode),
123    /// A tabbed interface container.
124    Tabs(TabsNode),
125    /// A hierarchical tree view.
126    Tree(TreeNode),
127    /// A data table with columns and rows.
128    Table(TableNode),
129    /// A progress bar showing completion status.
130    ProgressBar(ProgressBarNode),
131    /// A status bar displayed at the bottom of the screen.
132    StatusBar(StatusBarNode),
133    /// A command palette overlay for searching and executing commands.
134    CommandPalette(CommandPaletteNode),
135    /// A horizontal menu bar with dropdown menus.
136    MenuBar(MenuBarNode),
137    /// A container for toast notifications.
138    ToastContainer(ToastContainerNode),
139    /// A form container with validation support.
140    Form(FormNode),
141    /// A form field with label and error display.
142    FormField(FormFieldNode),
143    /// A pixel-level canvas using Kitty graphics protocol.
144    Canvas(CanvasNode),
145    /// An image display using Kitty graphics protocol.
146    Image(ImageNode),
147    /// An interactive PTY terminal emulator.
148    Terminal(TerminalNode),
149    /// An error boundary that catches panics in its child view.
150    ErrorBoundary(ErrorBoundaryNode),
151    /// A user-defined custom widget.
152    Custom(CustomNode),
153    /// A slider for bounded numeric values.
154    Slider(SliderNode),
155    /// An empty placeholder.
156    Empty,
157}
158
159impl std::fmt::Debug for View {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        match self {
162            View::Text(n) => f.debug_tuple("Text").field(n).finish(),
163            View::VStack(n) => f.debug_tuple("VStack").field(n).finish(),
164            View::HStack(n) => f.debug_tuple("HStack").field(n).finish(),
165            View::Button(n) => f
166                .debug_struct("Button")
167                .field("label", &n.label)
168                .field("on_press", &"<callback>")
169                .finish(),
170            View::Box(n) => f.debug_tuple("Box").field(n).finish(),
171            View::Spacer(n) => f.debug_tuple("Spacer").field(n).finish(),
172            View::List(n) => f
173                .debug_struct("List")
174                .field("items", &n.items.len())
175                .field("selected", &n.selected)
176                .finish(),
177            View::TextInput(n) => f
178                .debug_struct("TextInput")
179                .field("value", &n.value)
180                .finish(),
181            View::TextArea(n) => f
182                .debug_struct("TextArea")
183                .field("value", &n.value)
184                .field("cursor", &(n.cursor_line, n.cursor_col))
185                .finish(),
186            View::Checkbox(n) => f
187                .debug_struct("Checkbox")
188                .field("checked", &n.checked)
189                .field("label", &n.label)
190                .finish(),
191            View::RadioGroup(n) => f
192                .debug_struct("RadioGroup")
193                .field("selected", &n.selected)
194                .field("options", &n.options)
195                .finish(),
196            View::Modal(n) => f
197                .debug_struct("Modal")
198                .field("visible", &n.visible)
199                .field("title", &n.title)
200                .finish(),
201            View::Split(n) => f
202                .debug_struct("Split")
203                .field("orientation", &n.orientation)
204                .field("ratio", &n.ratio)
205                .finish(),
206            View::Tabs(n) => f
207                .debug_struct("Tabs")
208                .field("tabs", &n.tabs)
209                .field("active", &n.active)
210                .finish(),
211            View::Tree(n) => f
212                .debug_struct("Tree")
213                .field("items", &n.items.len())
214                .field("selected", &n.selected)
215                .finish(),
216            View::Table(n) => f
217                .debug_struct("Table")
218                .field("columns", &n.columns.len())
219                .field("rows", &n.rows.len())
220                .field("selected", &n.selected)
221                .finish(),
222            View::ProgressBar(n) => f
223                .debug_struct("ProgressBar")
224                .field("value", &n.value)
225                .field("label", &n.label)
226                .finish(),
227            View::StatusBar(n) => f
228                .debug_struct("StatusBar")
229                .field("left", &n.left)
230                .field("center", &n.center)
231                .field("right", &n.right)
232                .finish(),
233            View::CommandPalette(n) => f
234                .debug_struct("CommandPalette")
235                .field("visible", &n.visible)
236                .field("query", &n.query)
237                .field("selected", &n.selected)
238                .finish(),
239            View::MenuBar(n) => f
240                .debug_struct("MenuBar")
241                .field("menus", &n.menus.len())
242                .field("active_menu", &n.active_menu)
243                .finish(),
244            View::ToastContainer(n) => f
245                .debug_struct("ToastContainer")
246                .field("toasts", &n.toasts.len())
247                .finish(),
248            View::Form(n) => f
249                .debug_struct("Form")
250                .field("children", &n.children.len())
251                .finish(),
252            View::FormField(n) => f
253                .debug_struct("FormField")
254                .field("name", &n.name)
255                .field("label", &n.label)
256                .finish(),
257            View::Canvas(n) => f
258                .debug_struct("Canvas")
259                .field("width", &n.pixel_width)
260                .field("height", &n.pixel_height)
261                .finish(),
262            View::Image(n) => f
263                .debug_struct("Image")
264                .field("has_data", &n.source.is_some())
265                .finish(),
266            View::Terminal(n) => f
267                .debug_struct("Terminal")
268                .field("rows", &n.rows)
269                .field("cols", &n.cols)
270                .field("border", &n.border)
271                .finish(),
272            View::ErrorBoundary(_) => f.debug_struct("ErrorBoundary").finish(),
273            View::Custom(_) => f.debug_struct("Custom").finish(),
274            View::Slider(n) => f
275                .debug_struct("Slider")
276                .field("min", &n.min)
277                .field("max", &n.max)
278                .field("value", &n.value)
279                .field("step", &n.step)
280                .finish(),
281            View::Empty => write!(f, "Empty"),
282        }
283    }
284}
285
286impl View {
287    /// Create a text view with the given content.
288    pub fn text(content: impl Into<String>) -> Self {
289        View::Text(TextNode {
290            content: content.into(),
291            color: None,
292            bg_color: None,
293            bold: false,
294            italic: false,
295            underline: false,
296            dim: false,
297        })
298    }
299
300    /// Create a styled text builder.
301    pub fn styled_text(content: impl Into<String>) -> TextBuilder {
302        TextBuilder::new(content)
303    }
304
305    /// Create a vertical stack builder.
306    pub fn vstack() -> VStackBuilder {
307        VStackBuilder::new()
308    }
309
310    /// Create a horizontal stack builder.
311    pub fn hstack() -> HStackBuilder {
312        HStackBuilder::new()
313    }
314
315    /// Create a button builder.
316    pub fn button() -> ButtonBuilder {
317        ButtonBuilder::new()
318    }
319
320    /// Create a box builder.
321    pub fn boxed() -> BoxBuilder {
322        BoxBuilder::new()
323    }
324
325    /// Create a spacer with flex factor 1 (expands to fill available space).
326    pub fn spacer() -> Self {
327        View::Spacer(SpacerNode { flex: 1, height: 0 })
328    }
329
330    /// Create a spacer with a specific flex factor.
331    pub fn spacer_flex(flex: u16) -> Self {
332        View::Spacer(SpacerNode { flex, height: 0 })
333    }
334
335    /// Create a fixed-height gap (blank lines).
336    ///
337    /// Unlike `spacer()` which expands to fill space, `gap()` is a fixed height.
338    ///
339    /// # Example
340    /// ```rust,ignore
341    /// View::vstack()
342    ///     .child(View::text("Header"))
343    ///     .child(View::gap(1))  // One blank line
344    ///     .child(View::text("Content"))
345    ///     .build()
346    /// ```
347    pub fn gap(height: u16) -> Self {
348        View::Spacer(SpacerNode { flex: 0, height })
349    }
350
351    /// Create a list builder.
352    pub fn list() -> ListBuilder {
353        ListBuilder::new()
354    }
355
356    /// Create a text input builder.
357    pub fn text_input() -> TextInputBuilder {
358        TextInputBuilder::new()
359    }
360
361    /// Create a checkbox builder.
362    pub fn checkbox() -> CheckboxBuilder {
363        CheckboxBuilder::new()
364    }
365
366    /// Create a radio group builder.
367    pub fn radio_group() -> RadioGroupBuilder {
368        RadioGroupBuilder::new()
369    }
370
371    /// Create a text area builder.
372    pub fn text_area() -> TextAreaBuilder {
373        TextAreaBuilder::new()
374    }
375
376    /// Create a modal dialog builder.
377    pub fn modal() -> ModalBuilder {
378        ModalBuilder::new()
379    }
380
381    /// Create a split pane builder.
382    pub fn split() -> SplitBuilder {
383        SplitBuilder::new()
384    }
385
386    /// Create a tabs builder.
387    pub fn tabs() -> TabsBuilder {
388        TabsBuilder::new()
389    }
390
391    /// Create a tree builder.
392    pub fn tree() -> TreeBuilder {
393        TreeBuilder::new()
394    }
395
396    /// Create a table builder.
397    pub fn table() -> TableBuilder {
398        TableBuilder::new()
399    }
400
401    /// Create a progress bar builder.
402    pub fn progress_bar() -> ProgressBarBuilder {
403        ProgressBarBuilder::new()
404    }
405
406    /// Create a status bar builder.
407    pub fn status_bar() -> StatusBarBuilder {
408        StatusBarBuilder::new()
409    }
410
411    /// Create a command palette builder.
412    pub fn command_palette() -> CommandPaletteBuilder {
413        CommandPaletteBuilder::new()
414    }
415
416    /// Create a menu bar builder.
417    pub fn menu_bar() -> MenuBarBuilder {
418        MenuBarBuilder::new()
419    }
420
421    /// Create a toast container builder.
422    pub fn toast_container() -> ToastContainerBuilder {
423        ToastContainerBuilder::new()
424    }
425
426    /// Create a form builder.
427    pub fn form() -> FormBuilder {
428        FormBuilder::new()
429    }
430
431    /// Create a form field builder.
432    pub fn form_field(name: impl Into<String>) -> FormFieldBuilder {
433        FormFieldBuilder::new(name)
434    }
435
436    /// Create a canvas builder for pixel-level drawing.
437    ///
438    /// **Experimental Feature**
439    ///
440    /// Canvas uses the Kitty graphics protocol for actual pixel rendering.
441    /// Requires a compatible terminal (Kitty, Ghostty, WezTerm).
442    /// Other terminals will show a placeholder message.
443    pub fn canvas() -> CanvasBuilder {
444        CanvasBuilder::new()
445    }
446
447    /// Create an image builder for displaying images.
448    ///
449    /// **Experimental Feature**
450    ///
451    /// Displays PNG, JPEG, or GIF images using the Kitty graphics protocol.
452    /// GIF animations are handled natively by Kitty.
453    /// Requires a compatible terminal (Kitty, Ghostty, WezTerm).
454    /// Other terminals will show alt text or a placeholder message.
455    pub fn image() -> ImageBuilder {
456        ImageBuilder::new()
457    }
458
459    /// Create a terminal builder for interactive PTY terminal emulation.
460    ///
461    /// **Status: Experimental Preview**
462    ///
463    /// Supports running shell commands (bash, vim, htop, etc.) with full
464    /// keyboard input and ANSI color/style rendering.
465    ///
466    /// # Known Limitations
467    ///
468    /// - No scrollback buffer
469    /// - No terminal resize support
470    /// - No copy/paste
471    /// - No mouse input
472    ///
473    /// Use for prototyping and experimentation. Breaking changes likely.
474    ///
475    /// # Example
476    /// ```rust,ignore
477    /// let terminal = cx.use_terminal();
478    /// if !terminal.is_started() {
479    ///     terminal.spawn("bash", &[], 80, 24);
480    /// }
481    /// View::terminal().handle(terminal).build()
482    /// ```
483    pub fn terminal() -> TerminalBuilder {
484        TerminalBuilder::new()
485    }
486
487    /// Create a custom widget view.
488    ///
489    /// Wraps a user-defined `Widget` implementation in a View.
490    /// Use this for custom character-cell rendering that can't be
491    /// composed from built-in widgets.
492    ///
493    /// # Example
494    /// ```rust,ignore
495    /// let my_widget = Rc::new(RefCell::new(MyWidget::new()));
496    /// View::custom(my_widget)
497    /// ```
498    pub fn custom(widget: Rc<RefCell<dyn Widget>>) -> Self {
499        View::Custom(CustomNode { widget })
500    }
501
502    /// Create a slider builder for bounded numeric values.
503    ///
504    /// # Example
505    /// ```rust,ignore
506    /// View::slider()
507    ///     .min(0.0)
508    ///     .max(127.0)
509    ///     .value(64.0)
510    ///     .step(1.0)
511    ///     .label("Volume")
512    ///     .on_change(move |v| vol.set(v))
513    ///     .build()
514    /// ```
515    pub fn slider() -> SliderBuilder {
516        SliderBuilder::new()
517    }
518
519    /// Create an error boundary builder.
520    ///
521    /// An error boundary catches panics in its child view and displays
522    /// a fallback view instead of crashing the application.
523    ///
524    /// # Example
525    /// ```rust,ignore
526    /// View::error_boundary()
527    ///     .child(risky_component_view)
528    ///     .fallback(View::text("Something went wrong"))
529    ///     .build()
530    /// ```
531    pub fn error_boundary() -> ErrorBoundaryBuilder {
532        ErrorBoundaryBuilder::new()
533    }
534
535    /// Create an empty view.
536    pub fn empty() -> Self {
537        View::Empty
538    }
539
540    /// Check if this view is focusable.
541    pub fn is_focusable(&self) -> bool {
542        match self {
543            View::Button(_) => true,
544            View::Box(node) => node.scroll,
545            View::List(_) => true,
546            View::TextInput(_) => true,
547            View::TextArea(_) => true,
548            View::Checkbox(_) => true,
549            View::RadioGroup(_) => true, // RadioGroup is focusable for option selection
550            View::Split(_) => false,     // Split is a layout container, not focusable itself
551            View::Tabs(_) => true,       // Tabs is focusable for tab switching
552            View::Tree(_) => true,       // Tree is focusable for navigation
553            View::Table(_) => true,      // Table is focusable for row selection
554            View::CommandPalette(_) => true, // Command palette captures all input when visible
555            View::MenuBar(_) => true,    // Menu bar is focusable for navigation
556            View::FormField(_) => true,  // Form fields are focusable for input
557            View::Terminal(_) => true,   // Terminal is focusable for PTY input
558            View::Slider(_) => true,    // Slider is focusable for value adjustment
559            _ => false,
560        }
561    }
562
563    /// Get the flex factor of this view (for layout).
564    pub fn flex(&self) -> u16 {
565        match self {
566            View::Box(n) => n.flex,
567            View::Spacer(n) => n.flex,
568            _ => 0,
569        }
570    }
571
572    /// Get the minimum height constraint, if any.
573    pub fn min_height(&self) -> Option<u16> {
574        match self {
575            View::Box(n) => n.min_height,
576            _ => None,
577        }
578    }
579
580    /// Get the maximum height constraint, if any.
581    pub fn max_height(&self) -> Option<u16> {
582        match self {
583            View::Box(n) => n.max_height,
584            _ => None,
585        }
586    }
587
588    /// Get the minimum width constraint, if any.
589    pub fn min_width(&self) -> Option<u16> {
590        match self {
591            View::Box(n) => n.min_width,
592            _ => None,
593        }
594    }
595
596    /// Get the maximum width constraint, if any.
597    pub fn max_width(&self) -> Option<u16> {
598        match self {
599            View::Box(n) => n.max_width,
600            _ => None,
601        }
602    }
603
604    /// Calculate the intrinsic (natural) height of this view based on its content.
605    /// Returns None for views that have no intrinsic height (flexible).
606    pub fn intrinsic_height(&self) -> Option<u16> {
607        match self {
608            View::Text(n) => Some(n.content.lines().count().max(1) as u16),
609            View::Button(_) => Some(1),
610            View::Box(n) => {
611                let border = if n.border { 2 } else { 0 };
612                let padding = n.padding * 2;
613                let inner = n
614                    .child
615                    .as_ref()
616                    .and_then(|c| c.intrinsic_height())
617                    .unwrap_or(0);
618                Some(inner + border + padding)
619            }
620            View::VStack(n) => {
621                if n.children.is_empty() {
622                    return Some(0);
623                }
624                let spacing = if n.children.len() > 1 {
625                    n.spacing * (n.children.len() as u16 - 1)
626                } else {
627                    0
628                };
629                let children_height: u16 =
630                    n.children.iter().filter_map(|c| c.intrinsic_height()).sum();
631                Some(children_height + spacing)
632            }
633            View::HStack(n) => {
634                // HStack height is max of children heights
635                n.children
636                    .iter()
637                    .filter_map(|c| c.intrinsic_height())
638                    .max()
639                    .or(Some(1))
640            }
641            View::List(n) => Some(n.items.len().max(1) as u16),
642            View::TextInput(_) => Some(1),
643            View::TextArea(n) => Some(n.rows),
644            View::Checkbox(_) => Some(1),
645            View::RadioGroup(n) => Some(n.options.len() as u16), // One row per option
646            View::Modal(_) => None, // Modal is an overlay, no intrinsic size
647            View::Spacer(n) => {
648                if n.flex == 0 {
649                    Some(n.height) // Fixed-height gap
650                } else {
651                    None // Flexible spacer expands
652                }
653            }
654            View::Split(_) => None,          // Split fills available space
655            View::Tabs(_) => None,           // Tabs fills available space
656            View::Tree(_) => None,           // Tree fills available space
657            View::Table(_) => None,          // Table fills available space
658            View::ProgressBar(_) => Some(1), // Progress bar is 1 row tall
659            View::StatusBar(_) => Some(1),   // Status bar is 1 row tall
660            View::CommandPalette(_) => None, // Command palette is an overlay
661            View::MenuBar(_) => Some(1),     // Menu bar is 1 row tall
662            View::ToastContainer(_) => None, // Toast container is an overlay
663            View::Form(n) => {
664                // Form height is sum of children
665                let children_height: u16 =
666                    n.children.iter().filter_map(|c| c.intrinsic_height()).sum();
667                Some(children_height)
668            }
669            View::FormField(n) => {
670                // Label (1) + input (1) + error (1 if present) = 2-3 rows
671                let base_height = 2u16; // Label + input
672                let error_height = if n.error.is_some() { 1 } else { 0 };
673                Some(base_height + error_height)
674            }
675            View::Canvas(n) => {
676                // Canvas height in cells (pixels / cell_height)
677                // Approximate: assume ~20 pixels per cell height
678                Some((n.pixel_height / 20).max(1))
679            }
680            View::Image(n) => {
681                // Image height based on detected dimensions or default
682                n.cell_height.or(Some(5))
683            }
684            View::Terminal(n) => {
685                // Terminal height is rows + border
686                let border = if n.border { 2 } else { 0 };
687                Some(n.rows as u16 + border)
688            }
689            View::ErrorBoundary(n) => n.child.intrinsic_height(),
690            View::Custom(n) => n.widget.borrow().height_hint(80), // Use default width hint
691            View::Slider(_) => Some(1), // Slider is a single row
692            View::Empty => Some(0),
693        }
694    }
695
696    /// Calculate the intrinsic (natural) width of this view based on its content.
697    /// Returns None for views that have no intrinsic width (flexible).
698    pub fn intrinsic_width(&self) -> Option<u16> {
699        match self {
700            View::Text(n) => {
701                let max_line_width = n.content.lines().map(|l| l.len()).max().unwrap_or(0);
702                Some(max_line_width as u16)
703            }
704            View::Button(n) => {
705                // [ label ] = 4 chars for brackets + spaces + label
706                Some(n.label.len() as u16 + 4)
707            }
708            View::Box(n) => {
709                let border = if n.border { 2 } else { 0 };
710                let padding = n.padding * 2;
711                let inner = n
712                    .child
713                    .as_ref()
714                    .and_then(|c| c.intrinsic_width())
715                    .unwrap_or(0);
716                Some(inner + border + padding)
717            }
718            View::VStack(n) => {
719                // VStack width is max of children widths
720                n.children
721                    .iter()
722                    .filter_map(|c| c.intrinsic_width())
723                    .max()
724                    .or(Some(1))
725            }
726            View::HStack(n) => {
727                if n.children.is_empty() {
728                    return Some(0);
729                }
730                let spacing = if n.children.len() > 1 {
731                    n.spacing * (n.children.len() as u16 - 1)
732                } else {
733                    0
734                };
735                let children_width: u16 =
736                    n.children.iter().filter_map(|c| c.intrinsic_width()).sum();
737                Some(children_width + spacing)
738            }
739            View::List(n) => {
740                // "> " prefix + max item length
741                let max_item = n.items.iter().map(|i| i.len()).max().unwrap_or(0);
742                Some(max_item as u16 + 2)
743            }
744            View::TextInput(_) => {
745                // TextInput should be sized by its container, not by content
746                // Return None to allow flex/container sizing with internal scrolling
747                None
748            }
749            View::TextArea(_) => {
750                // TextArea should be sized by its container, not by content
751                // Return None to allow flex/container sizing with internal scrolling
752                None
753            }
754            View::Checkbox(n) => {
755                // "[x] " + label
756                Some(n.label.len() as u16 + 4)
757            }
758            View::RadioGroup(n) => {
759                // "(o) " + longest option
760                let max_option = n.options.iter().map(|o| o.len()).max().unwrap_or(0);
761                Some(max_option as u16 + 4)
762            }
763            View::Modal(_) => None, // Modal is an overlay
764            View::Spacer(n) => {
765                if n.flex == 0 {
766                    Some(0) // Fixed-height gap has no width requirement
767                } else {
768                    None // Flexible spacer expands
769                }
770            }
771            View::Split(_) => None, // Split fills available space
772            View::Tabs(_) => None,  // Tabs fills available space
773            View::Tree(_) => None,  // Tree fills available space
774            View::Table(_) => None, // Table fills available space
775            View::ProgressBar(n) => {
776                // Label + bar width + percentage
777                let label_width = n.label.as_ref().map(|l| l.len() + 1).unwrap_or(0);
778                let bar_width = n.width.unwrap_or(10) as usize;
779                let percentage_width = if n.show_percentage { 5 } else { 0 };
780                Some((label_width + bar_width + percentage_width) as u16)
781            }
782            View::StatusBar(n) => {
783                // Left + center + right sections
784                let left_width = n.left.len();
785                let center_width = n.center.as_ref().map(|c| c.len()).unwrap_or(0);
786                let right_width = n.right.as_ref().map(|r| r.len()).unwrap_or(0);
787                // Minimum spacing between sections
788                let spacing = if center_width > 0 || right_width > 0 {
789                    2
790                } else {
791                    0
792                };
793                Some((left_width + center_width + right_width + spacing) as u16)
794            }
795            View::CommandPalette(_) => None, // Command palette is an overlay
796            View::MenuBar(n) => {
797                // Sum of menu labels + separators
798                let labels_width: usize = n.menus.iter().map(|m| m.label.len() + 3).sum(); // " Label "
799                Some(labels_width as u16)
800            }
801            View::ToastContainer(_) => None, // Toast container is an overlay
802            View::Form(n) => {
803                // Form width is max of children
804                n.children.iter().filter_map(|c| c.intrinsic_width()).max()
805            }
806            View::FormField(n) => {
807                // Width is max of label and input
808                let label_width = n.label.len() as u16;
809                let input_width = 20u16; // Default minimum input width
810                Some(label_width.max(input_width))
811            }
812            View::Canvas(n) => {
813                // Canvas width in cells (pixels / cell_width)
814                // Approximate: assume ~10 pixels per cell width
815                Some((n.pixel_width / 10).max(1))
816            }
817            View::Image(n) => {
818                // Image width based on detected dimensions or default
819                n.cell_width.or(Some(10))
820            }
821            View::Terminal(n) => {
822                // Terminal width is cols + border
823                let border = if n.border { 2 } else { 0 };
824                Some(n.cols as u16 + border)
825            }
826            View::ErrorBoundary(n) => n.child.intrinsic_width(),
827            View::Custom(n) => n.widget.borrow().width_hint(),
828            View::Slider(n) => {
829                // Label + brackets + track + value display
830                let label_len = n.label.as_ref().map(|l| l.len() + 1).unwrap_or(0) as u16;
831                Some(label_len + 20) // Reasonable default width
832            }
833            View::Empty => Some(0),
834        }
835    }
836}
837
838/// Orientation for split panes.
839#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
840pub enum Orientation {
841    /// Panes side by side: [first | second]
842    #[default]
843    Horizontal,
844    /// Panes stacked: [first] / [second]
845    Vertical,
846}
847
848/// Position of the tab bar.
849#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
850pub enum TabPosition {
851    /// Tab bar at the top (default).
852    #[default]
853    Top,
854    /// Tab bar at the bottom.
855    Bottom,
856}
857
858/// A text node containing string content with optional styling.
859#[derive(Debug, Clone)]
860pub struct TextNode {
861    pub content: String,
862    pub color: Option<crossterm::style::Color>,
863    pub bg_color: Option<crossterm::style::Color>,
864    pub bold: bool,
865    pub italic: bool,
866    pub underline: bool,
867    pub dim: bool,
868}
869
870/// A vertical stack container.
871#[derive(Debug, Clone)]
872pub struct VStackNode {
873    pub children: Vec<View>,
874    /// Spacing between children (in rows).
875    pub spacing: u16,
876    /// Justify content along main axis (vertical).
877    pub justify: Justify,
878    /// Align items along cross axis (horizontal).
879    pub align: Align,
880    /// Layout algorithm to use.
881    pub layout_mode: LayoutMode,
882}
883
884/// A horizontal stack container.
885#[derive(Debug, Clone)]
886pub struct HStackNode {
887    pub children: Vec<View>,
888    /// Spacing between children (in columns).
889    pub spacing: u16,
890    /// Justify content along main axis (horizontal).
891    pub justify: Justify,
892    /// Align items along cross axis (vertical).
893    pub align: Align,
894    /// Layout algorithm to use.
895    pub layout_mode: LayoutMode,
896}
897
898/// A container with optional border, padding, and flex sizing.
899#[derive(Debug, Clone)]
900pub struct BoxNode {
901    /// The child view inside the box.
902    pub child: Option<std::boxed::Box<View>>,
903    /// Whether to draw a border around the box.
904    pub border: bool,
905    /// Padding inside the box (all sides).
906    pub padding: u16,
907    /// Flex factor for layout (0 = fixed size, >0 = flexible).
908    pub flex: u16,
909    /// Whether this box is scrollable.
910    pub scroll: bool,
911    /// Automatically scroll to show bottom content (for chat-like UIs).
912    pub auto_scroll_bottom: bool,
913    /// Whether this box participates in focus navigation (default: true for scrollable boxes).
914    pub focusable: bool,
915    /// Minimum width constraint.
916    pub min_width: Option<u16>,
917    /// Maximum width constraint.
918    pub max_width: Option<u16>,
919    /// Minimum height constraint.
920    pub min_height: Option<u16>,
921    /// Maximum height constraint.
922    pub max_height: Option<u16>,
923}
924
925/// Flexible space that expands to fill available space.
926#[derive(Debug, Clone)]
927pub struct SpacerNode {
928    /// Flex factor (default 1). If 0, uses fixed height.
929    pub flex: u16,
930    /// Fixed height in rows (only used when flex is 0).
931    pub height: u16,
932}
933
934/// A button node.
935#[derive(Clone)]
936pub struct ButtonNode {
937    pub label: String,
938    pub on_press: Option<Callback>,
939}
940
941/// A selectable list node.
942#[derive(Clone)]
943pub struct ListNode {
944    /// The list items to display.
945    pub items: Vec<String>,
946    /// Currently selected index.
947    pub selected: usize,
948    /// Callback when selection changes.
949    pub on_select: Option<SelectCallback>,
950}
951
952/// A text input node.
953#[derive(Clone)]
954pub struct TextInputNode {
955    /// Current text value.
956    pub value: String,
957    /// Placeholder text shown when empty.
958    pub placeholder: String,
959    /// Callback when text changes.
960    pub on_change: Option<ChangeCallback>,
961    /// Callback when cursor position changes.
962    pub on_cursor_change: Option<CursorPosCallback>,
963    /// Callback when Enter is pressed (submit).
964    pub on_submit: Option<Callback>,
965    /// Callback when Up arrow is pressed.
966    pub on_key_up: Option<Callback>,
967    /// Callback when Down arrow is pressed.
968    pub on_key_down: Option<Callback>,
969    /// Cursor position within the text.
970    pub cursor_pos: usize,
971    /// Whether this input should have initial focus.
972    pub focused: bool,
973}
974
975/// A multi-line text area node.
976#[derive(Clone)]
977pub struct TextAreaNode {
978    /// Current text value (may contain newlines).
979    pub value: String,
980    /// Placeholder text shown when empty.
981    pub placeholder: String,
982    /// Callback when text changes.
983    pub on_change: Option<ChangeCallback>,
984    /// Callback when cursor position changes (line, column).
985    pub on_cursor_change: Option<CursorChangeCallback>,
986    /// Cursor line position.
987    pub cursor_line: usize,
988    /// Cursor column position.
989    pub cursor_col: usize,
990    /// Number of visible rows.
991    pub rows: u16,
992    /// Width at which to auto-wrap text (None = no wrap, text truncated at display edge).
993    pub wrap_width: Option<u16>,
994}
995
996/// A checkbox node.
997#[derive(Clone)]
998pub struct CheckboxNode {
999    /// Whether the checkbox is checked.
1000    pub checked: bool,
1001    /// Label displayed next to the checkbox.
1002    pub label: String,
1003    /// Callback when toggled.
1004    pub on_toggle: Option<ToggleCallback>,
1005}
1006
1007/// A radio group node (mutually exclusive options).
1008#[derive(Clone)]
1009pub struct RadioGroupNode {
1010    /// The available options.
1011    pub options: Vec<String>,
1012    /// Index of the currently selected option.
1013    pub selected: usize,
1014    /// Optional label for the group.
1015    pub label: Option<String>,
1016    /// Callback when selection changes.
1017    pub on_change: Option<SelectCallback>,
1018}
1019
1020/// A modal dialog node.
1021#[derive(Clone)]
1022pub struct ModalNode {
1023    /// Whether the modal is visible.
1024    pub visible: bool,
1025    /// Title of the modal (shown in border).
1026    pub title: String,
1027    /// The content view inside the modal.
1028    pub child: Option<std::boxed::Box<View>>,
1029    /// Callback when modal is dismissed (Escape key).
1030    pub on_dismiss: Option<Callback>,
1031    /// Width of the modal (percentage of screen, 0-100).
1032    pub width_percent: u16,
1033    /// Height of the modal (percentage of screen, 0-100).
1034    pub height_percent: u16,
1035}
1036
1037/// A split pane container node.
1038#[derive(Clone)]
1039pub struct SplitNode {
1040    /// Orientation of the split (horizontal or vertical).
1041    pub orientation: Orientation,
1042    /// First pane content.
1043    pub first: std::boxed::Box<View>,
1044    /// Second pane content.
1045    pub second: std::boxed::Box<View>,
1046    /// Split ratio (0.0 to 1.0, where 0.5 is equal split).
1047    pub ratio: f32,
1048    /// Minimum size for first pane (in cells).
1049    pub min_first: Option<u16>,
1050    /// Minimum size for second pane (in cells).
1051    pub min_second: Option<u16>,
1052    /// Whether to show a divider line between panes.
1053    pub show_divider: bool,
1054}
1055
1056/// A tabbed interface container node.
1057#[derive(Clone)]
1058pub struct TabsNode {
1059    /// Tab labels displayed in the tab bar.
1060    pub tabs: Vec<String>,
1061    /// Content views for each tab.
1062    pub children: Vec<View>,
1063    /// Currently active tab index.
1064    pub active: usize,
1065    /// Callback when tab changes.
1066    pub on_change: Option<SelectCallback>,
1067    /// Position of the tab bar (top or bottom).
1068    pub position: TabPosition,
1069}
1070
1071/// A single item in a tree view.
1072#[derive(Clone, Debug)]
1073pub struct TreeItem {
1074    /// Display label for this item.
1075    pub label: String,
1076    /// Optional icon displayed before the label.
1077    pub icon: Option<String>,
1078    /// Child items (empty for leaf nodes).
1079    pub children: Vec<TreeItem>,
1080    /// Whether this node is expanded (showing children).
1081    pub expanded: bool,
1082}
1083
1084impl TreeItem {
1085    /// Create a new tree item with the given label.
1086    pub fn new(label: impl Into<String>) -> Self {
1087        Self {
1088            label: label.into(),
1089            icon: None,
1090            children: Vec::new(),
1091            expanded: false,
1092        }
1093    }
1094
1095    /// Set the icon for this item.
1096    pub fn icon(mut self, icon: impl Into<String>) -> Self {
1097        self.icon = Some(icon.into());
1098        self
1099    }
1100
1101    /// Add a child item.
1102    pub fn child(mut self, child: TreeItem) -> Self {
1103        self.children.push(child);
1104        self
1105    }
1106
1107    /// Set whether this node is expanded.
1108    pub fn expanded(mut self, expanded: bool) -> Self {
1109        self.expanded = expanded;
1110        self
1111    }
1112
1113    /// Check if this is a leaf node (no children).
1114    pub fn is_leaf(&self) -> bool {
1115        self.children.is_empty()
1116    }
1117}
1118
1119/// A hierarchical tree view node.
1120#[derive(Clone)]
1121pub struct TreeNode {
1122    /// Root-level tree items.
1123    pub items: Vec<TreeItem>,
1124    /// Path to the currently selected item.
1125    pub selected: TreePath,
1126    /// Callback when selection changes.
1127    pub on_select: Option<TreeSelectCallback>,
1128    /// Callback when an item is activated (Enter/Space).
1129    pub on_activate: Option<TreeActivateCallback>,
1130}
1131
1132/// Text alignment for table columns.
1133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1134pub enum TextAlign {
1135    #[default]
1136    Left,
1137    Center,
1138    Right,
1139}
1140
1141/// Column width specification for tables.
1142#[derive(Debug, Clone, Copy, PartialEq, Default)]
1143pub enum ColumnWidth {
1144    /// Size to fit content (default).
1145    #[default]
1146    Auto,
1147    /// Fixed width in characters.
1148    Fixed(u16),
1149    /// Flex factor for remaining space.
1150    Flex(u16),
1151}
1152
1153/// A column definition for a table.
1154#[derive(Debug, Clone)]
1155pub struct TableColumn {
1156    /// Header text for this column.
1157    pub header: String,
1158    /// Width specification.
1159    pub width: ColumnWidth,
1160    /// Whether this column is sortable.
1161    pub sortable: bool,
1162    /// Text alignment for this column.
1163    pub align: TextAlign,
1164}
1165
1166impl TableColumn {
1167    /// Create a new table column with the given header.
1168    pub fn new(header: impl Into<String>) -> Self {
1169        Self {
1170            header: header.into(),
1171            width: ColumnWidth::Auto,
1172            sortable: false,
1173            align: TextAlign::Left,
1174        }
1175    }
1176
1177    /// Set the width of this column.
1178    pub fn width(mut self, width: ColumnWidth) -> Self {
1179        self.width = width;
1180        self
1181    }
1182
1183    /// Make this column sortable.
1184    pub fn sortable(mut self, sortable: bool) -> Self {
1185        self.sortable = sortable;
1186        self
1187    }
1188
1189    /// Set the text alignment for this column.
1190    pub fn align(mut self, align: TextAlign) -> Self {
1191        self.align = align;
1192        self
1193    }
1194}
1195
1196/// A data table node with columns and rows.
1197#[derive(Clone)]
1198pub struct TableNode {
1199    /// Column definitions.
1200    pub columns: Vec<TableColumn>,
1201    /// Row data (each row is a Vec of cell strings).
1202    pub rows: Vec<Vec<String>>,
1203    /// Currently selected row index.
1204    pub selected: usize,
1205    /// Current sort state: (column_index, ascending).
1206    pub sort: Option<(usize, bool)>,
1207    /// Callback when selection changes.
1208    pub on_select: Option<SelectCallback>,
1209    /// Callback when sort changes.
1210    pub on_sort: Option<SortCallback>,
1211    /// Callback when a row is activated (Enter).
1212    pub on_activate: Option<RowActivateCallback>,
1213}
1214
1215/// A progress bar node.
1216#[derive(Clone)]
1217pub struct ProgressBarNode {
1218    /// Progress value from 0.0 to 1.0.
1219    pub value: f32,
1220    /// Optional label shown before the bar.
1221    pub label: Option<String>,
1222    /// Whether to show percentage after the bar.
1223    pub show_percentage: bool,
1224    /// Fixed width of the bar portion (None = expand to fill).
1225    pub width: Option<u16>,
1226    /// Character used for the filled portion.
1227    pub filled_char: char,
1228    /// Character used for the empty portion.
1229    pub empty_char: char,
1230}
1231
1232/// A status bar node displayed at the bottom of the screen.
1233#[derive(Clone)]
1234pub struct StatusBarNode {
1235    /// Content for the left section.
1236    pub left: String,
1237    /// Content for the center section (optional).
1238    pub center: Option<String>,
1239    /// Content for the right section (optional).
1240    pub right: Option<String>,
1241    /// Background color for the status bar.
1242    pub bg_color: Option<crossterm::style::Color>,
1243    /// Foreground color for the status bar.
1244    pub fg_color: Option<crossterm::style::Color>,
1245}
1246
1247/// Builder for VStack views.
1248#[derive(Debug, Default)]
1249pub struct VStackBuilder {
1250    children: Vec<View>,
1251    spacing: u16,
1252    justify: Justify,
1253    align: Align,
1254    layout_mode: LayoutMode,
1255}
1256
1257impl VStackBuilder {
1258    pub fn new() -> Self {
1259        Self::default()
1260    }
1261
1262    pub fn child(mut self, view: View) -> Self {
1263        self.children.push(view);
1264        self
1265    }
1266
1267    pub fn spacing(mut self, spacing: u16) -> Self {
1268        self.spacing = spacing;
1269        self
1270    }
1271
1272    /// Set justify (main axis alignment for VStack = vertical).
1273    pub fn justify(mut self, justify: Justify) -> Self {
1274        self.justify = justify;
1275        self
1276    }
1277
1278    /// Set align (cross axis alignment for VStack = horizontal).
1279    pub fn align(mut self, align: Align) -> Self {
1280        self.align = align;
1281        self
1282    }
1283
1284    /// Set layout mode (algorithm for distributing space).
1285    pub fn layout_mode(mut self, mode: LayoutMode) -> Self {
1286        self.layout_mode = mode;
1287        self
1288    }
1289
1290    pub fn build(self) -> View {
1291        View::VStack(VStackNode {
1292            children: self.children,
1293            spacing: self.spacing,
1294            justify: self.justify,
1295            align: self.align,
1296            layout_mode: self.layout_mode,
1297        })
1298    }
1299}
1300
1301/// Builder for HStack views.
1302#[derive(Debug, Default)]
1303pub struct HStackBuilder {
1304    children: Vec<View>,
1305    spacing: u16,
1306    justify: Justify,
1307    align: Align,
1308    layout_mode: LayoutMode,
1309}
1310
1311impl HStackBuilder {
1312    pub fn new() -> Self {
1313        Self::default()
1314    }
1315
1316    pub fn child(mut self, view: View) -> Self {
1317        self.children.push(view);
1318        self
1319    }
1320
1321    pub fn spacing(mut self, spacing: u16) -> Self {
1322        self.spacing = spacing;
1323        self
1324    }
1325
1326    /// Set justify (main axis alignment for HStack = horizontal).
1327    pub fn justify(mut self, justify: Justify) -> Self {
1328        self.justify = justify;
1329        self
1330    }
1331
1332    /// Set align (cross axis alignment for HStack = vertical).
1333    pub fn align(mut self, align: Align) -> Self {
1334        self.align = align;
1335        self
1336    }
1337
1338    /// Set layout mode (algorithm for distributing space).
1339    pub fn layout_mode(mut self, mode: LayoutMode) -> Self {
1340        self.layout_mode = mode;
1341        self
1342    }
1343
1344    pub fn build(self) -> View {
1345        View::HStack(HStackNode {
1346            children: self.children,
1347            spacing: self.spacing,
1348            justify: self.justify,
1349            align: self.align,
1350            layout_mode: self.layout_mode,
1351        })
1352    }
1353}
1354
1355/// Builder for Button views.
1356#[derive(Default)]
1357pub struct ButtonBuilder {
1358    label: String,
1359    on_press: Option<Callback>,
1360}
1361
1362impl ButtonBuilder {
1363    pub fn new() -> Self {
1364        Self::default()
1365    }
1366
1367    pub fn label(mut self, label: impl Into<String>) -> Self {
1368        self.label = label.into();
1369        self
1370    }
1371
1372    pub fn on_press(mut self, callback: impl Fn() + 'static) -> Self {
1373        self.on_press = Some(Rc::new(callback));
1374        self
1375    }
1376
1377    pub fn build(self) -> View {
1378        View::Button(ButtonNode {
1379            label: self.label,
1380            on_press: self.on_press,
1381        })
1382    }
1383}
1384
1385/// Builder for Box views.
1386#[derive(Default)]
1387pub struct BoxBuilder {
1388    child: Option<View>,
1389    border: bool,
1390    padding: u16,
1391    flex: u16,
1392    scroll: bool,
1393    auto_scroll_bottom: bool,
1394    focusable: Option<bool>,
1395    min_width: Option<u16>,
1396    max_width: Option<u16>,
1397    min_height: Option<u16>,
1398    max_height: Option<u16>,
1399}
1400
1401impl BoxBuilder {
1402    pub fn new() -> Self {
1403        Self::default()
1404    }
1405
1406    pub fn child(mut self, view: View) -> Self {
1407        self.child = Some(view);
1408        self
1409    }
1410
1411    pub fn border(mut self, border: bool) -> Self {
1412        self.border = border;
1413        self
1414    }
1415
1416    pub fn padding(mut self, padding: u16) -> Self {
1417        self.padding = padding;
1418        self
1419    }
1420
1421    pub fn flex(mut self, flex: u16) -> Self {
1422        self.flex = flex;
1423        self
1424    }
1425
1426    pub fn scroll(mut self, scroll: bool) -> Self {
1427        self.scroll = scroll;
1428        self
1429    }
1430
1431    /// Enable auto-scrolling to bottom (for chat-like UIs).
1432    pub fn auto_scroll_bottom(mut self, auto_scroll: bool) -> Self {
1433        self.auto_scroll_bottom = auto_scroll;
1434        self
1435    }
1436
1437    /// Set whether this box participates in focus navigation.
1438    /// By default, scrollable boxes are focusable. Use `focusable(false)` to
1439    /// disable focus for a scrollable box (e.g., auto-scroll chat messages).
1440    pub fn focusable(mut self, focusable: bool) -> Self {
1441        self.focusable = Some(focusable);
1442        self
1443    }
1444
1445    pub fn min_width(mut self, width: u16) -> Self {
1446        self.min_width = Some(width);
1447        self
1448    }
1449
1450    pub fn max_width(mut self, width: u16) -> Self {
1451        self.max_width = Some(width);
1452        self
1453    }
1454
1455    pub fn min_height(mut self, height: u16) -> Self {
1456        self.min_height = Some(height);
1457        self
1458    }
1459
1460    pub fn max_height(mut self, height: u16) -> Self {
1461        self.max_height = Some(height);
1462        self
1463    }
1464
1465    pub fn build(self) -> View {
1466        // Scrollable boxes are focusable by default so users can scroll back
1467        let default_focusable = self.scroll || self.auto_scroll_bottom;
1468        View::Box(BoxNode {
1469            child: self.child.map(std::boxed::Box::new),
1470            border: self.border,
1471            padding: self.padding,
1472            flex: self.flex,
1473            scroll: self.scroll,
1474            auto_scroll_bottom: self.auto_scroll_bottom,
1475            focusable: self.focusable.unwrap_or(default_focusable),
1476            min_width: self.min_width,
1477            max_width: self.max_width,
1478            min_height: self.min_height,
1479            max_height: self.max_height,
1480        })
1481    }
1482}
1483
1484/// Builder for List views.
1485#[derive(Default)]
1486pub struct ListBuilder {
1487    items: Vec<String>,
1488    selected: usize,
1489    on_select: Option<SelectCallback>,
1490}
1491
1492impl ListBuilder {
1493    pub fn new() -> Self {
1494        Self::default()
1495    }
1496
1497    pub fn items(mut self, items: Vec<String>) -> Self {
1498        self.items = items;
1499        self
1500    }
1501
1502    pub fn selected(mut self, selected: usize) -> Self {
1503        self.selected = selected;
1504        self
1505    }
1506
1507    pub fn on_select(mut self, callback: impl Fn(usize) + 'static) -> Self {
1508        self.on_select = Some(Rc::new(callback));
1509        self
1510    }
1511
1512    pub fn build(self) -> View {
1513        View::List(ListNode {
1514            items: self.items,
1515            selected: self.selected,
1516            on_select: self.on_select,
1517        })
1518    }
1519}
1520
1521/// Builder for TextInput views.
1522#[derive(Default)]
1523pub struct TextInputBuilder {
1524    value: String,
1525    placeholder: String,
1526    on_change: Option<ChangeCallback>,
1527    on_cursor_change: Option<CursorPosCallback>,
1528    on_submit: Option<Callback>,
1529    on_key_up: Option<Callback>,
1530    on_key_down: Option<Callback>,
1531    cursor_pos: usize,
1532    focused: bool,
1533}
1534
1535impl TextInputBuilder {
1536    pub fn new() -> Self {
1537        Self::default()
1538    }
1539
1540    pub fn value(mut self, value: impl Into<String>) -> Self {
1541        self.value = value.into();
1542        // Default cursor to end of value
1543        self.cursor_pos = self.value.len();
1544        self
1545    }
1546
1547    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1548        self.placeholder = placeholder.into();
1549        self
1550    }
1551
1552    pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
1553        self.on_change = Some(Rc::new(callback));
1554        self
1555    }
1556
1557    pub fn on_cursor_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
1558        self.on_cursor_change = Some(Rc::new(callback));
1559        self
1560    }
1561
1562    pub fn on_submit(mut self, callback: impl Fn() + 'static) -> Self {
1563        self.on_submit = Some(Rc::new(callback));
1564        self
1565    }
1566
1567    /// Set callback for when Up arrow is pressed (e.g., for command history).
1568    pub fn on_key_up(mut self, callback: impl Fn() + 'static) -> Self {
1569        self.on_key_up = Some(Rc::new(callback));
1570        self
1571    }
1572
1573    /// Set callback for when Down arrow is pressed (e.g., for command history).
1574    pub fn on_key_down(mut self, callback: impl Fn() + 'static) -> Self {
1575        self.on_key_down = Some(Rc::new(callback));
1576        self
1577    }
1578
1579    pub fn cursor(mut self, pos: usize) -> Self {
1580        self.cursor_pos = pos;
1581        self
1582    }
1583
1584    /// Set this input to have initial focus when the app starts.
1585    pub fn focused(mut self, focused: bool) -> Self {
1586        self.focused = focused;
1587        self
1588    }
1589
1590    pub fn build(self) -> View {
1591        View::TextInput(TextInputNode {
1592            value: self.value.clone(),
1593            placeholder: self.placeholder,
1594            on_change: self.on_change,
1595            on_cursor_change: self.on_cursor_change,
1596            on_submit: self.on_submit,
1597            on_key_up: self.on_key_up,
1598            on_key_down: self.on_key_down,
1599            cursor_pos: self.cursor_pos.min(self.value.len()),
1600            focused: self.focused,
1601        })
1602    }
1603}
1604
1605/// Builder for Checkbox views.
1606#[derive(Default)]
1607pub struct CheckboxBuilder {
1608    checked: bool,
1609    label: String,
1610    on_toggle: Option<ToggleCallback>,
1611}
1612
1613impl CheckboxBuilder {
1614    pub fn new() -> Self {
1615        Self::default()
1616    }
1617
1618    pub fn checked(mut self, checked: bool) -> Self {
1619        self.checked = checked;
1620        self
1621    }
1622
1623    pub fn label(mut self, label: impl Into<String>) -> Self {
1624        self.label = label.into();
1625        self
1626    }
1627
1628    pub fn on_toggle(mut self, callback: impl Fn(bool) + 'static) -> Self {
1629        self.on_toggle = Some(Rc::new(callback));
1630        self
1631    }
1632
1633    pub fn build(self) -> View {
1634        View::Checkbox(CheckboxNode {
1635            checked: self.checked,
1636            label: self.label,
1637            on_toggle: self.on_toggle,
1638        })
1639    }
1640}
1641
1642/// Builder for RadioGroup views.
1643#[derive(Default)]
1644pub struct RadioGroupBuilder {
1645    options: Vec<String>,
1646    selected: usize,
1647    label: Option<String>,
1648    on_change: Option<SelectCallback>,
1649}
1650
1651impl RadioGroupBuilder {
1652    pub fn new() -> Self {
1653        Self::default()
1654    }
1655
1656    /// Set the available options.
1657    pub fn options(mut self, options: Vec<impl Into<String>>) -> Self {
1658        self.options = options.into_iter().map(|s| s.into()).collect();
1659        self
1660    }
1661
1662    /// Add a single option.
1663    pub fn option(mut self, option: impl Into<String>) -> Self {
1664        self.options.push(option.into());
1665        self
1666    }
1667
1668    /// Set the currently selected option index.
1669    pub fn selected(mut self, selected: usize) -> Self {
1670        self.selected = selected;
1671        self
1672    }
1673
1674    /// Set an optional label for the group.
1675    pub fn label(mut self, label: impl Into<String>) -> Self {
1676        self.label = Some(label.into());
1677        self
1678    }
1679
1680    /// Set the callback when selection changes.
1681    pub fn on_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
1682        self.on_change = Some(Rc::new(callback));
1683        self
1684    }
1685
1686    pub fn build(self) -> View {
1687        View::RadioGroup(RadioGroupNode {
1688            options: self.options,
1689            selected: self.selected,
1690            label: self.label,
1691            on_change: self.on_change,
1692        })
1693    }
1694}
1695
1696/// Builder for styled Text views.
1697#[derive(Debug, Default)]
1698pub struct TextBuilder {
1699    content: String,
1700    color: Option<crossterm::style::Color>,
1701    bg_color: Option<crossterm::style::Color>,
1702    bold: bool,
1703    italic: bool,
1704    underline: bool,
1705    dim: bool,
1706}
1707
1708impl TextBuilder {
1709    pub fn new(content: impl Into<String>) -> Self {
1710        Self {
1711            content: content.into(),
1712            ..Default::default()
1713        }
1714    }
1715
1716    /// Set the text color.
1717    pub fn color(mut self, color: crossterm::style::Color) -> Self {
1718        self.color = Some(color);
1719        self
1720    }
1721
1722    /// Set the background color.
1723    pub fn bg(mut self, color: crossterm::style::Color) -> Self {
1724        self.bg_color = Some(color);
1725        self
1726    }
1727
1728    /// Make the text bold.
1729    pub fn bold(mut self) -> Self {
1730        self.bold = true;
1731        self
1732    }
1733
1734    /// Make the text italic.
1735    pub fn italic(mut self) -> Self {
1736        self.italic = true;
1737        self
1738    }
1739
1740    /// Underline the text.
1741    pub fn underline(mut self) -> Self {
1742        self.underline = true;
1743        self
1744    }
1745
1746    /// Make the text dim/faded.
1747    pub fn dim(mut self) -> Self {
1748        self.dim = true;
1749        self
1750    }
1751
1752    pub fn build(self) -> View {
1753        View::Text(TextNode {
1754            content: self.content,
1755            color: self.color,
1756            bg_color: self.bg_color,
1757            bold: self.bold,
1758            italic: self.italic,
1759            underline: self.underline,
1760            dim: self.dim,
1761        })
1762    }
1763}
1764
1765/// Builder for TextArea views.
1766#[derive(Default)]
1767pub struct TextAreaBuilder {
1768    value: String,
1769    placeholder: String,
1770    on_change: Option<ChangeCallback>,
1771    on_cursor_change: Option<CursorChangeCallback>,
1772    cursor_line: usize,
1773    cursor_col: usize,
1774    rows: u16,
1775    wrap_width: Option<u16>,
1776}
1777
1778impl TextAreaBuilder {
1779    pub fn new() -> Self {
1780        Self {
1781            rows: 5, // Default to 5 rows
1782            ..Default::default()
1783        }
1784    }
1785
1786    /// Set the current text value.
1787    pub fn value(mut self, value: impl Into<String>) -> Self {
1788        self.value = value.into();
1789        self
1790    }
1791
1792    /// Set the placeholder text shown when empty.
1793    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1794        self.placeholder = placeholder.into();
1795        self
1796    }
1797
1798    /// Set the callback for when text changes.
1799    pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
1800        self.on_change = Some(Rc::new(callback));
1801        self
1802    }
1803
1804    /// Set the callback for when cursor position changes.
1805    pub fn on_cursor_change(mut self, callback: impl Fn(usize, usize) + 'static) -> Self {
1806        self.on_cursor_change = Some(Rc::new(callback));
1807        self
1808    }
1809
1810    /// Set the cursor line position.
1811    pub fn cursor_line(mut self, line: usize) -> Self {
1812        self.cursor_line = line;
1813        self
1814    }
1815
1816    /// Set the cursor column position.
1817    pub fn cursor_col(mut self, col: usize) -> Self {
1818        self.cursor_col = col;
1819        self
1820    }
1821
1822    /// Set the number of visible rows.
1823    pub fn rows(mut self, rows: u16) -> Self {
1824        self.rows = rows;
1825        self
1826    }
1827
1828    /// Set the width at which text automatically wraps to the next line.
1829    /// If not set, text is truncated at the display edge without wrapping.
1830    pub fn wrap_width(mut self, width: u16) -> Self {
1831        self.wrap_width = Some(width);
1832        self
1833    }
1834
1835    pub fn build(self) -> View {
1836        View::TextArea(TextAreaNode {
1837            value: self.value,
1838            placeholder: self.placeholder,
1839            on_change: self.on_change,
1840            on_cursor_change: self.on_cursor_change,
1841            cursor_line: self.cursor_line,
1842            cursor_col: self.cursor_col,
1843            rows: self.rows,
1844            wrap_width: self.wrap_width,
1845        })
1846    }
1847}
1848
1849/// Builder for Modal views.
1850#[derive(Default)]
1851pub struct ModalBuilder {
1852    visible: bool,
1853    title: String,
1854    child: Option<View>,
1855    on_dismiss: Option<Callback>,
1856    width_percent: u16,
1857    height_percent: u16,
1858}
1859
1860impl ModalBuilder {
1861    pub fn new() -> Self {
1862        Self {
1863            width_percent: 60,
1864            height_percent: 50,
1865            ..Default::default()
1866        }
1867    }
1868
1869    /// Set whether the modal is visible.
1870    pub fn visible(mut self, visible: bool) -> Self {
1871        self.visible = visible;
1872        self
1873    }
1874
1875    /// Set the title shown in the modal border.
1876    pub fn title(mut self, title: impl Into<String>) -> Self {
1877        self.title = title.into();
1878        self
1879    }
1880
1881    /// Set the content of the modal.
1882    pub fn child(mut self, view: View) -> Self {
1883        self.child = Some(view);
1884        self
1885    }
1886
1887    /// Set the callback when modal is dismissed (Escape key).
1888    pub fn on_dismiss(mut self, callback: impl Fn() + 'static) -> Self {
1889        self.on_dismiss = Some(Rc::new(callback));
1890        self
1891    }
1892
1893    /// Set the width as percentage of screen (0-100).
1894    pub fn width(mut self, percent: u16) -> Self {
1895        self.width_percent = percent.min(100);
1896        self
1897    }
1898
1899    /// Set the height as percentage of screen (0-100).
1900    pub fn height(mut self, percent: u16) -> Self {
1901        self.height_percent = percent.min(100);
1902        self
1903    }
1904
1905    pub fn build(self) -> View {
1906        View::Modal(ModalNode {
1907            visible: self.visible,
1908            title: self.title,
1909            child: self.child.map(std::boxed::Box::new),
1910            on_dismiss: self.on_dismiss,
1911            width_percent: self.width_percent,
1912            height_percent: self.height_percent,
1913        })
1914    }
1915}
1916
1917/// Builder for Split pane views.
1918#[derive(Default)]
1919pub struct SplitBuilder {
1920    orientation: Orientation,
1921    first: Option<View>,
1922    second: Option<View>,
1923    ratio: f32,
1924    min_first: Option<u16>,
1925    min_second: Option<u16>,
1926    show_divider: bool,
1927}
1928
1929impl SplitBuilder {
1930    pub fn new() -> Self {
1931        Self {
1932            ratio: 0.5, // Default to equal split
1933            show_divider: true,
1934            ..Default::default()
1935        }
1936    }
1937
1938    /// Set the orientation to horizontal (side by side).
1939    pub fn horizontal(mut self) -> Self {
1940        self.orientation = Orientation::Horizontal;
1941        self
1942    }
1943
1944    /// Set the orientation to vertical (stacked).
1945    pub fn vertical(mut self) -> Self {
1946        self.orientation = Orientation::Vertical;
1947        self
1948    }
1949
1950    /// Set the first pane content.
1951    pub fn first(mut self, view: View) -> Self {
1952        self.first = Some(view);
1953        self
1954    }
1955
1956    /// Set the second pane content.
1957    pub fn second(mut self, view: View) -> Self {
1958        self.second = Some(view);
1959        self
1960    }
1961
1962    /// Set the split ratio (0.0 to 1.0, where 0.5 is equal split).
1963    pub fn ratio(mut self, ratio: f32) -> Self {
1964        self.ratio = ratio.clamp(0.0, 1.0);
1965        self
1966    }
1967
1968    /// Set the minimum size for the first pane (in cells).
1969    pub fn min_first(mut self, min: u16) -> Self {
1970        self.min_first = Some(min);
1971        self
1972    }
1973
1974    /// Set the minimum size for the second pane (in cells).
1975    pub fn min_second(mut self, min: u16) -> Self {
1976        self.min_second = Some(min);
1977        self
1978    }
1979
1980    /// Set whether to show a divider line between panes.
1981    pub fn show_divider(mut self, show: bool) -> Self {
1982        self.show_divider = show;
1983        self
1984    }
1985
1986    pub fn build(self) -> View {
1987        View::Split(SplitNode {
1988            orientation: self.orientation,
1989            first: std::boxed::Box::new(self.first.unwrap_or(View::Empty)),
1990            second: std::boxed::Box::new(self.second.unwrap_or(View::Empty)),
1991            ratio: self.ratio,
1992            min_first: self.min_first,
1993            min_second: self.min_second,
1994            show_divider: self.show_divider,
1995        })
1996    }
1997}
1998
1999/// Builder for Tabs views.
2000#[derive(Default)]
2001pub struct TabsBuilder {
2002    tabs: Vec<String>,
2003    children: Vec<View>,
2004    active: usize,
2005    on_change: Option<SelectCallback>,
2006    position: TabPosition,
2007}
2008
2009impl TabsBuilder {
2010    pub fn new() -> Self {
2011        Self::default()
2012    }
2013
2014    /// Add a tab with a label and content view.
2015    pub fn tab(mut self, label: impl Into<String>, content: View) -> Self {
2016        self.tabs.push(label.into());
2017        self.children.push(content);
2018        self
2019    }
2020
2021    /// Set the active tab index.
2022    pub fn active(mut self, index: usize) -> Self {
2023        self.active = index;
2024        self
2025    }
2026
2027    /// Set the callback when tab changes.
2028    pub fn on_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
2029        self.on_change = Some(Rc::new(callback));
2030        self
2031    }
2032
2033    /// Set the position of the tab bar.
2034    pub fn position(mut self, position: TabPosition) -> Self {
2035        self.position = position;
2036        self
2037    }
2038
2039    pub fn build(self) -> View {
2040        View::Tabs(TabsNode {
2041            tabs: self.tabs,
2042            children: self.children,
2043            active: self.active,
2044            on_change: self.on_change,
2045            position: self.position,
2046        })
2047    }
2048}
2049
2050/// Builder for Tree views.
2051#[derive(Default)]
2052pub struct TreeBuilder {
2053    items: Vec<TreeItem>,
2054    selected: TreePath,
2055    on_select: Option<TreeSelectCallback>,
2056    on_activate: Option<TreeActivateCallback>,
2057}
2058
2059impl TreeBuilder {
2060    pub fn new() -> Self {
2061        Self::default()
2062    }
2063
2064    /// Set the tree items.
2065    pub fn items(mut self, items: Vec<TreeItem>) -> Self {
2066        self.items = items;
2067        self
2068    }
2069
2070    /// Add a single root item.
2071    pub fn item(mut self, item: TreeItem) -> Self {
2072        self.items.push(item);
2073        self
2074    }
2075
2076    /// Set the selected path.
2077    pub fn selected(mut self, path: TreePath) -> Self {
2078        self.selected = path;
2079        self
2080    }
2081
2082    /// Set the callback when selection changes.
2083    pub fn on_select(mut self, callback: impl Fn(TreePath) + 'static) -> Self {
2084        self.on_select = Some(Rc::new(callback));
2085        self
2086    }
2087
2088    /// Set the callback when an item is activated.
2089    pub fn on_activate(mut self, callback: impl Fn(TreePath) + 'static) -> Self {
2090        self.on_activate = Some(Rc::new(callback));
2091        self
2092    }
2093
2094    pub fn build(self) -> View {
2095        View::Tree(TreeNode {
2096            items: self.items,
2097            selected: self.selected,
2098            on_select: self.on_select,
2099            on_activate: self.on_activate,
2100        })
2101    }
2102}
2103
2104/// Builder for Table views.
2105#[derive(Default)]
2106pub struct TableBuilder {
2107    columns: Vec<TableColumn>,
2108    rows: Vec<Vec<String>>,
2109    selected: usize,
2110    sort: Option<(usize, bool)>,
2111    on_select: Option<SelectCallback>,
2112    on_sort: Option<SortCallback>,
2113    on_activate: Option<RowActivateCallback>,
2114}
2115
2116impl TableBuilder {
2117    pub fn new() -> Self {
2118        Self::default()
2119    }
2120
2121    /// Add a column with just a header (auto width, left aligned, not sortable).
2122    pub fn column(mut self, header: impl Into<String>) -> Self {
2123        self.columns.push(TableColumn::new(header));
2124        self
2125    }
2126
2127    /// Add a column with full configuration.
2128    pub fn column_with(mut self, column: TableColumn) -> Self {
2129        self.columns.push(column);
2130        self
2131    }
2132
2133    /// Set the row data.
2134    pub fn rows(mut self, rows: Vec<Vec<String>>) -> Self {
2135        self.rows = rows;
2136        self
2137    }
2138
2139    /// Add a single row.
2140    pub fn row(mut self, row: Vec<String>) -> Self {
2141        self.rows.push(row);
2142        self
2143    }
2144
2145    /// Set the selected row index.
2146    pub fn selected(mut self, index: usize) -> Self {
2147        self.selected = index;
2148        self
2149    }
2150
2151    /// Set the sort state (column index, ascending).
2152    pub fn sort(mut self, sort: Option<(usize, bool)>) -> Self {
2153        self.sort = sort;
2154        self
2155    }
2156
2157    /// Set the sort state with explicit column and direction.
2158    pub fn sort_by(mut self, column: usize, ascending: bool) -> Self {
2159        self.sort = Some((column, ascending));
2160        self
2161    }
2162
2163    /// Set the callback when selection changes.
2164    pub fn on_select(mut self, callback: impl Fn(usize) + 'static) -> Self {
2165        self.on_select = Some(Rc::new(callback));
2166        self
2167    }
2168
2169    /// Set the callback when sort changes.
2170    pub fn on_sort(mut self, callback: impl Fn(usize, bool) + 'static) -> Self {
2171        self.on_sort = Some(Rc::new(callback));
2172        self
2173    }
2174
2175    /// Set the callback when a row is activated.
2176    pub fn on_activate(mut self, callback: impl Fn(usize) + 'static) -> Self {
2177        self.on_activate = Some(Rc::new(callback));
2178        self
2179    }
2180
2181    pub fn build(self) -> View {
2182        View::Table(TableNode {
2183            columns: self.columns,
2184            rows: self.rows,
2185            selected: self.selected,
2186            sort: self.sort,
2187            on_select: self.on_select,
2188            on_sort: self.on_sort,
2189            on_activate: self.on_activate,
2190        })
2191    }
2192}
2193
2194/// Builder for ProgressBar views.
2195#[derive(Debug, Clone)]
2196pub struct ProgressBarBuilder {
2197    value: f32,
2198    label: Option<String>,
2199    show_percentage: bool,
2200    width: Option<u16>,
2201    filled_char: char,
2202    empty_char: char,
2203}
2204
2205impl Default for ProgressBarBuilder {
2206    fn default() -> Self {
2207        Self {
2208            value: 0.0,
2209            label: None,
2210            show_percentage: true,
2211            width: None,
2212            filled_char: 'â–ˆ',
2213            empty_char: 'â–‘',
2214        }
2215    }
2216}
2217
2218impl ProgressBarBuilder {
2219    pub fn new() -> Self {
2220        Self::default()
2221    }
2222
2223    /// Set the progress value (0.0 to 1.0).
2224    pub fn value(mut self, value: f32) -> Self {
2225        self.value = value.clamp(0.0, 1.0);
2226        self
2227    }
2228
2229    /// Set a label shown before the bar.
2230    pub fn label(mut self, label: impl Into<String>) -> Self {
2231        self.label = Some(label.into());
2232        self
2233    }
2234
2235    /// Set whether to show percentage after the bar (default: true).
2236    pub fn show_percentage(mut self, show: bool) -> Self {
2237        self.show_percentage = show;
2238        self
2239    }
2240
2241    /// Set a fixed width for the bar portion.
2242    /// If not set, the bar expands to fill available space.
2243    pub fn width(mut self, width: u16) -> Self {
2244        self.width = Some(width);
2245        self
2246    }
2247
2248    /// Set the character used for the filled portion (default: â–ˆ).
2249    pub fn filled_char(mut self, ch: char) -> Self {
2250        self.filled_char = ch;
2251        self
2252    }
2253
2254    /// Set the character used for the empty portion (default: â–‘).
2255    pub fn empty_char(mut self, ch: char) -> Self {
2256        self.empty_char = ch;
2257        self
2258    }
2259
2260    pub fn build(self) -> View {
2261        View::ProgressBar(ProgressBarNode {
2262            value: self.value,
2263            label: self.label,
2264            show_percentage: self.show_percentage,
2265            width: self.width,
2266            filled_char: self.filled_char,
2267            empty_char: self.empty_char,
2268        })
2269    }
2270}
2271
2272/// Builder for StatusBar views.
2273#[derive(Debug, Clone, Default)]
2274pub struct StatusBarBuilder {
2275    left: String,
2276    center: Option<String>,
2277    right: Option<String>,
2278    bg_color: Option<crossterm::style::Color>,
2279    fg_color: Option<crossterm::style::Color>,
2280}
2281
2282impl StatusBarBuilder {
2283    pub fn new() -> Self {
2284        Self::default()
2285    }
2286
2287    /// Set the left section content.
2288    pub fn left(mut self, content: impl Into<String>) -> Self {
2289        self.left = content.into();
2290        self
2291    }
2292
2293    /// Set the center section content.
2294    pub fn center(mut self, content: impl Into<String>) -> Self {
2295        self.center = Some(content.into());
2296        self
2297    }
2298
2299    /// Set the right section content.
2300    pub fn right(mut self, content: impl Into<String>) -> Self {
2301        self.right = Some(content.into());
2302        self
2303    }
2304
2305    /// Set the background color.
2306    pub fn bg(mut self, color: crossterm::style::Color) -> Self {
2307        self.bg_color = Some(color);
2308        self
2309    }
2310
2311    /// Set the foreground (text) color.
2312    pub fn fg(mut self, color: crossterm::style::Color) -> Self {
2313        self.fg_color = Some(color);
2314        self
2315    }
2316
2317    pub fn build(self) -> View {
2318        View::StatusBar(StatusBarNode {
2319            left: self.left,
2320            center: self.center,
2321            right: self.right,
2322            bg_color: self.bg_color,
2323            fg_color: self.fg_color,
2324        })
2325    }
2326}
2327
2328// =============================================================================
2329// Command Palette
2330// =============================================================================
2331
2332/// A command in the command palette.
2333#[derive(Clone)]
2334pub struct PaletteCommand {
2335    /// Unique identifier for the command.
2336    pub id: &'static str,
2337    /// Display label.
2338    pub label: String,
2339    /// Optional keyboard shortcut display (e.g., "Ctrl+S").
2340    pub shortcut: Option<String>,
2341    /// Optional category for grouping.
2342    pub category: Option<String>,
2343}
2344
2345impl PaletteCommand {
2346    /// Create a new palette command.
2347    pub fn new(id: &'static str, label: impl Into<String>) -> Self {
2348        Self {
2349            id,
2350            label: label.into(),
2351            shortcut: None,
2352            category: None,
2353        }
2354    }
2355
2356    /// Set the shortcut display string.
2357    pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
2358        self.shortcut = Some(shortcut.into());
2359        self
2360    }
2361
2362    /// Set the category.
2363    pub fn category(mut self, category: impl Into<String>) -> Self {
2364        self.category = Some(category.into());
2365        self
2366    }
2367}
2368
2369/// A command palette overlay for searching and executing commands.
2370#[derive(Clone)]
2371pub struct CommandPaletteNode {
2372    /// Whether the palette is visible.
2373    pub visible: bool,
2374    /// Current search query.
2375    pub query: String,
2376    /// Available commands.
2377    pub commands: Vec<PaletteCommand>,
2378    /// Currently selected index in the filtered list.
2379    pub selected: usize,
2380    /// Callback when query changes.
2381    pub on_query_change: Option<ChangeCallback>,
2382    /// Callback when a command is selected (receives command ID).
2383    pub on_select: Option<CommandCallback>,
2384    /// Callback when the palette is dismissed.
2385    pub on_dismiss: Option<Callback>,
2386    /// Width percentage (0-100).
2387    pub width_percent: u16,
2388    /// Height percentage (0-100).
2389    pub height_percent: u16,
2390}
2391
2392/// Builder for CommandPalette views.
2393#[derive(Default)]
2394pub struct CommandPaletteBuilder {
2395    visible: bool,
2396    query: String,
2397    commands: Vec<PaletteCommand>,
2398    selected: usize,
2399    on_query_change: Option<ChangeCallback>,
2400    on_select: Option<CommandCallback>,
2401    on_dismiss: Option<Callback>,
2402    width_percent: u16,
2403    height_percent: u16,
2404}
2405
2406impl CommandPaletteBuilder {
2407    pub fn new() -> Self {
2408        Self {
2409            width_percent: 50,
2410            height_percent: 60,
2411            ..Default::default()
2412        }
2413    }
2414
2415    /// Set whether the palette is visible.
2416    pub fn visible(mut self, visible: bool) -> Self {
2417        self.visible = visible;
2418        self
2419    }
2420
2421    /// Set the current query.
2422    pub fn query(mut self, query: impl Into<String>) -> Self {
2423        self.query = query.into();
2424        self
2425    }
2426
2427    /// Set the available commands.
2428    pub fn commands(mut self, commands: Vec<PaletteCommand>) -> Self {
2429        self.commands = commands;
2430        self
2431    }
2432
2433    /// Add a single command.
2434    pub fn command(mut self, command: PaletteCommand) -> Self {
2435        self.commands.push(command);
2436        self
2437    }
2438
2439    /// Set the selected index.
2440    pub fn selected(mut self, selected: usize) -> Self {
2441        self.selected = selected;
2442        self
2443    }
2444
2445    /// Set the callback for query changes.
2446    pub fn on_query_change(mut self, callback: impl Fn(String) + 'static) -> Self {
2447        self.on_query_change = Some(Rc::new(callback));
2448        self
2449    }
2450
2451    /// Set the callback for command selection.
2452    pub fn on_select(mut self, callback: impl Fn(&'static str) + 'static) -> Self {
2453        self.on_select = Some(Rc::new(callback));
2454        self
2455    }
2456
2457    /// Set the callback when dismissed.
2458    pub fn on_dismiss(mut self, callback: impl Fn() + 'static) -> Self {
2459        self.on_dismiss = Some(Rc::new(callback));
2460        self
2461    }
2462
2463    /// Set the width as percentage of screen.
2464    pub fn width(mut self, percent: u16) -> Self {
2465        self.width_percent = percent.min(100);
2466        self
2467    }
2468
2469    /// Set the height as percentage of screen.
2470    pub fn height(mut self, percent: u16) -> Self {
2471        self.height_percent = percent.min(100);
2472        self
2473    }
2474
2475    pub fn build(self) -> View {
2476        View::CommandPalette(CommandPaletteNode {
2477            visible: self.visible,
2478            query: self.query,
2479            commands: self.commands,
2480            selected: self.selected,
2481            on_query_change: self.on_query_change,
2482            on_select: self.on_select,
2483            on_dismiss: self.on_dismiss,
2484            width_percent: self.width_percent,
2485            height_percent: self.height_percent,
2486        })
2487    }
2488}
2489
2490// =============================================================================
2491// Menu Bar
2492// =============================================================================
2493
2494/// A menu in the menu bar.
2495#[derive(Clone)]
2496pub struct Menu {
2497    /// Display label for the menu.
2498    pub label: String,
2499    /// Items in this menu.
2500    pub items: Vec<MenuItemNode>,
2501}
2502
2503impl Menu {
2504    /// Create a new menu with the given label.
2505    pub fn new(label: impl Into<String>) -> Self {
2506        Self {
2507            label: label.into(),
2508            items: Vec::new(),
2509        }
2510    }
2511
2512    /// Add an item to the menu.
2513    pub fn item(mut self, item: MenuItemNode) -> Self {
2514        self.items.push(item);
2515        self
2516    }
2517
2518    /// Add a command item.
2519    pub fn command(self, id: &'static str, label: impl Into<String>) -> Self {
2520        self.item(MenuItemNode::Command {
2521            id,
2522            label: label.into(),
2523            shortcut: None,
2524        })
2525    }
2526
2527    /// Add a command item with shortcut display.
2528    pub fn command_with_shortcut(
2529        self,
2530        id: &'static str,
2531        label: impl Into<String>,
2532        shortcut: impl Into<String>,
2533    ) -> Self {
2534        self.item(MenuItemNode::Command {
2535            id,
2536            label: label.into(),
2537            shortcut: Some(shortcut.into()),
2538        })
2539    }
2540
2541    /// Add a separator.
2542    pub fn separator(self) -> Self {
2543        self.item(MenuItemNode::Separator)
2544    }
2545}
2546
2547/// An item in a menu.
2548#[derive(Clone)]
2549pub enum MenuItemNode {
2550    /// A command with ID, label, and optional shortcut display.
2551    Command {
2552        id: &'static str,
2553        label: String,
2554        shortcut: Option<String>,
2555    },
2556    /// A visual separator.
2557    Separator,
2558}
2559
2560/// A horizontal menu bar with dropdown menus.
2561#[derive(Clone)]
2562pub struct MenuBarNode {
2563    /// The menus in the menu bar.
2564    pub menus: Vec<Menu>,
2565    /// Currently active (open) menu index, if any.
2566    pub active_menu: Option<usize>,
2567    /// Currently highlighted menu index (for keyboard navigation when no menu is open).
2568    pub highlighted_menu: usize,
2569    /// Currently selected item in the active menu.
2570    pub selected_item: usize,
2571    /// Callback when a command is selected.
2572    pub on_select: Option<CommandCallback>,
2573    /// Callback when the active menu changes (opens/closes).
2574    pub on_menu_change: Option<SelectCallback>,
2575    /// Callback when the highlighted menu changes (arrow key navigation).
2576    pub on_highlight_change: Option<SelectCallback>,
2577    /// Callback when the selected item within a menu changes.
2578    pub on_item_change: Option<SelectCallback>,
2579}
2580
2581/// Builder for MenuBar views.
2582#[derive(Default)]
2583pub struct MenuBarBuilder {
2584    menus: Vec<Menu>,
2585    active_menu: Option<usize>,
2586    highlighted_menu: usize,
2587    selected_item: usize,
2588    on_select: Option<CommandCallback>,
2589    on_menu_change: Option<SelectCallback>,
2590    on_highlight_change: Option<SelectCallback>,
2591    on_item_change: Option<SelectCallback>,
2592}
2593
2594impl MenuBarBuilder {
2595    pub fn new() -> Self {
2596        Self::default()
2597    }
2598
2599    /// Add a menu to the menu bar.
2600    pub fn menu(mut self, menu: Menu) -> Self {
2601        self.menus.push(menu);
2602        self
2603    }
2604
2605    /// Set the active menu index (which menu has its dropdown open).
2606    pub fn active_menu(mut self, index: Option<usize>) -> Self {
2607        self.active_menu = index;
2608        self
2609    }
2610
2611    /// Set the highlighted menu index (for keyboard navigation).
2612    pub fn highlighted_menu(mut self, index: usize) -> Self {
2613        self.highlighted_menu = index;
2614        self
2615    }
2616
2617    /// Set the selected item in the active menu.
2618    pub fn selected_item(mut self, index: usize) -> Self {
2619        self.selected_item = index;
2620        self
2621    }
2622
2623    /// Set the callback for command selection.
2624    pub fn on_select(mut self, callback: impl Fn(&'static str) + 'static) -> Self {
2625        self.on_select = Some(Rc::new(callback));
2626        self
2627    }
2628
2629    /// Set the callback for menu changes (opens/closes dropdown).
2630    pub fn on_menu_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
2631        self.on_menu_change = Some(Rc::new(callback));
2632        self
2633    }
2634
2635    /// Set the callback for highlight changes (arrow key navigation).
2636    pub fn on_highlight_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
2637        self.on_highlight_change = Some(Rc::new(callback));
2638        self
2639    }
2640
2641    /// Set the callback for item selection changes within a menu.
2642    pub fn on_item_change(mut self, callback: impl Fn(usize) + 'static) -> Self {
2643        self.on_item_change = Some(Rc::new(callback));
2644        self
2645    }
2646
2647    pub fn build(self) -> View {
2648        View::MenuBar(MenuBarNode {
2649            menus: self.menus,
2650            active_menu: self.active_menu,
2651            highlighted_menu: self.highlighted_menu,
2652            selected_item: self.selected_item,
2653            on_select: self.on_select,
2654            on_menu_change: self.on_menu_change,
2655            on_highlight_change: self.on_highlight_change,
2656            on_item_change: self.on_item_change,
2657        })
2658    }
2659}
2660
2661// =============================================================================
2662// Toast Container
2663// =============================================================================
2664
2665/// Position for toast notifications.
2666#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2667pub enum ToastPosition {
2668    /// Top-right corner.
2669    TopRight,
2670    /// Top-left corner.
2671    TopLeft,
2672    /// Bottom-right corner (default).
2673    #[default]
2674    BottomRight,
2675    /// Bottom-left corner.
2676    BottomLeft,
2677}
2678
2679/// Severity level for visual rendering of toasts.
2680#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2681pub enum ToastLevelView {
2682    /// Informational (default).
2683    #[default]
2684    Info,
2685    /// Success.
2686    Success,
2687    /// Warning.
2688    Warning,
2689    /// Error.
2690    Error,
2691}
2692
2693/// A toast item for rendering.
2694#[derive(Clone)]
2695pub struct ToastItem {
2696    /// The message to display.
2697    pub message: String,
2698    /// Severity level.
2699    pub level: ToastLevelView,
2700    /// Progress (0.0 to 1.0) for fade-out animation.
2701    pub progress: f32,
2702}
2703
2704/// A container for displaying toast notifications.
2705#[derive(Clone)]
2706pub struct ToastContainerNode {
2707    /// The toasts to display.
2708    pub toasts: Vec<ToastItem>,
2709    /// Position of the toast container.
2710    pub position: ToastPosition,
2711    /// Maximum number of visible toasts.
2712    pub max_visible: usize,
2713    /// Width of each toast (in characters).
2714    pub width: u16,
2715}
2716
2717/// Builder for ToastContainer views.
2718#[derive(Default)]
2719pub struct ToastContainerBuilder {
2720    toasts: Vec<ToastItem>,
2721    position: ToastPosition,
2722    max_visible: usize,
2723    width: u16,
2724}
2725
2726impl ToastContainerBuilder {
2727    pub fn new() -> Self {
2728        Self {
2729            toasts: Vec::new(),
2730            position: ToastPosition::BottomRight,
2731            max_visible: 5,
2732            width: 40,
2733        }
2734    }
2735
2736    /// Set the toasts to display.
2737    pub fn toasts(mut self, toasts: Vec<ToastItem>) -> Self {
2738        self.toasts = toasts;
2739        self
2740    }
2741
2742    /// Add a toast from the toast queue system.
2743    pub fn from_queue(mut self, queue: &crate::toast::ToastQueue) -> Self {
2744        let toasts = queue.collect();
2745        self.toasts = toasts
2746            .into_iter()
2747            .map(|t| {
2748                let progress = t.remaining_fraction();
2749                let level = match t.level {
2750                    crate::toast::ToastLevel::Info => ToastLevelView::Info,
2751                    crate::toast::ToastLevel::Success => ToastLevelView::Success,
2752                    crate::toast::ToastLevel::Warning => ToastLevelView::Warning,
2753                    crate::toast::ToastLevel::Error => ToastLevelView::Error,
2754                };
2755                ToastItem {
2756                    message: t.message,
2757                    level,
2758                    progress,
2759                }
2760            })
2761            .collect();
2762        self
2763    }
2764
2765    /// Set the position of the toast container.
2766    pub fn position(mut self, position: ToastPosition) -> Self {
2767        self.position = position;
2768        self
2769    }
2770
2771    /// Set the maximum number of visible toasts.
2772    pub fn max_visible(mut self, max: usize) -> Self {
2773        self.max_visible = max;
2774        self
2775    }
2776
2777    /// Set the width of each toast.
2778    pub fn width(mut self, width: u16) -> Self {
2779        self.width = width;
2780        self
2781    }
2782
2783    pub fn build(self) -> View {
2784        View::ToastContainer(ToastContainerNode {
2785            toasts: self.toasts,
2786            position: self.position,
2787            max_visible: self.max_visible,
2788            width: self.width,
2789        })
2790    }
2791}
2792
2793// =============================================================================
2794// Form
2795// =============================================================================
2796
2797/// Callback type for form submission (receives all field values).
2798pub type FormSubmitCallback = Rc<dyn Fn(std::collections::HashMap<String, String>)>;
2799
2800/// A form container that manages field validation.
2801#[derive(Clone)]
2802pub struct FormNode {
2803    /// Child views (typically FormField nodes).
2804    pub children: Vec<View>,
2805    /// Callback when form is submitted (all fields valid).
2806    pub on_submit: Option<FormSubmitCallback>,
2807    /// Spacing between children.
2808    pub spacing: u16,
2809}
2810
2811/// Builder for Form views.
2812#[derive(Default)]
2813pub struct FormBuilder {
2814    children: Vec<View>,
2815    on_submit: Option<FormSubmitCallback>,
2816    spacing: u16,
2817}
2818
2819impl FormBuilder {
2820    pub fn new() -> Self {
2821        Self {
2822            spacing: 1,
2823            ..Default::default()
2824        }
2825    }
2826
2827    /// Add a child view (typically a FormField).
2828    pub fn child(mut self, view: View) -> Self {
2829        self.children.push(view);
2830        self
2831    }
2832
2833    /// Set spacing between children.
2834    pub fn spacing(mut self, spacing: u16) -> Self {
2835        self.spacing = spacing;
2836        self
2837    }
2838
2839    /// Set the submit callback.
2840    pub fn on_submit(
2841        mut self,
2842        callback: impl Fn(std::collections::HashMap<String, String>) + 'static,
2843    ) -> Self {
2844        self.on_submit = Some(Rc::new(callback));
2845        self
2846    }
2847
2848    pub fn build(self) -> View {
2849        View::Form(FormNode {
2850            children: self.children,
2851            on_submit: self.on_submit,
2852            spacing: self.spacing,
2853        })
2854    }
2855}
2856
2857// =============================================================================
2858// Form Field
2859// =============================================================================
2860
2861/// A form field with label, input, and error display.
2862#[derive(Clone)]
2863pub struct FormFieldNode {
2864    /// Field name (identifier).
2865    pub name: String,
2866    /// Display label.
2867    pub label: String,
2868    /// Current value.
2869    pub value: String,
2870    /// Placeholder text.
2871    pub placeholder: String,
2872    /// Error message (if validation failed).
2873    pub error: Option<String>,
2874    /// Whether this is a password field (mask input).
2875    pub password: bool,
2876    /// Callback when value changes.
2877    pub on_change: Option<ChangeCallback>,
2878    /// Callback when field loses focus (for validation).
2879    pub on_blur: Option<Callback>,
2880    /// Cursor position.
2881    pub cursor_pos: usize,
2882}
2883
2884/// Builder for FormField views.
2885#[derive(Default)]
2886pub struct FormFieldBuilder {
2887    name: String,
2888    label: String,
2889    value: String,
2890    placeholder: String,
2891    error: Option<String>,
2892    password: bool,
2893    on_change: Option<ChangeCallback>,
2894    on_blur: Option<Callback>,
2895    cursor_pos: usize,
2896}
2897
2898impl FormFieldBuilder {
2899    pub fn new(name: impl Into<String>) -> Self {
2900        let name = name.into();
2901        Self {
2902            label: name.clone(),
2903            name,
2904            ..Default::default()
2905        }
2906    }
2907
2908    /// Set the display label.
2909    pub fn label(mut self, label: impl Into<String>) -> Self {
2910        self.label = label.into();
2911        self
2912    }
2913
2914    /// Set the current value.
2915    pub fn value(mut self, value: impl Into<String>) -> Self {
2916        self.value = value.into();
2917        self.cursor_pos = self.value.len();
2918        self
2919    }
2920
2921    /// Set the placeholder text.
2922    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
2923        self.placeholder = placeholder.into();
2924        self
2925    }
2926
2927    /// Set the error message.
2928    pub fn error(mut self, error: Option<String>) -> Self {
2929        self.error = error;
2930        self
2931    }
2932
2933    /// Set whether this is a password field.
2934    pub fn password(mut self, password: bool) -> Self {
2935        self.password = password;
2936        self
2937    }
2938
2939    /// Set the change callback.
2940    pub fn on_change(mut self, callback: impl Fn(String) + 'static) -> Self {
2941        self.on_change = Some(Rc::new(callback));
2942        self
2943    }
2944
2945    /// Set the blur callback.
2946    pub fn on_blur(mut self, callback: impl Fn() + 'static) -> Self {
2947        self.on_blur = Some(Rc::new(callback));
2948        self
2949    }
2950
2951    /// Set the cursor position.
2952    pub fn cursor(mut self, pos: usize) -> Self {
2953        self.cursor_pos = pos;
2954        self
2955    }
2956
2957    pub fn build(self) -> View {
2958        View::FormField(FormFieldNode {
2959            name: self.name,
2960            label: self.label,
2961            value: self.value.clone(),
2962            placeholder: self.placeholder,
2963            error: self.error,
2964            password: self.password,
2965            on_change: self.on_change,
2966            on_blur: self.on_blur,
2967            cursor_pos: self.cursor_pos.min(self.value.len()),
2968        })
2969    }
2970}
2971
2972// =============================================================================
2973// Canvas Widget
2974// =============================================================================
2975
2976/// A canvas node for pixel-level drawing using Kitty graphics protocol.
2977#[derive(Clone)]
2978pub struct CanvasNode {
2979    /// Width in pixels.
2980    pub pixel_width: u16,
2981    /// Height in pixels.
2982    pub pixel_height: u16,
2983    /// Callback to draw on the canvas.
2984    pub on_draw: Option<CanvasDrawCallback>,
2985    /// Unique ID for this canvas (for Kitty image caching).
2986    pub id: u32,
2987}
2988
2989/// Builder for Canvas views.
2990pub struct CanvasBuilder {
2991    pixel_width: u16,
2992    pixel_height: u16,
2993    on_draw: Option<CanvasDrawCallback>,
2994    id: u32,
2995}
2996
2997impl Default for CanvasBuilder {
2998    fn default() -> Self {
2999        use std::sync::atomic::{AtomicU32, Ordering};
3000        static NEXT_ID: AtomicU32 = AtomicU32::new(1);
3001
3002        Self {
3003            pixel_width: 100,
3004            pixel_height: 50,
3005            on_draw: None,
3006            id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
3007        }
3008    }
3009}
3010
3011impl CanvasBuilder {
3012    pub fn new() -> Self {
3013        Self::default()
3014    }
3015
3016    /// Set the canvas width in pixels.
3017    pub fn width(mut self, width: u16) -> Self {
3018        self.pixel_width = width;
3019        self
3020    }
3021
3022    /// Set the canvas height in pixels.
3023    pub fn height(mut self, height: u16) -> Self {
3024        self.pixel_height = height;
3025        self
3026    }
3027
3028    /// Set the draw callback.
3029    ///
3030    /// This callback receives a `DrawContext` and is called each render
3031    /// to draw the canvas content.
3032    pub fn on_draw<F>(mut self, callback: F) -> Self
3033    where
3034        F: Fn(&mut crate::canvas::DrawContext) + 'static,
3035    {
3036        self.on_draw = Some(Rc::new(callback));
3037        self
3038    }
3039
3040    /// Set a specific canvas ID (for manual caching control).
3041    pub fn id(mut self, id: u32) -> Self {
3042        self.id = id;
3043        self
3044    }
3045
3046    pub fn build(self) -> View {
3047        View::Canvas(CanvasNode {
3048            pixel_width: self.pixel_width,
3049            pixel_height: self.pixel_height,
3050            on_draw: self.on_draw,
3051            id: self.id,
3052        })
3053    }
3054}
3055
3056// =============================================================================
3057// Image Widget
3058// =============================================================================
3059
3060/// An image node for displaying images using Kitty graphics protocol.
3061#[derive(Clone)]
3062pub struct ImageNode {
3063    /// Image data source (bytes or file path).
3064    pub source: Option<crate::image::ImageSource>,
3065    /// Unique ID for this image (for Kitty caching).
3066    pub id: u32,
3067    /// Explicit width in cells (overrides auto-detection).
3068    pub cell_width: Option<u16>,
3069    /// Explicit height in cells (overrides auto-detection).
3070    pub cell_height: Option<u16>,
3071    /// Alt text for accessibility / fallback display.
3072    pub alt: Option<String>,
3073}
3074
3075/// Builder for Image views.
3076pub struct ImageBuilder {
3077    source: Option<crate::image::ImageSource>,
3078    id: u32,
3079    cell_width: Option<u16>,
3080    cell_height: Option<u16>,
3081    alt: Option<String>,
3082}
3083
3084impl Default for ImageBuilder {
3085    fn default() -> Self {
3086        Self {
3087            source: None,
3088            id: crate::image::next_image_id(),
3089            cell_width: None,
3090            cell_height: None,
3091            alt: None,
3092        }
3093    }
3094}
3095
3096impl ImageBuilder {
3097    pub fn new() -> Self {
3098        Self::default()
3099    }
3100
3101    /// Set the image data from raw bytes.
3102    ///
3103    /// Supports PNG, JPEG, and GIF formats. Kitty auto-detects the format.
3104    /// GIF animations are handled natively by Kitty.
3105    ///
3106    /// # Example
3107    /// ```rust,ignore
3108    /// View::image()
3109    ///     .data(include_bytes!("logo.png"))
3110    ///     .build()
3111    /// ```
3112    pub fn data(mut self, bytes: &[u8]) -> Self {
3113        self.source = Some(crate::image::ImageSource::Data(bytes.to_vec()));
3114
3115        // Try to detect dimensions for layout
3116        if let Some((w, h)) = crate::image::detect_image_dimensions(bytes) {
3117            let (cw, ch) = crate::image::pixels_to_cells(w, h);
3118            if self.cell_width.is_none() {
3119                self.cell_width = Some(cw);
3120            }
3121            if self.cell_height.is_none() {
3122                self.cell_height = Some(ch);
3123            }
3124        }
3125
3126        self
3127    }
3128
3129    /// Set the image source from a file path.
3130    ///
3131    /// The file is loaded at render time.
3132    ///
3133    /// # Example
3134    /// ```rust,ignore
3135    /// View::image()
3136    ///     .file("assets/animation.gif")
3137    ///     .build()
3138    /// ```
3139    pub fn file(mut self, path: impl Into<String>) -> Self {
3140        self.source = Some(crate::image::ImageSource::File(path.into()));
3141        self
3142    }
3143
3144    /// Set explicit width in terminal cells.
3145    pub fn width(mut self, cells: u16) -> Self {
3146        self.cell_width = Some(cells);
3147        self
3148    }
3149
3150    /// Set explicit height in terminal cells.
3151    pub fn height(mut self, cells: u16) -> Self {
3152        self.cell_height = Some(cells);
3153        self
3154    }
3155
3156    /// Set a specific image ID (for manual caching control).
3157    pub fn id(mut self, id: u32) -> Self {
3158        self.id = id;
3159        self
3160    }
3161
3162    /// Set alt text for accessibility or fallback display.
3163    pub fn alt(mut self, text: impl Into<String>) -> Self {
3164        self.alt = Some(text.into());
3165        self
3166    }
3167
3168    pub fn build(self) -> View {
3169        View::Image(ImageNode {
3170            source: self.source,
3171            id: self.id,
3172            cell_width: self.cell_width,
3173            cell_height: self.cell_height,
3174            alt: self.alt,
3175        })
3176    }
3177}
3178
3179/// Node representing an interactive PTY terminal emulator.
3180///
3181/// **Experimental Preview** - See `View::terminal()` for limitations.
3182#[derive(Clone)]
3183pub struct TerminalNode {
3184    /// Handle to the running PTY process.
3185    pub handle: crate::terminal_state::TerminalHandle,
3186    /// Visible rows (defaults to 24).
3187    pub rows: usize,
3188    /// Visible columns (defaults to 80).
3189    pub cols: usize,
3190    /// Show border around terminal.
3191    pub border: bool,
3192    /// Title displayed in border (if border is enabled).
3193    pub title: Option<String>,
3194    /// Callback invoked when the PTY process exits.
3195    pub on_exit: Option<Callback>,
3196}
3197
3198/// Builder for Terminal views.
3199pub struct TerminalBuilder {
3200    handle: Option<crate::terminal_state::TerminalHandle>,
3201    rows: usize,
3202    cols: usize,
3203    border: bool,
3204    title: Option<String>,
3205    on_exit: Option<Callback>,
3206}
3207
3208impl Default for TerminalBuilder {
3209    fn default() -> Self {
3210        Self {
3211            handle: None,
3212            rows: 24,
3213            cols: 80,
3214            border: true,
3215            title: Some("Terminal".to_string()),
3216            on_exit: None,
3217        }
3218    }
3219}
3220
3221impl TerminalBuilder {
3222    pub fn new() -> Self {
3223        Self::default()
3224    }
3225
3226    /// Set the terminal handle (required).
3227    ///
3228    /// Get a handle from `cx.use_terminal()` in your component.
3229    pub fn handle(mut self, handle: crate::terminal_state::TerminalHandle) -> Self {
3230        self.handle = Some(handle);
3231        self
3232    }
3233
3234    /// Set the number of visible rows (default: 24).
3235    pub fn rows(mut self, rows: usize) -> Self {
3236        self.rows = rows;
3237        self
3238    }
3239
3240    /// Set the number of visible columns (default: 80).
3241    pub fn cols(mut self, cols: usize) -> Self {
3242        self.cols = cols;
3243        self
3244    }
3245
3246    /// Enable or disable border (default: true).
3247    pub fn border(mut self, border: bool) -> Self {
3248        self.border = border;
3249        self
3250    }
3251
3252    /// Set the border title (default: "Terminal").
3253    pub fn title(mut self, title: impl Into<String>) -> Self {
3254        self.title = Some(title.into());
3255        self
3256    }
3257
3258    /// Set a callback to be invoked when the PTY process exits.
3259    pub fn on_exit(mut self, callback: impl Fn() + 'static) -> Self {
3260        self.on_exit = Some(Rc::new(callback));
3261        self
3262    }
3263
3264    pub fn build(self) -> View {
3265        View::Terminal(TerminalNode {
3266            handle: self.handle.expect("Terminal requires a handle (from cx.use_terminal())"),
3267            rows: self.rows,
3268            cols: self.cols,
3269            border: self.border,
3270            title: self.title,
3271            on_exit: self.on_exit,
3272        })
3273    }
3274}
3275
3276// ========== Error Boundary ==========
3277
3278/// An error boundary that catches panics in its child view.
3279///
3280/// During rendering, if the child view panics, the fallback view is
3281/// displayed instead. This prevents a single misbehaving component
3282/// from crashing the entire application.
3283#[derive(Clone)]
3284pub struct ErrorBoundaryNode {
3285    /// The child view to render (may panic).
3286    pub child: Box<View>,
3287    /// The fallback view shown when the child panics.
3288    pub fallback: Box<View>,
3289}
3290
3291/// Builder for error boundary views.
3292#[derive(Default)]
3293pub struct ErrorBoundaryBuilder {
3294    child: Option<View>,
3295    fallback: Option<View>,
3296}
3297
3298impl ErrorBoundaryBuilder {
3299    pub fn new() -> Self {
3300        Self {
3301            child: None,
3302            fallback: None,
3303        }
3304    }
3305
3306    /// Set the child view (the view that might panic).
3307    pub fn child(mut self, child: View) -> Self {
3308        self.child = Some(child);
3309        self
3310    }
3311
3312    /// Set the fallback view (shown when child panics).
3313    pub fn fallback(mut self, fallback: View) -> Self {
3314        self.fallback = Some(fallback);
3315        self
3316    }
3317
3318    pub fn build(self) -> View {
3319        View::ErrorBoundary(ErrorBoundaryNode {
3320            child: Box::new(self.child.unwrap_or(View::Empty)),
3321            fallback: Box::new(
3322                self.fallback
3323                    .unwrap_or_else(|| View::text("[error boundary: child panicked]")),
3324            ),
3325        })
3326    }
3327}
3328
3329// ========== Custom Widget ==========
3330
3331/// A node wrapping a user-defined custom widget.
3332///
3333/// Uses `Rc<RefCell<dyn Widget>>` because:
3334/// - `Rc` enables Clone (View derives Clone) without requiring Widget: Clone
3335/// - `RefCell` allows interior mutability for focus handling
3336#[derive(Clone)]
3337pub struct CustomNode {
3338    pub widget: Rc<RefCell<dyn Widget>>,
3339}
3340
3341// ========== Slider ==========
3342
3343/// A slider for bounded numeric values (e.g., MIDI CC, volume, brightness).
3344#[derive(Clone)]
3345pub struct SliderNode {
3346    pub min: f64,
3347    pub max: f64,
3348    pub value: f64,
3349    pub step: f64,
3350    pub label: Option<String>,
3351    pub on_change: Option<SliderCallback>,
3352    pub color: Option<crossterm::style::Color>,
3353}
3354
3355/// Builder for slider views.
3356#[derive(Default)]
3357pub struct SliderBuilder {
3358    min: f64,
3359    max: f64,
3360    value: f64,
3361    step: f64,
3362    label: Option<String>,
3363    on_change: Option<SliderCallback>,
3364    color: Option<crossterm::style::Color>,
3365}
3366
3367impl SliderBuilder {
3368    pub fn new() -> Self {
3369        Self {
3370            min: 0.0,
3371            max: 100.0,
3372            value: 0.0,
3373            step: 1.0,
3374            label: None,
3375            on_change: None,
3376            color: None,
3377        }
3378    }
3379
3380    pub fn min(mut self, min: f64) -> Self {
3381        self.min = min;
3382        self
3383    }
3384
3385    pub fn max(mut self, max: f64) -> Self {
3386        self.max = max;
3387        self
3388    }
3389
3390    pub fn value(mut self, value: f64) -> Self {
3391        self.value = value;
3392        self
3393    }
3394
3395    pub fn step(mut self, step: f64) -> Self {
3396        self.step = step;
3397        self
3398    }
3399
3400    pub fn label(mut self, label: impl Into<String>) -> Self {
3401        self.label = Some(label.into());
3402        self
3403    }
3404
3405    pub fn on_change(mut self, callback: impl Fn(f64) + 'static) -> Self {
3406        self.on_change = Some(Rc::new(callback));
3407        self
3408    }
3409
3410    pub fn color(mut self, color: crossterm::style::Color) -> Self {
3411        self.color = Some(color);
3412        self
3413    }
3414
3415    pub fn build(self) -> View {
3416        View::Slider(SliderNode {
3417            min: self.min,
3418            max: self.max,
3419            value: self.value.clamp(self.min, self.max),
3420            step: self.step,
3421            label: self.label,
3422            on_change: self.on_change,
3423            color: self.color,
3424        })
3425    }
3426}