1mod 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;
40use web_time::Instant;
42
43pub(crate) const BLINK: Duration = Duration::from_millis(530);
44
45pub const DEFAULT_TAB_WIDTH: usize = 4;
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum Mode {
51 Wysiwyg,
53 Source,
55}
56
57#[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 {
75 x: f32,
76 y: f32,
77 clicks: u8,
78 },
79 Drag {
81 x: f32,
82 y: f32,
83 },
84 PressOutside,
85 Blink,
86 ScrollBy {
88 dx: f32,
89 dy: f32,
90 },
91 ScrollTo {
93 x: Option<f32>,
94 y: Option<f32>,
95 },
96 Wheel(ScrollDelta),
98 Command(Command),
99 LinkUrl(String),
100 LinkApply,
101 LinkRemove,
102 LinkCancel,
103 Paste(Option<String>),
104 FileDropped(PathBuf),
106}
107
108#[derive(Debug, Clone, PartialEq)]
110pub enum Command {
111 Undo,
112 Redo,
113 ToggleBold,
114 ToggleItalic,
115 ToggleStrike,
116 ToggleCode,
117 SetBlock(BlockKind),
120 ToggleChecked,
122 SetCodeLanguage(Option<String>),
124 InsertRule,
125 EditLink,
127 InsertText(String),
129 Indent,
131 Outdent,
133 SelectAll,
134 Copy,
135 Cut,
136 Paste,
138 InsertImage {
140 bytes: Vec<u8>,
141 name: Option<String>,
143 },
144 InsertImageFile(PathBuf),
146 InsertImageLink {
148 url: String,
149 alt: String,
150 },
151 ResolveImage {
153 id: ImageId,
154 url: String,
155 alt: String,
156 },
157 CancelImage {
159 id: ImageId,
160 },
161 RequestImage,
163 SetMode(Mode),
164 Focus,
165 Blur,
166 ZoomIn,
167 ZoomOut,
168 ResetZoom,
169 SetZoom(f32),
171 FitWidth,
173 FitPage,
175}
176
177#[derive(Debug, Clone, PartialEq)]
179pub enum Event {
180 Changed,
183 SelectionChanged,
185 ModeChanged(Mode),
186 ZoomChanged(f32),
188 Focused,
189 Blurred,
190 LinkClicked(String),
192 ImagePasted(PastedImage),
195 ImageRejected {
197 source: ImageSource,
198 reason: ImageRejection,
199 },
200 ImageRequested,
202}
203
204pub struct Output {
206 pub task: Task<Action>,
208 pub events: Vec<Event>,
210}
211
212#[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 pub block: BlockKind,
222 pub list: Option<bool>,
224 pub task: Option<bool>,
226 pub list_depth: usize,
228 pub quote: bool,
230 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
247pub 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 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 pub fn new() -> Self {
298 Self::with_markdown("")
299 }
300
301 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 pub fn style(mut self, style: EditorStyle) -> Self {
348 self.set_style(style);
349 self
350 }
351
352 pub fn paper(mut self, paper: Paper) -> Self {
354 self.set_paper(paper);
355 self
356 }
357
358 pub fn zoom_settings(mut self, settings: ZoomSettings) -> Self {
360 self.set_zoom_settings(settings);
361 self
362 }
363
364 pub fn zoom(mut self, zoom: f32) -> Self {
366 self.set_zoom(zoom);
367 self
368 }
369
370 pub fn tab_width(mut self, spaces: usize) -> Self {
372 self.set_tab_width(spaces);
373 self
374 }
375
376 pub fn highlighter(mut self, highlighter: impl CodeHighlighter) -> Self {
378 self.set_highlighter(Some(Arc::new(highlighter)));
379 self
380 }
381
382 pub fn images(mut self, settings: ImageSettings) -> Self {
384 self.set_image_settings(settings);
385 self
386 }
387
388 pub fn image_base(mut self, folder: impl Into<PathBuf>) -> Self {
390 self.set_image_base(Some(folder.into()));
391 self
392 }
393
394 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 pub fn mode(mut self, mode: Mode) -> Self {
405 self.set_mode(mode);
406 self
407 }
408
409 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 pub fn plain_text(&self) -> String {
426 self.doc.plain_text(self.doc.start(), self.doc.end())
427 }
428
429 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 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 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 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 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 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 pub fn page_count(&self) -> usize {
521 self.layout.borrow().pages.len().max(1)
522 }
523
524 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 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 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 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 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 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 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 pub fn reload_images(&mut self) {
674 self.image_cache.borrow_mut().clear();
675 self.relayout();
676 }
677
678 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 pub fn execute(&mut self, command: Command) -> Output {
708 self.update(Action(Inner::Command(command)))
709 }
710
711 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 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 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 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 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 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 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 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 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
988fn 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}