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    pub(crate) undo: Vec<Snapshot>,
341    pub(crate) redo: Vec<Snapshot>,
342    pub(crate) last_edit: EditKind,
343    pub(crate) modifiers: Modifiers,
344    pub(crate) last_activity: Instant,
345    pub(crate) scroll_x: f32,
346    pub(crate) scroll_y: f32,
347    pub(crate) revision: u64,
348    pub(crate) dirty: bool,
349    pub(crate) pending_events: Vec<Event>,
350}
351
352impl Default for EditorState {
353    fn default() -> Self {
354        Self::new()
355    }
356}
357
358impl EditorState {
359    /// An empty document.
360    pub fn new() -> Self {
361        Self::with_markdown("")
362    }
363
364    /// A document parsed from CommonMark (with the GFM extensions
365    /// strikethrough, task lists and tables; see [`Self::features`]).
366    pub fn with_markdown(markdown_text: &str) -> Self {
367        let state = Self {
368            doc: markdown::parse(markdown_text),
369            sel: Selection::caret(Pos::new(0, 0)),
370            layout: RefCell::new(Layout::default()),
371            cache: RefCell::new(canvas::Cache::new()),
372            viewport: Cell::new(Size::new(1000.0, 700.0)),
373            style: EditorStyle::default(),
374            paper: Paper::default(),
375            zoom: 1.0,
376            zoom_settings: ZoomSettings::default(),
377            tab_width: DEFAULT_TAB_WIDTH,
378            highlighter: None,
379            highlight_cache: RefCell::new(HashMap::new()),
380            features: Features::default(),
381            language: Language::default(),
382            renderers: Vec::new(),
383            renderer_languages: HashMap::new(),
384            render_cache: RefCell::new(HashMap::new()),
385            image_settings: ImageSettings::default(),
386            image_base: None,
387            image_loader: None,
388            image_cache: RefCell::new(ImageCache::default()),
389            pending_images: HashMap::new(),
390            next_image_id: 1,
391            embedded: source::Embedded::default(),
392            mode: Mode::Wysiwyg,
393            focused: false,
394            caret_on: true,
395            link_url: None,
396            link_range: None,
397            link_dialog: None,
398            context_menu: None,
399            fonts: Fonts::load(),
400            pending: None,
401            goal_x: None,
402            undo: Vec::new(),
403            redo: Vec::new(),
404            last_edit: EditKind::None,
405            modifiers: Modifiers::empty(),
406            last_activity: Instant::now(),
407            scroll_x: 0.0,
408            scroll_y: 0.0,
409            revision: 0,
410            dirty: false,
411            pending_events: Vec::new(),
412        };
413        state.relayout();
414        state
415    }
416
417    /// Builder: sets the style.
418    pub fn style(mut self, style: EditorStyle) -> Self {
419        self.set_style(style);
420        self
421    }
422
423    /// Builder: sets the paper.
424    pub fn paper(mut self, paper: Paper) -> Self {
425        self.set_paper(paper);
426        self
427    }
428
429    /// Builder: sets the zoom settings.
430    pub fn zoom_settings(mut self, settings: ZoomSettings) -> Self {
431        self.set_zoom_settings(settings);
432        self
433    }
434
435    /// Builder: sets the initial zoom.
436    pub fn zoom(mut self, zoom: f32) -> Self {
437        self.set_zoom(zoom);
438        self
439    }
440
441    /// Builder: number of spaces the Tab key inserts (0 = Tab not handled).
442    pub fn tab_width(mut self, spaces: usize) -> Self {
443        self.set_tab_width(spaces);
444        self
445    }
446
447    /// Builder: installs a code highlighting plugin.
448    pub fn highlighter(mut self, highlighter: impl CodeHighlighter) -> Self {
449        self.set_highlighter(Some(Arc::new(highlighter)));
450        self
451    }
452
453    /// Builder: enables or disables optional Markdown features. The document
454    /// is read again with the new features.
455    pub fn features(mut self, features: Features) -> Self {
456        self.set_features(features);
457        self
458    }
459
460    /// Builder: sets the language of the toolbar, link bar and status bar.
461    pub fn language(mut self, language: Language) -> Self {
462        self.set_language(language);
463        self
464    }
465
466    /// Builder: adds a renderer for fenced code blocks of some languages.
467    /// Call it once per renderer; the first one listing a language wins.
468    pub fn block_renderer(mut self, renderer: impl BlockRenderer) -> Self {
469        let mut renderers = self.renderers.clone();
470        renderers.push(Arc::new(renderer));
471        self.set_block_renderers(renderers);
472        self
473    }
474
475    /// Builder: what happens with pasted, dropped and inserted images.
476    pub fn images(mut self, settings: ImageSettings) -> Self {
477        self.set_image_settings(settings);
478        self
479    }
480
481    /// Builder: folder for relative image URLs (desktop display).
482    pub fn image_base(mut self, folder: impl Into<PathBuf>) -> Self {
483        self.set_image_base(Some(folder.into()));
484        self
485    }
486
487    /// Builder: loads image bytes for display by URL.
488    pub fn image_loader(
489        mut self,
490        loader: impl Fn(&str) -> Option<Vec<u8>> + Send + Sync + 'static,
491    ) -> Self {
492        self.set_image_loader(Some(Arc::new(loader)));
493        self
494    }
495
496    /// Builder: sets the mode.
497    pub fn mode(mut self, mode: Mode) -> Self {
498        self.set_mode(mode);
499        self
500    }
501
502    // ----------------------------------------------------------------
503    // Reading
504    // ----------------------------------------------------------------
505
506    /// The document as Markdown, without image placeholders and with full
507    /// `data:` URLs. Builds the whole text: call it on changes, not in `view`.
508    pub fn markdown(&self) -> String {
509        match self.mode {
510            Mode::Wysiwyg => markdown::serialize(&images::without_pending(&self.doc)),
511            Mode::Source => self
512                .embedded
513                .expand(&source::text(&images::without_pending(&self.doc))),
514        }
515    }
516
517    /// The document as plain text.
518    pub fn plain_text(&self) -> String {
519        self.doc.plain_text(self.doc.start(), self.doc.end())
520    }
521
522    /// The selected text, if any.
523    pub fn selected_text(&self) -> Option<String> {
524        let (a, b) = self.sel.range();
525        (a != b).then(|| self.embedded.expand(&self.doc.plain_text(a, b)))
526    }
527
528    pub fn word_count(&self) -> usize {
529        self.doc.word_count()
530    }
531
532    pub fn current_mode(&self) -> Mode {
533        self.mode
534    }
535
536    pub fn current_style(&self) -> &EditorStyle {
537        &self.style
538    }
539
540    pub fn current_paper(&self) -> &Paper {
541        &self.paper
542    }
543
544    /// Current zoom factor (1.0 = 100 %).
545    pub fn current_zoom(&self) -> f32 {
546        self.zoom
547    }
548
549    pub fn current_zoom_settings(&self) -> &ZoomSettings {
550        &self.zoom_settings
551    }
552
553    /// Spaces per Tab (0 = Tab not handled).
554    pub fn current_tab_width(&self) -> usize {
555        self.tab_width
556    }
557
558    pub fn current_image_settings(&self) -> &ImageSettings {
559        &self.image_settings
560    }
561
562    /// Images handed to the application and not answered yet.
563    pub fn pending_images(&self) -> usize {
564        self.pending_images.len()
565    }
566
567    pub fn has_highlighter(&self) -> bool {
568        self.highlighter.is_some()
569    }
570
571    pub fn current_features(&self) -> Features {
572        self.features
573    }
574
575    pub fn current_language(&self) -> Language {
576        self.language
577    }
578
579    /// Languages drawn by block renderers.
580    pub fn rendered_languages(&self) -> Vec<String> {
581        self.renderers
582            .iter()
583            .flat_map(|renderer| renderer.languages())
584            .collect()
585    }
586
587    /// Languages offered for code blocks: from the highlighter (or a common
588    /// list) and the block renderers.
589    pub fn code_languages(&self) -> Vec<String> {
590        let languages = self
591            .highlighter
592            .as_ref()
593            .map(|highlighter| highlighter.languages())
594            .unwrap_or_default();
595        let mut languages: Vec<String> = if languages.is_empty() {
596            COMMON_LANGUAGES
597                .iter()
598                .map(|name| name.to_string())
599                .collect()
600        } else {
601            languages
602        };
603        for language in self.rendered_languages() {
604            if !languages.contains(&language) {
605                languages.push(language);
606            }
607        }
608        languages
609    }
610
611    /// `(checked, total)` task list items.
612    pub fn task_progress(&self) -> (usize, usize) {
613        let mut seen = Vec::new();
614        let mut checked = 0;
615        for block in &self.doc.blocks {
616            for container in &block.containers {
617                if let Container::Item {
618                    id,
619                    task: Some(done),
620                    ..
621                } = container
622                {
623                    if !seen.contains(id) {
624                        seen.push(*id);
625                        if *done {
626                            checked += 1;
627                        }
628                    }
629                }
630            }
631        }
632        (checked, seen.len())
633    }
634
635    /// Number of pages (1 in full-width mode).
636    pub fn page_count(&self) -> usize {
637        self.layout.borrow().pages.len().max(1)
638    }
639
640    /// Page (1-based) that contains the caret.
641    pub fn current_page(&self) -> usize {
642        self.layout.borrow().page_of(self.sel.head) + 1
643    }
644
645    pub fn is_focused(&self) -> bool {
646        self.focused
647    }
648
649    /// `true` when the content changed since the last load or save.
650    pub fn is_dirty(&self) -> bool {
651        self.dirty
652    }
653
654    pub fn can_undo(&self) -> bool {
655        !self.undo.is_empty()
656    }
657
658    pub fn can_redo(&self) -> bool {
659        !self.redo.is_empty()
660    }
661
662    /// Formatting at the caret (or at the start of the selection).
663    pub fn formatting(&self) -> Formatting {
664        let (a, b) = self.sel.range();
665        let style = if a == b {
666            self.typing_style()
667        } else {
668            self.doc.blocks[a.block]
669                .styles
670                .get(a.offset)
671                .cloned()
672                .unwrap_or_default()
673        };
674        let block = &self.doc.blocks[self.sel.head.block];
675        let leaf_kind = match block.leaf {
676            Leaf::Paragraph | Leaf::Line | Leaf::Cell(_) => BlockKind::Paragraph,
677            Leaf::Heading(level) => BlockKind::Heading(level),
678            Leaf::Code => BlockKind::Code,
679            Leaf::Html => BlockKind::Html,
680            Leaf::Rule => BlockKind::Rule,
681        };
682        let item = block
683            .containers
684            .iter()
685            .rev()
686            .find(|c| matches!(c, Container::Item { .. }));
687        Formatting {
688            bold: style.bold,
689            italic: style.italic,
690            strike: style.strike,
691            code: style.code,
692            link: style.link.map(|link| link.url.clone()),
693            block: leaf_kind,
694            list: item.and_then(Container::list).map(|list| list.ordered),
695            task: item.and_then(Container::task),
696            list_depth: block
697                .containers
698                .iter()
699                .filter(|c| c.list().is_some())
700                .count(),
701            quote: block.containers.iter().any(Container::is_quote),
702            code_language: if block.leaf == Leaf::Code {
703                block.language().map(str::to_string)
704            } else {
705                None
706            },
707            table: block.cell().is_some(),
708            column_align: block.cell().map(|cell| cell.align),
709        }
710    }
711
712    // ----------------------------------------------------------------
713    // Writing
714    // ----------------------------------------------------------------
715
716    /// Replaces the whole document. Clears history, pending images and the
717    /// dirty flag.
718    pub fn set_markdown(&mut self, markdown_text: &str) {
719        self.doc = match self.mode {
720            Mode::Wysiwyg => markdown::parse_with(markdown_text, self.features),
721            Mode::Source => source::document(&self.embedded.shorten(markdown_text)),
722        };
723        self.sel = Selection::caret(Pos::new(0, 0));
724        self.undo.clear();
725        self.redo.clear();
726        self.pending_images.clear();
727        self.reset_transient();
728        self.dirty = false;
729        self.revision += 1;
730        self.scroll_x = 0.0;
731        self.scroll_y = 0.0;
732        self.relayout();
733    }
734
735    /// Clears the dirty flag (call after saving).
736    pub fn mark_saved(&mut self) {
737        self.dirty = false;
738    }
739
740    pub fn set_style(&mut self, style: EditorStyle) {
741        self.style = style;
742        self.relayout();
743    }
744
745    pub fn set_paper(&mut self, paper: Paper) {
746        self.paper = paper;
747        self.relayout();
748        self.clamp_scroll();
749    }
750
751    pub fn set_zoom_settings(&mut self, settings: ZoomSettings) {
752        self.zoom_settings = settings;
753        self.zoom = self.zoom_settings.clamp(self.zoom);
754        self.relayout();
755        self.clamp_scroll();
756    }
757
758    /// Sets the zoom factor (1.0 = 100 %), also when zoom is disabled.
759    pub fn set_zoom(&mut self, zoom: f32) {
760        self.zoom = self.zoom_settings.clamp(zoom);
761        self.relayout();
762        self.clamp_scroll();
763    }
764
765    /// Spaces per Tab; `0` leaves Tab to the application.
766    pub fn set_tab_width(&mut self, spaces: usize) {
767        self.tab_width = spaces.min(16);
768    }
769
770    pub fn set_highlighter(&mut self, highlighter: Option<Arc<dyn CodeHighlighter>>) {
771        self.highlighter = highlighter;
772        self.highlight_cache.borrow_mut().clear();
773        self.relayout();
774    }
775
776    /// Sets the language of the toolbar, link bar and status bar. The
777    /// document is not changed.
778    pub fn set_language(&mut self, language: Language) {
779        self.language = language;
780    }
781
782    /// Enables or disables optional Markdown features. In WYSIWYG mode the
783    /// document is read again, as if it had been loaded with the new
784    /// features; the undo history starts again.
785    pub fn set_features(&mut self, features: Features) {
786        if features == self.features {
787            return;
788        }
789        self.features = features;
790        if self.mode == Mode::Wysiwyg {
791            let text = markdown::serialize(&self.doc);
792            self.doc = markdown::parse_with(&text, features);
793            self.sel = Selection {
794                anchor: self.doc.clamp(self.sel.anchor),
795                head: self.doc.clamp(self.sel.head),
796            };
797            self.undo.clear();
798            self.redo.clear();
799            self.reset_transient();
800        }
801        self.relayout();
802    }
803
804    /// Replaces the block renderers. The first renderer listing a language
805    /// draws it.
806    pub fn set_block_renderers(&mut self, renderers: Vec<Arc<dyn BlockRenderer>>) {
807        let mut languages = HashMap::new();
808        for (index, renderer) in renderers.iter().enumerate() {
809            for language in renderer.languages() {
810                languages.entry(language).or_insert(index);
811            }
812        }
813        self.renderers = renderers;
814        self.renderer_languages = languages;
815        self.render_cache.borrow_mut().clear();
816        self.relayout();
817    }
818
819    pub fn set_image_settings(&mut self, settings: ImageSettings) {
820        self.image_settings = settings;
821    }
822
823    pub fn set_image_base(&mut self, folder: Option<PathBuf>) {
824        self.image_base = folder;
825        self.reload_images();
826    }
827
828    pub fn set_image_loader(&mut self, loader: Option<ImageLoader>) {
829        self.image_loader = loader;
830        self.reload_images();
831    }
832
833    /// Forgets loaded images and loads them again.
834    pub fn reload_images(&mut self) {
835        self.image_cache.borrow_mut().clear();
836        self.relayout();
837    }
838
839    /// Switches between WYSIWYG and source mode. The undo history starts
840    /// again after a switch.
841    pub fn set_mode(&mut self, mode: Mode) {
842        if mode == self.mode {
843            return;
844        }
845        self.doc = match mode {
846            Mode::Source => {
847                let text = markdown::serialize(&self.doc);
848                let text = self.embedded.shorten(&text);
849                source::document(&text)
850            }
851            Mode::Wysiwyg => {
852                let text = self.embedded.expand(&source::text(&self.doc));
853                markdown::parse_with(&text, self.features)
854            }
855        };
856        self.mode = mode;
857        self.sel = Selection::caret(Pos::new(0, 0));
858        self.undo.clear();
859        self.redo.clear();
860        self.reset_transient();
861        self.highlight_cache.borrow_mut().clear();
862        self.scroll_x = 0.0;
863        self.scroll_y = 0.0;
864        self.relayout();
865    }
866
867    /// Runs a command and reports what happened.
868    pub fn execute(&mut self, command: Command) -> Output {
869        self.update(Action(Inner::Command(command)))
870    }
871
872    /// Handles an [`Action`] produced by the widget.
873    pub fn update(&mut self, action: Action) -> Output {
874        let revision = self.revision;
875        let sel = self.sel;
876        let mode = self.mode;
877        let focused = self.focused;
878        let zoom = self.zoom;
879
880        let task = self.apply(action.0);
881
882        // A rendered block shows its code while the caret is in it.
883        if self.sel.head.block != sel.head.block
884            && (self.renderer_for(sel.head.block).is_some()
885                || self.renderer_for(self.sel.head.block).is_some())
886        {
887            self.relayout();
888        }
889
890        let mut events = std::mem::take(&mut self.pending_events);
891        if self.focused != focused {
892            events.push(if self.focused {
893                Event::Focused
894            } else {
895                Event::Blurred
896            });
897        }
898        if self.mode != mode {
899            events.push(Event::ModeChanged(self.mode));
900        }
901        if (self.zoom - zoom).abs() > f32::EPSILON {
902            events.push(Event::ZoomChanged(self.zoom));
903        }
904        if self.revision != revision {
905            events.push(Event::Changed);
906        }
907        if self.sel != sel {
908            events.push(Event::SelectionChanged);
909        }
910
911        Output { task, events }
912    }
913
914    /// Caret blinking and dropped image files:
915    /// `state.subscription().map(Message::Editor)`.
916    pub fn subscription(&self) -> Subscription<Action> {
917        let blink = if self.focused {
918            iced::time::every(BLINK).map(|_| Action(Inner::Blink))
919        } else {
920            Subscription::none()
921        };
922        let drops = if self.image_settings.enabled() && self.image_settings.drop {
923            iced::event::listen_with(file_dropped)
924        } else {
925            Subscription::none()
926        };
927        Subscription::batch([blink, drops])
928    }
929
930    // ----------------------------------------------------------------
931    // Internal helpers
932    // ----------------------------------------------------------------
933
934    pub(crate) fn expand_width(&self) -> usize {
935        if self.tab_width == 0 {
936            DEFAULT_TAB_WIDTH
937        } else {
938            self.tab_width
939        }
940    }
941
942    pub(crate) fn new_image_id(&mut self) -> ImageId {
943        let id = ImageId(self.next_image_id);
944        self.next_image_id += 1;
945        id
946    }
947
948    /// Highlight colours for a code block, from the plugin (cached).
949    pub(crate) fn code_colors(&self, index: usize) -> Option<Arc<Vec<Color>>> {
950        let highlighter = self.highlighter.as_ref()?;
951        let block = self.doc.blocks.get(index)?;
952        if block.leaf != Leaf::Code || block.chars.is_empty() {
953            return None;
954        }
955        let language = block.language()?;
956        let palette = &self.style.palette;
957
958        let mut hasher = DefaultHasher::new();
959        language.hash(&mut hasher);
960        block.chars.hash(&mut hasher);
961        for channel in [palette.code_background, palette.code] {
962            channel.r.to_bits().hash(&mut hasher);
963            channel.g.to_bits().hash(&mut hasher);
964            channel.b.to_bits().hash(&mut hasher);
965        }
966        let key = hasher.finish();
967
968        if let Some(colors) = self.highlight_cache.borrow().get(&key) {
969            return Some(colors.clone());
970        }
971
972        let code: String = block.chars.iter().collect();
973        let spans = highlighter.highlight(&HighlightRequest {
974            language,
975            code: &code,
976            background: palette.code_background,
977            dark: highlight::is_dark(palette.code_background),
978        });
979
980        let len = block.chars.len();
981        let mut colors = vec![palette.code; len];
982        for span in spans {
983            let start = span.range.start.min(len);
984            let end = span.range.end.min(len);
985            for color in &mut colors[start..end.max(start)] {
986                *color = span.color;
987            }
988        }
989
990        let colors = Arc::new(colors);
991        let mut cache = self.highlight_cache.borrow_mut();
992        if cache.len() > 512 {
993            cache.clear();
994        }
995        cache.insert(key, colors.clone());
996        Some(colors)
997    }
998
999    /// The block renderer for block `index`, if it is a code block of one
1000    /// of its languages (WYSIWYG mode only).
1001    pub(crate) fn renderer_for(&self, index: usize) -> Option<&Arc<dyn BlockRenderer>> {
1002        if self.mode != Mode::Wysiwyg || self.renderers.is_empty() {
1003            return None;
1004        }
1005        let block = self.doc.blocks.get(index)?;
1006        if block.leaf != Leaf::Code {
1007            return None;
1008        }
1009        let renderer = *self.renderer_languages.get(block.language()?)?;
1010        self.renderers.get(renderer)
1011    }
1012
1013    /// Size of block `index` drawn by its renderer (cached); `None` while
1014    /// the caret is in it or when the renderer declines.
1015    fn measure_block(&self, index: usize, width: f32, max_height: f32) -> Option<Size> {
1016        if index == self.sel.head.block {
1017            return None;
1018        }
1019        let renderer = self.renderer_for(index)?;
1020        let block = &self.doc.blocks[index];
1021        let code: String = block.chars.iter().collect();
1022
1023        let mut hasher = DefaultHasher::new();
1024        Arc::as_ptr(renderer).cast::<()>().hash(&mut hasher);
1025        block.info.hash(&mut hasher);
1026        code.hash(&mut hasher);
1027        for value in [width, max_height, self.zoom] {
1028            value.to_bits().hash(&mut hasher);
1029        }
1030        let key = hasher.finish();
1031        if let Some(size) = self.render_cache.borrow().get(&key) {
1032            return *size;
1033        }
1034
1035        let size = renderer.measure(&RenderRequest {
1036            language: block.language().unwrap_or(""),
1037            info: block.info.as_deref().unwrap_or(""),
1038            code: &code,
1039            width,
1040            max_height,
1041            zoom: self.zoom,
1042            style: &self.style,
1043        });
1044        let mut cache = self.render_cache.borrow_mut();
1045        if cache.len() > 512 {
1046            cache.clear();
1047        }
1048        cache.insert(key, size);
1049        size
1050    }
1051
1052    /// Draws block `index` with its renderer; `available` is the room it
1053    /// was measured for.
1054    pub(crate) fn draw_block(
1055        &self,
1056        frame: &mut Frame,
1057        index: usize,
1058        bounds: Rectangle,
1059        available: Size,
1060    ) {
1061        let Some(renderer) = self.renderer_for(index) else {
1062            return;
1063        };
1064        let block = &self.doc.blocks[index];
1065        let code: String = block.chars.iter().collect();
1066        let request = RenderRequest {
1067            language: block.language().unwrap_or(""),
1068            info: block.info.as_deref().unwrap_or(""),
1069            code: &code,
1070            width: available.width,
1071            max_height: available.height,
1072            zoom: self.zoom,
1073            style: &self.style,
1074        };
1075        renderer.draw(frame, bounds, &request);
1076    }
1077
1078    pub(crate) fn relayout(&self) {
1079        let cache = &self.image_cache;
1080        let base = self.image_base.as_deref();
1081        let loader = self.image_loader.as_ref();
1082        let lookup = |url: &str| cache.borrow_mut().get(url, base, loader);
1083        let custom = |index: usize, width: f32, max_height: f32| {
1084            self.measure_block(index, width, max_height)
1085        };
1086        let layout = compute_layout(
1087            &self.doc,
1088            &self.fonts,
1089            &self.style,
1090            &self.paper,
1091            self.viewport.get(),
1092            self.zoom,
1093            Lookups {
1094                images: &lookup,
1095                custom: &custom,
1096            },
1097        );
1098        *self.layout.borrow_mut() = layout;
1099        self.cache.borrow_mut().clear();
1100    }
1101
1102    /// Called while drawing with the widget size.
1103    pub(crate) fn sync_viewport(&self, size: Size) {
1104        let current = self.viewport.get();
1105        if (current.width - size.width).abs() > 0.5 || (current.height - size.height).abs() > 0.5 {
1106            self.viewport.set(size);
1107            self.relayout();
1108        }
1109    }
1110
1111    /// Largest scroll offsets for the current layout and widget size.
1112    pub(crate) fn max_scroll(&self) -> (f32, f32) {
1113        let layout = self.layout.borrow();
1114        let viewport = self.viewport.get();
1115        (
1116            (layout.width - viewport.width).max(0.0),
1117            (layout.height - viewport.height).max(0.0),
1118        )
1119    }
1120
1121    /// Scroll offsets clamped to the content (used while drawing).
1122    pub(crate) fn clamped_scroll(&self) -> (f32, f32) {
1123        let (max_x, max_y) = self.max_scroll();
1124        (
1125            self.scroll_x.clamp(0.0, max_x),
1126            self.scroll_y.clamp(0.0, max_y),
1127        )
1128    }
1129
1130    pub(crate) fn set_scroll(&mut self, x: f32, y: f32) {
1131        let (max_x, max_y) = self.max_scroll();
1132        let x = x.clamp(0.0, max_x);
1133        let y = y.clamp(0.0, max_y);
1134        if (x - self.scroll_x).abs() > 0.01 || (y - self.scroll_y).abs() > 0.01 {
1135            self.scroll_x = x;
1136            self.scroll_y = y;
1137            self.cache.borrow_mut().clear();
1138        }
1139    }
1140
1141    pub(crate) fn clamp_scroll(&mut self) {
1142        let (x, y) = (self.scroll_x, self.scroll_y);
1143        self.set_scroll(x, y);
1144    }
1145
1146    pub(crate) fn reset_transient(&mut self) {
1147        self.pending = None;
1148        self.goal_x = None;
1149        self.last_edit = EditKind::None;
1150        self.link_url = None;
1151        self.link_range = None;
1152        self.link_dialog = None;
1153        self.context_menu = None;
1154    }
1155
1156    pub(crate) fn touch(&mut self) {
1157        self.last_activity = Instant::now();
1158        self.caret_on = true;
1159    }
1160
1161    pub(crate) fn typing_style(&self) -> Style {
1162        match &self.pending {
1163            Some(style) => style.clone(),
1164            None => self.doc.typing_style(self.sel.range().0),
1165        }
1166    }
1167
1168    pub(crate) fn begin_edit(&mut self, kind: EditKind) {
1169        let coalesce =
1170            kind == self.last_edit && matches!(kind, EditKind::Typing | EditKind::Delete);
1171        if !coalesce {
1172            self.undo.push(Snapshot {
1173                doc: self.doc.clone(),
1174                sel: self.sel,
1175            });
1176            if self.undo.len() > 300 {
1177                self.undo.remove(0);
1178            }
1179        }
1180        self.redo.clear();
1181        self.last_edit = kind;
1182    }
1183
1184    pub(crate) fn after_edit(&mut self) -> Task<Action> {
1185        self.dirty = true;
1186        self.revision += 1;
1187        self.goal_x = None;
1188        self.relayout();
1189        self.touch();
1190        self.scroll_to_caret()
1191    }
1192
1193    /// Scrolls so that the caret is visible.
1194    pub(crate) fn scroll_to_caret(&mut self) -> Task<Action> {
1195        let caret = self.layout.borrow().caret(self.sel.head);
1196        let Some((x, y, height)) = caret else {
1197            return Task::none();
1198        };
1199        let viewport = self.viewport.get();
1200        let margin = 24.0;
1201        let (mut sx, mut sy) = (self.scroll_x, self.scroll_y);
1202
1203        if y < sy + margin {
1204            sy = y - 2.0 * margin;
1205        } else if y + height > sy + viewport.height - margin {
1206            sy = y + height - viewport.height + 2.0 * margin;
1207        }
1208        if !self.paper.is_fill() {
1209            if x < sx + margin {
1210                sx = x - 4.0 * margin;
1211            } else if x > sx + viewport.width - margin {
1212                sx = x - viewport.width + 4.0 * margin;
1213            }
1214        }
1215        self.set_scroll(sx, sy);
1216        Task::none()
1217    }
1218
1219    /// Changes the zoom and keeps the visible area roughly in place.
1220    pub(crate) fn zoom_to(&mut self, zoom: f32) -> Task<Action> {
1221        let zoom = self.zoom_settings.clamp(zoom);
1222        if (zoom - self.zoom).abs() < 1.0e-4 {
1223            return Task::none();
1224        }
1225        let ratio = zoom / self.zoom;
1226        let viewport = self.viewport.get();
1227        let center_x = self.scroll_x + viewport.width / 2.0;
1228        let center_y = self.scroll_y + viewport.height / 2.0;
1229
1230        self.zoom = zoom;
1231        self.relayout();
1232        self.set_scroll(
1233            center_x * ratio - viewport.width / 2.0,
1234            center_y * ratio - viewport.height / 2.0,
1235        );
1236        Task::none()
1237    }
1238
1239    pub(crate) fn block_len(&self, index: usize) -> usize {
1240        self.doc.blocks.get(index).map(Block::len).unwrap_or(0)
1241    }
1242}
1243
1244/// Files dropped onto the window (images are filtered in the editor).
1245fn file_dropped(
1246    event: iced::Event,
1247    _status: iced::event::Status,
1248    _window: iced::window::Id,
1249) -> Option<Action> {
1250    match event {
1251        iced::Event::Window(iced::window::Event::FileDropped(path)) => {
1252            Some(Action(Inner::FileDropped(path)))
1253        }
1254        _ => None,
1255    }
1256}