1mod dialog;
20mod edit;
21mod menu;
22mod source;
23mod toolbar;
24mod view;
25
26pub use view::{MarkdownEditor, markdown_editor};
27
28use crate::doc::{Block, BlockKind, ColumnAlign, Container, Document, Leaf, Pos, Selection, Style};
29use crate::extension::{BlockRenderer, Frame, Rectangle, RenderRequest};
30use crate::features::Features;
31use crate::fonts::Fonts;
32use crate::highlight::{self, COMMON_LANGUAGES, CodeHighlighter, HighlightRequest};
33use crate::i18n::Language;
34use crate::images::{
35 self, ImageCache, ImageId, ImageLoader, ImageRejection, ImageSettings, ImageSource, PastedImage,
36};
37use crate::layout::{Layout, Lookups, compute as compute_layout};
38use crate::markdown;
39use crate::page::Paper;
40use crate::style::EditorStyle;
41use crate::zoom::ZoomSettings;
42use iced::keyboard::{Key, Modifiers};
43use iced::mouse::ScrollDelta;
44use iced::widget::canvas;
45use iced::{Color, Point, Size, Subscription, Task};
46use std::cell::{Cell, RefCell};
47use std::collections::HashMap;
48use std::collections::hash_map::DefaultHasher;
49use std::hash::{Hash, Hasher};
50use std::path::PathBuf;
51use std::sync::Arc;
52use std::time::Duration;
53use web_time::Instant;
55
56pub(crate) const BLINK: Duration = Duration::from_millis(530);
57
58pub const DEFAULT_TAB_WIDTH: usize = 4;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Mode {
64 Wysiwyg,
66 Source,
68}
69
70#[derive(Debug, Clone)]
74pub struct Action(pub(crate) Inner);
75
76impl From<Command> for Action {
77 fn from(command: Command) -> Self {
78 Action(Inner::Command(command))
79 }
80}
81
82#[derive(Debug, Clone)]
83pub(crate) enum Inner {
84 Key(Key, Modifiers, Option<String>),
85 Modifiers(Modifiers),
86 Press {
88 x: f32,
89 y: f32,
90 clicks: u8,
91 },
92 Drag {
94 x: f32,
95 y: f32,
96 },
97 PressOutside,
98 ContextMenu {
101 at: Point,
102 x: f32,
103 y: f32,
104 },
105 Blink,
106 ScrollBy {
108 dx: f32,
109 dy: f32,
110 },
111 ScrollTo {
113 x: Option<f32>,
114 y: Option<f32>,
115 },
116 Wheel(ScrollDelta),
118 Command(Command),
119 LinkUrl(String),
120 LinkDialog,
122 LinkText(String),
124 LinkTab,
126 LinkApply,
127 LinkRemove,
128 LinkCancel,
129 Paste(Option<String>),
130 FileDropped(PathBuf),
132}
133
134#[derive(Debug, Clone, PartialEq)]
136pub enum Command {
137 Undo,
138 Redo,
139 ToggleBold,
140 ToggleItalic,
141 ToggleStrike,
142 ToggleCode,
143 SetBlock(BlockKind),
146 ToggleChecked,
148 SetCodeLanguage(Option<String>),
150 InsertRule,
151 InsertTable {
153 rows: usize,
154 columns: usize,
155 },
156 InsertRow {
158 below: bool,
159 },
160 InsertColumn {
162 right: bool,
163 },
164 DeleteRow,
166 DeleteColumn,
168 DeleteTable,
170 SetColumnAlign(ColumnAlign),
172 EditLink,
174 InsertText(String),
176 Indent,
178 Outdent,
180 SelectAll,
181 Copy,
182 Cut,
183 Paste,
185 InsertImage {
187 bytes: Vec<u8>,
188 name: Option<String>,
190 },
191 InsertImageFile(PathBuf),
193 InsertImageLink {
195 url: String,
196 alt: String,
197 },
198 ResolveImage {
200 id: ImageId,
201 url: String,
202 alt: String,
203 },
204 CancelImage {
206 id: ImageId,
207 },
208 RequestImage,
210 SetMode(Mode),
211 Focus,
212 Blur,
213 ZoomIn,
214 ZoomOut,
215 ResetZoom,
216 SetZoom(f32),
218 FitWidth,
220 FitPage,
222}
223
224#[derive(Debug, Clone, PartialEq)]
226pub enum Event {
227 Changed,
230 SelectionChanged,
232 ModeChanged(Mode),
233 ZoomChanged(f32),
235 Focused,
236 Blurred,
237 LinkClicked(String),
239 ImagePasted(PastedImage),
242 ImageRejected {
244 source: ImageSource,
245 reason: ImageRejection,
246 },
247 ImageRequested,
249}
250
251pub struct Output {
253 pub task: Task<Action>,
255 pub events: Vec<Event>,
257}
258
259#[derive(Debug, Clone, PartialEq)]
261pub struct Formatting {
262 pub bold: bool,
263 pub italic: bool,
264 pub strike: bool,
265 pub code: bool,
266 pub link: Option<String>,
267 pub block: BlockKind,
269 pub list: Option<bool>,
271 pub task: Option<bool>,
273 pub list_depth: usize,
275 pub quote: bool,
277 pub code_language: Option<String>,
279 pub table: bool,
281 pub column_align: Option<ColumnAlign>,
283}
284
285pub(crate) struct Snapshot {
286 pub doc: Document,
287 pub sel: Selection,
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
291pub(crate) enum EditKind {
292 None,
293 Typing,
294 Delete,
295 Other,
296}
297
298pub struct EditorState {
300 pub(crate) doc: Document,
301 pub(crate) sel: Selection,
302 pub(crate) layout: RefCell<Layout>,
303 pub(crate) cache: RefCell<canvas::Cache>,
304 pub(crate) viewport: Cell<Size>,
305 pub(crate) style: EditorStyle,
306 pub(crate) paper: Paper,
307 pub(crate) zoom: f32,
308 pub(crate) zoom_settings: ZoomSettings,
309 pub(crate) tab_width: usize,
310 pub(crate) highlighter: Option<Arc<dyn CodeHighlighter>>,
311 pub(crate) highlight_cache: RefCell<HashMap<u64, Arc<Vec<Color>>>>,
312 pub(crate) features: Features,
313 pub(crate) language: Language,
314 pub(crate) renderers: Vec<Arc<dyn BlockRenderer>>,
315 pub(crate) renderer_languages: HashMap<String, usize>,
317 pub(crate) render_cache: RefCell<HashMap<u64, Option<Size>>>,
319 pub(crate) image_settings: ImageSettings,
320 pub(crate) image_base: Option<PathBuf>,
321 pub(crate) image_loader: Option<ImageLoader>,
322 pub(crate) image_cache: RefCell<ImageCache>,
323 pub(crate) pending_images: HashMap<ImageId, Arc<[u8]>>,
324 pub(crate) next_image_id: u64,
325 pub(crate) embedded: source::Embedded,
327 pub(crate) mode: Mode,
328 pub(crate) focused: bool,
329 pub(crate) caret_on: bool,
330 pub(crate) link_url: Option<String>,
331 pub(crate) link_range: Option<(Pos, Pos)>,
332 pub(crate) link_dialog: Option<dialog::LinkDialog>,
335 pub(crate) context_menu: Option<Point>,
337 pub(crate) fonts: Fonts,
338 pub(crate) pending: Option<Style>,
339 pub(crate) goal_x: Option<f32>,
340 pub(crate) drag_unit: Option<(u8, Pos, Pos)>,
343 pub(crate) undo: Vec<Snapshot>,
344 pub(crate) redo: Vec<Snapshot>,
345 pub(crate) last_edit: EditKind,
346 pub(crate) modifiers: Modifiers,
347 pub(crate) last_activity: Instant,
348 pub(crate) scroll_x: f32,
349 pub(crate) scroll_y: f32,
350 pub(crate) revision: u64,
351 pub(crate) dirty: bool,
352 pub(crate) pending_events: Vec<Event>,
353}
354
355impl Default for EditorState {
356 fn default() -> Self {
357 Self::new()
358 }
359}
360
361impl EditorState {
362 pub fn new() -> Self {
364 Self::with_markdown("")
365 }
366
367 pub fn with_markdown(markdown_text: &str) -> Self {
370 let state = Self {
371 doc: markdown::parse(markdown_text),
372 sel: Selection::caret(Pos::new(0, 0)),
373 layout: RefCell::new(Layout::default()),
374 cache: RefCell::new(canvas::Cache::new()),
375 viewport: Cell::new(Size::new(1000.0, 700.0)),
376 style: EditorStyle::default(),
377 paper: Paper::default(),
378 zoom: 1.0,
379 zoom_settings: ZoomSettings::default(),
380 tab_width: DEFAULT_TAB_WIDTH,
381 highlighter: None,
382 highlight_cache: RefCell::new(HashMap::new()),
383 features: Features::default(),
384 language: Language::default(),
385 renderers: Vec::new(),
386 renderer_languages: HashMap::new(),
387 render_cache: RefCell::new(HashMap::new()),
388 image_settings: ImageSettings::default(),
389 image_base: None,
390 image_loader: None,
391 image_cache: RefCell::new(ImageCache::default()),
392 pending_images: HashMap::new(),
393 next_image_id: 1,
394 embedded: source::Embedded::default(),
395 mode: Mode::Wysiwyg,
396 focused: false,
397 caret_on: true,
398 link_url: None,
399 link_range: None,
400 link_dialog: None,
401 context_menu: None,
402 fonts: Fonts::load(),
403 pending: None,
404 goal_x: None,
405 drag_unit: None,
406 undo: Vec::new(),
407 redo: Vec::new(),
408 last_edit: EditKind::None,
409 modifiers: Modifiers::empty(),
410 last_activity: Instant::now(),
411 scroll_x: 0.0,
412 scroll_y: 0.0,
413 revision: 0,
414 dirty: false,
415 pending_events: Vec::new(),
416 };
417 state.relayout();
418 state
419 }
420
421 pub fn style(mut self, style: EditorStyle) -> Self {
423 self.set_style(style);
424 self
425 }
426
427 pub fn paper(mut self, paper: Paper) -> Self {
429 self.set_paper(paper);
430 self
431 }
432
433 pub fn zoom_settings(mut self, settings: ZoomSettings) -> Self {
435 self.set_zoom_settings(settings);
436 self
437 }
438
439 pub fn zoom(mut self, zoom: f32) -> Self {
441 self.set_zoom(zoom);
442 self
443 }
444
445 pub fn tab_width(mut self, spaces: usize) -> Self {
447 self.set_tab_width(spaces);
448 self
449 }
450
451 pub fn highlighter(mut self, highlighter: impl CodeHighlighter) -> Self {
453 self.set_highlighter(Some(Arc::new(highlighter)));
454 self
455 }
456
457 pub fn features(mut self, features: Features) -> Self {
460 self.set_features(features);
461 self
462 }
463
464 pub fn language(mut self, language: Language) -> Self {
466 self.set_language(language);
467 self
468 }
469
470 pub fn block_renderer(mut self, renderer: impl BlockRenderer) -> Self {
473 let mut renderers = self.renderers.clone();
474 renderers.push(Arc::new(renderer));
475 self.set_block_renderers(renderers);
476 self
477 }
478
479 pub fn images(mut self, settings: ImageSettings) -> Self {
481 self.set_image_settings(settings);
482 self
483 }
484
485 pub fn image_base(mut self, folder: impl Into<PathBuf>) -> Self {
487 self.set_image_base(Some(folder.into()));
488 self
489 }
490
491 pub fn image_loader(
493 mut self,
494 loader: impl Fn(&str) -> Option<Vec<u8>> + Send + Sync + 'static,
495 ) -> Self {
496 self.set_image_loader(Some(Arc::new(loader)));
497 self
498 }
499
500 pub fn mode(mut self, mode: Mode) -> Self {
502 self.set_mode(mode);
503 self
504 }
505
506 pub fn markdown(&self) -> String {
513 match self.mode {
514 Mode::Wysiwyg => markdown::serialize(&images::without_pending(&self.doc)),
515 Mode::Source => self
516 .embedded
517 .expand(&source::text(&images::without_pending(&self.doc))),
518 }
519 }
520
521 pub fn plain_text(&self) -> String {
523 self.doc.plain_text(self.doc.start(), self.doc.end())
524 }
525
526 pub fn selected_text(&self) -> Option<String> {
528 let (a, b) = self.sel.range();
529 (a != b).then(|| self.embedded.expand(&self.doc.plain_text(a, b)))
530 }
531
532 pub fn word_count(&self) -> usize {
533 self.doc.word_count()
534 }
535
536 pub fn current_mode(&self) -> Mode {
537 self.mode
538 }
539
540 pub fn current_style(&self) -> &EditorStyle {
541 &self.style
542 }
543
544 pub fn current_paper(&self) -> &Paper {
545 &self.paper
546 }
547
548 pub fn current_zoom(&self) -> f32 {
550 self.zoom
551 }
552
553 pub fn current_zoom_settings(&self) -> &ZoomSettings {
554 &self.zoom_settings
555 }
556
557 pub fn current_tab_width(&self) -> usize {
559 self.tab_width
560 }
561
562 pub fn current_image_settings(&self) -> &ImageSettings {
563 &self.image_settings
564 }
565
566 pub fn pending_images(&self) -> usize {
568 self.pending_images.len()
569 }
570
571 pub fn has_highlighter(&self) -> bool {
572 self.highlighter.is_some()
573 }
574
575 pub fn current_features(&self) -> Features {
576 self.features
577 }
578
579 pub fn current_language(&self) -> Language {
580 self.language
581 }
582
583 pub fn rendered_languages(&self) -> Vec<String> {
585 self.renderers
586 .iter()
587 .flat_map(|renderer| renderer.languages())
588 .collect()
589 }
590
591 pub fn code_languages(&self) -> Vec<String> {
594 let languages = self
595 .highlighter
596 .as_ref()
597 .map(|highlighter| highlighter.languages())
598 .unwrap_or_default();
599 let mut languages: Vec<String> = if languages.is_empty() {
600 COMMON_LANGUAGES
601 .iter()
602 .map(|name| name.to_string())
603 .collect()
604 } else {
605 languages
606 };
607 for language in self.rendered_languages() {
608 if !languages.contains(&language) {
609 languages.push(language);
610 }
611 }
612 languages
613 }
614
615 pub fn task_progress(&self) -> (usize, usize) {
617 let mut seen = Vec::new();
618 let mut checked = 0;
619 for block in &self.doc.blocks {
620 for container in &block.containers {
621 if let Container::Item {
622 id,
623 task: Some(done),
624 ..
625 } = container
626 {
627 if !seen.contains(id) {
628 seen.push(*id);
629 if *done {
630 checked += 1;
631 }
632 }
633 }
634 }
635 }
636 (checked, seen.len())
637 }
638
639 pub fn page_count(&self) -> usize {
641 self.layout.borrow().pages.len().max(1)
642 }
643
644 pub fn current_page(&self) -> usize {
646 self.layout.borrow().page_of(self.sel.head) + 1
647 }
648
649 pub fn is_focused(&self) -> bool {
650 self.focused
651 }
652
653 pub fn is_dirty(&self) -> bool {
655 self.dirty
656 }
657
658 pub fn can_undo(&self) -> bool {
659 !self.undo.is_empty()
660 }
661
662 pub fn can_redo(&self) -> bool {
663 !self.redo.is_empty()
664 }
665
666 pub fn formatting(&self) -> Formatting {
668 let (a, b) = self.sel.range();
669 let style = if a == b {
670 self.typing_style()
671 } else {
672 self.doc.blocks[a.block]
673 .styles
674 .get(a.offset)
675 .cloned()
676 .unwrap_or_default()
677 };
678 let block = &self.doc.blocks[self.sel.head.block];
679 let leaf_kind = match block.leaf {
680 Leaf::Paragraph | Leaf::Line | Leaf::Cell(_) => BlockKind::Paragraph,
681 Leaf::Heading(level) => BlockKind::Heading(level),
682 Leaf::Code => BlockKind::Code,
683 Leaf::Html => BlockKind::Html,
684 Leaf::Rule => BlockKind::Rule,
685 };
686 let item = block
687 .containers
688 .iter()
689 .rev()
690 .find(|c| matches!(c, Container::Item { .. }));
691 Formatting {
692 bold: style.bold,
693 italic: style.italic,
694 strike: style.strike,
695 code: style.code,
696 link: style.link.map(|link| link.url.clone()),
697 block: leaf_kind,
698 list: item.and_then(Container::list).map(|list| list.ordered),
699 task: item.and_then(Container::task),
700 list_depth: block
701 .containers
702 .iter()
703 .filter(|c| c.list().is_some())
704 .count(),
705 quote: block.containers.iter().any(Container::is_quote),
706 code_language: if block.leaf == Leaf::Code {
707 block.language().map(str::to_string)
708 } else {
709 None
710 },
711 table: block.cell().is_some(),
712 column_align: block.cell().map(|cell| cell.align),
713 }
714 }
715
716 pub fn set_markdown(&mut self, markdown_text: &str) {
723 self.doc = match self.mode {
724 Mode::Wysiwyg => markdown::parse_with(markdown_text, self.features),
725 Mode::Source => source::document(&self.embedded.shorten(markdown_text)),
726 };
727 self.sel = Selection::caret(Pos::new(0, 0));
728 self.undo.clear();
729 self.redo.clear();
730 self.pending_images.clear();
731 self.reset_transient();
732 self.dirty = false;
733 self.revision += 1;
734 self.scroll_x = 0.0;
735 self.scroll_y = 0.0;
736 self.relayout();
737 }
738
739 pub fn mark_saved(&mut self) {
741 self.dirty = false;
742 }
743
744 pub fn set_style(&mut self, style: EditorStyle) {
745 self.style = style;
746 self.relayout();
747 }
748
749 pub fn set_paper(&mut self, paper: Paper) {
750 self.paper = paper;
751 self.relayout();
752 self.clamp_scroll();
753 }
754
755 pub fn set_zoom_settings(&mut self, settings: ZoomSettings) {
756 self.zoom_settings = settings;
757 self.zoom = self.zoom_settings.clamp(self.zoom);
758 self.relayout();
759 self.clamp_scroll();
760 }
761
762 pub fn set_zoom(&mut self, zoom: f32) {
764 self.zoom = self.zoom_settings.clamp(zoom);
765 self.relayout();
766 self.clamp_scroll();
767 }
768
769 pub fn set_tab_width(&mut self, spaces: usize) {
771 self.tab_width = spaces.min(16);
772 }
773
774 pub fn set_highlighter(&mut self, highlighter: Option<Arc<dyn CodeHighlighter>>) {
775 self.highlighter = highlighter;
776 self.highlight_cache.borrow_mut().clear();
777 self.relayout();
778 }
779
780 pub fn set_language(&mut self, language: Language) {
783 self.language = language;
784 }
785
786 pub fn set_features(&mut self, features: Features) {
790 if features == self.features {
791 return;
792 }
793 self.features = features;
794 if self.mode == Mode::Wysiwyg {
795 let text = markdown::serialize(&self.doc);
796 self.doc = markdown::parse_with(&text, features);
797 self.sel = Selection {
798 anchor: self.doc.clamp(self.sel.anchor),
799 head: self.doc.clamp(self.sel.head),
800 };
801 self.undo.clear();
802 self.redo.clear();
803 self.reset_transient();
804 }
805 self.relayout();
806 }
807
808 pub fn set_block_renderers(&mut self, renderers: Vec<Arc<dyn BlockRenderer>>) {
811 let mut languages = HashMap::new();
812 for (index, renderer) in renderers.iter().enumerate() {
813 for language in renderer.languages() {
814 languages.entry(language).or_insert(index);
815 }
816 }
817 self.renderers = renderers;
818 self.renderer_languages = languages;
819 self.render_cache.borrow_mut().clear();
820 self.relayout();
821 }
822
823 pub fn set_image_settings(&mut self, settings: ImageSettings) {
824 self.image_settings = settings;
825 }
826
827 pub fn set_image_base(&mut self, folder: Option<PathBuf>) {
828 self.image_base = folder;
829 self.reload_images();
830 }
831
832 pub fn set_image_loader(&mut self, loader: Option<ImageLoader>) {
833 self.image_loader = loader;
834 self.reload_images();
835 }
836
837 pub fn reload_images(&mut self) {
839 self.image_cache.borrow_mut().clear();
840 self.relayout();
841 }
842
843 pub fn set_mode(&mut self, mode: Mode) {
846 if mode == self.mode {
847 return;
848 }
849 self.doc = match mode {
850 Mode::Source => {
851 let text = markdown::serialize(&self.doc);
852 let text = self.embedded.shorten(&text);
853 source::document(&text)
854 }
855 Mode::Wysiwyg => {
856 let text = self.embedded.expand(&source::text(&self.doc));
857 markdown::parse_with(&text, self.features)
858 }
859 };
860 self.mode = mode;
861 self.sel = Selection::caret(Pos::new(0, 0));
862 self.undo.clear();
863 self.redo.clear();
864 self.reset_transient();
865 self.highlight_cache.borrow_mut().clear();
866 self.scroll_x = 0.0;
867 self.scroll_y = 0.0;
868 self.relayout();
869 }
870
871 pub fn execute(&mut self, command: Command) -> Output {
873 self.update(Action(Inner::Command(command)))
874 }
875
876 pub fn update(&mut self, action: Action) -> Output {
878 let revision = self.revision;
879 let sel = self.sel;
880 let mode = self.mode;
881 let focused = self.focused;
882 let zoom = self.zoom;
883
884 let task = self.apply(action.0);
885
886 if self.sel.head.block != sel.head.block
888 && (self.renderer_for(sel.head.block).is_some()
889 || self.renderer_for(self.sel.head.block).is_some())
890 {
891 self.relayout();
892 }
893
894 let mut events = std::mem::take(&mut self.pending_events);
895 if self.focused != focused {
896 events.push(if self.focused {
897 Event::Focused
898 } else {
899 Event::Blurred
900 });
901 }
902 if self.mode != mode {
903 events.push(Event::ModeChanged(self.mode));
904 }
905 if (self.zoom - zoom).abs() > f32::EPSILON {
906 events.push(Event::ZoomChanged(self.zoom));
907 }
908 if self.revision != revision {
909 events.push(Event::Changed);
910 }
911 if self.sel != sel {
912 events.push(Event::SelectionChanged);
913 }
914
915 Output { task, events }
916 }
917
918 pub fn subscription(&self) -> Subscription<Action> {
921 let blink = if self.focused {
922 iced::time::every(BLINK).map(|_| Action(Inner::Blink))
923 } else {
924 Subscription::none()
925 };
926 let drops = if self.image_settings.enabled() && self.image_settings.drop {
927 iced::event::listen_with(file_dropped)
928 } else {
929 Subscription::none()
930 };
931 Subscription::batch([blink, drops])
932 }
933
934 pub(crate) fn expand_width(&self) -> usize {
939 if self.tab_width == 0 {
940 DEFAULT_TAB_WIDTH
941 } else {
942 self.tab_width
943 }
944 }
945
946 pub(crate) fn new_image_id(&mut self) -> ImageId {
947 let id = ImageId(self.next_image_id);
948 self.next_image_id += 1;
949 id
950 }
951
952 pub(crate) fn code_colors(&self, index: usize) -> Option<Arc<Vec<Color>>> {
954 let highlighter = self.highlighter.as_ref()?;
955 let block = self.doc.blocks.get(index)?;
956 if block.leaf != Leaf::Code || block.chars.is_empty() {
957 return None;
958 }
959 let language = block.language()?;
960 let palette = &self.style.palette;
961
962 let mut hasher = DefaultHasher::new();
963 language.hash(&mut hasher);
964 block.chars.hash(&mut hasher);
965 for channel in [palette.code_background, palette.code] {
966 channel.r.to_bits().hash(&mut hasher);
967 channel.g.to_bits().hash(&mut hasher);
968 channel.b.to_bits().hash(&mut hasher);
969 }
970 let key = hasher.finish();
971
972 if let Some(colors) = self.highlight_cache.borrow().get(&key) {
973 return Some(colors.clone());
974 }
975
976 let code: String = block.chars.iter().collect();
977 let spans = highlighter.highlight(&HighlightRequest {
978 language,
979 code: &code,
980 background: palette.code_background,
981 dark: highlight::is_dark(palette.code_background),
982 });
983
984 let len = block.chars.len();
985 let mut colors = vec![palette.code; len];
986 for span in spans {
987 let start = span.range.start.min(len);
988 let end = span.range.end.min(len);
989 for color in &mut colors[start..end.max(start)] {
990 *color = span.color;
991 }
992 }
993
994 let colors = Arc::new(colors);
995 let mut cache = self.highlight_cache.borrow_mut();
996 if cache.len() > 512 {
997 cache.clear();
998 }
999 cache.insert(key, colors.clone());
1000 Some(colors)
1001 }
1002
1003 pub(crate) fn renderer_for(&self, index: usize) -> Option<&Arc<dyn BlockRenderer>> {
1006 if self.mode != Mode::Wysiwyg || self.renderers.is_empty() {
1007 return None;
1008 }
1009 let block = self.doc.blocks.get(index)?;
1010 if block.leaf != Leaf::Code {
1011 return None;
1012 }
1013 let renderer = *self.renderer_languages.get(block.language()?)?;
1014 self.renderers.get(renderer)
1015 }
1016
1017 fn measure_block(&self, index: usize, width: f32, max_height: f32) -> Option<Size> {
1020 if index == self.sel.head.block {
1021 return None;
1022 }
1023 let renderer = self.renderer_for(index)?;
1024 let block = &self.doc.blocks[index];
1025 let code: String = block.chars.iter().collect();
1026
1027 let mut hasher = DefaultHasher::new();
1028 Arc::as_ptr(renderer).cast::<()>().hash(&mut hasher);
1029 block.info.hash(&mut hasher);
1030 code.hash(&mut hasher);
1031 for value in [width, max_height, self.zoom] {
1032 value.to_bits().hash(&mut hasher);
1033 }
1034 let key = hasher.finish();
1035 if let Some(size) = self.render_cache.borrow().get(&key) {
1036 return *size;
1037 }
1038
1039 let size = renderer.measure(&RenderRequest {
1040 language: block.language().unwrap_or(""),
1041 info: block.info.as_deref().unwrap_or(""),
1042 code: &code,
1043 width,
1044 max_height,
1045 zoom: self.zoom,
1046 style: &self.style,
1047 });
1048 let mut cache = self.render_cache.borrow_mut();
1049 if cache.len() > 512 {
1050 cache.clear();
1051 }
1052 cache.insert(key, size);
1053 size
1054 }
1055
1056 pub(crate) fn draw_block(
1059 &self,
1060 frame: &mut Frame,
1061 index: usize,
1062 bounds: Rectangle,
1063 available: Size,
1064 ) {
1065 let Some(renderer) = self.renderer_for(index) else {
1066 return;
1067 };
1068 let block = &self.doc.blocks[index];
1069 let code: String = block.chars.iter().collect();
1070 let request = RenderRequest {
1071 language: block.language().unwrap_or(""),
1072 info: block.info.as_deref().unwrap_or(""),
1073 code: &code,
1074 width: available.width,
1075 max_height: available.height,
1076 zoom: self.zoom,
1077 style: &self.style,
1078 };
1079 renderer.draw(frame, bounds, &request);
1080 }
1081
1082 pub(crate) fn relayout(&self) {
1083 let cache = &self.image_cache;
1084 let base = self.image_base.as_deref();
1085 let loader = self.image_loader.as_ref();
1086 let lookup = |url: &str| cache.borrow_mut().get(url, base, loader);
1087 let custom = |index: usize, width: f32, max_height: f32| {
1088 self.measure_block(index, width, max_height)
1089 };
1090 let layout = compute_layout(
1091 &self.doc,
1092 &self.fonts,
1093 &self.style,
1094 &self.paper,
1095 self.viewport.get(),
1096 self.zoom,
1097 Lookups {
1098 images: &lookup,
1099 custom: &custom,
1100 },
1101 );
1102 *self.layout.borrow_mut() = layout;
1103 self.cache.borrow_mut().clear();
1104 }
1105
1106 pub(crate) fn sync_viewport(&self, size: Size) {
1108 let current = self.viewport.get();
1109 if (current.width - size.width).abs() > 0.5 || (current.height - size.height).abs() > 0.5 {
1110 self.viewport.set(size);
1111 self.relayout();
1112 }
1113 }
1114
1115 pub(crate) fn max_scroll(&self) -> (f32, f32) {
1117 let layout = self.layout.borrow();
1118 let viewport = self.viewport.get();
1119 (
1120 (layout.width - viewport.width).max(0.0),
1121 (layout.height - viewport.height).max(0.0),
1122 )
1123 }
1124
1125 pub(crate) fn clamped_scroll(&self) -> (f32, f32) {
1127 let (max_x, max_y) = self.max_scroll();
1128 (
1129 self.scroll_x.clamp(0.0, max_x),
1130 self.scroll_y.clamp(0.0, max_y),
1131 )
1132 }
1133
1134 pub(crate) fn set_scroll(&mut self, x: f32, y: f32) {
1135 let (max_x, max_y) = self.max_scroll();
1136 let x = x.clamp(0.0, max_x);
1137 let y = y.clamp(0.0, max_y);
1138 if (x - self.scroll_x).abs() > 0.01 || (y - self.scroll_y).abs() > 0.01 {
1139 self.scroll_x = x;
1140 self.scroll_y = y;
1141 self.cache.borrow_mut().clear();
1142 }
1143 }
1144
1145 pub(crate) fn clamp_scroll(&mut self) {
1146 let (x, y) = (self.scroll_x, self.scroll_y);
1147 self.set_scroll(x, y);
1148 }
1149
1150 pub(crate) fn reset_transient(&mut self) {
1151 self.pending = None;
1152 self.goal_x = None;
1153 self.last_edit = EditKind::None;
1154 self.link_url = None;
1155 self.link_range = None;
1156 self.link_dialog = None;
1157 self.context_menu = None;
1158 }
1159
1160 pub(crate) fn touch(&mut self) {
1161 self.last_activity = Instant::now();
1162 self.caret_on = true;
1163 }
1164
1165 pub(crate) fn typing_style(&self) -> Style {
1166 match &self.pending {
1167 Some(style) => style.clone(),
1168 None => self.doc.typing_style(self.sel.range().0),
1169 }
1170 }
1171
1172 pub(crate) fn begin_edit(&mut self, kind: EditKind) {
1173 let coalesce =
1174 kind == self.last_edit && matches!(kind, EditKind::Typing | EditKind::Delete);
1175 if !coalesce {
1176 self.undo.push(Snapshot {
1177 doc: self.doc.clone(),
1178 sel: self.sel,
1179 });
1180 if self.undo.len() > 300 {
1181 self.undo.remove(0);
1182 }
1183 }
1184 self.redo.clear();
1185 self.last_edit = kind;
1186 }
1187
1188 pub(crate) fn after_edit(&mut self) -> Task<Action> {
1189 self.dirty = true;
1190 self.revision += 1;
1191 self.goal_x = None;
1192 self.relayout();
1193 self.touch();
1194 self.scroll_to_caret()
1195 }
1196
1197 pub(crate) fn scroll_to_caret(&mut self) -> Task<Action> {
1199 let caret = self.layout.borrow().caret(self.sel.head);
1200 let Some((x, y, height)) = caret else {
1201 return Task::none();
1202 };
1203 let viewport = self.viewport.get();
1204 let margin = 24.0;
1205 let (mut sx, mut sy) = (self.scroll_x, self.scroll_y);
1206
1207 if y < sy + margin {
1208 sy = y - 2.0 * margin;
1209 } else if y + height > sy + viewport.height - margin {
1210 sy = y + height - viewport.height + 2.0 * margin;
1211 }
1212 if !self.paper.is_fill() {
1213 if x < sx + margin {
1214 sx = x - 4.0 * margin;
1215 } else if x > sx + viewport.width - margin {
1216 sx = x - viewport.width + 4.0 * margin;
1217 }
1218 }
1219 self.set_scroll(sx, sy);
1220 Task::none()
1221 }
1222
1223 pub(crate) fn zoom_to(&mut self, zoom: f32) -> Task<Action> {
1225 let zoom = self.zoom_settings.clamp(zoom);
1226 if (zoom - self.zoom).abs() < 1.0e-4 {
1227 return Task::none();
1228 }
1229 let ratio = zoom / self.zoom;
1230 let viewport = self.viewport.get();
1231 let center_x = self.scroll_x + viewport.width / 2.0;
1232 let center_y = self.scroll_y + viewport.height / 2.0;
1233
1234 self.zoom = zoom;
1235 self.relayout();
1236 self.set_scroll(
1237 center_x * ratio - viewport.width / 2.0,
1238 center_y * ratio - viewport.height / 2.0,
1239 );
1240 Task::none()
1241 }
1242
1243 pub(crate) fn block_len(&self, index: usize) -> usize {
1244 self.doc.blocks.get(index).map(Block::len).unwrap_or(0)
1245 }
1246}
1247
1248fn file_dropped(
1250 event: iced::Event,
1251 _status: iced::event::Status,
1252 _window: iced::window::Id,
1253) -> Option<Action> {
1254 match event {
1255 iced::Event::Window(iced::window::Event::FileDropped(path)) => {
1256 Some(Action(Inner::FileDropped(path)))
1257 }
1258 _ => None,
1259 }
1260}