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