Skip to main content

slt/context/widgets_display/
status.rs

1use super::*;
2
3impl Context {
4    /// Render an alert banner with icon and level-based coloring.
5    pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
6        use crate::widgets::AlertLevel;
7
8        let theme = self.theme;
9        let (icon, color) = match level {
10            AlertLevel::Info => ("ℹ", theme.accent),
11            AlertLevel::Success => ("✓", theme.success),
12            AlertLevel::Warning => ("⚠", theme.warning),
13            AlertLevel::Error => ("✕", theme.error),
14        };
15
16        let focused = self.register_focusable();
17        let key_dismiss = if focused {
18            let consumed: Vec<usize> = self
19                .available_key_presses()
20                .filter_map(|(i, key)| {
21                    if matches!(key.code, KeyCode::Enter | KeyCode::Char('x')) {
22                        Some(i)
23                    } else {
24                        None
25                    }
26                })
27                .collect();
28            let dismissed = !consumed.is_empty();
29            self.consume_indices(consumed);
30            dismissed
31        } else {
32            false
33        };
34
35        let mut response = self.container().col(|ui| {
36            ui.line(|ui| {
37                let mut icon_text = String::with_capacity(icon.len() + 2);
38                icon_text.push(' ');
39                icon_text.push_str(icon);
40                icon_text.push(' ');
41                ui.text(icon_text).fg(color).bold();
42                ui.text(message).grow(1);
43                ui.text(" [×] ").dim();
44            });
45        });
46        response.focused = focused;
47        if key_dismiss {
48            response.clicked = true;
49        }
50
51        response
52    }
53
54    /// Yes/No confirmation dialog. Returns Response with .clicked=true when answered.
55    ///
56    /// `result` is set to true for Yes, false for No.
57    ///
58    /// # Examples
59    /// ```
60    /// # use slt::*;
61    /// # TestBackend::new(80, 24).render(|ui| {
62    /// let mut answer = false;
63    /// let r = ui.confirm("Delete this file?", &mut answer);
64    /// if r.clicked && answer { /* user confirmed */ }
65    /// # });
66    /// ```
67    pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
68        let focused = self.register_focusable();
69        let mut is_yes = *result;
70        let mut clicked = false;
71
72        if focused {
73            let mut consumed_indices = Vec::new();
74            for (i, key) in self.available_key_presses() {
75                match key.code {
76                    KeyCode::Char('y') => {
77                        is_yes = true;
78                        *result = true;
79                        clicked = true;
80                        consumed_indices.push(i);
81                    }
82                    KeyCode::Char('n') => {
83                        is_yes = false;
84                        *result = false;
85                        clicked = true;
86                        consumed_indices.push(i);
87                    }
88                    KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
89                        is_yes = !is_yes;
90                        *result = is_yes;
91                        consumed_indices.push(i);
92                    }
93                    KeyCode::Enter => {
94                        *result = is_yes;
95                        clicked = true;
96                        consumed_indices.push(i);
97                    }
98                    _ => {}
99                }
100            }
101            self.consume_indices(consumed_indices);
102        }
103
104        let yes_style = if is_yes {
105            if focused {
106                Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
107            } else {
108                Style::new().fg(self.theme.success).bold()
109            }
110        } else {
111            Style::new().fg(self.theme.text_dim)
112        };
113        let no_style = if !is_yes {
114            if focused {
115                Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
116            } else {
117                Style::new().fg(self.theme.error).bold()
118            }
119        } else {
120            Style::new().fg(self.theme.text_dim)
121        };
122
123        let q_width = UnicodeWidthStr::width(question) as u32;
124        let mut response = self.row(|ui| {
125            ui.text(question);
126            ui.text(" ");
127            ui.styled("[Yes]", yes_style);
128            ui.text(" ");
129            ui.styled("[No]", no_style);
130        });
131
132        if !clicked && response.clicked {
133            if let Some((mx, _)) = self.click_pos {
134                let yes_start = response.rect.x + q_width + 1;
135                let yes_end = yes_start + 5;
136                let no_start = yes_end + 1;
137                if mx >= yes_start && mx < yes_end {
138                    is_yes = true;
139                    *result = true;
140                    clicked = true;
141                } else if mx >= no_start {
142                    is_yes = false;
143                    *result = false;
144                    clicked = true;
145                }
146            }
147        }
148
149        response.focused = focused;
150        response.clicked = clicked;
151        response.changed = clicked;
152        let _ = is_yes;
153        response
154    }
155
156    /// Render a breadcrumb navigation bar. Returns the clicked segment index.
157    pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
158        self.breadcrumb_with(segments, " › ")
159    }
160
161    /// Render a breadcrumb with a custom separator string.
162    pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
163        let theme = self.theme;
164        let last_idx = segments.len().saturating_sub(1);
165        let mut clicked_idx: Option<usize> = None;
166
167        let _ = self.row(|ui| {
168            for (i, segment) in segments.iter().enumerate() {
169                let is_last = i == last_idx;
170                if is_last {
171                    ui.text(*segment).bold();
172                } else {
173                    let focused = ui.register_focusable();
174                    let resp = ui.interaction();
175                    let activated = resp.clicked || ui.consume_activation_keys(focused);
176                    let color = if resp.hovered || focused {
177                        theme.accent
178                    } else {
179                        theme.primary
180                    };
181                    ui.text(*segment).fg(color).underline();
182                    if activated {
183                        clicked_idx = Some(i);
184                    }
185                    ui.text(separator).dim();
186                }
187            }
188        });
189
190        clicked_idx
191    }
192
193    /// Collapsible section that toggles on click, Enter, or Space.
194    pub fn accordion(
195        &mut self,
196        title: &str,
197        open: &mut bool,
198        f: impl FnOnce(&mut Context),
199    ) -> Response {
200        let theme = self.theme;
201        let focused = self.register_focusable();
202        let old_open = *open;
203        let toggled_from_key = self.consume_activation_keys(focused);
204        if toggled_from_key {
205            *open = !*open;
206        }
207
208        let icon = if *open { "▾" } else { "▸" };
209        let title_color = if focused { theme.primary } else { theme.text };
210
211        let mut response = self.container().col(|ui| {
212            ui.line(|ui| {
213                ui.text(icon).fg(title_color);
214                let mut title_text = String::with_capacity(1 + title.len());
215                title_text.push(' ');
216                title_text.push_str(title);
217                ui.text(title_text).bold().fg(title_color);
218            });
219        });
220
221        if response.clicked {
222            *open = !*open;
223        }
224
225        if *open {
226            let _ = self.container().pl(2).col(f);
227        }
228
229        response.focused = focused;
230        response.changed = *open != old_open;
231        response
232    }
233
234    /// Render a key-value definition list with aligned columns.
235    pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
236        let max_key_width = items
237            .iter()
238            .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
239            .max()
240            .unwrap_or(0);
241
242        let _ = self.col(|ui| {
243            for (key, value) in items {
244                ui.line(|ui| {
245                    let padded = format!("{:>width$}", key, width = max_key_width);
246                    ui.text(padded).dim();
247                    ui.text("  ");
248                    ui.text(*value);
249                });
250            }
251        });
252
253        Response::none()
254    }
255
256    /// Render a horizontal divider with a centered text label.
257    pub fn divider_text(&mut self, label: &str) -> Response {
258        let w = self.width();
259        let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
260        let pad = 1u32;
261        let left_len = 4u32;
262        let right_len = w.saturating_sub(left_len + pad + label_len + pad);
263        let left: String = "─".repeat(left_len as usize);
264        let right: String = "─".repeat(right_len as usize);
265        let theme = self.theme;
266        self.line(|ui| {
267            ui.text(&left).fg(theme.border);
268            let mut label_text = String::with_capacity(label.len() + 2);
269            label_text.push(' ');
270            label_text.push_str(label);
271            label_text.push(' ');
272            ui.text(label_text).fg(theme.text);
273            ui.text(&right).fg(theme.border);
274        });
275
276        Response::none()
277    }
278
279    /// Render a badge with the theme's primary color.
280    pub fn badge(&mut self, label: &str) -> Response {
281        let theme = self.theme;
282        self.badge_colored(label, theme.primary)
283    }
284
285    /// Render a badge with a custom background color.
286    pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
287        let fg = Color::contrast_fg(color);
288        let mut label_text = String::with_capacity(label.len() + 2);
289        label_text.push(' ');
290        label_text.push_str(label);
291        label_text.push(' ');
292        self.text(label_text).fg(fg).bg(color);
293
294        Response::none()
295    }
296
297    /// Render a keyboard shortcut hint with reversed styling.
298    pub fn key_hint(&mut self, key: &str) -> Response {
299        let theme = self.theme;
300        let mut key_text = String::with_capacity(key.len() + 2);
301        key_text.push(' ');
302        key_text.push_str(key);
303        key_text.push(' ');
304        self.text(key_text).reversed().fg(theme.text_dim);
305
306        Response::none()
307    }
308
309    /// Render a label-value stat pair.
310    pub fn stat(&mut self, label: &str, value: &str) -> Response {
311        let _ = self.col(|ui| {
312            ui.text(label).dim();
313            ui.text(value).bold();
314        });
315
316        Response::none()
317    }
318
319    /// Render a stat pair with a custom value color.
320    pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
321        let _ = self.col(|ui| {
322            ui.text(label).dim();
323            ui.text(value).bold().fg(color);
324        });
325
326        Response::none()
327    }
328
329    /// Render a stat pair with an up/down trend arrow.
330    pub fn stat_trend(
331        &mut self,
332        label: &str,
333        value: &str,
334        trend: crate::widgets::Trend,
335    ) -> Response {
336        let theme = self.theme;
337        let (arrow, color) = match trend {
338            crate::widgets::Trend::Up => ("↑", theme.success),
339            crate::widgets::Trend::Down => ("↓", theme.error),
340        };
341        let _ = self.col(|ui| {
342            ui.text(label).dim();
343            ui.line(|ui| {
344                ui.text(value).bold();
345                let mut arrow_text = String::with_capacity(1 + arrow.len());
346                arrow_text.push(' ');
347                arrow_text.push_str(arrow);
348                ui.text(arrow_text).fg(color);
349            });
350        });
351
352        Response::none()
353    }
354
355    /// Render a centered empty-state placeholder.
356    pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
357        let _ = self.container().center().col(|ui| {
358            ui.text(title).align(Align::Center);
359            ui.text(description).dim().align(Align::Center);
360        });
361
362        Response::none()
363    }
364
365    /// Render a centered empty-state placeholder with an action button.
366    pub fn empty_state_action(
367        &mut self,
368        title: &str,
369        description: &str,
370        action_label: &str,
371    ) -> Response {
372        let mut clicked = false;
373        let _ = self.container().center().col(|ui| {
374            ui.text(title).align(Align::Center);
375            ui.text(description).dim().align(Align::Center);
376            if ui.button(action_label).clicked {
377                clicked = true;
378            }
379        });
380
381        Response {
382            clicked,
383            changed: clicked,
384            ..Response::none()
385        }
386    }
387
388    /// Render a code block with keyword-based syntax highlighting.
389    pub fn code_block(&mut self, code: &str) -> Response {
390        self.code_block_lang(code, "")
391    }
392
393    /// Render a code block with language-aware syntax highlighting.
394    pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
395        let theme = self.theme;
396        let highlighted: Option<Vec<Vec<(String, Style)>>> =
397            crate::syntax::highlight_code(code, lang, &theme);
398        let _ = self
399            .bordered(Border::Rounded)
400            .bg(theme.surface)
401            .pad(1)
402            .col(|ui| {
403                if let Some(ref lines) = highlighted {
404                    render_tree_sitter_lines(ui, lines);
405                } else {
406                    for line in code.lines() {
407                        render_highlighted_line(ui, line);
408                    }
409                }
410            });
411
412        Response::none()
413    }
414
415    /// Render a code block with line numbers and keyword highlighting.
416    pub fn code_block_numbered(&mut self, code: &str) -> Response {
417        self.code_block_numbered_lang(code, "")
418    }
419
420    /// Render a code block with line numbers and language-aware highlighting.
421    pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
422        let lines: Vec<&str> = code.lines().collect();
423        let gutter_w = format!("{}", lines.len()).len();
424        let theme = self.theme;
425        let highlighted: Option<Vec<Vec<(String, Style)>>> =
426            crate::syntax::highlight_code(code, lang, &theme);
427        let _ = self
428            .bordered(Border::Rounded)
429            .bg(theme.surface)
430            .pad(1)
431            .col(|ui| {
432                if let Some(ref hl_lines) = highlighted {
433                    for (i, segs) in hl_lines.iter().enumerate() {
434                        ui.line(|ui| {
435                            ui.text(format!("{:>gutter_w$} │ ", i + 1))
436                                .fg(theme.text_dim);
437                            for (text, style) in segs {
438                                ui.styled(text, *style);
439                            }
440                        });
441                    }
442                } else {
443                    for (i, line) in lines.iter().enumerate() {
444                        ui.line(|ui| {
445                            ui.text(format!("{:>gutter_w$} │ ", i + 1))
446                                .fg(theme.text_dim);
447                            render_highlighted_line(ui, line);
448                        });
449                    }
450                }
451            });
452
453        Response::none()
454    }
455}