slt/context/widgets_input/
feedback.rs1use super::*;
2
3impl Context {
4 pub fn spinner(&mut self, state: &SpinnerState) -> &mut Self {
9 self.styled(
10 state.frame(self.tick).to_string(),
11 Style::new().fg(self.theme.primary),
12 )
13 }
14
15 pub fn toast(&mut self, state: &mut ToastState) -> &mut Self {
20 state.cleanup(self.tick);
21 if state.messages.is_empty() {
22 return self;
23 }
24
25 self.interaction_count += 1;
26 self.commands.push(Command::BeginContainer {
27 direction: Direction::Column,
28 gap: 0,
29 align: Align::Start,
30 align_self: None,
31 justify: Justify::Start,
32 border: None,
33 border_sides: BorderSides::all(),
34 border_style: Style::new().fg(self.theme.border),
35 bg_color: None,
36 padding: Padding::default(),
37 margin: Margin::default(),
38 constraints: Constraints::default(),
39 title: None,
40 grow: 0,
41 group_name: None,
42 });
43 for message in state.messages.iter().rev() {
44 let color = match message.level {
45 ToastLevel::Info => self.theme.primary,
46 ToastLevel::Success => self.theme.success,
47 ToastLevel::Warning => self.theme.warning,
48 ToastLevel::Error => self.theme.error,
49 };
50 let mut line = String::with_capacity(4 + message.text.len());
51 line.push_str(" ● ");
52 line.push_str(&message.text);
53 self.styled(line, Style::new().fg(color));
54 }
55 self.commands.push(Command::EndContainer);
56 self.last_text_idx = None;
57
58 self
59 }
60
61 pub fn slider(
73 &mut self,
74 label: &str,
75 value: &mut f64,
76 range: std::ops::RangeInclusive<f64>,
77 ) -> Response {
78 let focused = self.register_focusable();
79 let mut changed = false;
80
81 let start = *range.start();
82 let end = *range.end();
83 let span = (end - start).max(0.0);
84 let step = if span > 0.0 { span / 20.0 } else { 0.0 };
85
86 *value = (*value).clamp(start, end);
87
88 if focused {
89 let mut consumed_indices = Vec::new();
90 for (i, event) in self.events.iter().enumerate() {
91 if let Event::Key(key) = event {
92 if key.kind != KeyEventKind::Press {
93 continue;
94 }
95
96 match key.code {
97 KeyCode::Left | KeyCode::Char('h') => {
98 if step > 0.0 {
99 let next = (*value - step).max(start);
100 if (next - *value).abs() > f64::EPSILON {
101 *value = next;
102 changed = true;
103 }
104 }
105 consumed_indices.push(i);
106 }
107 KeyCode::Right | KeyCode::Char('l') => {
108 if step > 0.0 {
109 let next = (*value + step).min(end);
110 if (next - *value).abs() > f64::EPSILON {
111 *value = next;
112 changed = true;
113 }
114 }
115 consumed_indices.push(i);
116 }
117 _ => {}
118 }
119 }
120 }
121
122 for idx in consumed_indices {
123 self.consumed[idx] = true;
124 }
125 }
126
127 let ratio = if span <= f64::EPSILON {
128 0.0
129 } else {
130 ((*value - start) / span).clamp(0.0, 1.0)
131 };
132
133 let value_text = format_compact_number(*value);
134 let label_width = UnicodeWidthStr::width(label) as u32;
135 let value_width = UnicodeWidthStr::width(value_text.as_str()) as u32;
136 let track_width = self
137 .area_width
138 .saturating_sub(label_width + value_width + 8)
139 .max(10) as usize;
140 let thumb_idx = if track_width <= 1 {
141 0
142 } else {
143 (ratio * (track_width as f64 - 1.0)).round() as usize
144 };
145
146 let mut track = String::with_capacity(track_width);
147 for i in 0..track_width {
148 if i == thumb_idx {
149 track.push('○');
150 } else if i < thumb_idx {
151 track.push('█');
152 } else {
153 track.push('━');
154 }
155 }
156
157 let text_color = self.theme.text;
158 let border_color = self.theme.border;
159 let primary_color = self.theme.primary;
160 let dim_color = self.theme.text_dim;
161 let mut response = self.container().row(|ui| {
162 ui.text(label).fg(text_color);
163 ui.text("[").fg(border_color);
164 ui.text(track).grow(1).fg(primary_color);
165 ui.text("]").fg(border_color);
166 if focused {
167 ui.text(value_text.as_str()).bold().fg(primary_color);
168 } else {
169 ui.text(value_text.as_str()).fg(dim_color);
170 }
171 });
172 response.focused = focused;
173 response.changed = changed;
174 response
175 }
176}