mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-01 16:24:51 +00:00
164 lines
8.5 KiB
Rust
164 lines
8.5 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
|
|
|
//! Keyboard bindings for the Craft suite.
|
|
//!
|
|
//! [`chord`] holds the key and modifier types, the canonical text form that settings and control
|
|
//! protocols store, display text per platform, and a reader for other applications' key text.
|
|
//! [`command`] names what a binding runs, [`scope`] where it works, and [`keymap`] holds an app's
|
|
//! catalog with default bindings, the user's [`Overrides`], dispatch ([`Keymap::lookup`]),
|
|
//! editing and conflicts. [`profiles`] keeps named sets of overrides; [`reserved`] lists the
|
|
//! chords each operating system takes, and a keymap reserves no chord unless the app opts in with
|
|
//! [`Keymap::with_reserved`], e.g. `with_reserved(reserved::system(platform))`. [`import`] brings
|
|
//! in other applications' shortcut sets. [`layout`] decides which chord a key press means on the
|
|
//! user's keyboard layout, for matching, recording and labels. [`editor`] is the model of a
|
|
//! shortcut editor: undo, a chord recorder, conflict queries and search. With the `egui` feature,
|
|
//! the `egui` module
|
|
//! converts to and from egui keys, matches key presses against bindings and dispatches a frame's
|
|
//! key events.
|
|
//!
|
|
//! ```
|
|
//! use craft_keymap::{Chord, Platform};
|
|
//!
|
|
//! let chord = Chord::parse("⇧⌘Z").unwrap();
|
|
//! assert_eq!(chord.to_string(), "Cmd+Shift+Z");
|
|
//! assert_eq!(chord.display(Platform::Mac), "⇧⌘Z");
|
|
//! assert_eq!(chord.display(Platform::Windows), "Ctrl+Shift+Z");
|
|
//! ```
|
|
//!
|
|
//! # A catalog and dispatch
|
|
//!
|
|
//! The app lists every command it can bind. A command with parameters has the key
|
|
//! `window.panel {"panel":"layers"}` in settings and reports. Tools of one group share a key and
|
|
//! cycle. A binding can name a scope other than the command's home scope.
|
|
//!
|
|
//! ```
|
|
//! use craft_keymap::{Active, Bindable, Binding, Chord, CommandRef, Keymap, Kind, Lookup, Platform, Scope, Scopes};
|
|
//! use serde_json::json;
|
|
//!
|
|
//! let key = |text: &str| Binding::parse(text).unwrap();
|
|
//! let layers = CommandRef::with_params("window.panel", json!({"panel": "layers"}));
|
|
//! let select = Kind::Tool { group: Some("select".into()) };
|
|
//! let catalog = vec![
|
|
//! Bindable::new("file.save", "Save", Scope::app()).with_path(vec!["File".into(), "Save".into()]).with_default(key("Cmd+S")),
|
|
//! Bindable::new(layers.clone(), "Layers", Scope::app()).with_path(vec!["Window".into(), "Layers".into()]).with_default(key("F7")),
|
|
//! Bindable::new("tool.marquee", "Marquee Tool", Scope::app()).with_kind(select.clone()).with_default(key("M")),
|
|
//! Bindable::new("tool.ellipse", "Elliptical Marquee Tool", Scope::app()).with_kind(select).with_default(key("M")),
|
|
//! // Home scope: the Timeline panel, where it wins over the app's L.
|
|
//! Bindable::new("timeline.loop", "Loop", Scope::new("Timeline").unwrap()).with_default(key("L")),
|
|
//! Bindable::new("layer.lock", "Lock Layer", Scope::app()).with_default(key("L")),
|
|
//! // An app command whose binding works only in the Text context.
|
|
//! Bindable::new("type.bold", "Bold", Scope::app()).with_default(key("Text: Cmd+B")),
|
|
//! ];
|
|
//! let keymap = Keymap::new(catalog, Scopes::new().with_panel("Timeline").with_context("Text"), Platform::Mac);
|
|
//! assert_eq!(layers.key(), r#"window.panel {"panel":"layers"}"#);
|
|
//!
|
|
//! // Dispatch: the chord pressed, whether it is a double press, and what has the keyboard.
|
|
//! let press = |text: &str, active: &Active| keymap.lookup(&Chord::parse(text).unwrap(), false, active);
|
|
//! assert_eq!(press("F7", &Active::NONE), Lookup::Run(layers));
|
|
//! assert_eq!(press("L", &Active::panel("Timeline")), Lookup::Run("timeline.loop".into()));
|
|
//! assert_eq!(press("L", &Active::NONE), Lookup::Run("layer.lock".into()));
|
|
//! assert_eq!(press("M", &Active::NONE), Lookup::Cycle(vec!["tool.marquee".into(), "tool.ellipse".into()]));
|
|
//! let editing = Active { contexts: &["Text"], typing: true, ..Active::NONE };
|
|
//! assert_eq!(press("Cmd+B", &editing), Lookup::Run("type.bold".into()));
|
|
//! assert_eq!(press("M", &editing), Lookup::None, "plain keys go to the text field");
|
|
//! ```
|
|
//!
|
|
//! With the `egui` feature, `egui::Dispatcher` does this for a frame's key events.
|
|
//!
|
|
//! # Storing overrides
|
|
//!
|
|
//! [`Overrides`] is a field of the app's craft-settings type, marked
|
|
//! `#[serde(with = "craft_keymap::keymap::overrides_list")]` so that it is stored as an array of
|
|
//! `{ command, keys }` tables. craft-settings merges tables key by key with what the file holds,
|
|
//! but replaces an array whole, so a reset command (an entry removed) stays removed after saving.
|
|
//! Without the attribute, overrides with at most one text per command are written as a table.
|
|
//!
|
|
//! ```no_run
|
|
//! use craft_keymap::{Binding, Keymap, Overrides, Platform, Policy, Scopes};
|
|
//! use craft_settings::{Location, Settings, SettingsStore};
|
|
//! use serde::{Deserialize, Serialize};
|
|
//!
|
|
//! #[derive(Serialize, Deserialize, Default, Clone, PartialEq)]
|
|
//! #[serde(default)]
|
|
//! struct Prefs {
|
|
//! #[serde(with = "craft_keymap::keymap::overrides_list")]
|
|
//! shortcuts: Overrides,
|
|
//! }
|
|
//!
|
|
//! impl Settings for Prefs {
|
|
//! const APP_ID: &'static str = "acmecraft";
|
|
//! const SCHEMA_VERSION: u32 = 1;
|
|
//! }
|
|
//!
|
|
//! # let catalog = Vec::new();
|
|
//! let (mut store, _report) = SettingsStore::<Prefs>::open(Location::Auto);
|
|
//! let mut keymap = Keymap::new(catalog, Scopes::new(), Platform::current()).with_overrides(store.get().shortcuts.clone());
|
|
//! // Entries for unknown commands and texts that do not parse are kept and listed here.
|
|
//! let _problems = keymap.problems();
|
|
//!
|
|
//! // After each change in the shortcut editor:
|
|
//! keymap.assign(&"file.save".into(), Binding::parse("Cmd+Alt+S").unwrap(), Policy::Steal).ok();
|
|
//! store.update(|p| p.shortcuts = keymap.overrides().clone());
|
|
//! ```
|
|
//!
|
|
//! An app with named sets stores only [`Profiles`] in place of the `Overrides` field and builds
|
|
//! the keymap from the active set:
|
|
//! `with_overrides(prefs.shortcut_sets.active_overrides(&builtins).clone())`.
|
|
//!
|
|
//! # Importing a shortcut set
|
|
//!
|
|
//! The app's reader builds a [`ForeignSet`]; [`plan`] matches it without changing anything, so a
|
|
//! review UI can show [`ImportPlan::commands`] and the report first.
|
|
//!
|
|
//! ```
|
|
//! use craft_keymap::{Bindable, Binding, ContextRule, Flavor, ForeignEntry, ForeignKey, ForeignSet, ImportOptions, Keymap, Platform};
|
|
//! use craft_keymap::{Scope, Scopes, plan};
|
|
//!
|
|
//! let save = Bindable::new("file.save", "Save", Scope::app())
|
|
//! .with_path(vec!["File".into(), "Save".into()])
|
|
//! .with_default(Binding::parse("Cmd+S").unwrap());
|
|
//! let mut keymap = Keymap::new(vec![save], Scopes::new(), Platform::Windows);
|
|
//!
|
|
//! let read = |text: &str| ForeignKey::read(Some("Default"), text, Flavor::Windows);
|
|
//! let set = ForeignSet::new(vec![
|
|
//! ForeignEntry::new(Some("File"), ["Save"]).with_key(read("Ctrl+Alt+S")).at_line(12),
|
|
//! ForeignEntry::new(Some("File"), ["Frobnicate"]).with_key(read("Ctrl+F")).at_line(13),
|
|
//! ])
|
|
//! .with_name("My Set");
|
|
//! let options = ImportOptions { contexts: vec![("Default".into(), ContextRule::Home)], ..ImportOptions::default() };
|
|
//! let planned = plan(&set, &keymap, &options);
|
|
//! assert_eq!(planned.commands.len(), 1);
|
|
//!
|
|
//! let report = planned.apply(&mut keymap);
|
|
//! assert_eq!(report.changed[0].to, ["Cmd+Alt+S"]);
|
|
//! assert_eq!((report.unmatched[0].item.as_str(), report.unmatched[0].line), ("File > Frobnicate", Some(13)));
|
|
//! assert_eq!(keymap.bindings(&"file.save".into()), [Binding::parse("Cmd+Alt+S").unwrap()]);
|
|
//! ```
|
|
|
|
pub mod chord;
|
|
pub mod command;
|
|
pub mod editor;
|
|
#[cfg(feature = "egui")]
|
|
pub mod egui;
|
|
pub mod import;
|
|
pub mod keymap;
|
|
pub mod layout;
|
|
pub mod profiles;
|
|
pub mod reserved;
|
|
pub mod scope;
|
|
#[cfg(feature = "ui")]
|
|
pub mod ui;
|
|
|
|
pub use chord::foreign::Flavor;
|
|
pub use chord::{Chord, Key, MAX_CHORD_TEXT, Mods, ParseError, Platform, Trigger};
|
|
pub use command::{CommandRef, MAX_KEY};
|
|
pub use editor::{DOUBLE_PRESS, EditSession, MAX_UNDO, MAX_UNDO_BYTES, Recorded, Recorder};
|
|
pub use import::{ContextRule, ForeignEntry, ForeignKey, ForeignSet, ImportOptions, ImportPlan, ImportReport, NoneDefined, Start, plan};
|
|
pub use keymap::{
|
|
AssignError, Assigned, Bindable, Binding, BindingError, Conflict, ConflictKind, DropReason, Holder, Keymap, Kind, Lookup, MAX_BINDING_TEXT,
|
|
MAX_BINDINGS, MAX_COMMANDS, MAX_CONFLICTS, Overrides, Policy, Problem, TypingRule, text_editing_chords,
|
|
};
|
|
pub use layout::{KeyInput, Layout};
|
|
pub use profiles::{Builtins, MAX_EXPORT_BYTES, MAX_SET_NAME, MAX_SETS, ProfileError, Profiles, SetProblem};
|
|
pub use scope::{Active, MAX_SCOPE_NAME, Scope, ScopeKind, Scopes};
|