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.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    /// 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, 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}