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