1use crate::content::{ContentLoader, ContentProvider};
2use crate::fltk_draw_context::FltkDrawContext;
3use crate::fltk_structured_rich_display::FltkStructuredRichDisplay;
4use crate::live_share::HighlightTarget;
5use crate::markdown_converter::document_to_markdown;
6use crate::note_ui::NoteUI;
7use fltk::{app, enums::Color, prelude::*, window};
8use rutle::editor::Editor;
9use rutle::renderer::SearchMatch;
10use rutle::structured_document::BlockType;
11use rutle::tree_path::{DocumentPosition, PathSegment, TreePath};
12use rutle::tree_walk::LeafInfo;
13use std::any::Any;
14use std::collections::{HashMap, HashSet};
15use tdoc::Document;
16
17const ANCHOR_TOP_MARGIN: i32 = 12;
20
21pub struct StructuredRichUI(pub FltkStructuredRichDisplay);
23
24impl StructuredRichUI {
25 pub fn new(x: i32, y: i32, w: i32, h: i32, edit_mode: bool) -> Self {
26 Self(FltkStructuredRichDisplay::new(x, y, w, h, edit_mode))
27 }
28
29 pub fn has_selection(&self) -> bool {
30 self.0.display.borrow().editor().selection().is_some()
31 }
32
33 pub fn highlight_targets(&self) -> Vec<HighlightTarget> {
39 let disp = self.0.display.borrow();
40 let editor = disp.editor();
41 let Some((a, b)) = editor.selection() else {
42 return Vec::new();
43 };
44 let (start, end) = if a <= b { (a, b) } else { (b, a) };
46 selection_targets(editor.document(), &start, &end)
47 }
48
49 pub fn cut_selection(&mut self) -> bool {
52 let doc = self.0.display.borrow().editor().get_selection_document();
53 let Some(doc) = doc else {
54 return false;
55 };
56 crate::clipboard::copy_structured_to_system(&doc);
57 {
58 let mut disp = self.0.display.borrow_mut();
59 let _ = disp.editor_mut().delete_selection();
60 }
61 self.0.notify_change();
62 true
63 }
64
65 pub fn copy_selection(&self) -> bool {
68 let doc = self.0.display.borrow().editor().get_selection_document();
69 match doc {
70 Some(doc) => {
71 crate::clipboard::copy_structured_to_system(&doc);
72 true
73 }
74 None => false,
75 }
76 }
77
78 pub fn paste_from_clipboard(&mut self) {
79 let group = self.0.group.clone();
80 app::paste(&group);
81 }
82
83 pub fn undo(&mut self) -> bool {
84 let changed = {
85 let mut disp = self.0.display.borrow_mut();
86 disp.editor_mut().undo()
87 };
88 if changed {
89 self.0.notify_change();
90 self.0.emit_paragraph_state();
91 }
92 changed
93 }
94
95 pub fn redo(&mut self) -> bool {
96 let changed = {
97 let mut disp = self.0.display.borrow_mut();
98 disp.editor_mut().redo()
99 };
100 if changed {
101 self.0.notify_change();
102 self.0.emit_paragraph_state();
103 }
104 changed
105 }
106
107 pub fn clear_formatting(&mut self) -> bool {
108 self.apply_edit(|editor| editor.clear_formatting())
109 }
110
111 pub fn set_block_type(&mut self, block_type: BlockType) -> bool {
112 self.apply_edit(move |editor| editor.set_block_type(block_type))
113 }
114
115 pub fn toggle_quote(&mut self) -> bool {
116 self.apply_edit(|editor| editor.toggle_quote())
117 }
118
119 pub fn toggle_code_block(&mut self) -> bool {
120 self.apply_edit(|editor| editor.toggle_code_block())
121 }
122
123 pub fn toggle_list(&mut self) -> bool {
124 self.apply_edit(|editor| editor.toggle_list())
125 }
126
127 pub fn toggle_checklist(&mut self) -> bool {
128 self.apply_edit(|editor| editor.toggle_checklist())
129 }
130
131 pub fn toggle_ordered_list(&mut self) -> bool {
132 self.apply_edit(|editor| editor.toggle_ordered_list())
133 }
134
135 pub fn toggle_bold(&mut self) -> bool {
136 self.apply_edit(|editor| editor.toggle_bold())
137 }
138
139 pub fn toggle_italic(&mut self) -> bool {
140 self.apply_edit(|editor| editor.toggle_italic())
141 }
142
143 pub fn toggle_code(&mut self) -> bool {
144 self.apply_edit(|editor| editor.toggle_code())
145 }
146
147 pub fn toggle_strikethrough(&mut self) -> bool {
148 self.apply_edit(|editor| editor.toggle_strikethrough())
149 }
150
151 pub fn toggle_underline(&mut self) -> bool {
152 self.apply_edit(|editor| editor.toggle_underline())
153 }
154
155 pub fn toggle_highlight(&mut self) -> bool {
156 self.apply_edit(|editor| editor.toggle_highlight())
157 }
158
159 pub fn current_block_type(&self) -> Option<BlockType> {
160 let disp = self.0.display.borrow();
161 Some(disp.editor().current_block_type())
162 }
163
164 pub fn current_heading_anchor(&self) -> Option<String> {
169 let disp = self.0.display.borrow();
170 let cursor = disp.editor().cursor();
171 let para = match cursor.path.segments().first() {
174 Some(PathSegment::Paragraph(i)) => *i,
175 _ => return None,
176 };
177 heading_anchor_map(disp.editor().document())
178 .into_iter()
179 .find(|(idx, _)| *idx == para)
180 .map(|(_, anchor)| anchor)
181 }
182
183 pub fn scroll_to_anchor(&mut self, anchor: &str) -> bool {
186 let target = {
187 let disp = self.0.display.borrow();
188 heading_anchor_map(disp.editor().document())
189 .into_iter()
190 .find(|(_, a)| a == anchor)
191 .map(|(idx, _)| idx)
192 };
193 match target {
194 Some(idx) => self.scroll_to_block(idx),
195 None => false,
196 }
197 }
198
199 pub fn scroll_to_block(&mut self, block_index: usize) -> bool {
206 let mut ctx = FltkDrawContext::new(true, true);
207 let mut disp = self.0.display.borrow_mut();
208 if block_index >= disp.editor().document().paragraphs.len() {
209 return false;
210 }
211 disp.editor_mut()
212 .set_cursor(DocumentPosition::new(block_index, 0));
213 disp.ensure_cursor_visible(&mut ctx);
215 if let Some((content_y, _line_h)) = disp.cursor_content_y(&mut ctx) {
216 let max_scroll = (disp.content_height() - disp.h()).max(0);
217 let target = (content_y - ANCHOR_TOP_MARGIN).max(0).min(max_scroll);
218 disp.set_scroll(target);
219 }
220 drop(disp);
221 self.0.group.redraw();
222 true
223 }
224
225 pub fn set_horizontal_padding(&mut self, padding: i32) {
227 self.0.display.borrow_mut().set_horizontal_padding(padding);
228 self.0.group.redraw();
229 }
230
231 pub fn horizontal_padding(&self) -> i32 {
233 self.0.display.borrow().horizontal_padding()
234 }
235
236 pub fn reveal_codes(&self) -> bool {
239 self.0.display.borrow().reveal_codes()
240 }
241
242 pub fn toggle_reveal_codes(&mut self) -> bool {
246 let new_state = !self.0.display.borrow().reveal_codes();
247 self.0.display.borrow_mut().set_reveal_codes(new_state);
248 self.0.group.redraw();
249 new_state
250 }
251
252 pub fn resize(&mut self, x: i32, y: i32, w: i32, h: i32) {
254 self.0.group.resize(x, y, w, h);
255 self.0.group.redraw();
256 }
257
258 pub fn height(&self) -> i32 {
260 self.0.group.height()
261 }
262
263 pub fn width(&self) -> i32 {
265 self.0.group.width()
266 }
267
268 pub fn x(&self) -> i32 {
270 self.0.group.x()
271 }
272
273 pub fn y(&self) -> i32 {
275 self.0.group.y()
276 }
277
278 pub fn search(&mut self, term: &str) -> usize {
282 self.0.display.borrow_mut().search(term)
283 }
284
285 pub fn clear_search(&mut self) {
287 self.0.display.borrow_mut().clear_search();
288 }
289
290 pub fn search_matches(&self) -> Vec<SearchMatch> {
292 self.0.display.borrow().search_matches().to_vec()
293 }
294
295 pub fn search_current_index(&self) -> Option<usize> {
297 self.0.display.borrow().search_current_index()
298 }
299
300 pub fn next_match(&mut self) -> bool {
302 self.0.display.borrow_mut().next_match()
303 }
304
305 pub fn prev_match(&mut self) -> bool {
307 self.0.display.borrow_mut().prev_match()
308 }
309
310 pub fn scroll_to_current_match(&mut self) {
312 let mut ctx = FltkDrawContext::new(true, true);
313 self.0
314 .display
315 .borrow_mut()
316 .scroll_to_current_match(&mut ctx);
317 self.0.group.redraw();
318 }
319
320 pub fn take_focus(&mut self) {
322 let _ = self.0.group.take_focus();
323 }
324
325 fn apply_edit<F>(&mut self, edit: F) -> bool
326 where
327 F: FnOnce(&mut Editor) -> rutle::editor::EditResult,
328 {
329 let result = {
330 let mut disp = self.0.display.borrow_mut();
331 let editor = disp.editor_mut();
332 edit(editor)
333 };
334 if result.is_ok() {
335 self.0.notify_change();
336 self.0.emit_paragraph_state();
337 true
338 } else {
339 false
340 }
341 }
342}
343
344fn selection_targets(
353 doc: &Document,
354 start: &DocumentPosition,
355 end: &DocumentPosition,
356) -> Vec<HighlightTarget> {
357 let leaves = rutle::tree_walk::enumerate_leaves(doc);
358 let li_of = build_li_index(&leaves);
359
360 let mut out = Vec::new();
361 let mut seen = HashSet::new();
362 for leaf in &leaves {
363 if leaf.path < start.path || leaf.path > end.path {
364 continue;
365 }
366 if leaf.path == end.path && end.offset == 0 && start.path != end.path {
369 continue;
370 }
371 let target = leaf_element(&li_of, &leaf.path);
372 if seen.insert(target.clone()) {
373 out.push(target);
374 }
375 }
376 out
377}
378
379fn build_li_index(leaves: &[LeafInfo]) -> HashMap<TreePath, usize> {
384 let mut map = HashMap::new();
385 let mut cur_block = None;
386 let mut counter = 0;
387 for leaf in leaves {
388 let block = match leaf.path.segments().first() {
389 Some(PathSegment::Paragraph(i)) => *i,
390 _ => continue,
391 };
392 if cur_block != Some(block) {
393 cur_block = Some(block);
394 counter = 0;
395 }
396 if leaf.marker.is_some() {
397 map.insert(leaf.path.clone(), counter);
398 counter += 1;
399 }
400 }
401 map
402}
403
404fn leaf_element(li_of: &HashMap<TreePath, usize>, path: &TreePath) -> HighlightTarget {
408 let block = match path.segments().first() {
409 Some(PathSegment::Paragraph(i)) => *i,
410 _ => 0,
411 };
412 let li = enclosing_li_path(path).and_then(|p| li_of.get(&p).copied());
413 HighlightTarget { block, li }
414}
415
416fn enclosing_li_path(path: &TreePath) -> Option<TreePath> {
421 let segs = path.segments();
422 let last_item = segs.iter().rposition(|s| {
423 matches!(
424 s,
425 PathSegment::ListEntry { .. } | PathSegment::ChecklistItem(_)
426 )
427 })?;
428 let mut li_segs = segs[..=last_item].to_vec();
429 if let Some(PathSegment::ListEntry { entry, .. }) = li_segs.last() {
430 let entry = *entry;
431 *li_segs.last_mut().unwrap() = PathSegment::ListEntry { entry, para: 0 };
432 }
433 Some(TreePath(li_segs))
434}
435
436fn heading_anchor_map(doc: &tdoc::Document) -> Vec<(usize, String)> {
442 let mut indices = Vec::new();
443 let mut texts = Vec::new();
444 for i in 0..doc.paragraphs.len() {
445 let path = TreePath::root(i);
446 if matches!(
447 rutle::tree_walk::effective_block_type(doc, &path),
448 BlockType::Heading { .. }
449 ) {
450 texts.push(rutle::tree_walk::leaf_plain_text(doc, &path));
451 indices.push(i);
452 }
453 }
454 indices
455 .into_iter()
456 .zip(crate::section_link::heading_anchors(&texts))
457 .collect()
458}
459
460impl ContentProvider for StructuredRichUI {
461 fn get_content(&self) -> String {
462 let disp = self.0.display.borrow();
463 document_to_markdown(disp.editor().document())
464 }
465}
466
467impl ContentLoader for StructuredRichUI {
468 fn set_content_from_markdown(&mut self, markdown: &str) {
469 let mut disp = self.0.display.borrow_mut();
470 let mut doc = crate::markdown_converter::markdown_to_document(markdown);
472 if doc.paragraphs.is_empty() {
480 doc.add_paragraph(tdoc::Paragraph::new_text());
481 }
482 disp.editor_mut().set_document(doc);
483 disp.set_scroll(0);
484 disp.set_hovered_link(None);
488 drop(disp);
489 self.0.emit_paragraph_state();
490 }
491}
492
493impl NoteUI for StructuredRichUI {
494 fn on_change(&mut self, f: Box<dyn FnMut() + 'static>) {
495 self.0.set_change_callback(Some(f));
496 }
497
498 fn set_readonly(&mut self, readonly: bool) {
499 self.0.display.borrow_mut().set_cursor_visible(!readonly);
500 }
501
502 fn is_readonly(&self) -> bool {
503 !self.0.display.borrow().cursor_visible()
504 }
505
506 fn scroll_pos(&self) -> i32 {
507 self.0.display.borrow().scroll_offset()
508 }
509
510 fn set_scroll_pos(&mut self, pos: i32) {
511 self.0.display.borrow_mut().set_scroll(pos.max(0));
512 }
513
514 fn cursor_pos(&self) -> Option<DocumentPosition> {
515 Some(self.0.display.borrow().editor().cursor())
516 }
517
518 fn set_cursor_pos(&mut self, pos: DocumentPosition) {
519 self.0.display.borrow_mut().editor_mut().set_cursor(pos);
523 }
524
525 fn set_bg_color(&mut self, color: Color) {
526 self.0.group.set_color(color);
527 }
528
529 fn set_resizable(&self, wind: &mut window::Window) {
530 wind.resizable(&self.0.group);
531 }
532
533 fn on_link_click(&mut self, f: Box<dyn Fn(String) + 'static>) {
534 self.0.set_link_callback(Some(f));
535 }
536
537 fn tick(&mut self, ms_since_start: u64) {
538 self.0.tick(ms_since_start);
539 }
540
541 fn on_link_hover(&mut self, f: Box<dyn Fn(Option<String>) + 'static>) {
542 self.0.set_link_hover_callback(Some(f));
543 }
544
545 fn on_paragraph_style_change(&mut self, f: Box<dyn FnMut(BlockType) + 'static>) {
546 self.0.set_paragraph_callback(Some(f));
547 }
548
549 fn as_any(&self) -> &dyn Any {
550 self
551 }
552
553 fn as_any_mut(&mut self) -> &mut dyn Any {
554 self
555 }
556
557 fn hide(&mut self) {
558 self.0.group.hide();
559 }
560
561 fn take_focus(&mut self) {
562 let _ = self.0.group.take_focus();
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 fn target_at(doc: &Document, path: &TreePath) -> HighlightTarget {
572 let leaves = rutle::tree_walk::enumerate_leaves(doc);
573 leaf_element(&build_li_index(&leaves), path)
574 }
575
576 fn item_targets(doc: &Document, block: usize) -> Vec<HighlightTarget> {
580 rutle::tree_walk::enumerate_leaves(doc)
581 .iter()
582 .filter(|l| {
583 l.marker.is_some()
584 && l.path.segments().first() == Some(&PathSegment::Paragraph(block))
585 })
586 .map(|l| target_at(doc, &l.path))
587 .collect()
588 }
589
590 #[test]
591 fn highlight_target_uses_block_for_non_list_leaves() {
592 let md = "# Title\n\nA paragraph\n";
593 let doc = crate::markdown_converter::markdown_to_document(md);
594 let leaves = rutle::tree_walk::enumerate_leaves(&doc);
595 assert_eq!(
597 target_at(&doc, &leaves[0].path),
598 HighlightTarget { block: 0, li: None }
599 );
600 assert_eq!(
601 target_at(&doc, &leaves[1].path),
602 HighlightTarget { block: 1, li: None }
603 );
604 }
605
606 #[test]
607 fn highlight_target_indexes_flat_list_items() {
608 let md = "intro\n\n- one\n- two\n- three\n";
609 let doc = crate::markdown_converter::markdown_to_document(md);
610 assert_eq!(
612 item_targets(&doc, 1),
613 vec![
614 HighlightTarget {
615 block: 1,
616 li: Some(0)
617 },
618 HighlightTarget {
619 block: 1,
620 li: Some(1)
621 },
622 HighlightTarget {
623 block: 1,
624 li: Some(2)
625 },
626 ]
627 );
628 }
629
630 #[test]
631 fn highlight_target_indexes_nested_list_items_in_document_order() {
632 let md = "- a\n - b\n - c\n- d\n";
634 let doc = crate::markdown_converter::markdown_to_document(md);
635 assert_eq!(
636 item_targets(&doc, 0),
637 vec![
638 HighlightTarget {
639 block: 0,
640 li: Some(0)
641 },
642 HighlightTarget {
643 block: 0,
644 li: Some(1)
645 },
646 HighlightTarget {
647 block: 0,
648 li: Some(2)
649 },
650 HighlightTarget {
651 block: 0,
652 li: Some(3)
653 },
654 ]
655 );
656 }
657
658 #[test]
659 fn highlight_target_indexes_checklist_items() {
660 let md = "- [ ] x\n- [x] y\n";
661 let doc = crate::markdown_converter::markdown_to_document(md);
662 assert_eq!(
663 item_targets(&doc, 0),
664 vec![
665 HighlightTarget {
666 block: 0,
667 li: Some(0)
668 },
669 HighlightTarget {
670 block: 0,
671 li: Some(1)
672 },
673 ]
674 );
675 }
676
677 #[test]
678 fn selection_spanning_paragraphs_yields_one_target_each() {
679 let md = "# Title\n\nfirst\n\nsecond\n\nthird\n";
680 let doc = crate::markdown_converter::markdown_to_document(md);
681 let leaves = rutle::tree_walk::enumerate_leaves(&doc);
682 let start = DocumentPosition::at(leaves[1].path.clone(), 0);
684 let end = DocumentPosition::at(leaves[3].path.clone(), 1);
685 assert_eq!(
686 selection_targets(&doc, &start, &end),
687 vec![
688 HighlightTarget { block: 1, li: None },
689 HighlightTarget { block: 2, li: None },
690 HighlightTarget { block: 3, li: None },
691 ]
692 );
693 }
694
695 #[test]
696 fn selection_spanning_list_items_yields_each_item() {
697 let md = "- one\n- two\n- three\n";
698 let doc = crate::markdown_converter::markdown_to_document(md);
699 let leaves = rutle::tree_walk::enumerate_leaves(&doc);
700 let start = DocumentPosition::at(leaves[0].path.clone(), 0);
702 let end = DocumentPosition::at(leaves[1].path.clone(), 1);
703 assert_eq!(
704 selection_targets(&doc, &start, &end),
705 vec![
706 HighlightTarget {
707 block: 0,
708 li: Some(0)
709 },
710 HighlightTarget {
711 block: 0,
712 li: Some(1)
713 },
714 ]
715 );
716 }
717
718 #[test]
719 fn selection_from_paragraph_into_a_list() {
720 let md = "intro\n\n- one\n- two\n";
721 let doc = crate::markdown_converter::markdown_to_document(md);
722 let leaves = rutle::tree_walk::enumerate_leaves(&doc);
723 let start = DocumentPosition::at(leaves[0].path.clone(), 0);
725 let end = DocumentPosition::at(leaves[1].path.clone(), 1);
726 assert_eq!(
727 selection_targets(&doc, &start, &end),
728 vec![
729 HighlightTarget { block: 0, li: None },
730 HighlightTarget {
731 block: 1,
732 li: Some(0)
733 },
734 ]
735 );
736 }
737
738 #[test]
739 fn selection_ending_at_a_block_start_excludes_it() {
740 let md = "one\n\ntwo\n\nthree\n";
741 let doc = crate::markdown_converter::markdown_to_document(md);
742 let leaves = rutle::tree_walk::enumerate_leaves(&doc);
743 let start = DocumentPosition::at(leaves[0].path.clone(), 0);
746 let end = DocumentPosition::at(leaves[2].path.clone(), 0);
747 assert_eq!(
748 selection_targets(&doc, &start, &end),
749 vec![
750 HighlightTarget { block: 0, li: None },
751 HighlightTarget { block: 1, li: None },
752 ]
753 );
754 }
755
756 #[test]
757 fn heading_anchor_map_slugs_and_dedup() {
758 let md = "# Overview\n\nsome text\n\n## Details\n\n## Details\n\n# Overview\n";
761 let doc = crate::markdown_converter::markdown_to_document(md);
762
763 let map = heading_anchor_map(&doc);
764 let slugs: Vec<&str> = map.iter().map(|(_, s)| s.as_str()).collect();
765 assert_eq!(slugs, ["overview", "details", "details-1", "overview-1"]);
766
767 for (idx, _) in &map {
769 assert!(matches!(
770 rutle::tree_walk::effective_block_type(&doc, &TreePath::root(*idx)),
771 BlockType::Heading { .. }
772 ));
773 }
774 }
775
776 #[test]
780 fn block_type_is_a_no_op_on_an_empty_document() {
781 let mut editor = Editor::with_tdoc(tdoc::Document::new());
782 let _ = editor.set_block_type(BlockType::Heading { level: 1 });
783 assert_eq!(editor.current_block_type(), BlockType::Paragraph);
784 }
785
786 #[test]
789 fn seeded_empty_paragraph_accepts_a_heading() {
790 let mut doc = tdoc::Document::new();
791 assert!(doc.paragraphs.is_empty());
792 doc.add_paragraph(tdoc::Paragraph::new_text());
793
794 let mut editor = Editor::with_tdoc(doc);
795 assert!(
796 editor
797 .set_block_type(BlockType::Heading { level: 1 })
798 .is_ok()
799 );
800 assert_eq!(editor.current_block_type(), BlockType::Heading { level: 1 });
801 }
802}