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 && 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 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
158 self.breadcrumb_with(segments, " › ")
159 }
160
161 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 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 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 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 pub fn badge(&mut self, label: &str) -> Response {
281 let theme = self.theme;
282 self.badge_colored(label, theme.primary)
283 }
284
285 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 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 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 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 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 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 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 pub fn code_block(&mut self, code: &str) -> Response {
390 self.code_block_lang(code, "")
391 }
392
393 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 pub fn code_block_numbered(&mut self, code: &str) -> Response {
417 self.code_block_numbered_lang(code, "")
418 }
419
420 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}