1use super::*;
2use crate::RichLogState;
3
4impl Context {
5 pub fn rich_log(&mut self, state: &mut RichLogState) -> Response {
7 let focused = self.register_focusable();
8 let interaction_id = self.next_interaction_id();
9 let mut response = self.response_for(interaction_id);
10 response.focused = focused;
11
12 let widget_height = if response.rect.height > 0 {
13 response.rect.height as usize
14 } else {
15 self.area_height as usize
16 };
17 let viewport_height = widget_height.saturating_sub(2);
18 let effective_height = if viewport_height == 0 {
19 state.entries.len().max(1)
20 } else {
21 viewport_height
22 };
23 let show_indicator = state.entries.len() > effective_height;
24 let visible_rows = if show_indicator {
25 effective_height.saturating_sub(1).max(1)
26 } else {
27 effective_height
28 };
29 let max_offset = state.entries.len().saturating_sub(visible_rows);
30 if state.auto_scroll && state.scroll_offset == usize::MAX {
31 state.scroll_offset = max_offset;
32 } else {
33 state.scroll_offset = state.scroll_offset.min(max_offset);
34 }
35 let old_offset = state.scroll_offset;
36
37 if focused {
38 let mut consumed_indices = Vec::new();
39 for (i, event) in self.events.iter().enumerate() {
40 if self.consumed[i] {
41 continue;
42 }
43 if let Event::Key(key) = event {
44 if key.kind != KeyEventKind::Press {
45 continue;
46 }
47 match key.code {
48 KeyCode::Up | KeyCode::Char('k') => {
49 state.scroll_offset = state.scroll_offset.saturating_sub(1);
50 consumed_indices.push(i);
51 }
52 KeyCode::Down | KeyCode::Char('j') => {
53 state.scroll_offset = (state.scroll_offset + 1).min(max_offset);
54 consumed_indices.push(i);
55 }
56 KeyCode::PageUp => {
57 state.scroll_offset = state.scroll_offset.saturating_sub(10);
58 consumed_indices.push(i);
59 }
60 KeyCode::PageDown => {
61 state.scroll_offset = (state.scroll_offset + 10).min(max_offset);
62 consumed_indices.push(i);
63 }
64 KeyCode::Home => {
65 state.scroll_offset = 0;
66 consumed_indices.push(i);
67 }
68 KeyCode::End => {
69 state.scroll_offset = max_offset;
70 consumed_indices.push(i);
71 }
72 _ => {}
73 }
74 }
75 }
76 for idx in consumed_indices {
77 self.consumed[idx] = true;
78 }
79 }
80
81 if let Some(rect) = self.prev_hit_map.get(interaction_id).copied() {
82 for (i, event) in self.events.iter().enumerate() {
83 if self.consumed[i] {
84 continue;
85 }
86 if let Event::Mouse(mouse) = event {
87 let in_bounds = mouse.x >= rect.x
88 && mouse.x < rect.right()
89 && mouse.y >= rect.y
90 && mouse.y < rect.bottom();
91 if !in_bounds {
92 continue;
93 }
94 let delta = self.scroll_lines_per_event as usize;
95 match mouse.kind {
96 MouseKind::ScrollUp => {
97 state.scroll_offset = state.scroll_offset.saturating_sub(delta);
98 self.consumed[i] = true;
99 }
100 MouseKind::ScrollDown => {
101 state.scroll_offset = (state.scroll_offset + delta).min(max_offset);
102 self.consumed[i] = true;
103 }
104 _ => {}
105 }
106 }
107 }
108 }
109
110 state.scroll_offset = state.scroll_offset.min(max_offset);
111 let start = state
112 .scroll_offset
113 .min(state.entries.len().saturating_sub(visible_rows));
114 let end = (start + visible_rows).min(state.entries.len());
115
116 self.commands.push(Command::BeginContainer {
117 direction: Direction::Column,
118 gap: 0,
119 align: Align::Start,
120 align_self: None,
121 justify: Justify::Start,
122 border: Some(Border::Single),
123 border_sides: BorderSides::all(),
124 border_style: Style::new().fg(self.theme.border),
125 bg_color: None,
126 padding: Padding::default(),
127 margin: Margin::default(),
128 constraints: Constraints::default(),
129 title: None,
130 grow: 0,
131 group_name: None,
132 });
133
134 for entry in state
135 .entries
136 .iter()
137 .skip(start)
138 .take(end.saturating_sub(start))
139 {
140 self.commands.push(Command::RichText {
141 segments: entry.segments.clone(),
142 wrap: false,
143 align: Align::Start,
144 margin: Margin::default(),
145 constraints: Constraints::default(),
146 });
147 }
148
149 if show_indicator {
150 let end_pos = end.min(state.entries.len());
151 let line = format!(
152 "{}-{} / {}",
153 start.saturating_add(1),
154 end_pos,
155 state.entries.len()
156 );
157 self.styled(line, Style::new().dim().fg(self.theme.text_dim));
158 }
159
160 self.commands.push(Command::EndContainer);
161 self.last_text_idx = None;
162 response.changed = state.scroll_offset != old_offset;
163 response
164 }
165
166 pub fn virtual_list(
173 &mut self,
174 state: &mut ListState,
175 visible_height: usize,
176 f: impl Fn(&mut Context, usize),
177 ) -> Response {
178 if state.items.is_empty() {
179 return Response::none();
180 }
181 state.selected = state.selected.min(state.items.len().saturating_sub(1));
182 let interaction_id = self.next_interaction_id();
183 let focused = self.register_focusable();
184 let old_selected = state.selected;
185
186 if focused {
187 let mut consumed_indices = Vec::new();
188 for (i, event) in self.events.iter().enumerate() {
189 if self.consumed[i] {
190 continue;
191 }
192 if let Event::Key(key) = event {
193 if key.kind != KeyEventKind::Press {
194 continue;
195 }
196 match key.code {
197 KeyCode::Up | KeyCode::Char('k') | KeyCode::Down | KeyCode::Char('j') => {
198 let _ = handle_vertical_nav(
199 &mut state.selected,
200 state.items.len().saturating_sub(1),
201 key.code.clone(),
202 );
203 consumed_indices.push(i);
204 }
205 KeyCode::PageUp => {
206 state.selected = state.selected.saturating_sub(visible_height);
207 consumed_indices.push(i);
208 }
209 KeyCode::PageDown => {
210 state.selected = (state.selected + visible_height)
211 .min(state.items.len().saturating_sub(1));
212 consumed_indices.push(i);
213 }
214 KeyCode::Home => {
215 state.selected = 0;
216 consumed_indices.push(i);
217 }
218 KeyCode::End => {
219 state.selected = state.items.len().saturating_sub(1);
220 consumed_indices.push(i);
221 }
222 _ => {}
223 }
224 }
225 }
226 for idx in consumed_indices {
227 self.consumed[idx] = true;
228 }
229 }
230
231 let start = if state.selected >= visible_height {
232 state.selected - visible_height + 1
233 } else {
234 0
235 };
236 let end = (start + visible_height).min(state.items.len());
237
238 self.commands.push(Command::BeginContainer {
239 direction: Direction::Column,
240 gap: 0,
241 align: Align::Start,
242 align_self: None,
243 justify: Justify::Start,
244 border: None,
245 border_sides: BorderSides::all(),
246 border_style: Style::new().fg(self.theme.border),
247 bg_color: None,
248 padding: Padding::default(),
249 margin: Margin::default(),
250 constraints: Constraints::default(),
251 title: None,
252 grow: 0,
253 group_name: None,
254 });
255
256 if start > 0 {
257 let hidden = start.to_string();
258 let mut line = String::with_capacity(hidden.len() + 10);
259 line.push_str(" ↑ ");
260 line.push_str(&hidden);
261 line.push_str(" more");
262 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
263 }
264
265 for idx in start..end {
266 f(self, idx);
267 }
268
269 let remaining = state.items.len().saturating_sub(end);
270 if remaining > 0 {
271 let hidden = remaining.to_string();
272 let mut line = String::with_capacity(hidden.len() + 10);
273 line.push_str(" ↓ ");
274 line.push_str(&hidden);
275 line.push_str(" more");
276 self.styled(line, Style::new().fg(self.theme.text_dim).dim());
277 }
278
279 self.commands.push(Command::EndContainer);
280 self.last_text_idx = None;
281 let mut response = self.response_for(interaction_id);
282 response.focused = focused;
283 response.changed = state.selected != old_selected;
284 response
285 }
286
287 pub fn command_palette(&mut self, state: &mut CommandPaletteState) -> Response {
291 if !state.open {
292 return Response::none();
293 }
294
295 state.last_selected = None;
296 let interaction_id = self.next_interaction_id();
297
298 let filtered = state.filtered_indices();
299 let sel = state.selected().min(filtered.len().saturating_sub(1));
300 state.set_selected(sel);
301
302 let mut consumed_indices = Vec::new();
303
304 for (i, event) in self.events.iter().enumerate() {
305 if self.consumed[i] {
306 continue;
307 }
308 if let Event::Key(key) = event {
309 if key.kind != KeyEventKind::Press {
310 continue;
311 }
312 match key.code {
313 KeyCode::Esc => {
314 state.open = false;
315 consumed_indices.push(i);
316 }
317 KeyCode::Up => {
318 let s = state.selected();
319 state.set_selected(s.saturating_sub(1));
320 consumed_indices.push(i);
321 }
322 KeyCode::Down => {
323 let s = state.selected();
324 state.set_selected((s + 1).min(filtered.len().saturating_sub(1)));
325 consumed_indices.push(i);
326 }
327 KeyCode::Enter => {
328 if let Some(&cmd_idx) = filtered.get(state.selected()) {
329 state.last_selected = Some(cmd_idx);
330 state.open = false;
331 }
332 consumed_indices.push(i);
333 }
334 KeyCode::Backspace => {
335 if state.cursor > 0 {
336 let byte_idx = byte_index_for_char(&state.input, state.cursor - 1);
337 let end_idx = byte_index_for_char(&state.input, state.cursor);
338 state.input.replace_range(byte_idx..end_idx, "");
339 state.cursor -= 1;
340 state.set_selected(0);
341 }
342 consumed_indices.push(i);
343 }
344 KeyCode::Char(ch) => {
345 let byte_idx = byte_index_for_char(&state.input, state.cursor);
346 state.input.insert(byte_idx, ch);
347 state.cursor += 1;
348 state.set_selected(0);
349 consumed_indices.push(i);
350 }
351 _ => {}
352 }
353 }
354 }
355 for idx in consumed_indices {
356 self.consumed[idx] = true;
357 }
358
359 let filtered = state.filtered_indices();
360
361 let _ = self.modal(|ui| {
362 let primary = ui.theme.primary;
363 let _ = ui
364 .container()
365 .border(Border::Rounded)
366 .border_style(Style::new().fg(primary))
367 .pad(1)
368 .max_w(60)
369 .col(|ui| {
370 let border_color = ui.theme.primary;
371 let _ = ui
372 .bordered(Border::Rounded)
373 .border_style(Style::new().fg(border_color))
374 .px(1)
375 .col(|ui| {
376 let display = if state.input.is_empty() {
377 "Type to search...".to_string()
378 } else {
379 state.input.clone()
380 };
381 let style = if state.input.is_empty() {
382 Style::new().dim().fg(ui.theme.text_dim)
383 } else {
384 Style::new().fg(ui.theme.text)
385 };
386 ui.styled(display, style);
387 });
388
389 for (list_idx, &cmd_idx) in filtered.iter().enumerate() {
390 let cmd = &state.commands[cmd_idx];
391 let is_selected = list_idx == state.selected();
392 let style = if is_selected {
393 Style::new().bold().fg(ui.theme.primary)
394 } else {
395 Style::new().fg(ui.theme.text)
396 };
397 let prefix = if is_selected { "▸ " } else { " " };
398 let shortcut_text = cmd
399 .shortcut
400 .as_deref()
401 .map(|s| {
402 let mut text = String::with_capacity(s.len() + 4);
403 text.push_str(" (");
404 text.push_str(s);
405 text.push(')');
406 text
407 })
408 .unwrap_or_default();
409 let mut line = String::with_capacity(
410 prefix.len() + cmd.label.len() + shortcut_text.len(),
411 );
412 line.push_str(prefix);
413 line.push_str(&cmd.label);
414 line.push_str(&shortcut_text);
415 ui.styled(line, style);
416 if is_selected && !cmd.description.is_empty() {
417 let mut desc = String::with_capacity(4 + cmd.description.len());
418 desc.push_str(" ");
419 desc.push_str(&cmd.description);
420 ui.styled(desc, Style::new().dim().fg(ui.theme.text_dim));
421 }
422 }
423
424 if filtered.is_empty() {
425 ui.styled(
426 " No matching commands",
427 Style::new().dim().fg(ui.theme.text_dim),
428 );
429 }
430 });
431 });
432
433 let mut response = self.response_for(interaction_id);
434 response.changed = state.last_selected.is_some();
435 response
436 }
437
438 pub fn markdown(&mut self, text: &str) -> Response {
448 self.commands.push(Command::BeginContainer {
449 direction: Direction::Column,
450 gap: 0,
451 align: Align::Start,
452 align_self: None,
453 justify: Justify::Start,
454 border: None,
455 border_sides: BorderSides::all(),
456 border_style: Style::new().fg(self.theme.border),
457 bg_color: None,
458 padding: Padding::default(),
459 margin: Margin::default(),
460 constraints: Constraints::default(),
461 title: None,
462 grow: 0,
463 group_name: None,
464 });
465 self.interaction_count += 1;
466
467 let text_style = Style::new().fg(self.theme.text);
468 let bold_style = Style::new().fg(self.theme.text).bold();
469 let code_style = Style::new().fg(self.theme.accent);
470 let border_style = Style::new().fg(self.theme.border).dim();
471
472 let mut in_code_block = false;
473 let mut code_block_lang = String::new();
474 let mut code_block_lines: Vec<String> = Vec::new();
475 let mut table_lines: Vec<String> = Vec::new();
476
477 for line in text.lines() {
478 let trimmed = line.trim();
479
480 if in_code_block {
481 if trimmed.starts_with("```") {
482 in_code_block = false;
483 let code_content = code_block_lines.join("\n");
484 let theme = self.theme;
485 let highlighted: Option<Vec<Vec<(String, Style)>>> =
486 crate::syntax::highlight_code(&code_content, &code_block_lang, &theme);
487 let _ = self.container().bg(theme.surface).p(1).col(|ui| {
488 if let Some(ref hl_lines) = highlighted {
489 for segs in hl_lines {
490 if segs.is_empty() {
491 ui.text(" ");
492 } else {
493 ui.line(|ui| {
494 for (t, s) in segs {
495 ui.styled(t, *s);
496 }
497 });
498 }
499 }
500 } else {
501 for cl in &code_block_lines {
502 ui.styled(cl, code_style);
503 }
504 }
505 });
506 code_block_lang.clear();
507 code_block_lines.clear();
508 } else {
509 code_block_lines.push(line.to_string());
510 }
511 continue;
512 }
513
514 if trimmed.starts_with('|') && trimmed.matches('|').count() >= 2 {
516 table_lines.push(trimmed.to_string());
517 continue;
518 }
519 if !table_lines.is_empty() {
521 self.render_markdown_table(
522 &table_lines,
523 text_style,
524 bold_style,
525 code_style,
526 border_style,
527 );
528 table_lines.clear();
529 }
530
531 if trimmed.is_empty() {
532 self.text(" ");
533 continue;
534 }
535 if trimmed == "---" || trimmed == "***" || trimmed == "___" {
536 self.styled("─".repeat(40), border_style);
537 continue;
538 }
539 if let Some(quote) = trimmed.strip_prefix("> ") {
540 let quote_style = Style::new().fg(self.theme.text_dim).italic();
541 let bar_style = Style::new().fg(self.theme.border);
542 self.line(|ui| {
543 ui.styled("│ ", bar_style);
544 ui.styled(quote, quote_style);
545 });
546 } else if let Some(heading) = trimmed.strip_prefix("### ") {
547 self.styled(heading, Style::new().bold().fg(self.theme.accent));
548 } else if let Some(heading) = trimmed.strip_prefix("## ") {
549 self.styled(heading, Style::new().bold().fg(self.theme.secondary));
550 } else if let Some(heading) = trimmed.strip_prefix("# ") {
551 self.styled(heading, Style::new().bold().fg(self.theme.primary));
552 } else if let Some(item) = trimmed
553 .strip_prefix("- ")
554 .or_else(|| trimmed.strip_prefix("* "))
555 {
556 self.line_wrap(|ui| {
557 ui.styled(" • ", text_style);
558 Self::render_md_inline_into(ui, item, text_style, bold_style, code_style);
559 });
560 } else if trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(". ") {
561 let parts: Vec<&str> = trimmed.splitn(2, ". ").collect();
562 if parts.len() == 2 {
563 self.line_wrap(|ui| {
564 let mut prefix = String::with_capacity(4 + parts[0].len());
565 prefix.push_str(" ");
566 prefix.push_str(parts[0]);
567 prefix.push_str(". ");
568 ui.styled(prefix, text_style);
569 Self::render_md_inline_into(
570 ui, parts[1], text_style, bold_style, code_style,
571 );
572 });
573 } else {
574 self.text(trimmed);
575 }
576 } else if let Some(lang) = trimmed.strip_prefix("```") {
577 in_code_block = true;
578 code_block_lang = lang.trim().to_string();
579 } else {
580 self.render_md_inline(trimmed, text_style, bold_style, code_style);
581 }
582 }
583
584 if in_code_block && !code_block_lines.is_empty() {
585 for cl in &code_block_lines {
586 self.styled(cl, code_style);
587 }
588 }
589
590 if !table_lines.is_empty() {
592 self.render_markdown_table(
593 &table_lines,
594 text_style,
595 bold_style,
596 code_style,
597 border_style,
598 );
599 }
600
601 self.commands.push(Command::EndContainer);
602 self.last_text_idx = None;
603 Response::none()
604 }
605
606 fn render_markdown_table(
608 &mut self,
609 lines: &[String],
610 text_style: Style,
611 bold_style: Style,
612 code_style: Style,
613 border_style: Style,
614 ) {
615 if lines.is_empty() {
616 return;
617 }
618
619 let is_separator = |line: &str| -> bool {
621 let inner = line.trim_matches('|').trim();
622 !inner.is_empty()
623 && inner
624 .chars()
625 .all(|c| c == '-' || c == ':' || c == '|' || c == ' ')
626 };
627
628 let parse_row = |line: &str| -> Vec<String> {
629 let trimmed = line.trim().trim_start_matches('|').trim_end_matches('|');
630 trimmed.split('|').map(|c| c.trim().to_string()).collect()
631 };
632
633 let mut header: Option<Vec<String>> = None;
634 let mut data_rows: Vec<Vec<String>> = Vec::new();
635 let mut found_separator = false;
636
637 for (i, line) in lines.iter().enumerate() {
638 if is_separator(line) {
639 found_separator = true;
640 continue;
641 }
642 if i == 0 && !found_separator {
643 header = Some(parse_row(line));
644 } else {
645 data_rows.push(parse_row(line));
646 }
647 }
648
649 if !found_separator && header.is_none() && !data_rows.is_empty() {
651 header = Some(data_rows.remove(0));
652 }
653
654 let all_rows: Vec<&Vec<String>> = header.iter().chain(data_rows.iter()).collect();
656 let col_count = all_rows.iter().map(|r| r.len()).max().unwrap_or(0);
657 if col_count == 0 {
658 return;
659 }
660 let mut col_widths = vec![0usize; col_count];
661 let stripped_rows: Vec<Vec<String>> = all_rows
663 .iter()
664 .map(|row| row.iter().map(|c| Self::md_strip(c)).collect())
665 .collect();
666 for row in &stripped_rows {
667 for (i, cell) in row.iter().enumerate() {
668 if i < col_count {
669 col_widths[i] = col_widths[i].max(UnicodeWidthStr::width(cell.as_str()));
670 }
671 }
672 }
673
674 let mut top = String::from("┌");
676 for (i, &w) in col_widths.iter().enumerate() {
677 for _ in 0..w + 2 {
678 top.push('─');
679 }
680 top.push(if i < col_count - 1 { '┬' } else { '┐' });
681 }
682 self.styled(&top, border_style);
683
684 if let Some(ref hdr) = header {
686 self.line(|ui| {
687 ui.styled("│", border_style);
688 for (i, w) in col_widths.iter().enumerate() {
689 let raw = hdr.get(i).map(String::as_str).unwrap_or("");
690 let display_text = Self::md_strip(raw);
691 let cell_w = UnicodeWidthStr::width(display_text.as_str());
692 let padding: String = " ".repeat(w.saturating_sub(cell_w));
693 ui.styled(" ", bold_style);
694 ui.styled(&display_text, bold_style);
695 ui.styled(padding, bold_style);
696 ui.styled(" │", border_style);
697 }
698 });
699
700 let mut sep = String::from("├");
702 for (i, &w) in col_widths.iter().enumerate() {
703 for _ in 0..w + 2 {
704 sep.push('─');
705 }
706 sep.push(if i < col_count - 1 { '┼' } else { '┤' });
707 }
708 self.styled(&sep, border_style);
709 }
710
711 for row in &data_rows {
713 self.line(|ui| {
714 ui.styled("│", border_style);
715 for (i, w) in col_widths.iter().enumerate() {
716 let raw = row.get(i).map(String::as_str).unwrap_or("");
717 let display_text = Self::md_strip(raw);
718 let cell_w = UnicodeWidthStr::width(display_text.as_str());
719 let padding: String = " ".repeat(w.saturating_sub(cell_w));
720 ui.styled(" ", text_style);
721 Self::render_md_inline_into(ui, raw, text_style, bold_style, code_style);
722 ui.styled(padding, text_style);
723 ui.styled(" │", border_style);
724 }
725 });
726 }
727
728 let mut bot = String::from("└");
730 for (i, &w) in col_widths.iter().enumerate() {
731 for _ in 0..w + 2 {
732 bot.push('─');
733 }
734 bot.push(if i < col_count - 1 { '┴' } else { '┘' });
735 }
736 self.styled(&bot, border_style);
737 }
738
739 pub(crate) fn parse_inline_segments(
740 text: &str,
741 base: Style,
742 bold: Style,
743 code: Style,
744 ) -> Vec<(String, Style)> {
745 let mut segments: Vec<(String, Style)> = Vec::new();
746 let mut current = String::new();
747 let chars: Vec<char> = text.chars().collect();
748 let mut i = 0;
749 while i < chars.len() {
750 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
751 let rest: String = chars[i + 2..].iter().collect();
752 if let Some(end) = rest.find("**") {
753 if !current.is_empty() {
754 segments.push((std::mem::take(&mut current), base));
755 }
756 let inner: String = rest[..end].to_string();
757 let char_count = inner.chars().count();
758 segments.push((inner, bold));
759 i += 2 + char_count + 2;
760 continue;
761 }
762 }
763 if chars[i] == '*'
764 && (i + 1 >= chars.len() || chars[i + 1] != '*')
765 && (i == 0 || chars[i - 1] != '*')
766 {
767 let rest: String = chars[i + 1..].iter().collect();
768 if let Some(end) = rest.find('*') {
769 if !current.is_empty() {
770 segments.push((std::mem::take(&mut current), base));
771 }
772 let inner: String = rest[..end].to_string();
773 let char_count = inner.chars().count();
774 segments.push((inner, base.italic()));
775 i += 1 + char_count + 1;
776 continue;
777 }
778 }
779 if chars[i] == '`' {
780 let rest: String = chars[i + 1..].iter().collect();
781 if let Some(end) = rest.find('`') {
782 if !current.is_empty() {
783 segments.push((std::mem::take(&mut current), base));
784 }
785 let inner: String = rest[..end].to_string();
786 let char_count = inner.chars().count();
787 segments.push((inner, code));
788 i += 1 + char_count + 1;
789 continue;
790 }
791 }
792 current.push(chars[i]);
793 i += 1;
794 }
795 if !current.is_empty() {
796 segments.push((current, base));
797 }
798 segments
799 }
800
801 fn render_md_inline(
806 &mut self,
807 text: &str,
808 text_style: Style,
809 bold_style: Style,
810 code_style: Style,
811 ) {
812 let items = Self::split_md_links(text);
813
814 if items.len() == 1 {
816 if let MdInline::Text(ref t) = items[0] {
817 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
818 if segs.len() <= 1 {
819 self.text(text)
820 .wrap()
821 .fg(text_style.fg.unwrap_or(Color::Reset));
822 } else {
823 self.line_wrap(|ui| {
824 for (s, st) in segs {
825 ui.styled(s, st);
826 }
827 });
828 }
829 return;
830 }
831 }
832
833 self.line_wrap(|ui| {
835 for item in &items {
836 match item {
837 MdInline::Text(t) => {
838 let segs =
839 Self::parse_inline_segments(t, text_style, bold_style, code_style);
840 for (s, st) in segs {
841 ui.styled(s, st);
842 }
843 }
844 MdInline::Link { text, url } => {
845 ui.link(text.clone(), url.clone());
846 }
847 MdInline::Image { alt, .. } => {
848 ui.styled(alt.as_str(), code_style);
850 }
851 }
852 }
853 });
854 }
855
856 fn render_md_inline_into(
862 ui: &mut Context,
863 text: &str,
864 text_style: Style,
865 bold_style: Style,
866 code_style: Style,
867 ) {
868 let items = Self::split_md_links(text);
869 for item in &items {
870 match item {
871 MdInline::Text(t) => {
872 let segs = Self::parse_inline_segments(t, text_style, bold_style, code_style);
873 for (s, st) in segs {
874 ui.styled(s, st);
875 }
876 }
877 MdInline::Link { text, url } => {
878 ui.link(text.clone(), url.clone());
879 }
880 MdInline::Image { alt, .. } => {
881 ui.styled(alt.as_str(), code_style);
882 }
883 }
884 }
885 }
886
887 fn split_md_links(text: &str) -> Vec<MdInline> {
889 let chars: Vec<char> = text.chars().collect();
890 let mut items: Vec<MdInline> = Vec::new();
891 let mut current = String::new();
892 let mut i = 0;
893
894 while i < chars.len() {
895 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
897 if let Some((alt, _url, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
898 if !current.is_empty() {
899 items.push(MdInline::Text(std::mem::take(&mut current)));
900 }
901 items.push(MdInline::Image { alt });
902 i += 1 + consumed;
903 continue;
904 }
905 }
906 if chars[i] == '[' {
908 if let Some((link_text, url, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
909 if !current.is_empty() {
910 items.push(MdInline::Text(std::mem::take(&mut current)));
911 }
912 items.push(MdInline::Link {
913 text: link_text,
914 url,
915 });
916 i += consumed;
917 continue;
918 }
919 }
920 current.push(chars[i]);
921 i += 1;
922 }
923 if !current.is_empty() {
924 items.push(MdInline::Text(current));
925 }
926 if items.is_empty() {
927 items.push(MdInline::Text(String::new()));
928 }
929 items
930 }
931
932 fn parse_md_bracket_paren(chars: &[char], start: usize) -> Option<(String, String, usize)> {
935 if start >= chars.len() || chars[start] != '[' {
936 return None;
937 }
938 let mut depth = 0i32;
940 let mut bracket_end = None;
941 for (j, &ch) in chars.iter().enumerate().skip(start) {
942 if ch == '[' {
943 depth += 1;
944 } else if ch == ']' {
945 depth -= 1;
946 if depth == 0 {
947 bracket_end = Some(j);
948 break;
949 }
950 }
951 }
952 let bracket_end = bracket_end?;
953 if bracket_end + 1 >= chars.len() || chars[bracket_end + 1] != '(' {
955 return None;
956 }
957 let paren_start = bracket_end + 2;
959 let mut paren_end = None;
960 let mut paren_depth = 1i32;
961 for (j, &ch) in chars.iter().enumerate().skip(paren_start) {
962 if ch == '(' {
963 paren_depth += 1;
964 } else if ch == ')' {
965 paren_depth -= 1;
966 if paren_depth == 0 {
967 paren_end = Some(j);
968 break;
969 }
970 }
971 }
972 let paren_end = paren_end?;
973 let text: String = chars[start + 1..bracket_end].iter().collect();
974 let url: String = chars[paren_start..paren_end].iter().collect();
975 let consumed = paren_end - start + 1;
976 Some((text, url, consumed))
977 }
978
979 fn md_strip(text: &str) -> String {
984 let mut result = String::with_capacity(text.len());
985 let chars: Vec<char> = text.chars().collect();
986 let mut i = 0;
987 while i < chars.len() {
988 if chars[i] == '!' && i + 1 < chars.len() && chars[i + 1] == '[' {
990 if let Some((alt, _, consumed)) = Self::parse_md_bracket_paren(&chars, i + 1) {
991 result.push_str(&alt);
992 i += 1 + consumed;
993 continue;
994 }
995 }
996 if chars[i] == '[' {
998 if let Some((link_text, _, consumed)) = Self::parse_md_bracket_paren(&chars, i) {
999 result.push_str(&link_text);
1000 i += consumed;
1001 continue;
1002 }
1003 }
1004 if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == '*' {
1006 let rest: String = chars[i + 2..].iter().collect();
1007 if let Some(end) = rest.find("**") {
1008 let inner = &rest[..end];
1009 result.push_str(inner);
1010 i += 2 + inner.chars().count() + 2;
1011 continue;
1012 }
1013 }
1014 if chars[i] == '*'
1016 && (i + 1 >= chars.len() || chars[i + 1] != '*')
1017 && (i == 0 || chars[i - 1] != '*')
1018 {
1019 let rest: String = chars[i + 1..].iter().collect();
1020 if let Some(end) = rest.find('*') {
1021 let inner = &rest[..end];
1022 result.push_str(inner);
1023 i += 1 + inner.chars().count() + 1;
1024 continue;
1025 }
1026 }
1027 if chars[i] == '`' {
1029 let rest: String = chars[i + 1..].iter().collect();
1030 if let Some(end) = rest.find('`') {
1031 let inner = &rest[..end];
1032 result.push_str(inner);
1033 i += 1 + inner.chars().count() + 1;
1034 continue;
1035 }
1036 }
1037 result.push(chars[i]);
1038 i += 1;
1039 }
1040 result
1041 }
1042
1043 pub fn key_seq(&self, seq: &str) -> bool {
1050 if seq.is_empty() {
1051 return false;
1052 }
1053 if (self.modal_active || self.prev_modal_active) && self.overlay_depth == 0 {
1054 return false;
1055 }
1056 let target: Vec<char> = seq.chars().collect();
1057 let mut matched = 0;
1058 for (i, event) in self.events.iter().enumerate() {
1059 if self.consumed[i] {
1060 continue;
1061 }
1062 if let Event::Key(key) = event {
1063 if key.kind != KeyEventKind::Press {
1064 continue;
1065 }
1066 if let KeyCode::Char(c) = key.code {
1067 if c == target[matched] {
1068 matched += 1;
1069 if matched == target.len() {
1070 return true;
1071 }
1072 } else {
1073 matched = 0;
1074 if c == target[0] {
1075 matched = 1;
1076 }
1077 }
1078 }
1079 }
1080 }
1081 false
1082 }
1083}