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.skip_interaction_slot();
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.rollback.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, key) in self.available_key_presses() {
91 match key.code {
92 KeyCode::Left | KeyCode::Char('h') => {
93 if step > 0.0 {
94 let next = (*value - step).max(start);
95 if (next - *value).abs() > f64::EPSILON {
96 *value = next;
97 changed = true;
98 }
99 }
100 consumed_indices.push(i);
101 }
102 KeyCode::Right | KeyCode::Char('l') => {
103 if step > 0.0 {
104 let next = (*value + step).min(end);
105 if (next - *value).abs() > f64::EPSILON {
106 *value = next;
107 changed = true;
108 }
109 }
110 consumed_indices.push(i);
111 }
112 _ => {}
113 }
114 }
115 self.consume_indices(consumed_indices);
116 }
117
118 let ratio = if span <= f64::EPSILON {
119 0.0
120 } else {
121 ((*value - start) / span).clamp(0.0, 1.0)
122 };
123
124 let value_text = format_compact_number(*value);
125 let label_width = UnicodeWidthStr::width(label) as u32;
126 let value_width = UnicodeWidthStr::width(value_text.as_str()) as u32;
127 let track_width = self
128 .area_width
129 .saturating_sub(label_width + value_width + 8)
130 .max(10) as usize;
131 let thumb_idx = if track_width <= 1 {
132 0
133 } else {
134 (ratio * (track_width as f64 - 1.0)).round() as usize
135 };
136
137 let mut track = String::with_capacity(track_width);
138 for i in 0..track_width {
139 if i == thumb_idx {
140 track.push('○');
141 } else if i < thumb_idx {
142 track.push('█');
143 } else {
144 track.push('━');
145 }
146 }
147
148 let text_color = self.theme.text;
149 let border_color = self.theme.border;
150 let primary_color = self.theme.primary;
151 let dim_color = self.theme.text_dim;
152 let mut response = self.container().row(|ui| {
153 ui.text(label).fg(text_color);
154 ui.text("[").fg(border_color);
155 ui.text(track).grow(1).fg(primary_color);
156 ui.text("]").fg(border_color);
157 if focused {
158 ui.text(value_text.as_str()).bold().fg(primary_color);
159 } else {
160 ui.text(value_text.as_str()).fg(dim_color);
161 }
162 });
163 response.focused = focused;
164 response.changed = changed;
165 response
166 }
167}