use teksilo_core::event::{Key, Modifiers};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ListNavConvention {
Mac,
Desktop,
}
impl ListNavConvention {
pub(crate) const CURRENT: Self = if cfg!(target_os = "macos") {
Self::Mac
} else {
Self::Desktop
};
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum ViewKind {
Linear,
TileGrid,
CellGrid,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum NavMove {
First,
Last,
RowFirst,
RowLast,
Page { down: bool },
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum SelectionOp {
Replace,
Suppress,
Extend,
ExtendAdditive,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct NavChord {
pub(crate) movement: NavMove,
pub(crate) selection: SelectionOp,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum TreeChord {
ExpandOne,
CollapseOne,
ExpandSubtree,
CollapseSubtree,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) enum MacAlias {
Activate,
CollapseOrParent,
ExpandSubtree,
CollapseSubtree,
}
pub(crate) fn nav_chord(key: Key, modifiers: Modifiers, kind: ViewKind) -> Option<NavChord> {
let movement = match key {
Key::Home => match kind {
ViewKind::CellGrid if !modifiers.command() => NavMove::RowFirst,
_ => NavMove::First,
},
Key::End => match kind {
ViewKind::CellGrid if !modifiers.command() => NavMove::RowLast,
_ => NavMove::Last,
},
Key::PageUp => NavMove::Page { down: false },
Key::PageDown => NavMove::Page { down: true },
_ => return None,
};
Some(NavChord {
movement,
selection: selection_op(modifiers),
})
}
pub(crate) fn selection_op(modifiers: Modifiers) -> SelectionOp {
match (modifiers.command(), modifiers.shift()) {
(true, true) => SelectionOp::ExtendAdditive,
(true, false) => SelectionOp::Suppress,
(false, true) => SelectionOp::Extend,
(false, false) => SelectionOp::Replace,
}
}
pub(crate) fn tree_chord(key: Key, modifiers: Modifiers) -> Option<TreeChord> {
if modifiers.ctrl() || modifiers.alt() || modifiers.super_key() {
return None;
}
match key {
Key::Character('*') => Some(TreeChord::ExpandSubtree),
Key::Character('+') => Some(TreeChord::ExpandOne),
Key::Character('-') => Some(TreeChord::CollapseOne),
_ => None,
}
}
pub(crate) fn mac_alias(key: Key, modifiers: Modifiers, rtl: bool) -> Option<MacAlias> {
mac_alias_for(ListNavConvention::CURRENT, key, modifiers, rtl)
}
pub(crate) fn mac_alias_for(
convention: ListNavConvention,
key: Key,
modifiers: Modifiers,
rtl: bool,
) -> Option<MacAlias> {
if convention != ListNavConvention::Mac {
return None;
}
if modifiers.shift() || modifiers.ctrl() {
return None;
}
let expand_key = if rtl { Key::ArrowLeft } else { Key::ArrowRight };
let collapse_key = if rtl { Key::ArrowRight } else { Key::ArrowLeft };
match key {
Key::ArrowDown if modifiers.super_key() && !modifiers.alt() => Some(MacAlias::Activate),
Key::ArrowUp if modifiers.super_key() && !modifiers.alt() => {
Some(MacAlias::CollapseOrParent)
}
k if k == expand_key && modifiers.alt() && !modifiers.super_key() => {
Some(MacAlias::ExpandSubtree)
}
k if k == collapse_key && modifiers.alt() && !modifiers.super_key() => {
Some(MacAlias::CollapseSubtree)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::ListNavConvention::{Desktop, Mac};
use super::ViewKind::{CellGrid, Linear, TileGrid};
use super::*;
const NONE: Modifiers = Modifiers::NONE;
const SHIFT: Modifiers = Modifiers::SHIFT;
const ALT: Modifiers = Modifiers::ALT;
const SUPER: Modifiers = Modifiers::SUPER;
const CTRL: Modifiers = Modifiers::CTRL;
const CMD: Modifiers = Modifiers::COMMAND;
fn movement(key: Key, mods: Modifiers, kind: ViewKind) -> NavMove {
nav_chord(key, mods, kind).expect("a nav key").movement
}
fn selection(key: Key, mods: Modifiers) -> SelectionOp {
nav_chord(key, mods, Linear).expect("a nav key").selection
}
#[test]
fn a_linear_view_sends_home_and_end_to_the_ends_of_the_collection() {
assert_eq!(movement(Key::Home, NONE, Linear), NavMove::First);
assert_eq!(movement(Key::End, NONE, Linear), NavMove::Last);
assert_eq!(movement(Key::Home, CMD, Linear), NavMove::First);
assert_eq!(movement(Key::End, CMD, Linear), NavMove::Last);
}
#[test]
fn a_tile_grid_ignores_its_reflow_rows() {
for mods in [NONE, CMD, SHIFT] {
assert_eq!(movement(Key::Home, mods, TileGrid), NavMove::First);
assert_eq!(movement(Key::End, mods, TileGrid), NavMove::Last);
}
}
#[test]
fn a_cell_grid_scopes_home_to_the_row_until_the_accelerator_escalates_it() {
assert_eq!(movement(Key::Home, NONE, CellGrid), NavMove::RowFirst);
assert_eq!(movement(Key::End, NONE, CellGrid), NavMove::RowLast);
assert_eq!(movement(Key::Home, CMD, CellGrid), NavMove::First);
assert_eq!(movement(Key::End, CMD, CellGrid), NavMove::Last);
assert_eq!(movement(Key::Home, SHIFT, CellGrid), NavMove::RowFirst);
assert_eq!(movement(Key::Home, CMD | SHIFT, CellGrid), NavMove::First);
}
#[test]
fn paging_is_the_same_move_in_every_kind_of_view() {
for kind in [Linear, TileGrid, CellGrid] {
assert_eq!(
movement(Key::PageUp, NONE, kind),
NavMove::Page { down: false }
);
assert_eq!(
movement(Key::PageDown, NONE, kind),
NavMove::Page { down: true }
);
}
}
#[test]
fn the_accelerator_moves_the_cursor_without_touching_the_selection() {
for key in [Key::Home, Key::End, Key::PageUp, Key::PageDown] {
assert_eq!(selection(key, NONE), SelectionOp::Replace);
assert_eq!(selection(key, CMD), SelectionOp::Suppress);
assert_eq!(selection(key, SHIFT), SelectionOp::Extend);
assert_eq!(selection(key, CMD | SHIFT), SelectionOp::ExtendAdditive);
}
}
#[test]
fn keys_outside_the_edge_and_page_family_are_not_ours() {
for key in [
Key::ArrowUp,
Key::ArrowDown,
Key::ArrowLeft,
Key::ArrowRight,
Key::Space,
Key::Enter,
Key::Escape,
Key::Tab,
Key::A,
] {
assert!(nav_chord(key, NONE, Linear).is_none());
}
}
#[test]
fn tree_chords_take_the_desktop_reading_of_asterisk() {
assert_eq!(
tree_chord(Key::Character('*'), NONE),
Some(TreeChord::ExpandSubtree)
);
assert_eq!(
tree_chord(Key::Character('+'), NONE),
Some(TreeChord::ExpandOne)
);
assert_eq!(
tree_chord(Key::Character('-'), NONE),
Some(TreeChord::CollapseOne)
);
}
#[test]
fn a_modified_tree_chord_belongs_to_the_application() {
for mods in [CTRL, ALT, SUPER] {
for ch in ['*', '+', '-'] {
assert_eq!(tree_chord(Key::Character(ch), mods), None);
}
}
}
#[test]
fn a_shifted_tree_chord_is_still_the_chord() {
assert_eq!(
tree_chord(Key::Character('*'), SHIFT),
Some(TreeChord::ExpandSubtree)
);
assert_eq!(
tree_chord(Key::Character('+'), SHIFT),
Some(TreeChord::ExpandOne)
);
assert_eq!(
tree_chord(Key::Character('-'), SHIFT),
Some(TreeChord::CollapseOne)
);
}
#[test]
fn tree_chords_must_be_read_before_type_ahead() {
for ch in ['*', '+', '-'] {
assert!(Key::Character(ch).to_char().is_some());
assert!(tree_chord(Key::Character(ch), NONE).is_some());
}
}
#[test]
fn the_mac_aliases_exist_only_on_mac() {
for key in [
Key::ArrowUp,
Key::ArrowDown,
Key::ArrowLeft,
Key::ArrowRight,
] {
for mods in [SUPER, ALT, SUPER | ALT] {
assert_eq!(
mac_alias_for(Desktop, key, mods, false),
None,
"Alt+Right is history-forward on Windows and Ctrl+arrow is \
already cursor-only movement; neither may be reused"
);
}
}
}
#[test]
fn mac_reads_command_arrows_the_way_finder_does() {
assert_eq!(
mac_alias_for(Mac, Key::ArrowDown, SUPER, false),
Some(MacAlias::Activate)
);
assert_eq!(
mac_alias_for(Mac, Key::ArrowUp, SUPER, false),
Some(MacAlias::CollapseOrParent)
);
}
#[test]
fn mac_option_arrows_expand_a_whole_subtree_and_mirror_under_rtl() {
assert_eq!(
mac_alias_for(Mac, Key::ArrowRight, ALT, false),
Some(MacAlias::ExpandSubtree)
);
assert_eq!(
mac_alias_for(Mac, Key::ArrowLeft, ALT, false),
Some(MacAlias::CollapseSubtree)
);
assert_eq!(
mac_alias_for(Mac, Key::ArrowLeft, ALT, true),
Some(MacAlias::ExpandSubtree)
);
assert_eq!(
mac_alias_for(Mac, Key::ArrowRight, ALT, true),
Some(MacAlias::CollapseSubtree)
);
}
#[test]
fn a_shifted_mac_alias_falls_through_to_the_view() {
for key in [
Key::ArrowUp,
Key::ArrowDown,
Key::ArrowLeft,
Key::ArrowRight,
] {
assert_eq!(mac_alias_for(Mac, key, SUPER | SHIFT, false), None);
assert_eq!(mac_alias_for(Mac, key, ALT | SHIFT, false), None);
}
}
#[test]
fn physical_control_is_not_a_mac_item_view_chord() {
for key in [Key::ArrowUp, Key::ArrowDown] {
assert_eq!(mac_alias_for(Mac, key, CTRL, false), None);
}
}
#[test]
fn the_current_convention_matches_the_build_target() {
if cfg!(target_os = "macos") {
assert_eq!(ListNavConvention::CURRENT, Mac);
assert_eq!(
mac_alias(Key::ArrowDown, SUPER, false),
Some(MacAlias::Activate)
);
} else {
assert_eq!(ListNavConvention::CURRENT, Desktop);
assert_eq!(mac_alias(Key::ArrowDown, SUPER, false), None);
}
}
}