Skip to main content

iced_markdown_editor/
zoom.rs

1//! Zoom settings and key bindings.
2//!
3//! ```
4//! use iced_markdown_editor::{KeyBinding, ZoomSettings, ZoomShortcuts};
5//!
6//! let standard = ZoomSettings::default();
7//! let off = ZoomSettings::disabled();
8//! let custom = ZoomSettings {
9//!     min: 0.5,
10//!     max: 3.0,
11//!     mouse_wheel: false,
12//!     shortcuts: ZoomShortcuts {
13//!         zoom_in: vec![KeyBinding::command("+"), KeyBinding::command_shift("i")],
14//!         zoom_out: vec![KeyBinding::command("-"), KeyBinding::command_shift("o")],
15//!         reset: vec![KeyBinding::command("0")],
16//!     },
17//!     ..ZoomSettings::default()
18//! };
19//! ```
20
21use crate::widget::Command;
22use iced::keyboard::{Key, Modifiers};
23
24/// A key combination.
25///
26/// Character keys are compared case-insensitively, against both the key and
27/// the key with modifiers applied (so `Ctrl` + `+` matches on keyboards
28/// where `+` needs Shift). Ctrl, Alt and Logo must match exactly; Shift is
29/// required when the binding contains it, otherwise ignored.
30#[derive(Debug, Clone, PartialEq)]
31pub struct KeyBinding {
32    pub key: Key,
33    pub modifiers: Modifiers,
34}
35
36impl KeyBinding {
37    pub fn new(key: Key, modifiers: Modifiers) -> Self {
38        Self { key, modifiers }
39    }
40
41    /// `Ctrl` (Windows, Linux) or `Cmd` (macOS) plus a character.
42    pub fn command(character: &str) -> Self {
43        Self::new(Key::Character(character.into()), Modifiers::COMMAND)
44    }
45
46    /// `Ctrl`/`Cmd` + `Shift` plus a character.
47    pub fn command_shift(character: &str) -> Self {
48        Self::new(
49            Key::Character(character.into()),
50            Modifiers::COMMAND | Modifiers::SHIFT,
51        )
52    }
53
54    /// Whether the pressed key matches this binding.
55    pub fn matches(&self, key: &Key, modified_key: &Key, modifiers: Modifiers) -> bool {
56        let same_key = |pressed: &Key| match (self.key.as_ref(), pressed.as_ref()) {
57            (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b),
58            (a, b) => a == b,
59        };
60
61        (same_key(key) || same_key(modified_key))
62            && modifiers.control() == self.modifiers.control()
63            && modifiers.alt() == self.modifiers.alt()
64            && modifiers.logo() == self.modifiers.logo()
65            && (!self.modifiers.shift() || modifiers.shift())
66    }
67}
68
69/// Keyboard shortcuts for zooming. Empty lists disable a shortcut.
70#[derive(Debug, Clone, PartialEq)]
71pub struct ZoomShortcuts {
72    pub zoom_in: Vec<KeyBinding>,
73    pub zoom_out: Vec<KeyBinding>,
74    pub reset: Vec<KeyBinding>,
75}
76
77impl Default for ZoomShortcuts {
78    /// `Ctrl`/`Cmd` + `+` or `=` (in), `-` (out), `0` (reset to 100 %).
79    fn default() -> Self {
80        Self {
81            zoom_in: vec![KeyBinding::command("+"), KeyBinding::command("=")],
82            zoom_out: vec![KeyBinding::command("-")],
83            reset: vec![KeyBinding::command("0")],
84        }
85    }
86}
87
88impl ZoomShortcuts {
89    /// No keyboard shortcuts.
90    pub fn none() -> Self {
91        Self {
92            zoom_in: Vec::new(),
93            zoom_out: Vec::new(),
94            reset: Vec::new(),
95        }
96    }
97}
98
99/// Zoom behaviour.
100#[derive(Debug, Clone, PartialEq)]
101pub struct ZoomSettings {
102    /// Master switch for shortcuts, mouse wheel, status bar control and the
103    /// zoom commands. [`EditorState::set_zoom`](crate::EditorState::set_zoom)
104    /// still works.
105    pub enabled: bool,
106    pub min: f32,
107    pub max: f32,
108    /// Steps used by zoom in / zoom out (ascending). When empty, each step
109    /// multiplies or divides by 1.1.
110    pub levels: Vec<f32>,
111    /// `Ctrl`/`Cmd` + mouse wheel.
112    pub mouse_wheel: bool,
113    /// Zoom control (− 100 % +) in the status bar.
114    pub controls: bool,
115    pub shortcuts: ZoomShortcuts,
116}
117
118impl Default for ZoomSettings {
119    fn default() -> Self {
120        Self {
121            enabled: true,
122            min: 0.25,
123            max: 5.0,
124            levels: vec![
125                0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1.0, 1.1, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0,
126                4.0, 5.0,
127            ],
128            mouse_wheel: true,
129            controls: true,
130            shortcuts: ZoomShortcuts::default(),
131        }
132    }
133}
134
135impl ZoomSettings {
136    /// Zoom fixed at its current value; no user interaction.
137    pub fn disabled() -> Self {
138        Self {
139            enabled: false,
140            ..Self::default()
141        }
142    }
143
144    pub fn clamp(&self, zoom: f32) -> f32 {
145        let (min, max) = (self.min.min(self.max), self.max.max(self.min));
146        zoom.clamp(min.max(0.05), max.max(0.05))
147    }
148
149    /// The next zoom level in `direction` (positive = in).
150    pub fn step(&self, zoom: f32, direction: i32) -> f32 {
151        let next = if self.levels.is_empty() {
152            if direction > 0 {
153                zoom * 1.1
154            } else {
155                zoom / 1.1
156            }
157        } else if direction > 0 {
158            self.levels
159                .iter()
160                .copied()
161                .find(|level| *level > zoom * 1.001)
162                .unwrap_or(self.max)
163        } else {
164            self.levels
165                .iter()
166                .rev()
167                .copied()
168                .find(|level| *level < zoom * 0.999)
169                .unwrap_or(self.min)
170        };
171        self.clamp(next)
172    }
173
174    /// The zoom command bound to a key press, if any.
175    pub(crate) fn command_for(
176        &self,
177        key: &Key,
178        modified_key: &Key,
179        modifiers: Modifiers,
180    ) -> Option<Command> {
181        if !self.enabled {
182            return None;
183        }
184        let hit = |bindings: &[KeyBinding]| {
185            bindings
186                .iter()
187                .any(|binding| binding.matches(key, modified_key, modifiers))
188        };
189        if hit(&self.shortcuts.zoom_in) {
190            Some(Command::ZoomIn)
191        } else if hit(&self.shortcuts.zoom_out) {
192            Some(Command::ZoomOut)
193        } else if hit(&self.shortcuts.reset) {
194            Some(Command::ResetZoom)
195        } else {
196            None
197        }
198    }
199}