use std::any::Any;
use std::cell::Cell;
use std::fmt;
use std::thread::LocalKey;
use kurbo::{Affine, Point, Rect, Size, Vec2};
use crate::overlay::OverlayKey;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerButton {
Primary,
Secondary,
Middle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PointerPhase {
Down,
Move,
Up,
Cancel,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PointerEvent {
pub phase: PointerPhase,
pub position: Point,
pub button: PointerButton,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PointerSource {
Mouse,
Touch,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PointerId {
pub source: PointerSource,
pub slot: u32,
}
impl PointerId {
pub const MOUSE: PointerId = PointerId {
source: PointerSource::Mouse,
slot: 0,
};
pub const fn touch(slot: u32) -> PointerId {
PointerId {
source: PointerSource::Touch,
slot,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum CursorIcon {
#[default]
Default,
Pointer,
Text,
Grab,
Grabbing,
ColResize,
RowResize,
NotAllowed,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ScrollDelta {
Lines(f64, f64),
Pixels(f64, f64),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScalePhase {
Begin,
Update,
End,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScaleEvent {
pub phase: ScalePhase,
pub scale_delta: f64,
pub focal: Point,
pub velocity: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NamedKey {
Enter,
Backspace,
Delete,
ArrowLeft,
ArrowRight,
ArrowUp,
ArrowDown,
Home,
End,
Escape,
Tab,
Copy,
Cut,
Paste,
Insert,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Key {
Named(NamedKey),
Character(String),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Modifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyEvent {
pub key: Key,
pub modifiers: Modifiers,
pub repeat: bool,
}
#[derive(Clone, PartialEq, Eq)]
pub enum EditCommand {
Copy,
Cut,
Paste(String),
SelectAll,
}
impl fmt::Debug for EditCommand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EditCommand::Copy => f.write_str("Copy"),
EditCommand::Cut => f.write_str("Cut"),
EditCommand::Paste(_) => f.debug_tuple("Paste").field(&"<redacted>").finish(),
EditCommand::SelectAll => f.write_str("SelectAll"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EditingState {
pub text: String,
pub selection_base: i32,
pub selection_extent: i32,
pub composing_base: i32,
pub composing_extent: i32,
}
impl Default for EditingState {
fn default() -> Self {
Self {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ImeEvent {
Compose {
text: String,
cursor: Option<(usize, usize)>,
},
Commit(String),
ApplyEditingState(EditingState),
Enabled,
Disabled,
}
#[derive(Clone, Debug, PartialEq)]
pub struct OverlayEvent {
pub key: OverlayKey,
pub kind: OverlayEventKind,
}
#[derive(Clone, Debug, PartialEq)]
pub enum OverlayEventKind {
Pointer(PointerEvent),
Scroll {
position: Point,
delta: ScrollDelta,
},
Scale {
focal: Point,
phase: ScalePhase,
scale_delta: f64,
velocity: f64,
},
OutsideDown,
}
#[derive(Clone, Debug, PartialEq)]
pub enum InputEvent {
Pointer(PointerEvent),
PointerContact {
pointer_id: PointerId,
event: PointerEvent,
},
Scroll {
position: Point,
delta: ScrollDelta,
},
Scale(ScaleEvent),
Key(KeyEvent),
Ime(ImeEvent),
EditCommand(EditCommand),
Housekeeping,
Overlay(OverlayEvent),
}
impl InputEvent {
pub fn position(&self) -> Point {
match self {
InputEvent::Pointer(p) | InputEvent::PointerContact { event: p, .. } => p.position,
InputEvent::Scroll { position, .. } => *position,
InputEvent::Scale(scale) => scale.focal,
InputEvent::Key(_)
| InputEvent::Ime(_)
| InputEvent::EditCommand(_)
| InputEvent::Housekeeping
| InputEvent::Overlay(_) => Point::ZERO,
}
}
pub fn translated(&self, offset: Vec2) -> InputEvent {
match self {
InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
position: p.position + offset,
..*p
}),
InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
pointer_id: *pointer_id,
event: PointerEvent {
position: event.position + offset,
..*event
},
},
InputEvent::Scroll { position, delta } => InputEvent::Scroll {
position: *position + offset,
delta: *delta,
},
InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
focal: scale.focal + offset,
..*scale
}),
InputEvent::Key(_)
| InputEvent::Ime(_)
| InputEvent::EditCommand(_)
| InputEvent::Housekeeping
| InputEvent::Overlay(_) => self.clone(),
}
}
pub fn transformed(&self, affine: &Affine) -> InputEvent {
match self {
InputEvent::Pointer(p) => InputEvent::Pointer(PointerEvent {
position: *affine * p.position,
..*p
}),
InputEvent::PointerContact { pointer_id, event } => InputEvent::PointerContact {
pointer_id: *pointer_id,
event: PointerEvent {
position: *affine * event.position,
..*event
},
},
InputEvent::Scroll { position, delta } => InputEvent::Scroll {
position: *affine * *position,
delta: *delta,
},
InputEvent::Scale(scale) => InputEvent::Scale(ScaleEvent {
focal: *affine * scale.focal,
..*scale
}),
InputEvent::Key(_)
| InputEvent::Ime(_)
| InputEvent::EditCommand(_)
| InputEvent::Housekeeping
| InputEvent::Overlay(_) => self.clone(),
}
}
pub fn is_focus_routed(&self) -> bool {
matches!(
self,
InputEvent::Key(_) | InputEvent::Ime(_) | InputEvent::EditCommand(_)
)
}
pub fn is_broadcast(&self) -> bool {
matches!(self, InputEvent::Housekeeping | InputEvent::Overlay(_))
}
}
thread_local! {
static PENDING_RESULT_FLUSH: Cell<bool> = const { Cell::new(false) };
}
pub fn mark_pending_result_flush() {
PENDING_RESULT_FLUSH.with(|flag| flag.set(true));
}
pub fn take_pending_result_flush() -> bool {
PENDING_RESULT_FLUSH.with(|flag| flag.replace(false))
}
pub fn has_pending_result_flush() -> bool {
PENDING_RESULT_FLUSH.with(|flag| flag.get())
}
thread_local! {
static FOCUS_ORPHANED: Cell<bool> = const { Cell::new(false) };
}
pub fn mark_focus_orphaned() {
FOCUS_ORPHANED.with(|flag| flag.set(true));
}
pub fn take_focus_orphaned() -> bool {
FOCUS_ORPHANED.with(|flag| flag.replace(false))
}
thread_local! {
static HOVER_ORPHANED: Cell<Option<u64>> = const { Cell::new(None) };
static LIVE_HOVER_LINK: Cell<(u64, u64)> = const { Cell::new((0, 0)) };
}
pub(crate) fn mark_hover_orphaned(root: u64) {
HOVER_ORPHANED.with(|slot| slot.set(Some(root)));
}
pub(crate) fn take_hover_orphaned(root: u64) -> bool {
HOVER_ORPHANED.with(|slot| {
if slot.get() == Some(root) {
slot.set(None);
true
} else {
false
}
})
}
pub(crate) fn set_live_hover_link(root: u64, epoch: u64) {
LIVE_HOVER_LINK.with(|slot| slot.set((root, epoch)));
}
pub(crate) fn live_hover_link_is(root: u64, epoch: u64) -> bool {
epoch != 0 && LIVE_HOVER_LINK.with(|slot| slot.get()) == (root, epoch)
}
thread_local! {
static CURSOR_REQUEST: Cell<Option<CursorIcon>> = const { Cell::new(None) };
static CLIPBOARD_WRITE: Cell<Option<String>> = const { Cell::new(None) };
static PASTE_REQUEST: Cell<bool> = const { Cell::new(false) };
static EDIT_COMMAND_QUEUE: Cell<Vec<EditCommand>> = const { Cell::new(Vec::new()) };
static REQUEST_PASS_OPEN: Cell<bool> = const { Cell::new(false) };
}
pub(crate) struct PassSlot<T: Default + 'static> {
slot: &'static LocalKey<Cell<T>>,
restore: T,
}
impl<T: Default + 'static> PassSlot<T> {
pub(crate) fn enter(slot: &'static LocalKey<Cell<T>>, nested: bool) -> Self {
let stashed = slot.with(|cell| cell.take());
Self {
slot,
restore: if nested { stashed } else { T::default() },
}
}
pub(crate) fn take(&self) -> T {
self.slot.with(|cell| cell.take())
}
}
impl<T: Default + 'static> Drop for PassSlot<T> {
fn drop(&mut self) {
let restore = std::mem::take(&mut self.restore);
self.slot.with(|cell| cell.set(restore));
}
}
pub(crate) struct PassBracket<T: Default + 'static> {
slot: PassSlot<T>,
open: &'static LocalKey<Cell<bool>>,
was_open: bool,
}
impl<T: Default + 'static> PassBracket<T> {
pub(crate) fn enter(
slot: &'static LocalKey<Cell<T>>,
open: &'static LocalKey<Cell<bool>>,
) -> Self {
let was_open = open.with(|flag| flag.replace(true));
Self {
slot: PassSlot::enter(slot, was_open),
open,
was_open,
}
}
pub(crate) fn take(&self) -> T {
self.slot.take()
}
}
impl<T: Default + 'static> Drop for PassBracket<T> {
fn drop(&mut self) {
self.open.with(|flag| flag.set(self.was_open));
}
}
pub(crate) struct PassRequests {
pub(crate) cursor: Option<CursorIcon>,
pub(crate) clipboard_write: Option<String>,
pub(crate) paste_request: bool,
}
pub(crate) struct RequestPass {
cursor: PassSlot<Option<CursorIcon>>,
clipboard_write: PassSlot<Option<String>>,
paste_request: PassSlot<bool>,
edit_commands: PassSlot<Vec<EditCommand>>,
was_open: bool,
}
impl RequestPass {
pub(crate) fn enter() -> Self {
let was_open = REQUEST_PASS_OPEN.with(|open| open.replace(true));
Self {
cursor: PassSlot::enter(&CURSOR_REQUEST, was_open),
clipboard_write: PassSlot::enter(&CLIPBOARD_WRITE, was_open),
paste_request: PassSlot::enter(&PASTE_REQUEST, was_open),
edit_commands: PassSlot::enter(&EDIT_COMMAND_QUEUE, was_open),
was_open,
}
}
pub(crate) fn take(self) -> PassRequests {
let leaked = self.edit_commands.take();
#[cfg(debug_assertions)]
if !leaked.is_empty() {
eprintln!(
"frust-core: {} edit command(s) dispatched but never taken in this \
pass ({leaked:?}); clearing — the dispatching widget's consumer \
must call EventCtx::take_edit_commands in the same pass",
leaked.len()
);
}
drop(leaked);
PassRequests {
cursor: self.cursor.take(),
clipboard_write: self.clipboard_write.take(),
paste_request: self.paste_request.take(),
}
}
}
impl Drop for RequestPass {
fn drop(&mut self) {
REQUEST_PASS_OPEN.with(|open| open.set(self.was_open));
}
}
#[cfg(test)]
pub(crate) fn clear_cursor_request() {
CURSOR_REQUEST.with(|slot| slot.set(None));
}
#[cfg(test)]
pub(crate) fn take_cursor_request() -> Option<CursorIcon> {
CURSOR_REQUEST.with(|slot| slot.take())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct ContactPassState {
open: bool,
pointer_id: PointerId,
secondary: bool,
contacts_requested: bool,
capture_released: bool,
}
impl ContactPassState {
const IDLE: ContactPassState = ContactPassState {
open: false,
pointer_id: PointerId::MOUSE,
secondary: false,
contacts_requested: false,
capture_released: false,
};
}
thread_local! {
static CONTACT_PASS: Cell<ContactPassState> = const { Cell::new(ContactPassState::IDLE) };
}
pub(crate) struct ContactPass {
saved: ContactPassState,
}
impl ContactPass {
pub(crate) fn enter(pointer_id: PointerId, secondary: bool) -> Self {
let saved = CONTACT_PASS.with(|slot| {
slot.replace(ContactPassState {
open: true,
pointer_id,
secondary,
contacts_requested: false,
capture_released: false,
})
});
Self { saved }
}
pub(crate) fn contacts_requested(&self) -> bool {
CONTACT_PASS.with(|slot| slot.get().contacts_requested)
}
pub(crate) fn capture_released(&self) -> bool {
CONTACT_PASS.with(|slot| slot.get().capture_released)
}
}
impl Drop for ContactPass {
fn drop(&mut self) {
CONTACT_PASS.with(|slot| slot.set(self.saved));
}
}
pub(crate) fn current_pointer_id() -> PointerId {
CONTACT_PASS.with(|slot| slot.get().pointer_id)
}
pub(crate) fn in_secondary_contact_pass() -> bool {
CONTACT_PASS.with(|slot| slot.get().secondary)
}
fn note_contacts_requested() {
CONTACT_PASS.with(|slot| {
let mut state = slot.get();
if state.open {
state.contacts_requested = true;
slot.set(state);
}
});
CONTACT_FRAME.with(|slot| {
let mut frame = slot.get();
frame.opted_in = true;
slot.set(frame);
});
}
fn note_capture_released() {
CONTACT_PASS.with(|slot| {
let mut state = slot.get();
if state.open {
state.capture_released = true;
state.contacts_requested = false;
slot.set(state);
}
});
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct ContactFrameState {
opted_in: bool,
opted_in_below: bool,
under_captor: bool,
}
thread_local! {
static CONTACT_FRAME: Cell<ContactFrameState> =
const { Cell::new(ContactFrameState { opted_in: false, opted_in_below: false, under_captor: false }) };
}
pub(crate) struct ContactFrame {
saved: ContactFrameState,
}
impl ContactFrame {
pub(crate) fn enter(captor: bool) -> Self {
let saved = CONTACT_FRAME.with(|slot| {
let saved = slot.get();
slot.set(ContactFrameState {
opted_in: false,
opted_in_below: false,
under_captor: saved.under_captor || captor,
});
saved
});
Self { saved }
}
pub(crate) fn close(self) -> (bool, bool) {
let frame = CONTACT_FRAME.with(|slot| slot.get());
(frame.opted_in, frame.opted_in || frame.opted_in_below)
}
}
impl Drop for ContactFrame {
fn drop(&mut self) {
CONTACT_FRAME.with(|slot| {
let frame = slot.get();
let mut restored = self.saved;
restored.opted_in_below |= frame.opted_in || frame.opted_in_below;
slot.set(restored);
});
}
}
fn under_contact_captor() -> bool {
CONTACT_FRAME.with(|slot| slot.get().under_captor)
}
struct SecondaryWalkFrame {
event: InputEvent,
delivered: Option<EventResult>,
}
thread_local! {
static SECONDARY_WALK: std::cell::RefCell<Option<SecondaryWalkFrame>> =
const { std::cell::RefCell::new(None) };
static SECONDARY_WALK_KEY: OverlayKey = OverlayKey::next();
}
pub(crate) fn secondary_walk_carrier() -> InputEvent {
InputEvent::Overlay(OverlayEvent {
key: SECONDARY_WALK_KEY.with(|key| *key),
kind: OverlayEventKind::OutsideDown,
})
}
fn is_secondary_walk_carrier(event: &InputEvent) -> bool {
matches!(event, InputEvent::Overlay(overlay)
if overlay.key == SECONDARY_WALK_KEY.with(|key| *key))
}
pub(crate) fn secondary_walk_event(event: &InputEvent) -> Option<InputEvent> {
if !is_secondary_walk_carrier(event) {
return None;
}
SECONDARY_WALK.with(|slot| slot.borrow().as_ref().map(|frame| frame.event.clone()))
}
struct SecondaryWalkRestore(Option<SecondaryWalkFrame>);
impl Drop for SecondaryWalkRestore {
fn drop(&mut self) {
let saved = self.0.take();
SECONDARY_WALK.with(|slot| *slot.borrow_mut() = saved);
}
}
pub(crate) fn run_secondary_walk<R>(
event: InputEvent,
f: impl FnOnce() -> R,
) -> (R, Option<EventResult>) {
let saved = SECONDARY_WALK.with(|slot| {
slot.borrow_mut().replace(SecondaryWalkFrame {
event,
delivered: None,
})
});
let restore = SecondaryWalkRestore(saved);
let result = f();
let delivered =
SECONDARY_WALK.with(|slot| slot.borrow().as_ref().and_then(|frame| frame.delivered));
drop(restore);
(result, delivered)
}
pub(crate) fn without_secondary_walk<R>(f: impl FnOnce() -> R) -> R {
let saved = SECONDARY_WALK.with(|slot| slot.borrow_mut().take());
let _restore = SecondaryWalkRestore(saved);
f()
}
pub(crate) fn note_secondary_delivered(result: EventResult) {
SECONDARY_WALK.with(|slot| {
if let Some(frame) = slot.borrow_mut().as_mut() {
frame.delivered = Some(result);
}
});
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ImeContentType {
#[default]
Normal,
Password,
NoSuggestions,
Terminal,
}
impl ImeContentType {
pub fn is_secret(self) -> bool {
match self {
Self::Password => true,
Self::Normal | Self::NoSuggestions | Self::Terminal => false,
}
}
pub fn suppresses_suggestions(self) -> bool {
match self {
Self::Password | Self::NoSuggestions | Self::Terminal => true,
Self::Normal => false,
}
}
}
#[derive(Clone, PartialEq)]
pub struct ImeState {
pub active: bool,
pub editing: EditingState,
pub caret: Option<Rect>,
pub content_type: ImeContentType,
pub suppress_soft_keyboard: bool,
}
impl Default for ImeState {
fn default() -> Self {
Self {
active: false,
editing: EditingState::default(),
caret: None,
content_type: ImeContentType::Normal,
suppress_soft_keyboard: false,
}
}
}
impl fmt::Debug for ImeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
struct Redacted<'a>(&'a EditingState);
impl fmt::Debug for Redacted<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("EditingState")
.field("text", &"<redacted>")
.field("selection_base", &self.0.selection_base)
.field("selection_extent", &self.0.selection_extent)
.field("composing_base", &self.0.composing_base)
.field("composing_extent", &self.0.composing_extent)
.finish()
}
}
let mut s = f.debug_struct("ImeState");
s.field("active", &self.active);
if self.content_type.is_secret() {
s.field("editing", &Redacted(&self.editing));
} else {
s.field("editing", &self.editing);
}
s.field("caret", &self.caret)
.field("content_type", &self.content_type)
.field("suppress_soft_keyboard", &self.suppress_soft_keyboard)
.finish()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventResult {
Ignored,
Handled,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct EventOutcome {
pub handled: bool,
pub needs_redraw: bool,
}
pub struct EventCtx<'a> {
state: &'a mut dyn Any,
needs_redraw: bool,
capture_requested: bool,
contacts_requested: bool,
capture_released: bool,
pointer_id: PointerId,
focus_requested: bool,
focus_released: bool,
has_focus: bool,
hovered: bool,
hover_claimed: bool,
hover_eligible: bool,
hover_epoch: u64,
hover_root: u64,
ime_state: Option<ImeState>,
origin: Point,
size: Size,
}
impl<'a> EventCtx<'a> {
pub fn new(state: &'a mut dyn Any, origin: Point, size: Size) -> Self {
Self {
state,
needs_redraw: false,
capture_requested: false,
contacts_requested: false,
capture_released: false,
pointer_id: current_pointer_id(),
focus_requested: false,
focus_released: false,
has_focus: false,
hovered: false,
hover_claimed: false,
hover_eligible: false,
hover_epoch: 0,
hover_root: 0,
ime_state: None,
origin,
size,
}
}
pub fn state_mut<T: Any>(&mut self) -> &mut T {
self.state
.downcast_mut::<T>()
.expect("event state is not the expected application-state type")
}
pub fn request_redraw(&mut self) {
self.needs_redraw = true;
}
pub fn needs_redraw(&self) -> bool {
self.needs_redraw
}
pub fn capture_pointer(&mut self) {
self.capture_requested = true;
}
pub fn is_pointer_captured(&self) -> bool {
self.capture_requested
}
pub fn pointer_id(&self) -> PointerId {
self.pointer_id
}
pub fn capture_contacts(&mut self) {
self.contacts_requested = true;
note_contacts_requested();
}
pub fn is_contact_capture_requested(&self) -> bool {
self.contacts_requested
}
pub fn release_captured_child(&mut self, child: &mut crate::widget::ChildPod) {
if !child.is_active() {
return;
}
let held_opt_in = child.holds_contact_opt_in();
child.set_active(false);
if child.is_active() || !held_opt_in || under_contact_captor() {
return;
}
self.capture_released = true;
note_capture_released();
}
pub fn is_capture_released(&self) -> bool {
self.capture_released
}
pub fn request_focus(&mut self) {
self.focus_requested = true;
}
pub fn release_focus(&mut self) {
self.focus_released = true;
}
pub fn has_focus(&self) -> bool {
self.has_focus
}
pub fn claim_hover(&mut self) {
if self.hover_eligible {
self.hover_claimed = true;
}
}
pub fn is_hovered(&self) -> bool {
self.hovered
}
pub fn set_cursor(&mut self, icon: CursorIcon) {
CURSOR_REQUEST.with(|slot| slot.set(Some(icon)));
}
pub fn write_clipboard(&mut self, text: String) {
CLIPBOARD_WRITE.with(|slot| slot.set(Some(text)));
}
pub fn request_paste(&mut self) {
PASTE_REQUEST.with(|slot| slot.set(true));
}
pub fn dispatch_edit_command(&mut self, cmd: EditCommand) {
EDIT_COMMAND_QUEUE.with(|slot| {
let mut queue = slot.take();
queue.push(cmd);
slot.set(queue);
});
}
pub fn take_edit_commands(&mut self) -> Vec<EditCommand> {
EDIT_COMMAND_QUEUE.with(|slot| slot.take())
}
pub fn publish_ime_state(&mut self, state: ImeState) {
self.ime_state = Some(state);
}
pub(crate) fn is_focus_requested(&self) -> bool {
self.focus_requested
}
pub(crate) fn is_focus_released(&self) -> bool {
self.focus_released
}
pub(crate) fn take_ime_state(&mut self) -> Option<ImeState> {
self.ime_state.take()
}
pub(crate) fn set_has_focus(&mut self, has_focus: bool) {
self.has_focus = has_focus;
}
pub(crate) fn is_hover_claimed(&self) -> bool {
self.hover_claimed
}
pub(crate) fn is_hover_eligible(&self) -> bool {
self.hover_eligible
}
pub(crate) fn set_hovered(&mut self, hovered: bool) {
self.hovered = hovered;
}
pub(crate) fn set_hover_eligible(&mut self, eligible: bool) {
self.hover_eligible = eligible;
}
pub(crate) fn set_hover_epoch(&mut self, epoch: u64) {
self.hover_epoch = epoch;
}
pub(crate) fn hover_epoch(&self) -> u64 {
self.hover_epoch
}
pub(crate) fn set_hover_root(&mut self, root: u64) {
self.hover_root = root;
}
pub(crate) fn hover_root(&self) -> u64 {
self.hover_root
}
pub(crate) fn hover_claim_epoch(&self) -> u64 {
self.hover_epoch.wrapping_add(1)
}
pub fn origin(&self) -> Point {
self.origin
}
pub fn size(&self) -> Size {
self.size
}
pub(crate) fn child_ctx(
&mut self,
origin: Point,
size: Size,
focused: bool,
hovered: bool,
hover_eligible: bool,
) -> EventCtx<'_> {
EventCtx {
state: &mut *self.state,
needs_redraw: false,
capture_requested: false,
contacts_requested: false,
capture_released: false,
pointer_id: self.pointer_id,
focus_requested: false,
focus_released: false,
has_focus: focused,
hovered,
hover_claimed: false,
hover_eligible,
hover_epoch: self.hover_epoch,
hover_root: self.hover_root,
ime_state: None,
origin,
size,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn absorb_child(
&mut self,
child_needs_redraw: bool,
child_captured: bool,
child_contacts_requested: bool,
child_capture_released: bool,
child_hover_claimed: bool,
child_focus_requested: bool,
child_focus_released: bool,
child_ime_state: Option<ImeState>,
) {
self.needs_redraw |= child_needs_redraw;
self.capture_requested |= child_captured;
self.contacts_requested |= child_contacts_requested;
self.capture_released |= child_capture_released;
self.hover_claimed |= child_hover_claimed;
self.focus_requested |= child_focus_requested;
self.focus_released |= child_focus_released;
if child_ime_state.is_some() {
self.ime_state = child_ime_state;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn down(x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
#[test]
fn state_mut_recovers_concrete_state() {
let mut count = 3u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::new(10.0, 10.0));
*ctx.state_mut::<u32>() += 1;
assert_eq!(count, 4);
}
#[test]
fn request_redraw_and_capture_set_flags() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert!(!ctx.needs_redraw());
assert!(!ctx.is_pointer_captured());
ctx.request_redraw();
ctx.capture_pointer();
assert!(ctx.needs_redraw());
assert!(ctx.is_pointer_captured());
}
#[test]
fn translated_shifts_pointer_position() {
let e = down(20.0, 30.0);
let local = e.translated(-Vec2::new(5.0, 7.0));
assert_eq!(local.position(), Point::new(15.0, 23.0));
assert_eq!(e.position(), Point::new(20.0, 30.0));
}
#[test]
fn transformed_maps_every_positioned_variant_like_translated() {
let offset = Vec2::new(-5.0, -7.0);
let affine = Affine::translate(offset);
let scale = ScaleEvent {
phase: ScalePhase::Update,
scale_delta: 1.5,
focal: Point::new(20.0, 30.0),
velocity: 0.25,
};
let events = [
down(20.0, 30.0),
InputEvent::PointerContact {
pointer_id: PointerId::touch(1),
event: PointerEvent {
phase: PointerPhase::Move,
position: Point::new(20.0, 30.0),
button: PointerButton::Primary,
},
},
InputEvent::Scroll {
position: Point::new(20.0, 30.0),
delta: ScrollDelta::Pixels(3.0, 4.0),
},
InputEvent::Scale(scale),
InputEvent::Housekeeping,
];
for event in &events {
assert_eq!(event.transformed(&affine), event.translated(offset));
}
}
#[test]
fn transformed_maps_positions_through_scale_and_keeps_magnitudes() {
let inverse = (Affine::translate(Vec2::new(10.0, 20.0)) * Affine::scale(2.0)).inverse();
assert_eq!(
down(30.0, 60.0).transformed(&inverse).position(),
Point::new(10.0, 20.0)
);
let scroll = InputEvent::Scroll {
position: Point::new(30.0, 60.0),
delta: ScrollDelta::Pixels(3.0, 4.0),
};
assert_eq!(
scroll.transformed(&inverse),
InputEvent::Scroll {
position: Point::new(10.0, 20.0),
delta: ScrollDelta::Pixels(3.0, 4.0),
}
);
let scale = InputEvent::Scale(ScaleEvent {
phase: ScalePhase::Begin,
scale_delta: 1.25,
focal: Point::new(30.0, 60.0),
velocity: 2.0,
});
assert_eq!(
scale.transformed(&inverse),
InputEvent::Scale(ScaleEvent {
phase: ScalePhase::Begin,
scale_delta: 1.25,
focal: Point::new(10.0, 20.0),
velocity: 2.0,
})
);
}
#[test]
fn absorb_child_folds_flags_upward() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
{
let mut child = ctx.child_ctx(
Point::new(1.0, 2.0),
Size::new(3.0, 4.0),
false,
false,
false,
);
child.request_redraw();
child.capture_pointer();
let (redraw, cap) = (child.needs_redraw(), child.is_pointer_captured());
ctx.absorb_child(redraw, cap, false, false, false, false, false, None);
}
assert!(ctx.needs_redraw());
assert!(ctx.is_pointer_captured());
}
#[test]
fn request_and_release_focus_set_flags() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert!(!ctx.is_focus_requested());
assert!(!ctx.is_focus_released());
assert!(!ctx.has_focus());
ctx.request_focus();
ctx.release_focus();
assert!(ctx.is_focus_requested());
assert!(ctx.is_focus_released());
}
#[test]
fn child_ctx_seeds_has_focus_from_pod_flag() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
let focused_child = ctx.child_ctx(Point::ZERO, Size::ZERO, true, false, false);
assert!(focused_child.has_focus());
let unfocused_child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
assert!(!unfocused_child.has_focus());
}
#[test]
fn absorb_child_folds_focus_and_ime_upward() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
let published = ImeState {
active: true,
editing: EditingState {
text: "hi".to_string(),
selection_base: 2,
selection_extent: 2,
composing_base: -1,
composing_extent: -1,
},
caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
content_type: ImeContentType::Normal,
suppress_soft_keyboard: false,
};
{
let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
child.request_focus();
child.publish_ime_state(published.clone());
let (fr, frl, ime) = (
child.is_focus_requested(),
child.is_focus_released(),
child.take_ime_state(),
);
ctx.absorb_child(false, false, false, false, false, fr, frl, ime);
}
assert!(ctx.is_focus_requested());
assert!(!ctx.is_focus_released());
assert_eq!(ctx.take_ime_state(), Some(published));
}
#[test]
fn claim_hover_records_nothing_unless_the_pass_is_eligible() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert!(!ctx.is_hover_eligible());
ctx.claim_hover();
assert!(
!ctx.is_hover_claimed(),
"an ineligible claim records nothing"
);
ctx.set_hover_eligible(true);
ctx.claim_hover();
assert!(ctx.is_hover_claimed());
assert!(!ctx.needs_redraw());
}
#[test]
fn child_ctx_seeds_hover_and_absorb_bubbles_a_claim() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.set_hover_epoch(7);
{
let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, true, true);
assert!(child.is_hovered());
assert_eq!(child.hover_epoch(), 7, "the live epoch threads down");
assert_eq!(child.hover_claim_epoch(), 8, "a claim takes the next epoch");
child.claim_hover();
let claimed = child.is_hover_claimed();
ctx.absorb_child(false, false, false, false, claimed, false, false, None);
}
assert!(ctx.is_hover_claimed());
}
#[test]
fn content_type_defaults_to_no_hint() {
assert_eq!(ImeContentType::default(), ImeContentType::Normal);
assert!(!ImeContentType::default().is_secret());
assert!(!ImeContentType::default().suppresses_suggestions());
}
#[test]
fn content_type_predicates_classify_every_variant() {
assert!(ImeContentType::Password.is_secret());
assert!(ImeContentType::Password.suppresses_suggestions());
assert!(!ImeContentType::NoSuggestions.is_secret());
assert!(ImeContentType::NoSuggestions.suppresses_suggestions());
assert!(!ImeContentType::Terminal.is_secret());
assert!(ImeContentType::Terminal.suppresses_suggestions());
assert!(!ImeContentType::Normal.is_secret());
assert!(!ImeContentType::Normal.suppresses_suggestions());
}
#[test]
fn default_ime_state_is_cleared_and_unhinted() {
let s = ImeState::default();
assert!(!s.active);
assert!(s.caret.is_none());
assert_eq!(s.content_type, ImeContentType::Normal);
assert_eq!(
s.editing,
EditingState {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
}
);
}
#[test]
fn publish_ime_state_round_trips_the_content_type() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
let published = ImeState {
active: true,
editing: EditingState {
text: "hunter2".to_string(),
selection_base: 7,
selection_extent: 7,
composing_base: -1,
composing_extent: -1,
},
caret: Some(Rect::new(0.0, 0.0, 1.0, 10.0)),
content_type: ImeContentType::Password,
suppress_soft_keyboard: false,
};
ctx.publish_ime_state(published.clone());
let taken = ctx.take_ime_state().expect("published state");
assert_eq!(taken, published);
assert_eq!(taken.content_type, ImeContentType::Password);
assert_eq!(taken.editing.text, "hunter2");
}
#[test]
fn content_type_bubbles_up_the_focus_chain() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
let published = ImeState {
content_type: ImeContentType::NoSuggestions,
..ImeState::default()
};
{
let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, true, false, false);
child.publish_ime_state(published.clone());
let ime = child.take_ime_state();
ctx.absorb_child(false, false, false, false, false, false, false, ime);
}
assert_eq!(ctx.take_ime_state(), Some(published));
}
#[test]
fn suppress_soft_keyboard_defaults_off_round_trips_and_prints_plainly() {
assert!(
!ImeState::default().suppress_soft_keyboard,
"a publisher that says nothing must behave as it did before the hint existed"
);
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
let published = ImeState {
active: true,
suppress_soft_keyboard: true,
..ImeState::default()
};
ctx.publish_ime_state(published.clone());
let taken = ctx.take_ime_state().expect("published state");
assert_eq!(taken, published);
assert!(taken.suppress_soft_keyboard);
assert!(format!("{taken:?}").contains("suppress_soft_keyboard: true"));
}
#[test]
fn debug_redacts_a_secret_field_but_not_a_normal_one() {
let secret = ImeState {
active: true,
editing: EditingState {
text: "hunter2".to_string(),
..EditingState::default()
},
caret: None,
content_type: ImeContentType::Password,
suppress_soft_keyboard: false,
};
let rendered = format!("{secret:?}");
assert!(
!rendered.contains("hunter2"),
"secret text leaked: {rendered}"
);
assert!(rendered.contains("<redacted>"));
assert!(rendered.contains("Password"));
let plain = ImeState {
content_type: ImeContentType::Normal,
..secret
};
assert!(format!("{plain:?}").contains("hunter2"));
}
#[test]
fn key_and_ime_events_are_focus_routed_with_zero_position() {
let key = InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
repeat: false,
});
assert!(key.is_focus_routed());
assert_eq!(key.position(), Point::ZERO);
assert_eq!(key.translated(Vec2::new(5.0, 5.0)), key);
let ime = InputEvent::Ime(ImeEvent::Commit("x".to_string()));
assert!(ime.is_focus_routed());
assert!(!down(1.0, 1.0).is_focus_routed());
}
#[test]
fn pending_result_flush_peek_observes_the_mark_without_draining_it() {
let _ = take_pending_result_flush();
assert!(!has_pending_result_flush(), "starts clear");
mark_pending_result_flush();
assert!(has_pending_result_flush(), "the peek observes the mark");
assert!(has_pending_result_flush());
assert!(has_pending_result_flush());
assert!(take_pending_result_flush(), "the drain still sees the mark");
assert!(
!has_pending_result_flush(),
"the drain is what clears it, not the peek"
);
}
#[test]
fn a_hover_mark_belongs_to_the_root_whose_link_it_names() {
const ROOT_A: u64 = 11;
const ROOT_B: u64 = 22;
const EPOCH: u64 = 7;
set_live_hover_link(ROOT_B, EPOCH);
assert!(
live_hover_link_is(ROOT_B, EPOCH),
"the publishing root's pod recognizes its own live link"
);
assert!(
!live_hover_link_is(ROOT_A, EPOCH),
"the same epoch integer under another root is not this link"
);
assert!(
!live_hover_link_is(ROOT_B, 0),
"epoch 0 is 'no link' and matches nothing"
);
mark_hover_orphaned(ROOT_A);
assert!(
!take_hover_orphaned(ROOT_B),
"a root does not end its hover on another root's severance"
);
assert!(
take_hover_orphaned(ROOT_A),
"and the mark is still standing for the root that owns it"
);
assert!(
!take_hover_orphaned(ROOT_A),
"the drain is destructive for the matching root"
);
set_live_hover_link(0, 0);
}
#[test]
fn a_cursor_request_is_one_slot_the_last_writer_owns() {
clear_cursor_request();
assert_eq!(take_cursor_request(), None, "absence means Default");
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.set_cursor(CursorIcon::Text);
{
let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
child.set_cursor(CursorIcon::Pointer);
}
assert_eq!(
take_cursor_request(),
Some(CursorIcon::Pointer),
"the last set_cursor of the pass is the resolved one"
);
assert_eq!(
take_cursor_request(),
None,
"the drain is destructive — one request per pass"
);
}
#[test]
fn a_nested_request_pass_resolves_its_own_and_hands_the_slot_back() {
let outer = RequestPass::enter();
let mut count = 0u32;
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.set_cursor(CursorIcon::Grab);
}
assert_eq!(
RequestPass::enter().take().cursor,
None,
"a nested pass starts from absence, like any other"
);
{
let inner = RequestPass::enter();
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.set_cursor(CursorIcon::Text);
drop(ctx);
assert_eq!(
inner.take().cursor,
Some(CursorIcon::Text),
"and resolves exactly what was asked inside it"
);
}
assert_eq!(
outer.take().cursor,
Some(CursorIcon::Grab),
"the enclosing pass's request survived the nested one"
);
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.set_cursor(CursorIcon::NotAllowed);
}
let next = RequestPass::enter();
assert_eq!(
next.take().cursor,
None,
"a request made between passes belongs to no pass"
);
}
#[test]
fn the_default_cursor_is_the_platform_arrow() {
assert_eq!(CursorIcon::default(), CursorIcon::Default);
}
#[test]
fn a_nested_request_pass_hands_the_clipboard_slots_back_too() {
let outer = RequestPass::enter();
let mut count = 0u32;
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.write_clipboard("outer".to_string());
ctx.request_paste();
}
{
let inner = RequestPass::enter();
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.write_clipboard("inner".to_string());
drop(ctx);
let resolved = inner.take();
assert_eq!(
resolved.clipboard_write.as_deref(),
Some("inner"),
"a nested pass resolves exactly what was written inside it"
);
assert!(
!resolved.paste_request,
"and starts from absence rather than inheriting the enclosing ask"
);
}
let resolved = outer.take();
assert_eq!(
resolved.clipboard_write.as_deref(),
Some("outer"),
"the enclosing pass's write survived the nested one"
);
assert!(resolved.paste_request, "and so did its paste request");
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.write_clipboard("stray".to_string());
ctx.request_paste();
}
let next = RequestPass::enter().take();
assert_eq!(next.clipboard_write, None);
assert!(!next.paste_request);
}
#[test]
fn the_last_clipboard_write_of_a_pass_wins() {
let pass = RequestPass::enter();
let mut count = 0u32;
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.write_clipboard("container".to_string());
ctx.write_clipboard("leaf".to_string());
}
assert_eq!(
pass.take().clipboard_write.as_deref(),
Some("leaf"),
"one write is resolved per pass, and the last caller is it"
);
}
#[test]
fn a_paste_request_is_idempotent_within_a_pass() {
let pass = RequestPass::enter();
let mut count = 0u32;
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.request_paste();
ctx.request_paste();
}
assert!(pass.take().paste_request);
}
#[test]
fn an_edit_command_is_focus_routed_with_zero_position() {
let copy = InputEvent::EditCommand(EditCommand::Copy);
assert!(copy.is_focus_routed(), "a clipboard verb follows the focus");
assert!(!copy.is_broadcast(), "and is not a broadcast");
assert_eq!(copy.position(), Point::ZERO);
assert_eq!(
copy.translated(Vec2::new(10.0, 20.0)),
copy,
"a positionless event is returned unchanged by a container's translate"
);
}
fn overlay(kind: OverlayEventKind) -> InputEvent {
InputEvent::Overlay(OverlayEvent {
key: OverlayKey::next(),
kind,
})
}
#[test]
fn overlay_is_a_broadcast_and_housekeeping_is_the_only_other_one() {
let routed = overlay(OverlayEventKind::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(120.0, 80.0),
button: PointerButton::Primary,
}));
assert!(
routed.is_broadcast(),
"an overlay event reaches its owner by broadcast, wherever the owner sits"
);
assert!(
!routed.is_focus_routed(),
"and not down the focus chain — the surface's owner need not be focused"
);
assert!(InputEvent::Housekeeping.is_broadcast());
for event in [
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::ZERO,
button: PointerButton::Primary,
}),
InputEvent::Scroll {
position: Point::ZERO,
delta: ScrollDelta::Lines(0.0, 1.0),
},
InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::Enter),
modifiers: Modifiers::default(),
repeat: false,
}),
InputEvent::Ime(ImeEvent::Enabled),
InputEvent::EditCommand(EditCommand::Copy),
] {
assert!(
!event.is_broadcast(),
"only Housekeeping and Overlay broadcast, but {event:?} claims to"
);
}
}
#[test]
fn an_overlay_event_carries_no_local_position_and_is_never_translated() {
let routed = overlay(OverlayEventKind::Pointer(PointerEvent {
phase: PointerPhase::Move,
position: Point::new(120.0, 80.0),
button: PointerButton::Primary,
}));
assert_eq!(
routed.position(),
Point::ZERO,
"a broadcast is never hit-tested, so it reports no position to hit-test on"
);
assert_eq!(
routed.translated(Vec2::new(-10.0, -20.0)),
routed,
"the container chain must not shift a window-space payload"
);
let scrolled = overlay(OverlayEventKind::Scroll {
position: Point::new(120.0, 80.0),
delta: ScrollDelta::Pixels(0.0, 12.0),
});
assert_eq!(scrolled.translated(Vec2::new(5.0, 5.0)), scrolled);
let outside = overlay(OverlayEventKind::OutsideDown);
assert_eq!(outside.position(), Point::ZERO);
assert_eq!(outside.translated(Vec2::new(5.0, 5.0)), outside);
}
#[test]
fn edit_commands_drain_in_dispatch_order_within_one_pass() {
let pass = RequestPass::enter();
let mut count = 0u32;
let taken = {
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.dispatch_edit_command(EditCommand::Copy);
ctx.dispatch_edit_command(EditCommand::SelectAll);
ctx.dispatch_edit_command(EditCommand::Cut);
ctx.take_edit_commands()
};
assert_eq!(
taken,
vec![EditCommand::Copy, EditCommand::SelectAll, EditCommand::Cut],
"a FIFO, not a last-writer-wins slot"
);
let mut second = 0u32;
let mut ctx = EventCtx::new(&mut second, Point::ZERO, Size::ZERO);
assert!(ctx.take_edit_commands().is_empty());
drop(ctx);
drop(pass.take());
}
#[test]
fn a_leaked_edit_command_is_cleared_with_the_pass_and_never_reaches_the_next() {
{
let pass = RequestPass::enter();
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.dispatch_edit_command(EditCommand::Cut);
drop(ctx);
drop(pass.take());
}
let next = RequestPass::enter();
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert!(
ctx.take_edit_commands().is_empty(),
"a leaked command must not survive into the next pass"
);
drop(ctx);
drop(next.take());
}
#[test]
fn a_nested_pass_hands_the_enclosing_passs_edit_commands_back() {
let outer = RequestPass::enter();
let mut count = 0u32;
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
ctx.dispatch_edit_command(EditCommand::Copy);
}
{
let inner = RequestPass::enter();
let mut inner_state = 0u32;
let mut ctx = EventCtx::new(&mut inner_state, Point::ZERO, Size::ZERO);
assert!(ctx.take_edit_commands().is_empty());
ctx.dispatch_edit_command(EditCommand::SelectAll);
assert_eq!(ctx.take_edit_commands(), vec![EditCommand::SelectAll]);
drop(ctx);
drop(inner.take());
}
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert_eq!(
ctx.take_edit_commands(),
vec![EditCommand::Copy],
"the enclosing pass's queue is handed back intact"
);
drop(ctx);
drop(outer.take());
}
#[test]
fn debug_redacts_a_pasted_payload() {
let rendered = format!("{:?}", EditCommand::Paste("hunter2".to_string()));
assert!(
!rendered.contains("hunter2"),
"paste payload leaked: {rendered}"
);
assert!(rendered.contains("<redacted>"));
assert!(
rendered.contains("Paste"),
"the verb still prints: {rendered}"
);
assert!(!rendered.contains('7'));
assert_eq!(format!("{:?}", EditCommand::SelectAll), "SelectAll");
}
#[test]
fn a_pointer_contact_is_positioned_and_translated_like_a_pointer() {
let contact = InputEvent::PointerContact {
pointer_id: PointerId::touch(2),
event: PointerEvent {
phase: PointerPhase::Move,
position: Point::new(30.0, 40.0),
button: PointerButton::Primary,
},
};
assert_eq!(contact.position(), Point::new(30.0, 40.0));
assert_eq!(
contact.translated(Vec2::new(-10.0, -20.0)),
InputEvent::PointerContact {
pointer_id: PointerId::touch(2),
event: PointerEvent {
phase: PointerPhase::Move,
position: Point::new(20.0, 20.0),
button: PointerButton::Primary,
},
},
"the position shifts; the id and the rest of the event do not"
);
assert!(!contact.is_focus_routed());
assert!(!contact.is_broadcast());
}
#[test]
fn pointer_ids_name_the_mouse_and_touch_slots() {
assert_eq!(
PointerId::MOUSE,
PointerId {
source: PointerSource::Mouse,
slot: 0
}
);
assert_eq!(
PointerId::touch(3),
PointerId {
source: PointerSource::Touch,
slot: 3
}
);
assert_ne!(PointerId::MOUSE, PointerId::touch(0));
}
#[test]
fn a_context_reports_the_mouse_outside_any_contact_pass() {
let mut count = 0u32;
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert_eq!(ctx.pointer_id(), PointerId::MOUSE);
ctx.capture_contacts();
assert!(ctx.is_contact_capture_requested());
drop(ctx);
let pass = ContactPass::enter(PointerId::touch(0), false);
assert!(!pass.contacts_requested());
}
#[test]
fn a_contact_pass_seeds_every_context_and_collects_the_opt_in() {
let mut count = 0u32;
let outer = ContactPass::enter(PointerId::touch(1), true);
assert!(in_secondary_contact_pass());
{
let mut ctx = EventCtx::new(&mut count, Point::ZERO, Size::ZERO);
assert_eq!(ctx.pointer_id(), PointerId::touch(1));
let (contacts, id) = {
let mut child = ctx.child_ctx(Point::ZERO, Size::ZERO, false, false, false);
let id = child.pointer_id();
child.capture_contacts();
(child.is_contact_capture_requested(), id)
};
assert_eq!(id, PointerId::touch(1));
ctx.absorb_child(false, false, contacts, false, false, false, false, None);
assert!(ctx.is_contact_capture_requested(), "the opt-in bubbles");
}
assert!(outer.contacts_requested(), "and is recorded in the pass");
{
let inner = ContactPass::enter(PointerId::MOUSE, false);
assert_eq!(current_pointer_id(), PointerId::MOUSE);
assert!(!in_secondary_contact_pass());
assert!(!inner.contacts_requested());
}
assert_eq!(current_pointer_id(), PointerId::touch(1));
assert!(in_secondary_contact_pass());
assert!(outer.contacts_requested());
drop(outer);
assert_eq!(current_pointer_id(), PointerId::MOUSE);
assert!(!in_secondary_contact_pass());
}
}