a2ui_tui/components/
button.rs1use ratatui::{
4 Frame,
5 layout::Rect,
6 style::{Color, Modifier, Style},
7 widgets::{Block, Borders},
8};
9
10use a2ui_base::model::component_context::ComponentContext;
11use crate::component_impl::TuiComponent;
12
13pub struct ButtonComponent;
20
21impl TuiComponent for ButtonComponent {
22 fn name(&self) -> &'static str {
23 "Button"
24 }
25
26 fn render(
27 &self,
28 ctx: &ComponentContext,
29 area: Rect,
30 frame: &mut Frame,
31 render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
32 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
33 ) {
34 let comp_model = match ctx.components.get(&ctx.component_id) {
35 Some(m) => m,
36 None => return,
37 };
38
39 let inner = crate::layout_engine::padded_content(area);
41
42 if inner.width == 0 || inner.height == 0 {
43 return;
44 }
45
46 let variant: Option<String> = comp_model.get_property("variant");
48
49 let is_focused = ctx.focused_id.as_deref() == Some(ctx.component_id.as_str());
51
52 let checks_pass = evaluate_checks(ctx, comp_model);
54
55 let (block, base_style) = match variant.as_deref() {
57 Some("primary") => {
58 let style = Style::default()
59 .bg(Color::Blue)
60 .fg(Color::White)
61 .add_modifier(Modifier::BOLD);
62 let block = Block::default().style(style).borders(Borders::NONE);
63 (block, style)
64 }
65 Some("borderless") => {
66 let style = Style::default().add_modifier(Modifier::UNDERLINED);
67 let block = Block::default().style(style).borders(Borders::NONE);
68 (block, style)
69 }
70 _ => {
71 let style = Style::default();
73 let block = Block::default().style(style).borders(Borders::ALL);
74 (block, style)
75 }
76 };
77
78 let final_style = if !checks_pass {
80 base_style.add_modifier(Modifier::DIM)
81 } else if is_focused {
82 base_style.add_modifier(Modifier::REVERSED)
83 } else {
84 base_style
85 };
86
87 let block = block.style(final_style);
88
89 let child_area = block.inner(inner);
91
92 frame.render_widget(block, inner);
94
95 if let Some(child_id) = comp_model.child() {
97 if child_area.width > 0 && child_area.height > 0 {
98 render_child(&child_id, child_area, frame, "");
99 }
100 } else if let Some(a11y) = comp_model.accessibility() {
101 let a11y_text = a11y.label
103 .as_ref()
104 .map(|ds| ctx.data_context.resolve_dynamic_string(ds))
105 .unwrap_or_default();
106 if !a11y_text.is_empty() && child_area.width > 0 && child_area.height > 0 {
107 let text = ratatui::text::Line::from(a11y_text);
108 let widget = ratatui::widgets::Paragraph::new(text).style(final_style);
109 frame.render_widget(widget, child_area);
110 }
111 }
112 }
113
114 fn natural_height(
115 &self,
116 ctx: &ComponentContext,
117 _available_width: u16,
118 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
119 ) -> Option<u16> {
120 let comp_model = match ctx.components.get(&ctx.component_id) {
125 Some(m) => m,
126 None => return Some(3),
127 };
128 let variant: Option<String> = comp_model.get_property("variant");
129 match variant.as_deref() {
130 Some("primary") | Some("borderless") => Some(3),
131 _ => Some(5),
132 }
133 }
134
135 fn handle_event(
136 &self,
137 ctx: &ComponentContext,
138 event: &a2ui_base::event::InputEvent,
139 ) -> Option<a2ui_base::event::EventResult> {
140 a2ui_base::components::button::handle_event(ctx, event)
141 }
142}
143
144fn evaluate_checks(ctx: &ComponentContext, comp_model: &a2ui_base::model::component_model::ComponentModel) -> bool {
146 match comp_model.checks() {
147 Some(checks) => checks.iter().all(|rule| {
148 ctx.data_context.resolve_dynamic_boolean_condition(&rule.condition)
149 }),
150 None => true,
151 }
152}