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 = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
18
19 let mut response = self.container().col(|ui| {
20 ui.line(|ui| {
21 let mut icon_text = String::with_capacity(icon.len() + 2);
22 icon_text.push(' ');
23 icon_text.push_str(icon);
24 icon_text.push(' ');
25 ui.text(icon_text).fg(color).bold();
26 ui.text(message).grow(1);
27 ui.text(" [×] ").dim();
28 });
29 });
30 response.focused = focused;
31 if key_dismiss {
32 response.clicked = true;
33 }
34
35 response
36 }
37
38 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
52 let focused = self.register_focusable();
53 let mut is_yes = *result;
54 let mut clicked = false;
55
56 if focused {
57 let mut consumed_indices = Vec::new();
58 for (i, event) in self.events.iter().enumerate() {
59 if let Event::Key(key) = event {
60 if key.kind != KeyEventKind::Press {
61 continue;
62 }
63
64 match key.code {
65 KeyCode::Char('y') => {
66 is_yes = true;
67 *result = true;
68 clicked = true;
69 consumed_indices.push(i);
70 }
71 KeyCode::Char('n') => {
72 is_yes = false;
73 *result = false;
74 clicked = true;
75 consumed_indices.push(i);
76 }
77 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
78 is_yes = !is_yes;
79 *result = is_yes;
80 consumed_indices.push(i);
81 }
82 KeyCode::Enter => {
83 *result = is_yes;
84 clicked = true;
85 consumed_indices.push(i);
86 }
87 _ => {}
88 }
89 }
90 }
91
92 for idx in consumed_indices {
93 self.consumed[idx] = true;
94 }
95 }
96
97 let yes_style = if is_yes {
98 if focused {
99 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
100 } else {
101 Style::new().fg(self.theme.success).bold()
102 }
103 } else {
104 Style::new().fg(self.theme.text_dim)
105 };
106 let no_style = if !is_yes {
107 if focused {
108 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
109 } else {
110 Style::new().fg(self.theme.error).bold()
111 }
112 } else {
113 Style::new().fg(self.theme.text_dim)
114 };
115
116 let q_width = UnicodeWidthStr::width(question) as u32;
117 let mut response = self.row(|ui| {
118 ui.text(question);
119 ui.text(" ");
120 ui.styled("[Yes]", yes_style);
121 ui.text(" ");
122 ui.styled("[No]", no_style);
123 });
124
125 if !clicked && response.clicked {
126 if let Some((mx, _)) = self.click_pos {
127 let yes_start = response.rect.x + q_width + 1;
128 let yes_end = yes_start + 5;
129 let no_start = yes_end + 1;
130 if mx >= yes_start && mx < yes_end {
131 is_yes = true;
132 *result = true;
133 clicked = true;
134 } else if mx >= no_start {
135 is_yes = false;
136 *result = false;
137 clicked = true;
138 }
139 }
140 }
141
142 response.focused = focused;
143 response.clicked = clicked;
144 response.changed = clicked;
145 let _ = is_yes;
146 response
147 }
148
149 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
151 self.breadcrumb_with(segments, " › ")
152 }
153
154 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
156 let theme = self.theme;
157 let last_idx = segments.len().saturating_sub(1);
158 let mut clicked_idx: Option<usize> = None;
159
160 let _ = self.row(|ui| {
161 for (i, segment) in segments.iter().enumerate() {
162 let is_last = i == last_idx;
163 if is_last {
164 ui.text(*segment).bold();
165 } else {
166 let focused = ui.register_focusable();
167 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
168 let resp = ui.interaction();
169 let color = if resp.hovered || focused {
170 theme.accent
171 } else {
172 theme.primary
173 };
174 ui.text(*segment).fg(color).underline();
175 if resp.clicked || pressed {
176 clicked_idx = Some(i);
177 }
178 ui.text(separator).dim();
179 }
180 }
181 });
182
183 clicked_idx
184 }
185
186 pub fn accordion(
188 &mut self,
189 title: &str,
190 open: &mut bool,
191 f: impl FnOnce(&mut Context),
192 ) -> Response {
193 let theme = self.theme;
194 let focused = self.register_focusable();
195 let old_open = *open;
196
197 if focused && self.key_code(KeyCode::Enter) {
198 *open = !*open;
199 }
200
201 let icon = if *open { "▾" } else { "▸" };
202 let title_color = if focused { theme.primary } else { theme.text };
203
204 let mut response = self.container().col(|ui| {
205 ui.line(|ui| {
206 ui.text(icon).fg(title_color);
207 let mut title_text = String::with_capacity(1 + title.len());
208 title_text.push(' ');
209 title_text.push_str(title);
210 ui.text(title_text).bold().fg(title_color);
211 });
212 });
213
214 if response.clicked {
215 *open = !*open;
216 }
217
218 if *open {
219 let _ = self.container().pl(2).col(f);
220 }
221
222 response.focused = focused;
223 response.changed = *open != old_open;
224 response
225 }
226
227 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
229 let max_key_width = items
230 .iter()
231 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
232 .max()
233 .unwrap_or(0);
234
235 let _ = self.col(|ui| {
236 for (key, value) in items {
237 ui.line(|ui| {
238 let padded = format!("{:>width$}", key, width = max_key_width);
239 ui.text(padded).dim();
240 ui.text(" ");
241 ui.text(*value);
242 });
243 }
244 });
245
246 Response::none()
247 }
248
249 pub fn divider_text(&mut self, label: &str) -> Response {
251 let w = self.width();
252 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
253 let pad = 1u32;
254 let left_len = 4u32;
255 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
256 let left: String = "─".repeat(left_len as usize);
257 let right: String = "─".repeat(right_len as usize);
258 let theme = self.theme;
259 self.line(|ui| {
260 ui.text(&left).fg(theme.border);
261 let mut label_text = String::with_capacity(label.len() + 2);
262 label_text.push(' ');
263 label_text.push_str(label);
264 label_text.push(' ');
265 ui.text(label_text).fg(theme.text);
266 ui.text(&right).fg(theme.border);
267 });
268
269 Response::none()
270 }
271
272 pub fn badge(&mut self, label: &str) -> Response {
274 let theme = self.theme;
275 self.badge_colored(label, theme.primary)
276 }
277
278 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
280 let fg = Color::contrast_fg(color);
281 let mut label_text = String::with_capacity(label.len() + 2);
282 label_text.push(' ');
283 label_text.push_str(label);
284 label_text.push(' ');
285 self.text(label_text).fg(fg).bg(color);
286
287 Response::none()
288 }
289
290 pub fn key_hint(&mut self, key: &str) -> Response {
292 let theme = self.theme;
293 let mut key_text = String::with_capacity(key.len() + 2);
294 key_text.push(' ');
295 key_text.push_str(key);
296 key_text.push(' ');
297 self.text(key_text).reversed().fg(theme.text_dim);
298
299 Response::none()
300 }
301
302 pub fn stat(&mut self, label: &str, value: &str) -> Response {
304 let _ = self.col(|ui| {
305 ui.text(label).dim();
306 ui.text(value).bold();
307 });
308
309 Response::none()
310 }
311
312 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
314 let _ = self.col(|ui| {
315 ui.text(label).dim();
316 ui.text(value).bold().fg(color);
317 });
318
319 Response::none()
320 }
321
322 pub fn stat_trend(
324 &mut self,
325 label: &str,
326 value: &str,
327 trend: crate::widgets::Trend,
328 ) -> Response {
329 let theme = self.theme;
330 let (arrow, color) = match trend {
331 crate::widgets::Trend::Up => ("↑", theme.success),
332 crate::widgets::Trend::Down => ("↓", theme.error),
333 };
334 let _ = self.col(|ui| {
335 ui.text(label).dim();
336 ui.line(|ui| {
337 ui.text(value).bold();
338 let mut arrow_text = String::with_capacity(1 + arrow.len());
339 arrow_text.push(' ');
340 arrow_text.push_str(arrow);
341 ui.text(arrow_text).fg(color);
342 });
343 });
344
345 Response::none()
346 }
347
348 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
350 let _ = self.container().center().col(|ui| {
351 ui.text(title).align(Align::Center);
352 ui.text(description).dim().align(Align::Center);
353 });
354
355 Response::none()
356 }
357
358 pub fn empty_state_action(
360 &mut self,
361 title: &str,
362 description: &str,
363 action_label: &str,
364 ) -> Response {
365 let mut clicked = false;
366 let _ = self.container().center().col(|ui| {
367 ui.text(title).align(Align::Center);
368 ui.text(description).dim().align(Align::Center);
369 if ui.button(action_label).clicked {
370 clicked = true;
371 }
372 });
373
374 Response {
375 clicked,
376 changed: clicked,
377 ..Response::none()
378 }
379 }
380
381 pub fn code_block(&mut self, code: &str) -> Response {
383 self.code_block_lang(code, "")
384 }
385
386 pub fn code_block_lang(&mut self, code: &str, lang: &str) -> Response {
388 let theme = self.theme;
389 let highlighted: Option<Vec<Vec<(String, Style)>>> =
390 crate::syntax::highlight_code(code, lang, &theme);
391 let _ = self
392 .bordered(Border::Rounded)
393 .bg(theme.surface)
394 .pad(1)
395 .col(|ui| {
396 if let Some(ref lines) = highlighted {
397 render_tree_sitter_lines(ui, lines);
398 } else {
399 for line in code.lines() {
400 render_highlighted_line(ui, line);
401 }
402 }
403 });
404
405 Response::none()
406 }
407
408 pub fn code_block_numbered(&mut self, code: &str) -> Response {
410 self.code_block_numbered_lang(code, "")
411 }
412
413 pub fn code_block_numbered_lang(&mut self, code: &str, lang: &str) -> Response {
415 let lines: Vec<&str> = code.lines().collect();
416 let gutter_w = format!("{}", lines.len()).len();
417 let theme = self.theme;
418 let highlighted: Option<Vec<Vec<(String, Style)>>> =
419 crate::syntax::highlight_code(code, lang, &theme);
420 let _ = self
421 .bordered(Border::Rounded)
422 .bg(theme.surface)
423 .pad(1)
424 .col(|ui| {
425 if let Some(ref hl_lines) = highlighted {
426 for (i, segs) in hl_lines.iter().enumerate() {
427 ui.line(|ui| {
428 ui.text(format!("{:>gutter_w$} │ ", i + 1))
429 .fg(theme.text_dim);
430 for (text, style) in segs {
431 ui.styled(text, *style);
432 }
433 });
434 }
435 } else {
436 for (i, line) in lines.iter().enumerate() {
437 ui.line(|ui| {
438 ui.text(format!("{:>gutter_w$} │ ", i + 1))
439 .fg(theme.text_dim);
440 render_highlighted_line(ui, line);
441 });
442 }
443 }
444 });
445
446 Response::none()
447 }
448}