a2ui_tui/components/
slider.rs1use ratatui::{
4 Frame,
5 layout::Rect,
6 style::{Color, Style},
7 text::{Line, Span},
8 widgets::Paragraph,
9};
10
11use a2ui_base::model::component_context::ComponentContext;
12use a2ui_base::protocol::common_types::{DynamicNumber, DynamicString};
13use crate::component_impl::TuiComponent;
14
15pub struct SliderComponent;
21
22impl TuiComponent for SliderComponent {
23 fn name(&self) -> &'static str {
24 "Slider"
25 }
26
27 fn render(
28 &self,
29 ctx: &ComponentContext,
30 area: Rect,
31 frame: &mut Frame,
32 _render_child: &mut dyn FnMut(&str, Rect, &mut Frame, &str),
33 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
34 ) {
35 let comp_model = match ctx.components.get(&ctx.component_id) {
36 Some(m) => m,
37 None => return,
38 };
39
40 let inner = crate::layout_engine::padded_content(area);
42
43 if inner.width == 0 || inner.height == 0 {
44 return;
45 }
46
47 let label = match comp_model.get_property::<DynamicString>("label") {
49 Some(ds) => ctx.data_context.resolve_dynamic_string(&ds),
50 None => String::new(),
51 };
52
53 let value = match comp_model.get_property::<DynamicNumber>("value") {
55 Some(dn) => ctx.data_context.resolve_dynamic_number(&dn),
56 None => 0.0,
57 };
58 let min = comp_model
59 .get_property::<DynamicNumber>("min")
60 .map(|dn| ctx.data_context.resolve_dynamic_number(&dn))
61 .unwrap_or(0.0);
62 let max = comp_model
63 .get_property::<DynamicNumber>("max")
64 .map(|dn| ctx.data_context.resolve_dynamic_number(&dn))
65 .unwrap_or(100.0);
66
67 let range = max - min;
69 let ratio = if range.abs() < f64::EPSILON {
70 0.0
71 } else {
72 ((value - min) / range).clamp(0.0, 1.0)
73 };
74
75 let value_text = format!("{:.0}", value);
78 let label_width = if label.is_empty() { 0 } else { label.len() + 1 };
80 let value_width = value_text.len() + 1; let overhead = 2 + label_width + value_width; let bar_width = if (inner.width as usize) > overhead {
83 inner.width as usize - overhead
84 } else {
85 0
86 };
87
88 let filled = (bar_width as f64 * ratio).round() as usize;
89 let unfilled = bar_width.saturating_sub(filled);
90
91 let is_focused = ctx.focused_id.as_deref() == Some(ctx.component_id.as_str());
93 let bar_color = if is_focused { Color::Yellow } else { Color::Cyan };
94
95 let steps = comp_model
97 .get_property::<DynamicNumber>("steps")
98 .map(|dn| ctx.data_context.resolve_dynamic_number(&dn) as usize);
99
100 let bar_str = if bar_width > 0 {
101 if let Some(step_count) = steps {
102 if step_count > 0 && step_count <= bar_width {
103 let mut bar: Vec<char> = vec!['─'; bar_width];
105 for i in 0..=step_count {
106 let pos = (bar_width as f64 * i as f64 / step_count as f64).round() as usize;
107 if pos < bar_width {
108 bar[pos] = '┬';
109 }
110 }
111 for j in 0..filled {
113 if j < bar.len() {
114 bar[j] = '━';
115 }
116 }
117 format!("[{}]", bar.into_iter().collect::<String>())
118 } else {
119 let filled_str: String = "━".repeat(filled);
120 let unfilled_str: String = "─".repeat(unfilled);
121 format!("[{}{}]", filled_str, unfilled_str)
122 }
123 } else {
124 let filled_str: String = "━".repeat(filled);
125 let unfilled_str: String = "─".repeat(unfilled);
126 format!("[{}{}]", filled_str, unfilled_str)
127 }
128 } else {
129 String::new()
130 };
131
132 let mut spans = Vec::new();
134 if !label.is_empty() {
135 spans.push(Span::styled(
136 format!("{} ", label),
137 Style::default().fg(Color::White),
138 ));
139 }
140 spans.push(Span::styled(
141 bar_str,
142 Style::default().fg(bar_color),
143 ));
144 spans.push(Span::styled(
145 format!(" {}", value_text),
146 Style::default().fg(Color::White),
147 ));
148
149 let paragraph = Paragraph::new(Line::from(spans));
150 frame.render_widget(paragraph, inner);
151 }
152
153 fn natural_height(
154 &self,
155 _ctx: &ComponentContext,
156 _available_width: u16,
157 _measure_child: &mut dyn FnMut(&str, &str, u16) -> Option<u16>,
158 ) -> Option<u16> {
159 Some(3)
161 }
162
163 fn handle_event(
164 &self,
165 ctx: &ComponentContext,
166 event: &a2ui_base::event::InputEvent,
167 ) -> Option<a2ui_base::event::EventResult> {
168 a2ui_base::components::slider::handle_event(ctx, event)
169 }
170}