1use crate::focus::{FocusIntent, FocusTarget};
10use crate::input::{
11 BindableActionInfo, BindingCatalog, BindingConflict, BindingInfo, BindingLayer, BindingSource,
12 CanvasRoutingPrecedence, KeyChord, KeyMap,
13};
14use crate::runtime::{
15 ActionContext, ActionOutcome, CanvasHooks, InputLayerContext, KeyHook, KeyHookKind,
16 KeyHookOutcome, KeyHookRouting, ModeId, PageSpec, TuiEffect, TuiPagesBuilder, TuiPagesStatus,
17 modes,
18};
19use crossterm::{
20 event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
21 execute,
22};
23use ratatui::{Frame, layout::Rect};
24#[cfg(feature = "tui")]
25use ratatui::style::Style;
26use std::io;
27use std::marker::PhantomData;
28
29pub use ::canvas::integration::focus_handoff::{
34 BoundaryExit, HostActionOutcome, execute_action_for_host, execute_action_for_host_with_options,
35};
36pub use ::canvas::{
37 ActionResult, AppMode, CanvasAction, DataProvider, EditorState, TextFormEventOutcome,
38 TextFormState,
39};
40
41pub use ::canvas::integration::focus_handoff::{
43 HostKeyEventOutcome, boundary_from_key_outcome, handle_key_event_for_host,
44 key_outcome_for_vertical_navigation, map_key_event_outcome_for_host,
45};
46pub use ::canvas::keybindings::{
47 CanvasKeyAction, CanvasKeybindingPresetError, KeyStroke,
48 try_parse_binding as try_parse_canvas_binding,
49};
50pub use ::canvas::{
51 BuiltinCanvasKeybindingPreset, CanvasActionBinding, CanvasActionKeyBinding,
52 CanvasKeyBindingEntry, CanvasKeyBindings, CanvasKeybindingConflictKind,
53 CanvasKeybindingProfile, KeyEventOutcome, default_builtin_action_bindings,
54 default_emacs_action_bindings, default_helix_action_bindings, default_vim_action_bindings,
55 display_binding, preset,
56};
57
58pub use ::canvas::CursorManager;
60
61pub use ::canvas::{
63 SuggestionItem, SuggestionQuery, SuggestionTrigger, render_suggestions_dropdown,
64};
65
66pub use ::canvas::validation::limits::{CountMode, LimitCheckResult};
68pub use ::canvas::{
69 AppliedValidation, CharacterFilter, CharacterLimits, CustomFormatter, DefaultPositionMapper,
70 DisplayMask, FormattingResult, PatternFilters, PositionFilter, PositionMapper, PositionRange,
71 ValidationConfig, ValidationConfigBuilder, ValidationError, ValidationResult, ValidationRule,
72 ValidationSet, ValidationSettings, ValidationState, ValidationSummary,
73};
74
75pub use ::canvas::{ComputedContext, ComputedProvider, ComputedState};
77
78pub use ::canvas::{
80 CanvasDisplayOptions, CanvasTheme, DefaultCanvasTheme, OverflowMode,
81};
82
83#[cfg(feature = "tui")]
84impl CanvasTheme for crate::ThemeStyles {
85 fn background(&self) -> Style {
86 self.background
87 }
88 fn label(&self) -> Style {
89 self.muted
90 }
91 fn label_active(&self) -> Style {
92 self.line_number_selected
93 }
94 fn input(&self) -> Style {
95 self.text
96 }
97 fn input_active(&self) -> Style {
98 self.text.patch(self.cursorline).patch(self.text_focus)
99 }
100 fn selection(&self) -> Style {
101 self.selection
102 }
103 fn cursorline(&self) -> Style {
104 self.cursorline
105 }
106 fn completion(&self) -> Style {
107 self.text_inactive
108 }
109 fn cursor_normal(&self) -> Style {
110 self.cursor_normal
111 }
112 fn cursor_insert(&self) -> Style {
113 self.cursor_insert
114 }
115 fn cursor_select(&self) -> Style {
116 self.cursor_select
117 }
118 fn suggestions(&self) -> Style {
119 self.menu
120 }
121 fn suggestion_selected(&self) -> Style {
122 self.menu_selected
123 }
124 fn warning(&self) -> Style {
125 self.warning
126 }
127 fn border(&self) -> Style {
128 self.window
129 }
130 fn border_active(&self) -> Style {
131 self.text_focus.patch(self.cursor_normal)
132 }
133}
134
135pub use ::canvas::integration::crossterm_input::{
139 CrosstermInputGuard, CrosstermInputOptions, CrosstermInputSession,
140};
141
142pub use ::canvas::textarea::{
144 TextAreaCommandLineState, TextAreaEventOutcome, TextAreaLineNumberMode, TextAreaSearchMatch,
145 TextOverflowMode,
146};
147pub use ::canvas::{TextArea, TextAreaDataProvider, TextAreaProvider, TextAreaState};
148
149pub use ::canvas::{
151 CommandLine, CommandLineCommand, CommandLineCommandInvocation, CommandLineDispatchError,
152 CommandLineEventOutcome, CommandLineMode, CommandLineParseError, CommandLineParsedCommand,
153 CommandLinePlacement, CommandLineRegistrationError, CommandLineRegistry, CommandLineState,
154 CommandLineSubmit, parse_command_args, parse_command_line,
155};
156
157pub use ::canvas::{
159 TextInput, TextInputDataProvider, TextInputEventOutcome, TextInputProvider, TextInputState,
160};
161
162pub type FormEditor<D> = TextFormState<D>;
163pub type FormInputEventOutcome = TextFormEventOutcome;
164pub type TextAreaEditor<P> = TextAreaState<P>;
165pub type TextInputEditor<P> = TextInputState<P>;
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum DefaultCursorBehavior {
169 Hidden,
170 Active { mode: AppMode },
171 InactiveUnderscore,
172}
173
174#[derive(Debug, Clone)]
175pub enum CanvasDispatchOutcome<O = (), M = ()> {
176 Applied(::canvas::ActionResult),
177 Focus(FocusIntent<O, M>),
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
181pub enum CanvasKeyDispatchOutcome<O = (), M = ()> {
182 Consumed(Option<String>),
183 PendingSequence,
184 NotHandled,
185 Focus(FocusIntent<O, M>),
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum CanvasTextWidgetOutcome<O = (), M = ()> {
190 Handled,
191 Submitted,
192 NotHandled,
193 Focus(FocusIntent<O, M>),
194}
195
196impl<O, M> CanvasDispatchOutcome<O, M> {
197 pub fn into_focus_intent(self) -> Option<FocusIntent<O, M>> {
198 match self {
199 CanvasDispatchOutcome::Applied(_) => None,
200 CanvasDispatchOutcome::Focus(intent) => Some(intent),
201 }
202 }
203}
204
205impl<O, M> CanvasKeyDispatchOutcome<O, M> {
206 pub fn into_focus_intent(self) -> Option<FocusIntent<O, M>> {
207 match self {
208 CanvasKeyDispatchOutcome::Focus(intent) => Some(intent),
209 _ => None,
210 }
211 }
212}
213
214impl<O, M> CanvasTextWidgetOutcome<O, M> {
215 pub fn into_focus_intent(self) -> Option<FocusIntent<O, M>> {
216 match self {
217 CanvasTextWidgetOutcome::Focus(intent) => Some(intent),
218 _ => None,
219 }
220 }
221}
222
223pub trait CanvasFormEditorHost {
224 fn mode(&self) -> AppMode;
225 fn has_keybindings(&self) -> bool;
226 fn use_keybinding_preset(&mut self, preset: BuiltinCanvasKeybindingPreset);
227 fn install_keybindings(&mut self, bindings: CanvasKeyBindings);
228 fn is_sequence_pending(&self) -> bool {
232 false
233 }
234 fn input_key(&mut self, key: KeyEvent) -> CanvasKeyDispatchOutcome;
235 fn paste(&mut self, text: &str) -> bool;
238 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> CanvasDispatchOutcome;
239}
240
241impl<D> CanvasFormEditorHost for FormEditor<D>
242where
243 D: DataProvider,
244{
245 fn mode(&self) -> AppMode {
246 self.core().mode()
247 }
248
249 fn has_keybindings(&self) -> bool {
250 self.core().has_keybindings()
251 }
252
253 fn use_keybinding_preset(&mut self, preset: BuiltinCanvasKeybindingPreset) {
254 FormEditor::use_keybinding_preset(self, preset);
255 }
256
257 fn install_keybindings(&mut self, bindings: CanvasKeyBindings) {
258 FormEditor::set_keybindings(self, bindings);
259 }
260
261 fn is_sequence_pending(&self) -> bool {
262 FormEditor::is_sequence_pending(self)
263 }
264
265 fn input_key(&mut self, key: KeyEvent) -> CanvasKeyDispatchOutcome {
266 dispatch_key_event(self, key)
267 }
268
269 fn paste(&mut self, text: &str) -> bool {
270 matches!(FormEditor::paste(self, text), TextFormEventOutcome::Handled)
271 }
272
273 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> CanvasDispatchOutcome {
274 dispatch_action(self, action)
275 }
276}
277
278pub trait CanvasTextAreaHost {
279 fn mode(&self) -> AppMode;
280 fn has_keybindings(&self) -> bool;
281 fn use_keybinding_preset(&mut self, preset: BuiltinCanvasKeybindingPreset);
282 fn install_keybindings(&mut self, bindings: CanvasKeyBindings);
283 fn is_sequence_pending(&self) -> bool {
287 false
288 }
289 fn commandline_enabled(&self) -> bool {
290 false
291 }
292 fn boundary_for_key(&self, key: &KeyEvent) -> Option<BoundaryExit>;
293 fn input_key(&mut self, key: KeyEvent) -> TextAreaEventOutcome;
294 fn paste(&mut self, text: &str) -> bool;
297 fn exit_edit_mode(&mut self);
298 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> HostActionOutcome;
299}
300
301impl<P> CanvasTextAreaHost for TextAreaState<P>
302where
303 P: TextAreaDataProvider,
304{
305 fn mode(&self) -> AppMode {
306 self.mode()
307 }
308
309 fn has_keybindings(&self) -> bool {
310 self.core().has_keybindings()
311 }
312
313 fn use_keybinding_preset(&mut self, preset: BuiltinCanvasKeybindingPreset) {
314 TextAreaState::use_keybinding_preset(self, preset);
315 }
316
317 fn install_keybindings(&mut self, bindings: CanvasKeyBindings) {
318 TextAreaState::set_keybindings(self, bindings);
319 }
320
321 fn is_sequence_pending(&self) -> bool {
322 TextAreaState::is_sequence_pending(self)
323 }
324
325 fn commandline_enabled(&self) -> bool {
326 self.commandline().is_some()
327 }
328
329 fn boundary_for_key(&self, key: &KeyEvent) -> Option<BoundaryExit> {
330 text_area_boundary_for_key(self, key)
331 }
332
333 fn input_key(&mut self, key: KeyEvent) -> TextAreaEventOutcome {
334 if self.core().has_keybindings() || self.commandline_enabled() {
335 match self.handle_key_event(key) {
336 KeyEventOutcome::Consumed(_)
337 | KeyEventOutcome::Pending
338 | KeyEventOutcome::ExitTop
339 | KeyEventOutcome::ExitBottom => TextAreaEventOutcome::Handled,
340 KeyEventOutcome::NotMatched => TextAreaEventOutcome::Ignored,
341 }
342 } else {
343 self.input(key)
344 }
345 }
346
347 fn paste(&mut self, text: &str) -> bool {
348 matches!(
349 TextAreaState::paste(self, text),
350 TextAreaEventOutcome::Handled
351 )
352 }
353
354 fn exit_edit_mode(&mut self) {
355 let _ = self.core_mut().exit_edit_mode();
356 }
357
358 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> HostActionOutcome {
359 HostActionOutcome::Applied(self.core_mut().execute(action))
360 }
361}
362
363pub trait CanvasTextInputHost {
368 fn mode(&self) -> AppMode;
369 fn text(&self) -> String;
370 fn has_keybindings(&self) -> bool;
371 fn install_keybindings(&mut self, bindings: CanvasKeyBindings);
372 fn is_sequence_pending(&self) -> bool {
375 false
376 }
377 fn input_key(&mut self, key: KeyEvent) -> CanvasKeyDispatchOutcome;
378 fn accept_suggestion_suffix(&mut self) -> bool;
381 fn paste(&mut self, text: &str) -> bool;
384 fn set_suggestion_suffix(&mut self, suffix: String);
385 fn clear_suggestion_suffix(&mut self);
386 fn exit_edit_mode(&mut self);
387 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> HostActionOutcome;
388}
389
390impl<P> CanvasTextInputHost for TextInputState<P>
391where
392 P: TextInputDataProvider,
393{
394 fn mode(&self) -> AppMode {
395 self.mode()
396 }
397
398 fn text(&self) -> String {
399 TextInputState::text(self)
400 }
401
402 fn has_keybindings(&self) -> bool {
403 self.form().core().has_keybindings()
404 }
405
406 fn install_keybindings(&mut self, bindings: CanvasKeyBindings) {
407 self.form_mut().set_keybindings(bindings);
408 }
409
410 fn is_sequence_pending(&self) -> bool {
411 self.form().is_sequence_pending()
412 }
413
414 fn input_key(&mut self, key: KeyEvent) -> CanvasKeyDispatchOutcome {
415 dispatch_key_event(self.form_mut(), key)
416 }
417
418 fn accept_suggestion_suffix(&mut self) -> bool {
419 matches!(
420 TextInputState::accept_suggestion_suffix(self),
421 TextInputEventOutcome::Handled
422 )
423 }
424
425 fn paste(&mut self, text: &str) -> bool {
426 matches!(
427 TextInputState::paste(self, text),
428 TextInputEventOutcome::Handled
429 )
430 }
431
432 fn set_suggestion_suffix(&mut self, suffix: String) {
433 TextInputState::set_suggestion_suffix(self, suffix);
434 }
435
436 fn clear_suggestion_suffix(&mut self) {
437 TextInputState::clear_suggestion_suffix(self);
438 }
439
440 fn exit_edit_mode(&mut self) {
441 let _ = self.form_mut().exit_edit_mode();
442 }
443
444 fn dispatch_canvas_action(&mut self, action: CanvasAction) -> HostActionOutcome {
445 execute_action_for_host_with_options(self.form_mut(), action, false)
446 }
447}
448
449pub trait CanvasWidgetState {
450 fn canvas_form_editor_ref(&self, _id: usize) -> Option<&dyn CanvasFormEditorHost> {
451 None
452 }
453
454 fn canvas_form_editor(&mut self, _id: usize) -> Option<&mut dyn CanvasFormEditorHost> {
455 None
456 }
457
458 fn canvas_textarea_ref(&self, _focus_index: usize) -> Option<&dyn CanvasTextAreaHost> {
459 None
460 }
461
462 fn canvas_textarea(&mut self, _focus_index: usize) -> Option<&mut dyn CanvasTextAreaHost> {
463 None
464 }
465
466 fn canvas_textarea_entered_ref(&self, _focus_index: usize) -> Option<&bool> {
467 None
468 }
469
470 fn canvas_textarea_entered(&mut self, _focus_index: usize) -> Option<&mut bool> {
471 None
472 }
473
474 fn canvas_textinput_ref(&self, _focus_index: usize) -> Option<&dyn CanvasTextInputHost> {
475 None
476 }
477
478 fn canvas_textinput(&mut self, _focus_index: usize) -> Option<&mut dyn CanvasTextInputHost> {
479 None
480 }
481
482 fn canvas_textinput_entered_ref(&self, _focus_index: usize) -> Option<&bool> {
483 None
484 }
485
486 fn canvas_textinput_entered(&mut self, _focus_index: usize) -> Option<&mut bool> {
487 None
488 }
489
490 fn canvas_textinput_suggestion_suffix(
491 &mut self,
492 _focus_index: usize,
493 _text: &str,
494 ) -> Option<String> {
495 None
496 }
497}
498
499pub fn mode_for_app_mode(mode: AppMode) -> ModeId {
500 match mode {
501 AppMode::Ins => modes::INSERT,
502 AppMode::Sel => modes::SELECT,
503 AppMode::Command => modes::COMMAND,
504 AppMode::General => modes::GENERAL,
505 AppMode::Nor => modes::NORMAL,
506 }
507}
508
509pub fn modes_for_app_mode(mode: AppMode) -> Vec<ModeId> {
510 match mode {
511 AppMode::Command => vec![modes::COMMAND],
512 AppMode::General => vec![modes::GENERAL, modes::GLOBAL],
513 mode => vec![mode_for_app_mode(mode), modes::COMMON, modes::GLOBAL],
514 }
515}
516
517pub fn accepts_text_input(mode: AppMode) -> bool {
518 matches!(mode, AppMode::Ins | AppMode::Command)
519}
520
521pub fn text_chord_to_canvas_action(chord: KeyChord) -> Option<CanvasAction> {
522 let is_plain_char = chord.modifiers.is_empty() || chord.modifiers == KeyModifiers::SHIFT;
523 match chord.code {
524 KeyCode::Char(c) if is_plain_char => Some(CanvasAction::InsertChar(c)),
525 _ => None,
526 }
527}
528
529pub fn text_chord_to_action<A>(chord: KeyChord) -> Option<A>
530where
531 A: From<CanvasAction>,
532{
533 text_chord_to_canvas_action(chord).map(A::from)
534}
535
536#[cfg(feature = "canvas")]
543fn seed_canvas_profile_if_unconfigured(
544 handle: &crate::runtime::CanvasKeybindingProfileHandle,
545 preset: BuiltinCanvasKeybindingPreset,
546) {
547 let mut state = handle.borrow_mut();
548 if state.generation == 0 {
549 state.replace(preset.profile());
550 }
551}
552
553pub fn focus_intent_for_boundary<O, M>(boundary: BoundaryExit) -> FocusIntent<O, M> {
554 match boundary {
555 BoundaryExit::Top => FocusIntent::ExitCanvasBackward,
556 BoundaryExit::Bottom => FocusIntent::ExitCanvasForward,
557 }
558}
559
560pub fn dispatch_action<D, O, M>(
561 editor: &mut FormEditor<D>,
562 action: CanvasAction,
563) -> CanvasDispatchOutcome<O, M>
564where
565 D: DataProvider,
566{
567 let before_field = editor.current_field();
568 let at_boundary = action_boundary(editor, &action).is_some();
569 match execute_action_for_host(editor, action) {
570 HostActionOutcome::Applied(result) => CanvasDispatchOutcome::Applied(
571 validation_aware_action_result(editor, before_field, at_boundary, result),
572 ),
573 HostActionOutcome::ExitCanvas(boundary) => {
574 CanvasDispatchOutcome::Focus(focus_intent_for_boundary(boundary))
575 }
576 }
577}
578
579pub fn render_canvas<T, D>(
580 frame: &mut Frame,
581 area: Rect,
582 editor: &FormEditor<D>,
583 theme: &T,
584) -> Option<Rect>
585where
586 T: CanvasTheme,
587 D: DataProvider,
588{
589 ::canvas::render_canvas(frame, area, editor, theme)
590}
591
592pub fn render_canvas_unmanaged_cursor<T, D>(
593 frame: &mut Frame,
594 area: Rect,
595 editor: &FormEditor<D>,
596 theme: &T,
597) -> Option<Rect>
598where
599 T: CanvasTheme,
600 D: DataProvider,
601{
602 ::canvas::render_canvas(frame, area, editor, theme)
603}
604
605pub fn render_canvas_with_options<T, D>(
606 frame: &mut Frame,
607 area: Rect,
608 editor: &FormEditor<D>,
609 theme: &T,
610 opts: CanvasDisplayOptions,
611) -> Option<Rect>
612where
613 T: CanvasTheme,
614 D: DataProvider,
615{
616 ::canvas::render_canvas_with_options(frame, area, editor, theme, opts)
617}
618
619pub fn render_canvas_with_options_unmanaged_cursor<T, D>(
620 frame: &mut Frame,
621 area: Rect,
622 editor: &FormEditor<D>,
623 theme: &T,
624 opts: CanvasDisplayOptions,
625) -> Option<Rect>
626where
627 T: CanvasTheme,
628 D: DataProvider,
629{
630 ::canvas::render_canvas_with_options(frame, area, editor, theme, opts)
631}
632
633pub fn render_canvas_default<D>(
634 frame: &mut Frame,
635 area: Rect,
636 editor: &FormEditor<D>,
637) -> Option<Rect>
638where
639 D: DataProvider,
640{
641 let theme = DefaultCanvasTheme;
642 render_canvas(frame, area, editor, &theme)
643}
644
645pub fn render_canvas_default_unmanaged_cursor<D>(
646 frame: &mut Frame,
647 area: Rect,
648 editor: &FormEditor<D>,
649) -> Option<Rect>
650where
651 D: DataProvider,
652{
653 let theme = DefaultCanvasTheme;
654 render_canvas_unmanaged_cursor(frame, area, editor, &theme)
655}
656
657pub fn render_canvas_with_suggestions<T, D>(
658 frame: &mut Frame,
659 frame_area: Rect,
660 canvas_area: Rect,
661 editor: &FormEditor<D>,
662 theme: &T,
663) -> Option<Rect>
664where
665 T: CanvasTheme,
666 D: DataProvider,
667{
668 let opts = CanvasDisplayOptions::default();
669 render_canvas_with_suggestions_with_options(frame, frame_area, canvas_area, editor, theme, opts)
670}
671
672pub fn render_canvas_with_suggestions_with_options<T, D>(
673 frame: &mut Frame,
674 frame_area: Rect,
675 canvas_area: Rect,
676 editor: &FormEditor<D>,
677 theme: &T,
678 opts: CanvasDisplayOptions,
679) -> Option<Rect>
680where
681 T: CanvasTheme,
682 D: DataProvider,
683{
684 let input_rect = render_canvas_with_options(frame, canvas_area, editor, theme, opts);
685 if let Some(input_rect) = input_rect {
686 render_suggestions_dropdown(frame, frame_area, input_rect, theme, editor);
687 }
688 input_rect
689}
690
691pub fn render_canvas_with_suggestions_default<D>(
692 frame: &mut Frame,
693 frame_area: Rect,
694 canvas_area: Rect,
695 editor: &FormEditor<D>,
696) -> Option<Rect>
697where
698 D: DataProvider,
699{
700 let theme = DefaultCanvasTheme;
701 render_canvas_with_suggestions(frame, frame_area, canvas_area, editor, &theme)
702}
703
704pub fn render_canvas_with_suggestions_default_options<D>(
705 frame: &mut Frame,
706 frame_area: Rect,
707 canvas_area: Rect,
708 editor: &FormEditor<D>,
709 opts: CanvasDisplayOptions,
710) -> Option<Rect>
711where
712 D: DataProvider,
713{
714 let theme = DefaultCanvasTheme;
715 render_canvas_with_suggestions_with_options(
716 frame,
717 frame_area,
718 canvas_area,
719 editor,
720 &theme,
721 opts,
722 )
723}
724
725pub fn update_cursor_style_for_mode(mode: AppMode) -> std::io::Result<()> {
726 CursorManager::update_for_mode(mode)
727}
728
729pub fn update_default_cursor_style(behavior: DefaultCursorBehavior) -> std::io::Result<()> {
730 match behavior {
731 DefaultCursorBehavior::Hidden => execute!(io::stdout(), crossterm::cursor::Hide),
732 DefaultCursorBehavior::Active { mode } => {
733 execute!(io::stdout(), crossterm::cursor::Show)?;
734 update_cursor_style_for_mode(mode)
735 }
736 DefaultCursorBehavior::InactiveUnderscore => {
737 execute!(io::stdout(), crossterm::cursor::Show)?;
738 update_cursor_style_for_mode(AppMode::Command)
739 }
740 }
741}
742
743pub fn update_cursor_style_for_editor<D>(editor: &FormEditor<D>) -> std::io::Result<()>
744where
745 D: DataProvider,
746{
747 update_cursor_style_for_mode(editor.mode())
748}
749
750pub fn dispatch_key_event<D, O, M>(
751 editor: &mut FormEditor<D>,
752 event: KeyEvent,
753) -> CanvasKeyDispatchOutcome<O, M>
754where
755 D: DataProvider,
756{
757 let before_field = editor.current_field();
758 let before_boundary = key_boundary(editor, &event);
759 let outcome = handle_key_event_for_host(editor, event);
760 host_key_event_outcome(validation_aware_key_event_outcome(
761 editor,
762 before_field,
763 before_boundary,
764 outcome,
765 ))
766}
767
768pub fn host_key_event_outcome<O, M>(
769 outcome: HostKeyEventOutcome,
770) -> CanvasKeyDispatchOutcome<O, M> {
771 match outcome {
772 HostKeyEventOutcome::Consumed(message) => CanvasKeyDispatchOutcome::Consumed(message),
773 HostKeyEventOutcome::PendingSequence => CanvasKeyDispatchOutcome::PendingSequence,
774 HostKeyEventOutcome::NotHandled => CanvasKeyDispatchOutcome::NotHandled,
775 HostKeyEventOutcome::ExitCanvas(boundary) => {
776 CanvasKeyDispatchOutcome::Focus(focus_intent_for_boundary(boundary))
777 }
778 }
779}
780
781fn validation_aware_action_result<D>(
782 editor: &FormEditor<D>,
783 before_field: usize,
784 at_boundary: bool,
785 result: ActionResult,
786) -> ActionResult
787where
788 D: DataProvider,
789{
790 if editor.current_field() == before_field && !at_boundary {
791 if let Some(reason) = editor.last_switch_block() {
792 return ActionResult::Error(reason.to_string());
793 }
794 }
795 result
796}
797
798fn validation_aware_key_event_outcome<D>(
799 editor: &FormEditor<D>,
800 before_field: usize,
801 before_boundary: Option<BoundaryExit>,
802 outcome: HostKeyEventOutcome,
803) -> HostKeyEventOutcome
804where
805 D: DataProvider,
806{
807 if matches!(outcome, HostKeyEventOutcome::ExitCanvas(_))
808 && editor.current_field() == before_field
809 && before_boundary.is_none()
810 {
811 if let Some(reason) = editor.last_switch_block() {
812 return HostKeyEventOutcome::Consumed(Some(reason.to_string()));
813 }
814 }
815 outcome
816}
817
818fn action_boundary<D>(editor: &FormEditor<D>, action: &CanvasAction) -> Option<BoundaryExit>
819where
820 D: DataProvider,
821{
822 match action {
823 CanvasAction::MoveUp | CanvasAction::PrevField if editor.current_field() == 0 => {
824 Some(BoundaryExit::Top)
825 }
826 CanvasAction::MoveDown | CanvasAction::NextField
827 if editor.current_field() >= editor.data_provider().field_count().saturating_sub(1) =>
828 {
829 Some(BoundaryExit::Bottom)
830 }
831 _ => None,
832 }
833}
834
835fn key_boundary<D>(editor: &FormEditor<D>, event: &KeyEvent) -> Option<BoundaryExit>
836where
837 D: DataProvider,
838{
839 match event.code {
840 KeyCode::Up | KeyCode::BackTab if editor.current_field() == 0 => Some(BoundaryExit::Top),
841 KeyCode::Down | KeyCode::Tab
842 if editor.current_field() >= editor.data_provider().field_count().saturating_sub(1) =>
843 {
844 Some(BoundaryExit::Bottom)
845 }
846 _ => None,
847 }
848}
849
850pub fn key_dispatch_status<A, O, M>(
851 outcome: &CanvasKeyDispatchOutcome<O, M>,
852) -> Option<TuiPagesStatus<A>> {
853 match outcome {
854 CanvasKeyDispatchOutcome::Consumed(_) | CanvasKeyDispatchOutcome::Focus(_) => {
855 Some(TuiPagesStatus::ActionHandled)
856 }
857 CanvasKeyDispatchOutcome::PendingSequence => Some(TuiPagesStatus::Waiting(Vec::new())),
858 CanvasKeyDispatchOutcome::NotHandled => None,
859 }
860}
861
862pub fn dispatch_text_input_key<P, O, M>(
863 input: &mut TextInputState<P>,
864 event: KeyEvent,
865) -> CanvasTextWidgetOutcome<O, M>
866where
867 P: TextInputDataProvider,
868{
869 let boundary = text_input_boundary_for_key(&event);
870 match input.input(event) {
871 TextInputEventOutcome::Handled => CanvasTextWidgetOutcome::Handled,
872 TextInputEventOutcome::Submitted => CanvasTextWidgetOutcome::Submitted,
873 TextInputEventOutcome::Ignored => boundary
874 .map(|boundary| CanvasTextWidgetOutcome::Focus(focus_intent_for_boundary(boundary)))
875 .unwrap_or(CanvasTextWidgetOutcome::NotHandled),
876 }
877}
878
879pub fn dispatch_text_area_key<P, O, M>(
880 textarea: &mut TextAreaState<P>,
881 event: KeyEvent,
882) -> CanvasTextWidgetOutcome<O, M>
883where
884 P: TextAreaDataProvider,
885{
886 if let Some(boundary) = text_area_boundary_for_key(textarea, &event) {
887 return CanvasTextWidgetOutcome::Focus(focus_intent_for_boundary(boundary));
888 }
889
890 if textarea.commandline().is_some() {
891 match textarea.handle_key_event(event) {
892 KeyEventOutcome::Consumed(_) | KeyEventOutcome::Pending => {
893 CanvasTextWidgetOutcome::Handled
894 }
895 KeyEventOutcome::ExitTop => {
896 CanvasTextWidgetOutcome::Focus(focus_intent_for_boundary(BoundaryExit::Top))
897 }
898 KeyEventOutcome::ExitBottom => {
899 CanvasTextWidgetOutcome::Focus(focus_intent_for_boundary(BoundaryExit::Bottom))
900 }
901 KeyEventOutcome::NotMatched => CanvasTextWidgetOutcome::NotHandled,
902 }
903 } else {
904 match textarea.input(event) {
905 TextAreaEventOutcome::Handled => CanvasTextWidgetOutcome::Handled,
906 TextAreaEventOutcome::Ignored => CanvasTextWidgetOutcome::NotHandled,
907 }
908 }
909}
910
911pub fn text_input_boundary_for_key(event: &KeyEvent) -> Option<BoundaryExit> {
912 if event.kind != KeyEventKind::Press {
913 return None;
914 }
915
916 match (event.code, event.modifiers) {
917 (KeyCode::Up | KeyCode::BackTab, _) => Some(BoundaryExit::Top),
918 (KeyCode::Down, _) => Some(BoundaryExit::Bottom),
919 (KeyCode::Tab, modifiers) if modifiers.is_empty() => Some(BoundaryExit::Bottom),
920 _ => None,
921 }
922}
923
924pub fn text_area_boundary_for_key<P>(
925 textarea: &TextAreaState<P>,
926 event: &KeyEvent,
927) -> Option<BoundaryExit>
928where
929 P: TextAreaDataProvider,
930{
931 if event.kind != KeyEventKind::Press {
932 return None;
933 }
934
935 let current = textarea.current_field();
936 let last = textarea.data_provider().field_count().saturating_sub(1);
937
938 match event.code {
939 KeyCode::Up | KeyCode::BackTab if current == 0 => Some(BoundaryExit::Top),
940 KeyCode::Down if current >= last => Some(BoundaryExit::Bottom),
941 _ => None,
942 }
943}
944
945pub fn bind_default_keymaps<A>(
946 normal: &mut KeyMap<A>,
947 insert: &mut KeyMap<A>,
948 select: &mut KeyMap<A>,
949) where
950 A: From<CanvasAction>,
951{
952 bind_builtin_keymaps(BuiltinCanvasKeybindingPreset::Vim, normal, insert, select);
953}
954
955pub fn bind_builtin_keymaps<A>(
956 preset: BuiltinCanvasKeybindingPreset,
957 normal: &mut KeyMap<A>,
958 insert: &mut KeyMap<A>,
959 select: &mut KeyMap<A>,
960) where
961 A: From<CanvasAction>,
962{
963 bind_builtin_defaults_for_mode(preset, normal, AppMode::Nor);
964 bind_builtin_defaults_for_mode(preset, insert, AppMode::Ins);
965 bind_suggestion_defaults(insert);
966 bind_builtin_defaults_for_mode(preset, select, AppMode::Sel);
967 bind_suggestion_defaults(select);
968}
969
970pub fn bind_normal_defaults<A>(map: &mut KeyMap<A>)
971where
972 A: From<CanvasAction>,
973{
974 bind_builtin_defaults_for_mode(BuiltinCanvasKeybindingPreset::Vim, map, AppMode::Nor);
975}
976
977pub fn bind_insert_defaults<A>(map: &mut KeyMap<A>)
978where
979 A: From<CanvasAction>,
980{
981 bind_builtin_defaults_for_mode(BuiltinCanvasKeybindingPreset::Vim, map, AppMode::Ins);
982 bind_suggestion_defaults(map);
983}
984
985pub fn bind_select_defaults<A>(map: &mut KeyMap<A>)
986where
987 A: From<CanvasAction>,
988{
989 bind_builtin_defaults_for_mode(BuiltinCanvasKeybindingPreset::Vim, map, AppMode::Sel);
990 bind_suggestion_defaults(map);
991}
992
993fn bind_builtin_defaults_for_mode<A>(
994 preset: BuiltinCanvasKeybindingPreset,
995 map: &mut KeyMap<A>,
996 mode: AppMode,
997) where
998 A: From<CanvasAction>,
999{
1000 for binding in default_builtin_action_bindings(preset)
1001 .into_iter()
1002 .filter(|binding| binding.mode == mode)
1003 {
1004 let sequence = binding
1005 .sequence
1006 .into_iter()
1007 .map(|stroke| KeyChord::new(stroke.code, stroke.modifiers))
1008 .collect::<Vec<_>>();
1009 map.bind(sequence, A::from(binding.action));
1010 }
1011}
1012
1013fn normalize_shift(mut key: KeyEvent) -> KeyEvent {
1014 if matches!(key.code, KeyCode::Char(_)) && key.modifiers == KeyModifiers::SHIFT {
1015 key.modifiers = KeyModifiers::NONE;
1016 }
1017 key
1018}
1019
1020fn focused_canvas_field<V, O>(ctx: &ActionContext<V, O>, index: usize) -> bool {
1021 matches!(
1022 ctx.focus.as_ref(),
1023 Some(FocusTarget::CanvasField(field) | FocusTarget::InternalCanvasField(field))
1024 if *field == index
1025 )
1026}
1027
1028fn input_layer_context_for_mode(mode: AppMode) -> InputLayerContext {
1029 if accepts_text_input(mode) {
1030 InputLayerContext::Text
1031 } else {
1032 InputLayerContext::Command
1033 }
1034}
1035
1036fn focus_intent_for_top_level_key<O, M>(key: KeyEvent) -> Option<FocusIntent<O, M>> {
1037 match (key.code, key.modifiers) {
1038 (KeyCode::Down | KeyCode::Tab, _) => Some(FocusIntent::ExitCanvasForward),
1039 (KeyCode::Char('j') | KeyCode::Char('l'), modifiers)
1040 if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT =>
1041 {
1042 Some(FocusIntent::ExitCanvasForward)
1043 }
1044 (KeyCode::Up | KeyCode::BackTab, _) => Some(FocusIntent::ExitCanvasBackward),
1045 (KeyCode::Char('k') | KeyCode::Char('h'), modifiers)
1046 if modifiers.is_empty() || modifiers == KeyModifiers::SHIFT =>
1047 {
1048 Some(FocusIntent::ExitCanvasBackward)
1049 }
1050 _ => None,
1051 }
1052}
1053
1054fn hook_outcome<V, A, O, M>(
1055 status: TuiPagesStatus<A>,
1056 outcome: ActionOutcome<V, O, M>,
1057 routing: KeyHookRouting,
1058) -> Option<KeyHookOutcome<V, A, O, M>> {
1059 Some(KeyHookOutcome {
1060 status,
1061 outcome,
1062 routing,
1063 })
1064}
1065
1066fn hook_focus_outcome<V, A, O, M>(intent: FocusIntent<O, M>) -> Option<KeyHookOutcome<V, A, O, M>> {
1067 hook_outcome(
1068 TuiPagesStatus::ActionHandled,
1069 ActionOutcome::effect(TuiEffect::Focus(intent)),
1070 KeyHookRouting::Handled,
1071 )
1072}
1073
1074fn hook_status_outcome<V, A, O, M>(
1075 status: TuiPagesStatus<A>,
1076) -> Option<KeyHookOutcome<V, A, O, M>> {
1077 hook_outcome(status, ActionOutcome::none(), KeyHookRouting::Handled)
1078}
1079
1080fn hook_pending_outcome<V, A, O, M>(
1081 status: TuiPagesStatus<A>,
1082) -> Option<KeyHookOutcome<V, A, O, M>> {
1083 hook_outcome(status, ActionOutcome::none(), KeyHookRouting::Pending)
1084}
1085
1086fn refresh_textinput_suggestion_suffix<S>(state: &mut S, focus_index: usize)
1087where
1088 S: CanvasWidgetState + ?Sized,
1089{
1090 let Some(text) = state
1091 .canvas_textinput(focus_index)
1092 .map(|input| input.text())
1093 else {
1094 return;
1095 };
1096 let suffix = state.canvas_textinput_suggestion_suffix(focus_index, &text);
1097 let Some(input) = state.canvas_textinput(focus_index) else {
1098 return;
1099 };
1100 if let Some(suffix) = suffix {
1101 input.set_suggestion_suffix(suffix);
1102 } else {
1103 input.clear_suggestion_suffix();
1104 }
1105}
1106
1107pub(crate) fn canvas_hook_context<V, S, O>(
1108 kind: &KeyHookKind,
1109 ctx: &ActionContext<V, O>,
1110 state: &S,
1111) -> Option<InputLayerContext>
1112where
1113 S: CanvasWidgetState + ?Sized,
1114{
1115 match kind {
1116 KeyHookKind::CanvasFormEditor { id, .. } => {
1117 if !ctx.focus.as_ref().is_some_and(FocusTarget::is_canvas) {
1118 return None;
1119 }
1120 state
1121 .canvas_form_editor_ref(*id)
1122 .map(|editor| input_layer_context_for_mode(editor.mode()))
1123 }
1124 KeyHookKind::CanvasTextArea { focus_index, .. } => {
1125 if !focused_canvas_field(ctx, *focus_index) {
1126 return None;
1127 }
1128 if !state
1129 .canvas_textarea_entered_ref(*focus_index)
1130 .is_some_and(|entered| *entered)
1131 {
1132 return Some(InputLayerContext::Command);
1133 }
1134 state
1135 .canvas_textarea_ref(*focus_index)
1136 .map(|textarea| input_layer_context_for_mode(textarea.mode()))
1137 }
1138 KeyHookKind::CanvasTextInput { focus_index, .. } => {
1139 if !focused_canvas_field(ctx, *focus_index) {
1140 return None;
1141 }
1142 if !state
1143 .canvas_textinput_entered_ref(*focus_index)
1144 .is_some_and(|entered| *entered)
1145 {
1146 return Some(InputLayerContext::Command);
1147 }
1148 state
1149 .canvas_textinput_ref(*focus_index)
1150 .map(|input| input_layer_context_for_mode(input.mode()))
1151 }
1152 }
1153}
1154
1155pub(crate) fn canvas_hook_cursor_behavior<V, S, O>(
1156 kind: &KeyHookKind,
1157 ctx: &ActionContext<V, O>,
1158 state: &S,
1159) -> Option<DefaultCursorBehavior>
1160where
1161 S: CanvasWidgetState + ?Sized,
1162{
1163 match kind {
1164 KeyHookKind::CanvasFormEditor { id, .. } => {
1165 let editor = state.canvas_form_editor_ref(*id)?;
1166 if ctx.focus.as_ref().is_some_and(FocusTarget::is_canvas) {
1167 Some(DefaultCursorBehavior::Active {
1168 mode: editor.mode(),
1169 })
1170 } else {
1171 Some(DefaultCursorBehavior::InactiveUnderscore)
1172 }
1173 }
1174 KeyHookKind::CanvasTextArea { focus_index, .. } => {
1175 let textarea = state.canvas_textarea_ref(*focus_index)?;
1176 if focused_canvas_field(ctx, *focus_index)
1177 && state
1178 .canvas_textarea_entered_ref(*focus_index)
1179 .is_some_and(|entered| *entered)
1180 {
1181 Some(DefaultCursorBehavior::Active {
1182 mode: textarea.mode(),
1183 })
1184 } else {
1185 Some(DefaultCursorBehavior::InactiveUnderscore)
1186 }
1187 }
1188 KeyHookKind::CanvasTextInput { focus_index, .. } => {
1189 let input = state.canvas_textinput_ref(*focus_index)?;
1190 if focused_canvas_field(ctx, *focus_index)
1191 && state
1192 .canvas_textinput_entered_ref(*focus_index)
1193 .is_some_and(|entered| *entered)
1194 {
1195 Some(DefaultCursorBehavior::Active { mode: input.mode() })
1196 } else {
1197 Some(DefaultCursorBehavior::InactiveUnderscore)
1198 }
1199 }
1200 }
1201}
1202
1203fn canvas_profile_generation_and_bindings(
1204 profile: &crate::runtime::CanvasKeybindingProfileHandle,
1205) -> (u64, CanvasKeyBindings) {
1206 let profile = profile.borrow();
1207 (profile.generation, profile.profile.current().clone())
1208}
1209
1210fn install_form_editor_profile(
1211 editor: &mut dyn CanvasFormEditorHost,
1212 profile: &crate::runtime::CanvasKeybindingProfileHandle,
1213 installed_generation: &mut Option<u64>,
1214) {
1215 let (generation, bindings) = canvas_profile_generation_and_bindings(profile);
1216 if *installed_generation != Some(generation) || !editor.has_keybindings() {
1217 editor.install_keybindings(bindings);
1218 *installed_generation = Some(generation);
1219 }
1220}
1221
1222fn install_textarea_profile(
1223 textarea: &mut dyn CanvasTextAreaHost,
1224 profile: &crate::runtime::CanvasKeybindingProfileHandle,
1225 installed_generation: &mut Option<u64>,
1226) {
1227 let (generation, bindings) = canvas_profile_generation_and_bindings(profile);
1228 if *installed_generation != Some(generation) || !textarea.has_keybindings() {
1229 textarea.install_keybindings(bindings);
1230 *installed_generation = Some(generation);
1231 }
1232}
1233
1234fn install_textinput_profile(
1235 input: &mut dyn CanvasTextInputHost,
1236 profile: &crate::runtime::CanvasKeybindingProfileHandle,
1237 installed_generation: &mut Option<u64>,
1238) {
1239 let (generation, bindings) = canvas_profile_generation_and_bindings(profile);
1240 if *installed_generation != Some(generation) || !input.has_keybindings() {
1241 input.install_keybindings(bindings);
1242 *installed_generation = Some(generation);
1243 }
1244}
1245
1246pub(crate) fn dispatch_canvas_key_hook<V, A, S, O, M>(
1247 kind: &mut KeyHookKind,
1248 key: KeyEvent,
1249 ctx: ActionContext<V, O>,
1250 state: &mut S,
1251) -> Option<KeyHookOutcome<V, A, O, M>>
1252where
1253 S: CanvasWidgetState + ?Sized,
1254{
1255 match kind {
1256 KeyHookKind::CanvasFormEditor {
1257 id,
1258 profile,
1259 installed_generation,
1260 } => {
1261 if !ctx.focus.as_ref().is_some_and(FocusTarget::is_canvas) {
1262 return None;
1263 }
1264
1265 let editor = state.canvas_form_editor(*id)?;
1266 install_form_editor_profile(editor, profile, installed_generation);
1267
1268 let outcome = editor.input_key(normalize_shift(key));
1269 let pending = state.canvas_form_editor(*id)?.is_sequence_pending();
1273 match outcome {
1274 CanvasKeyDispatchOutcome::Consumed(_) if pending => {
1275 hook_pending_outcome(TuiPagesStatus::Waiting(Vec::new()))
1276 }
1277 CanvasKeyDispatchOutcome::Consumed(_) => {
1278 hook_status_outcome(TuiPagesStatus::ActionHandled)
1279 }
1280 CanvasKeyDispatchOutcome::PendingSequence => {
1281 hook_pending_outcome(TuiPagesStatus::Waiting(Vec::new()))
1282 }
1283 CanvasKeyDispatchOutcome::NotHandled => None,
1284 CanvasKeyDispatchOutcome::Focus(FocusIntent::ExitCanvasForward) => {
1285 hook_focus_outcome(FocusIntent::ExitCanvasForward)
1286 }
1287 CanvasKeyDispatchOutcome::Focus(FocusIntent::ExitCanvasBackward) => {
1288 hook_focus_outcome(FocusIntent::ExitCanvasBackward)
1289 }
1290 CanvasKeyDispatchOutcome::Focus(_) => {
1291 hook_status_outcome(TuiPagesStatus::ActionHandled)
1292 }
1293 }
1294 }
1295 KeyHookKind::CanvasTextArea {
1296 focus_index,
1297 profile,
1298 installed_generation,
1299 } => {
1300 if !focused_canvas_field(&ctx, *focus_index) {
1301 if let Some(entered) = state.canvas_textarea_entered(*focus_index) {
1302 *entered = false;
1303 }
1304 return None;
1305 }
1306
1307 let is_entered = state
1308 .canvas_textarea_entered(*focus_index)
1309 .is_some_and(|entered| *entered);
1310 if !is_entered {
1311 if key.kind != KeyEventKind::Press {
1312 return None;
1313 }
1314 if matches!(key.code, KeyCode::Enter) {
1315 if let Some(entered) = state.canvas_textarea_entered(*focus_index) {
1316 *entered = true;
1317 }
1318 if let Some(textarea) = state.canvas_textarea(*focus_index) {
1319 install_textarea_profile(textarea, profile, installed_generation);
1320 textarea.exit_edit_mode();
1321 }
1322 return hook_status_outcome(TuiPagesStatus::ActionHandled);
1323 }
1324 return focus_intent_for_top_level_key(key).and_then(hook_focus_outcome);
1325 }
1326
1327 if let Some(textarea) = state.canvas_textarea(*focus_index) {
1328 install_textarea_profile(textarea, profile, installed_generation);
1329 }
1330
1331 let mode = state.canvas_textarea(*focus_index)?.mode();
1332 if mode == AppMode::Ins {
1333 return match (key.code, key.modifiers) {
1334 (KeyCode::Char('c'), KeyModifiers::CONTROL) => None,
1335 (KeyCode::Esc, _) => {
1336 state.canvas_textarea(*focus_index)?.exit_edit_mode();
1337 hook_status_outcome(TuiPagesStatus::ActionHandled)
1338 }
1339 _ => match state
1340 .canvas_textarea(*focus_index)?
1341 .input_key(normalize_shift(key))
1342 {
1343 TextAreaEventOutcome::Handled => {
1344 hook_status_outcome(TuiPagesStatus::TextHandled)
1345 }
1346 TextAreaEventOutcome::Ignored => None,
1347 },
1348 };
1349 }
1350
1351 if matches!(
1352 (key.code, key.modifiers),
1353 (KeyCode::Char('g'), KeyModifiers::CONTROL)
1354 ) && key.kind == KeyEventKind::Press
1355 {
1356 if let Some(entered) = state.canvas_textarea_entered(*focus_index) {
1357 *entered = false;
1358 }
1359 return hook_focus_outcome(FocusIntent::ExitCanvasForward);
1360 }
1361
1362 if let Some(boundary) = state.canvas_textarea(*focus_index)?.boundary_for_key(&key) {
1363 return hook_focus_outcome(focus_intent_for_boundary(boundary));
1364 }
1365
1366 let outcome = state
1367 .canvas_textarea(*focus_index)?
1368 .input_key(normalize_shift(key));
1369 let pending = state.canvas_textarea(*focus_index)?.is_sequence_pending();
1376 match outcome {
1377 TextAreaEventOutcome::Handled if pending => {
1378 hook_pending_outcome(TuiPagesStatus::Waiting(Vec::new()))
1379 }
1380 TextAreaEventOutcome::Handled => hook_status_outcome(TuiPagesStatus::TextHandled),
1381 TextAreaEventOutcome::Ignored => None,
1382 }
1383 }
1384 KeyHookKind::CanvasTextInput {
1385 focus_index,
1386 profile,
1387 installed_generation,
1388 } => {
1389 if !focused_canvas_field(&ctx, *focus_index) {
1390 if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1391 *entered = false;
1392 }
1393 return None;
1394 }
1395
1396 let is_entered = state
1397 .canvas_textinput_entered(*focus_index)
1398 .is_some_and(|entered| *entered);
1399 if !is_entered {
1400 if key.kind != KeyEventKind::Press {
1401 return None;
1402 }
1403 if matches!(key.code, KeyCode::Enter) {
1404 if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1405 *entered = true;
1406 }
1407 return hook_status_outcome(TuiPagesStatus::ActionHandled);
1408 }
1409 return focus_intent_for_top_level_key(key).and_then(hook_focus_outcome);
1410 }
1411
1412 if let Some(input) = state.canvas_textinput(*focus_index) {
1413 install_textinput_profile(input, profile, installed_generation);
1414 }
1415
1416 if matches!(
1418 (key.code, key.modifiers),
1419 (KeyCode::Char('c'), KeyModifiers::CONTROL)
1420 ) {
1421 return None;
1422 }
1423
1424 if matches!(key.code, KeyCode::Esc) && key.kind == KeyEventKind::Press {
1427 if state.canvas_textinput(*focus_index)?.mode() == AppMode::Ins {
1428 state.canvas_textinput(*focus_index)?.exit_edit_mode();
1429 } else if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1430 *entered = false;
1431 }
1432 return hook_status_outcome(TuiPagesStatus::ActionHandled);
1433 }
1434
1435 if matches!(key.code, KeyCode::Enter) && key.kind == KeyEventKind::Press {
1438 if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1439 *entered = false;
1440 }
1441 return hook_focus_outcome(FocusIntent::ExitCanvasForward);
1442 }
1443
1444 if matches!(
1448 (key.code, key.modifiers),
1449 (KeyCode::Tab, KeyModifiers::NONE)
1450 ) && key.kind == KeyEventKind::Press
1451 && state
1452 .canvas_textinput(*focus_index)?
1453 .accept_suggestion_suffix()
1454 {
1455 refresh_textinput_suggestion_suffix(state, *focus_index);
1456 return hook_status_outcome(TuiPagesStatus::TextHandled);
1457 }
1458
1459 let outcome = state
1462 .canvas_textinput(*focus_index)?
1463 .input_key(normalize_shift(key));
1464 let pending = state.canvas_textinput(*focus_index)?.is_sequence_pending();
1465 match outcome {
1466 CanvasKeyDispatchOutcome::Consumed(_) if pending => {
1467 hook_pending_outcome(TuiPagesStatus::Waiting(Vec::new()))
1468 }
1469 CanvasKeyDispatchOutcome::Consumed(_) => {
1470 refresh_textinput_suggestion_suffix(state, *focus_index);
1471 hook_status_outcome(TuiPagesStatus::TextHandled)
1472 }
1473 CanvasKeyDispatchOutcome::PendingSequence => {
1474 hook_pending_outcome(TuiPagesStatus::Waiting(Vec::new()))
1475 }
1476 CanvasKeyDispatchOutcome::NotHandled => None,
1477 CanvasKeyDispatchOutcome::Focus(FocusIntent::ExitCanvasBackward) => {
1478 if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1479 *entered = false;
1480 }
1481 hook_focus_outcome(FocusIntent::ExitCanvasBackward)
1482 }
1483 CanvasKeyDispatchOutcome::Focus(FocusIntent::ExitCanvasForward) => {
1484 if let Some(entered) = state.canvas_textinput_entered(*focus_index) {
1485 *entered = false;
1486 }
1487 hook_focus_outcome(FocusIntent::ExitCanvasForward)
1488 }
1489 CanvasKeyDispatchOutcome::Focus(_) => {
1490 hook_status_outcome(TuiPagesStatus::ActionHandled)
1491 }
1492 }
1493 }
1494 }
1495}
1496
1497pub(crate) fn dispatch_canvas_paste_hook<V, A, S, O, M>(
1502 kind: &mut KeyHookKind,
1503 text: &str,
1504 ctx: ActionContext<V, O>,
1505 state: &mut S,
1506) -> Option<KeyHookOutcome<V, A, O, M>>
1507where
1508 S: CanvasWidgetState + ?Sized,
1509{
1510 let handled = match kind {
1511 KeyHookKind::CanvasFormEditor { id, .. } => {
1512 if !ctx.focus.as_ref().is_some_and(FocusTarget::is_canvas) {
1513 return None;
1514 }
1515 state.canvas_form_editor(*id)?.paste(text)
1516 }
1517 KeyHookKind::CanvasTextArea { focus_index, .. } => {
1518 if !focused_canvas_field(&ctx, *focus_index)
1519 || !state
1520 .canvas_textarea_entered(*focus_index)
1521 .is_some_and(|entered| *entered)
1522 {
1523 return None;
1524 }
1525 state.canvas_textarea(*focus_index)?.paste(text)
1526 }
1527 KeyHookKind::CanvasTextInput { focus_index, .. } => {
1528 if !focused_canvas_field(&ctx, *focus_index)
1529 || !state
1530 .canvas_textinput_entered(*focus_index)
1531 .is_some_and(|entered| *entered)
1532 {
1533 return None;
1534 }
1535 state.canvas_textinput(*focus_index)?.paste(text)
1536 }
1537 };
1538
1539 if handled {
1540 hook_status_outcome(TuiPagesStatus::TextHandled)
1541 } else {
1542 None
1543 }
1544}
1545
1546impl<O> PageSpec<O> {
1547 pub fn canvas_mode(mut self, mode: AppMode) -> Self {
1548 self.modes = modes_for_app_mode(mode);
1549 self.accepts_text_input = accepts_text_input(mode);
1550 self
1551 }
1552
1553 pub fn canvas_editor<D>(self, editor: &FormEditor<D>) -> Self
1554 where
1555 D: DataProvider,
1556 {
1557 self.canvas_mode(editor.mode())
1558 }
1559}
1560
1561impl<V, A, O, M, Pages, Handler, Hooks> TuiPagesBuilder<V, A, O, M, Pages, Handler, Hooks>
1562where
1563 A: From<CanvasAction>,
1564{
1565 pub fn canvas_defaults(self) -> Self {
1566 self.canvas_keybindings().canvas_text_actions()
1567 }
1568
1569 pub fn canvas_keybindings(mut self) -> Self {
1570 bind_normal_defaults(self.input_registry.map_mut(modes::NORMAL.as_str()));
1571 bind_insert_defaults(self.input_registry.map_mut(modes::INSERT.as_str()));
1572 bind_select_defaults(self.input_registry.map_mut(modes::SELECT.as_str()));
1573 self
1574 }
1575
1576 pub fn canvas_text_actions(mut self) -> Self {
1577 self.text_input_mapper = Some(text_chord_to_action::<A>);
1578 self
1579 }
1580}
1581
1582impl<V, A, O, M, Pages, Handler, Hooks> TuiPagesBuilder<V, A, O, M, Pages, Handler, Hooks> {
1583 pub fn canvas_form_editor(
1584 self,
1585 id: usize,
1586 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1587 self.canvas_form_editor_with_preset(id, BuiltinCanvasKeybindingPreset::Vim)
1588 }
1589
1590 pub fn canvas_form_editor_with_preset(
1591 mut self,
1592 id: usize,
1593 preset: BuiltinCanvasKeybindingPreset,
1594 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1595 seed_canvas_profile_if_unconfigured(&self.canvas_keybinding_profile, preset);
1596 self.key_hooks.push(KeyHook {
1597 kind: KeyHookKind::CanvasFormEditor {
1598 id,
1599 profile: self.canvas_keybinding_profile.clone(),
1600 installed_generation: None,
1601 },
1602 });
1603 self.into_canvas_hooks()
1604 }
1605
1606 pub fn canvas_textarea_widget(
1607 self,
1608 focus_index: usize,
1609 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1610 self.canvas_textarea_widget_with_preset(focus_index, BuiltinCanvasKeybindingPreset::Vim)
1611 }
1612
1613 pub fn canvas_textarea_widget_with_preset(
1614 mut self,
1615 focus_index: usize,
1616 preset: BuiltinCanvasKeybindingPreset,
1617 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1618 seed_canvas_profile_if_unconfigured(&self.canvas_keybinding_profile, preset);
1619 self.key_hooks.push(KeyHook {
1620 kind: KeyHookKind::CanvasTextArea {
1621 focus_index,
1622 profile: self.canvas_keybinding_profile.clone(),
1623 installed_generation: None,
1624 },
1625 });
1626 self.into_canvas_hooks()
1627 }
1628
1629 pub fn canvas_textinput_widget(
1630 self,
1631 focus_index: usize,
1632 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1633 self.canvas_textinput_widget_with_preset(focus_index, BuiltinCanvasKeybindingPreset::Vim)
1634 }
1635
1636 pub fn canvas_textinput_widget_with_preset(
1637 mut self,
1638 focus_index: usize,
1639 preset: BuiltinCanvasKeybindingPreset,
1640 ) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1641 seed_canvas_profile_if_unconfigured(&self.canvas_keybinding_profile, preset);
1642 self.key_hooks.push(KeyHook {
1643 kind: KeyHookKind::CanvasTextInput {
1644 focus_index,
1645 profile: self.canvas_keybinding_profile.clone(),
1646 installed_generation: None,
1647 },
1648 });
1649 self.into_canvas_hooks()
1650 }
1651
1652 fn into_canvas_hooks(self) -> TuiPagesBuilder<V, A, O, M, Pages, Handler, CanvasHooks> {
1653 TuiPagesBuilder {
1654 initial_view: self.initial_view,
1655 fallback_view: self.fallback_view,
1656 input_registry: self.input_registry,
1657 command_registry: self.command_registry,
1658 input_timeout_ms: self.input_timeout_ms,
1659 command_timeout_ms: self.command_timeout_ms,
1660 focus_wrap: self.focus_wrap,
1661 reserve_command_line: self.reserve_command_line,
1662 text_input_mapper: self.text_input_mapper,
1663 key_hooks: self.key_hooks,
1664 keybinding_store: self.keybinding_store,
1665 keybinding_report: self.keybinding_report,
1666 keybinding_inheritances: self.keybinding_inheritances,
1667 action_registry: self.action_registry,
1668 canvas_keybinding_profile: self.canvas_keybinding_profile,
1669 pages: self.pages,
1670 handler: self.handler,
1671 _overlay: PhantomData,
1672 _modal: PhantomData,
1673 _hooks: PhantomData,
1674 }
1675 }
1676}
1677
1678fn bind_suggestion_defaults<A>(map: &mut KeyMap<A>)
1679where
1680 A: From<CanvasAction>,
1681{
1682 bind_key_with_modifiers(
1683 map,
1684 KeyCode::Char(' '),
1685 KeyModifiers::CONTROL,
1686 CanvasAction::TriggerSuggestions,
1687 );
1688 bind_key_with_modifiers(
1689 map,
1690 KeyCode::Char('n'),
1691 KeyModifiers::CONTROL,
1692 CanvasAction::SuggestionDown,
1693 );
1694 bind_key_with_modifiers(
1695 map,
1696 KeyCode::Char('p'),
1697 KeyModifiers::CONTROL,
1698 CanvasAction::SuggestionUp,
1699 );
1700 bind_key_with_modifiers(
1701 map,
1702 KeyCode::Char('y'),
1703 KeyModifiers::CONTROL,
1704 CanvasAction::SelectSuggestion,
1705 );
1706 bind_key_with_modifiers(
1707 map,
1708 KeyCode::Char('g'),
1709 KeyModifiers::CONTROL,
1710 CanvasAction::ExitSuggestions,
1711 );
1712}
1713
1714fn bind_key_with_modifiers<A>(
1715 map: &mut KeyMap<A>,
1716 code: KeyCode,
1717 modifiers: KeyModifiers,
1718 action: CanvasAction,
1719) where
1720 A: From<CanvasAction>,
1721{
1722 map.bind(vec![KeyChord::new(code, modifiers)], A::from(action));
1723}
1724
1725const CANVAS_EDIT_MODES: &[&str] = &["nor", "ins", "sel"];
1734const CANVAS_SUGGESTION_MODES: &[&str] = &["ins", "sel"];
1736
1737pub fn canvas_action_name(action: &CanvasAction) -> Option<&'static str> {
1741 Some(match action {
1742 CanvasAction::MoveLeft => "move_left",
1743 CanvasAction::MoveRight => "move_right",
1744 CanvasAction::MoveUp => "move_up",
1745 CanvasAction::MoveDown => "move_down",
1746 CanvasAction::MoveWordNext => "move_word_next",
1747 CanvasAction::MoveWordPrev => "move_word_prev",
1748 CanvasAction::MoveWordEnd => "move_word_end",
1749 CanvasAction::MoveWordEndPrev => "move_word_end_prev",
1750 CanvasAction::MoveBigWordNext => "move_big_word_next",
1751 CanvasAction::MoveBigWordPrev => "move_big_word_prev",
1752 CanvasAction::MoveBigWordEnd => "move_big_word_end",
1753 CanvasAction::MoveBigWordEndPrev => "move_big_word_end_prev",
1754 CanvasAction::MoveLineStart => "move_line_start",
1755 CanvasAction::MoveLineEnd => "move_line_end",
1756 CanvasAction::NextField => "next_field",
1757 CanvasAction::PrevField => "prev_field",
1758 CanvasAction::MoveFirstLine => "move_first_line",
1759 CanvasAction::MoveLastLine => "move_last_line",
1760 CanvasAction::DeleteBackward => "delete_char_backward",
1761 CanvasAction::DeleteForward => "delete_char_forward",
1762 CanvasAction::Undo => "undo",
1763 CanvasAction::Redo => "redo",
1764 CanvasAction::TriggerSuggestions => "trigger_suggestions",
1765 CanvasAction::SuggestionUp => "suggestion_up",
1766 CanvasAction::SuggestionDown => "suggestion_down",
1767 CanvasAction::SelectSuggestion => "select_suggestion",
1768 CanvasAction::ExitSuggestions => "exit_suggestions",
1769 CanvasAction::EnterEditMode => "enter_edit_mode_before",
1770 CanvasAction::EnterEditModeAfter => "enter_edit_mode_after",
1771 CanvasAction::ExitEditMode => "exit_edit_mode",
1772 CanvasAction::EnterHighlightMode => "enter_highlight_mode",
1773 CanvasAction::EnterHighlightModeLinewise => "enter_highlight_mode_linewise",
1774 CanvasAction::ExitHighlightMode => "exit_highlight_mode",
1775 CanvasAction::OpenLineBelow => "open_line_below",
1776 CanvasAction::OpenLineAbove => "open_line_above",
1777 CanvasAction::InsertChar(_) | CanvasAction::Custom(_) => return None,
1778 _ => return None,
1781 })
1782}
1783
1784fn is_suggestion_action(action: &CanvasAction) -> bool {
1785 matches!(
1786 action,
1787 CanvasAction::TriggerSuggestions
1788 | CanvasAction::SuggestionUp
1789 | CanvasAction::SuggestionDown
1790 | CanvasAction::SelectSuggestion
1791 | CanvasAction::ExitSuggestions
1792 )
1793}
1794
1795pub fn canvas_bindable_actions<A>() -> Vec<BindableActionInfo<A>>
1799where
1800 A: From<CanvasAction>,
1801{
1802 let mut actions = CanvasAction::movement_actions();
1803 actions.extend([CanvasAction::DeleteBackward, CanvasAction::DeleteForward]);
1804 actions.extend([CanvasAction::Undo, CanvasAction::Redo]);
1805 actions.extend(CanvasAction::suggestions_actions());
1806 actions.extend([
1807 CanvasAction::EnterEditMode,
1808 CanvasAction::EnterEditModeAfter,
1809 CanvasAction::ExitEditMode,
1810 CanvasAction::EnterHighlightMode,
1811 CanvasAction::EnterHighlightModeLinewise,
1812 CanvasAction::ExitHighlightMode,
1813 CanvasAction::OpenLineBelow,
1814 CanvasAction::OpenLineAbove,
1815 ]);
1816
1817 actions
1818 .into_iter()
1819 .filter_map(|action| {
1820 let name = canvas_action_name(&action)?;
1821 let modes = if is_suggestion_action(&action) {
1822 CANVAS_SUGGESTION_MODES
1823 } else {
1824 CANVAS_EDIT_MODES
1825 };
1826 Some(BindableActionInfo {
1827 description: action.description(),
1828 action: A::from(action),
1829 name,
1830 modes,
1831 })
1832 })
1833 .collect()
1834}
1835
1836fn key_strokes_to_chords(sequence: &[KeyStroke]) -> Vec<KeyChord> {
1837 sequence
1838 .iter()
1839 .map(|stroke| KeyChord::new(stroke.code, stroke.modifiers))
1840 .collect()
1841}
1842
1843pub fn canvas_default_binding_catalog<A>(preset: BuiltinCanvasKeybindingPreset) -> BindingCatalog<A>
1851where
1852 A: From<CanvasAction>,
1853{
1854 let bindings = default_builtin_action_bindings(preset)
1855 .into_iter()
1856 .map(|binding| BindingInfo {
1857 layer: BindingLayer::Canvas,
1858 mode: mode_for_app_mode(binding.mode).as_str().to_string(),
1859 sequence: key_strokes_to_chords(&binding.sequence),
1860 action: A::from(binding.action),
1861 source: BindingSource::CanvasBuiltin,
1862 })
1863 .collect();
1864 BindingCatalog { bindings }
1865}
1866
1867pub fn canvas_suggestion_default_bindings<A>() -> Vec<BindingInfo<A>>
1871where
1872 A: From<CanvasAction>,
1873{
1874 let defaults = [
1875 (KeyCode::Char(' '), CanvasAction::TriggerSuggestions),
1876 (KeyCode::Char('n'), CanvasAction::SuggestionDown),
1877 (KeyCode::Char('p'), CanvasAction::SuggestionUp),
1878 (KeyCode::Char('y'), CanvasAction::SelectSuggestion),
1879 (KeyCode::Char('g'), CanvasAction::ExitSuggestions),
1880 ];
1881 let mut bindings = Vec::new();
1882 for mode in CANVAS_SUGGESTION_MODES {
1883 for (code, action) in &defaults {
1884 bindings.push(BindingInfo {
1885 layer: BindingLayer::Canvas,
1886 mode: (*mode).to_string(),
1887 sequence: vec![KeyChord::new(*code, KeyModifiers::CONTROL)],
1888 action: A::from(action.clone()),
1889 source: BindingSource::CanvasBuiltin,
1890 });
1891 }
1892 }
1893 bindings
1894}
1895
1896pub fn analyze_canvas_overlaps<A>(
1906 keymap_catalog: &BindingCatalog<A>,
1907 canvas_catalog: &BindingCatalog<CanvasAction>,
1908 context: InputLayerContext,
1909) -> Vec<BindingConflict<A>>
1910where
1911 A: Clone,
1912{
1913 let routing = match context {
1914 InputLayerContext::Command => CanvasRoutingPrecedence::KeymapFirst,
1915 InputLayerContext::Text => CanvasRoutingPrecedence::CanvasFirst,
1916 };
1917
1918 let mut conflicts = Vec::new();
1919 for keymap_binding in &keymap_catalog.bindings {
1920 if keymap_binding.layer != BindingLayer::Keymap {
1921 continue;
1922 }
1923 for canvas_binding in &canvas_catalog.bindings {
1924 if canvas_binding.mode == keymap_binding.mode
1925 && canvas_binding.sequence == keymap_binding.sequence
1926 {
1927 conflicts.push(BindingConflict::CanvasOverlap {
1928 mode: keymap_binding.mode.clone(),
1929 sequence: keymap_binding.sequence.clone(),
1930 keymap_action: keymap_binding.action.clone(),
1931 canvas_action: canvas_binding.action.clone(),
1932 routing,
1933 });
1934 }
1935 }
1936 }
1937 conflicts
1938}
1939
1940#[cfg(test)]
1941mod report_tests {
1942 use super::*;
1943 use crate::input::InputRegistry;
1944
1945 #[derive(Debug, Clone, PartialEq, Eq)]
1946 enum AppAction {
1947 Canvas(CanvasAction),
1948 }
1949
1950 impl From<CanvasAction> for AppAction {
1951 fn from(action: CanvasAction) -> Self {
1952 AppAction::Canvas(action)
1953 }
1954 }
1955
1956 #[test]
1957 fn default_catalog_carries_canvas_layer_and_source() {
1958 let catalog: BindingCatalog<AppAction> =
1959 canvas_default_binding_catalog(BuiltinCanvasKeybindingPreset::Vim);
1960 assert!(!catalog.bindings.is_empty());
1961 assert!(catalog.bindings.iter().all(|b| {
1962 b.layer == BindingLayer::Canvas && b.source == BindingSource::CanvasBuiltin
1963 }));
1964 }
1965
1966 #[test]
1967 fn suggestion_defaults_cover_ins_and_sel() {
1968 let bindings: Vec<BindingInfo<AppAction>> = canvas_suggestion_default_bindings();
1969 assert_eq!(bindings.len(), 10);
1970 assert!(bindings.iter().any(|b| b.mode == "ins"));
1971 assert!(bindings.iter().any(|b| b.mode == "sel"));
1972 }
1973
1974 #[test]
1975 fn bindable_actions_have_names() {
1976 let actions: Vec<BindableActionInfo<AppAction>> = canvas_bindable_actions();
1977 assert!(
1978 actions
1979 .iter()
1980 .any(|a| a.name == "suggestion_down" && a.modes == CANVAS_SUGGESTION_MODES)
1981 );
1982 assert!(actions.iter().all(|a| !a.name.is_empty()));
1983 }
1984
1985 #[test]
1986 fn overlap_routing_depends_on_context() {
1987 let mut registry = InputRegistry::<AppAction>::empty();
1988 registry.map_mut("nor").bind(
1990 vec![KeyChord::new(KeyCode::Char('u'), KeyModifiers::empty())],
1991 AppAction::Canvas(CanvasAction::Undo),
1992 );
1993 let keymap_catalog = BindingCatalog::from_registry(®istry, BindingSource::Config);
1994 let canvas_catalog: BindingCatalog<CanvasAction> =
1995 canvas_default_binding_catalog(BuiltinCanvasKeybindingPreset::Vim);
1996
1997 let command =
1998 analyze_canvas_overlaps(&keymap_catalog, &canvas_catalog, InputLayerContext::Command);
1999 assert!(command.iter().any(|c| matches!(
2000 c,
2001 BindingConflict::CanvasOverlap {
2002 routing: CanvasRoutingPrecedence::KeymapFirst,
2003 ..
2004 }
2005 )));
2006
2007 let text =
2008 analyze_canvas_overlaps(&keymap_catalog, &canvas_catalog, InputLayerContext::Text);
2009 assert!(text.iter().any(|c| matches!(
2010 c,
2011 BindingConflict::CanvasOverlap {
2012 routing: CanvasRoutingPrecedence::CanvasFirst,
2013 ..
2014 }
2015 )));
2016 }
2017}