1use super::*;
2use crate::KeyMap;
3
4impl Context {
5 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 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 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 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 pub fn bold(&mut self) -> &mut Self {
121 self.modify_last_style(|s| s.modifiers |= Modifiers::BOLD);
122 self
123 }
124
125 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 pub fn italic(&mut self) -> &mut Self {
142 self.modify_last_style(|s| s.modifiers |= Modifiers::ITALIC);
143 self
144 }
145
146 pub fn underline(&mut self) -> &mut Self {
148 self.modify_last_style(|s| s.modifiers |= Modifiers::UNDERLINE);
149 self
150 }
151
152 pub fn reversed(&mut self) -> &mut Self {
154 self.modify_last_style(|s| s.modifiers |= Modifiers::REVERSED);
155 self
156 }
157
158 pub fn strikethrough(&mut self) -> &mut Self {
160 self.modify_last_style(|s| s.modifiers |= Modifiers::STRIKETHROUGH);
161 self
162 }
163
164 pub fn fg(&mut self, color: Color) -> &mut Self {
166 self.modify_last_style(|s| s.fg = Some(color));
167 self
168 }
169
170 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 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 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 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 pub fn tool_approval(&mut self, state: &mut ToolApprovalState) -> Response {
313 let old_action = state.action;
314 let theme = self.theme;
315 self.bordered(Border::Rounded).col(|ui| {
316 ui.row(|ui| {
317 ui.text("⚡").fg(theme.warning);
318 ui.text(&state.tool_name).bold().fg(theme.primary);
319 });
320 ui.text(&state.description).dim();
321
322 if state.action == ApprovalAction::Pending {
323 ui.row(|ui| {
324 if ui.button("✓ Approve").clicked {
325 state.action = ApprovalAction::Approved;
326 }
327 if ui.button("✗ Reject").clicked {
328 state.action = ApprovalAction::Rejected;
329 }
330 });
331 } else {
332 let (label, color) = match state.action {
333 ApprovalAction::Approved => ("✓ Approved", theme.success),
334 ApprovalAction::Rejected => ("✗ Rejected", theme.error),
335 ApprovalAction::Pending => unreachable!(),
336 };
337 ui.text(label).fg(color).bold();
338 }
339 });
340
341 Response {
342 changed: state.action != old_action,
343 ..Response::none()
344 }
345 }
346
347 pub fn context_bar(&mut self, items: &[ContextItem]) -> Response {
360 if items.is_empty() {
361 return Response::none();
362 }
363
364 let theme = self.theme;
365 let total: usize = items.iter().map(|item| item.tokens).sum();
366
367 self.container().row(|ui| {
368 ui.text("📎").dim();
369 for item in items {
370 ui.text(format!(
371 "{} ({})",
372 item.label,
373 format_token_count(item.tokens)
374 ))
375 .fg(theme.secondary);
376 }
377 ui.spacer();
378 ui.text(format!("Σ {}", format_token_count(total))).dim();
379 });
380
381 Response::none()
382 }
383
384 pub fn alert(&mut self, message: &str, level: crate::widgets::AlertLevel) -> Response {
385 use crate::widgets::AlertLevel;
386
387 let theme = self.theme;
388 let (icon, color) = match level {
389 AlertLevel::Info => ("ℹ", theme.accent),
390 AlertLevel::Success => ("✓", theme.success),
391 AlertLevel::Warning => ("⚠", theme.warning),
392 AlertLevel::Error => ("✕", theme.error),
393 };
394
395 let focused = self.register_focusable();
396 let key_dismiss = focused && (self.key_code(KeyCode::Enter) || self.key('x'));
397
398 let mut response = self.container().col(|ui| {
399 ui.line(|ui| {
400 ui.text(format!(" {icon} ")).fg(color).bold();
401 ui.text(message).grow(1);
402 ui.text(" [×] ").dim();
403 });
404 });
405 response.focused = focused;
406 if key_dismiss {
407 response.clicked = true;
408 }
409
410 response
411 }
412
413 pub fn confirm(&mut self, question: &str, result: &mut bool) -> Response {
427 let focused = self.register_focusable();
428 let selected_yes = self.use_state(|| true);
429 let mut is_yes = *selected_yes.get(self);
430 let mut clicked = false;
431
432 if focused {
433 let mut consumed_indices = Vec::new();
434 for (i, event) in self.events.iter().enumerate() {
435 if let Event::Key(key) = event {
436 if key.kind != KeyEventKind::Press {
437 continue;
438 }
439
440 match key.code {
441 KeyCode::Char('y') => {
442 is_yes = true;
443 *result = true;
444 clicked = true;
445 consumed_indices.push(i);
446 }
447 KeyCode::Char('n') => {
448 is_yes = false;
449 *result = false;
450 clicked = true;
451 consumed_indices.push(i);
452 }
453 KeyCode::Tab | KeyCode::BackTab | KeyCode::Left | KeyCode::Right => {
454 is_yes = !is_yes;
455 consumed_indices.push(i);
456 }
457 KeyCode::Enter => {
458 *result = is_yes;
459 clicked = true;
460 consumed_indices.push(i);
461 }
462 _ => {}
463 }
464 }
465 }
466
467 for idx in consumed_indices {
468 self.consumed[idx] = true;
469 }
470 }
471
472 *selected_yes.get_mut(self) = is_yes;
473
474 let yes_style = if is_yes {
475 if focused {
476 Style::new().fg(self.theme.bg).bg(self.theme.success).bold()
477 } else {
478 Style::new().fg(self.theme.success).bold()
479 }
480 } else {
481 Style::new().fg(self.theme.text_dim)
482 };
483 let no_style = if !is_yes {
484 if focused {
485 Style::new().fg(self.theme.bg).bg(self.theme.error).bold()
486 } else {
487 Style::new().fg(self.theme.error).bold()
488 }
489 } else {
490 Style::new().fg(self.theme.text_dim)
491 };
492
493 let mut response = self.row(|ui| {
494 ui.text(question);
495 ui.text(" ");
496 ui.styled("[Yes]", yes_style);
497 ui.text(" ");
498 ui.styled("[No]", no_style);
499 });
500 response.focused = focused;
501 response.clicked = clicked;
502 response.changed = clicked;
503 response
504 }
505
506 pub fn breadcrumb(&mut self, segments: &[&str]) -> Option<usize> {
507 self.breadcrumb_with(segments, " › ")
508 }
509
510 pub fn breadcrumb_with(&mut self, segments: &[&str], separator: &str) -> Option<usize> {
511 let theme = self.theme;
512 let last_idx = segments.len().saturating_sub(1);
513 let mut clicked_idx: Option<usize> = None;
514
515 self.row(|ui| {
516 for (i, segment) in segments.iter().enumerate() {
517 let is_last = i == last_idx;
518 if is_last {
519 ui.text(*segment).bold();
520 } else {
521 let focused = ui.register_focusable();
522 let pressed = focused && (ui.key_code(KeyCode::Enter) || ui.key(' '));
523 let resp = ui.interaction();
524 let color = if resp.hovered || focused {
525 theme.accent
526 } else {
527 theme.primary
528 };
529 ui.text(*segment).fg(color).underline();
530 if resp.clicked || pressed {
531 clicked_idx = Some(i);
532 }
533 ui.text(separator).dim();
534 }
535 }
536 });
537
538 clicked_idx
539 }
540
541 pub fn accordion(
542 &mut self,
543 title: &str,
544 open: &mut bool,
545 f: impl FnOnce(&mut Context),
546 ) -> Response {
547 let theme = self.theme;
548 let focused = self.register_focusable();
549 let old_open = *open;
550
551 if focused && self.key_code(KeyCode::Enter) {
552 *open = !*open;
553 }
554
555 let icon = if *open { "▾" } else { "▸" };
556 let title_color = if focused { theme.primary } else { theme.text };
557
558 let mut response = self.container().col(|ui| {
559 ui.line(|ui| {
560 ui.text(icon).fg(title_color);
561 ui.text(format!(" {title}")).bold().fg(title_color);
562 });
563 });
564
565 if response.clicked {
566 *open = !*open;
567 }
568
569 if *open {
570 self.container().pl(2).col(f);
571 }
572
573 response.focused = focused;
574 response.changed = *open != old_open;
575 response
576 }
577
578 pub fn definition_list(&mut self, items: &[(&str, &str)]) -> Response {
579 let max_key_width = items
580 .iter()
581 .map(|(k, _)| unicode_width::UnicodeWidthStr::width(*k))
582 .max()
583 .unwrap_or(0);
584
585 self.col(|ui| {
586 for (key, value) in items {
587 ui.line(|ui| {
588 let padded = format!("{:>width$}", key, width = max_key_width);
589 ui.text(padded).dim();
590 ui.text(" ");
591 ui.text(*value);
592 });
593 }
594 });
595
596 Response::none()
597 }
598
599 pub fn divider_text(&mut self, label: &str) -> Response {
600 let w = self.width();
601 let label_len = unicode_width::UnicodeWidthStr::width(label) as u32;
602 let pad = 1u32;
603 let left_len = 4u32;
604 let right_len = w.saturating_sub(left_len + pad + label_len + pad);
605 let left: String = "─".repeat(left_len as usize);
606 let right: String = "─".repeat(right_len as usize);
607 let theme = self.theme;
608 self.line(|ui| {
609 ui.text(&left).fg(theme.border);
610 ui.text(format!(" {} ", label)).fg(theme.text);
611 ui.text(&right).fg(theme.border);
612 });
613
614 Response::none()
615 }
616
617 pub fn badge(&mut self, label: &str) -> Response {
618 let theme = self.theme;
619 self.badge_colored(label, theme.primary)
620 }
621
622 pub fn badge_colored(&mut self, label: &str, color: Color) -> Response {
623 let fg = Color::contrast_fg(color);
624 self.text(format!(" {} ", label)).fg(fg).bg(color);
625
626 Response::none()
627 }
628
629 pub fn key_hint(&mut self, key: &str) -> Response {
630 let theme = self.theme;
631 self.text(format!(" {} ", key))
632 .reversed()
633 .fg(theme.text_dim);
634
635 Response::none()
636 }
637
638 pub fn stat(&mut self, label: &str, value: &str) -> Response {
639 self.col(|ui| {
640 ui.text(label).dim();
641 ui.text(value).bold();
642 });
643
644 Response::none()
645 }
646
647 pub fn stat_colored(&mut self, label: &str, value: &str, color: Color) -> Response {
648 self.col(|ui| {
649 ui.text(label).dim();
650 ui.text(value).bold().fg(color);
651 });
652
653 Response::none()
654 }
655
656 pub fn stat_trend(
657 &mut self,
658 label: &str,
659 value: &str,
660 trend: crate::widgets::Trend,
661 ) -> Response {
662 let theme = self.theme;
663 let (arrow, color) = match trend {
664 crate::widgets::Trend::Up => ("↑", theme.success),
665 crate::widgets::Trend::Down => ("↓", theme.error),
666 };
667 self.col(|ui| {
668 ui.text(label).dim();
669 ui.line(|ui| {
670 ui.text(value).bold();
671 ui.text(format!(" {arrow}")).fg(color);
672 });
673 });
674
675 Response::none()
676 }
677
678 pub fn empty_state(&mut self, title: &str, description: &str) -> Response {
679 self.container().center().col(|ui| {
680 ui.text(title).align(Align::Center);
681 ui.text(description).dim().align(Align::Center);
682 });
683
684 Response::none()
685 }
686
687 pub fn empty_state_action(
688 &mut self,
689 title: &str,
690 description: &str,
691 action_label: &str,
692 ) -> Response {
693 let mut clicked = false;
694 self.container().center().col(|ui| {
695 ui.text(title).align(Align::Center);
696 ui.text(description).dim().align(Align::Center);
697 if ui.button(action_label).clicked {
698 clicked = true;
699 }
700 });
701
702 Response {
703 clicked,
704 changed: clicked,
705 ..Response::none()
706 }
707 }
708
709 pub fn code_block(&mut self, code: &str) -> Response {
710 let theme = self.theme;
711 self.bordered(Border::Rounded)
712 .bg(theme.surface)
713 .pad(1)
714 .col(|ui| {
715 for line in code.lines() {
716 render_highlighted_line(ui, line);
717 }
718 });
719
720 Response::none()
721 }
722
723 pub fn code_block_numbered(&mut self, code: &str) -> Response {
724 let lines: Vec<&str> = code.lines().collect();
725 let gutter_w = format!("{}", lines.len()).len();
726 let theme = self.theme;
727 self.bordered(Border::Rounded)
728 .bg(theme.surface)
729 .pad(1)
730 .col(|ui| {
731 for (i, line) in lines.iter().enumerate() {
732 ui.line(|ui| {
733 ui.text(format!("{:>gutter_w$} │ ", i + 1))
734 .fg(theme.text_dim);
735 render_highlighted_line(ui, line);
736 });
737 }
738 });
739
740 Response::none()
741 }
742
743 pub fn wrap(&mut self) -> &mut Self {
745 if let Some(idx) = self.last_text_idx {
746 if let Command::Text { wrap, .. } = &mut self.commands[idx] {
747 *wrap = true;
748 }
749 }
750 self
751 }
752
753 fn modify_last_style(&mut self, f: impl FnOnce(&mut Style)) {
754 if let Some(idx) = self.last_text_idx {
755 match &mut self.commands[idx] {
756 Command::Text { style, .. } | Command::Link { style, .. } => f(style),
757 _ => {}
758 }
759 }
760 }
761
762 pub fn col(&mut self, f: impl FnOnce(&mut Context)) -> Response {
780 self.push_container(Direction::Column, 0, f)
781 }
782
783 pub fn col_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
787 self.push_container(Direction::Column, gap, f)
788 }
789
790 pub fn row(&mut self, f: impl FnOnce(&mut Context)) -> Response {
807 self.push_container(Direction::Row, 0, f)
808 }
809
810 pub fn row_gap(&mut self, gap: u32, f: impl FnOnce(&mut Context)) -> Response {
814 self.push_container(Direction::Row, gap, f)
815 }
816
817 pub fn line(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
834 let _ = self.push_container(Direction::Row, 0, f);
835 self
836 }
837
838 pub fn line_wrap(&mut self, f: impl FnOnce(&mut Context)) -> &mut Self {
857 let start = self.commands.len();
858 f(self);
859 let mut segments: Vec<(String, Style)> = Vec::new();
860 for cmd in self.commands.drain(start..) {
861 if let Command::Text { content, style, .. } = cmd {
862 segments.push((content, style));
863 }
864 }
865 self.commands.push(Command::RichText {
866 segments,
867 wrap: true,
868 align: Align::Start,
869 margin: Margin::default(),
870 constraints: Constraints::default(),
871 });
872 self.last_text_idx = None;
873 self
874 }
875
876 pub fn modal(&mut self, f: impl FnOnce(&mut Context)) {
885 self.commands.push(Command::BeginOverlay { modal: true });
886 self.overlay_depth += 1;
887 self.modal_active = true;
888 f(self);
889 self.overlay_depth = self.overlay_depth.saturating_sub(1);
890 self.commands.push(Command::EndOverlay);
891 self.last_text_idx = None;
892 }
893
894 pub fn overlay(&mut self, f: impl FnOnce(&mut Context)) {
896 self.commands.push(Command::BeginOverlay { modal: false });
897 self.overlay_depth += 1;
898 f(self);
899 self.overlay_depth = self.overlay_depth.saturating_sub(1);
900 self.commands.push(Command::EndOverlay);
901 self.last_text_idx = None;
902 }
903
904 pub fn group(&mut self, name: &str) -> ContainerBuilder<'_> {
912 self.group_count = self.group_count.saturating_add(1);
913 self.group_stack.push(name.to_string());
914 self.container().group_name(name.to_string())
915 }
916
917 pub fn container(&mut self) -> ContainerBuilder<'_> {
938 let border = self.theme.border;
939 ContainerBuilder {
940 ctx: self,
941 gap: 0,
942 align: Align::Start,
943 justify: Justify::Start,
944 border: None,
945 border_sides: BorderSides::all(),
946 border_style: Style::new().fg(border),
947 bg: None,
948 dark_bg: None,
949 dark_border_style: None,
950 group_hover_bg: None,
951 group_hover_border_style: None,
952 group_name: None,
953 padding: Padding::default(),
954 margin: Margin::default(),
955 constraints: Constraints::default(),
956 title: None,
957 grow: 0,
958 scroll_offset: None,
959 }
960 }
961
962 pub fn scrollable(&mut self, state: &mut ScrollState) -> ContainerBuilder<'_> {
981 let index = self.scroll_count;
982 self.scroll_count += 1;
983 if let Some(&(ch, vh)) = self.prev_scroll_infos.get(index) {
984 state.set_bounds(ch, vh);
985 let max = ch.saturating_sub(vh) as usize;
986 state.offset = state.offset.min(max);
987 }
988
989 let next_id = self.interaction_count;
990 if let Some(rect) = self.prev_hit_map.get(next_id).copied() {
991 let inner_rects: Vec<Rect> = self
992 .prev_scroll_rects
993 .iter()
994 .enumerate()
995 .filter(|&(j, sr)| {
996 j != index
997 && sr.width > 0
998 && sr.height > 0
999 && sr.x >= rect.x
1000 && sr.right() <= rect.right()
1001 && sr.y >= rect.y
1002 && sr.bottom() <= rect.bottom()
1003 })
1004 .map(|(_, sr)| *sr)
1005 .collect();
1006 self.auto_scroll_nested(&rect, state, &inner_rects);
1007 }
1008
1009 self.container().scroll_offset(state.offset as u32)
1010 }
1011
1012 pub fn scrollbar(&mut self, state: &ScrollState) {
1032 let vh = state.viewport_height();
1033 let ch = state.content_height();
1034 if vh == 0 || ch <= vh {
1035 return;
1036 }
1037
1038 let track_height = vh;
1039 let thumb_height = ((vh as f64 * vh as f64 / ch as f64).ceil() as u32).max(1);
1040 let max_offset = ch.saturating_sub(vh);
1041 let thumb_pos = if max_offset == 0 {
1042 0
1043 } else {
1044 ((state.offset as f64 / max_offset as f64) * (track_height - thumb_height) as f64)
1045 .round() as u32
1046 };
1047
1048 let theme = self.theme;
1049 let track_char = '│';
1050 let thumb_char = '█';
1051
1052 self.container().w(1).h(track_height).col(|ui| {
1053 for i in 0..track_height {
1054 if i >= thumb_pos && i < thumb_pos + thumb_height {
1055 ui.styled(thumb_char.to_string(), Style::new().fg(theme.primary));
1056 } else {
1057 ui.styled(
1058 track_char.to_string(),
1059 Style::new().fg(theme.text_dim).dim(),
1060 );
1061 }
1062 }
1063 });
1064 }
1065
1066 fn auto_scroll_nested(
1067 &mut self,
1068 rect: &Rect,
1069 state: &mut ScrollState,
1070 inner_scroll_rects: &[Rect],
1071 ) {
1072 let mut to_consume: Vec<usize> = Vec::new();
1073
1074 for (i, event) in self.events.iter().enumerate() {
1075 if self.consumed[i] {
1076 continue;
1077 }
1078 if let Event::Mouse(mouse) = event {
1079 let in_bounds = mouse.x >= rect.x
1080 && mouse.x < rect.right()
1081 && mouse.y >= rect.y
1082 && mouse.y < rect.bottom();
1083 if !in_bounds {
1084 continue;
1085 }
1086 let in_inner = inner_scroll_rects.iter().any(|sr| {
1087 mouse.x >= sr.x
1088 && mouse.x < sr.right()
1089 && mouse.y >= sr.y
1090 && mouse.y < sr.bottom()
1091 });
1092 if in_inner {
1093 continue;
1094 }
1095 match mouse.kind {
1096 MouseKind::ScrollUp => {
1097 state.scroll_up(1);
1098 to_consume.push(i);
1099 }
1100 MouseKind::ScrollDown => {
1101 state.scroll_down(1);
1102 to_consume.push(i);
1103 }
1104 MouseKind::Drag(MouseButton::Left) => {}
1105 _ => {}
1106 }
1107 }
1108 }
1109
1110 for i in to_consume {
1111 self.consumed[i] = true;
1112 }
1113 }
1114
1115 pub fn bordered(&mut self, border: Border) -> ContainerBuilder<'_> {
1119 self.container()
1120 .border(border)
1121 .border_sides(BorderSides::all())
1122 }
1123
1124 fn push_container(
1125 &mut self,
1126 direction: Direction,
1127 gap: u32,
1128 f: impl FnOnce(&mut Context),
1129 ) -> Response {
1130 let interaction_id = self.interaction_count;
1131 self.interaction_count += 1;
1132 let border = self.theme.border;
1133
1134 self.commands.push(Command::BeginContainer {
1135 direction,
1136 gap,
1137 align: Align::Start,
1138 justify: Justify::Start,
1139 border: None,
1140 border_sides: BorderSides::all(),
1141 border_style: Style::new().fg(border),
1142 bg_color: None,
1143 padding: Padding::default(),
1144 margin: Margin::default(),
1145 constraints: Constraints::default(),
1146 title: None,
1147 grow: 0,
1148 group_name: None,
1149 });
1150 f(self);
1151 self.commands.push(Command::EndContainer);
1152 self.last_text_idx = None;
1153
1154 self.response_for(interaction_id)
1155 }
1156
1157 pub(super) fn response_for(&self, interaction_id: usize) -> Response {
1158 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1159 return Response::none();
1160 }
1161 if let Some(rect) = self.prev_hit_map.get(interaction_id) {
1162 let clicked = self
1163 .click_pos
1164 .map(|(mx, my)| {
1165 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1166 })
1167 .unwrap_or(false);
1168 let hovered = self
1169 .mouse_pos
1170 .map(|(mx, my)| {
1171 mx >= rect.x && mx < rect.right() && my >= rect.y && my < rect.bottom()
1172 })
1173 .unwrap_or(false);
1174 Response {
1175 clicked,
1176 hovered,
1177 changed: false,
1178 focused: false,
1179 rect: *rect,
1180 }
1181 } else {
1182 Response::none()
1183 }
1184 }
1185
1186 pub fn is_group_hovered(&self, name: &str) -> bool {
1188 if let Some(pos) = self.mouse_pos {
1189 self.prev_group_rects.iter().any(|(n, rect)| {
1190 n == name
1191 && pos.0 >= rect.x
1192 && pos.0 < rect.x + rect.width
1193 && pos.1 >= rect.y
1194 && pos.1 < rect.y + rect.height
1195 })
1196 } else {
1197 false
1198 }
1199 }
1200
1201 pub fn is_group_focused(&self, name: &str) -> bool {
1203 if self.prev_focus_count == 0 {
1204 return false;
1205 }
1206 let focused_index = self.focus_index % self.prev_focus_count;
1207 self.prev_focus_groups
1208 .get(focused_index)
1209 .and_then(|group| group.as_deref())
1210 .map(|group| group == name)
1211 .unwrap_or(false)
1212 }
1213
1214 pub fn grow(&mut self, value: u16) -> &mut Self {
1219 if let Some(idx) = self.last_text_idx {
1220 if let Command::Text { grow, .. } = &mut self.commands[idx] {
1221 *grow = value;
1222 }
1223 }
1224 self
1225 }
1226
1227 pub fn align(&mut self, align: Align) -> &mut Self {
1229 if let Some(idx) = self.last_text_idx {
1230 if let Command::Text {
1231 align: text_align, ..
1232 } = &mut self.commands[idx]
1233 {
1234 *text_align = align;
1235 }
1236 }
1237 self
1238 }
1239
1240 pub fn spacer(&mut self) -> &mut Self {
1244 self.commands.push(Command::Spacer { grow: 1 });
1245 self.last_text_idx = None;
1246 self
1247 }
1248
1249 pub fn form(
1253 &mut self,
1254 state: &mut FormState,
1255 f: impl FnOnce(&mut Context, &mut FormState),
1256 ) -> &mut Self {
1257 self.col(|ui| {
1258 f(ui, state);
1259 });
1260 self
1261 }
1262
1263 pub fn form_field(&mut self, field: &mut FormField) -> &mut Self {
1267 self.col(|ui| {
1268 ui.styled(field.label.clone(), Style::new().bold().fg(ui.theme.text));
1269 ui.text_input(&mut field.input);
1270 if let Some(error) = field.error.as_deref() {
1271 ui.styled(error.to_string(), Style::new().dim().fg(ui.theme.error));
1272 }
1273 });
1274 self
1275 }
1276
1277 pub fn form_submit(&mut self, label: impl Into<String>) -> Response {
1281 self.button(label)
1282 }
1283}
1284
1285const KEYWORDS: &[&str] = &[
1286 "fn",
1287 "let",
1288 "mut",
1289 "pub",
1290 "use",
1291 "impl",
1292 "struct",
1293 "enum",
1294 "trait",
1295 "type",
1296 "const",
1297 "static",
1298 "if",
1299 "else",
1300 "match",
1301 "for",
1302 "while",
1303 "loop",
1304 "return",
1305 "break",
1306 "continue",
1307 "where",
1308 "self",
1309 "super",
1310 "crate",
1311 "mod",
1312 "async",
1313 "await",
1314 "move",
1315 "ref",
1316 "in",
1317 "as",
1318 "true",
1319 "false",
1320 "Some",
1321 "None",
1322 "Ok",
1323 "Err",
1324 "Self",
1325 "def",
1326 "class",
1327 "import",
1328 "from",
1329 "pass",
1330 "lambda",
1331 "yield",
1332 "with",
1333 "try",
1334 "except",
1335 "raise",
1336 "finally",
1337 "elif",
1338 "del",
1339 "global",
1340 "nonlocal",
1341 "assert",
1342 "is",
1343 "not",
1344 "and",
1345 "or",
1346 "function",
1347 "var",
1348 "const",
1349 "export",
1350 "default",
1351 "switch",
1352 "case",
1353 "throw",
1354 "catch",
1355 "typeof",
1356 "instanceof",
1357 "new",
1358 "delete",
1359 "void",
1360 "this",
1361 "null",
1362 "undefined",
1363 "func",
1364 "package",
1365 "defer",
1366 "go",
1367 "chan",
1368 "select",
1369 "range",
1370 "map",
1371 "interface",
1372 "fallthrough",
1373 "nil",
1374];
1375
1376fn render_highlighted_line(ui: &mut Context, line: &str) {
1377 let theme = ui.theme;
1378 let is_light = matches!(
1379 theme.bg,
1380 Color::Reset | Color::White | Color::Rgb(255, 255, 255)
1381 );
1382 let keyword_color = if is_light {
1383 Color::Rgb(166, 38, 164)
1384 } else {
1385 Color::Rgb(198, 120, 221)
1386 };
1387 let string_color = if is_light {
1388 Color::Rgb(80, 161, 79)
1389 } else {
1390 Color::Rgb(152, 195, 121)
1391 };
1392 let comment_color = theme.text_dim;
1393 let number_color = if is_light {
1394 Color::Rgb(152, 104, 1)
1395 } else {
1396 Color::Rgb(209, 154, 102)
1397 };
1398 let fn_color = if is_light {
1399 Color::Rgb(64, 120, 242)
1400 } else {
1401 Color::Rgb(97, 175, 239)
1402 };
1403 let macro_color = if is_light {
1404 Color::Rgb(1, 132, 188)
1405 } else {
1406 Color::Rgb(86, 182, 194)
1407 };
1408
1409 let trimmed = line.trim_start();
1410 let indent = &line[..line.len() - trimmed.len()];
1411 if !indent.is_empty() {
1412 ui.text(indent);
1413 }
1414
1415 if trimmed.starts_with("//") {
1416 ui.text(trimmed).fg(comment_color).italic();
1417 return;
1418 }
1419
1420 let mut pos = 0;
1421
1422 while pos < trimmed.len() {
1423 let ch = trimmed.as_bytes()[pos];
1424
1425 if ch == b'"' {
1426 if let Some(end) = trimmed[pos + 1..].find('"') {
1427 let s = &trimmed[pos..pos + end + 2];
1428 ui.text(s).fg(string_color);
1429 pos += end + 2;
1430 continue;
1431 }
1432 }
1433
1434 if ch.is_ascii_digit() && (pos == 0 || !trimmed.as_bytes()[pos - 1].is_ascii_alphanumeric())
1435 {
1436 let end = trimmed[pos..]
1437 .find(|c: char| !c.is_ascii_digit() && c != '.' && c != '_')
1438 .map_or(trimmed.len(), |e| pos + e);
1439 ui.text(&trimmed[pos..end]).fg(number_color);
1440 pos = end;
1441 continue;
1442 }
1443
1444 if ch.is_ascii_alphabetic() || ch == b'_' {
1445 let end = trimmed[pos..]
1446 .find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
1447 .map_or(trimmed.len(), |e| pos + e);
1448 let word = &trimmed[pos..end];
1449
1450 if end < trimmed.len() && trimmed.as_bytes()[end] == b'!' {
1451 ui.text(&trimmed[pos..end + 1]).fg(macro_color);
1452 pos = end + 1;
1453 } else if end < trimmed.len()
1454 && trimmed.as_bytes()[end] == b'('
1455 && !KEYWORDS.contains(&word)
1456 {
1457 ui.text(word).fg(fn_color);
1458 pos = end;
1459 } else if KEYWORDS.contains(&word) {
1460 ui.text(word).fg(keyword_color);
1461 pos = end;
1462 } else {
1463 ui.text(word);
1464 pos = end;
1465 }
1466 continue;
1467 }
1468
1469 let end = trimmed[pos..]
1470 .find(|c: char| c.is_ascii_alphanumeric() || c == '_' || c == '"')
1471 .map_or(trimmed.len(), |e| pos + e);
1472 ui.text(&trimmed[pos..end]);
1473 pos = end;
1474 }
1475}