Skip to main content

slt/context/widgets_input/
feedback.rs

1use super::*;
2
3impl Context {
4    ///
5    /// The spinner advances one frame per tick. Use [`SpinnerState::dots`] or
6    /// [`SpinnerState::line`] to create the state, then chain style methods to
7    /// color it.
8    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    /// Render toast notifications. Calls `state.cleanup(tick)` automatically.
16    ///
17    /// Expired messages are removed before rendering. If there are no active
18    /// messages, nothing is rendered and `self` is returned unchanged.
19    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    /// Horizontal slider for numeric values.
62    ///
63    /// # Examples
64    /// ```
65    /// # use slt::*;
66    /// # TestBackend::new(80, 24).render(|ui| {
67    /// let mut volume = 75.0_f64;
68    /// let r = ui.slider("Volume", &mut volume, 0.0..=100.0);
69    /// if r.changed { /* volume was adjusted */ }
70    /// # });
71    /// ```
72    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}