Skip to main content

a2ui_tui/
component_impl.rs

1//! Ratatui-specific component trait and registry.
2//!
3//! Each A2UI component type (Text, Button, etc.) implements [`TuiComponent`]
4//! so the renderer can delegate rendering to the appropriate handler.
5
6use std::collections::HashMap;
7
8use ratatui::{Frame, layout::Rect};
9
10use a2ui_base::model::component_context::ComponentContext;
11
12/// Trait for ratatui component implementations.
13///
14/// Each A2UI component type (Text, Button, etc.) implements this.
15pub trait TuiComponent: Send + Sync + 'static {
16    /// The component name (must match the A2UI catalog name).
17    fn name(&self) -> &'static str;
18
19    /// Render this component.
20    ///
21    /// - `ctx` provides access to the component's properties and data bindings.
22    /// - `area` is the allocated area for this component.
23    /// - `frame` is the ratatui frame to render into.
24    /// - `render_child` is a closure to recursively render a child component by ID.
25    /// - `measure_child` is a closure to ask a child for its natural content height
26    ///   given an available width, mirroring `render_child`'s `(id, base_path, …)`
27    ///   shape so template children measure against their own data path.
28    fn render(
29        &self,
30        ctx: &ComponentContext,
31        area: Rect,
32        frame: &mut Frame,
33        render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
34        measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
35    );
36
37    /// The intrinsic content height of this component **including its own chrome**
38    /// (margins/borders), given `available_width` cells.
39    ///
40    /// `measure_child` lets container components measure their own children to sum
41    /// (Column/vertical-List) or max (Row) their natural heights. Leaf components
42    /// ignore it.
43    ///
44    /// Returning `None` means "no opinion" — containers treat the component as a
45    /// legacy fill participant (it gets an equal/weighted share of the available
46    /// space, exactly as before this measure pass existed). Leaf/content components
47    /// override this to return a content-driven height so containers can reserve
48    /// only as much vertical space as the content actually needs.
49    ///
50    /// The default `None` keeps unconverted components behaving exactly as today,
51    /// so migration is gradual and regression-free.
52    fn natural_height(
53        &self,
54        _ctx: &ComponentContext,
55        _available_width: u16,
56        _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
57    ) -> Option<u16> {
58        None
59    }
60
61    /// Handle an input event directed at this component.
62    ///
63    /// Returns `Some(EventResult)` if the component produced an action or data change
64    /// that the application should process, or `None` if the event was not handled.
65    ///
66    /// The default implementation ignores all events (non-interactive components).
67    fn handle_event(
68        &self,
69        _ctx: &ComponentContext,
70        _event: &a2ui_base::event::InputEvent,
71    ) -> Option<a2ui_base::event::EventResult> {
72        None
73    }
74}
75
76// After the workspace split, ComponentApi lives in a2ui-base — an external
77// crate from here — so the blanket `impl<T: TuiComponent> ComponentApi for T`
78// would violate the orphan rule (foreign trait for a bare type parameter).
79// Instead we impl ComponentApi concretely for each registered component type
80// (a local type impl'ing a foreign trait is always allowed). Add a line here
81// whenever a new component is registered into a catalog.
82macro_rules! impl_component_api {
83    ($t:path) => {
84        impl a2ui_base::catalog::component_api::ComponentApi for $t {
85            fn name(&self) -> &'static str {
86                <Self as crate::component_impl::TuiComponent>::name(self)
87            }
88        }
89    };
90}
91
92impl_component_api!(crate::components::audio_player::AudioPlayerComponent);
93impl_component_api!(crate::components::button::ButtonComponent);
94impl_component_api!(crate::components::card::CardComponent);
95impl_component_api!(crate::components::checkbox::CheckBoxComponent);
96impl_component_api!(crate::components::choice_picker::ChoicePickerComponent);
97impl_component_api!(crate::components::column::ColumnComponent);
98impl_component_api!(crate::components::date_time_input::DateTimeInputComponent);
99impl_component_api!(crate::components::divider::DividerComponent);
100impl_component_api!(crate::components::icon::IconComponent);
101impl_component_api!(crate::components::image::ImageComponent);
102impl_component_api!(crate::components::list::ListComponent);
103impl_component_api!(crate::components::modal::ModalComponent);
104impl_component_api!(crate::components::row::RowComponent);
105impl_component_api!(crate::components::slider::SliderComponent);
106impl_component_api!(crate::components::tabs::TabsComponent);
107impl_component_api!(crate::components::text::TextComponent);
108impl_component_api!(crate::components::text_field::TextFieldComponent);
109impl_component_api!(crate::components::video::VideoComponent);
110
111/// Registry that maps component type names to their [`TuiComponent`] implementations.
112pub type ComponentRegistry = HashMap<String, Box<dyn TuiComponent>>;
113
114/// Build a [`ComponentRegistry`] from a list of component implementations.
115///
116/// Each component is keyed by its [`TuiComponent::name`].
117///
118/// # Example
119///
120/// ```ignore
121/// use crate::component_impl::{ComponentRegistry, build_registry};
122/// use crate::components::text::TextComponent;
123/// use crate::components::button::ButtonComponent;
124///
125/// let registry = build_registry(vec![
126///     Box::new(TextComponent),
127///     Box::new(ButtonComponent),
128/// ]);
129/// ```
130pub fn build_registry(components: Vec<Box<dyn TuiComponent>>) -> ComponentRegistry {
131    components
132        .into_iter()
133        .map(|c| {
134            let name = c.name().to_string();
135            (name, c)
136        })
137        .collect()
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    /// A trivial component for testing the registry.
145    struct FakeComponent;
146
147    impl TuiComponent for FakeComponent {
148        fn name(&self) -> &'static str {
149            "Fake"
150        }
151
152        fn render(
153            &self,
154            _ctx: &ComponentContext,
155            _area: Rect,
156            _frame: &mut Frame,
157            _render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
158            _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
159        ) {
160        }
161    }
162
163    #[test]
164    fn build_registry_keys_by_name() {
165        let registry = build_registry(vec![Box::new(FakeComponent)]);
166        assert!(registry.contains_key("Fake"));
167    }
168}