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
17mod edit;
18mod source;
19mod toolbar;
20mod view;
21
22pub use view::{MarkdownEditor, markdown_editor};
23
24use crate::doc::{Block, BlockKind, ColumnAlign, Container, Document, Leaf, Pos, Selection, Style};
25use crate::extension::{BlockRenderer, Frame, Rectangle, RenderRequest};
26use crate::features::Features;
27use crate::fonts::Fonts;
28use crate::highlight::{self, COMMON_LANGUAGES, CodeHighlighter, HighlightRequest};
29use crate::images::{
30    self, ImageCache, ImageId, ImageLoader, ImageRejection, ImageSettings, ImageSource, PastedImage,
31};
32use crate::layout::{Layout, Lookups, compute as compute_layout};
33use crate::markdown;
34use crate::page::Paper;
35use crate::style::EditorStyle;
36use crate::zoom::ZoomSettings;
37use iced::keyboard::{Key, Modifiers};
38use iced::mouse::ScrollDelta;
39use iced::widget::canvas;
40use iced::{Color, Size, Subscription, Task};
41use std::cell::{Cell, RefCell};
42use std::collections::HashMap;
43use std::collections::hash_map::DefaultHasher;
44use std::hash::{Hash, Hasher};
45use std::path::PathBuf;
46use std::sync::Arc;
47use std::time::Duration;
48// `std::time::Instant` panics in the browser; `web_time` is std on desktop.
49use web_time::Instant;
50
51pub(crate) const BLINK: Duration = Duration::from_millis(530);
52
53/// Default number of spaces inserted by the Tab key.
54pub const DEFAULT_TAB_WIDTH: usize = 4;
55
56/// Editing mode.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum Mode {
59    /// Formatted, Word-like editing.
60    Wysiwyg,
61    /// Raw Markdown text.
62    Source,
63}
64
65/// A message produced by the widget. Opaque: forward it to
66/// [`EditorState::update`]. Create one from a [`Command`] with
67/// `Action::from(command)` to drive the editor from your own widgets.
68#[derive(Debug, Clone)]
69pub struct Action(pub(crate) Inner);
70
71impl From<Command> for Action {
72    fn from(command: Command) -> Self {
73        Action(Inner::Command(command))
74    }
75}
76
77#[derive(Debug, Clone)]
78pub(crate) enum Inner {
79    Key(Key, Modifiers, Option<String>),
80    Modifiers(Modifiers),
81    /// Press in document coordinates.
82    Press {
83        x: f32,
84        y: f32,
85        clicks: u8,
86    },
87    /// Drag in document coordinates.
88    Drag {
89        x: f32,
90        y: f32,
91    },
92    PressOutside,
93    Blink,
94    /// Scroll by a distance in pixels.
95    ScrollBy {
96        dx: f32,
97        dy: f32,
98    },
99    /// Scroll to an absolute offset (scrollbar drag).
100    ScrollTo {
101        x: Option<f32>,
102        y: Option<f32>,
103    },
104    /// Ctrl/Cmd + wheel: zoom.
105    Wheel(ScrollDelta),
106    Command(Command),
107    LinkUrl(String),
108    LinkApply,
109    LinkRemove,
110    LinkCancel,
111    Paste(Option<String>),
112    /// A file dropped onto the window.
113    FileDropped(PathBuf),
114}
115
116/// Operations that can be triggered programmatically.
117#[derive(Debug, Clone, PartialEq)]
118pub enum Command {
119    Undo,
120    Redo,
121    ToggleBold,
122    ToggleItalic,
123    ToggleStrike,
124    ToggleCode,
125    /// Changes the paragraph(s) under the selection: block type, list,
126    /// task list or block quote. Applying the current kind again reverts.
127    SetBlock(BlockKind),
128    /// Checks or unchecks the task list item(s) under the selection.
129    ToggleChecked,
130    /// Sets the language of the code block(s) under the selection.
131    SetCodeLanguage(Option<String>),
132    InsertRule,
133    /// Inserts an empty table; `rows` includes the header row.
134    InsertTable {
135        rows: usize,
136        columns: usize,
137    },
138    /// Inserts a row above or below the row at the caret.
139    InsertRow {
140        below: bool,
141    },
142    /// Inserts a column left or right of the column at the caret.
143    InsertColumn {
144        right: bool,
145    },
146    /// Deletes the table row at the caret.
147    DeleteRow,
148    /// Deletes the table column at the caret.
149    DeleteColumn,
150    /// Deletes the table at the caret.
151    DeleteTable,
152    /// Aligns the table column at the caret.
153    SetColumnAlign(ColumnAlign),
154    /// Opens the link bar for the selection (or word under the caret).
155    EditLink,
156    /// Inserts text at the caret, replacing the selection.
157    InsertText(String),
158    /// Same as the Tab key.
159    Indent,
160    /// Same as Shift+Tab.
161    Outdent,
162    SelectAll,
163    Copy,
164    Cut,
165    /// Pastes an image (with the `images` feature, desktop) or text.
166    Paste,
167    /// Inserts an image from its encoded bytes, following the [`ImageSettings`].
168    InsertImage {
169        bytes: Vec<u8>,
170        /// File name, used as description and in [`PastedImage::name`].
171        name: Option<String>,
172    },
173    /// Like [`Command::InsertImage`] with the content of a file (desktop).
174    InsertImageFile(PathBuf),
175    /// Inserts `![alt](url)` for an image that already has a URL.
176    InsertImageLink {
177        url: String,
178        alt: String,
179    },
180    /// Answer to [`Event::ImagePasted`]: the placeholder becomes `![alt](url)`.
181    ResolveImage {
182        id: ImageId,
183        url: String,
184        alt: String,
185    },
186    /// Answer to [`Event::ImagePasted`]: the placeholder is removed.
187    CancelImage {
188        id: ImageId,
189    },
190    /// Emits [`Event::ImageRequested`] (the toolbar's image button).
191    RequestImage,
192    SetMode(Mode),
193    Focus,
194    Blur,
195    ZoomIn,
196    ZoomOut,
197    ResetZoom,
198    /// Sets a zoom factor (1.0 = 100 %).
199    SetZoom(f32),
200    /// Zooms so the page width fits the widget.
201    FitWidth,
202    /// Zooms so a whole page fits the widget.
203    FitPage,
204}
205
206/// Something that happened inside the editor.
207#[derive(Debug, Clone, PartialEq)]
208pub enum Event {
209    /// The document content changed. Call [`EditorState::markdown`] here,
210    /// not in `view`.
211    Changed,
212    /// The caret or selection moved.
213    SelectionChanged,
214    ModeChanged(Mode),
215    /// The zoom factor changed (1.0 = 100 %).
216    ZoomChanged(f32),
217    Focused,
218    Blurred,
219    /// A link was Ctrl/Cmd-clicked.
220    LinkClicked(String),
221    /// An image to store ([`ImageMode::Delegate`](crate::ImageMode::Delegate)).
222    /// Answer with [`Command::ResolveImage`] or [`Command::CancelImage`].
223    ImagePasted(PastedImage),
224    /// An image was not accepted.
225    ImageRejected {
226        source: ImageSource,
227        reason: ImageRejection,
228    },
229    /// The toolbar's image button was pressed. Show a file dialog.
230    ImageRequested,
231}
232
233/// Result of [`EditorState::update`].
234pub struct Output {
235    /// Must be returned from your `update` (mapped to your message type).
236    pub task: Task<Action>,
237    /// Events produced by this update, in order.
238    pub events: Vec<Event>,
239}
240
241/// Formatting at the caret, for building custom toolbars.
242#[derive(Debug, Clone, PartialEq)]
243pub struct Formatting {
244    pub bold: bool,
245    pub italic: bool,
246    pub strike: bool,
247    pub code: bool,
248    pub link: Option<String>,
249    /// Block type: `Paragraph`, `Heading`, `Code`, `Rule` or `Html`.
250    pub block: BlockKind,
251    /// Innermost list: `Some(true)` numbered, `Some(false)` bullets.
252    pub list: Option<bool>,
253    /// Task state of the innermost list item.
254    pub task: Option<bool>,
255    /// Number of nested lists at the caret.
256    pub list_depth: usize,
257    /// The caret is inside a block quote.
258    pub quote: bool,
259    /// Language of the code block at the caret, if any.
260    pub code_language: Option<String>,
261    /// The caret is inside a table.
262    pub table: bool,
263    /// Alignment of the table column at the caret.
264    pub column_align: Option<ColumnAlign>,
265}
266
267pub(crate) struct Snapshot {
268    pub doc: Document,
269    pub sel: Selection,
270}
271
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub(crate) enum EditKind {
274    None,
275    Typing,
276    Delete,
277    Other,
278}
279
280/// The editor state. Keep one per editor in your application.
281pub struct EditorState {
282    pub(crate) doc: Document,
283    pub(crate) sel: Selection,
284    pub(crate) layout: RefCell<Layout>,
285    pub(crate) cache: RefCell<canvas::Cache>,
286    pub(crate) viewport: Cell<Size>,
287    pub(crate) style: EditorStyle,
288    pub(crate) paper: Paper,
289    pub(crate) zoom: f32,
290    pub(crate) zoom_settings: ZoomSettings,
291    pub(crate) tab_width: usize,
292    pub(crate) highlighter: Option<Arc<dyn CodeHighlighter>>,
293    pub(crate) highlight_cache: RefCell<HashMap<u64, Arc<Vec<Color>>>>,
294    pub(crate) features: Features,
295    pub(crate) renderers: Vec<Arc<dyn BlockRenderer>>,
296    /// Language -> index in `renderers`.
297    pub(crate) renderer_languages: HashMap<String, usize>,
298    /// Measured sizes of rendered blocks.
299    pub(crate) render_cache: RefCell<HashMap<u64, Option<Size>>>,
300    pub(crate) image_settings: ImageSettings,
301    pub(crate) image_base: Option<PathBuf>,
302    pub(crate) image_loader: Option<ImageLoader>,
303    pub(crate) image_cache: RefCell<ImageCache>,
304    pub(crate) pending_images: HashMap<ImageId, Arc<[u8]>>,
305    pub(crate) next_image_id: u64,
306    /// Long `data:` URLs shortened in source mode.
307    pub(crate) embedded: source::Embedded,
308    pub(crate) mode: Mode,
309    pub(crate) focused: bool,
310    pub(crate) caret_on: bool,
311    pub(crate) link_url: Option<String>,
312    pub(crate) link_range: Option<(Pos, Pos)>,
313    pub(crate) fonts: Fonts,
314    pub(crate) pending: Option<Style>,
315    pub(crate) goal_x: Option<f32>,
316    pub(crate) undo: Vec<Snapshot>,
317    pub(crate) redo: Vec<Snapshot>,
318    pub(crate) last_edit: EditKind,
319    pub(crate) modifiers: Modifiers,
320    pub(crate) last_activity: Instant,
321    pub(crate) scroll_x: f32,
322    pub(crate) scroll_y: f32,
323    pub(crate) revision: u64,
324    pub(crate) dirty: bool,
325    pub(crate) pending_events: Vec<Event>,
326}
327
328impl Default for EditorState {
329    fn default() -> Self {
330        Self::new()
331    }
332}
333
334impl EditorState {
335    /// An empty document.
336    pub fn new() -> Self {
337        Self::with_markdown("")
338    }
339
340    /// A document parsed from CommonMark (with the GFM extensions
341    /// strikethrough, task lists and tables; see [`Self::features`]).
342    pub fn with_markdown(markdown_text: &str) -> Self {
343        let state = Self {
344            doc: markdown::parse(markdown_text),
345            sel: Selection::caret(Pos::new(0, 0)),
346            layout: RefCell::new(Layout::default()),
347            cache: RefCell::new(canvas::Cache::new()),
348            viewport: Cell::new(Size::new(1000.0, 700.0)),
349            style: EditorStyle::default(),
350            paper: Paper::default(),
351            zoom: 1.0,
352            zoom_settings: ZoomSettings::default(),
353            tab_width: DEFAULT_TAB_WIDTH,
354            highlighter: None,
355            highlight_cache: RefCell::new(HashMap::new()),
356            features: Features::default(),
357            renderers: Vec::new(),
358            renderer_languages: HashMap::new(),
359            render_cache: RefCell::new(HashMap::new()),
360            image_settings: ImageSettings::default(),
361            image_base: None,
362            image_loader: None,
363            image_cache: RefCell::new(ImageCache::default()),
364            pending_images: HashMap::new(),
365            next_image_id: 1,
366            embedded: source::Embedded::default(),
367            mode: Mode::Wysiwyg,
368            focused: false,
369            caret_on: true,
370            link_url: None,
371            link_range: None,
372            fonts: Fonts::load(),
373            pending: None,
374            goal_x: None,
375            undo: Vec::new(),
376            redo: Vec::new(),
377            last_edit: EditKind::None,
378            modifiers: Modifiers::empty(),
379            last_activity: Instant::now(),
380            scroll_x: 0.0,
381            scroll_y: 0.0,
382            revision: 0,
383            dirty: false,
384            pending_events: Vec::new(),
385        };
386        state.relayout();
387        state
388    }
389
390    /// Builder: sets the style.
391    pub fn style(mut self, style: EditorStyle) -> Self {
392        self.set_style(style);
393        self
394    }
395
396    /// Builder: sets the paper.
397    pub fn paper(mut self, paper: Paper) -> Self {
398        self.set_paper(paper);
399        self
400    }
401
402    /// Builder: sets the zoom settings.
403    pub fn zoom_settings(mut self, settings: ZoomSettings) -> Self {
404        self.set_zoom_settings(settings);
405        self
406    }
407
408    /// Builder: sets the initial zoom.
409    pub fn zoom(mut self, zoom: f32) -> Self {
410        self.set_zoom(zoom);
411        self
412    }
413
414    /// Builder: number of spaces the Tab key inserts (0 = Tab not handled).
415    pub fn tab_width(mut self, spaces: usize) -> Self {
416        self.set_tab_width(spaces);
417        self
418    }
419
420    /// Builder: installs a code highlighting plugin.
421    pub fn highlighter(mut self, highlighter: impl CodeHighlighter) -> Self {
422        self.set_highlighter(Some(Arc::new(highlighter)));
423        self
424    }
425
426    /// Builder: enables or disables optional Markdown features. The document
427    /// is read again with the new features.
428    pub fn features(mut self, features: Features) -> Self {
429        self.set_features(features);
430        self
431    }
432
433    /// Builder: adds a renderer for fenced code blocks of some languages.
434    /// Call it once per renderer; the first one listing a language wins.
435    pub fn block_renderer(mut self, renderer: impl BlockRenderer) -> Self {
436        let mut renderers = self.renderers.clone();
437        renderers.push(Arc::new(renderer));
438        self.set_block_renderers(renderers);
439        self
440    }
441
442    /// Builder: what happens with pasted, dropped and inserted images.
443    pub fn images(mut self, settings: ImageSettings) -> Self {
444        self.set_image_settings(settings);
445        self
446    }
447
448    /// Builder: folder for relative image URLs (desktop display).
449    pub fn image_base(mut self, folder: impl Into<PathBuf>) -> Self {
450        self.set_image_base(Some(folder.into()));
451        self
452    }
453
454    /// Builder: loads image bytes for display by URL.
455    pub fn image_loader(
456        mut self,
457        loader: impl Fn(&str) -> Option<Vec<u8>> + Send + Sync + 'static,
458    ) -> Self {
459        self.set_image_loader(Some(Arc::new(loader)));
460        self
461    }
462
463    /// Builder: sets the mode.
464    pub fn mode(mut self, mode: Mode) -> Self {
465        self.set_mode(mode);
466        self
467    }
468
469    // ----------------------------------------------------------------
470    // Reading
471    // ----------------------------------------------------------------
472
473    /// The document as Markdown, without image placeholders and with full
474    /// `data:` URLs. Builds the whole text: call it on changes, not in `view`.
475    pub fn markdown(&self) -> String {
476        match self.mode {
477            Mode::Wysiwyg => markdown::serialize(&images::without_pending(&self.doc)),
478            Mode::Source => self
479                .embedded
480                .expand(&source::text(&images::without_pending(&self.doc))),
481        }
482    }
483
484    /// The document as plain text.
485    pub fn plain_text(&self) -> String {
486        self.doc.plain_text(self.doc.start(), self.doc.end())
487    }
488
489    /// The selected text, if any.
490    pub fn selected_text(&self) -> Option<String> {
491        let (a, b) = self.sel.range();
492        (a != b).then(|| self.embedded.expand(&self.doc.plain_text(a, b)))
493    }
494
495    pub fn word_count(&self) -> usize {
496        self.doc.word_count()
497    }
498
499    pub fn current_mode(&self) -> Mode {
500        self.mode
501    }
502
503    pub fn current_style(&self) -> &EditorStyle {
504        &self.style
505    }
506
507    pub fn current_paper(&self) -> &Paper {
508        &self.paper
509    }
510
511    /// Current zoom factor (1.0 = 100 %).
512    pub fn current_zoom(&self) -> f32 {
513        self.zoom
514    }
515
516    pub fn current_zoom_settings(&self) -> &ZoomSettings {
517        &self.zoom_settings
518    }
519
520    /// Spaces per Tab (0 = Tab not handled).
521    pub fn current_tab_width(&self) -> usize {
522        self.tab_width
523    }
524
525    pub fn current_image_settings(&self) -> &ImageSettings {
526        &self.image_settings
527    }
528
529    /// Images handed to the application and not answered yet.
530    pub fn pending_images(&self) -> usize {
531        self.pending_images.len()
532    }
533
534    pub fn has_highlighter(&self) -> bool {
535        self.highlighter.is_some()
536    }
537
538    pub fn current_features(&self) -> Features {
539        self.features
540    }
541
542    /// Languages drawn by block renderers.
543    pub fn rendered_languages(&self) -> Vec<String> {
544        self.renderers
545            .iter()
546            .flat_map(|renderer| renderer.languages())
547            .collect()
548    }
549
550    /// Languages offered for code blocks: from the highlighter (or a common
551    /// list) and the block renderers.
552    pub fn code_languages(&self) -> Vec<String> {
553        let languages = self
554            .highlighter
555            .as_ref()
556            .map(|highlighter| highlighter.languages())
557            .unwrap_or_default();
558        let mut languages: Vec<String> = if languages.is_empty() {
559            COMMON_LANGUAGES
560                .iter()
561                .map(|name| name.to_string())
562                .collect()
563        } else {
564            languages
565        };
566        for language in self.rendered_languages() {
567            if !languages.contains(&language) {
568                languages.push(language);
569            }
570        }
571        languages
572    }
573
574    /// `(checked, total)` task list items.
575    pub fn task_progress(&self) -> (usize, usize) {
576        let mut seen = Vec::new();
577        let mut checked = 0;
578        for block in &self.doc.blocks {
579            for container in &block.containers {
580                if let Container::Item {
581                    id,
582                    task: Some(done),
583                    ..
584                } = container
585                {
586                    if !seen.contains(id) {
587                        seen.push(*id);
588                        if *done {
589                            checked += 1;
590                        }
591                    }
592                }
593            }
594        }
595        (checked, seen.len())
596    }
597
598    /// Number of pages (1 in full-width mode).
599    pub fn page_count(&self) -> usize {
600        self.layout.borrow().pages.len().max(1)
601    }
602
603    /// Page (1-based) that contains the caret.
604    pub fn current_page(&self) -> usize {
605        self.layout.borrow().page_of(self.sel.head) + 1
606    }
607
608    pub fn is_focused(&self) -> bool {
609        self.focused
610    }
611
612    /// `true` when the content changed since the last load or save.
613    pub fn is_dirty(&self) -> bool {
614        self.dirty
615    }
616
617    pub fn can_undo(&self) -> bool {
618        !self.undo.is_empty()
619    }
620
621    pub fn can_redo(&self) -> bool {
622        !self.redo.is_empty()
623    }
624
625    /// Formatting at the caret (or at the start of the selection).
626    pub fn formatting(&self) -> Formatting {
627        let (a, b) = self.sel.range();
628        let style = if a == b {
629            self.typing_style()
630        } else {
631            self.doc.blocks[a.block]
632                .styles
633                .get(a.offset)
634                .cloned()
635                .unwrap_or_default()
636        };
637        let block = &self.doc.blocks[self.sel.head.block];
638        let leaf_kind = match block.leaf {
639            Leaf::Paragraph | Leaf::Line | Leaf::Cell(_) => BlockKind::Paragraph,
640            Leaf::Heading(level) => BlockKind::Heading(level),
641            Leaf::Code => BlockKind::Code,
642            Leaf::Html => BlockKind::Html,
643            Leaf::Rule => BlockKind::Rule,
644        };
645        let item = block
646            .containers
647            .iter()
648            .rev()
649            .find(|c| matches!(c, Container::Item { .. }));
650        Formatting {
651            bold: style.bold,
652            italic: style.italic,
653            strike: style.strike,
654            code: style.code,
655            link: style.link.map(|link| link.url.clone()),
656            block: leaf_kind,
657            list: item.and_then(Container::list).map(|list| list.ordered),
658            task: item.and_then(Container::task),
659            list_depth: block
660                .containers
661                .iter()
662                .filter(|c| c.list().is_some())
663                .count(),
664            quote: block.containers.iter().any(Container::is_quote),
665            code_language: if block.leaf == Leaf::Code {
666                block.language().map(str::to_string)
667            } else {
668                None
669            },
670            table: block.cell().is_some(),
671            column_align: block.cell().map(|cell| cell.align),
672        }
673    }
674
675    // ----------------------------------------------------------------
676    // Writing
677    // ----------------------------------------------------------------
678
679    /// Replaces the whole document. Clears history, pending images and the
680    /// dirty flag.
681    pub fn set_markdown(&mut self, markdown_text: &str) {
682        self.doc = match self.mode {
683            Mode::Wysiwyg => markdown::parse_with(markdown_text, self.features),
684            Mode::Source => source::document(&self.embedded.shorten(markdown_text)),
685        };
686        self.sel = Selection::caret(Pos::new(0, 0));
687        self.undo.clear();
688        self.redo.clear();
689        self.pending_images.clear();
690        self.reset_transient();
691        self.dirty = false;
692        self.revision += 1;
693        self.scroll_x = 0.0;
694        self.scroll_y = 0.0;
695        self.relayout();
696    }
697
698    /// Clears the dirty flag (call after saving).
699    pub fn mark_saved(&mut self) {
700        self.dirty = false;
701    }
702
703    pub fn set_style(&mut self, style: EditorStyle) {
704        self.style = style;
705        self.relayout();
706    }
707
708    pub fn set_paper(&mut self, paper: Paper) {
709        self.paper = paper;
710        self.relayout();
711        self.clamp_scroll();
712    }
713
714    pub fn set_zoom_settings(&mut self, settings: ZoomSettings) {
715        self.zoom_settings = settings;
716        self.zoom = self.zoom_settings.clamp(self.zoom);
717        self.relayout();
718        self.clamp_scroll();
719    }
720
721    /// Sets the zoom factor (1.0 = 100 %), also when zoom is disabled.
722    pub fn set_zoom(&mut self, zoom: f32) {
723        self.zoom = self.zoom_settings.clamp(zoom);
724        self.relayout();
725        self.clamp_scroll();
726    }
727
728    /// Spaces per Tab; `0` leaves Tab to the application.
729    pub fn set_tab_width(&mut self, spaces: usize) {
730        self.tab_width = spaces.min(16);
731    }
732
733    pub fn set_highlighter(&mut self, highlighter: Option<Arc<dyn CodeHighlighter>>) {
734        self.highlighter = highlighter;
735        self.highlight_cache.borrow_mut().clear();
736        self.relayout();
737    }
738
739    /// Enables or disables optional Markdown features. In WYSIWYG mode the
740    /// document is read again, as if it had been loaded with the new
741    /// features; the undo history starts again.
742    pub fn set_features(&mut self, features: Features) {
743        if features == self.features {
744            return;
745        }
746        self.features = features;
747        if self.mode == Mode::Wysiwyg {
748            let text = markdown::serialize(&self.doc);
749            self.doc = markdown::parse_with(&text, features);
750            self.sel = Selection {
751                anchor: self.doc.clamp(self.sel.anchor),
752                head: self.doc.clamp(self.sel.head),
753            };
754            self.undo.clear();
755            self.redo.clear();
756            self.reset_transient();
757        }
758        self.relayout();
759    }
760
761    /// Replaces the block renderers. The first renderer listing a language
762    /// draws it.
763    pub fn set_block_renderers(&mut self, renderers: Vec<Arc<dyn BlockRenderer>>) {
764        let mut languages = HashMap::new();
765        for (index, renderer) in renderers.iter().enumerate() {
766            for language in renderer.languages() {
767                languages.entry(language).or_insert(index);
768            }
769        }
770        self.renderers = renderers;
771        self.renderer_languages = languages;
772        self.render_cache.borrow_mut().clear();
773        self.relayout();
774    }
775
776    pub fn set_image_settings(&mut self, settings: ImageSettings) {
777        self.image_settings = settings;
778    }
779
780    pub fn set_image_base(&mut self, folder: Option<PathBuf>) {
781        self.image_base = folder;
782        self.reload_images();
783    }
784
785    pub fn set_image_loader(&mut self, loader: Option<ImageLoader>) {
786        self.image_loader = loader;
787        self.reload_images();
788    }
789
790    /// Forgets loaded images and loads them again.
791    pub fn reload_images(&mut self) {
792        self.image_cache.borrow_mut().clear();
793        self.relayout();
794    }
795
796    /// Switches between WYSIWYG and source mode. The undo history starts
797    /// again after a switch.
798    pub fn set_mode(&mut self, mode: Mode) {
799        if mode == self.mode {
800            return;
801        }
802        self.doc = match mode {
803            Mode::Source => {
804                let text = markdown::serialize(&self.doc);
805                let text = self.embedded.shorten(&text);
806                source::document(&text)
807            }
808            Mode::Wysiwyg => {
809                let text = self.embedded.expand(&source::text(&self.doc));
810                markdown::parse_with(&text, self.features)
811            }
812        };
813        self.mode = mode;
814        self.sel = Selection::caret(Pos::new(0, 0));
815        self.undo.clear();
816        self.redo.clear();
817        self.reset_transient();
818        self.highlight_cache.borrow_mut().clear();
819        self.scroll_x = 0.0;
820        self.scroll_y = 0.0;
821        self.relayout();
822    }
823
824    /// Runs a command and reports what happened.
825    pub fn execute(&mut self, command: Command) -> Output {
826        self.update(Action(Inner::Command(command)))
827    }
828
829    /// Handles an [`Action`] produced by the widget.
830    pub fn update(&mut self, action: Action) -> Output {
831        let revision = self.revision;
832        let sel = self.sel;
833        let mode = self.mode;
834        let focused = self.focused;
835        let zoom = self.zoom;
836
837        let task = self.apply(action.0);
838
839        // A rendered block shows its code while the caret is in it.
840        if self.sel.head.block != sel.head.block
841            && (self.renderer_for(sel.head.block).is_some()
842                || self.renderer_for(self.sel.head.block).is_some())
843        {
844            self.relayout();
845        }
846
847        let mut events = std::mem::take(&mut self.pending_events);
848        if self.focused != focused {
849            events.push(if self.focused {
850                Event::Focused
851            } else {
852                Event::Blurred
853            });
854        }
855        if self.mode != mode {
856            events.push(Event::ModeChanged(self.mode));
857        }
858        if (self.zoom - zoom).abs() > f32::EPSILON {
859            events.push(Event::ZoomChanged(self.zoom));
860        }
861        if self.revision != revision {
862            events.push(Event::Changed);
863        }
864        if self.sel != sel {
865            events.push(Event::SelectionChanged);
866        }
867
868        Output { task, events }
869    }
870
871    /// Caret blinking and dropped image files:
872    /// `state.subscription().map(Message::Editor)`.
873    pub fn subscription(&self) -> Subscription<Action> {
874        let blink = if self.focused {
875            iced::time::every(BLINK).map(|_| Action(Inner::Blink))
876        } else {
877            Subscription::none()
878        };
879        let drops = if self.image_settings.enabled() && self.image_settings.drop {
880            iced::event::listen_with(file_dropped)
881        } else {
882            Subscription::none()
883        };
884        Subscription::batch([blink, drops])
885    }
886
887    // ----------------------------------------------------------------
888    // Internal helpers
889    // ----------------------------------------------------------------
890
891    pub(crate) fn expand_width(&self) -> usize {
892        if self.tab_width == 0 {
893            DEFAULT_TAB_WIDTH
894        } else {
895            self.tab_width
896        }
897    }
898
899    pub(crate) fn new_image_id(&mut self) -> ImageId {
900        let id = ImageId(self.next_image_id);
901        self.next_image_id += 1;
902        id
903    }
904
905    /// Highlight colours for a code block, from the plugin (cached).
906    pub(crate) fn code_colors(&self, index: usize) -> Option<Arc<Vec<Color>>> {
907        let highlighter = self.highlighter.as_ref()?;
908        let block = self.doc.blocks.get(index)?;
909        if block.leaf != Leaf::Code || block.chars.is_empty() {
910            return None;
911        }
912        let language = block.language()?;
913        let palette = &self.style.palette;
914
915        let mut hasher = DefaultHasher::new();
916        language.hash(&mut hasher);
917        block.chars.hash(&mut hasher);
918        for channel in [palette.code_background, palette.code] {
919            channel.r.to_bits().hash(&mut hasher);
920            channel.g.to_bits().hash(&mut hasher);
921            channel.b.to_bits().hash(&mut hasher);
922        }
923        let key = hasher.finish();
924
925        if let Some(colors) = self.highlight_cache.borrow().get(&key) {
926            return Some(colors.clone());
927        }
928
929        let code: String = block.chars.iter().collect();
930        let spans = highlighter.highlight(&HighlightRequest {
931            language,
932            code: &code,
933            background: palette.code_background,
934            dark: highlight::is_dark(palette.code_background),
935        });
936
937        let len = block.chars.len();
938        let mut colors = vec![palette.code; len];
939        for span in spans {
940            let start = span.range.start.min(len);
941            let end = span.range.end.min(len);
942            for color in &mut colors[start..end.max(start)] {
943                *color = span.color;
944            }
945        }
946
947        let colors = Arc::new(colors);
948        let mut cache = self.highlight_cache.borrow_mut();
949        if cache.len() > 512 {
950            cache.clear();
951        }
952        cache.insert(key, colors.clone());
953        Some(colors)
954    }
955
956    /// The block renderer for block `index`, if it is a code block of one
957    /// of its languages (WYSIWYG mode only).
958    pub(crate) fn renderer_for(&self, index: usize) -> Option<&Arc<dyn BlockRenderer>> {
959        if self.mode != Mode::Wysiwyg || self.renderers.is_empty() {
960            return None;
961        }
962        let block = self.doc.blocks.get(index)?;
963        if block.leaf != Leaf::Code {
964            return None;
965        }
966        let renderer = *self.renderer_languages.get(block.language()?)?;
967        self.renderers.get(renderer)
968    }
969
970    /// Size of block `index` drawn by its renderer (cached); `None` while
971    /// the caret is in it or when the renderer declines.
972    fn measure_block(&self, index: usize, width: f32, max_height: f32) -> Option<Size> {
973        if index == self.sel.head.block {
974            return None;
975        }
976        let renderer = self.renderer_for(index)?;
977        let block = &self.doc.blocks[index];
978        let code: String = block.chars.iter().collect();
979
980        let mut hasher = DefaultHasher::new();
981        Arc::as_ptr(renderer).cast::<()>().hash(&mut hasher);
982        block.info.hash(&mut hasher);
983        code.hash(&mut hasher);
984        for value in [width, max_height, self.zoom] {
985            value.to_bits().hash(&mut hasher);
986        }
987        let key = hasher.finish();
988        if let Some(size) = self.render_cache.borrow().get(&key) {
989            return *size;
990        }
991
992        let size = renderer.measure(&RenderRequest {
993            language: block.language().unwrap_or(""),
994            info: block.info.as_deref().unwrap_or(""),
995            code: &code,
996            width,
997            max_height,
998            zoom: self.zoom,
999            style: &self.style,
1000        });
1001        let mut cache = self.render_cache.borrow_mut();
1002        if cache.len() > 512 {
1003            cache.clear();
1004        }
1005        cache.insert(key, size);
1006        size
1007    }
1008
1009    /// Draws block `index` with its renderer; `available` is the room it
1010    /// was measured for.
1011    pub(crate) fn draw_block(
1012        &self,
1013        frame: &mut Frame,
1014        index: usize,
1015        bounds: Rectangle,
1016        available: Size,
1017    ) {
1018        let Some(renderer) = self.renderer_for(index) else {
1019            return;
1020        };
1021        let block = &self.doc.blocks[index];
1022        let code: String = block.chars.iter().collect();
1023        let request = RenderRequest {
1024            language: block.language().unwrap_or(""),
1025            info: block.info.as_deref().unwrap_or(""),
1026            code: &code,
1027            width: available.width,
1028            max_height: available.height,
1029            zoom: self.zoom,
1030            style: &self.style,
1031        };
1032        renderer.draw(frame, bounds, &request);
1033    }
1034
1035    pub(crate) fn relayout(&self) {
1036        let cache = &self.image_cache;
1037        let base = self.image_base.as_deref();
1038        let loader = self.image_loader.as_ref();
1039        let lookup = |url: &str| cache.borrow_mut().get(url, base, loader);
1040        let custom = |index: usize, width: f32, max_height: f32| {
1041            self.measure_block(index, width, max_height)
1042        };
1043        let layout = compute_layout(
1044            &self.doc,
1045            &self.fonts,
1046            &self.style,
1047            &self.paper,
1048            self.viewport.get(),
1049            self.zoom,
1050            Lookups {
1051                images: &lookup,
1052                custom: &custom,
1053            },
1054        );
1055        *self.layout.borrow_mut() = layout;
1056        self.cache.borrow_mut().clear();
1057    }
1058
1059    /// Called while drawing with the widget size.
1060    pub(crate) fn sync_viewport(&self, size: Size) {
1061        let current = self.viewport.get();
1062        if (current.width - size.width).abs() > 0.5 || (current.height - size.height).abs() > 0.5 {
1063            self.viewport.set(size);
1064            self.relayout();
1065        }
1066    }
1067
1068    /// Largest scroll offsets for the current layout and widget size.
1069    pub(crate) fn max_scroll(&self) -> (f32, f32) {
1070        let layout = self.layout.borrow();
1071        let viewport = self.viewport.get();
1072        (
1073            (layout.width - viewport.width).max(0.0),
1074            (layout.height - viewport.height).max(0.0),
1075        )
1076    }
1077
1078    /// Scroll offsets clamped to the content (used while drawing).
1079    pub(crate) fn clamped_scroll(&self) -> (f32, f32) {
1080        let (max_x, max_y) = self.max_scroll();
1081        (
1082            self.scroll_x.clamp(0.0, max_x),
1083            self.scroll_y.clamp(0.0, max_y),
1084        )
1085    }
1086
1087    pub(crate) fn set_scroll(&mut self, x: f32, y: f32) {
1088        let (max_x, max_y) = self.max_scroll();
1089        let x = x.clamp(0.0, max_x);
1090        let y = y.clamp(0.0, max_y);
1091        if (x - self.scroll_x).abs() > 0.01 || (y - self.scroll_y).abs() > 0.01 {
1092            self.scroll_x = x;
1093            self.scroll_y = y;
1094            self.cache.borrow_mut().clear();
1095        }
1096    }
1097
1098    pub(crate) fn clamp_scroll(&mut self) {
1099        let (x, y) = (self.scroll_x, self.scroll_y);
1100        self.set_scroll(x, y);
1101    }
1102
1103    pub(crate) fn reset_transient(&mut self) {
1104        self.pending = None;
1105        self.goal_x = None;
1106        self.last_edit = EditKind::None;
1107        self.link_url = None;
1108        self.link_range = None;
1109    }
1110
1111    pub(crate) fn touch(&mut self) {
1112        self.last_activity = Instant::now();
1113        self.caret_on = true;
1114    }
1115
1116    pub(crate) fn typing_style(&self) -> Style {
1117        match &self.pending {
1118            Some(style) => style.clone(),
1119            None => self.doc.typing_style(self.sel.range().0),
1120        }
1121    }
1122
1123    pub(crate) fn begin_edit(&mut self, kind: EditKind) {
1124        let coalesce =
1125            kind == self.last_edit && matches!(kind, EditKind::Typing | EditKind::Delete);
1126        if !coalesce {
1127            self.undo.push(Snapshot {
1128                doc: self.doc.clone(),
1129                sel: self.sel,
1130            });
1131            if self.undo.len() > 300 {
1132                self.undo.remove(0);
1133            }
1134        }
1135        self.redo.clear();
1136        self.last_edit = kind;
1137    }
1138
1139    pub(crate) fn after_edit(&mut self) -> Task<Action> {
1140        self.dirty = true;
1141        self.revision += 1;
1142        self.goal_x = None;
1143        self.relayout();
1144        self.touch();
1145        self.scroll_to_caret()
1146    }
1147
1148    /// Scrolls so that the caret is visible.
1149    pub(crate) fn scroll_to_caret(&mut self) -> Task<Action> {
1150        let caret = self.layout.borrow().caret(self.sel.head);
1151        let Some((x, y, height)) = caret else {
1152            return Task::none();
1153        };
1154        let viewport = self.viewport.get();
1155        let margin = 24.0;
1156        let (mut sx, mut sy) = (self.scroll_x, self.scroll_y);
1157
1158        if y < sy + margin {
1159            sy = y - 2.0 * margin;
1160        } else if y + height > sy + viewport.height - margin {
1161            sy = y + height - viewport.height + 2.0 * margin;
1162        }
1163        if !self.paper.is_fill() {
1164            if x < sx + margin {
1165                sx = x - 4.0 * margin;
1166            } else if x > sx + viewport.width - margin {
1167                sx = x - viewport.width + 4.0 * margin;
1168            }
1169        }
1170        self.set_scroll(sx, sy);
1171        Task::none()
1172    }
1173
1174    /// Changes the zoom and keeps the visible area roughly in place.
1175    pub(crate) fn zoom_to(&mut self, zoom: f32) -> Task<Action> {
1176        let zoom = self.zoom_settings.clamp(zoom);
1177        if (zoom - self.zoom).abs() < 1.0e-4 {
1178            return Task::none();
1179        }
1180        let ratio = zoom / self.zoom;
1181        let viewport = self.viewport.get();
1182        let center_x = self.scroll_x + viewport.width / 2.0;
1183        let center_y = self.scroll_y + viewport.height / 2.0;
1184
1185        self.zoom = zoom;
1186        self.relayout();
1187        self.set_scroll(
1188            center_x * ratio - viewport.width / 2.0,
1189            center_y * ratio - viewport.height / 2.0,
1190        );
1191        Task::none()
1192    }
1193
1194    pub(crate) fn block_len(&self, index: usize) -> usize {
1195        self.doc.blocks.get(index).map(Block::len).unwrap_or(0)
1196    }
1197}
1198
1199/// Files dropped onto the window (images are filtered in the editor).
1200fn file_dropped(
1201    event: iced::Event,
1202    _status: iced::event::Status,
1203    _window: iced::window::Id,
1204) -> Option<Action> {
1205    match event {
1206        iced::Event::Window(iced::window::Event::FileDropped(path)) => {
1207            Some(Action(Inner::FileDropped(path)))
1208        }
1209        _ => None,
1210    }
1211}