a2ui_tui/components/
text_field.rs1use ratatui::{
4 Frame,
5 layout::Rect,
6 style::{Color, Style},
7 text::{Line, Span},
8 widgets::{Block, Borders, Paragraph},
9};
10
11use a2ui_base::model::component_context::ComponentContext;
12use a2ui_base::protocol::common_types::DynamicString;
13use crate::component_impl::TuiComponent;
14
15pub struct TextFieldComponent;
28
29impl TuiComponent for TextFieldComponent {
30 fn name(&self) -> &'static str {
31 "TextField"
32 }
33
34 fn render(
35 &self,
36 ctx: &ComponentContext,
37 area: Rect,
38 frame: &mut Frame,
39 _render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
40 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
41 ) {
42 let comp_model = match ctx.components.get(&ctx.component_id) {
43 Some(m) => m,
44 None => return,
45 };
46
47 let inner = crate::layout_engine::padded_content(area);
49
50 if inner.width == 0 || inner.height == 0 {
51 return;
52 }
53
54 let label = match comp_model.get_property::<DynamicString>("label") {
56 Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
57 None => String::new(),
58 };
59
60 let raw_value = match comp_model.get_property::<DynamicString>("value") {
62 Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
63 None => String::new(),
64 };
65
66 let variant: Option<String> = comp_model.get_property("variant");
68 let display_value = match variant.as_deref() {
69 Some("obscured") => obscure_value(&raw_value),
70 _ => raw_value.clone(),
71 };
72
73 let placeholder = comp_model
75 .get_property::<DynamicString>("placeholder")
76 .map(|ds| ctx.data_context.resolve_dynamic_string(&ds));
77
78 let (display_text, is_placeholder) = if raw_value.is_empty() {
81 match &placeholder {
82 Some(p) if !p.is_empty() => (p.clone(), true),
83 _ => ("\u{2588}".to_string(), false), }
85 } else {
86 (format!("{}\u{2588}", display_value), false)
87 };
88
89 let is_focused = ctx.focused_id.as_deref() == Some(ctx.component_id.as_str());
91
92 let block_style = if is_focused {
95 Style::default().fg(Color::Yellow)
96 } else {
97 Style::default()
98 };
99 let block = Block::default()
100 .borders(Borders::ALL)
101 .title(label)
102 .style(block_style);
103
104 let content_area = block.inner(inner);
105
106 frame.render_widget(block, inner);
108
109 if content_area.width == 0 || content_area.height == 0 {
110 return;
111 }
112
113 let paragraph_style = if is_placeholder {
115 Style::default().fg(Color::DarkGray)
116 } else {
117 Style::default()
118 };
119 let paragraph = Paragraph::new(Line::from(Span::styled(display_text, paragraph_style)));
120 frame.render_widget(paragraph, content_area);
121 }
122
123 fn natural_height(
124 &self,
125 _ctx: &ComponentContext,
126 _available_width: u16,
127 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
128 ) -> Option<u16> {
129 Some(5)
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::text_field::handle_event(ctx, event)
141 }
142}
143
144fn obscure_value(value: &str) -> String {
146 value.chars().map(|_| '\u{2022}').collect()
147}