Skip to main content

iced_markdown_editor/widget/
mod.rs

1//! The public widget API.
2//!
3//! * [`EditorState`] owns the document, selection, history, paper, zoom,
4//!   tab width, highlighter, image settings and style. Keep it in your
5//!   application state.
6//! * [`markdown_editor`] builds the widget from a `&EditorState`.
7//! * The widget produces [`Action`]s. Pass them to
8//!   [`EditorState::update`], which returns an [`Output`] with a [`Task`]
9//!   and the [`Event`]s that happened.
10//! * [`EditorState::execute`] runs a [`Command`] programmatically.
11//!
12//! * `edit`: keyboard, mouse and commands.
13//! * `source`: Markdown source mode.
14//! * `view`: the widget (page canvas, link bar, status bar).
15//! * `toolbar`: the built-in toolbar.
16//! * `menu`: the context menu (right mouse button).
17//! * `dialog`: the link dialog opened from the context menu.
18
19mod 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;
53// `std::time::Instant` panics in the browser; `web_time` is std on desktop.
54use web_time::Instant;
55
56pub(crate) const BLINK: Duration = Duration::from_millis(530);
57
58/// Default number of spaces inserted by the Tab key.
59pub const DEFAULT_TAB_WIDTH: usize = 4;
60
61/// Editing mode.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Mode {
64    /// Formatted, Word-like editing.
65    Wysiwyg,
66    /// Raw Markdown text.
67    Source,
68}
69
70/// A message produced by the widget. Opaque: forward it to
71/// [`EditorState::update`]. Create one from a [`Command`] with
72/// `Action::from(command)` to drive the editor from your own widgets.
73#[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 in document coordinates.
87    Press {
88        x: f32,
89        y: f32,
90        clicks: u8,
91    },
92    /// Drag in document coordinates.
93    Drag {
94        x: f32,
95        y: f32,
96    },
97    PressOutside,
98    /// Right press: `at` in widget coordinates, `x` / `y` in document
99    /// coordinates.
100    ContextMenu {
101        at: Point,
102        x: f32,
103        y: f32,
104    },
105    Blink,
106    /// Scroll by a distance in pixels.
107    ScrollBy {
108        dx: f32,
109        dy: f32,
110    },
111    /// Scroll to an absolute offset (scrollbar drag).
112    ScrollTo {
113        x: Option<f32>,
114        y: Option<f32>,
115    },
116    /// Ctrl/Cmd + wheel: zoom.
117    Wheel(ScrollDelta),
118    Command(Command),
119    LinkUrl(String),
120    /// Opens the link dialog for the selection (context menu).
121    LinkDialog,
122    /// Text to display, typed in the link dialog.
123    LinkText(String),
124    /// Tab in the link dialog: the other field.
125    LinkTab,
126    LinkApply,
127    LinkRemove,
128    LinkCancel,
129    Paste(Option<String>),
130    /// A file dropped onto the window.
131    FileDropped(PathBuf),
132}
133
134/// Operations that can be triggered programmatically.
135#[derive(Debug, Clone, PartialEq)]
136pub enum Command {
137    Undo,
138    Redo,
139    ToggleBold,
140    ToggleItalic,
141    ToggleStrike,
142    ToggleCode,
143    /// Changes the paragraph(s) under the selection: block type, list,
144    /// task list or block quote. Applying the current kind again reverts.
145    SetBlock(BlockKind),
146    /// Checks or unchecks the task list item(s) under the selection.
147    ToggleChecked,
148    /// Sets the language of the code block(s) under the selection.
149    SetCodeLanguage(Option<String>),
150    InsertRule,
151    /// Inserts an empty table; `rows` includes the header row.
152    InsertTable {
153        rows: usize,
154        columns: usize,
155    },
156    /// Inserts a row above or below the row at the caret.
157    InsertRow {
158        below: bool,
159    },
160    /// Inserts a column left or right of the column at the caret.
161    InsertColumn {
162        right: bool,
163    },
164    /// Deletes the table row at the caret.
165    DeleteRow,
166    /// Deletes the table column at the caret.
167    DeleteColumn,
168    /// Deletes the table at the caret.
169    DeleteTable,
170    /// Aligns the table column at the caret.
171    SetColumnAlign(ColumnAlign),
172    /// Opens the link bar for the selection (or word under the caret).
173    EditLink,
174    /// Inserts text at the caret, replacing the selection.
175    InsertText(String),
176    /// Same as the Tab key.
177    Indent,
178    /// Same as Shift+Tab.
179    Outdent,
180    SelectAll,
181    Copy,
182    Cut,
183    /// Pastes an image (with the `images` feature, desktop) or text.
184    Paste,
185    /// Inserts an image from its encoded bytes, following the [`ImageSettings`].
186    InsertImage {
187        bytes: Vec<u8>,
188        /// File name, used as description and in [`PastedImage::name`].
189        name: Option<String>,
190    },
191    /// Like [`Command::InsertImage`] with the content of a file (desktop).
192    InsertImageFile(PathBuf),
193    /// Inserts `![alt](url)` for an image that already has a URL.
194    InsertImageLink {
195        url: String,
196        alt: String,
197    },
198    /// Answer to [`Event::ImagePasted`]: the placeholder becomes `![alt](url)`.
199    ResolveImage {
200        id: ImageId,
201        url: String,
202        alt: String,
203    },
204    /// Answer to [`Event::ImagePasted`]: the placeholder is removed.
205    CancelImage {
206        id: ImageId,
207    },
208    /// Emits [`Event::ImageRequested`] (the toolbar's image button).
209    RequestImage,
210    SetMode(Mode),
211    Focus,
212    Blur,
213    ZoomIn,
214    ZoomOut,
215    ResetZoom,
216    /// Sets a zoom factor (1.0 = 100 %).
217    SetZoom(f32),
218    /// Zooms so the page width fits the widget.
219    FitWidth,
220    /// Zooms so a whole page fits the widget.
221    FitPage,
222}
223
224/// Something that happened inside the editor.
225#[derive(Debug, Clone, PartialEq)]
226pub enum Event {
227    /// The document content changed. Call [`EditorState::markdown`] here,
228    /// not in `view`.
229    Changed,
230    /// The caret or selection moved.
231    SelectionChanged,
232    ModeChanged(Mode),
233    /// The zoom factor changed (1.0 = 100 %).
234    ZoomChanged(f32),
235    Focused,
236    Blurred,
237    /// A link was Ctrl/Cmd-clicked.
238    LinkClicked(String),
239    /// An image to store ([`ImageMode::Delegate`](crate::ImageMode::Delegate)).
240    /// Answer with [`Command::ResolveImage`] or [`Command::CancelImage`].
241    ImagePasted(PastedImage),
242    /// An image was not accepted.
243    ImageRejected {
244        source: ImageSource,
245        reason: ImageRejection,
246    },
247    /// The toolbar's image button was pressed. Show a file dialog.
248    ImageRequested,
249}
250
251/// Result of [`EditorState::update`].
252pub struct Output {
253    /// Must be returned from your `update` (mapped to your message type).
254    pub task: Task<Action>,
255    /// Events produced by this update, in order.
256    pub events: Vec<Event>,
257}
258
259/// Formatting at the caret, for building custom toolbars.
260#[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    /// Block type: `Paragraph`, `Heading`, `Code`, `Rule` or `Html`.
268    pub block: BlockKind,
269    /// Innermost list: `Some(true)` numbered, `Some(false)` bullets.
270    pub list: Option<bool>,
271    /// Task state of the innermost list item.
272    pub task: Option<bool>,
273    /// Number of nested lists at the caret.
274    pub list_depth: usize,
275    /// The caret is inside a block quote.
276    pub quote: bool,
277    /// Language of the code block at the caret, if any.
278    pub code_language: Option<String>,
279    /// The caret is inside a table.
280    pub table: bool,
281    /// Alignment of the table column at the caret.
282    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
298/// The editor state. Keep one per editor in your application.
299pub 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    /// Language -> index in `renderers`.
316    pub(crate) renderer_languages: HashMap<String, usize>,
317    /// Measured sizes of rendered blocks.
318    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    /// Long `data:` URLs shortened in source mode.
326    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    /// Open link dialog. The address is in `link_url`, the range in
333    /// `link_range`; without a dialog `link_url` shows the link bar.
334    pub(crate) link_dialog: Option<dialog::LinkDialog>,
335    /// Open context menu: its anchor in widget coordinates.
336    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    /// Word or block picked by a double or triple click. A drag that
341    /// follows extends by whole words or blocks and keeps it selected.
342    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    /// An empty document.
363    pub fn new() -> Self {
364        Self::with_markdown("")
365    }
366
367    /// A document parsed from CommonMark (with the GFM extensions
368    /// strikethrough, task lists and tables; see [`Self::features`]).
369    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    /// Builder: sets the style.
422    pub fn style(mut self, style: EditorStyle) -> Self {
423        self.set_style(style);
424        self
425    }
426
427    /// Builder: sets the paper.
428    pub fn paper(mut self, paper: Paper) -> Self {
429        self.set_paper(paper);
430        self
431    }
432
433    /// Builder: sets the zoom settings.
434    pub fn zoom_settings(mut self, settings: ZoomSettings) -> Self {
435        self.set_zoom_settings(settings);
436        self
437    }
438
439    /// Builder: sets the initial zoom.
440    pub fn zoom(mut self, zoom: f32) -> Self {
441        self.set_zoom(zoom);
442        self
443    }
444
445    /// Builder: number of spaces the Tab key inserts (0 = Tab not handled).
446    pub fn tab_width(mut self, spaces: usize) -> Self {
447        self.set_tab_width(spaces);
448        self
449    }
450
451    /// Builder: installs a code highlighting plugin.
452    pub fn highlighter(mut self, highlighter: impl CodeHighlighter) -> Self {
453        self.set_highlighter(Some(Arc::new(highlighter)));
454        self
455    }
456
457    /// Builder: enables or disables optional Markdown features. The document
458    /// is read again with the new features.
459    pub fn features(mut self, features: Features) -> Self {
460        self.set_features(features);
461        self
462    }
463
464    /// Builder: sets the language of the toolbar, link bar and status bar.
465    pub fn language(mut self, language: Language) -> Self {
466        self.set_language(language);
467        self
468    }
469
470    /// Builder: adds a renderer for fenced code blocks of some languages.
471    /// Call it once per renderer; the first one listing a language wins.
472    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    /// Builder: what happens with pasted, dropped and inserted images.
480    pub fn images(mut self, settings: ImageSettings) -> Self {
481        self.set_image_settings(settings);
482        self
483    }
484
485    /// Builder: folder for relative image URLs (desktop display).
486    pub fn image_base(mut self, folder: impl Into<PathBuf>) -> Self {
487        self.set_image_base(Some(folder.into()));
488        self
489    }
490
491    /// Builder: loads image bytes for display by URL.
492    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    /// Builder: sets the mode.
501    pub fn mode(mut self, mode: Mode) -> Self {
502        self.set_mode(mode);
503        self
504    }
505
506    // ----------------------------------------------------------------
507    // Reading
508    // ----------------------------------------------------------------
509
510    /// The document as Markdown, without image placeholders and with full
511    /// `data:` URLs. Builds the whole text: call it on changes, not in `view`.
512    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    /// The document as plain text.
522    pub fn plain_text(&self) -> String {
523        self.doc.plain_text(self.doc.start(), self.doc.end())
524    }
525
526    /// The selected text, if any.
527    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    /// Current zoom factor (1.0 = 100 %).
549    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    /// Spaces per Tab (0 = Tab not handled).
558    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    /// Images handed to the application and not answered yet.
567    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    /// Languages drawn by block renderers.
584    pub fn rendered_languages(&self) -> Vec<String> {
585        self.renderers
586            .iter()
587            .flat_map(|renderer| renderer.languages())
588            .collect()
589    }
590
591    /// Languages offered for code blocks: from the highlighter (or a common
592    /// list) and the block renderers.
593    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    /// `(checked, total)` task list items.
616    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    /// Number of pages (1 in full-width mode).
640    pub fn page_count(&self) -> usize {
641        self.layout.borrow().pages.len().max(1)
642    }
643
644    /// Page (1-based) that contains the caret.
645    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    /// `true` when the content changed since the last load or save.
654    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    /// Formatting at the caret (or at the start of the selection).
667    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    // ----------------------------------------------------------------
717    // Writing
718    // ----------------------------------------------------------------
719
720    /// Replaces the whole document. Clears history, pending images and the
721    /// dirty flag.
722    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    /// Clears the dirty flag (call after saving).
740    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    /// Sets the zoom factor (1.0 = 100 %), also when zoom is disabled.
763    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    /// Spaces per Tab; `0` leaves Tab to the application.
770    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    /// Sets the language of the toolbar, link bar and status bar. The
781    /// document is not changed.
782    pub fn set_language(&mut self, language: Language) {
783        self.language = language;
784    }
785
786    /// Enables or disables optional Markdown features. In WYSIWYG mode the
787    /// document is read again, as if it had been loaded with the new
788    /// features; the undo history starts again.
789    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    /// Replaces the block renderers. The first renderer listing a language
809    /// draws it.
810    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    /// Forgets loaded images and loads them again.
838    pub fn reload_images(&mut self) {
839        self.image_cache.borrow_mut().clear();
840        self.relayout();
841    }
842
843    /// Switches between WYSIWYG and source mode. The undo history starts
844    /// again after a switch.
845    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    /// Runs a command and reports what happened.
872    pub fn execute(&mut self, command: Command) -> Output {
873        self.update(Action(Inner::Command(command)))
874    }
875
876    /// Handles an [`Action`] produced by the widget.
877    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        // A rendered block shows its code while the caret is in it.
887        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    /// Caret blinking and dropped image files:
919    /// `state.subscription().map(Message::Editor)`.
920    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    // ----------------------------------------------------------------
935    // Internal helpers
936    // ----------------------------------------------------------------
937
938    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    /// Highlight colours for a code block, from the plugin (cached).
953    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    /// The block renderer for block `index`, if it is a code block of one
1004    /// of its languages (WYSIWYG mode only).
1005    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    /// Size of block `index` drawn by its renderer (cached); `None` while
1018    /// the caret is in it or when the renderer declines.
1019    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    /// Draws block `index` with its renderer; `available` is the room it
1057    /// was measured for.
1058    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    /// Called while drawing with the widget size.
1107    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    /// Largest scroll offsets for the current layout and widget size.
1116    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    /// Scroll offsets clamped to the content (used while drawing).
1126    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    /// Scrolls so that the caret is visible.
1198    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    /// Changes the zoom and keeps the visible area roughly in place.
1224    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
1248/// Files dropped onto the window (images are filtered in the editor).
1249fn 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}