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 {
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 {
133 if let Some((mx, my)) = self.click_pos {
134 let row_x = response.rect.x;
142 let in_row_y = response.rect.height == 0
143 || (my >= response.rect.y && my < response.rect.bottom());
144 if in_row_y {
145 let yes_start = row_x + q_width + 1;
146 let yes_end = yes_start + 5;
147 let no_start = yes_end + 1;
148 let no_end = no_start + 4; if mx >= yes_start && mx < yes_end {
150 is_yes = true;
151 *result = true;
152 clicked = true;
153 } else if mx >= no_start && mx < no_end {
154 is_yes = false;
155 *result = false;
156 clicked = true;
157 }
158 }
159 }
160 }
161
162 response.focused = focused;
163 response.clicked = clicked;
164 response.changed = clicked;
165 let _ = is_yes;
166 response
167 }
168
169 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
171 self.breadcrumb_with(segments, " › ")
172 }
173
174 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
176 let theme = self.theme;
177 let last_idx = segments.len().saturating_sub(1);
178 let mut clicked_idx: Option<usize> = None;
179
180 let _ = self.row(|ui| {
181 for (i, segment) in segments.iter().enumerate() {
182 let is_last = i == last_idx;
183 if is_last {
184 ui.text(*segment).bold();
185 } else {
186 let focused = ui.register_focusable();
187 let resp = ui.interaction();
188 let activated = resp.clicked || ui.consume_activation_keys(focused);
189 let color = if resp.hovered || focused {
190 theme.accent
191 } else {
192 theme.primary
193 };
194 ui.text(*segment).fg(color).underline();
195 if activated {
196 clicked_idx = Some(i);
197 }
198 ui.text(separator).dim();
199 }
200 }
201 });
202
203 clicked_idx
204 }
205
206 pub fn accordion(
208 &mut self,
209 title: &str,
210 open: &mut bool,
211 f: impl FnOnce(&mut Context),
212 ) -> Response {
213 let theme = self.theme;
214 let focused = self.register_focusable();
215 let old_open = *open;
216 let toggled_from_key = self.consume_activation_keys(focused);
217 if toggled_from_key {
218 *open = !*open;
219 }
220
221 let icon = if *open { "▾" } else { "▸" };
222 let title_color = if focused { theme.primary } else { theme.text };
223
224 let mut response = self.container().col(|ui| {
225 ui.line(|ui| {
226 ui.text(icon).fg(title_color);
227 let mut title_text = String::with_capacity(1 + title.len());
228 title_text.push(' ');
229 title_text.push_str(title);
230 ui.text(title_text).bold().fg(title_color);
231 });
232 });
233
234 if response.clicked {
235 *open = !*open;
236 }
237
238 if *open {
239 let _ = self.container().pl(2).col(f);
240 }
241
242 response.focused = focused;
243 response.changed = *open != old_open;
244 response
245 }
246
247 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
249 let max_key_width = items
250 .iter()
251 .map(|(k, _)| UnicodeWidthStr::width(*k))
252 .max()
253 .unwrap_or(0);
254
255 let _ = self.col(|ui| {
256 for (key, value) in items {
257 ui.line(|ui| {
258 let key_display_w = UnicodeWidthStr::width(*key);
259 let pad = max_key_width.saturating_sub(key_display_w);
260 let mut padded = String::with_capacity(key.len() + pad);
261 padded.extend(std::iter::repeat(' ').take(pad));
262 padded.push_str(key);
263 ui.text(padded).dim();
264 ui.text(" ");
265 ui.text(*value);
266 });
267 }
268 });
269
270 Response::none()
271 }
272
273 pub fn divider_text(&mut self, label: &str) -> Response {
275 let w = self.width();
276 let label_len = UnicodeWidthStr::width(label) as u32;
277 let total_separator = w.saturating_sub(label_len + 2);
281 let left_len = total_separator / 2;
282 let right_len = total_separator - left_len;
283 let left: String = "─".repeat(left_len as usize);
284 let right: String = "─".repeat(right_len as usize);
285 let theme = self.theme;
286 self.line(|ui| {
287 ui.text(&left).fg(theme.border);
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 ui.text(label_text).fg(theme.text);
293 ui.text(&right).fg(theme.border);
294 });
295
296 Response::none()
297 }
298
299 pub fn badge(&mut self, label: &str) -> Response {
301 let theme = self.theme;
302 self.badge_colored(label, theme.primary)
303 }
304
305 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
307 let fg = Color::contrast_fg(color);
308 let mut label_text = String::with_capacity(label.len() + 2);
309 label_text.push(' ');
310 label_text.push_str(label);
311 label_text.push(' ');
312 self.text(label_text).fg(fg).bg(color);
313
314 Response::none()
315 }
316
317 pub fn key_hint(&mut self, key: &str) -> Response {
319 let theme = self.theme;
320 let mut key_text = String::with_capacity(key.len() + 2);
321 key_text.push(' ');
322 key_text.push_str(key);
323 key_text.push(' ');
324 self.text(key_text).reversed().fg(theme.text_dim);
325
326 Response::none()
327 }
328
329 pub fn stat(&mut self, label: &str, value: &str) -> Response {
331 let _ = self.col(|ui| {
332 ui.text(label).dim();
333 ui.text(value).bold();
334 });
335
336 Response::none()
337 }
338
339 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
341 let _ = self.col(|ui| {
342 ui.text(label).dim();
343 ui.text(value).bold().fg(color);
344 });
345
346 Response::none()
347 }
348
349 pub fn stat_trend(
351 &mut self,
352 label: &str,
353 value: &str,
354 trend: crate::widgets::Trend,
355 ) -> Response {
356 let theme = self.theme;
357 let (arrow, color) = match trend {
358 crate::widgets::Trend::Up => ("↑", theme.success),
359 crate::widgets::Trend::Down => ("↓", theme.error),
360 };
361 let _ = self.col(|ui| {
362 ui.text(label).dim();
363 ui.line(|ui| {
364 ui.text(value).bold();
365 let mut arrow_text = String::with_capacity(1 + arrow.len());
366 arrow_text.push(' ');
367 arrow_text.push_str(arrow);
368 ui.text(arrow_text).fg(color);
369 });
370 });
371
372 Response::none()
373 }
374
375 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
377 let _ = self.container().center().col(|ui| {
378 ui.text(title).align(Align::Center);
379 ui.text(description).dim().align(Align::Center);
380 });
381
382 Response::none()
383 }
384
385 pub fn empty_state_action(
387 &mut self,
388 title: &str,
389 description: &str,
390 action_label: &str,
391 ) -> Response {
392 let mut clicked = false;
393 let _ = self.container().center().col(|ui| {
394 ui.text(title).align(Align::Center);
395 ui.text(description).dim().align(Align::Center);
396 if ui.button(action_label).clicked {
397 clicked = true;
398 }
399 });
400
401 Response {
402 clicked,
403 changed: clicked,
404 ..Response::none()
405 }
406 }
407
408 pub fn code_block(&mut self, code: &str) -> Response {
410 self.code_block_lang(code, "")
411 }
412
413 pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
415 let theme = self.theme;
416 let highlighted: Option<Vec<Vec<(String, Style)>>> =
417 crate::syntax::highlight_code(code, lang, &theme);
418 let _ = self
419 .bordered(Border::Rounded)
420 .bg(theme.surface)
421 .pad(1)
422 .col(|ui| {
423 if let Some(ref lines) = highlighted {
424 render_tree_sitter_lines(ui, lines);
425 } else {
426 for line in code.lines() {
427 render_highlighted_line(ui, line);
428 }
429 }
430 });
431
432 Response::none()
433 }
434
435 pub fn code_block_numbered(&mut self, code: &str) -> Response {
437 self.code_block_numbered_lang(code, "")
438 }
439
440 pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
442 let lines: Vec<&str> = code.lines().collect();
443 let gutter_w = (lines.len().max(1).ilog10() + 1) as usize;
444 let theme = self.theme;
445 let highlighted: Option<Vec<Vec<(String, Style)>>> =
446 crate::syntax::highlight_code(code, lang, &theme);
447 let _ = self
448 .bordered(Border::Rounded)
449 .bg(theme.surface)
450 .pad(1)
451 .col(|ui| {
452 if let Some(ref hl_lines) = highlighted {
453 for (i, segs) in hl_lines.iter().enumerate() {
454 ui.line(|ui| {
455 ui.text(format!("{:>gutter_w$} │ ", i + 1))
456 .fg(theme.text_dim);
457 for (text, style) in segs {
458 ui.styled(text, *style);
459 }
460 });
461 }
462 } else {
463 for (i, line) in lines.iter().enumerate() {
464 ui.line(|ui| {
465 ui.text(format!("{:>gutter_w$} │ ", i + 1))
466 .fg(theme.text_dim);
467 render_highlighted_line(ui, line);
468 });
469 }
470 }
471 });
472
473 Response::none()
474 }
475}