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(),
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(),
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.line(|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().fg(theme.text);
401 } else {
402 if ui.button_with(*segment, ButtonVariant::Default) {
403 clicked_idx = Some(i);
404 }
405 ui.text(separator).dim();
406 }
407 }
408 });
409
410 clicked_idx
411 }
412
413 pub fn accordion(&mut self, title: &str, open: &mut bool, f: impl FnOnce(&mut Context)) {
414 let theme = self.theme;
415 let focused = self.register_focusable();
416
417 if focused && self.key_code(KeyCode::Enter) {
418 *open = !*open;
419 }
420
421 let icon = if *open { "▾" } else { "▸" };
422 let title_color = if focused { theme.primary } else { theme.text };
423
424 let resp = self.container().col(|ui| {
425 ui.line(|ui| {
426 ui.text(icon).fg(title_color);
427 ui.text(format!(" {title}")).bold().fg(title_color);
428 });
429 });
430
431 if resp.clicked {
432 *open = !*open;
433 }
434
435 if *open {
436 self.container().pl(2).col(f);
437 }
438 }
439
440 pub fn definition_list(&mut self, items: &[(&str, &str)]) {
441 let max_key_width = items
442 .iter()
443 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
444 .max()
445 .unwrap_or(0);
446
447 self.col(|ui| {
448 for (key, value) in items {
449 ui.line(|ui| {
450 let padded = format!("{:>width$}", key, width = max_key_width);
451 ui.text(padded).dim();
452 ui.text(" ");
453 ui.text(*value);
454 });
455 }
456 });
457 }
458
459 pub fn divider_text(&mut self, label: &str) {
460 let w = self.width();
461 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
462 let pad = 1u32;
463 let left_len = 4u32;
464 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
465 let left: String = "─".repeat(left_len as usize);
466 let right: String = "─".repeat(right_len as usize);
467 let theme = self.theme;
468 self.line(|ui| {
469 ui.text(&left).fg(theme.border);
470 ui.text(format!(" {} ", label)).fg(theme.text);
471 ui.text(&right).fg(theme.border);
472 });
473 }
474
475 pub fn badge(&mut self, label: &str) {
476 let theme = self.theme;
477 self.badge_colored(label, theme.primary);
478 }
479
480 pub fn badge_colored(&mut self, label: &str, color: Color) {
481 let fg = Color::contrast_fg(color);
482 self.text(format!(" {} ", label)).fg(fg).bg(color);
483 }
484
485 pub fn key_hint(&mut self, key: &str) {
486 let theme = self.theme;
487 self.text(format!(" {} ", key))
488 .reversed()
489 .fg(theme.text_dim);
490 }
491
492 pub fn stat(&mut self, label: &str, value: &str) {
493 self.col(|ui| {
494 ui.text(label).dim();
495 ui.text(value).bold();
496 });
497 }
498
499 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) {
500 self.col(|ui| {
501 ui.text(label).dim();
502 ui.text(value).bold().fg(color);
503 });
504 }
505
506 pub fn stat_trend(&mut self, label: &str, value: &str, trend: crate::widgets::Trend) {
507 let theme = self.theme;
508 let (arrow, color) = match trend {
509 crate::widgets::Trend::Up => ("↑", theme.success),
510 crate::widgets::Trend::Down => ("↓", theme.error),
511 };
512 self.col(|ui| {
513 ui.text(label).dim();
514 ui.line(|ui| {
515 ui.text(value).bold();
516 ui.text(format!(" {arrow}")).fg(color);
517 });
518 });
519 }
520
521 pub fn empty_state(&mut self, title: &str, description: &str) {
522 self.container().center().col(|ui| {
523 ui.text(title).align(Align::Center);
524 ui.text(description).dim().align(Align::Center);
525 });
526 }
527
528 pub fn empty_state_action(
529 &mut self,
530 title: &str,
531 description: &str,
532 action_label: &str,
533 ) -> bool {
534 let mut clicked = false;
535 self.container().center().col(|ui| {
536 ui.text(title).align(Align::Center);
537 ui.text(description).dim().align(Align::Center);
538 if ui.button(action_label) {
539 clicked = true;
540 }
541 });
542 clicked
543 }
544
545 pub fn code_block(&mut self, code: &str) {
546 let theme = self.theme;
547 self.bordered(Border::Rounded)
548 .bg(theme.surface)
549 .pad(1)
550 .col(|ui| {
551 for line in code.lines() {
552 render_highlighted_line(ui, line);
553 }
554 });
555 }
556
557 pub fn code_block_numbered(&mut self, code: &str) {
558 let lines: Vec<&str> = code.lines().collect();
559 let gutter_w = format!("{}", lines.len()).len();
560 let theme = self.theme;
561 self.bordered(Border::Rounded)
562 .bg(theme.surface)
563 .pad(1)
564 .col(|ui| {
565 for (i, line) in lines.iter().enumerate() {
566 ui.line(|ui| {
567 ui.text(format!("{:>gutter_w$} │ ", i + 1))
568 .fg(theme.text_dim);
569 render_highlighted_line(ui, line);
570 });
571 }
572 });
573 }
574
575 pub fn wrap(&mut self) -> &mut Self {
577 if let Some(idx) = self.last_text_idx {
578 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
579 *wrap = true;
580 }
581 }
582 self
583 }
584
585 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
586 if let Some(idx) = self.last_text_idx {
587 match &mut self.commands[idx] {
588 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
589 _ => {}
590 }
591 }
592 }
593
594 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
612 self.push_container(Direction::Column, 0, f)
613 }
614
615 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
619 self.push_container(Direction::Column, gap, f)
620 }
621
622 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
639 self.push_container(Direction::Row, 0, f)
640 }
641
642 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
646 self.push_container(Direction::Row, gap, f)
647 }
648
649 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
666 let _ = self.push_container(Direction::Row, 0, f);
667 self
668 }
669
670 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
689 let start = self.commands.len();
690 f(self);
691 let mut segments: Vec<(String, Style)> = Vec::new();
692 for cmd in self.commands.drain(start..) {
693 if let Command::Text { content, style, .. } = cmd {
694 segments.push((content, style));
695 }
696 }
697 self.commands.push(Command::RichText {
698 segments,
699 wrap: true,
700 align: Align::Start,
701 margin: Margin::default(),
702 constraints: Constraints::default(),
703 });
704 self.last_text_idx = None;
705 self
706 }
707
708 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
717 self.commands.push(Command::BeginOverlay { modal: true });
718 self.overlay_depth += 1;
719 self.modal_active = true;
720 f(self);
721 self.overlay_depth = self.overlay_depth.saturating_sub(1);
722 self.commands.push(Command::EndOverlay);
723 self.last_text_idx = None;
724 }
725
726 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
728 self.commands.push(Command::BeginOverlay { modal: false });
729 self.overlay_depth += 1;
730 f(self);
731 self.overlay_depth = self.overlay_depth.saturating_sub(1);
732 self.commands.push(Command::EndOverlay);
733 self.last_text_idx = None;
734 }
735
736 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
744 self.group_count = self.group_count.saturating_add(1);
745 self.group_stack.push(name.to_string());
746 self.container().group_name(name.to_string())
747 }
748
749 pub fn container(&mut self) -> ContainerBuilder<'_> {
770 let border = self.theme.border;
771 ContainerBuilder {
772 ctx: self,
773 gap: 0,
774 align: Align::Start,
775 justify: Justify::Start,
776 border: None,
777 border_sides: BorderSides::all(),
778 border_style: Style::new().fg(border),
779 bg: None,
780 dark_bg: None,
781 dark_border_style: None,
782 group_hover_bg: None,
783 group_hover_border_style: None,
784 group_name: None,
785 padding: Padding::default(),
786 margin: Margin::default(),
787 constraints: Constraints::default(),
788 title: None,
789 grow: 0,
790 scroll_offset: None,
791 }
792 }
793
794 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
813 let index = self.scroll_count;
814 self.scroll_count += 1;
815 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
816 state.set_bounds(ch, vh);
817 let max = ch.saturating_sub(vh) as usize;
818 state.offset = state.offset.min(max);
819 }
820
821 let next_id = self.interaction_count;
822 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
823 let inner_rects: Vec<Rect> = self
824 .prev_scroll_rects
825 .iter()
826 .enumerate()
827 .filter(|&(j, sr)| {
828 j != index
829 && sr.width > 0
830 && sr.height > 0
831 && sr.x >= rect.x
832 && sr.right() <= rect.right()
833 && sr.y >= rect.y
834 && sr.bottom() <= rect.bottom()
835 })
836 .map(|(_, sr)| *sr)
837 .collect();
838 self.auto_scroll_nested(&rect, state, &inner_rects);
839 }
840
841 self.container().scroll_offset(state.offset as u32)
842 }
843
844 pub fn scrollbar(&mut self, state: &ScrollState) {
864 let vh = state.viewport_height();
865 let ch = state.content_height();
866 if vh == 0 || ch <= vh {
867 return;
868 }
869
870 let track_height = vh;
871 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
872 let max_offset = ch.saturating_sub(vh);
873 let thumb_pos = if max_offset == 0 {
874 0
875 } else {
876 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
877 .round() as u32
878 };
879
880 let theme = self.theme;
881 let track_char = '│';
882 let thumb_char = '█';
883
884 self.container().w(1).h(track_height).col(|ui| {
885 for i in 0..track_height {
886 if i >= thumb_pos && i < thumb_pos + thumb_height {
887 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
888 } else {
889 ui.styled(
890 track_char.to_string(),
891 Style::new().fg(theme.text_dim).dim(),
892 );
893 }
894 }
895 });
896 }
897
898 fn auto_scroll_nested(
899 &mut self,
900 rect: &Rect,
901 state: &mut ScrollState,
902 inner_scroll_rects: &[Rect],
903 ) {
904 let mut to_consume: Vec<usize> = Vec::new();
905
906 for (i, event) in self.events.iter().enumerate() {
907 if self.consumed[i] {
908 continue;
909 }
910 if let Event::Mouse(mouse) = event {
911 let in_bounds = mouse.x >= rect.x
912 && mouse.x < rect.right()
913 && mouse.y >= rect.y
914 && mouse.y < rect.bottom();
915 if !in_bounds {
916 continue;
917 }
918 let in_inner = inner_scroll_rects.iter().any(|sr| {
919 mouse.x >= sr.x
920 && mouse.x < sr.right()
921 && mouse.y >= sr.y
922 && mouse.y < sr.bottom()
923 });
924 if in_inner {
925 continue;
926 }
927 match mouse.kind {
928 MouseKind::ScrollUp => {
929 state.scroll_up(1);
930 to_consume.push(i);
931 }
932 MouseKind::ScrollDown => {
933 state.scroll_down(1);
934 to_consume.push(i);
935 }
936 MouseKind::Drag(MouseButton::Left) => {}
937 _ => {}
938 }
939 }
940 }
941
942 for i in to_consume {
943 self.consumed[i] = true;
944 }
945 }
946
947 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
951 self.container()
952 .border(border)
953 .border_sides(BorderSides::all())
954 }
955
956 fn push_container(
957 &mut self,
958 direction: Direction,
959 gap: u32,
960 f: impl FnOnce(&mut Context),
961 ) -> Response {
962 let interaction_id = self.interaction_count;
963 self.interaction_count += 1;
964 let border = self.theme.border;
965
966 self.commands.push(Command::BeginContainer {
967 direction,
968 gap,
969 align: Align::Start,
970 justify: Justify::Start,
971 border: None,
972 border_sides: BorderSides::all(),
973 border_style: Style::new().fg(border),
974 bg_color: None,
975 padding: Padding::default(),
976 margin: Margin::default(),
977 constraints: Constraints::default(),
978 title: None,
979 grow: 0,
980 group_name: None,
981 });
982 f(self);
983 self.commands.push(Command::EndContainer);
984 self.last_text_idx = None;
985
986 self.response_for(interaction_id)
987 }
988
989 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
990 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
991 return Response::default();
992 }
993 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
994 let clicked = self
995 .click_pos
996 .map(|(mx, my)| {
997 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
998 })
999 .unwrap_or(false);
1000 let hovered = self
1001 .mouse_pos
1002 .map(|(mx, my)| {
1003 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1004 })
1005 .unwrap_or(false);
1006 Response { clicked, hovered }
1007 } else {
1008 Response::default()
1009 }
1010 }
1011
1012 pub fn is_group_hovered(&self, name: &str) -> bool {
1014 if let Some(pos) = self.mouse_pos {
1015 self.prev_group_rects.iter().any(|(n, rect)| {
1016 n == name
1017 && pos.0 >= rect.x
1018 && pos.0 < rect.x + rect.width
1019 && pos.1 >= rect.y
1020 && pos.1 < rect.y + rect.height
1021 })
1022 } else {
1023 false
1024 }
1025 }
1026
1027 pub fn is_group_focused(&self, name: &str) -> bool {
1029 if self.prev_focus_count == 0 {
1030 return false;
1031 }
1032 let focused_index = self.focus_index % self.prev_focus_count;
1033 self.prev_focus_groups
1034 .get(focused_index)
1035 .and_then(|group| group.as_deref())
1036 .map(|group| group == name)
1037 .unwrap_or(false)
1038 }
1039
1040 pub fn grow(&mut self, value: u16) -> &mut Self {
1045 if let Some(idx) = self.last_text_idx {
1046 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1047 *grow = value;
1048 }
1049 }
1050 self
1051 }
1052
1053 pub fn align(&mut self, align: Align) -> &mut Self {
1055 if let Some(idx) = self.last_text_idx {
1056 if let Command::Text {
1057 align: text_align, ..
1058 } = &mut self.commands[idx]
1059 {
1060 *text_align = align;
1061 }
1062 }
1063 self
1064 }
1065
1066 pub fn spacer(&mut self) -> &mut Self {
1070 self.commands.push(Command::Spacer { grow: 1 });
1071 self.last_text_idx = None;
1072 self
1073 }
1074
1075 pub fn form(
1079 &mut self,
1080 state: &mut FormState,
1081 f: impl FnOnce(&mut Context, &mut FormState),
1082 ) -> &mut Self {
1083 self.col(|ui| {
1084 f(ui, state);
1085 });
1086 self
1087 }
1088
1089 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1093 self.col(|ui| {
1094 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1095 ui.text_input(&mut field.input);
1096 if let Some(error) = field.error.as_deref() {
1097 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1098 }
1099 });
1100 self
1101 }
1102
1103 pub fn form_submit(&mut self, label: impl Into<String>) -> bool {
1107 self.button(label)
1108 }
1109}
1110
1111const RUST_KEYWORDS: &[&str] = &[
1112 "fn", "let", "mut", "pub", "use", "impl", "struct", "enum", "trait", "type", "const", "static",
1113 "if", "else", "match", "for", "while", "loop", "return", "break", "continue", "where", "self",
1114 "super", "crate", "mod", "async", "await", "move", "ref", "in", "as", "true", "false", "Some",
1115 "None", "Ok", "Err", "Self",
1116];
1117
1118fn render_highlighted_line(ui: &mut Context, line: &str) {
1119 let theme = ui.theme;
1120 let keyword_color = Color::Rgb(198, 120, 221);
1121 let string_color = Color::Rgb(152, 195, 121);
1122 let comment_color = theme.text_dim;
1123 let number_color = Color::Rgb(209, 154, 102);
1124 let fn_color = Color::Rgb(97, 175, 239);
1125 let macro_color = Color::Rgb(86, 182, 194);
1126
1127 let trimmed = line.trim_start();
1128 let indent = &line[..line.len() - trimmed.len()];
1129 if !indent.is_empty() {
1130 ui.text(indent);
1131 }
1132
1133 if trimmed.starts_with("//") {
1134 ui.text(trimmed).fg(comment_color).italic();
1135 return;
1136 }
1137
1138 let mut pos = 0;
1139
1140 while pos < trimmed.len() {
1141 let ch = trimmed.as_bytes()[pos];
1142
1143 if ch == b'"' {
1144 if let Some(end) = trimmed[pos + 1..].find('"') {
1145 let s = &trimmed[pos..pos + end + 2];
1146 ui.text(s).fg(string_color);
1147 pos += end + 2;
1148 continue;
1149 }
1150 }
1151
1152 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1153 {
1154 let end = trimmed[pos..]
1155 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1156 .map_or(trimmed.len(), |e| pos + e);
1157 ui.text(&trimmed[pos..end]).fg(number_color);
1158 pos = end;
1159 continue;
1160 }
1161
1162 if ch.is_ascii_alphabetic() || ch == b'_' {
1163 let end = trimmed[pos..]
1164 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1165 .map_or(trimmed.len(), |e| pos + e);
1166 let word = &trimmed[pos..end];
1167
1168 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1169 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1170 pos = end + 1;
1171 } else if end < trimmed.len()
1172 && trimmed.as_bytes()[end] == b'('
1173 && !RUST_KEYWORDS.contains(&word)
1174 {
1175 ui.text(word).fg(fn_color);
1176 pos = end;
1177 } else if RUST_KEYWORDS.contains(&word) {
1178 ui.text(word).fg(keyword_color);
1179 pos = end;
1180 } else {
1181 ui.text(word);
1182 pos = end;
1183 }
1184 continue;
1185 }
1186
1187 let end = trimmed[pos..]
1188 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1189 .map_or(trimmed.len(), |e| pos + e);
1190 ui.text(&trimmed[pos..end]);
1191 pos = end;
1192 }
1193}