Skip to main content

slt/context/
widgets_display.rs

1use super::*;
2use crate::KeyMap;
3
4impl Context {
5    // ── text ──────────────────────────────────────────────────────────
6
7    /// Render a text element. Returns `&mut Self` for style chaining.
8    ///
9    /// # Example
10    ///
11    /// ```no_run
12    /// # slt::run(|ui: &mut slt::Context| {
13    /// use slt::Color;
14    /// ui.text("hello").bold().fg(Color::Cyan);
15    /// # });
16    /// ```
17    pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
18        let content = s.into();
19        self.commands.push(Command::Text {
20            content,
21            style: Style::new().fg(self.theme.text),
22            grow: 0,
23            align: Align::Start,
24            wrap: false,
25            margin: Margin::default(),
26            constraints: Constraints::default(),
27        });
28        self.last_text_idx = Some(self.commands.len() - 1);
29        self
30    }
31
32    /// Render a clickable hyperlink.
33    ///
34    /// The link is interactive: clicking it (or pressing Enter/Space when
35    /// focused) opens the URL in the system browser. OSC 8 is also emitted
36    /// for terminals that support native hyperlinks.
37    pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
38        let url_str = url.into();
39        let focused = self.register_focusable();
40        let interaction_id = self.interaction_count;
41        self.interaction_count += 1;
42        let response = self.response_for(interaction_id);
43
44        let mut activated = response.clicked;
45        if focused {
46            for (i, event) in self.events.iter().enumerate() {
47                if let Event::Key(key) = event {
48                    if key.kind != KeyEventKind::Press {
49                        continue;
50                    }
51                    if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
52                        activated = true;
53                        self.consumed[i] = true;
54                    }
55                }
56            }
57        }
58
59        if activated {
60            let _ = open_url(&url_str);
61        }
62
63        let style = if focused {
64            Style::new()
65                .fg(self.theme.primary)
66                .bg(self.theme.surface_hover)
67                .underline()
68                .bold()
69        } else if response.hovered {
70            Style::new()
71                .fg(self.theme.accent)
72                .bg(self.theme.surface_hover)
73                .underline()
74        } else {
75            Style::new().fg(self.theme.primary).underline()
76        };
77
78        self.commands.push(Command::Link {
79            text: text.into(),
80            url: url_str,
81            style,
82            margin: Margin::default(),
83            constraints: Constraints::default(),
84        });
85        self.last_text_idx = Some(self.commands.len() - 1);
86        self
87    }
88
89    /// Render a text element with word-boundary wrapping.
90    ///
91    /// Long lines are broken at word boundaries to fit the container width.
92    /// Style chaining works the same as [`Context::text`].
93    pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
94        let content = s.into();
95        self.commands.push(Command::Text {
96            content,
97            style: Style::new().fg(self.theme.text),
98            grow: 0,
99            align: Align::Start,
100            wrap: true,
101            margin: Margin::default(),
102            constraints: Constraints::default(),
103        });
104        self.last_text_idx = Some(self.commands.len() - 1);
105        self
106    }
107
108    /// Render help bar from a KeyMap. Shows visible bindings as key-description pairs.
109    pub fn help_from_keymap(&mut self, keymap: &KeyMap) -> Response {
110        let pairs: Vec<(&str, &str)> = keymap
111            .visible_bindings()
112            .map(|binding| (binding.display.as_str(), binding.description.as_str()))
113            .collect();
114        self.help(&pairs)
115    }
116
117    // ── style chain (applies to last text) ───────────────────────────
118
119    /// Apply bold to the last rendered text element.
120    pub fn bold(&mut self) -> &mut Self {
121        self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
122        self
123    }
124
125    /// Apply dim styling to the last rendered text element.
126    ///
127    /// Also sets the foreground color to the theme's `text_dim` color if no
128    /// explicit foreground has been set.
129    pub fn dim(&mut self) -> &mut Self {
130        let text_dim = self.theme.text_dim;
131        self.modify_last_style(|s| {
132            s.modifiers |= Modifiers::DIM;
133            if s.fg.is_none() {
134                s.fg = Some(text_dim);
135            }
136        });
137        self
138    }
139
140    /// Apply italic to the last rendered text element.
141    pub fn italic(&mut self) -> &mut Self {
142        self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
143        self
144    }
145
146    /// Apply underline to the last rendered text element.
147    pub fn underline(&mut self) -> &mut Self {
148        self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
149        self
150    }
151
152    /// Apply reverse-video to the last rendered text element.
153    pub fn reversed(&mut self) -> &mut Self {
154        self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
155        self
156    }
157
158    /// Apply strikethrough to the last rendered text element.
159    pub fn strikethrough(&mut self) -> &mut Self {
160        self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
161        self
162    }
163
164    /// Set the foreground color of the last rendered text element.
165    pub fn fg(&mut self, color: Color) -> &mut Self {
166        self.modify_last_style(|s| s.fg = Some(color));
167        self
168    }
169
170    /// Set the background color of the last rendered text element.
171    pub fn bg(&mut self, color: Color) -> &mut Self {
172        self.modify_last_style(|s| s.bg = Some(color));
173        self
174    }
175
176    pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
177        let apply_group_style = self
178            .group_stack
179            .last()
180            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
181            .unwrap_or(false);
182        if apply_group_style {
183            self.modify_last_style(|s| s.fg = Some(color));
184        }
185        self
186    }
187
188    pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
189        let apply_group_style = self
190            .group_stack
191            .last()
192            .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
193            .unwrap_or(false);
194        if apply_group_style {
195            self.modify_last_style(|s| s.bg = Some(color));
196        }
197        self
198    }
199
200    /// Render a text element with an explicit [`Style`] applied immediately.
201    ///
202    /// Equivalent to calling `text(s)` followed by style-chain methods, but
203    /// more concise when you already have a `Style` value.
204    pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
205        self.commands.push(Command::Text {
206            content: s.into(),
207            style,
208            grow: 0,
209            align: Align::Start,
210            wrap: false,
211            margin: Margin::default(),
212            constraints: Constraints::default(),
213        });
214        self.last_text_idx = Some(self.commands.len() - 1);
215        self
216    }
217
218    /// Render a half-block image in the terminal.
219    ///
220    /// Each terminal cell displays two vertical pixels using the `▀` character
221    /// with foreground (upper pixel) and background (lower pixel) colors.
222    ///
223    /// Create a [`HalfBlockImage`] from a file (requires `image` feature):
224    /// ```ignore
225    /// let img = image::open("photo.png").unwrap();
226    /// let half = HalfBlockImage::from_dynamic(&img, 40, 20);
227    /// ui.image(&half);
228    /// ```
229    ///
230    /// Or from raw RGB data (no feature needed):
231    /// ```no_run
232    /// # use slt::{Context, HalfBlockImage};
233    /// # slt::run(|ui: &mut Context| {
234    /// let rgb = vec![255u8; 30 * 20 * 3];
235    /// let half = HalfBlockImage::from_rgb(&rgb, 30, 10);
236    /// ui.image(&half);
237    /// # });
238    /// ```
239    pub fn image(&mut self, img: &HalfBlockImage) -> Response {
240        let width = img.width;
241        let height = img.height;
242
243        self.container().w(width).h(height).gap(0).col(|ui| {
244            for row in 0..height {
245                ui.container().gap(0).row(|ui| {
246                    for col in 0..width {
247                        let idx = (row * width + col) as usize;
248                        if let Some(&(upper, lower)) = img.pixels.get(idx) {
249                            ui.styled("▀", Style::new().fg(upper).bg(lower));
250                        }
251                    }
252                });
253            }
254        });
255
256        Response::none()
257    }
258
259    /// Render streaming text with a typing cursor indicator.
260    ///
261    /// Displays the accumulated text content. While `streaming` is true,
262    /// shows a blinking cursor (`▌`) at the end.
263    ///
264    /// ```no_run
265    /// # use slt::widgets::StreamingTextState;
266    /// # slt::run(|ui: &mut slt::Context| {
267    /// let mut stream = StreamingTextState::new();
268    /// stream.start();
269    /// stream.push("Hello from ");
270    /// stream.push("the AI!");
271    /// ui.streaming_text(&mut stream);
272    /// # });
273    /// ```
274    pub fn streaming_text(&mut self, state: &mut StreamingTextState) -> Response {
275        if state.streaming {
276            state.cursor_tick = state.cursor_tick.wrapping_add(1);
277            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
278        }
279
280        if state.content.is_empty() && state.streaming {
281            let cursor = if state.cursor_visible { "▌" } else { " " };
282            let primary = self.theme.primary;
283            self.text(cursor).fg(primary);
284            return Response::none();
285        }
286
287        if !state.content.is_empty() {
288            if state.streaming && state.cursor_visible {
289                self.text_wrap(format!("{}▌", state.content));
290            } else {
291                self.text_wrap(&state.content);
292            }
293        }
294
295        Response::none()
296    }
297
298    /// Render streaming markdown with a typing cursor indicator.
299    ///
300    /// Parses accumulated markdown content line-by-line while streaming.
301    /// Supports headings, lists, inline formatting, horizontal rules, and
302    /// fenced code blocks with open/close tracking across stream chunks.
303    ///
304    /// ```no_run
305    /// # use slt::widgets::StreamingMarkdownState;
306    /// # slt::run(|ui: &mut slt::Context| {
307    /// let mut stream = StreamingMarkdownState::new();
308    /// stream.start();
309    /// stream.push("# Hello\n");
310    /// stream.push("- **streaming** markdown\n");
311    /// stream.push("```rust\nlet x = 1;\n");
312    /// ui.streaming_markdown(&mut stream);
313    /// # });
314    /// ```
315    pub fn streaming_markdown(
316        &mut self,
317        state: &mut crate::widgets::StreamingMarkdownState,
318    ) -> Response {
319        if state.streaming {
320            state.cursor_tick = state.cursor_tick.wrapping_add(1);
321            state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
322        }
323
324        if state.content.is_empty() && state.streaming {
325            let cursor = if state.cursor_visible { "▌" } else { " " };
326            let primary = self.theme.primary;
327            self.text(cursor).fg(primary);
328            return Response::none();
329        }
330
331        let show_cursor = state.streaming && state.cursor_visible;
332        let trailing_newline = state.content.ends_with('\n');
333        let lines: Vec<&str> = state.content.lines().collect();
334        let last_line_index = lines.len().saturating_sub(1);
335
336        self.commands.push(Command::BeginContainer {
337            direction: Direction::Column,
338            gap: 0,
339            align: Align::Start,
340            justify: Justify::Start,
341            border: None,
342            border_sides: BorderSides::all(),
343            border_style: Style::new().fg(self.theme.border),
344            bg_color: None,
345            padding: Padding::default(),
346            margin: Margin::default(),
347            constraints: Constraints::default(),
348            title: None,
349            grow: 0,
350            group_name: None,
351        });
352        self.interaction_count += 1;
353
354        let text_style = Style::new().fg(self.theme.text);
355        let bold_style = Style::new().fg(self.theme.text).bold();
356        let code_style = Style::new().fg(self.theme.accent);
357        let border_style = Style::new().fg(self.theme.border).dim();
358
359        let mut in_code_block = false;
360        let mut code_block_lang = String::new();
361
362        for (idx, line) in lines.iter().enumerate() {
363            let line = *line;
364            let trimmed = line.trim();
365            let append_cursor = show_cursor && !trailing_newline && idx == last_line_index;
366            let cursor = if append_cursor { "▌" } else { "" };
367
368            if in_code_block {
369                if trimmed.starts_with("```") {
370                    in_code_block = false;
371                    code_block_lang.clear();
372                    self.styled(format!("  └────{cursor}"), border_style);
373                } else {
374                    self.styled(format!("  {line}{cursor}"), code_style);
375                }
376                continue;
377            }
378
379            if trimmed.is_empty() {
380                if append_cursor {
381                    self.styled("▌", Style::new().fg(self.theme.primary));
382                } else {
383                    self.text(" ");
384                }
385                continue;
386            }
387
388            if trimmed == "---" || trimmed == "***" || trimmed == "___" {
389                self.styled(format!("{}{}", "─".repeat(40), cursor), border_style);
390                continue;
391            }
392
393            if let Some(heading) = trimmed.strip_prefix("### ") {
394                self.styled(
395                    format!("{heading}{cursor}"),
396                    Style::new().bold().fg(self.theme.accent),
397                );
398                continue;
399            }
400
401            if let Some(heading) = trimmed.strip_prefix("## ") {
402                self.styled(
403                    format!("{heading}{cursor}"),
404                    Style::new().bold().fg(self.theme.secondary),
405                );
406                continue;
407            }
408
409            if let Some(heading) = trimmed.strip_prefix("# ") {
410                self.styled(
411                    format!("{heading}{cursor}"),
412                    Style::new().bold().fg(self.theme.primary),
413                );
414                continue;
415            }
416
417            if let Some(code) = trimmed.strip_prefix("```") {
418                in_code_block = true;
419                code_block_lang = code.trim().to_string();
420                let label = if code_block_lang.is_empty() {
421                    "code".to_string()
422                } else {
423                    format!("code:{}", code_block_lang)
424                };
425                self.styled(format!("  ┌─{label}─{cursor}"), border_style);
426                continue;
427            }
428
429            if let Some(item) = trimmed
430                .strip_prefix("- ")
431                .or_else(|| trimmed.strip_prefix("* "))
432            {
433                let segs = Self::parse_inline_segments(item, text_style, bold_style, code_style);
434                if segs.len() <= 1 {
435                    self.styled(format!("  • {item}{cursor}"), text_style);
436                } else {
437                    self.line(|ui| {
438                        ui.styled("  • ", text_style);
439                        for (s, st) in segs {
440                            ui.styled(s, st);
441                        }
442                        if append_cursor {
443                            ui.styled("▌", Style::new().fg(ui.theme.primary));
444                        }
445                    });
446                }
447                continue;
448            }
449
450            if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
451                let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
452                if parts.len() == 2 {
453                    let segs =
454                        Self::parse_inline_segments(parts[1], text_style, bold_style, code_style);
455                    if segs.len() <= 1 {
456                        self.styled(
457                            format!("  {}. {}{}", parts[0], parts[1], cursor),
458                            text_style,
459                        );
460                    } else {
461                        self.line(|ui| {
462                            ui.styled(format!("  {}. ", parts[0]), text_style);
463                            for (s, st) in segs {
464                                ui.styled(s, st);
465                            }
466                            if append_cursor {
467                                ui.styled("▌", Style::new().fg(ui.theme.primary));
468                            }
469                        });
470                    }
471                } else {
472                    self.styled(format!("{trimmed}{cursor}"), text_style);
473                }
474                continue;
475            }
476
477            let segs = Self::parse_inline_segments(trimmed, text_style, bold_style, code_style);
478            if segs.len() <= 1 {
479                self.styled(format!("{trimmed}{cursor}"), text_style);
480            } else {
481                self.line(|ui| {
482                    for (s, st) in segs {
483                        ui.styled(s, st);
484                    }
485                    if append_cursor {
486                        ui.styled("▌", Style::new().fg(ui.theme.primary));
487                    }
488                });
489            }
490        }
491
492        if show_cursor && trailing_newline {
493            if in_code_block {
494                self.styled("  ▌", code_style);
495            } else {
496                self.styled("▌", Style::new().fg(self.theme.primary));
497            }
498        }
499
500        state.in_code_block = in_code_block;
501        state.code_block_lang = code_block_lang;
502
503        self.commands.push(Command::EndContainer);
504        self.last_text_idx = None;
505        Response::none()
506    }
507
508    /// Render a tool approval widget with approve/reject buttons.
509    ///
510    /// Shows the tool name, description, and two action buttons.
511    /// Returns the updated [`ApprovalAction`] each frame.
512    ///
513    /// ```no_run
514    /// # use slt::widgets::{ApprovalAction, ToolApprovalState};
515    /// # slt::run(|ui: &mut slt::Context| {
516    /// let mut tool = ToolApprovalState::new("read_file", "Read contents of config.toml");
517    /// ui.tool_approval(&mut tool);
518    /// if tool.action == ApprovalAction::Approved {
519    /// }
520    /// # });
521    /// ```
522    pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
523        let old_action = state.action;
524        let theme = self.theme;
525        self.bordered(Border::Rounded).col(|ui| {
526            ui.row(|ui| {
527                ui.text("⚡").fg(theme.warning);
528                ui.text(&state.tool_name).bold().fg(theme.primary);
529            });
530            ui.text(&state.description).dim();
531
532            if state.action == ApprovalAction::Pending {
533                ui.row(|ui| {
534                    if ui.button("✓ Approve").clicked {
535                        state.action = ApprovalAction::Approved;
536                    }
537                    if ui.button("✗ Reject").clicked {
538                        state.action = ApprovalAction::Rejected;
539                    }
540                });
541            } else {
542                let (label, color) = match state.action {
543                    ApprovalAction::Approved => ("✓ Approved", theme.success),
544                    ApprovalAction::Rejected => ("✗ Rejected", theme.error),
545                    ApprovalAction::Pending => unreachable!(),
546                };
547                ui.text(label).fg(color).bold();
548            }
549        });
550
551        Response {
552            changed: state.action != old_action,
553            ..Response::none()
554        }
555    }
556
557    /// Render a context bar showing active context items with token counts.
558    ///
559    /// Displays a horizontal bar of context sources (files, URLs, etc.)
560    /// with their token counts, useful for AI chat interfaces.
561    ///
562    /// ```no_run
563    /// # use slt::widgets::ContextItem;
564    /// # slt::run(|ui: &mut slt::Context| {
565    /// let items = vec![ContextItem::new("main.rs", 1200), ContextItem::new("lib.rs", 800)];
566    /// ui.context_bar(&items);
567    /// # });
568    /// ```
569    pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
570        if items.is_empty() {
571            return Response::none();
572        }
573
574        let theme = self.theme;
575        let total: usize = items.iter().map(|item| item.tokens).sum();
576
577        self.container().row(|ui| {
578            ui.text("📎").dim();
579            for item in items {
580                ui.text(format!(
581                    "{} ({})",
582                    item.label,
583                    format_token_count(item.tokens)
584                ))
585                .fg(theme.secondary);
586            }
587            ui.spacer();
588            ui.text(format!("Σ {}", format_token_count(total))).dim();
589        });
590
591        Response::none()
592    }
593
594    pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
595        use crate::widgets::AlertLevel;
596
597        let theme = self.theme;
598        let (icon, color) = match level {
599            AlertLevel::Info => ("ℹ", theme.accent),
600            AlertLevel::Success => ("✓", theme.success),
601            AlertLevel::Warning => ("⚠", theme.warning),
602            AlertLevel::Error => ("✕", theme.error),
603        };
604
605        let focused = self.register_focusable();
606        let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
607
608        let mut response = self.container().col(|ui| {
609            ui.line(|ui| {
610                ui.text(format!(" {icon} ")).fg(color).bold();
611                ui.text(message).grow(1);
612                ui.text(" [×] ").dim();
613            });
614        });
615        response.focused = focused;
616        if key_dismiss {
617            response.clicked = true;
618        }
619
620        response
621    }
622
623    /// Yes/No confirmation dialog. Returns Response with .clicked=true when answered.
624    ///
625    /// `result` is set to true for Yes, false for No.
626    ///
627    /// # Examples
628    /// ```
629    /// # use slt::*;
630    /// # TestBackend::new(80, 24).render(|ui| {
631    /// let mut answer = false;
632    /// let r = ui.confirm("Delete this file?", &mut answer);
633    /// if r.clicked && answer { /* user confirmed */ }
634    /// # });
635    /// ```
636    pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
637        let focused = self.register_focusable();
638        let mut is_yes = *result;
639        let mut clicked = false;
640
641        if focused {
642            let mut consumed_indices = Vec::new();
643            for (i, event) in self.events.iter().enumerate() {
644                if let Event::Key(key) = event {
645                    if key.kind != KeyEventKind::Press {
646                        continue;
647                    }
648
649                    match key.code {
650                        KeyCode::Char('y') => {
651                            is_yes = true;
652                            *result = true;
653                            clicked = true;
654                            consumed_indices.push(i);
655                        }
656                        KeyCode::Char('n') => {
657                            is_yes = false;
658                            *result = false;
659                            clicked = true;
660                            consumed_indices.push(i);
661                        }
662                        KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
663                            is_yes = !is_yes;
664                            *result = is_yes;
665                            consumed_indices.push(i);
666                        }
667                        KeyCode::Enter => {
668                            *result = is_yes;
669                            clicked = true;
670                            consumed_indices.push(i);
671                        }
672                        _ => {}
673                    }
674                }
675            }
676
677            for idx in consumed_indices {
678                self.consumed[idx] = true;
679            }
680        }
681
682        let yes_style = if is_yes {
683            if focused {
684                Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
685            } else {
686                Style::new().fg(self.theme.success).bold()
687            }
688        } else {
689            Style::new().fg(self.theme.text_dim)
690        };
691        let no_style = if !is_yes {
692            if focused {
693                Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
694            } else {
695                Style::new().fg(self.theme.error).bold()
696            }
697        } else {
698            Style::new().fg(self.theme.text_dim)
699        };
700
701        let mut response = self.row(|ui| {
702            ui.text(question);
703            ui.text(" ");
704            ui.styled("[Yes]", yes_style);
705            ui.text(" ");
706            ui.styled("[No]", no_style);
707        });
708        response.focused = focused;
709        response.clicked = clicked;
710        response.changed = clicked;
711        response
712    }
713
714    pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
715        self.breadcrumb_with(segments, " › ")
716    }
717
718    pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
719        let theme = self.theme;
720        let last_idx = segments.len().saturating_sub(1);
721        let mut clicked_idx: Option<usize> = None;
722
723        self.row(|ui| {
724            for (i, segment) in segments.iter().enumerate() {
725                let is_last = i == last_idx;
726                if is_last {
727                    ui.text(*segment).bold();
728                } else {
729                    let focused = ui.register_focusable();
730                    let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
731                    let resp = ui.interaction();
732                    let color = if resp.hovered || focused {
733                        theme.accent
734                    } else {
735                        theme.primary
736                    };
737                    ui.text(*segment).fg(color).underline();
738                    if resp.clicked || pressed {
739                        clicked_idx = Some(i);
740                    }
741                    ui.text(separator).dim();
742                }
743            }
744        });
745
746        clicked_idx
747    }
748
749    pub fn accordion(
750        &mut self,
751        title: &str,
752        open: &mut bool,
753        f: impl FnOnce(&mut Context),
754    ) -> Response {
755        let theme = self.theme;
756        let focused = self.register_focusable();
757        let old_open = *open;
758
759        if focused && self.key_code(KeyCode::Enter) {
760            *open = !*open;
761        }
762
763        let icon = if *open { "▾" } else { "▸" };
764        let title_color = if focused { theme.primary } else { theme.text };
765
766        let mut response = self.container().col(|ui| {
767            ui.line(|ui| {
768                ui.text(icon).fg(title_color);
769                ui.text(format!(" {title}")).bold().fg(title_color);
770            });
771        });
772
773        if response.clicked {
774            *open = !*open;
775        }
776
777        if *open {
778            self.container().pl(2).col(f);
779        }
780
781        response.focused = focused;
782        response.changed = *open != old_open;
783        response
784    }
785
786    pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
787        let max_key_width = items
788            .iter()
789            .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
790            .max()
791            .unwrap_or(0);
792
793        self.col(|ui| {
794            for (key, value) in items {
795                ui.line(|ui| {
796                    let padded = format!("{:>width$}", key, width = max_key_width);
797                    ui.text(padded).dim();
798                    ui.text("  ");
799                    ui.text(*value);
800                });
801            }
802        });
803
804        Response::none()
805    }
806
807    pub fn divider_text(&mut self, label: &str) -> Response {
808        let w = self.width();
809        let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
810        let pad = 1u32;
811        let left_len = 4u32;
812        let right_len = w.saturating_sub(left_len + pad + label_len + pad);
813        let left: String = "─".repeat(left_len as usize);
814        let right: String = "─".repeat(right_len as usize);
815        let theme = self.theme;
816        self.line(|ui| {
817            ui.text(&left).fg(theme.border);
818            ui.text(format!(" {} ", label)).fg(theme.text);
819            ui.text(&right).fg(theme.border);
820        });
821
822        Response::none()
823    }
824
825    pub fn badge(&mut self, label: &str) -> Response {
826        let theme = self.theme;
827        self.badge_colored(label, theme.primary)
828    }
829
830    pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
831        let fg = Color::contrast_fg(color);
832        self.text(format!(" {} ", label)).fg(fg).bg(color);
833
834        Response::none()
835    }
836
837    pub fn key_hint(&mut self, key: &str) -> Response {
838        let theme = self.theme;
839        self.text(format!(" {} ", key))
840            .reversed()
841            .fg(theme.text_dim);
842
843        Response::none()
844    }
845
846    pub fn stat(&mut self, label: &str, value: &str) -> Response {
847        self.col(|ui| {
848            ui.text(label).dim();
849            ui.text(value).bold();
850        });
851
852        Response::none()
853    }
854
855    pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
856        self.col(|ui| {
857            ui.text(label).dim();
858            ui.text(value).bold().fg(color);
859        });
860
861        Response::none()
862    }
863
864    pub fn stat_trend(
865        &mut self,
866        label: &str,
867        value: &str,
868        trend: crate::widgets::Trend,
869    ) -> Response {
870        let theme = self.theme;
871        let (arrow, color) = match trend {
872            crate::widgets::Trend::Up => ("↑", theme.success),
873            crate::widgets::Trend::Down => ("↓", theme.error),
874        };
875        self.col(|ui| {
876            ui.text(label).dim();
877            ui.line(|ui| {
878                ui.text(value).bold();
879                ui.text(format!(" {arrow}")).fg(color);
880            });
881        });
882
883        Response::none()
884    }
885
886    pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
887        self.container().center().col(|ui| {
888            ui.text(title).align(Align::Center);
889            ui.text(description).dim().align(Align::Center);
890        });
891
892        Response::none()
893    }
894
895    pub fn empty_state_action(
896        &mut self,
897        title: &str,
898        description: &str,
899        action_label: &str,
900    ) -> Response {
901        let mut clicked = false;
902        self.container().center().col(|ui| {
903            ui.text(title).align(Align::Center);
904            ui.text(description).dim().align(Align::Center);
905            if ui.button(action_label).clicked {
906                clicked = true;
907            }
908        });
909
910        Response {
911            clicked,
912            changed: clicked,
913            ..Response::none()
914        }
915    }
916
917    pub fn code_block(&mut self, code: &str) -> Response {
918        let theme = self.theme;
919        self.bordered(Border::Rounded)
920            .bg(theme.surface)
921            .pad(1)
922            .col(|ui| {
923                for line in code.lines() {
924                    render_highlighted_line(ui, line);
925                }
926            });
927
928        Response::none()
929    }
930
931    pub fn code_block_numbered(&mut self, code: &str) -> Response {
932        let lines: Vec<&str> = code.lines().collect();
933        let gutter_w = format!("{}", lines.len()).len();
934        let theme = self.theme;
935        self.bordered(Border::Rounded)
936            .bg(theme.surface)
937            .pad(1)
938            .col(|ui| {
939                for (i, line) in lines.iter().enumerate() {
940                    ui.line(|ui| {
941                        ui.text(format!("{:>gutter_w$} │ ", i + 1))
942                            .fg(theme.text_dim);
943                        render_highlighted_line(ui, line);
944                    });
945                }
946            });
947
948        Response::none()
949    }
950
951    /// Enable word-boundary wrapping on the last rendered text element.
952    pub fn wrap(&mut self) -> &mut Self {
953        if let Some(idx) = self.last_text_idx {
954            if let Command::Text { wrap, .. } = &mut self.commands[idx] {
955                *wrap = true;
956            }
957        }
958        self
959    }
960
961    fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
962        if let Some(idx) = self.last_text_idx {
963            match &mut self.commands[idx] {
964                Command::Text { style, .. } | Command::Link { style, .. } => f(style),
965                _ => {}
966            }
967        }
968    }
969
970    // ── containers ───────────────────────────────────────────────────
971
972    /// Create a vertical (column) container.
973    ///
974    /// Children are stacked top-to-bottom. Returns a [`Response`] with
975    /// click/hover state for the container area.
976    ///
977    /// # Example
978    ///
979    /// ```no_run
980    /// # slt::run(|ui: &mut slt::Context| {
981    /// ui.col(|ui| {
982    ///     ui.text("line one");
983    ///     ui.text("line two");
984    /// });
985    /// # });
986    /// ```
987    pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
988        self.push_container(Direction::Column, 0, f)
989    }
990
991    /// Create a vertical (column) container with a gap between children.
992    ///
993    /// `gap` is the number of blank rows inserted between each child.
994    pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
995        self.push_container(Direction::Column, gap, f)
996    }
997
998    /// Create a horizontal (row) container.
999    ///
1000    /// Children are placed left-to-right. Returns a [`Response`] with
1001    /// click/hover state for the container area.
1002    ///
1003    /// # Example
1004    ///
1005    /// ```no_run
1006    /// # slt::run(|ui: &mut slt::Context| {
1007    /// ui.row(|ui| {
1008    ///     ui.text("left");
1009    ///     ui.spacer();
1010    ///     ui.text("right");
1011    /// });
1012    /// # });
1013    /// ```
1014    pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
1015        self.push_container(Direction::Row, 0, f)
1016    }
1017
1018    /// Create a horizontal (row) container with a gap between children.
1019    ///
1020    /// `gap` is the number of blank columns inserted between each child.
1021    pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
1022        self.push_container(Direction::Row, gap, f)
1023    }
1024
1025    /// Render inline text with mixed styles on a single line.
1026    ///
1027    /// Unlike [`row`](Context::row), `line()` is designed for rich text —
1028    /// children are rendered as continuous inline text without gaps.
1029    ///
1030    /// # Example
1031    ///
1032    /// ```no_run
1033    /// # use slt::Color;
1034    /// # slt::run(|ui: &mut slt::Context| {
1035    /// ui.line(|ui| {
1036    ///     ui.text("Status: ");
1037    ///     ui.text("Online").bold().fg(Color::Green);
1038    /// });
1039    /// # });
1040    /// ```
1041    pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1042        let _ = self.push_container(Direction::Row, 0, f);
1043        self
1044    }
1045
1046    /// Render inline text with mixed styles, wrapping at word boundaries.
1047    ///
1048    /// Like [`line`](Context::line), but when the combined text exceeds
1049    /// the container width it wraps across multiple lines while
1050    /// preserving per-segment styles.
1051    ///
1052    /// # Example
1053    ///
1054    /// ```no_run
1055    /// # use slt::{Color, Style};
1056    /// # slt::run(|ui: &mut slt::Context| {
1057    /// ui.line_wrap(|ui| {
1058    ///     ui.text("This is a long ");
1059    ///     ui.text("important").bold().fg(Color::Red);
1060    ///     ui.text(" message that wraps across lines");
1061    /// });
1062    /// # });
1063    /// ```
1064    pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
1065        let start = self.commands.len();
1066        f(self);
1067        let mut segments: Vec<(String, Style)> = Vec::new();
1068        for cmd in self.commands.drain(start..) {
1069            if let Command::Text { content, style, .. } = cmd {
1070                segments.push((content, style));
1071            }
1072        }
1073        self.commands.push(Command::RichText {
1074            segments,
1075            wrap: true,
1076            align: Align::Start,
1077            margin: Margin::default(),
1078            constraints: Constraints::default(),
1079        });
1080        self.last_text_idx = None;
1081        self
1082    }
1083
1084    /// Render content in a modal overlay with dimmed background.
1085    ///
1086    /// ```ignore
1087    /// ui.modal(|ui| {
1088    ///     ui.text("Are you sure?");
1089    ///     if ui.button("OK") { show = false; }
1090    /// });
1091    /// ```
1092    pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
1093        self.commands.push(Command::BeginOverlay { modal: true });
1094        self.overlay_depth += 1;
1095        self.modal_active = true;
1096        f(self);
1097        self.overlay_depth = self.overlay_depth.saturating_sub(1);
1098        self.commands.push(Command::EndOverlay);
1099        self.last_text_idx = None;
1100    }
1101
1102    /// Render floating content without dimming the background.
1103    pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
1104        self.commands.push(Command::BeginOverlay { modal: false });
1105        self.overlay_depth += 1;
1106        f(self);
1107        self.overlay_depth = self.overlay_depth.saturating_sub(1);
1108        self.commands.push(Command::EndOverlay);
1109        self.last_text_idx = None;
1110    }
1111
1112    /// Create a named group container for shared hover/focus styling.
1113    ///
1114    /// ```ignore
1115    /// ui.group("card").border(Border::Rounded)
1116    ///     .group_hover_bg(Color::Indexed(238))
1117    ///     .col(|ui| { ui.text("Hover anywhere"); });
1118    /// ```
1119    pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
1120        self.group_count = self.group_count.saturating_add(1);
1121        self.group_stack.push(name.to_string());
1122        self.container().group_name(name.to_string())
1123    }
1124
1125    /// Create a container with a fluent builder.
1126    ///
1127    /// Use this for borders, padding, grow, constraints, and titles. Chain
1128    /// configuration methods on the returned [`ContainerBuilder`], then call
1129    /// `.col()` or `.row()` to finalize.
1130    ///
1131    /// # Example
1132    ///
1133    /// ```no_run
1134    /// # slt::run(|ui: &mut slt::Context| {
1135    /// use slt::Border;
1136    /// ui.container()
1137    ///     .border(Border::Rounded)
1138    ///     .pad(1)
1139    ///     .title("My Panel")
1140    ///     .col(|ui| {
1141    ///         ui.text("content");
1142    ///     });
1143    /// # });
1144    /// ```
1145    pub fn container(&mut self) -> ContainerBuilder<'_> {
1146        let border = self.theme.border;
1147        ContainerBuilder {
1148            ctx: self,
1149            gap: 0,
1150            align: Align::Start,
1151            justify: Justify::Start,
1152            border: None,
1153            border_sides: BorderSides::all(),
1154            border_style: Style::new().fg(border),
1155            bg: None,
1156            dark_bg: None,
1157            dark_border_style: None,
1158            group_hover_bg: None,
1159            group_hover_border_style: None,
1160            group_name: None,
1161            padding: Padding::default(),
1162            margin: Margin::default(),
1163            constraints: Constraints::default(),
1164            title: None,
1165            grow: 0,
1166            scroll_offset: None,
1167        }
1168    }
1169
1170    /// Create a scrollable container. Handles wheel scroll and drag-to-scroll automatically.
1171    ///
1172    /// Pass a [`ScrollState`] to persist scroll position across frames. The state
1173    /// is updated in-place with the current scroll offset and bounds.
1174    ///
1175    /// # Example
1176    ///
1177    /// ```no_run
1178    /// # use slt::widgets::ScrollState;
1179    /// # slt::run(|ui: &mut slt::Context| {
1180    /// let mut scroll = ScrollState::new();
1181    /// ui.scrollable(&mut scroll).col(|ui| {
1182    ///     for i in 0..100 {
1183    ///         ui.text(format!("Line {i}"));
1184    ///     }
1185    /// });
1186    /// # });
1187    /// ```
1188    pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
1189        let index = self.scroll_count;
1190        self.scroll_count += 1;
1191        if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
1192            state.set_bounds(ch, vh);
1193            let max = ch.saturating_sub(vh) as usize;
1194            state.offset = state.offset.min(max);
1195        }
1196
1197        let next_id = self.interaction_count;
1198        if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
1199            let inner_rects: Vec<Rect> = self
1200                .prev_scroll_rects
1201                .iter()
1202                .enumerate()
1203                .filter(|&(j, sr)| {
1204                    j != index
1205                        && sr.width > 0
1206                        && sr.height > 0
1207                        && sr.x >= rect.x
1208                        && sr.right() <= rect.right()
1209                        && sr.y >= rect.y
1210                        && sr.bottom() <= rect.bottom()
1211                })
1212                .map(|(_, sr)| *sr)
1213                .collect();
1214            self.auto_scroll_nested(&rect, state, &inner_rects);
1215        }
1216
1217        self.container().scroll_offset(state.offset as u32)
1218    }
1219
1220    /// Render a scrollbar track for a [`ScrollState`].
1221    ///
1222    /// Displays a track (`│`) with a proportional thumb (`█`). The thumb size
1223    /// and position are calculated from the scroll state's content height,
1224    /// viewport height, and current offset.
1225    ///
1226    /// Typically placed beside a `scrollable()` container in a `row()`:
1227    /// ```no_run
1228    /// # use slt::widgets::ScrollState;
1229    /// # slt::run(|ui: &mut slt::Context| {
1230    /// let mut scroll = ScrollState::new();
1231    /// ui.row(|ui| {
1232    ///     ui.scrollable(&mut scroll).grow(1).col(|ui| {
1233    ///         for i in 0..100 { ui.text(format!("Line {i}")); }
1234    ///     });
1235    ///     ui.scrollbar(&scroll);
1236    /// });
1237    /// # });
1238    /// ```
1239    pub fn scrollbar(&mut self, state: &ScrollState) {
1240        let vh = state.viewport_height();
1241        let ch = state.content_height();
1242        if vh == 0 || ch <= vh {
1243            return;
1244        }
1245
1246        let track_height = vh;
1247        let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1248        let max_offset = ch.saturating_sub(vh);
1249        let thumb_pos = if max_offset == 0 {
1250            0
1251        } else {
1252            ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1253                .round() as u32
1254        };
1255
1256        let theme = self.theme;
1257        let track_char = '│';
1258        let thumb_char = '█';
1259
1260        self.container().w(1).h(track_height).col(|ui| {
1261            for i in 0..track_height {
1262                if i >= thumb_pos && i < thumb_pos + thumb_height {
1263                    ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1264                } else {
1265                    ui.styled(
1266                        track_char.to_string(),
1267                        Style::new().fg(theme.text_dim).dim(),
1268                    );
1269                }
1270            }
1271        });
1272    }
1273
1274    fn auto_scroll_nested(
1275        &mut self,
1276        rect: &Rect,
1277        state: &mut ScrollState,
1278        inner_scroll_rects: &[Rect],
1279    ) {
1280        let mut to_consume: Vec<usize> = Vec::new();
1281
1282        for (i, event) in self.events.iter().enumerate() {
1283            if self.consumed[i] {
1284                continue;
1285            }
1286            if let Event::Mouse(mouse) = event {
1287                let in_bounds = mouse.x >= rect.x
1288                    && mouse.x < rect.right()
1289                    && mouse.y >= rect.y
1290                    && mouse.y < rect.bottom();
1291                if !in_bounds {
1292                    continue;
1293                }
1294                let in_inner = inner_scroll_rects.iter().any(|sr| {
1295                    mouse.x >= sr.x
1296                        && mouse.x < sr.right()
1297                        && mouse.y >= sr.y
1298                        && mouse.y < sr.bottom()
1299                });
1300                if in_inner {
1301                    continue;
1302                }
1303                match mouse.kind {
1304                    MouseKind::ScrollUp => {
1305                        state.scroll_up(1);
1306                        to_consume.push(i);
1307                    }
1308                    MouseKind::ScrollDown => {
1309                        state.scroll_down(1);
1310                        to_consume.push(i);
1311                    }
1312                    MouseKind::Drag(MouseButton::Left) => {}
1313                    _ => {}
1314                }
1315            }
1316        }
1317
1318        for i in to_consume {
1319            self.consumed[i] = true;
1320        }
1321    }
1322
1323    /// Shortcut for `container().border(border)`.
1324    ///
1325    /// Returns a [`ContainerBuilder`] pre-configured with the given border style.
1326    pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1327        self.container()
1328            .border(border)
1329            .border_sides(BorderSides::all())
1330    }
1331
1332    fn push_container(
1333        &mut self,
1334        direction: Direction,
1335        gap: u32,
1336        f: impl FnOnce(&mut Context),
1337    ) -> Response {
1338        let interaction_id = self.interaction_count;
1339        self.interaction_count += 1;
1340        let border = self.theme.border;
1341
1342        self.commands.push(Command::BeginContainer {
1343            direction,
1344            gap,
1345            align: Align::Start,
1346            justify: Justify::Start,
1347            border: None,
1348            border_sides: BorderSides::all(),
1349            border_style: Style::new().fg(border),
1350            bg_color: None,
1351            padding: Padding::default(),
1352            margin: Margin::default(),
1353            constraints: Constraints::default(),
1354            title: None,
1355            grow: 0,
1356            group_name: None,
1357        });
1358        f(self);
1359        self.commands.push(Command::EndContainer);
1360        self.last_text_idx = None;
1361
1362        self.response_for(interaction_id)
1363    }
1364
1365    pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1366        if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1367            return Response::none();
1368        }
1369        if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1370            let clicked = self
1371                .click_pos
1372                .map(|(mx, my)| {
1373                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1374                })
1375                .unwrap_or(false);
1376            let hovered = self
1377                .mouse_pos
1378                .map(|(mx, my)| {
1379                    mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1380                })
1381                .unwrap_or(false);
1382            Response {
1383                clicked,
1384                hovered,
1385                changed: false,
1386                focused: false,
1387                rect: *rect,
1388            }
1389        } else {
1390            Response::none()
1391        }
1392    }
1393
1394    /// Returns true if the named group is currently hovered by the mouse.
1395    pub fn is_group_hovered(&self, name: &str) -> bool {
1396        if let Some(pos) = self.mouse_pos {
1397            self.prev_group_rects.iter().any(|(n, rect)| {
1398                n == name
1399                    && pos.0 >= rect.x
1400                    && pos.0 < rect.x + rect.width
1401                    && pos.1 >= rect.y
1402                    && pos.1 < rect.y + rect.height
1403            })
1404        } else {
1405            false
1406        }
1407    }
1408
1409    /// Returns true if the named group contains the currently focused widget.
1410    pub fn is_group_focused(&self, name: &str) -> bool {
1411        if self.prev_focus_count == 0 {
1412            return false;
1413        }
1414        let focused_index = self.focus_index % self.prev_focus_count;
1415        self.prev_focus_groups
1416            .get(focused_index)
1417            .and_then(|group| group.as_deref())
1418            .map(|group| group == name)
1419            .unwrap_or(false)
1420    }
1421
1422    /// Set the flex-grow factor of the last rendered text element.
1423    ///
1424    /// A value of `1` causes the element to expand and fill remaining space
1425    /// along the main axis.
1426    pub fn grow(&mut self, value: u16) -> &mut Self {
1427        if let Some(idx) = self.last_text_idx {
1428            if let Command::Text { grow, .. } = &mut self.commands[idx] {
1429                *grow = value;
1430            }
1431        }
1432        self
1433    }
1434
1435    /// Set the text alignment of the last rendered text element.
1436    pub fn align(&mut self, align: Align) -> &mut Self {
1437        if let Some(idx) = self.last_text_idx {
1438            if let Command::Text {
1439                align: text_align, ..
1440            } = &mut self.commands[idx]
1441            {
1442                *text_align = align;
1443            }
1444        }
1445        self
1446    }
1447
1448    /// Render an invisible spacer that expands to fill available space.
1449    ///
1450    /// Useful for pushing siblings to opposite ends of a row or column.
1451    pub fn spacer(&mut self) -> &mut Self {
1452        self.commands.push(Command::Spacer { grow: 1 });
1453        self.last_text_idx = None;
1454        self
1455    }
1456
1457    /// Render a form that groups input fields vertically.
1458    ///
1459    /// Use [`Context::form_field`] inside the closure to render each field.
1460    pub fn form(
1461        &mut self,
1462        state: &mut FormState,
1463        f: impl FnOnce(&mut Context, &mut FormState),
1464    ) -> &mut Self {
1465        self.col(|ui| {
1466            f(ui, state);
1467        });
1468        self
1469    }
1470
1471    /// Render a single form field with label and input.
1472    ///
1473    /// Shows a validation error below the input when present.
1474    pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1475        self.col(|ui| {
1476            ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1477            ui.text_input(&mut field.input);
1478            if let Some(error) = field.error.as_deref() {
1479                ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1480            }
1481        });
1482        self
1483    }
1484
1485    /// Render a submit button.
1486    ///
1487    /// Returns `true` when the button is clicked or activated.
1488    pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1489        self.button(label)
1490    }
1491}
1492
1493const KEYWORDS: &[&str] = &[
1494    "fn",
1495    "let",
1496    "mut",
1497    "pub",
1498    "use",
1499    "impl",
1500    "struct",
1501    "enum",
1502    "trait",
1503    "type",
1504    "const",
1505    "static",
1506    "if",
1507    "else",
1508    "match",
1509    "for",
1510    "while",
1511    "loop",
1512    "return",
1513    "break",
1514    "continue",
1515    "where",
1516    "self",
1517    "super",
1518    "crate",
1519    "mod",
1520    "async",
1521    "await",
1522    "move",
1523    "ref",
1524    "in",
1525    "as",
1526    "true",
1527    "false",
1528    "Some",
1529    "None",
1530    "Ok",
1531    "Err",
1532    "Self",
1533    "def",
1534    "class",
1535    "import",
1536    "from",
1537    "pass",
1538    "lambda",
1539    "yield",
1540    "with",
1541    "try",
1542    "except",
1543    "raise",
1544    "finally",
1545    "elif",
1546    "del",
1547    "global",
1548    "nonlocal",
1549    "assert",
1550    "is",
1551    "not",
1552    "and",
1553    "or",
1554    "function",
1555    "var",
1556    "const",
1557    "export",
1558    "default",
1559    "switch",
1560    "case",
1561    "throw",
1562    "catch",
1563    "typeof",
1564    "instanceof",
1565    "new",
1566    "delete",
1567    "void",
1568    "this",
1569    "null",
1570    "undefined",
1571    "func",
1572    "package",
1573    "defer",
1574    "go",
1575    "chan",
1576    "select",
1577    "range",
1578    "map",
1579    "interface",
1580    "fallthrough",
1581    "nil",
1582];
1583
1584fn render_highlighted_line(ui: &mut Context, line: &str) {
1585    let theme = ui.theme;
1586    let is_light = matches!(
1587        theme.bg,
1588        Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1589    );
1590    let keyword_color = if is_light {
1591        Color::Rgb(166, 38, 164)
1592    } else {
1593        Color::Rgb(198, 120, 221)
1594    };
1595    let string_color = if is_light {
1596        Color::Rgb(80, 161, 79)
1597    } else {
1598        Color::Rgb(152, 195, 121)
1599    };
1600    let comment_color = theme.text_dim;
1601    let number_color = if is_light {
1602        Color::Rgb(152, 104, 1)
1603    } else {
1604        Color::Rgb(209, 154, 102)
1605    };
1606    let fn_color = if is_light {
1607        Color::Rgb(64, 120, 242)
1608    } else {
1609        Color::Rgb(97, 175, 239)
1610    };
1611    let macro_color = if is_light {
1612        Color::Rgb(1, 132, 188)
1613    } else {
1614        Color::Rgb(86, 182, 194)
1615    };
1616
1617    let trimmed = line.trim_start();
1618    let indent = &line[..line.len() - trimmed.len()];
1619    if !indent.is_empty() {
1620        ui.text(indent);
1621    }
1622
1623    if trimmed.starts_with("//") {
1624        ui.text(trimmed).fg(comment_color).italic();
1625        return;
1626    }
1627
1628    let mut pos = 0;
1629
1630    while pos < trimmed.len() {
1631        let ch = trimmed.as_bytes()[pos];
1632
1633        if ch == b'"' {
1634            if let Some(end) = trimmed[pos + 1..].find('"') {
1635                let s = &trimmed[pos..pos + end + 2];
1636                ui.text(s).fg(string_color);
1637                pos += end + 2;
1638                continue;
1639            }
1640        }
1641
1642        if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1643        {
1644            let end = trimmed[pos..]
1645                .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1646                .map_or(trimmed.len(), |e| pos + e);
1647            ui.text(&trimmed[pos..end]).fg(number_color);
1648            pos = end;
1649            continue;
1650        }
1651
1652        if ch.is_ascii_alphabetic() || ch == b'_' {
1653            let end = trimmed[pos..]
1654                .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1655                .map_or(trimmed.len(), |e| pos + e);
1656            let word = &trimmed[pos..end];
1657
1658            if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1659                ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1660                pos = end + 1;
1661            } else if end < trimmed.len()
1662                && trimmed.as_bytes()[end] == b'('
1663                && !KEYWORDS.contains(&word)
1664            {
1665                ui.text(word).fg(fn_color);
1666                pos = end;
1667            } else if KEYWORDS.contains(&word) {
1668                ui.text(word).fg(keyword_color);
1669                pos = end;
1670            } else {
1671                ui.text(word);
1672                pos = end;
1673            }
1674            continue;
1675        }
1676
1677        let end = trimmed[pos..]
1678            .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1679            .map_or(trimmed.len(), |e| pos + e);
1680        ui.text(&trimmed[pos..end]);
1681        pos = end;
1682    }
1683}