teksilo_widgets/rich_text/
policy.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum EditCommandKind {
16 InsertChar,
18 InsertBlock,
19 InsertBlockForced,
24 InsertTab,
26 NavigateTableCell,
29 NavigateTableCellDown,
32 ExitList,
34 ExitFrame,
39 ToggleBlockquote,
42 IncreaseBlockquoteDepth,
44 DecreaseBlockquoteDepth,
46 DeletePrev,
47 DeleteNext,
48 DeleteWordLeft,
49 DeleteWordRight,
50 IndentBlock,
51 DedentBlock,
52 ToggleBold,
53 ToggleItalic,
54 ToggleUnderline,
55 Undo,
56 Redo,
57
58 MoveLeft,
60 MoveRight,
61 MoveUp,
62 MoveDown,
63 MoveWordLeft,
64 MoveWordRight,
65 MoveHome,
66 MoveEnd,
67 MoveDocStart,
68 MoveDocEnd,
69 PageUp,
70 PageDown,
71 SelectLeft,
72 SelectRight,
73 SelectUp,
74 SelectDown,
75 SelectWordLeft,
76 SelectWordRight,
77 SelectHome,
78 SelectEnd,
79 SelectDocStart,
80 SelectDocEnd,
81 SelectAll,
82
83 Copy,
85 Cut,
86 Paste,
87 PasteUnformatted,
94}
95
96impl crate::common::editor_runtime::EditorCommand for EditCommandKind {
97 fn is_regressive(&self) -> bool {
118 matches!(
119 self,
120 Self::DeletePrev
121 | Self::DeleteNext
122 | Self::DeleteWordLeft
123 | Self::DeleteWordRight
124 | Self::Cut
125 | Self::Undo
126 | Self::Redo
127 )
128 }
129
130 fn mutates_document(&self) -> bool {
133 matches!(
134 self,
135 Self::InsertChar
136 | Self::InsertBlock
137 | Self::InsertBlockForced
138 | Self::InsertTab
139 | Self::ExitList
140 | Self::ExitFrame
141 | Self::ToggleBlockquote
142 | Self::IncreaseBlockquoteDepth
143 | Self::DecreaseBlockquoteDepth
144 | Self::NavigateTableCell
145 | Self::NavigateTableCellDown
146 | Self::DeletePrev
147 | Self::DeleteNext
148 | Self::DeleteWordLeft
149 | Self::DeleteWordRight
150 | Self::IndentBlock
151 | Self::DedentBlock
152 | Self::ToggleBold
153 | Self::ToggleItalic
154 | Self::ToggleUnderline
155 | Self::Undo
156 | Self::Redo
157 | Self::Cut
158 | Self::Paste
159 | Self::PasteUnformatted
160 )
161 }
162}
163
164pub use crate::common::editor_runtime::{
169 AccessibilityRole, CaretPolicy, ClipboardPolicy, CommandFilter, EDITOR_PRESET, PolicyBundle,
170 READ_ONLY_PRESET,
171};
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn read_only_filter_blocks_mutations() {
179 let f = CommandFilter::ReadOnly;
180 assert!(!f.accepts(EditCommandKind::InsertChar));
181 assert!(!f.accepts(EditCommandKind::DeletePrev));
182 assert!(!f.accepts(EditCommandKind::ToggleBold));
183 assert!(!f.accepts(EditCommandKind::Cut));
184 assert!(!f.accepts(EditCommandKind::Paste));
185 assert!(
186 !f.accepts(EditCommandKind::PasteUnformatted),
187 "read-only must reject PasteUnformatted — it mutates the document"
188 );
189 assert!(!f.accepts(EditCommandKind::Undo));
190 }
191
192 #[test]
195 fn forward_only_filter_blocks_every_regressive_command() {
196 let f = CommandFilter::ForwardOnly;
197 for cmd in [
198 EditCommandKind::DeletePrev,
199 EditCommandKind::DeleteNext,
200 EditCommandKind::DeleteWordLeft,
201 EditCommandKind::DeleteWordRight,
202 EditCommandKind::Cut,
203 EditCommandKind::Undo,
204 ] {
205 assert!(
206 !f.accepts(cmd),
207 "{cmd:?} takes text away and must be rejected"
208 );
209 }
210 assert!(
211 !f.accepts(EditCommandKind::Redo),
212 "redo can re-apply a deletion made before the mode was switched on"
213 );
214 }
215
216 #[test]
220 fn forward_only_filter_allows_additive_navigation_and_structure() {
221 let f = CommandFilter::ForwardOnly;
222 for cmd in [
223 EditCommandKind::InsertChar,
224 EditCommandKind::InsertBlock,
225 EditCommandKind::InsertBlockForced,
226 EditCommandKind::InsertTab,
227 EditCommandKind::Paste,
228 EditCommandKind::PasteUnformatted,
229 EditCommandKind::Copy,
230 EditCommandKind::SelectAll,
231 EditCommandKind::MoveLeft,
232 EditCommandKind::MoveDocEnd,
233 EditCommandKind::SelectWordRight,
234 EditCommandKind::ToggleBold,
235 EditCommandKind::ToggleItalic,
236 EditCommandKind::ToggleUnderline,
237 EditCommandKind::IndentBlock,
238 EditCommandKind::DedentBlock,
239 EditCommandKind::ToggleBlockquote,
240 EditCommandKind::IncreaseBlockquoteDepth,
241 EditCommandKind::DecreaseBlockquoteDepth,
242 EditCommandKind::NavigateTableCell,
243 EditCommandKind::NavigateTableCellDown,
244 ] {
245 assert!(
246 f.accepts(cmd),
247 "{cmd:?} adds or navigates and must be allowed"
248 );
249 }
250 assert!(
251 f.accepts(EditCommandKind::ExitList) && f.accepts(EditCommandKind::ExitFrame),
252 "popping out of a list or a quote drops no characters — blocking it \
253 would trap the writer inside the structure with no way out"
254 );
255 }
256
257 #[test]
261 fn regressive_is_a_strict_subset_of_mutating() {
262 let all = [
263 EditCommandKind::InsertChar,
264 EditCommandKind::InsertBlock,
265 EditCommandKind::InsertBlockForced,
266 EditCommandKind::InsertTab,
267 EditCommandKind::NavigateTableCell,
268 EditCommandKind::NavigateTableCellDown,
269 EditCommandKind::ExitList,
270 EditCommandKind::ExitFrame,
271 EditCommandKind::ToggleBlockquote,
272 EditCommandKind::IncreaseBlockquoteDepth,
273 EditCommandKind::DecreaseBlockquoteDepth,
274 EditCommandKind::DeletePrev,
275 EditCommandKind::DeleteNext,
276 EditCommandKind::DeleteWordLeft,
277 EditCommandKind::DeleteWordRight,
278 EditCommandKind::IndentBlock,
279 EditCommandKind::DedentBlock,
280 EditCommandKind::ToggleBold,
281 EditCommandKind::ToggleItalic,
282 EditCommandKind::ToggleUnderline,
283 EditCommandKind::Undo,
284 EditCommandKind::Redo,
285 EditCommandKind::MoveLeft,
286 EditCommandKind::Copy,
287 EditCommandKind::Cut,
288 EditCommandKind::Paste,
289 EditCommandKind::PasteUnformatted,
290 ];
291 use crate::common::editor_runtime::EditorCommand;
292 let mut strictly_narrower = false;
293 for cmd in all {
294 if cmd.is_regressive() {
295 assert!(
296 cmd.mutates_document(),
297 "{cmd:?} claims to take text away without mutating the document"
298 );
299 }
300 if cmd.mutates_document() && !cmd.is_regressive() {
301 strictly_narrower = true;
302 }
303 if CommandFilter::ReadOnly.accepts(cmd) {
305 assert!(
306 CommandFilter::ForwardOnly.accepts(cmd),
307 "{cmd:?} is allowed read-only but rejected forward-only"
308 );
309 }
310 }
311 assert!(strictly_narrower, "the two predicates collapsed into one");
312 }
313
314 #[test]
317 fn only_forward_only_collapses_the_selection_before_inserting() {
318 assert!(CommandFilter::ForwardOnly.collapses_selection_before_insert());
319 assert!(!CommandFilter::All.collapses_selection_before_insert());
320 assert!(!CommandFilter::ReadOnly.collapses_selection_before_insert());
321 }
322
323 #[test]
326 fn only_the_unrestricted_filter_allows_wholesale_replacement() {
327 assert!(CommandFilter::All.allows_wholesale_replacement());
328 assert!(!CommandFilter::ForwardOnly.allows_wholesale_replacement());
329 assert!(!CommandFilter::ReadOnly.allows_wholesale_replacement());
330 }
331
332 #[test]
336 fn with_command_filter_changes_only_the_filter() {
337 let fwd = EDITOR_PRESET.with_command_filter(CommandFilter::ForwardOnly);
338 assert_eq!(fwd.command_filter, CommandFilter::ForwardOnly);
339 assert!(!fwd.is_read_only());
340 assert_eq!(fwd.caret_policy, EDITOR_PRESET.caret_policy);
341 assert_eq!(fwd.access_role, EDITOR_PRESET.access_role);
342 assert_eq!(fwd.clipboard_policy, EDITOR_PRESET.clipboard_policy);
343 }
344
345 #[test]
346 fn paste_unformatted_policy_mirrors_paste() {
347 let full = ClipboardPolicy::Full;
348 let ro = ClipboardPolicy::CopyAndSelectAllOnly;
349 assert!(full.allows_paste());
350 assert!(full.allows_paste_unformatted());
351 assert!(!ro.allows_paste());
352 assert!(!ro.allows_paste_unformatted());
353 }
354
355 #[test]
356 fn read_only_filter_allows_navigation_and_copy() {
357 let f = CommandFilter::ReadOnly;
358 assert!(f.accepts(EditCommandKind::MoveLeft));
359 assert!(f.accepts(EditCommandKind::SelectWordRight));
360 assert!(f.accepts(EditCommandKind::MoveDocEnd));
361 assert!(f.accepts(EditCommandKind::Copy));
362 assert!(f.accepts(EditCommandKind::SelectAll));
363 }
364
365 #[test]
366 fn editor_filter_accepts_everything() {
367 let f = CommandFilter::All;
368 for cmd in [
369 EditCommandKind::InsertChar,
370 EditCommandKind::Paste,
371 EditCommandKind::Undo,
372 EditCommandKind::Copy,
373 EditCommandKind::MoveHome,
374 ] {
375 assert!(f.accepts(cmd));
376 }
377 }
378
379 #[test]
380 fn presets_report_read_only_correctly() {
381 assert!(!EDITOR_PRESET.is_read_only());
382 assert!(READ_ONLY_PRESET.is_read_only());
383 }
384}