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}