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
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    /// Horizontal slider for numeric values.
63    ///
64    /// # Examples
65    /// ```
66    /// # use slt::*;
67    /// # TestBackend::new(80, 24).render(|ui| {
68    /// let mut volume = 75.0_f64;
69    /// let r = ui.slider("Volume", &mut volume, 0.0..=100.0);
70    /// if r.changed { /* volume was adjusted */ }
71    /// # });
72    /// ```
73    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}