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