use crate::cursor::CursorShape;
use crate::geom::{Rect, Vec2};
use crate::key::Key;
use crate::tree::OriginId;
use crate::value::Value;
use crate::window::{WindowCommand, WindowId, WindowRole};
#[derive(Clone, Debug, PartialEq)]
pub enum InputEvent {
CursorMoved(Vec2),
CursorLeft,
MouseDown {
button: MouseButton,
clicks: u8,
},
MouseUp {
button: MouseButton,
},
Scroll(Vec2),
ScrollGesture {
delta: Vec2,
begins: bool,
},
Text(String),
Commit(String),
Paste {
text: String,
marks: ClipboardMarks,
},
Preedit(String, Option<(usize, usize)>),
Key(EditKey, Mods),
KeyDown(KeyPress),
KeyUp(KeyPress),
Access(crate::access::AccessRequest),
Modifiers(KeyMods),
ForceClick(Vec2),
DragFiles {
paths: Vec<String>,
at: Vec2,
},
DropFiles {
paths: Vec<String>,
at: Vec2,
},
DragCancel,
Files(Vec<String>),
Open(Vec<String>),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ClipboardMarks {
pub concealed: bool,
pub transient: bool,
}
impl ClipboardMarks {
pub const SECRET: Self = Self {
concealed: true,
transient: true,
};
pub fn is_empty(self) -> bool {
!self.concealed && !self.transient
}
pub fn bits(self) -> u32 {
self.concealed as u32 | (self.transient as u32) << 1
}
pub fn from_bits(bits: u32) -> Self {
Self {
concealed: bits & 1 != 0,
transient: bits & 2 != 0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MouseButton {
#[default]
Primary,
Secondary,
Middle,
Other(u8),
}
impl MouseButton {
pub fn code(self) -> u32 {
match self {
MouseButton::Primary => 0,
MouseButton::Secondary => 1,
MouseButton::Middle => 2,
MouseButton::Other(n) => 3 + n as u32,
}
}
pub fn from_code(code: u32) -> Self {
match code {
0 => MouseButton::Primary,
1 => MouseButton::Secondary,
2 => MouseButton::Middle,
n => MouseButton::Other((n - 3).min(u8::MAX as u32) as u8),
}
}
pub const NAMED: [MouseButton; 3] = [
MouseButton::Primary,
MouseButton::Secondary,
MouseButton::Middle,
];
pub fn name(self) -> Option<&'static str> {
match self {
MouseButton::Primary => Some("primary"),
MouseButton::Secondary => Some("secondary"),
MouseButton::Middle => Some("middle"),
MouseButton::Other(_) => None,
}
}
pub fn from_name(name: &str) -> Option<Self> {
Self::NAMED.into_iter().find(|b| b.name() == Some(name))
}
pub fn to_value(self) -> Value {
match self.name() {
Some(name) => Value::str(name),
None => Value::Int(self.code() as i64),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Buttons(u8);
impl Buttons {
pub const NONE: Self = Self(0);
pub const SECONDARY: Self = Self(1);
pub const MIDDLE: Self = Self(2);
pub const OTHER: Self = Self(4);
pub const ALL: Self = Self(7);
pub fn contains(self, button: MouseButton) -> bool {
let bit = match button {
MouseButton::Primary => return false,
MouseButton::Secondary => Self::SECONDARY,
MouseButton::Middle => Self::MIDDLE,
MouseButton::Other(_) => Self::OTHER,
};
self.0 & bit.0 != 0
}
pub const fn bits(self) -> u32 {
self.0 as u32
}
pub fn from_bits(bits: u32) -> Self {
Self((bits & Self::ALL.0 as u32) as u8)
}
pub fn parse(names: &str) -> Self {
names.split(|c: char| c == ',' || c.is_whitespace()).fold(
Self::NONE,
|set, name| match name {
"secondary" => set | Self::SECONDARY,
"middle" => set | Self::MIDDLE,
"other" => set | Self::OTHER,
_ => set,
},
)
}
}
impl Default for Buttons {
fn default() -> Self {
Self::ALL
}
}
impl std::ops::BitOr for Buttons {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for Buttons {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl InputEvent {
pub fn mouse_down(clicks: u8) -> Self {
InputEvent::MouseDown {
button: MouseButton::Primary,
clicks,
}
}
pub fn mouse_up() -> Self {
InputEvent::MouseUp {
button: MouseButton::Primary,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EditKey {
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Backspace,
Delete,
Enter,
Tab,
SelectAll,
Undo,
Redo,
Escape,
}
impl EditKey {
pub const ALL: [EditKey; 16] = [
EditKey::Left,
EditKey::Right,
EditKey::Up,
EditKey::Down,
EditKey::Home,
EditKey::End,
EditKey::PageUp,
EditKey::PageDown,
EditKey::Backspace,
EditKey::Delete,
EditKey::Enter,
EditKey::Tab,
EditKey::SelectAll,
EditKey::Undo,
EditKey::Redo,
EditKey::Escape,
];
pub fn name(self) -> &'static str {
match self {
EditKey::Left => "left",
EditKey::Right => "right",
EditKey::Up => "up",
EditKey::Down => "down",
EditKey::Home => "home",
EditKey::End => "end",
EditKey::PageUp => "pageup",
EditKey::PageDown => "pagedown",
EditKey::Backspace => "backspace",
EditKey::Delete => "delete",
EditKey::Enter => "enter",
EditKey::Tab => "tab",
EditKey::SelectAll => "selectall",
EditKey::Undo => "undo",
EditKey::Redo => "redo",
EditKey::Escape => "escape",
}
}
pub fn from_name(name: &str) -> Option<EditKey> {
Self::ALL.into_iter().find(|k| k.name() == name)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Mods {
pub shift: bool,
pub word: bool,
pub doc: bool,
}
impl Mods {
pub const NONE: Mods = Mods {
shift: false,
word: false,
doc: false,
};
pub const fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub const fn with_word(mut self) -> Self {
self.word = true;
self
}
pub const fn with_doc(mut self) -> Self {
self.doc = true;
self
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyCode {
Char(char),
F(u8),
Left,
Right,
Up,
Down,
Home,
End,
PageUp,
PageDown,
Backspace,
Delete,
Enter,
Tab,
Escape,
Space,
Insert,
PrintScreen,
Pause,
Menu,
Clear,
Shift,
Ctrl,
Alt,
Super,
CapsLock,
NumLock,
ScrollLock,
MediaPlay,
MediaPause,
MediaPlayPause,
MediaStop,
MediaNext,
MediaPrev,
MediaRecord,
MediaFastForward,
MediaRewind,
VolumeUp,
VolumeDown,
VolumeMute,
Unknown,
}
const NAMED_KEYS: [(KeyCode, &str); 38] = [
(KeyCode::Left, "left"),
(KeyCode::Right, "right"),
(KeyCode::Up, "up"),
(KeyCode::Down, "down"),
(KeyCode::Home, "home"),
(KeyCode::End, "end"),
(KeyCode::PageUp, "pageup"),
(KeyCode::PageDown, "pagedown"),
(KeyCode::Backspace, "backspace"),
(KeyCode::Delete, "delete"),
(KeyCode::Enter, "enter"),
(KeyCode::Tab, "tab"),
(KeyCode::Escape, "escape"),
(KeyCode::Space, "space"),
(KeyCode::Insert, "insert"),
(KeyCode::PrintScreen, "printscreen"),
(KeyCode::Pause, "pause"),
(KeyCode::Menu, "menu"),
(KeyCode::Clear, "clear"),
(KeyCode::Shift, "shift"),
(KeyCode::Ctrl, "ctrl"),
(KeyCode::Alt, "alt"),
(KeyCode::Super, "super"),
(KeyCode::CapsLock, "capslock"),
(KeyCode::NumLock, "numlock"),
(KeyCode::ScrollLock, "scrolllock"),
(KeyCode::MediaPlay, "mediaplay"),
(KeyCode::MediaPause, "mediapause"),
(KeyCode::MediaPlayPause, "mediaplaypause"),
(KeyCode::MediaStop, "mediastop"),
(KeyCode::MediaNext, "medianext"),
(KeyCode::MediaPrev, "mediaprev"),
(KeyCode::MediaRecord, "mediarecord"),
(KeyCode::MediaFastForward, "mediafastforward"),
(KeyCode::MediaRewind, "mediarewind"),
(KeyCode::VolumeUp, "volumeup"),
(KeyCode::VolumeDown, "volumedown"),
(KeyCode::VolumeMute, "volumemute"),
];
impl KeyCode {
pub fn name(self) -> String {
match self {
KeyCode::Char(c) => c.to_string(),
KeyCode::F(n) => format!("f{n}"),
KeyCode::Unknown => "unknown".into(),
named => NAMED_KEYS
.iter()
.find(|(k, _)| *k == named)
.map_or("unknown", |(_, n)| n)
.into(),
}
}
pub fn named() -> &'static [(KeyCode, &'static str)] {
&NAMED_KEYS
}
pub fn is_modifier(self) -> bool {
matches!(
self,
KeyCode::Shift
| KeyCode::Ctrl
| KeyCode::Alt
| KeyCode::Super
| KeyCode::CapsLock
| KeyCode::NumLock
| KeyCode::ScrollLock
)
}
pub fn from_name(s: &str) -> Option<KeyCode> {
let mut chars = s.chars();
if let (Some(c), None) = (chars.next(), chars.next()) {
return Some(KeyCode::Char(c));
}
if let Some(n) = s.strip_prefix('f').and_then(|n| n.parse::<u8>().ok())
&& (1..=35).contains(&n)
{
return Some(KeyCode::F(n));
}
if s == "unknown" {
return Some(KeyCode::Unknown);
}
NAMED_KEYS.iter().find(|(_, n)| *n == s).map(|(k, _)| *k)
}
pub fn typed(self, mods: KeyMods) -> Option<String> {
if mods.ctrl || mods.alt || mods.super_key {
return None;
}
match self {
KeyCode::Char(c) => Some(c.to_string()),
KeyCode::Space => Some(" ".to_string()),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum KeyPhase {
#[default]
Down,
Up,
}
impl KeyPhase {
pub fn name(self) -> &'static str {
match self {
KeyPhase::Down => "down",
KeyPhase::Up => "up",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum KeyLocation {
#[default]
Standard,
Left,
Right,
Numpad,
}
impl KeyLocation {
pub fn name(self) -> &'static str {
match self {
KeyLocation::Standard => "standard",
KeyLocation::Left => "left",
KeyLocation::Right => "right",
KeyLocation::Numpad => "numpad",
}
}
pub fn from_name(s: &str) -> Option<Self> {
Some(match s {
"standard" => KeyLocation::Standard,
"left" => KeyLocation::Left,
"right" => KeyLocation::Right,
"numpad" => KeyLocation::Numpad,
_ => return None,
})
}
pub const SHIFT: u32 = 8;
pub const MASK: u32 = 3 << Self::SHIFT;
pub fn bits(self) -> u32 {
(match self {
KeyLocation::Standard => 0,
KeyLocation::Left => 1,
KeyLocation::Right => 2,
KeyLocation::Numpad => 3,
}) << Self::SHIFT
}
pub fn from_bits(bits: u32) -> Self {
match (bits & Self::MASK) >> Self::SHIFT {
1 => KeyLocation::Left,
2 => KeyLocation::Right,
3 => KeyLocation::Numpad,
_ => KeyLocation::Standard,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum OptionAsAlt {
#[default]
None,
Left,
Right,
Both,
}
impl OptionAsAlt {
pub const ALL: [OptionAsAlt; 4] = [
OptionAsAlt::None,
OptionAsAlt::Left,
OptionAsAlt::Right,
OptionAsAlt::Both,
];
pub fn name(self) -> &'static str {
match self {
OptionAsAlt::None => "none",
OptionAsAlt::Left => "left",
OptionAsAlt::Right => "right",
OptionAsAlt::Both => "both",
}
}
pub fn from_name(s: &str) -> Option<Self> {
Self::ALL.into_iter().find(|v| v.name() == s)
}
pub fn index(self) -> u32 {
self as u32
}
pub fn from_index(index: u32) -> Option<Self> {
Self::ALL.get(index as usize).copied()
}
pub fn covers(self, location: KeyLocation) -> bool {
match self {
OptionAsAlt::None => false,
OptionAsAlt::Left => location == KeyLocation::Left,
OptionAsAlt::Right => location == KeyLocation::Right,
OptionAsAlt::Both => matches!(location, KeyLocation::Left | KeyLocation::Right),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KeyLocks {
pub caps: bool,
pub num: bool,
}
impl KeyLocks {
pub const CAPS: u32 = 1 << 4;
pub const NUM: u32 = 1 << 5;
pub fn bits(self) -> u32 {
(if self.caps { Self::CAPS } else { 0 }) | (if self.num { Self::NUM } else { 0 })
}
pub fn from_bits(bits: u32) -> Self {
KeyLocks {
caps: bits & Self::CAPS != 0,
num: bits & Self::NUM != 0,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum LayoutScript {
#[default]
Latin,
NonLatin,
}
impl LayoutScript {
pub const NON_LATIN: u32 = 1 << 6;
pub fn from_bits(bits: u32) -> Self {
if bits & Self::NON_LATIN != 0 {
LayoutScript::NonLatin
} else {
LayoutScript::Latin
}
}
pub fn from_name(s: &str) -> Option<Self> {
match s {
"latin" => Some(LayoutScript::Latin),
"nonLatin" | "non_latin" => Some(LayoutScript::NonLatin),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct KeyMods {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub super_key: bool,
}
impl KeyMods {
pub const SHIFT: u32 = 1 << 0;
pub const CTRL: u32 = 1 << 1;
pub const ALT: u32 = 1 << 2;
pub const SUPER: u32 = 1 << 3;
pub const NONE: KeyMods = KeyMods {
shift: false,
ctrl: false,
alt: false,
super_key: false,
};
pub const fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub const fn with_ctrl(mut self) -> Self {
self.ctrl = true;
self
}
pub const fn with_alt(mut self) -> Self {
self.alt = true;
self
}
pub const fn with_super(mut self) -> Self {
self.super_key = true;
self
}
pub const fn with_primary(self) -> Self {
if cfg!(target_os = "macos") {
self.with_super()
} else {
self.with_ctrl()
}
}
pub fn from_bits(bits: u32) -> Self {
Self {
shift: bits & Self::SHIFT != 0,
ctrl: bits & Self::CTRL != 0,
alt: bits & Self::ALT != 0,
super_key: bits & Self::SUPER != 0,
}
}
pub fn bits(self) -> u32 {
let bit = |on: bool, b: u32| if on { b } else { 0 };
bit(self.shift, Self::SHIFT)
| bit(self.ctrl, Self::CTRL)
| bit(self.alt, Self::ALT)
| bit(self.super_key, Self::SUPER)
}
pub fn any(self) -> bool {
self.shift || self.ctrl || self.alt || self.super_key
}
pub fn any_of(self, other: KeyMods) -> bool {
self.bits() & other.bits() != 0
}
pub fn parse(names: &str) -> Self {
names.split(|c: char| c == ',' || c.is_whitespace()).fold(
Self::NONE,
|m, name| match name {
"shift" => m.with_shift(),
"ctrl" => m.with_ctrl(),
"alt" => m.with_alt(),
"super" => m.with_super(),
_ => m,
},
)
}
pub fn to_fields(self) -> Value {
Value::map([
("shift", Value::Bool(self.shift)),
("ctrl", Value::Bool(self.ctrl)),
("alt", Value::Bool(self.alt)),
("super", Value::Bool(self.super_key)),
])
}
pub fn primary(self) -> bool {
if cfg!(target_os = "macos") {
self.super_key
} else {
self.ctrl
}
}
pub fn to_value(self) -> Value {
Value::map([
("kind", Value::str("modifiers")),
("shift", Value::Bool(self.shift)),
("ctrl", Value::Bool(self.ctrl)),
("alt", Value::Bool(self.alt)),
("super", Value::Bool(self.super_key)),
])
}
}
fn us_shifted(c: char) -> char {
match c {
'a'..='z' => c.to_ascii_uppercase(),
'1' => '!',
'2' => '@',
'3' => '#',
'4' => '$',
'5' => '%',
'6' => '^',
'7' => '&',
'8' => '*',
'9' => '(',
'0' => ')',
'`' => '~',
'-' => '_',
'=' => '+',
'[' => '{',
']' => '}',
'\\' => '|',
';' => ':',
'\'' => '"',
',' => '<',
'.' => '>',
'/' => '?',
other => other,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct KeyPress {
pub code: KeyCode,
pub physical: KeyCode,
pub mods: KeyMods,
pub text: Option<String>,
pub repeat: bool,
pub location: KeyLocation,
pub locks: KeyLocks,
}
impl KeyPress {
pub fn new(code: KeyCode, mods: KeyMods) -> Self {
let physical = match code {
KeyCode::Char(c) if c.is_ascii_uppercase() => KeyCode::Char(c.to_ascii_lowercase()),
other => other,
};
Self {
code,
physical,
mods,
text: None,
repeat: false,
location: KeyLocation::Standard,
locks: KeyLocks::default(),
}
}
pub fn with_location(mut self, location: KeyLocation) -> Self {
self.location = location;
self
}
pub fn with_locks(mut self, locks: KeyLocks) -> Self {
self.locks = locks;
self
}
pub fn with_physical(mut self, physical: KeyCode) -> Self {
self.physical = physical;
self
}
pub fn from_layout(layout: KeyCode, physical: KeyCode, mods: KeyMods) -> Self {
Self::from_layout_in(layout, physical, mods, LayoutScript::Latin)
}
pub fn from_layout_in(
layout: KeyCode,
physical: KeyCode,
mods: KeyMods,
script: LayoutScript,
) -> Self {
let stand_in = || match (mods.shift && !mods.alt, physical) {
(true, KeyCode::Char(c)) => KeyCode::Char(us_shifted(c)),
_ => physical,
};
let code = match layout {
KeyCode::Char(c) if !c.is_ascii() => stand_in(),
KeyCode::Unknown => stand_in(),
KeyCode::Char(c)
if script == LayoutScript::NonLatin
&& matches!(physical, KeyCode::Char(p) if p != c) =>
{
stand_in()
}
named_or_ascii => named_or_ascii,
};
Self {
code,
physical,
mods,
text: None,
repeat: false,
location: KeyLocation::Standard,
locks: KeyLocks::default(),
}
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self
}
pub fn same_key(&self, other: &KeyPress) -> bool {
self.location == other.location
&& if self.physical != KeyCode::Unknown && other.physical != KeyCode::Unknown {
self.physical == other.physical
} else {
self.code == other.code
}
}
pub fn edit_event(&self) -> Option<InputEvent> {
let key = match self.code {
KeyCode::Left => EditKey::Left,
KeyCode::Right => EditKey::Right,
KeyCode::Up => EditKey::Up,
KeyCode::Down => EditKey::Down,
KeyCode::Home => EditKey::Home,
KeyCode::End => EditKey::End,
KeyCode::PageUp => EditKey::PageUp,
KeyCode::PageDown => EditKey::PageDown,
KeyCode::Backspace => EditKey::Backspace,
KeyCode::Delete => EditKey::Delete,
KeyCode::Enter => EditKey::Enter,
KeyCode::Tab => EditKey::Tab,
KeyCode::Escape => EditKey::Escape,
KeyCode::Space => {
let m = self.mods;
let text = self
.text
.as_deref()
.filter(|t| t.chars().any(|c| !c.is_control()))
.unwrap_or(" ");
return (!m.ctrl && !m.alt && !m.super_key)
.then(|| InputEvent::Text(text.to_string()));
}
_ => {
let text = self.text.as_deref()?;
return text
.chars()
.any(|c| !c.is_control())
.then(|| InputEvent::Text(text.to_string()));
}
};
Some(InputEvent::Key(
key,
Mods {
shift: self.mods.shift,
word: self.mods.alt,
doc: self.mods.primary(),
},
))
}
pub fn released(mut self) -> Self {
self.text = None;
self.repeat = false;
self
}
pub fn to_value(&self, phase: KeyPhase) -> Value {
Value::map([
("kind", Value::str("key")),
("phase", Value::str(phase.name())),
("code", Value::Str(self.code.name())),
("physical", Value::Str(self.physical.name())),
("shift", Value::Bool(self.mods.shift)),
("ctrl", Value::Bool(self.mods.ctrl)),
("alt", Value::Bool(self.mods.alt)),
("super", Value::Bool(self.mods.super_key)),
(
"text",
match &self.text {
Some(t) => Value::Str(t.clone()),
None => Value::Null,
},
),
("repeat", Value::Bool(self.repeat)),
("location", Value::str(self.location.name())),
("caps_lock", Value::Bool(self.locks.caps)),
("num_lock", Value::Bool(self.locks.num)),
])
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct UiEvent {
pub origin: OriginId,
pub window: WindowId,
pub key: Key,
pub payload: Value,
pub slot: Option<Key>,
}
impl UiEvent {
pub fn on(origin: OriginId, key: Key, payload: Value) -> Self {
Self {
origin,
window: WindowId::MAIN,
key,
payload,
slot: None,
}
}
#[inline]
pub fn kind(&self) -> Option<&str> {
self.payload.get_str("kind")
}
pub fn tagged(mut self, tag: Option<&Value>) -> Self {
if let Some(tag) = tag
&& *tag != Value::Null
&& let Value::Map(entries) = &mut self.payload
{
entries.push(("tag".to_string(), tag.clone()));
}
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MenuOwner {
pub key: Key,
pub origin: OriginId,
pub tag: Value,
}
pub type DropOwner = MenuOwner;
pub type ButtonOwner = MenuOwner;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum HitShape {
Rect,
Rounded([f32; 4]),
Segments { first: u32, len: u32, width: f32 },
Polygon { first: u32, len: u32 },
Path {
first: u32,
len: u32,
rule: crate::path::FillRule,
stroke: f32,
},
}
pub const MIN_STROKE_GRAB: f32 = 4.0;
#[derive(Clone, Debug)]
pub struct HitRegion {
pub key: Key,
pub origin: OriginId,
pub rect: Rect,
pub clip: Rect,
pub shape: HitShape,
pub payload: Option<Value>,
pub drag: Option<Value>,
pub parent_rect: Rect,
pub edit_origin: Option<Vec2>,
pub select_scope: Option<Key>,
pub key_sink: Option<Value>,
pub key_up: bool,
pub context_menu: Option<MenuOwner>,
pub drop: Option<DropOwner>,
pub focusable: bool,
pub window: Option<WindowRole>,
pub hover: Option<Value>,
pub group: Option<u64>,
pub click_sound: Option<crate::resources::SoundId>,
pub hover_sound: Option<crate::resources::SoundId>,
pub cursor: Option<CursorShape>,
pub slider: Option<Box<crate::slider::SliderTrack>>,
}
#[derive(Clone, Debug)]
struct DropHover {
owner: DropOwner,
last: Vec2,
leave: UiEvent,
}
#[derive(Clone, Debug, Default)]
pub struct HitShapes {
pub points: Vec<Vec2>,
}
impl HitShapes {
pub fn segments(&mut self, points: &[Vec2], width: f32) -> HitShape {
let first = self.points.len() as u32;
self.points.extend_from_slice(points);
HitShape::Segments {
first,
len: points.len() as u32,
width,
}
}
pub fn path(&mut self, points: &[Vec2], rule: crate::path::FillRule, stroke: f32) -> HitShape {
let first = self.points.len() as u32;
self.points.extend_from_slice(points);
HitShape::Path {
first,
len: points.len() as u32,
rule,
stroke,
}
}
pub fn polygon(&mut self, points: &[Vec2]) -> HitShape {
let first = self.points.len() as u32;
self.points.extend_from_slice(points);
HitShape::Polygon {
first,
len: points.len() as u32,
}
}
}
pub fn in_rounded_rect(p: Vec2, w: f32, h: f32, radii: [f32; 4]) -> bool {
let cap = (w * 0.5).min(h * 0.5).max(0.0);
let corners = [
(radii[0].min(cap), radii[0].min(cap), radii[0].min(cap)),
(w - radii[1].min(cap), radii[1].min(cap), radii[1].min(cap)),
(
w - radii[2].min(cap),
h - radii[2].min(cap),
radii[2].min(cap),
),
(radii[3].min(cap), h - radii[3].min(cap), radii[3].min(cap)),
];
for (i, &(cx, cy, r)) in corners.iter().enumerate() {
if r <= 0.0 {
continue;
}
let in_square = match i {
0 => p.x < cx && p.y < cy,
1 => p.x > cx && p.y < cy,
2 => p.x > cx && p.y > cy,
_ => p.x < cx && p.y > cy,
};
if in_square && (p.x - cx).powi(2) + (p.y - cy).powi(2) > r * r {
return false;
}
}
true
}
pub fn segment_distance(p: Vec2, a: Vec2, b: Vec2) -> f32 {
let (ex, ey) = (b.x - a.x, b.y - a.y);
let (wx, wy) = (p.x - a.x, p.y - a.y);
let ee = ex * ex + ey * ey;
let t = if ee > 0.0 {
((wx * ex + wy * ey) / ee).clamp(0.0, 1.0)
} else {
0.0
};
let (dx, dy) = (wx - ex * t, wy - ey * t);
(dx * dx + dy * dy).sqrt()
}
pub fn in_polygon(p: Vec2, pts: &[Vec2]) -> bool {
let n = pts.len();
if n < 3 {
return false;
}
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let (a, b) = (pts[i], pts[j]);
if (a.y > p.y) != (b.y > p.y) {
let x = a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x);
if p.x < x {
inside = !inside;
}
}
j = i;
}
inside
}
#[derive(Clone, Copy, Debug)]
pub struct ScrollRegion {
pub key: Key,
pub(crate) node: u32,
pub rect: Rect,
pub clip: Rect,
pub inert: bool,
pub handler: bool,
pub(crate) takes_x: bool,
pub(crate) takes_y: bool,
pub(crate) scrolls_x: bool,
pub(crate) scrolls_y: bool,
pub(crate) contain: bool,
pub(crate) mods: u32,
pub(crate) parent: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ScrollAxis {
X,
Y,
}
#[derive(Clone, Copy, Debug)]
pub struct ScrollbarRegion {
pub key: Key,
pub axis: ScrollAxis,
pub thumb: Rect,
pub track: Rect,
pub bar_len: f32,
pub max: f32,
pub inert: bool,
pub(crate) above: u32,
}
pub(crate) enum Target<'a> {
Bar(ScrollbarRegion),
Hit(&'a HitRegion),
}
impl ScrollbarRegion {
pub(crate) fn offset_for(&self, p: Vec2, grab: f32) -> f32 {
let (pos, track_start, track_len) = match self.axis {
ScrollAxis::X => (p.x, self.track.x, self.track.w),
ScrollAxis::Y => (p.y, self.track.y, self.track.h),
};
let range = (track_len - self.bar_len).max(1.0);
((pos - track_start - grab) / range).clamp(0.0, 1.0) * self.max
}
}
#[derive(Clone, Debug)]
struct DragState {
key: Key,
origin: OriginId,
tag: Value,
parent_rect: Rect,
press: Vec2,
last: Vec2,
moved: bool,
}
const DRAG_SLOP: f32 = 3.0;
impl DragState {
fn displacement(&self, p: Vec2) -> Vec2 {
Vec2::new(p.x - self.press.x, p.y - self.press.y)
}
}
#[derive(Clone, Debug)]
struct ButtonCapture {
button: MouseButton,
owner: ButtonOwner,
last: Vec2,
}
#[derive(Default)]
pub struct Interaction {
pub(crate) hits: Vec<HitRegion>,
shape_points: Vec<Vec2>,
pub(crate) scroll_regions: Vec<ScrollRegion>,
pub(crate) scrollbars: Vec<ScrollbarRegion>,
pub(crate) scrollbar_drag: Option<(Key, ScrollAxis, f32)>,
pub(crate) window_commands: Vec<WindowCommand>,
pub(crate) window: WindowId,
pub(crate) sound_requests: Vec<crate::resources::SoundId>,
drag: Option<DragState>,
held_buttons: Vec<ButtonCapture>,
slide: Option<(Key, OriginId, Box<crate::slider::SliderTrack>, f64)>,
press_clicks: u8,
modifiers: KeyMods,
cursor: Option<Vec2>,
hovered: Option<Key>,
pressed: Option<Key>,
hovered_group: Option<u64>,
pressed_group: Option<u64>,
hovered_leave: Option<UiEvent>,
drop: Option<DropHover>,
pending: Vec<UiEvent>,
}
impl Interaction {
pub fn set_hits(&mut self, hits: Vec<HitRegion>) {
self.set_hits_shaped(hits, HitShapes::default());
}
pub fn set_hits_shaped(&mut self, hits: Vec<HitRegion>, shapes: HitShapes) {
self.hits = hits;
self.shape_points = shapes.points;
let mut out = std::mem::take(&mut self.pending);
self.refresh_hover(&mut out, "content");
self.pending = out;
}
pub fn take_pending(&mut self) -> Vec<UiEvent> {
std::mem::take(&mut self.pending)
}
pub(crate) fn take_sound_requests(&mut self) -> Vec<crate::resources::SoundId> {
std::mem::take(&mut self.sound_requests)
}
pub fn take_hit_buffer(&mut self) -> Vec<HitRegion> {
let mut hits = std::mem::take(&mut self.hits);
hits.clear();
hits
}
pub fn take_shape_buffer(&mut self) -> HitShapes {
let mut shapes = HitShapes {
points: std::mem::take(&mut self.shape_points),
};
shapes.points.clear();
shapes
}
#[inline]
fn contains(&self, h: &HitRegion, p: Vec2) -> bool {
if !(h.rect.contains(p) && h.clip.contains(p)) {
return false;
}
if h.shape == HitShape::Rect {
return true;
}
let local = Vec2::new(p.x - h.rect.x, p.y - h.rect.y);
match h.shape {
HitShape::Rect => true,
HitShape::Rounded(radii) => in_rounded_rect(local, h.rect.w, h.rect.h, radii),
HitShape::Segments { first, len, width } => {
let Some(pts) = self
.shape_points
.get(first as usize..(first + len) as usize)
else {
return false;
};
let half = (width * 0.5).max(MIN_STROKE_GRAB * 0.5);
pts.windows(2)
.any(|w| segment_distance(local, w[0], w[1]) <= half)
}
HitShape::Polygon { first, len } => {
let Some(pts) = self
.shape_points
.get(first as usize..(first + len) as usize)
else {
return false;
};
in_polygon(local, pts)
}
HitShape::Path {
first,
len,
rule,
stroke,
} => {
let Some(pts) = self
.shape_points
.get(first as usize..(first + len) as usize)
else {
return false;
};
crate::path::in_path(local, pts, rule)
|| (stroke > 0.0
&& pts
.windows(2)
.any(|w| segment_distance(local, w[0], w[1]) <= stroke * 0.5))
}
}
}
pub fn cursor(&self) -> Option<Vec2> {
self.cursor
}
pub fn modifiers(&self) -> KeyMods {
self.modifiers
}
pub fn hits(&self) -> &[HitRegion] {
&self.hits
}
pub(crate) fn hit_at(&self, p: Vec2) -> Option<&HitRegion> {
self.hits.iter().rev().find(|h| self.contains(h, p))
}
pub(crate) fn scroll_regions_at(&self) -> impl Iterator<Item = &ScrollRegion> {
let p = self.cursor;
self.scroll_regions.iter().rev().filter(move |r| {
p.is_some_and(|p| !r.inert && r.rect.contains(p) && r.clip.contains(p))
})
}
pub(crate) fn edit_origin_of(&self, key: Key) -> Option<Vec2> {
self.hits
.iter()
.rev()
.find(|h| h.key == key && h.edit_origin.is_some())
.and_then(|h| h.edit_origin)
}
fn refresh_hover(&mut self, out: &mut Vec<UiEvent>, by: &'static str) {
let before = self.hovered;
let idx = self.cursor.and_then(|p| match self.target_at(p)? {
Target::Bar(_) => None,
Target::Hit(h) => self.hits.iter().rposition(|x| std::ptr::eq(x, h)),
});
let (hovered, group) = match idx {
Some(i) => (Some(self.hits[i].key), self.hits[i].group),
None => (None, None),
};
self.hovered = hovered;
self.hovered_group = group;
if before == hovered {
return;
}
out.extend(self.hovered_leave.take().map(|ev| Self::moved_by(ev, by)));
if let Some(i) = idx {
out.extend(Self::hover_event(&self.hits[i], "enter").map(|ev| Self::moved_by(ev, by)));
self.hovered_leave = Self::hover_event(&self.hits[i], "leave");
if let Some(sound) = self.hits[i].hover_sound {
self.sound_requests.push(sound);
}
}
}
fn moved_by(mut ev: UiEvent, by: &'static str) -> UiEvent {
if let Value::Map(entries) = &mut ev.payload {
let at = entries
.iter()
.position(|(k, _)| k == "tag")
.unwrap_or(entries.len());
entries.insert(at, ("by".to_string(), Value::str(by)));
}
ev
}
fn hover_event(region: &HitRegion, phase: &str) -> Option<UiEvent> {
let tag = region.hover.as_ref()?;
let payload = Value::map([("kind", Value::str("hover")), ("phase", Value::str(phase))]);
Some(UiEvent::on(region.origin, region.key, payload).tagged(Some(tag)))
}
fn context_menu_event(region: &HitRegion, p: Vec2) -> Option<UiEvent> {
let owner = region.context_menu.as_ref()?;
let payload = Value::map([
("kind", Value::str("contextmenu")),
("x", Value::Float(p.x as f64)),
("y", Value::Float(p.y as f64)),
]);
Some(UiEvent::on(owner.origin, owner.key, payload).tagged(Some(&owner.tag)))
}
pub(crate) fn target_at(&self, p: Vec2) -> Option<Target<'_>> {
let hit = self.hits.iter().rposition(|h| self.contains(h, p));
let bar = self
.scrollbars
.iter()
.rev()
.find(|b| !b.inert && b.track.contains(p));
match (bar, hit) {
(Some(b), Some(h)) if (h as u32) < b.above => Some(Target::Bar(*b)),
(Some(b), None) => Some(Target::Bar(*b)),
(_, Some(h)) => Some(Target::Hit(&self.hits[h])),
(None, None) => None,
}
}
pub fn is_scrollbar_dragging(&self, key: Key, axis: ScrollAxis) -> bool {
matches!(self.scrollbar_drag, Some((k, a, _)) if k == key && a == axis)
}
fn drag_event(state: &DragState, phase: &str, p: Vec2, d: Vec2) -> UiEvent {
let pr = state.parent_rect;
let payload = Value::map([
("kind", Value::str("drag")),
("phase", Value::str(phase)),
("x", Value::Float(p.x as f64)),
("y", Value::Float(p.y as f64)),
("dx", Value::Float(d.x as f64)),
("dy", Value::Float(d.y as f64)),
(
"parent",
Value::map([
("x", Value::Float(pr.x as f64)),
("y", Value::Float(pr.y as f64)),
("w", Value::Float(pr.w as f64)),
("h", Value::Float(pr.h as f64)),
]),
),
]);
UiEvent::on(state.origin, state.key, payload).tagged(Some(&state.tag))
}
fn button_event(
owner: &ButtonOwner,
button: MouseButton,
phase: &str,
p: Vec2,
clicks: Option<u8>,
) -> UiEvent {
let mut fields = vec![
("kind", Value::str("button")),
("phase", Value::str(phase)),
("button", button.to_value()),
("x", Value::Float(p.x as f64)),
("y", Value::Float(p.y as f64)),
];
if let Some(clicks) = clicks {
fields.push(("clicks", Value::Int(clicks as i64)));
}
UiEvent::on(owner.origin, owner.key, Value::map(fields)).tagged(Some(&owner.tag))
}
pub(crate) fn press_button(
&mut self,
button: MouseButton,
clicks: u8,
owner: ButtonOwner,
out: &mut Vec<UiEvent>,
) -> usize {
out.append(&mut self.pending);
let Some(p) = self.cursor else {
return 0;
};
let mut n = 0;
if let Some(i) = self.held_buttons.iter().position(|h| h.button == button) {
let held = self.held_buttons.remove(i);
out.push(Self::button_event(&held.owner, button, "release", p, None));
n += 1;
}
out.push(Self::button_event(&owner, button, "press", p, Some(clicks)));
self.held_buttons.push(ButtonCapture {
button,
owner,
last: p,
});
n + 1
}
pub(crate) fn release_buttons(&mut self, out: &mut Vec<UiEvent>) -> usize {
let n = self.held_buttons.len();
for held in std::mem::take(&mut self.held_buttons) {
let p = self.cursor.unwrap_or(held.last);
out.push(Self::button_event(
&held.owner,
held.button,
"release",
p,
None,
));
}
n
}
pub(crate) fn release_primary(&mut self, out: &mut Vec<UiEvent>) -> usize {
let mut n = 0;
if let Some(drag) = self.drag.take() {
let p = self.cursor.unwrap_or(drag.last);
out.push(Self::drag_event(&drag, "end", p, drag.displacement(p)));
n += 1;
}
if let Some((key, origin, track, last)) = self.slide.take() {
let v = self.cursor.map_or(last, |p| track.value_at(p));
out.push(crate::slider::change_event(
origin, key, v, "end", &track.tag,
));
n += 1;
}
self.pressed = None;
self.pressed_group = None;
n
}
pub(crate) fn drop_gone_buttons(&mut self, alive: impl Fn(Key) -> bool) {
if !self.held_buttons.is_empty() {
self.held_buttons.retain(|h| alive(h.owner.key));
}
}
pub fn button_owner(&self, button: MouseButton) -> Option<Key> {
self.held_buttons
.iter()
.find(|h| h.button == button)
.map(|h| h.owner.key)
}
pub fn handle(&mut self, ev: InputEvent, out: &mut Vec<UiEvent>) -> usize {
out.append(&mut self.pending);
let mut pointer_made = 0;
match ev {
InputEvent::CursorMoved(p) => {
self.cursor = Some(p);
self.refresh_hover(out, "pointer");
if let Some((key, origin, track, last)) = &mut self.slide {
let v = track.value_at(p);
if v != *last {
*last = v;
out.push(crate::slider::change_event(
*origin, *key, v, "move", &track.tag,
));
pointer_made += 1;
}
}
if let Some(drag) = &mut self.drag
&& p != drag.last
{
drag.last = p;
let d = drag.displacement(p);
if d.x.abs() + d.y.abs() > DRAG_SLOP {
drag.moved = true;
}
if drag.moved {
out.push(Self::drag_event(drag, "move", p, d));
pointer_made += 1;
}
}
for held in &mut self.held_buttons {
if p != held.last {
held.last = p;
out.push(Self::button_event(&held.owner, held.button, "move", p, None));
pointer_made += 1;
}
}
}
InputEvent::CursorLeft => {
self.cursor = None;
self.refresh_hover(out, "pointer");
}
InputEvent::MouseDown { button, .. } if button != MouseButton::Primary => {
if button == MouseButton::Secondary
&& let Some(p) = self.cursor
&& let Some(ev) = self.hit_at(p).and_then(|h| Self::context_menu_event(h, p))
{
out.push(ev);
}
}
InputEvent::MouseDown { clicks, .. } => {
self.pressed = self.hovered;
self.pressed_group = self.hovered_group;
self.press_clicks = clicks;
if let Some(h) = self.cursor.and_then(|p| self.hit_at(p)) {
if h.window == Some(WindowRole::Drag) {
self.pressed = None;
self.window_commands
.push(WindowCommand::StartDrag(self.window));
} else if let Some(track) = &h.slider {
let p = self.cursor.unwrap();
let v = track.value_at(p);
out.push(crate::slider::change_event(
h.origin, h.key, v, "move", &track.tag,
));
pointer_made += 1;
self.slide = Some((h.key, h.origin, track.clone(), v));
} else if let Some(tag) = &h.drag {
let p = self.cursor.unwrap();
let state = DragState {
key: h.key,
origin: h.origin,
tag: tag.clone(),
parent_rect: h.parent_rect,
press: p,
last: p,
moved: false,
};
out.push(Self::drag_event(&state, "start", p, Vec2::ZERO));
pointer_made += 1;
self.drag = Some(state);
}
}
}
InputEvent::Modifiers(m) => {
if m != self.modifiers {
self.modifiers = m;
out.push(UiEvent {
origin: OriginId::HOST,
window: WindowId::MAIN,
key: Key::ROOT,
payload: m.to_value(),
slot: None,
});
}
}
InputEvent::Scroll(_)
| InputEvent::ScrollGesture { .. }
| InputEvent::Text(_)
| InputEvent::Commit(_)
| InputEvent::Paste { .. }
| InputEvent::Preedit(..)
| InputEvent::Key(..)
| InputEvent::KeyDown(_)
| InputEvent::KeyUp(_)
| InputEvent::Access(_)
| InputEvent::ForceClick(_) => {}
InputEvent::DragFiles { paths, at } => self.drag_files(&paths, at, out),
InputEvent::DropFiles { paths, at } => self.drop_files(&paths, at, out),
InputEvent::DragCancel => self.drag_cancel(out),
InputEvent::Files(_) | InputEvent::Open(_) => {}
InputEvent::MouseUp { button } if button != MouseButton::Primary => {
if let Some(i) = self.held_buttons.iter().position(|h| h.button == button) {
let held = self.held_buttons.remove(i);
let p = self.cursor.unwrap_or(held.last);
out.push(Self::button_event(&held.owner, button, "release", p, None));
pointer_made += 1;
}
}
InputEvent::MouseUp { .. } => {
let dragged = self.drag.take().inspect(|drag| {
let p = self.cursor.unwrap_or(drag.last);
out.push(Self::drag_event(drag, "end", p, drag.displacement(p)));
pointer_made += 1;
});
let slid = self.slide.take().inspect(|(key, origin, track, last)| {
let v = self.cursor.map_or(*last, |p| track.value_at(p));
out.push(crate::slider::change_event(
*origin, *key, v, "end", &track.tag,
));
pointer_made += 1;
});
let click_ok = !dragged.is_some_and(|d| d.moved) && slid.is_none();
if click_ok
&& let (Some(pressed), Some(hovered)) = (self.pressed, self.hovered)
&& pressed == hovered
&& let Some(region) = self.hits.iter().rev().find(|h| h.key == pressed)
{
if let Some(sound) = region.click_sound {
self.sound_requests.push(sound);
}
match (region.window, ®ion.payload) {
(Some(WindowRole::Button(b)), _) => {
self.window_commands.push(b.command(self.window))
}
(Some(WindowRole::Drag), _) | (None, None) => {}
(None, Some(payload)) => {
out.push(UiEvent {
origin: region.origin,
window: WindowId::MAIN,
key: region.key,
payload: payload.clone(),
slot: None,
});
pointer_made += 1;
}
}
}
self.pressed = None;
self.pressed_group = None;
}
}
pointer_made
}
pub fn is_hovered(&self, key: Key) -> bool {
self.hovered == Some(key)
}
pub fn is_drop_target(&self, key: Key) -> bool {
self.drop.as_ref().is_some_and(|d| d.owner.key == key)
}
pub fn drop_target(&self) -> Option<Key> {
self.drop.as_ref().map(|d| d.owner.key)
}
fn zone_at(&self, p: Vec2) -> Option<&DropOwner> {
self.hits
.iter()
.rev()
.find(|h| h.drop.is_some() && self.contains(h, p))
.and_then(|h| h.drop.as_ref())
}
fn drop_event(owner: &DropOwner, phase: &str, paths: &[String], at: Option<Vec2>) -> UiEvent {
let mut fields = vec![
("kind", Value::str("drop")),
("phase", Value::str(phase)),
(
"paths",
Value::list(
paths
.iter()
.map(|p| Value::str(p.as_str()))
.collect::<Vec<_>>(),
),
),
];
if let Some(p) = at {
fields.push(("x", Value::Float(p.x as f64)));
fields.push(("y", Value::Float(p.y as f64)));
}
UiEvent::on(owner.origin, owner.key, Value::map(fields)).tagged(Some(&owner.tag))
}
fn drag_files(&mut self, paths: &[String], at: Vec2, out: &mut Vec<UiEvent>) {
let zone = self.zone_at(at).cloned();
if let (Some(cur), Some(z)) = (&mut self.drop, &zone)
&& cur.owner.key == z.key
&& cur.owner.origin == z.origin
{
if cur.last != at {
cur.last = at;
out.push(Self::drop_event(z, "move", paths, Some(at)));
}
return;
}
out.extend(self.drop.take().map(|d| d.leave));
if let Some(owner) = zone {
out.push(Self::drop_event(&owner, "enter", paths, Some(at)));
let leave = Self::drop_event(&owner, "leave", paths, None);
self.drop = Some(DropHover {
owner,
last: at,
leave,
});
}
}
fn drop_files(&mut self, paths: &[String], at: Vec2, out: &mut Vec<UiEvent>) {
let zone = self.zone_at(at).cloned();
if let Some(cur) = self.drop.take()
&& !zone
.as_ref()
.is_some_and(|z| z.key == cur.owner.key && z.origin == cur.owner.origin)
{
out.push(cur.leave);
}
if let Some(owner) = zone {
out.push(Self::drop_event(&owner, "drop", paths, Some(at)));
}
}
fn drag_cancel(&mut self, out: &mut Vec<UiEvent>) {
out.extend(self.drop.take().map(|d| d.leave));
}
pub fn is_pressed(&self, key: Key) -> bool {
self.pressed == Some(key) && (self.hovered == Some(key) || self.drag_captured(key))
}
fn drag_captured(&self, key: Key) -> bool {
self.drag.as_ref().is_some_and(|d| d.key == key)
}
pub fn hovered(&self) -> Option<Key> {
self.hovered
}
pub(crate) fn press_clicks(&self) -> u8 {
self.press_clicks
}
pub(crate) fn note_synthetic_click(&mut self) {
self.press_clicks = 0;
}
pub fn pressed_key(&self) -> Option<Key> {
self.pressed
}
pub fn cursor_shape(&self) -> CursorShape {
if let Some(drag) = &self.drag {
return self
.hits
.iter()
.rev()
.find(|h| h.key == drag.key)
.and_then(|h| h.cursor)
.unwrap_or(CursorShape::Default);
}
if self.scrollbar_drag.is_some() {
return CursorShape::Default;
}
let Some(p) = self.cursor else {
return CursorShape::Default;
};
match self.target_at(p) {
Some(Target::Hit(region)) => {
region.cursor.unwrap_or_else(|| Self::implied_shape(region))
}
Some(Target::Bar(_)) | None => CursorShape::Default,
}
}
fn implied_shape(region: &HitRegion) -> CursorShape {
match region {
_ if region.window.is_some() => CursorShape::Default,
_ if region.edit_origin.is_some() => CursorShape::Text,
_ if region.select_scope.is_some() => CursorShape::Text,
_ => CursorShape::Default,
}
}
pub fn is_group_hovered(&self, group: u64) -> bool {
self.hovered_group == Some(group)
}
pub fn is_group_pressed(&self, group: u64) -> bool {
self.pressed_group == Some(group)
&& (self.hovered_group == Some(group) || self.drag.is_some())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn region(key: Key, origin: u16, x: f32, y: f32, w: f32, h: f32, tag: &str) -> HitRegion {
HitRegion {
key,
origin: OriginId(origin),
rect: Rect::new(x, y, w, h),
clip: Rect::new(-1e9, -1e9, 2e9, 2e9),
shape: HitShape::Rect,
payload: Some(Value::str(tag)),
drag: None,
parent_rect: Rect::new(0.0, 0.0, 0.0, 0.0),
edit_origin: None,
select_scope: None,
key_sink: None,
key_up: false,
context_menu: None,
drop: None,
focusable: true,
window: None,
hover: None,
group: None,
click_sound: None,
hover_sound: None,
cursor: None,
slider: None,
}
}
fn drive(interaction: &mut Interaction, events: &[InputEvent]) -> Vec<UiEvent> {
let mut out = Vec::new();
for ev in events {
interaction.handle(ev.clone(), &mut out);
}
out
}
#[test]
fn click_inside_produces_event() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 10.0, 10.0, 100.0, 30.0, "go")]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(50.0, 20.0)),
InputEvent::mouse_down(1),
InputEvent::mouse_up(),
],
);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, k);
assert_eq!(evs[0].payload.as_str(), Some("go"));
}
#[test]
fn press_then_drag_away_does_not_click() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "go")]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(10.0, 10.0)),
InputEvent::mouse_down(1),
InputEvent::CursorMoved(Vec2::new(500.0, 500.0)),
InputEvent::mouse_up(),
],
);
assert!(evs.is_empty());
}
#[test]
fn topmost_region_wins_on_overlap() {
let mut it = Interaction::default();
let bottom = Key::ROOT.str("bottom");
let top = Key::ROOT.str("top");
it.set_hits(vec![
region(bottom, 0, 0.0, 0.0, 100.0, 100.0, "bottom"),
region(top, 0, 25.0, 25.0, 50.0, 50.0, "top"),
]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(50.0, 50.0)),
InputEvent::mouse_down(1),
InputEvent::mouse_up(),
],
);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, top);
}
#[test]
fn event_carries_declaring_origin() {
let mut it = Interaction::default();
let k = Key::ROOT.str("ext-btn");
it.set_hits(vec![region(k, 3, 0.0, 0.0, 10.0, 10.0, "x")]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(5.0, 5.0)),
InputEvent::mouse_down(1),
InputEvent::mouse_up(),
],
);
assert_eq!(evs[0].origin, OriginId(3));
}
#[test]
fn cursor_leave_clears_hover() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
drive(&mut it, &[InputEvent::CursorMoved(Vec2::new(10.0, 10.0))]);
assert!(it.is_hovered(k));
drive(&mut it, &[InputEvent::CursorLeft]);
assert!(!it.is_hovered(k));
let evs = drive(
&mut it,
&[InputEvent::mouse_down(1), InputEvent::mouse_up()],
);
assert!(evs.is_empty());
}
#[test]
fn secondary_press_asks_the_node_under_it_for_a_menu() {
let mut it = Interaction::default();
let k = Key::ROOT.str("panel");
let mut r = region(k, 0, 0.0, 0.0, 100.0, 100.0, "click-me");
r.context_menu = Some(MenuOwner {
key: k,
origin: OriginId::HOST,
tag: Value::str("panel-menu"),
});
it.set_hits(vec![r]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
InputEvent::MouseDown {
button: MouseButton::Secondary,
clicks: 1,
},
InputEvent::MouseUp {
button: MouseButton::Secondary,
},
],
);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].key, k);
assert_eq!(
evs[0].payload.get("kind").unwrap().as_str(),
Some("contextmenu")
);
assert_eq!(evs[0].payload.get("x").unwrap().as_float(), Some(40.0));
assert_eq!(evs[0].payload.get("y").unwrap().as_float(), Some(30.0));
assert_eq!(
evs[0].payload.get("tag").unwrap().as_str(),
Some("panel-menu")
);
assert!(!it.is_pressed(k));
}
#[test]
fn dragged_files_find_the_topmost_zone_and_look_past_what_is_none() {
let mut it = Interaction::default();
let zone = Key::ROOT.str("zone");
let button = Key::ROOT.str("button");
let overlay = Key::ROOT.str("overlay");
let other = Key::ROOT.str("other");
let owner = |k: Key, tag: &str| {
Some(DropOwner {
key: k,
origin: OriginId::HOST,
tag: Value::str(tag),
})
};
let mut z = region(zone, 0, 0.0, 0.0, 100.0, 100.0, "z");
z.drop = owner(zone, "files");
let mut b = region(button, 0, 10.0, 10.0, 30.0, 30.0, "press");
b.drop = owner(zone, "files");
let o = region(overlay, 0, 0.0, 0.0, 100.0, 100.0, "overlay");
let mut second = region(other, 0, 100.0, 0.0, 100.0, 100.0, "o");
second.drop = owner(other, "other-files");
it.set_hits(vec![z, b, second, o]);
let paths = vec!["/drop/1.txt".to_string()];
let phases = |evs: &[UiEvent]| {
evs.iter()
.map(|e| {
(
e.key,
e.payload
.get("phase")
.unwrap()
.as_str()
.unwrap()
.to_string(),
)
})
.collect::<Vec<_>>()
};
let evs = drive(
&mut it,
&[InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(20.0, 20.0),
}],
);
assert_eq!(phases(&evs), vec![(zone, "enter".to_string())]);
assert_eq!(evs[0].payload.get("tag").unwrap().as_str(), Some("files"));
assert_eq!(evs[0].payload.get("x").unwrap().as_float(), Some(20.0));
assert_eq!(it.drop_target(), Some(zone));
assert!(it.is_drop_target(zone));
let evs = drive(
&mut it,
&[
InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(20.0, 20.0),
},
InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(60.0, 60.0),
},
],
);
assert_eq!(phases(&evs), vec![(zone, "move".to_string())]);
let evs = drive(
&mut it,
&[InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(150.0, 50.0),
}],
);
assert_eq!(
phases(&evs),
vec![(zone, "leave".to_string()), (other, "enter".to_string())]
);
assert!(evs[0].payload.get("x").is_none());
let evs = drive(
&mut it,
&[InputEvent::DropFiles {
paths: paths.clone(),
at: Vec2::new(150.0, 50.0),
}],
);
assert_eq!(phases(&evs), vec![(other, "drop".to_string())]);
assert_eq!(it.drop_target(), None);
let evs = drive(
&mut it,
&[
InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(50.0, 50.0),
},
InputEvent::DragCancel,
],
);
assert_eq!(
phases(&evs),
vec![(zone, "enter".to_string()), (zone, "leave".to_string())]
);
let evs = drive(
&mut it,
&[
InputEvent::DragFiles {
paths: paths.clone(),
at: Vec2::new(50.0, 50.0),
},
InputEvent::DropFiles {
paths: paths.clone(),
at: Vec2::new(250.0, 50.0),
},
],
);
assert_eq!(
phases(&evs),
vec![(zone, "enter".to_string()), (zone, "leave".to_string())]
);
assert_eq!(it.drop_target(), None);
}
#[test]
fn secondary_press_on_a_node_without_a_menu_emits_nothing() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 0.0, 0.0, 100.0, 100.0, "go")]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
InputEvent::MouseDown {
button: MouseButton::Secondary,
clicks: 1,
},
InputEvent::MouseUp {
button: MouseButton::Secondary,
},
],
);
assert!(evs.is_empty());
}
#[test]
fn secondary_press_does_not_interrupt_a_held_primary() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 0.0, 0.0, 100.0, 100.0, "go")]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
InputEvent::mouse_down(1),
InputEvent::MouseDown {
button: MouseButton::Secondary,
clicks: 1,
},
InputEvent::MouseUp {
button: MouseButton::Secondary,
},
],
);
assert!(evs.is_empty());
assert!(it.is_pressed(k));
let evs = drive(&mut it, &[InputEvent::mouse_up()]);
assert_eq!(evs.len(), 1);
assert_eq!(evs[0].payload.as_str(), Some("go"));
}
#[test]
fn middle_press_routes_nowhere() {
let mut it = Interaction::default();
let k = Key::ROOT.str("panel");
let mut r = region(k, 0, 0.0, 0.0, 100.0, 100.0, "go");
r.context_menu = Some(MenuOwner {
key: k,
origin: OriginId::HOST,
tag: Value::str("panel-menu"),
});
it.set_hits(vec![r]);
let evs = drive(
&mut it,
&[
InputEvent::CursorMoved(Vec2::new(40.0, 30.0)),
InputEvent::MouseDown {
button: MouseButton::Middle,
clicks: 1,
},
InputEvent::MouseUp {
button: MouseButton::Middle,
},
],
);
assert!(evs.is_empty());
}
#[test]
fn button_codes_round_trip() {
for b in [
MouseButton::Primary,
MouseButton::Secondary,
MouseButton::Middle,
MouseButton::Other(0),
MouseButton::Other(9),
] {
assert_eq!(MouseButton::from_code(b.code()), b);
}
}
#[test]
fn new_frame_hits_preserve_hover_state() {
let mut it = Interaction::default();
let k = Key::ROOT.str("btn");
it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
drive(&mut it, &[InputEvent::CursorMoved(Vec2::new(10.0, 10.0))]);
it.set_hits(vec![region(k, 0, 100.0, 100.0, 50.0, 50.0, "x")]);
assert!(!it.is_hovered(k));
it.set_hits(vec![region(k, 0, 0.0, 0.0, 50.0, 50.0, "x")]);
assert!(it.is_hovered(k));
}
}