slt/context/widgets_display/
status.rs1use super::*;
2
3impl Context {
4 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 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 {
74 let mut consumed_indices = Vec::new();
75 for (i, key) in self.available_key_presses() {
76 match key.code {
77 KeyCode::Char('y') => {
78 is_yes = true;
79 *result = true;
80 clicked = true;
81 consumed_indices.push(i);
82 }
83 KeyCode::Char('n') => {
84 is_yes = false;
85 *result = false;
86 clicked = true;
87 consumed_indices.push(i);
88 }
89 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
90 is_yes = !is_yes;
91 *result = is_yes;
92 consumed_indices.push(i);
93 }
94 KeyCode::Enter => {
95 *result = is_yes;
96 clicked = true;
97 consumed_indices.push(i);
98 }
99 _ => {}
100 }
101 }
102 self.consume_indices(consumed_indices);
103 }
104
105 let q_width = UnicodeWidthStr::width(question) as u32;
113 if !clicked {
114 if let Some((mx, my)) = self.click_pos {
115 let next_id = self.rollback.interaction_count;
116 let prev_rect = self.prev_hit_map.get(next_id).copied();
117 let row_x = prev_rect.map(|r| r.x).unwrap_or(0);
118 let in_row_y = match prev_rect {
119 Some(r) if r.height > 0 => my >= r.y && my < r.bottom(),
120 _ => true,
121 };
122 if in_row_y {
123 let yes_start = row_x + q_width + 1;
124 let yes_end = yes_start + 5;
125 let no_start = yes_end + 1;
126 let no_end = no_start + 4; if mx >= yes_start && mx < yes_end {
128 is_yes = true;
129 *result = true;
130 clicked = true;
131 } else if mx >= no_start && mx < no_end {
132 is_yes = false;
133 *result = false;
134 clicked = true;
135 }
136 }
137 }
138 }
139
140 let yes_style = if is_yes {
142 if focused {
143 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
144 } else {
145 Style::new().fg(self.theme.success).bold()
146 }
147 } else {
148 Style::new().fg(self.theme.text_dim)
149 };
150 let no_style = if !is_yes {
151 if focused {
152 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
153 } else {
154 Style::new().fg(self.theme.error).bold()
155 }
156 } else {
157 Style::new().fg(self.theme.text_dim)
158 };
159
160 let mut response = self.row(|ui| {
162 ui.text(question);
163 ui.text(" ");
164 ui.styled("[Yes]", yes_style);
165 ui.text(" ");
166 ui.styled("[No]", no_style);
167 });
168
169 response.focused = focused;
170 response.clicked = clicked;
171 response.changed = clicked;
172 response
173 }
174
175 pub fn breadcrumb<'a>(&'a mut self, segments: &'a [&'a str]) -> Breadcrumb<'a> {
201 Breadcrumb::new(self, segments)
202 }
203
204 pub fn accordion(
206 &mut self,
207 title: &str,
208 open: &mut bool,
209 f: impl FnOnce(&mut Context),
210 ) -> Response {
211 let theme = self.theme;
212 let focused = self.register_focusable();
213 let old_open = *open;
214 let toggled_from_key = self.consume_activation_keys(focused);
215 if toggled_from_key {
216 *open = !*open;
217 }
218
219 let icon = if *open { "▾" } else { "▸" };
220 let title_color = if focused { theme.primary } else { theme.text };
221
222 let mut response = self.container().col(|ui| {
223 ui.line(|ui| {
224 ui.text(icon).fg(title_color);
225 let mut title_text = String::with_capacity(1 + title.len());
226 title_text.push(' ');
227 title_text.push_str(title);
228 ui.text(title_text).bold().fg(title_color);
229 });
230 });
231
232 if response.clicked {
233 *open = !*open;
234 }
235
236 if *open {
237 let indent = self.theme.spacing.sm();
238 let _ = self.container().pl(indent).col(f);
239 }
240
241 response.focused = focused;
242 response.changed = *open != old_open;
243 response
244 }
245
246 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
248 let max_key_width = items
249 .iter()
250 .map(|(k, _)| UnicodeWidthStr::width(*k))
251 .max()
252 .unwrap_or(0);
253
254 let _ = self.col(|ui| {
255 for (key, value) in items {
256 ui.line(|ui| {
257 let key_display_w = UnicodeWidthStr::width(*key);
258 let pad = max_key_width.saturating_sub(key_display_w);
259 let mut padded = String::with_capacity(key.len() + pad);
260 padded.extend(std::iter::repeat(' ').take(pad));
261 padded.push_str(key);
262 ui.text(padded).dim();
263 ui.text(" ");
264 ui.text(*value);
265 });
266 }
267 });
268
269 Response::none()
270 }
271
272 pub fn divider_text(&mut self, label: &str) -> Response {
274 let w = self.width();
275 let label_len = UnicodeWidthStr::width(label) as u32;
276 let total_separator = w.saturating_sub(label_len + 2);
280 let left_len = total_separator / 2;
281 let right_len = total_separator - left_len;
282 let left: String = "─".repeat(left_len as usize);
283 let right: String = "─".repeat(right_len as usize);
284 let theme = self.theme;
285 self.line(|ui| {
286 ui.text(&left).fg(theme.border);
287 let mut label_text = String::with_capacity(label.len() + 2);
288 label_text.push(' ');
289 label_text.push_str(label);
290 label_text.push(' ');
291 ui.text(label_text).fg(theme.text);
292 ui.text(&right).fg(theme.border);
293 });
294
295 Response::none()
296 }
297
298 pub fn badge(&mut self, label: &str) -> Response {
308 let theme = self.theme;
309 self.badge_colored(label, theme.primary)
310 }
311
312 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
325 let fg = Color::contrast_fg(color);
326 let mut label_text = String::with_capacity(label.len() + 2);
327 label_text.push(' ');
328 label_text.push_str(label);
329 label_text.push(' ');
330 self.text(label_text).fg(fg).bg(color);
331
332 Response::none()
333 }
334
335 pub fn key_hint(&mut self, key: &str) -> Response {
348 let theme = self.theme;
349 let mut key_text = String::with_capacity(key.len() + 2);
350 key_text.push(' ');
351 key_text.push_str(key);
352 key_text.push(' ');
353 self.text(key_text).reversed().fg(theme.text_dim);
354
355 Response::none()
356 }
357
358 pub fn stat(&mut self, label: &str, value: &str) -> Response {
374 let _ = self.col(|ui| {
375 ui.text(label).dim();
376 ui.text(value).bold();
377 });
378
379 Response::none()
380 }
381
382 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
393 let _ = self.col(|ui| {
394 ui.text(label).dim();
395 ui.text(value).bold().fg(color);
396 });
397
398 Response::none()
399 }
400
401 pub fn stat_trend(
419 &mut self,
420 label: &str,
421 value: &str,
422 trend: crate::widgets::Trend,
423 ) -> Response {
424 let theme = self.theme;
425 let (arrow, color) = match trend {
426 crate::widgets::Trend::Up => ("↑", theme.success),
427 crate::widgets::Trend::Down => ("↓", theme.error),
428 };
429 let _ = self.col(|ui| {
430 ui.text(label).dim();
431 ui.line(|ui| {
432 ui.text(value).bold();
433 let mut arrow_text = String::with_capacity(1 + arrow.len());
434 arrow_text.push(' ');
435 arrow_text.push_str(arrow);
436 ui.text(arrow_text).fg(color);
437 });
438 });
439
440 Response::none()
441 }
442
443 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
459 let _ = self.container().center().col(|ui| {
460 ui.text(title).align(Align::Center);
461 ui.text(description).dim().align(Align::Center);
462 });
463
464 Response::none()
465 }
466
467 pub fn empty_state_action(
486 &mut self,
487 title: &str,
488 description: &str,
489 action_label: &str,
490 ) -> Response {
491 let mut clicked = false;
492 let _ = self.container().center().col(|ui| {
493 ui.text(title).align(Align::Center);
494 ui.text(description).dim().align(Align::Center);
495 if ui.button(action_label).clicked {
496 clicked = true;
497 }
498 });
499
500 Response {
501 clicked,
502 changed: clicked,
503 ..Response::none()
504 }
505 }
506
507 pub fn code_block(&mut self, code: &str) -> Response {
509 self.code_block_lang(code, "")
510 }
511
512 pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
514 let theme = self.theme;
515 let pad = theme.spacing.xs();
516 let highlighted: Option<Vec<Vec<(String, Style)>>> =
517 crate::syntax::highlight_code(code, lang, &theme);
518 let _ = self
519 .bordered(Border::Rounded)
520 .bg(theme.surface)
521 .p(pad)
522 .col(|ui| {
523 if let Some(ref lines) = highlighted {
524 render_tree_sitter_lines(ui, lines);
525 } else {
526 for line in code.lines() {
527 ui.line(|ui| render_highlighted_line(ui, line));
528 }
529 }
530 });
531
532 Response::none()
533 }
534
535 pub fn code_block_numbered(&mut self, code: &str) -> Response {
537 self.code_block_numbered_lang(code, "")
538 }
539
540 pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
542 let lines: Vec<&str> = code.lines().collect();
543 let gutter_w = (lines.len().max(1).ilog10() + 1) as usize;
544 let theme = self.theme;
545 let pad = theme.spacing.xs();
546 let highlighted: Option<Vec<Vec<(String, Style)>>> =
547 crate::syntax::highlight_code(code, lang, &theme);
548 let _ = self
549 .bordered(Border::Rounded)
550 .bg(theme.surface)
551 .p(pad)
552 .col(|ui| {
553 if let Some(ref hl_lines) = highlighted {
554 for (i, segs) in hl_lines.iter().enumerate() {
555 ui.line(|ui| {
556 ui.text(format!("{:>gutter_w$} │ ", i + 1))
557 .fg(theme.text_dim);
558 for (text, style) in segs {
559 ui.styled(text, *style);
560 }
561 });
562 }
563 } else {
564 for (i, line) in lines.iter().enumerate() {
565 ui.line(|ui| {
566 ui.text(format!("{:>gutter_w$} │ ", i + 1))
567 .fg(theme.text_dim);
568 render_highlighted_line(ui, line);
569 });
570 }
571 }
572 });
573
574 Response::none()
575 }
576}
577
578pub struct Breadcrumb<'a> {
588 ctx: Option<&'a mut Context>,
589 segments: &'a [&'a str],
590 separator: &'a str,
591 color: Option<Color>,
592}
593
594impl<'a> Breadcrumb<'a> {
595 pub(super) fn new(ctx: &'a mut Context, segments: &'a [&'a str]) -> Self {
596 Self {
597 ctx: Some(ctx),
598 segments,
599 separator: " › ",
600 color: None,
601 }
602 }
603
604 pub fn separator(mut self, sep: &'a str) -> Self {
606 self.separator = sep;
607 self
608 }
609
610 pub fn color(mut self, color: Color) -> Self {
612 self.color = Some(color);
613 self
614 }
615
616 pub fn show(mut self) -> BreadcrumbResponse {
618 let ctx = self.ctx.take().expect("Breadcrumb::show called twice");
619 render_breadcrumb(ctx, self.segments, self.separator, self.color)
620 }
621}
622
623impl Drop for Breadcrumb<'_> {
624 fn drop(&mut self) {
625 if let Some(ctx) = self.ctx.take() {
626 let _ = render_breadcrumb(ctx, self.segments, self.separator, self.color);
627 }
628 }
629}
630
631fn render_breadcrumb(
632 ctx: &mut Context,
633 segments: &[&str],
634 separator: &str,
635 color_override: Option<Color>,
636) -> BreadcrumbResponse {
637 let theme = ctx.theme;
638 let last_idx = segments.len().saturating_sub(1);
639 let mut clicked_segment: Option<usize> = None;
640 let link_color = color_override.unwrap_or(theme.primary);
641
642 let response = ctx.row(|ui| {
643 for (i, segment) in segments.iter().enumerate() {
644 let is_last = i == last_idx;
645 if is_last {
646 ui.text(*segment).bold();
647 } else {
648 let focused = ui.register_focusable();
649 let resp = ui.interaction();
650 let activated = resp.clicked || ui.consume_activation_keys(focused);
651 let color = if resp.hovered || focused {
652 theme.accent
653 } else {
654 link_color
655 };
656 ui.text(*segment).fg(color).underline();
657 if activated {
658 clicked_segment = Some(i);
659 }
660 ui.text(separator).dim();
661 }
662 }
663 });
664
665 BreadcrumbResponse {
666 response,
667 clicked_segment,
668 }
669}