1use super::*;
2
3impl Context {
4 pub fn text(&mut self, s: impl Into<String>) -> &mut Self {
17 let content = s.into();
18 self.commands.push(Command::Text {
19 content,
20 style: Style::new().fg(self.theme.text),
21 grow: 0,
22 align: Align::Start,
23 wrap: false,
24 margin: Margin::default(),
25 constraints: Constraints::default(),
26 });
27 self.last_text_idx = Some(self.commands.len() - 1);
28 self
29 }
30
31 pub fn link(&mut self, text: impl Into<String>, url: impl Into<String>) -> &mut Self {
37 let url_str = url.into();
38 let focused = self.register_focusable();
39 let interaction_id = self.interaction_count;
40 self.interaction_count += 1;
41 let response = self.response_for(interaction_id);
42
43 let mut activated = response.clicked;
44 if focused {
45 for (i, event) in self.events.iter().enumerate() {
46 if let Event::Key(key) = event {
47 if key.kind != KeyEventKind::Press {
48 continue;
49 }
50 if matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) {
51 activated = true;
52 self.consumed[i] = true;
53 }
54 }
55 }
56 }
57
58 if activated {
59 let _ = open_url(&url_str);
60 }
61
62 let style = if focused {
63 Style::new()
64 .fg(self.theme.primary)
65 .bg(self.theme.surface_hover)
66 .underline()
67 .bold()
68 } else if response.hovered {
69 Style::new()
70 .fg(self.theme.accent)
71 .bg(self.theme.surface_hover)
72 .underline()
73 } else {
74 Style::new().fg(self.theme.primary).underline()
75 };
76
77 self.commands.push(Command::Link {
78 text: text.into(),
79 url: url_str,
80 style,
81 margin: Margin::default(),
82 constraints: Constraints::default(),
83 });
84 self.last_text_idx = Some(self.commands.len() - 1);
85 self
86 }
87
88 pub fn text_wrap(&mut self, s: impl Into<String>) -> &mut Self {
93 let content = s.into();
94 self.commands.push(Command::Text {
95 content,
96 style: Style::new().fg(self.theme.text),
97 grow: 0,
98 align: Align::Start,
99 wrap: true,
100 margin: Margin::default(),
101 constraints: Constraints::default(),
102 });
103 self.last_text_idx = Some(self.commands.len() - 1);
104 self
105 }
106
107 pub fn bold(&mut self) -> &mut Self {
111 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
112 self
113 }
114
115 pub fn dim(&mut self) -> &mut Self {
120 let text_dim = self.theme.text_dim;
121 self.modify_last_style(|s| {
122 s.modifiers |= Modifiers::DIM;
123 if s.fg.is_none() {
124 s.fg = Some(text_dim);
125 }
126 });
127 self
128 }
129
130 pub fn italic(&mut self) -> &mut Self {
132 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
133 self
134 }
135
136 pub fn underline(&mut self) -> &mut Self {
138 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
139 self
140 }
141
142 pub fn reversed(&mut self) -> &mut Self {
144 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
145 self
146 }
147
148 pub fn strikethrough(&mut self) -> &mut Self {
150 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
151 self
152 }
153
154 pub fn fg(&mut self, color: Color) -> &mut Self {
156 self.modify_last_style(|s| s.fg = Some(color));
157 self
158 }
159
160 pub fn bg(&mut self, color: Color) -> &mut Self {
162 self.modify_last_style(|s| s.bg = Some(color));
163 self
164 }
165
166 pub fn group_hover_fg(&mut self, color: Color) -> &mut Self {
167 let apply_group_style = self
168 .group_stack
169 .last()
170 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
171 .unwrap_or(false);
172 if apply_group_style {
173 self.modify_last_style(|s| s.fg = Some(color));
174 }
175 self
176 }
177
178 pub fn group_hover_bg(&mut self, color: Color) -> &mut Self {
179 let apply_group_style = self
180 .group_stack
181 .last()
182 .map(|name| self.is_group_hovered(name) || self.is_group_focused(name))
183 .unwrap_or(false);
184 if apply_group_style {
185 self.modify_last_style(|s| s.bg = Some(color));
186 }
187 self
188 }
189
190 pub fn styled(&mut self, s: impl Into<String>, style: Style) -> &mut Self {
195 self.commands.push(Command::Text {
196 content: s.into(),
197 style,
198 grow: 0,
199 align: Align::Start,
200 wrap: false,
201 margin: Margin::default(),
202 constraints: Constraints::default(),
203 });
204 self.last_text_idx = Some(self.commands.len() - 1);
205 self
206 }
207
208 pub fn image(&mut self, img: &HalfBlockImage) {
230 let width = img.width;
231 let height = img.height;
232
233 self.container().w(width).h(height).gap(0).col(|ui| {
234 for row in 0..height {
235 ui.container().gap(0).row(|ui| {
236 for col in 0..width {
237 let idx = (row * width + col) as usize;
238 if let Some(&(upper, lower)) = img.pixels.get(idx) {
239 ui.styled("▀", Style::new().fg(upper).bg(lower));
240 }
241 }
242 });
243 }
244 });
245 }
246
247 pub fn streaming_text(&mut self, state: &mut StreamingTextState) {
263 if state.streaming {
264 state.cursor_tick = state.cursor_tick.wrapping_add(1);
265 state.cursor_visible = (state.cursor_tick / 8) % 2 == 0;
266 }
267
268 if state.content.is_empty() && state.streaming {
269 let cursor = if state.cursor_visible { "▌" } else { " " };
270 let primary = self.theme.primary;
271 self.text(cursor).fg(primary);
272 return;
273 }
274
275 if !state.content.is_empty() {
276 if state.streaming && state.cursor_visible {
277 self.text_wrap(format!("{}▌", state.content));
278 } else {
279 self.text_wrap(&state.content);
280 }
281 }
282 }
283
284 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) {
299 let theme = self.theme;
300 self.bordered(Border::Rounded).col(|ui| {
301 ui.row(|ui| {
302 ui.text("⚡").fg(theme.warning);
303 ui.text(&state.tool_name).bold().fg(theme.primary);
304 });
305 ui.text(&state.description).dim();
306
307 if state.action == ApprovalAction::Pending {
308 ui.row(|ui| {
309 if ui.button("✓ Approve") {
310 state.action = ApprovalAction::Approved;
311 }
312 if ui.button("✗ Reject") {
313 state.action = ApprovalAction::Rejected;
314 }
315 });
316 } else {
317 let (label, color) = match state.action {
318 ApprovalAction::Approved => ("✓ Approved", theme.success),
319 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
320 ApprovalAction::Pending => unreachable!(),
321 };
322 ui.text(label).fg(color).bold();
323 }
324 });
325 }
326
327 pub fn context_bar(&mut self, items: &[ContextItem]) {
340 if items.is_empty() {
341 return;
342 }
343
344 let theme = self.theme;
345 let total: usize = items.iter().map(|item| item.tokens).sum();
346
347 self.container().row(|ui| {
348 ui.text("📎").dim();
349 for item in items {
350 ui.text(format!(
351 "{} ({})",
352 item.label,
353 format_token_count(item.tokens)
354 ))
355 .fg(theme.secondary);
356 }
357 ui.spacer();
358 ui.text(format!("Σ {}", format_token_count(total))).dim();
359 });
360 }
361
362 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> bool {
363 use crate::widgets::AlertLevel;
364
365 let theme = self.theme;
366 let (icon, color) = match level {
367 AlertLevel::Info => ("ℹ", theme.accent),
368 AlertLevel::Success => ("✓", theme.success),
369 AlertLevel::Warning => ("⚠", theme.warning),
370 AlertLevel::Error => ("✕", theme.error),
371 };
372
373 let focused = self.register_focusable();
374 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
375
376 let resp = self.container().col(|ui| {
377 ui.line(|ui| {
378 ui.text(format!(" {icon} ")).fg(color).bold();
379 ui.text(message).grow(1);
380 ui.text(" [×] ").dim();
381 });
382 });
383
384 key_dismiss || resp.clicked
385 }
386
387 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
388 self.breadcrumb_with(segments, " › ")
389 }
390
391 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
392 let theme = self.theme;
393 let last_idx = segments.len().saturating_sub(1);
394 let mut clicked_idx: Option<usize> = None;
395
396 self.row(|ui| {
397 for (i, segment) in segments.iter().enumerate() {
398 let is_last = i == last_idx;
399 if is_last {
400 ui.text(*segment).bold();
401 } else {
402 let focused = ui.register_focusable();
403 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
404 let resp = ui.interaction();
405 let color = if resp.hovered || focused {
406 theme.accent
407 } else {
408 theme.primary
409 };
410 ui.text(*segment).fg(color).underline();
411 if resp.clicked || pressed {
412 clicked_idx = Some(i);
413 }
414 ui.text(separator).dim();
415 }
416 }
417 });
418
419 clicked_idx
420 }
421
422 pub fn accordion(&mut self, title: &str, open: &mut bool, f: impl FnOnce(&mut Context)) {
423 let theme = self.theme;
424 let focused = self.register_focusable();
425
426 if focused && self.key_code(KeyCode::Enter) {
427 *open = !*open;
428 }
429
430 let icon = if *open { "▾" } else { "▸" };
431 let title_color = if focused { theme.primary } else { theme.text };
432
433 let resp = self.container().col(|ui| {
434 ui.line(|ui| {
435 ui.text(icon).fg(title_color);
436 ui.text(format!(" {title}")).bold().fg(title_color);
437 });
438 });
439
440 if resp.clicked {
441 *open = !*open;
442 }
443
444 if *open {
445 self.container().pl(2).col(f);
446 }
447 }
448
449 pub fn definition_list(&mut self, items: &[(&str, &str)]) {
450 let max_key_width = items
451 .iter()
452 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
453 .max()
454 .unwrap_or(0);
455
456 self.col(|ui| {
457 for (key, value) in items {
458 ui.line(|ui| {
459 let padded = format!("{:>width$}", key, width = max_key_width);
460 ui.text(padded).dim();
461 ui.text(" ");
462 ui.text(*value);
463 });
464 }
465 });
466 }
467
468 pub fn divider_text(&mut self, label: &str) {
469 let w = self.width();
470 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
471 let pad = 1u32;
472 let left_len = 4u32;
473 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
474 let left: String = "─".repeat(left_len as usize);
475 let right: String = "─".repeat(right_len as usize);
476 let theme = self.theme;
477 self.line(|ui| {
478 ui.text(&left).fg(theme.border);
479 ui.text(format!(" {} ", label)).fg(theme.text);
480 ui.text(&right).fg(theme.border);
481 });
482 }
483
484 pub fn badge(&mut self, label: &str) {
485 let theme = self.theme;
486 self.badge_colored(label, theme.primary);
487 }
488
489 pub fn badge_colored(&mut self, label: &str, color: Color) {
490 let fg = Color::contrast_fg(color);
491 self.text(format!(" {} ", label)).fg(fg).bg(color);
492 }
493
494 pub fn key_hint(&mut self, key: &str) {
495 let theme = self.theme;
496 self.text(format!(" {} ", key))
497 .reversed()
498 .fg(theme.text_dim);
499 }
500
501 pub fn stat(&mut self, label: &str, value: &str) {
502 self.col(|ui| {
503 ui.text(label).dim();
504 ui.text(value).bold();
505 });
506 }
507
508 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) {
509 self.col(|ui| {
510 ui.text(label).dim();
511 ui.text(value).bold().fg(color);
512 });
513 }
514
515 pub fn stat_trend(&mut self, label: &str, value: &str, trend: crate::widgets::Trend) {
516 let theme = self.theme;
517 let (arrow, color) = match trend {
518 crate::widgets::Trend::Up => ("↑", theme.success),
519 crate::widgets::Trend::Down => ("↓", theme.error),
520 };
521 self.col(|ui| {
522 ui.text(label).dim();
523 ui.line(|ui| {
524 ui.text(value).bold();
525 ui.text(format!(" {arrow}")).fg(color);
526 });
527 });
528 }
529
530 pub fn empty_state(&mut self, title: &str, description: &str) {
531 self.container().center().col(|ui| {
532 ui.text(title).align(Align::Center);
533 ui.text(description).dim().align(Align::Center);
534 });
535 }
536
537 pub fn empty_state_action(
538 &mut self,
539 title: &str,
540 description: &str,
541 action_label: &str,
542 ) -> bool {
543 let mut clicked = false;
544 self.container().center().col(|ui| {
545 ui.text(title).align(Align::Center);
546 ui.text(description).dim().align(Align::Center);
547 if ui.button(action_label) {
548 clicked = true;
549 }
550 });
551 clicked
552 }
553
554 pub fn code_block(&mut self, code: &str) {
555 let theme = self.theme;
556 self.bordered(Border::Rounded)
557 .bg(theme.surface)
558 .pad(1)
559 .col(|ui| {
560 for line in code.lines() {
561 render_highlighted_line(ui, line);
562 }
563 });
564 }
565
566 pub fn code_block_numbered(&mut self, code: &str) {
567 let lines: Vec<&str> = code.lines().collect();
568 let gutter_w = format!("{}", lines.len()).len();
569 let theme = self.theme;
570 self.bordered(Border::Rounded)
571 .bg(theme.surface)
572 .pad(1)
573 .col(|ui| {
574 for (i, line) in lines.iter().enumerate() {
575 ui.line(|ui| {
576 ui.text(format!("{:>gutter_w$} │ ", i + 1))
577 .fg(theme.text_dim);
578 render_highlighted_line(ui, line);
579 });
580 }
581 });
582 }
583
584 pub fn wrap(&mut self) -> &mut Self {
586 if let Some(idx) = self.last_text_idx {
587 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
588 *wrap = true;
589 }
590 }
591 self
592 }
593
594 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
595 if let Some(idx) = self.last_text_idx {
596 match &mut self.commands[idx] {
597 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
598 _ => {}
599 }
600 }
601 }
602
603 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
621 self.push_container(Direction::Column, 0, f)
622 }
623
624 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
628 self.push_container(Direction::Column, gap, f)
629 }
630
631 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
648 self.push_container(Direction::Row, 0, f)
649 }
650
651 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
655 self.push_container(Direction::Row, gap, f)
656 }
657
658 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
675 let _ = self.push_container(Direction::Row, 0, f);
676 self
677 }
678
679 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
698 let start = self.commands.len();
699 f(self);
700 let mut segments: Vec<(String, Style)> = Vec::new();
701 for cmd in self.commands.drain(start..) {
702 if let Command::Text { content, style, .. } = cmd {
703 segments.push((content, style));
704 }
705 }
706 self.commands.push(Command::RichText {
707 segments,
708 wrap: true,
709 align: Align::Start,
710 margin: Margin::default(),
711 constraints: Constraints::default(),
712 });
713 self.last_text_idx = None;
714 self
715 }
716
717 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
726 self.commands.push(Command::BeginOverlay { modal: true });
727 self.overlay_depth += 1;
728 self.modal_active = true;
729 f(self);
730 self.overlay_depth = self.overlay_depth.saturating_sub(1);
731 self.commands.push(Command::EndOverlay);
732 self.last_text_idx = None;
733 }
734
735 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
737 self.commands.push(Command::BeginOverlay { modal: false });
738 self.overlay_depth += 1;
739 f(self);
740 self.overlay_depth = self.overlay_depth.saturating_sub(1);
741 self.commands.push(Command::EndOverlay);
742 self.last_text_idx = None;
743 }
744
745 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
753 self.group_count = self.group_count.saturating_add(1);
754 self.group_stack.push(name.to_string());
755 self.container().group_name(name.to_string())
756 }
757
758 pub fn container(&mut self) -> ContainerBuilder<'_> {
779 let border = self.theme.border;
780 ContainerBuilder {
781 ctx: self,
782 gap: 0,
783 align: Align::Start,
784 justify: Justify::Start,
785 border: None,
786 border_sides: BorderSides::all(),
787 border_style: Style::new().fg(border),
788 bg: None,
789 dark_bg: None,
790 dark_border_style: None,
791 group_hover_bg: None,
792 group_hover_border_style: None,
793 group_name: None,
794 padding: Padding::default(),
795 margin: Margin::default(),
796 constraints: Constraints::default(),
797 title: None,
798 grow: 0,
799 scroll_offset: None,
800 }
801 }
802
803 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
822 let index = self.scroll_count;
823 self.scroll_count += 1;
824 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
825 state.set_bounds(ch, vh);
826 let max = ch.saturating_sub(vh) as usize;
827 state.offset = state.offset.min(max);
828 }
829
830 let next_id = self.interaction_count;
831 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
832 let inner_rects: Vec<Rect> = self
833 .prev_scroll_rects
834 .iter()
835 .enumerate()
836 .filter(|&(j, sr)| {
837 j != index
838 && sr.width > 0
839 && sr.height > 0
840 && sr.x >= rect.x
841 && sr.right() <= rect.right()
842 && sr.y >= rect.y
843 && sr.bottom() <= rect.bottom()
844 })
845 .map(|(_, sr)| *sr)
846 .collect();
847 self.auto_scroll_nested(&rect, state, &inner_rects);
848 }
849
850 self.container().scroll_offset(state.offset as u32)
851 }
852
853 pub fn scrollbar(&mut self, state: &ScrollState) {
873 let vh = state.viewport_height();
874 let ch = state.content_height();
875 if vh == 0 || ch <= vh {
876 return;
877 }
878
879 let track_height = vh;
880 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
881 let max_offset = ch.saturating_sub(vh);
882 let thumb_pos = if max_offset == 0 {
883 0
884 } else {
885 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
886 .round() as u32
887 };
888
889 let theme = self.theme;
890 let track_char = '│';
891 let thumb_char = '█';
892
893 self.container().w(1).h(track_height).col(|ui| {
894 for i in 0..track_height {
895 if i >= thumb_pos && i < thumb_pos + thumb_height {
896 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
897 } else {
898 ui.styled(
899 track_char.to_string(),
900 Style::new().fg(theme.text_dim).dim(),
901 );
902 }
903 }
904 });
905 }
906
907 fn auto_scroll_nested(
908 &mut self,
909 rect: &Rect,
910 state: &mut ScrollState,
911 inner_scroll_rects: &[Rect],
912 ) {
913 let mut to_consume: Vec<usize> = Vec::new();
914
915 for (i, event) in self.events.iter().enumerate() {
916 if self.consumed[i] {
917 continue;
918 }
919 if let Event::Mouse(mouse) = event {
920 let in_bounds = mouse.x >= rect.x
921 && mouse.x < rect.right()
922 && mouse.y >= rect.y
923 && mouse.y < rect.bottom();
924 if !in_bounds {
925 continue;
926 }
927 let in_inner = inner_scroll_rects.iter().any(|sr| {
928 mouse.x >= sr.x
929 && mouse.x < sr.right()
930 && mouse.y >= sr.y
931 && mouse.y < sr.bottom()
932 });
933 if in_inner {
934 continue;
935 }
936 match mouse.kind {
937 MouseKind::ScrollUp => {
938 state.scroll_up(1);
939 to_consume.push(i);
940 }
941 MouseKind::ScrollDown => {
942 state.scroll_down(1);
943 to_consume.push(i);
944 }
945 MouseKind::Drag(MouseButton::Left) => {}
946 _ => {}
947 }
948 }
949 }
950
951 for i in to_consume {
952 self.consumed[i] = true;
953 }
954 }
955
956 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
960 self.container()
961 .border(border)
962 .border_sides(BorderSides::all())
963 }
964
965 fn push_container(
966 &mut self,
967 direction: Direction,
968 gap: u32,
969 f: impl FnOnce(&mut Context),
970 ) -> Response {
971 let interaction_id = self.interaction_count;
972 self.interaction_count += 1;
973 let border = self.theme.border;
974
975 self.commands.push(Command::BeginContainer {
976 direction,
977 gap,
978 align: Align::Start,
979 justify: Justify::Start,
980 border: None,
981 border_sides: BorderSides::all(),
982 border_style: Style::new().fg(border),
983 bg_color: None,
984 padding: Padding::default(),
985 margin: Margin::default(),
986 constraints: Constraints::default(),
987 title: None,
988 grow: 0,
989 group_name: None,
990 });
991 f(self);
992 self.commands.push(Command::EndContainer);
993 self.last_text_idx = None;
994
995 self.response_for(interaction_id)
996 }
997
998 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
999 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1000 return Response::default();
1001 }
1002 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1003 let clicked = self
1004 .click_pos
1005 .map(|(mx, my)| {
1006 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1007 })
1008 .unwrap_or(false);
1009 let hovered = self
1010 .mouse_pos
1011 .map(|(mx, my)| {
1012 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1013 })
1014 .unwrap_or(false);
1015 Response { clicked, hovered }
1016 } else {
1017 Response::default()
1018 }
1019 }
1020
1021 pub fn is_group_hovered(&self, name: &str) -> bool {
1023 if let Some(pos) = self.mouse_pos {
1024 self.prev_group_rects.iter().any(|(n, rect)| {
1025 n == name
1026 && pos.0 >= rect.x
1027 && pos.0 < rect.x + rect.width
1028 && pos.1 >= rect.y
1029 && pos.1 < rect.y + rect.height
1030 })
1031 } else {
1032 false
1033 }
1034 }
1035
1036 pub fn is_group_focused(&self, name: &str) -> bool {
1038 if self.prev_focus_count == 0 {
1039 return false;
1040 }
1041 let focused_index = self.focus_index % self.prev_focus_count;
1042 self.prev_focus_groups
1043 .get(focused_index)
1044 .and_then(|group| group.as_deref())
1045 .map(|group| group == name)
1046 .unwrap_or(false)
1047 }
1048
1049 pub fn grow(&mut self, value: u16) -> &mut Self {
1054 if let Some(idx) = self.last_text_idx {
1055 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1056 *grow = value;
1057 }
1058 }
1059 self
1060 }
1061
1062 pub fn align(&mut self, align: Align) -> &mut Self {
1064 if let Some(idx) = self.last_text_idx {
1065 if let Command::Text {
1066 align: text_align, ..
1067 } = &mut self.commands[idx]
1068 {
1069 *text_align = align;
1070 }
1071 }
1072 self
1073 }
1074
1075 pub fn spacer(&mut self) -> &mut Self {
1079 self.commands.push(Command::Spacer { grow: 1 });
1080 self.last_text_idx = None;
1081 self
1082 }
1083
1084 pub fn form(
1088 &mut self,
1089 state: &mut FormState,
1090 f: impl FnOnce(&mut Context, &mut FormState),
1091 ) -> &mut Self {
1092 self.col(|ui| {
1093 f(ui, state);
1094 });
1095 self
1096 }
1097
1098 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1102 self.col(|ui| {
1103 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1104 ui.text_input(&mut field.input);
1105 if let Some(error) = field.error.as_deref() {
1106 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1107 }
1108 });
1109 self
1110 }
1111
1112 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
1116 self.button(label)
1117 }
1118}
1119
1120const KEYWORDS: &[&str] = &[
1121 "fn",
1122 "let",
1123 "mut",
1124 "pub",
1125 "use",
1126 "impl",
1127 "struct",
1128 "enum",
1129 "trait",
1130 "type",
1131 "const",
1132 "static",
1133 "if",
1134 "else",
1135 "match",
1136 "for",
1137 "while",
1138 "loop",
1139 "return",
1140 "break",
1141 "continue",
1142 "where",
1143 "self",
1144 "super",
1145 "crate",
1146 "mod",
1147 "async",
1148 "await",
1149 "move",
1150 "ref",
1151 "in",
1152 "as",
1153 "true",
1154 "false",
1155 "Some",
1156 "None",
1157 "Ok",
1158 "Err",
1159 "Self",
1160 "def",
1161 "class",
1162 "import",
1163 "from",
1164 "pass",
1165 "lambda",
1166 "yield",
1167 "with",
1168 "try",
1169 "except",
1170 "raise",
1171 "finally",
1172 "elif",
1173 "del",
1174 "global",
1175 "nonlocal",
1176 "assert",
1177 "is",
1178 "not",
1179 "and",
1180 "or",
1181 "function",
1182 "var",
1183 "const",
1184 "export",
1185 "default",
1186 "switch",
1187 "case",
1188 "throw",
1189 "catch",
1190 "typeof",
1191 "instanceof",
1192 "new",
1193 "delete",
1194 "void",
1195 "this",
1196 "null",
1197 "undefined",
1198 "func",
1199 "package",
1200 "defer",
1201 "go",
1202 "chan",
1203 "select",
1204 "range",
1205 "map",
1206 "interface",
1207 "fallthrough",
1208 "nil",
1209];
1210
1211fn render_highlighted_line(ui: &mut Context, line: &str) {
1212 let theme = ui.theme;
1213 let is_light = matches!(
1214 theme.bg,
1215 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1216 );
1217 let keyword_color = if is_light {
1218 Color::Rgb(166, 38, 164)
1219 } else {
1220 Color::Rgb(198, 120, 221)
1221 };
1222 let string_color = if is_light {
1223 Color::Rgb(80, 161, 79)
1224 } else {
1225 Color::Rgb(152, 195, 121)
1226 };
1227 let comment_color = theme.text_dim;
1228 let number_color = if is_light {
1229 Color::Rgb(152, 104, 1)
1230 } else {
1231 Color::Rgb(209, 154, 102)
1232 };
1233 let fn_color = if is_light {
1234 Color::Rgb(64, 120, 242)
1235 } else {
1236 Color::Rgb(97, 175, 239)
1237 };
1238 let macro_color = if is_light {
1239 Color::Rgb(1, 132, 188)
1240 } else {
1241 Color::Rgb(86, 182, 194)
1242 };
1243
1244 let trimmed = line.trim_start();
1245 let indent = &line[..line.len() - trimmed.len()];
1246 if !indent.is_empty() {
1247 ui.text(indent);
1248 }
1249
1250 if trimmed.starts_with("//") {
1251 ui.text(trimmed).fg(comment_color).italic();
1252 return;
1253 }
1254
1255 let mut pos = 0;
1256
1257 while pos < trimmed.len() {
1258 let ch = trimmed.as_bytes()[pos];
1259
1260 if ch == b'"' {
1261 if let Some(end) = trimmed[pos + 1..].find('"') {
1262 let s = &trimmed[pos..pos + end + 2];
1263 ui.text(s).fg(string_color);
1264 pos += end + 2;
1265 continue;
1266 }
1267 }
1268
1269 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1270 {
1271 let end = trimmed[pos..]
1272 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1273 .map_or(trimmed.len(), |e| pos + e);
1274 ui.text(&trimmed[pos..end]).fg(number_color);
1275 pos = end;
1276 continue;
1277 }
1278
1279 if ch.is_ascii_alphabetic() || ch == b'_' {
1280 let end = trimmed[pos..]
1281 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1282 .map_or(trimmed.len(), |e| pos + e);
1283 let word = &trimmed[pos..end];
1284
1285 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1286 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1287 pos = end + 1;
1288 } else if end < trimmed.len()
1289 && trimmed.as_bytes()[end] == b'('
1290 && !KEYWORDS.contains(&word)
1291 {
1292 ui.text(word).fg(fn_color);
1293 pos = end;
1294 } else if KEYWORDS.contains(&word) {
1295 ui.text(word).fg(keyword_color);
1296 pos = end;
1297 } else {
1298 ui.text(word);
1299 pos = end;
1300 }
1301 continue;
1302 }
1303
1304 let end = trimmed[pos..]
1305 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1306 .map_or(trimmed.len(), |e| pos + e);
1307 ui.text(&trimmed[pos..end]);
1308 pos = end;
1309 }
1310}