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