use std::cell::Cell;
use std::fmt;
use std::rc::Rc;
use crate::event::{Key, KeyEvent, Modifiers, NamedKey, PassBracket};
use crate::overlay::OverlayKey;
thread_local! {
static NEXT_SHORTCUT_ID: Cell<u64> = const { Cell::new(1) };
static SHORTCUT_TABLE: Cell<Vec<ShortcutEntry>> = const { Cell::new(Vec::new()) };
static SHORTCUT_PASS_OPEN: Cell<bool> = const { Cell::new(false) };
static CURRENT_OVERLAY: Cell<Option<OverlayKey>> = const { Cell::new(None) };
static SHORTCUT_HANDLED: Cell<bool> = const { Cell::new(false) };
static KEY_FORWARD: Cell<Option<OverlayKey>> = const { Cell::new(None) };
static KEY_FORWARD_OPEN: Cell<bool> = const { Cell::new(false) };
}
const MOD_IS_META: bool = cfg!(any(target_os = "macos", target_os = "ios"));
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ShortcutId(u64);
impl ShortcutId {
pub fn next() -> Self {
ShortcutId(NEXT_SHORTCUT_ID.with(|next| {
let id = next.get();
next.set(id.wrapping_add(1));
id
}))
}
pub fn value(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ShortcutScope {
Local,
AppGlobal,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Chord {
pub key: Key,
pub mods: Modifiers,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChordParseError {
Empty,
UnknownToken(String),
UnsupportedKey(String),
MissingKey,
MultipleKeys,
}
impl fmt::Display for ChordParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ChordParseError::Empty => f.write_str("empty chord"),
ChordParseError::UnknownToken(token) => write!(f, "unknown chord token `{token}`"),
ChordParseError::UnsupportedKey(token) => {
write!(f, "key `{token}` is not in the input vocabulary")
}
ChordParseError::MissingKey => f.write_str("chord names no key"),
ChordParseError::MultipleKeys => f.write_str("chord names more than one key"),
}
}
}
impl std::error::Error for ChordParseError {}
impl Chord {
pub fn new(key: Key, mods: Modifiers) -> Self {
let key = match key {
Key::Character(text) => Key::Character(text.to_lowercase()),
named => named,
};
let mut mods = mods;
if shift_insensitive(&key) {
mods.shift = false;
}
Chord { key, mods }
}
pub fn parse(spec: &str) -> Result<Chord, ChordParseError> {
let spec = spec.trim();
if spec.is_empty() {
return Err(ChordParseError::Empty);
}
let (head, plus) = match spec.strip_suffix("++") {
Some(head) => (head, true),
None if spec == "+" => ("", true),
None => (spec, false),
};
let mut mods = Modifiers::default();
let mut key = plus.then(|| Key::Character("+".to_string()));
let tokens = (!head.is_empty()).then(|| head.split('+'));
for token in tokens.into_iter().flatten() {
let token = token.trim();
if token.is_empty() {
return Err(ChordParseError::MissingKey);
}
if apply_modifier(&mut mods, token) {
continue;
}
let parsed = parse_key(token)?;
if key.replace(parsed).is_some() {
return Err(ChordParseError::MultipleKeys);
}
}
let key = key.ok_or(ChordParseError::MissingKey)?;
Ok(Chord::new(key, mods))
}
pub fn matches(&self, event: &KeyEvent) -> bool {
let mut held = event.modifiers;
if shift_insensitive(&self.key) {
held.shift = false;
}
if held != self.mods {
return false;
}
match (&self.key, &event.key) {
(Key::Named(want), Key::Named(got)) => want == got,
(Key::Character(want), Key::Character(got)) => *want == got.to_lowercase(),
_ => false,
}
}
pub fn display(&self) -> String {
self.display_for(MOD_IS_META)
}
fn display_for(&self, apple: bool) -> String {
let m = self.mods;
let key = key_label(&self.key, apple);
let held = if apple {
[(m.ctrl, "⌃"), (m.alt, "⌥"), (m.shift, "⇧"), (m.meta, "⌘")]
} else {
[
(m.ctrl, "Ctrl"),
(m.alt, "Alt"),
(m.shift, "Shift"),
(m.meta, "Super"),
]
};
let mut parts: Vec<&str> = held.iter().filter(|(on, _)| *on).map(|(_, n)| *n).collect();
parts.push(&key);
parts.join(if apple { "" } else { "+" })
}
}
fn shift_insensitive(key: &Key) -> bool {
match key {
Key::Character(text) => {
!text.is_empty()
&& text
.chars()
.all(|c| !c.is_alphanumeric() && !c.is_whitespace())
}
Key::Named(_) => false,
}
}
fn key_label(key: &Key, apple: bool) -> String {
let named = match key {
Key::Character(text) if text == " " => "Space",
Key::Character(text) => return text.to_uppercase(),
Key::Named(named) => match (named, apple) {
(NamedKey::Enter, true) => "↩",
(NamedKey::Enter, false) => "Enter",
(NamedKey::Backspace, true) => "⌫",
(NamedKey::Backspace, false) => "Backspace",
(NamedKey::Delete, true) => "⌦",
(NamedKey::Delete, false) => "Delete",
(NamedKey::Escape, true) => "⎋",
(NamedKey::Escape, false) => "Esc",
(NamedKey::Tab, true) => "⇥",
(NamedKey::Tab, false) => "Tab",
(NamedKey::ArrowLeft, _) => "←",
(NamedKey::ArrowRight, _) => "→",
(NamedKey::ArrowUp, _) => "↑",
(NamedKey::ArrowDown, _) => "↓",
(NamedKey::Home, _) => "Home",
(NamedKey::End, _) => "End",
(NamedKey::Insert, _) => "Insert",
(NamedKey::Copy, _) => "Copy",
(NamedKey::Cut, _) => "Cut",
(NamedKey::Paste, _) => "Paste",
(
NamedKey::F1
| NamedKey::F2
| NamedKey::F3
| NamedKey::F4
| NamedKey::F5
| NamedKey::F6
| NamedKey::F7
| NamedKey::F8
| NamedKey::F9
| NamedKey::F10
| NamedKey::F11
| NamedKey::F12
| NamedKey::F13
| NamedKey::F14
| NamedKey::F15
| NamedKey::F16
| NamedKey::F17
| NamedKey::F18
| NamedKey::F19
| NamedKey::F20
| NamedKey::F21
| NamedKey::F22
| NamedKey::F23
| NamedKey::F24,
_,
) => return format!("F{}", named.function_number().unwrap_or(0)),
},
};
named.to_string()
}
fn apply_modifier(mods: &mut Modifiers, token: &str) -> bool {
match token.to_ascii_lowercase().as_str() {
"mod" if MOD_IS_META => mods.meta = true,
"mod" => mods.ctrl = true,
"ctrl" | "control" => mods.ctrl = true,
"shift" => mods.shift = true,
"alt" | "option" | "opt" => mods.alt = true,
"meta" | "cmd" | "command" | "super" | "win" => mods.meta = true,
_ => return false,
}
true
}
fn parse_key(token: &str) -> Result<Key, ChordParseError> {
if token.chars().count() == 1 {
return Ok(Key::Character(token.to_lowercase()));
}
let lower = token.to_ascii_lowercase();
let named = match lower.as_str() {
"enter" | "return" => NamedKey::Enter,
"backspace" => NamedKey::Backspace,
"delete" | "del" => NamedKey::Delete,
"left" | "arrowleft" => NamedKey::ArrowLeft,
"right" | "arrowright" => NamedKey::ArrowRight,
"up" | "arrowup" => NamedKey::ArrowUp,
"down" | "arrowdown" => NamedKey::ArrowDown,
"home" => NamedKey::Home,
"end" => NamedKey::End,
"escape" | "esc" => NamedKey::Escape,
"tab" => NamedKey::Tab,
"insert" | "ins" => NamedKey::Insert,
"copy" => NamedKey::Copy,
"cut" => NamedKey::Cut,
"paste" => NamedKey::Paste,
"space" => return Ok(Key::Character(" ".to_string())),
_ => match function_key(&lower) {
Some(named) => named,
None => return Err(ChordParseError::UnknownToken(token.to_string())),
},
};
Ok(Key::Named(named))
}
fn function_key(lower: &str) -> Option<NamedKey> {
let digits = lower.strip_prefix('f')?;
if !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
NamedKey::function(digits.parse::<u8>().ok()?)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShortcutDecl {
pub id: ShortcutId,
pub chord: Chord,
pub label: String,
pub scope: ShortcutScope,
}
#[derive(Clone, Debug, PartialEq)]
pub struct ShortcutHint {
pub chord: Chord,
pub label: String,
pub scope: ShortcutScope,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ShortcutEntry {
pub(crate) decl: ShortcutDecl,
pub(crate) overlay: Option<OverlayKey>,
}
pub(crate) struct ShortcutPaintPass(PassBracket<Vec<ShortcutEntry>>);
impl ShortcutPaintPass {
pub(crate) fn enter() -> Self {
ShortcutPaintPass(PassBracket::enter(&SHORTCUT_TABLE, &SHORTCUT_PASS_OPEN))
}
pub(crate) fn take(&self) -> Vec<ShortcutEntry> {
self.0.take()
}
}
pub(crate) fn register(decl: ShortcutDecl) {
let overlay = CURRENT_OVERLAY.with(Cell::get);
SHORTCUT_TABLE.with(|table| {
let mut entries = table.take();
entries.push(ShortcutEntry { decl, overlay });
table.set(entries);
});
}
pub(crate) struct OverlayStamp(Option<OverlayKey>);
impl OverlayStamp {
pub(crate) fn enter(key: OverlayKey) -> Self {
OverlayStamp(CURRENT_OVERLAY.with(|slot| slot.replace(Some(key))))
}
}
impl Drop for OverlayStamp {
fn drop(&mut self) {
CURRENT_OVERLAY.with(|slot| slot.set(self.0));
}
}
pub(crate) fn note_handled() {
SHORTCUT_HANDLED.with(|flag| flag.set(true));
}
pub(crate) struct DeliveryProbe(bool);
impl DeliveryProbe {
pub(crate) fn enter() -> Self {
DeliveryProbe(SHORTCUT_HANDLED.with(|flag| flag.replace(false)))
}
pub(crate) fn handled(&self) -> bool {
SHORTCUT_HANDLED.with(Cell::get)
}
}
impl Drop for DeliveryProbe {
fn drop(&mut self) {
SHORTCUT_HANDLED.with(|flag| flag.set(self.0));
}
}
pub(crate) fn request_key_forward(key: OverlayKey) {
KEY_FORWARD.with(|slot| slot.set(Some(key)));
}
pub(crate) struct KeyForwardPass(PassBracket<Option<OverlayKey>>);
impl KeyForwardPass {
pub(crate) fn enter() -> Self {
KeyForwardPass(PassBracket::enter(&KEY_FORWARD, &KEY_FORWARD_OPEN))
}
pub(crate) fn take(&self) -> Option<OverlayKey> {
self.0.take()
}
}
#[derive(Clone, Debug, Default)]
pub(crate) struct ShortcutTable {
entries: Vec<ShortcutEntry>,
}
impl ShortcutTable {
pub(crate) fn new(entries: Vec<ShortcutEntry>) -> Self {
Self { entries }
}
#[cfg(test)]
pub(crate) fn entries(&self) -> &[ShortcutEntry] {
&self.entries
}
pub(crate) fn hints(&self, mut filter: impl FnMut(&ShortcutHint) -> bool) -> Vec<ShortcutHint> {
let hints = self.entries.iter().map(|e| ShortcutHint {
chord: e.decl.chord.clone(),
label: e.decl.label.clone(),
scope: e.decl.scope,
});
hints.filter(|hint| filter(hint)).collect()
}
pub(crate) fn publish(&self) -> Rc<[ShortcutHint]> {
self.hints(|_| true).into()
}
pub(crate) fn candidates(
&self,
event: &KeyEvent,
top_overlay: Option<OverlayKey>,
masked: bool,
) -> Vec<(ShortcutId, Option<OverlayKey>)> {
let tiers: [&dyn Fn(&ShortcutEntry) -> bool; 3] = [
&|e| e.overlay.is_some() && e.overlay == top_overlay,
&|e| e.overlay.is_none() && e.decl.scope == ShortcutScope::Local,
&|e| e.overlay.is_none() && e.decl.scope == ShortcutScope::AppGlobal,
];
let mut out = Vec::new();
for in_tier in &tiers[..if masked { 1 } else { 3 }] {
let tier: Vec<_> = self
.entries
.iter()
.rev()
.filter(|e| in_tier(e) && e.decl.chord.matches(event))
.map(|e| (e.decl.id, e.overlay))
.collect();
#[cfg(debug_assertions)]
if tier.len() > 1 {
log::warn!(
"frust-core: {} shortcuts in one tier match {:?}; the last declared wins",
tier.len(),
event.key
);
}
out.extend(tier);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::PaintCtx;
use kurbo::{Point, Size};
fn mods(shift: bool, ctrl: bool, alt: bool, meta: bool) -> Modifiers {
Modifiers {
shift,
ctrl,
alt,
meta,
}
}
fn press(key: Key, modifiers: Modifiers) -> KeyEvent {
KeyEvent {
key,
modifiers,
repeat: false,
}
}
fn character(text: &str) -> Key {
Key::Character(text.to_string())
}
fn decl(chord: &str, scope: ShortcutScope) -> ShortcutDecl {
ShortcutDecl {
id: ShortcutId::next(),
chord: Chord::parse(chord).expect("valid chord"),
label: chord.to_string(),
scope,
}
}
#[test]
fn mod_maps_to_meta_on_apple_targets_and_ctrl_elsewhere() {
let chord = Chord::parse("Mod+K").unwrap();
let apple = cfg!(any(target_os = "macos", target_os = "ios"));
assert_eq!(chord.mods, mods(false, !apple, false, apple));
assert_eq!(chord.key, character("k"));
}
#[test]
fn parses_named_keys_modifier_stacks_and_bare_characters() {
assert_eq!(
Chord::parse("Shift+Tab").unwrap(),
Chord::new(Key::Named(NamedKey::Tab), mods(true, false, false, false))
);
assert_eq!(
Chord::parse("Ctrl+Shift+P").unwrap(),
Chord::new(character("p"), mods(true, true, false, false))
);
assert_eq!(
Chord::parse("1").unwrap(),
Chord::new(character("1"), Modifiers::default())
);
assert_eq!(
Chord::parse(" cmd + alt + Esc ").unwrap(),
Chord::new(Key::Named(NamedKey::Escape), mods(false, false, true, true))
);
assert_eq!(
Chord::parse("Ctrl++").unwrap(),
Chord::new(character("+"), mods(false, true, false, false))
);
assert_eq!(Chord::parse("Space").unwrap().key, character(" "));
}
#[test]
fn function_keys_parse_case_blind_and_display_without_apple_symbols() {
let f2 = Chord::new(Key::Named(NamedKey::F2), Modifiers::default());
assert_eq!(Chord::parse("F2"), Ok(f2));
assert_eq!(Chord::parse("f24").unwrap().key, Key::Named(NamedKey::F24));
assert_eq!(
Chord::parse("shift+f5").unwrap(),
Chord::new(Key::Named(NamedKey::F5), mods(true, false, false, false))
);
let ctrl = Chord::parse("Ctrl+F12").unwrap();
assert_eq!(ctrl.key, Key::Named(NamedKey::F12));
assert!(ctrl.mods.ctrl);
let key = Key::Named(NamedKey::F2);
let cmd_f2 = Chord::new(key.clone(), mods(false, false, false, true));
assert_eq!(cmd_f2.display_for(true), "⌘F2");
let ctrl_f2 = Chord::new(key, mods(false, true, false, false));
assert_eq!(ctrl_f2.display_for(false), "Ctrl+F2");
}
#[test]
fn unknown_tokens_and_malformed_specs_are_typed_errors() {
assert_eq!(
Chord::parse("Hyper+K"),
Err(ChordParseError::UnknownToken("Hyper".to_string()))
);
assert_eq!(
Chord::parse("Ctrl+Banana"),
Err(ChordParseError::UnknownToken("Banana".to_string()))
);
assert_eq!(
Chord::parse("F0"),
Err(ChordParseError::UnknownToken("F0".to_string()))
);
assert_eq!(
Chord::parse("F25"),
Err(ChordParseError::UnknownToken("F25".to_string()))
);
assert_eq!(Chord::parse("F+1"), Err(ChordParseError::MultipleKeys));
assert_eq!(Chord::parse(""), Err(ChordParseError::Empty));
assert_eq!(Chord::parse("Shift"), Err(ChordParseError::MissingKey));
assert_eq!(Chord::parse("Ctrl+"), Err(ChordParseError::MissingKey));
assert_eq!(Chord::parse("K+J"), Err(ChordParseError::MultipleKeys));
let message = Chord::parse("Hyper+K").unwrap_err().to_string();
assert!(message.contains("Hyper"), "{message}");
}
#[test]
fn matching_is_exact_on_modifiers_case_blind_on_characters_and_ignores_repeat() {
let chord = Chord::parse("Ctrl+K").unwrap();
let ctrl = mods(false, true, false, false);
assert!(chord.matches(&press(character("k"), ctrl)));
assert!(chord.matches(&press(character("K"), ctrl)));
let mut held = press(character("k"), ctrl);
held.repeat = true;
assert!(chord.matches(&held), "an auto-repeat matches too");
assert!(!chord.matches(&press(character("k"), Modifiers::default())));
assert!(!chord.matches(&press(character("k"), mods(true, true, false, false))));
assert!(!chord.matches(&press(character("j"), ctrl)));
assert!(!chord.matches(&press(Key::Named(NamedKey::Tab), ctrl)));
}
#[test]
fn display_follows_the_platform_rule() {
let chord = |spec: &str| Chord::parse(spec).unwrap();
assert_eq!(chord("Ctrl+K").display_for(false), "Ctrl+K");
assert_eq!(chord("Ctrl+K").display_for(true), "⌃K");
assert_eq!(chord("Meta+Shift+P").display_for(true), "⇧⌘P");
assert_eq!(chord("Meta+Shift+P").display_for(false), "Shift+Super+P");
assert_eq!(chord("Alt+Ctrl+Space").display_for(false), "Ctrl+Alt+Space");
assert_eq!(chord("Shift+Tab").display_for(false), "Shift+Tab");
assert_eq!(chord("Shift+Tab").display_for(true), "⇧⇥");
assert_eq!(chord("Esc").display_for(false), "Esc");
assert_eq!(chord("Cmd+Enter").display_for(true), "⌘↩");
assert_eq!(chord("Cmd+Enter").display_for(false), "Super+Enter");
assert_eq!(chord("1").display(), "1");
let apple = cfg!(any(target_os = "macos", target_os = "ios"));
let expected = if apple { "⌘K" } else { "Ctrl+K" };
assert_eq!(chord("Mod+K").display(), expected);
}
#[test]
fn publish_snapshots_the_table_as_one_shared_rc() {
let table = ShortcutTable::new(vec![
ShortcutEntry {
decl: decl("Mod+K", ShortcutScope::AppGlobal),
overlay: None,
},
ShortcutEntry {
decl: decl("Esc", ShortcutScope::Local),
overlay: Some(OverlayKey::next()),
},
]);
let first = table.publish();
let labels: Vec<_> = first.iter().map(|h| h.label.as_str()).collect();
assert_eq!(labels, ["Mod+K", "Esc"]);
assert!(ShortcutTable::default().publish().is_empty());
assert_eq!(first.len(), 2, "a held snapshot is unaffected");
}
#[test]
fn symbol_chords_ignore_shift_and_the_rest_stay_exact() {
let (ctrl, shift) = (
mods(false, true, false, false),
mods(true, false, false, false),
);
let both = mods(true, true, false, false);
let plus = Chord::parse("Ctrl++").unwrap();
assert!(plus.matches(&press(character("+"), both)));
assert!(plus.matches(&press(character("+"), ctrl)));
assert!(
!plus.matches(&press(character("+"), shift)),
"ctrl still counts"
);
let query = Chord::parse("?").unwrap();
assert!(query.matches(&press(character("?"), shift)));
assert!(query.matches(&press(character("?"), Modifiers::default())));
assert_eq!(Chord::parse("Shift+?").unwrap(), query);
assert_eq!(query.display_for(false), "?");
let space = Chord::parse("Shift+Space").unwrap();
assert!(!space.matches(&press(character(" "), Modifiers::default())));
assert!(space.matches(&press(character(" "), shift)));
let digit = Chord::parse("1").unwrap();
assert!(!digit.matches(&press(character("1"), shift)));
let palette = Chord::parse("Ctrl+Shift+P").unwrap();
assert!(!palette.matches(&press(character("p"), ctrl)));
assert!(palette.matches(&press(character("P"), both)));
}
#[test]
fn hints_list_every_declaration_in_paint_order_through_the_filter() {
let (global, local, floated) = (
decl("Mod+K", ShortcutScope::AppGlobal),
decl("Mod+B", ShortcutScope::Local),
decl("Esc", ShortcutScope::Local),
);
let table = ShortcutTable::new(vec![
ShortcutEntry {
decl: global.clone(),
overlay: None,
},
ShortcutEntry {
decl: local.clone(),
overlay: None,
},
ShortcutEntry {
decl: floated.clone(),
overlay: Some(OverlayKey::next()),
},
]);
let all = table.hints(|_| true);
let labels: Vec<_> = all.iter().map(|h| h.label.as_str()).collect();
assert_eq!(labels, ["Mod+K", "Mod+B", "Esc"]);
assert_eq!(
all[0],
ShortcutHint {
chord: global.chord,
label: global.label,
scope: ShortcutScope::AppGlobal,
}
);
let local_only = table.hints(|h| h.scope == ShortcutScope::Local);
assert_eq!(local_only.len(), 2);
assert!(ShortcutTable::default().hints(|_| true).is_empty());
}
#[test]
fn candidates_follow_tier_order_newest_first() {
let (top, other) = (OverlayKey::next(), OverlayKey::next());
let global = decl("Ctrl+K", ShortcutScope::AppGlobal);
let local_old = decl("Ctrl+K", ShortcutScope::Local);
let local_new = decl("Ctrl+K", ShortcutScope::Local);
let in_top = decl("Ctrl+K", ShortcutScope::AppGlobal);
let in_other = decl("Ctrl+K", ShortcutScope::Local);
let unrelated = decl("Ctrl+J", ShortcutScope::Local);
let stamped = |d: &ShortcutDecl, overlay| ShortcutEntry {
decl: d.clone(),
overlay,
};
let table = ShortcutTable::new(vec![
stamped(&global, None),
stamped(&local_old, None),
stamped(&in_top, Some(top)),
stamped(&local_new, None),
stamped(&in_other, Some(other)),
stamped(&unrelated, None),
]);
let event = press(character("k"), mods(false, true, false, false));
assert_eq!(
table.candidates(&event, Some(top), false),
vec![
(in_top.id, Some(top)),
(local_new.id, None),
(local_old.id, None),
(global.id, None),
],
"overlay tier, then Local (last declared first), then AppGlobal"
);
assert_eq!(
table.candidates(&event, None, false).len(),
3,
"with no interactive pod on top, no stamped declaration is reachable"
);
assert_eq!(
table.candidates(&event, Some(top), true),
vec![(in_top.id, Some(top))],
"a modal pod masks both main-tree tiers"
);
}
#[test]
fn the_table_is_pass_scoped_and_stamps_the_pod_being_painted() {
let pass = ShortcutPaintPass::enter();
let (plain, floated) = (
decl("1", ShortcutScope::Local),
decl("2", ShortcutScope::Local),
);
let key = OverlayKey::next();
register(plain.clone());
{
let _stamp = OverlayStamp::enter(key);
register(floated.clone());
}
let drained = pass.take();
assert_eq!(
drained.iter().map(|e| e.overlay).collect::<Vec<_>>(),
vec![None, Some(key)]
);
assert!(pass.take().is_empty(), "the drain empties the table");
drop(pass);
register(plain);
let next = ShortcutPaintPass::enter();
assert!(next.take().is_empty(), "a stray declaration never leaks in");
}
#[test]
fn declaring_counts_as_a_paint_publication() {
let _pass = ShortcutPaintPass::enter();
let mut ctx = PaintCtx::new(Point::ZERO, Size::new(10.0, 10.0));
assert_eq!(ctx.paint_publications(), 0);
ctx.register_shortcut(decl("Mod+S", ShortcutScope::AppGlobal));
assert_eq!(
ctx.paint_publications(),
1,
"a retained boundary must refuse to replay a declaring body"
);
}
#[test]
fn a_forward_request_is_pass_scoped_and_nests() {
let (outer_key, inner_key) = (OverlayKey::next(), OverlayKey::next());
let outer = KeyForwardPass::enter();
request_key_forward(outer_key);
{
let inner = KeyForwardPass::enter();
assert_eq!(inner.take(), None, "a nested pass starts empty");
request_key_forward(inner_key);
assert_eq!(inner.take(), Some(inner_key));
}
assert_eq!(
outer.take(),
Some(outer_key),
"and hands the enclosing request back"
);
drop(outer);
request_key_forward(inner_key);
assert_eq!(
KeyForwardPass::enter().take(),
None,
"a stray request never leaks in"
);
}
#[test]
fn a_delivery_probe_brackets_the_handled_flag() {
let outer = DeliveryProbe::enter();
note_handled();
{
let inner = DeliveryProbe::enter();
assert!(!inner.handled(), "a nested delivery starts unanswered");
}
assert!(outer.handled(), "and hands the enclosing answer back");
drop(outer);
assert!(!DeliveryProbe::enter().handled());
}
}