#[cfg(not(feature = "std"))]
use alloc::string::{String, ToString};
use alloc::{
boxed::Box,
collections::{btree_map::BTreeMap, btree_set::BTreeSet},
vec::Vec,
};
use azul_css::AzString;
use crate::{
callbacks::Update,
dom::{DomId, DomNodeId, On},
geom::{LogicalPosition, LogicalRect},
hit_test::{FullHitTest, HitTestItem},
id::NodeId,
styled_dom::{ChangedCssProperty, NodeHierarchyItemId},
task::Instant,
OrderedMap,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EasingFunction {
Linear,
EaseInOut,
EaseOut,
}
pub type RestyleNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
pub type RelayoutNodes = BTreeMap<NodeId, Vec<ChangedCssProperty>>;
pub type RelayoutWords = BTreeMap<NodeId, AzString>;
#[derive(Debug, Clone, PartialEq)]
pub struct FocusChange {
pub old: Option<DomNodeId>,
pub new: Option<DomNodeId>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CallbackToCall {
pub node_id: NodeId,
pub hit_test_item: Option<HitTestItem>,
pub event_filter: EventFilter,
}
impl CallbackToCall {
pub fn new(
node_id: NodeId,
hit_test_item: Option<HitTestItem>,
event_filter: EventFilter,
) -> Self {
Self { node_id, hit_test_item, event_filter }
}
pub fn from_hit_test(
hit_test: &FullHitTest,
dom_id: DomId,
event_filter: EventFilter,
) -> Vec<CallbackToCall> {
let Some(hit) = hit_test.hovered_nodes.get(&dom_id) else {
return Vec::new();
};
hit.regular_hit_test_nodes
.iter()
.map(|(node_id, item)| CallbackToCall {
node_id: *node_id,
hit_test_item: Some(item.clone()),
event_filter: event_filter.clone(),
})
.collect()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[must_use = "ProcessEventResult must be used to determine if relayout/repaint is needed"]
pub enum ProcessEventResult {
DoNothing = 0,
ShouldReRenderCurrentWindow = 1,
ShouldUpdateDisplayListCurrentWindow = 2,
UpdateHitTesterAndProcessAgain = 3,
ShouldIncrementalRelayout = 4,
ShouldRegenerateDomCurrentWindow = 5,
ShouldRegenerateDomAllWindows = 6,
}
impl ProcessEventResult {
pub fn order(&self) -> usize {
use self::ProcessEventResult::*;
match self {
DoNothing => 0,
ShouldReRenderCurrentWindow => 1,
ShouldUpdateDisplayListCurrentWindow => 2,
UpdateHitTesterAndProcessAgain => 3,
ShouldIncrementalRelayout => 4,
ShouldRegenerateDomCurrentWindow => 5,
ShouldRegenerateDomAllWindows => 6,
}
}
}
impl PartialOrd for ProcessEventResult {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
self.order().partial_cmp(&other.order())
}
}
impl Ord for ProcessEventResult {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
self.order().cmp(&other.order())
}
}
impl ProcessEventResult {
pub fn max_self(self, other: Self) -> Self {
self.max(other)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum EventSource {
User,
Programmatic,
Synthetic,
Lifecycle,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum EventPhase {
Capture,
Target,
Bubble,
}
impl Default for EventPhase {
fn default() -> Self {
EventPhase::Bubble
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum MouseButton {
Left,
Middle,
Right,
Other(u8),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum ScrollDeltaMode {
Pixel,
Line,
Page,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum ScrollDirection {
Up,
Down,
Left,
Right,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ScrollIntoViewOptions {
pub block: ScrollLogicalPosition,
pub inline_axis: ScrollLogicalPosition,
pub behavior: ScrollIntoViewBehavior,
}
impl ScrollIntoViewOptions {
pub fn nearest() -> Self {
Self {
block: ScrollLogicalPosition::Nearest,
inline_axis: ScrollLogicalPosition::Nearest,
behavior: ScrollIntoViewBehavior::Auto,
}
}
pub fn center() -> Self {
Self {
block: ScrollLogicalPosition::Center,
inline_axis: ScrollLogicalPosition::Center,
behavior: ScrollIntoViewBehavior::Auto,
}
}
pub fn start() -> Self {
Self {
block: ScrollLogicalPosition::Start,
inline_axis: ScrollLogicalPosition::Start,
behavior: ScrollIntoViewBehavior::Auto,
}
}
pub fn end() -> Self {
Self {
block: ScrollLogicalPosition::End,
inline_axis: ScrollLogicalPosition::End,
behavior: ScrollIntoViewBehavior::Auto,
}
}
pub fn with_instant(mut self) -> Self {
self.behavior = ScrollIntoViewBehavior::Instant;
self
}
pub fn with_smooth(mut self) -> Self {
self.behavior = ScrollIntoViewBehavior::Smooth;
self
}
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum ScrollLogicalPosition {
Start,
Center,
End,
#[default]
Nearest,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
pub enum ScrollIntoViewBehavior {
#[default]
Auto,
Instant,
Smooth,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum LifecycleReason {
InitialMount,
Remount,
Resize,
Update,
Unmount,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
#[repr(C)]
pub struct KeyModifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
impl KeyModifiers {
pub fn new() -> Self {
Self::default()
}
pub fn with_shift(mut self) -> Self {
self.shift = true;
self
}
pub fn with_ctrl(mut self) -> Self {
self.ctrl = true;
self
}
pub fn with_alt(mut self) -> Self {
self.alt = true;
self
}
pub fn with_meta(mut self) -> Self {
self.meta = true;
self
}
pub fn is_empty(&self) -> bool {
!self.shift && !self.ctrl && !self.alt && !self.meta
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MouseEventData {
pub position: LogicalPosition,
pub button: MouseButton,
pub buttons: u8,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Clone, PartialEq)]
pub struct KeyboardEventData {
pub key_code: u32,
pub char_code: Option<char>,
pub modifiers: KeyModifiers,
pub repeat: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ScrollEventData {
pub delta: LogicalPosition,
pub delta_mode: ScrollDeltaMode,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TouchEventData {
pub id: u64,
pub position: LogicalPosition,
pub force: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ClipboardEventData {
pub content: Option<String>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LifecycleEventData {
pub reason: LifecycleReason,
pub previous_bounds: Option<LogicalRect>,
pub current_bounds: LogicalRect,
}
#[derive(Debug, Clone, PartialEq)]
pub struct WindowEventData {
pub size: Option<LogicalRect>,
pub position: Option<LogicalPosition>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum EventData {
Mouse(MouseEventData),
Keyboard(KeyboardEventData),
Scroll(ScrollEventData),
Touch(TouchEventData),
Clipboard(ClipboardEventData),
Lifecycle(LifecycleEventData),
Window(WindowEventData),
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum EventType {
MouseOver,
MouseEnter,
MouseLeave,
MouseOut,
MouseDown,
MouseUp,
Click,
DoubleClick,
ContextMenu,
KeyDown,
KeyUp,
KeyPress,
CompositionStart,
CompositionUpdate,
CompositionEnd,
Focus,
Blur,
FocusIn,
FocusOut,
Input,
Change,
Submit,
Reset,
Invalid,
Scroll,
ScrollStart,
ScrollEnd,
DragStart,
Drag,
DragEnd,
DragEnter,
DragOver,
DragLeave,
Drop,
TouchStart,
TouchMove,
TouchEnd,
TouchCancel,
PenDown,
PenMove,
PenUp,
PenEnter,
PenLeave,
LongPress,
SwipeLeft,
SwipeRight,
SwipeUp,
SwipeDown,
PinchIn,
PinchOut,
RotateClockwise,
RotateCounterClockwise,
Copy,
Cut,
Paste,
Play,
Pause,
Ended,
TimeUpdate,
VolumeChange,
MediaError,
Mount,
Unmount,
Update,
Resize,
WindowResize,
WindowMove,
WindowClose,
WindowFocusIn,
WindowFocusOut,
ThemeChange,
WindowDpiChanged,
WindowMonitorChanged,
MonitorConnected,
MonitorDisconnected,
FileHover,
FileDrop,
FileHoverCancel,
SensorChanged,
GamepadInput,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SyntheticEvent {
pub event_type: EventType,
pub source: EventSource,
pub phase: EventPhase,
pub target: DomNodeId,
pub current_target: DomNodeId,
pub timestamp: Instant,
pub data: EventData,
pub stopped: bool,
pub stopped_immediate: bool,
pub prevented_default: bool,
}
impl SyntheticEvent {
pub fn new(
event_type: EventType,
source: EventSource,
target: DomNodeId,
timestamp: Instant,
data: EventData,
) -> Self {
Self {
event_type,
source,
phase: EventPhase::Target,
target,
current_target: target,
timestamp,
data,
stopped: false,
stopped_immediate: false,
prevented_default: false,
}
}
pub fn stop_propagation(&mut self) {
self.stopped = true;
}
pub fn stop_immediate_propagation(&mut self) {
self.stopped_immediate = true;
self.stopped = true;
}
pub fn prevent_default(&mut self) {
self.prevented_default = true;
}
pub fn is_propagation_stopped(&self) -> bool {
self.stopped
}
pub fn is_immediate_propagation_stopped(&self) -> bool {
self.stopped_immediate
}
pub fn is_default_prevented(&self) -> bool {
self.prevented_default
}
}
#[derive(Debug, Clone)]
pub struct PropagationResult {
pub callbacks_to_invoke: Vec<(NodeId, EventFilter)>,
pub default_prevented: bool,
}
pub fn get_dom_path(
node_hierarchy: &crate::id::NodeHierarchy,
target_node: NodeHierarchyItemId,
) -> Vec<NodeId> {
let mut path = Vec::new();
let target_node_id = match target_node.into_crate_internal() {
Some(id) => id,
None => return path,
};
let hier_ref = node_hierarchy.as_ref();
let mut current = Some(target_node_id);
while let Some(node_id) = current {
path.push(node_id);
current = hier_ref.get(node_id).and_then(|node| node.parent);
}
path.reverse();
path
}
pub fn propagate_event(
event: &mut SyntheticEvent,
node_hierarchy: &crate::id::NodeHierarchy,
callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
) -> PropagationResult {
let path = get_dom_path(node_hierarchy, event.target.node);
if path.is_empty() {
return PropagationResult::default();
}
let ancestors = &path[..path.len().saturating_sub(1)];
let target_node_id = *path.last().unwrap();
let mut result = PropagationResult::default();
propagate_phase(
event,
ancestors.iter().copied(),
EventPhase::Capture,
callbacks,
&mut result,
);
if !event.stopped {
propagate_target_phase(event, target_node_id, callbacks, &mut result);
}
if !event.stopped {
propagate_phase(
event,
ancestors.iter().rev().copied(),
EventPhase::Bubble,
callbacks,
&mut result,
);
}
result.default_prevented = event.prevented_default;
result
}
fn propagate_phase(
event: &mut SyntheticEvent,
nodes: impl Iterator<Item = NodeId>,
phase: EventPhase,
callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
result: &mut PropagationResult,
) {
event.phase = phase;
for node_id in nodes {
if event.stopped_immediate || event.stopped {
return;
}
event.current_target = DomNodeId {
dom: event.target.dom,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
};
collect_matching_callbacks(event, node_id, phase, callbacks, result);
}
}
fn propagate_target_phase(
event: &mut SyntheticEvent,
target_node_id: NodeId,
callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
result: &mut PropagationResult,
) {
event.phase = EventPhase::Target;
event.current_target = event.target;
collect_matching_callbacks(event, target_node_id, EventPhase::Target, callbacks, result);
}
fn collect_matching_callbacks(
event: &SyntheticEvent,
node_id: NodeId,
phase: EventPhase,
callbacks: &BTreeMap<NodeId, Vec<EventFilter>>,
result: &mut PropagationResult,
) {
let Some(node_callbacks) = callbacks.get(&node_id) else {
return;
};
let matching = node_callbacks
.iter()
.take_while(|_| !event.stopped_immediate)
.filter(|filter| matches_filter_phase(filter, event, phase))
.map(|filter| (node_id, *filter));
result.callbacks_to_invoke.extend(matching);
}
impl Default for PropagationResult {
fn default() -> Self {
Self {
callbacks_to_invoke: Vec::new(),
default_prevented: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[repr(C, u8)]
pub enum DefaultAction {
FocusNext,
FocusPrevious,
FocusFirst,
FocusLast,
ClearFocus,
ActivateFocusedElement {
target: DomNodeId,
},
SubmitForm {
form_node: DomNodeId,
},
CloseModal {
modal_node: DomNodeId,
},
ScrollFocusedContainer {
direction: ScrollDirection,
amount: ScrollAmount,
},
SelectAllText,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum ScrollAmount {
Line,
Page,
Document,
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct DefaultActionResult {
pub action: DefaultAction,
pub prevented: bool,
}
impl Default for DefaultActionResult {
fn default() -> Self {
Self {
action: DefaultAction::None,
prevented: false,
}
}
}
impl DefaultActionResult {
pub fn new(action: DefaultAction) -> Self {
Self {
action,
prevented: false,
}
}
pub fn prevented() -> Self {
Self {
action: DefaultAction::None,
prevented: true,
}
}
pub fn has_action(&self) -> bool {
!self.prevented && !matches!(self.action, DefaultAction::None)
}
}
pub trait ActivationBehavior {
fn has_activation_behavior(&self) -> bool;
fn is_activatable(&self) -> bool;
}
pub trait Focusable {
fn get_tabindex(&self) -> Option<i32>;
fn is_focusable(&self) -> bool;
fn is_in_tab_order(&self) -> bool {
match self.get_tabindex() {
None => self.is_naturally_focusable(),
Some(i) => i >= 0,
}
}
fn is_naturally_focusable(&self) -> bool;
}
fn matches_filter_phase(
filter: &EventFilter,
event: &SyntheticEvent,
current_phase: EventPhase,
) -> bool {
match filter {
EventFilter::Hover(hover_filter) => {
matches_hover_filter(hover_filter, event, current_phase)
}
EventFilter::Focus(focus_filter) => {
matches_focus_filter(focus_filter, event, current_phase)
}
EventFilter::Window(window_filter) => {
matches_window_filter(window_filter, event, current_phase)
}
EventFilter::Component(component_filter) => {
matches_component_filter(component_filter, event, current_phase)
}
EventFilter::Application(_) => {
false
}
}
}
fn matches_component_filter(
filter: &ComponentEventFilter,
event: &SyntheticEvent,
_phase: EventPhase,
) -> bool {
matches!(
(filter, &event.event_type),
(ComponentEventFilter::AfterMount, EventType::Mount)
| (ComponentEventFilter::BeforeUnmount, EventType::Unmount)
| (ComponentEventFilter::Updated, EventType::Update)
| (ComponentEventFilter::NodeResized, EventType::Resize)
)
}
fn check_mouse_button(data: &EventData, expected: MouseButton) -> bool {
if let EventData::Mouse(mouse_data) = data {
mouse_data.button == expected
} else {
false
}
}
fn matches_hover_filter(
filter: &HoverEventFilter,
event: &SyntheticEvent,
_phase: EventPhase,
) -> bool {
use HoverEventFilter::*;
match (filter, &event.event_type) {
(MouseOver, EventType::MouseOver) => true,
(MouseDown, EventType::MouseDown) => true,
(LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Right)
}
(MiddleMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Middle)
}
(MouseUp, EventType::MouseUp) => true,
(LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
(MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
(MouseEnter, EventType::MouseEnter) => true,
(MouseLeave, EventType::MouseLeave) => true,
(Scroll, EventType::Scroll) => true,
(ScrollStart, EventType::ScrollStart) => true,
(ScrollEnd, EventType::ScrollEnd) => true,
(TextInput, EventType::Input) => true,
(VirtualKeyDown, EventType::KeyDown) => true,
(VirtualKeyUp, EventType::KeyUp) => true,
(HoveredFile, EventType::FileHover) => true,
(DroppedFile, EventType::FileDrop) => true,
(HoveredFileCancelled, EventType::FileHoverCancel) => true,
(TouchStart, EventType::TouchStart) => true,
(TouchMove, EventType::TouchMove) => true,
(TouchEnd, EventType::TouchEnd) => true,
(TouchCancel, EventType::TouchCancel) => true,
(PenDown, EventType::PenDown) => true,
(PenMove, EventType::PenMove) => true,
(PenUp, EventType::PenUp) => true,
(PenEnter, EventType::PenEnter) => true,
(PenLeave, EventType::PenLeave) => true,
(DragStart, EventType::DragStart) => true,
(Drag, EventType::Drag) => true,
(DragEnd, EventType::DragEnd) => true,
(DragEnter, EventType::DragEnter) => true,
(DragOver, EventType::DragOver) => true,
(DragLeave, EventType::DragLeave) => true,
(Drop, EventType::Drop) => true,
(DoubleClick, EventType::DoubleClick) => true,
(SensorChanged, EventType::SensorChanged) => true,
(GamepadInput, EventType::GamepadInput) => true,
_ => false,
}
}
fn matches_focus_filter(
filter: &FocusEventFilter,
event: &SyntheticEvent,
_phase: EventPhase,
) -> bool {
use FocusEventFilter::*;
match (filter, &event.event_type) {
(MouseOver, EventType::MouseOver) => true,
(MouseDown, EventType::MouseDown) => true,
(LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Right)
}
(MiddleMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Middle)
}
(MouseUp, EventType::MouseUp) => true,
(LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
(MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
(MouseEnter, EventType::MouseEnter) => true,
(MouseLeave, EventType::MouseLeave) => true,
(Scroll, EventType::Scroll) => true,
(ScrollStart, EventType::ScrollStart) => true,
(ScrollEnd, EventType::ScrollEnd) => true,
(TextInput, EventType::Input) => true,
(VirtualKeyDown, EventType::KeyDown) => true,
(VirtualKeyUp, EventType::KeyUp) => true,
(FocusReceived, EventType::Focus) => true,
(FocusLost, EventType::Blur) => true,
(DragStart, EventType::DragStart) => true,
(Drag, EventType::Drag) => true,
(DragEnd, EventType::DragEnd) => true,
(DragEnter, EventType::DragEnter) => true,
(DragOver, EventType::DragOver) => true,
(DragLeave, EventType::DragLeave) => true,
(Drop, EventType::Drop) => true,
_ => false,
}
}
fn matches_window_filter(
filter: &WindowEventFilter,
event: &SyntheticEvent,
_phase: EventPhase,
) -> bool {
use WindowEventFilter::*;
match (filter, &event.event_type) {
(MouseOver, EventType::MouseOver) => true,
(MouseDown, EventType::MouseDown) => true,
(LeftMouseDown, EventType::MouseDown) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Right)
}
(MiddleMouseDown, EventType::MouseDown) => {
check_mouse_button(&event.data, MouseButton::Middle)
}
(MouseUp, EventType::MouseUp) => true,
(LeftMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Left),
(RightMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Right),
(MiddleMouseUp, EventType::MouseUp) => check_mouse_button(&event.data, MouseButton::Middle),
(MouseEnter, EventType::MouseEnter) => true,
(MouseLeave, EventType::MouseLeave) => true,
(Scroll, EventType::Scroll) => true,
(ScrollStart, EventType::ScrollStart) => true,
(ScrollEnd, EventType::ScrollEnd) => true,
(TextInput, EventType::Input) => true,
(VirtualKeyDown, EventType::KeyDown) => true,
(VirtualKeyUp, EventType::KeyUp) => true,
(HoveredFile, EventType::FileHover) => true,
(DroppedFile, EventType::FileDrop) => true,
(HoveredFileCancelled, EventType::FileHoverCancel) => true,
(Resized, EventType::WindowResize) => true,
(Moved, EventType::WindowMove) => true,
(TouchStart, EventType::TouchStart) => true,
(TouchMove, EventType::TouchMove) => true,
(TouchEnd, EventType::TouchEnd) => true,
(TouchCancel, EventType::TouchCancel) => true,
(PenDown, EventType::PenDown) => true,
(PenMove, EventType::PenMove) => true,
(PenUp, EventType::PenUp) => true,
(PenEnter, EventType::PenEnter) => true,
(PenLeave, EventType::PenLeave) => true,
(FocusReceived, EventType::Focus) => true,
(FocusLost, EventType::Blur) => true,
(CloseRequested, EventType::WindowClose) => true,
(ThemeChanged, EventType::ThemeChange) => true,
(WindowFocusReceived, EventType::WindowFocusIn) => true,
(WindowFocusLost, EventType::WindowFocusOut) => true,
(SensorChanged, EventType::SensorChanged) => true,
(GamepadInput, EventType::GamepadInput) => true,
(DragStart, EventType::DragStart) => true,
(Drag, EventType::Drag) => true,
(DragEnd, EventType::DragEnd) => true,
(DragEnter, EventType::DragEnter) => true,
(DragOver, EventType::DragOver) => true,
(DragLeave, EventType::DragLeave) => true,
(Drop, EventType::Drop) => true,
_ => false,
}
}
pub fn detect_lifecycle_events(
old_dom_id: DomId,
new_dom_id: DomId,
old_hierarchy: Option<&crate::id::NodeHierarchy>,
new_hierarchy: Option<&crate::id::NodeHierarchy>,
old_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
new_layout: Option<&BTreeMap<NodeId, LogicalRect>>,
timestamp: Instant,
) -> Vec<SyntheticEvent> {
let old_nodes = collect_node_ids(old_hierarchy);
let new_nodes = collect_node_ids(new_hierarchy);
let mut events = Vec::new();
if let Some(layout) = new_layout {
for &node_id in new_nodes.difference(&old_nodes) {
events.push(create_mount_event(node_id, new_dom_id, layout, ×tamp));
}
}
if let Some(layout) = old_layout {
for &node_id in old_nodes.difference(&new_nodes) {
events.push(create_unmount_event(
node_id, old_dom_id, layout, ×tamp,
));
}
}
if let (Some(old_l), Some(new_l)) = (old_layout, new_layout) {
for &node_id in old_nodes.intersection(&new_nodes) {
if let Some(ev) = create_resize_event(node_id, new_dom_id, old_l, new_l, ×tamp) {
events.push(ev);
}
}
}
events
}
fn collect_node_ids(hierarchy: Option<&crate::id::NodeHierarchy>) -> BTreeSet<NodeId> {
hierarchy
.map(|h| h.as_ref().linear_iter().collect())
.unwrap_or_default()
}
fn create_lifecycle_event(
event_type: EventType,
node_id: NodeId,
dom_id: DomId,
timestamp: &Instant,
data: LifecycleEventData,
) -> SyntheticEvent {
let dom_node_id = DomNodeId {
dom: dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
};
SyntheticEvent {
event_type,
source: EventSource::Lifecycle,
phase: EventPhase::Target,
target: dom_node_id,
current_target: dom_node_id,
timestamp: timestamp.clone(),
data: EventData::Lifecycle(data),
stopped: false,
stopped_immediate: false,
prevented_default: false,
}
}
fn create_mount_event(
node_id: NodeId,
dom_id: DomId,
layout: &BTreeMap<NodeId, LogicalRect>,
timestamp: &Instant,
) -> SyntheticEvent {
let current_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
create_lifecycle_event(
EventType::Mount,
node_id,
dom_id,
timestamp,
LifecycleEventData {
reason: LifecycleReason::InitialMount,
previous_bounds: None,
current_bounds,
},
)
}
fn create_unmount_event(
node_id: NodeId,
dom_id: DomId,
layout: &BTreeMap<NodeId, LogicalRect>,
timestamp: &Instant,
) -> SyntheticEvent {
let previous_bounds = layout.get(&node_id).copied().unwrap_or(LogicalRect::zero());
create_lifecycle_event(
EventType::Unmount,
node_id,
dom_id,
timestamp,
LifecycleEventData {
reason: LifecycleReason::Unmount,
previous_bounds: Some(previous_bounds),
current_bounds: LogicalRect::zero(),
},
)
}
fn create_resize_event(
node_id: NodeId,
dom_id: DomId,
old_layout: &BTreeMap<NodeId, LogicalRect>,
new_layout: &BTreeMap<NodeId, LogicalRect>,
timestamp: &Instant,
) -> Option<SyntheticEvent> {
let old_bounds = *old_layout.get(&node_id)?;
let new_bounds = *new_layout.get(&node_id)?;
if old_bounds.size == new_bounds.size {
return None;
}
Some(create_lifecycle_event(
EventType::Resize,
node_id,
dom_id,
timestamp,
LifecycleEventData {
reason: LifecycleReason::Resize,
previous_bounds: Some(old_bounds),
current_bounds: new_bounds,
},
))
}
#[derive(Debug, Clone, Default)]
pub struct LifecycleEventResult {
pub events: Vec<SyntheticEvent>,
pub node_id_mapping: crate::OrderedMap<NodeId, NodeId>,
}
pub fn detect_lifecycle_events_with_reconciliation(
dom_id: DomId,
old_node_data: &[crate::dom::NodeData],
new_node_data: &[crate::dom::NodeData],
old_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
new_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
old_layout: &crate::OrderedMap<NodeId, LogicalRect>,
new_layout: &crate::OrderedMap<NodeId, LogicalRect>,
timestamp: Instant,
) -> LifecycleEventResult {
let diff_result = crate::diff::reconcile_dom(
old_node_data,
new_node_data,
old_hierarchy,
new_hierarchy,
old_layout,
new_layout,
dom_id,
timestamp,
);
LifecycleEventResult {
events: diff_result.events,
node_id_mapping: crate::diff::create_migration_map(&diff_result.node_moves),
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum HoverEventFilter {
MouseOver,
MouseDown,
LeftMouseDown,
RightMouseDown,
MiddleMouseDown,
MouseUp,
LeftMouseUp,
RightMouseUp,
MiddleMouseUp,
MouseEnter,
MouseLeave,
Scroll,
ScrollStart,
ScrollEnd,
TextInput,
VirtualKeyDown,
VirtualKeyUp,
HoveredFile,
DroppedFile,
HoveredFileCancelled,
TouchStart,
TouchMove,
TouchEnd,
TouchCancel,
PenDown,
PenMove,
PenUp,
PenEnter,
PenLeave,
PenSqueeze,
PenDoubleTap,
PenHover,
GeolocationFix,
GeolocationError,
SensorChanged,
GamepadInput,
DragStart,
Drag,
DragEnd,
DragEnter,
DragOver,
DragLeave,
Drop,
DoubleClick,
LongPress,
SwipeLeft,
SwipeRight,
SwipeUp,
SwipeDown,
PinchIn,
PinchOut,
RotateClockwise,
RotateCounterClockwise,
MouseOut,
FocusIn,
FocusOut,
CompositionStart,
CompositionUpdate,
CompositionEnd,
#[doc(hidden)]
SystemTextSingleClick,
#[doc(hidden)]
SystemTextDoubleClick,
#[doc(hidden)]
SystemTextTripleClick,
}
impl HoverEventFilter {
pub const fn is_system_internal(&self) -> bool {
matches!(
self,
HoverEventFilter::SystemTextSingleClick
| HoverEventFilter::SystemTextDoubleClick
| HoverEventFilter::SystemTextTripleClick
)
}
pub fn to_focus_event_filter(&self) -> Option<FocusEventFilter> {
match self {
HoverEventFilter::MouseOver => Some(FocusEventFilter::MouseOver),
HoverEventFilter::MouseDown => Some(FocusEventFilter::MouseDown),
HoverEventFilter::LeftMouseDown => Some(FocusEventFilter::LeftMouseDown),
HoverEventFilter::RightMouseDown => Some(FocusEventFilter::RightMouseDown),
HoverEventFilter::MiddleMouseDown => Some(FocusEventFilter::MiddleMouseDown),
HoverEventFilter::MouseUp => Some(FocusEventFilter::MouseUp),
HoverEventFilter::LeftMouseUp => Some(FocusEventFilter::LeftMouseUp),
HoverEventFilter::RightMouseUp => Some(FocusEventFilter::RightMouseUp),
HoverEventFilter::MiddleMouseUp => Some(FocusEventFilter::MiddleMouseUp),
HoverEventFilter::MouseEnter => Some(FocusEventFilter::MouseEnter),
HoverEventFilter::MouseLeave => Some(FocusEventFilter::MouseLeave),
HoverEventFilter::Scroll => Some(FocusEventFilter::Scroll),
HoverEventFilter::ScrollStart => Some(FocusEventFilter::ScrollStart),
HoverEventFilter::ScrollEnd => Some(FocusEventFilter::ScrollEnd),
HoverEventFilter::TextInput => Some(FocusEventFilter::TextInput),
HoverEventFilter::VirtualKeyDown => Some(FocusEventFilter::VirtualKeyDown),
HoverEventFilter::VirtualKeyUp => Some(FocusEventFilter::VirtualKeyUp),
HoverEventFilter::HoveredFile => None,
HoverEventFilter::DroppedFile => None,
HoverEventFilter::HoveredFileCancelled => None,
HoverEventFilter::TouchStart => None,
HoverEventFilter::TouchMove => None,
HoverEventFilter::TouchEnd => None,
HoverEventFilter::TouchCancel => None,
HoverEventFilter::PenDown => Some(FocusEventFilter::PenDown),
HoverEventFilter::PenMove => Some(FocusEventFilter::PenMove),
HoverEventFilter::PenUp => Some(FocusEventFilter::PenUp),
HoverEventFilter::PenEnter => None,
HoverEventFilter::PenLeave => None,
HoverEventFilter::PenSqueeze => None,
HoverEventFilter::PenDoubleTap => None,
HoverEventFilter::PenHover => None,
HoverEventFilter::GeolocationFix => None,
HoverEventFilter::GeolocationError => None,
HoverEventFilter::SensorChanged => None,
HoverEventFilter::GamepadInput => None,
HoverEventFilter::DragStart => Some(FocusEventFilter::DragStart),
HoverEventFilter::Drag => Some(FocusEventFilter::Drag),
HoverEventFilter::DragEnd => Some(FocusEventFilter::DragEnd),
HoverEventFilter::DragEnter => Some(FocusEventFilter::DragEnter),
HoverEventFilter::DragOver => Some(FocusEventFilter::DragOver),
HoverEventFilter::DragLeave => Some(FocusEventFilter::DragLeave),
HoverEventFilter::Drop => Some(FocusEventFilter::Drop),
HoverEventFilter::DoubleClick => Some(FocusEventFilter::DoubleClick),
HoverEventFilter::LongPress => Some(FocusEventFilter::LongPress),
HoverEventFilter::SwipeLeft => Some(FocusEventFilter::SwipeLeft),
HoverEventFilter::SwipeRight => Some(FocusEventFilter::SwipeRight),
HoverEventFilter::SwipeUp => Some(FocusEventFilter::SwipeUp),
HoverEventFilter::SwipeDown => Some(FocusEventFilter::SwipeDown),
HoverEventFilter::PinchIn => Some(FocusEventFilter::PinchIn),
HoverEventFilter::PinchOut => Some(FocusEventFilter::PinchOut),
HoverEventFilter::RotateClockwise => Some(FocusEventFilter::RotateClockwise),
HoverEventFilter::RotateCounterClockwise => {
Some(FocusEventFilter::RotateCounterClockwise)
}
HoverEventFilter::MouseOut => Some(FocusEventFilter::MouseLeave), HoverEventFilter::FocusIn => Some(FocusEventFilter::FocusIn),
HoverEventFilter::FocusOut => Some(FocusEventFilter::FocusOut),
HoverEventFilter::CompositionStart => Some(FocusEventFilter::CompositionStart),
HoverEventFilter::CompositionUpdate => Some(FocusEventFilter::CompositionUpdate),
HoverEventFilter::CompositionEnd => Some(FocusEventFilter::CompositionEnd),
HoverEventFilter::SystemTextSingleClick => None,
HoverEventFilter::SystemTextDoubleClick => None,
HoverEventFilter::SystemTextTripleClick => None,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum FocusEventFilter {
MouseOver,
MouseDown,
LeftMouseDown,
RightMouseDown,
MiddleMouseDown,
MouseUp,
LeftMouseUp,
RightMouseUp,
MiddleMouseUp,
MouseEnter,
MouseLeave,
Scroll,
ScrollStart,
ScrollEnd,
TextInput,
VirtualKeyDown,
VirtualKeyUp,
FocusReceived,
FocusLost,
PenDown,
PenMove,
PenUp,
DragStart,
Drag,
DragEnd,
DragEnter,
DragOver,
DragLeave,
Drop,
DoubleClick,
LongPress,
SwipeLeft,
SwipeRight,
SwipeUp,
SwipeDown,
PinchIn,
PinchOut,
RotateClockwise,
RotateCounterClockwise,
FocusIn,
FocusOut,
CompositionStart,
CompositionUpdate,
CompositionEnd,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub enum WindowEventFilter {
MouseOver,
MouseDown,
LeftMouseDown,
RightMouseDown,
MiddleMouseDown,
MouseUp,
LeftMouseUp,
RightMouseUp,
MiddleMouseUp,
MouseEnter,
MouseLeave,
Scroll,
ScrollStart,
ScrollEnd,
TextInput,
VirtualKeyDown,
VirtualKeyUp,
HoveredFile,
DroppedFile,
HoveredFileCancelled,
Resized,
Moved,
TouchStart,
TouchMove,
TouchEnd,
TouchCancel,
FocusReceived,
FocusLost,
CloseRequested,
ThemeChanged,
WindowFocusReceived,
WindowFocusLost,
PenDown,
PenMove,
PenUp,
PenEnter,
PenLeave,
PenSqueeze,
PenDoubleTap,
PenHover,
GeolocationFix,
GeolocationError,
SensorChanged,
GamepadInput,
DragStart,
Drag,
DragEnd,
DragEnter,
DragOver,
DragLeave,
Drop,
DoubleClick,
LongPress,
SwipeLeft,
SwipeRight,
SwipeUp,
SwipeDown,
PinchIn,
PinchOut,
RotateClockwise,
RotateCounterClockwise,
DpiChanged,
MonitorChanged,
}
impl WindowEventFilter {
pub fn to_hover_event_filter(&self) -> Option<HoverEventFilter> {
match self {
WindowEventFilter::MouseOver => Some(HoverEventFilter::MouseOver),
WindowEventFilter::MouseDown => Some(HoverEventFilter::MouseDown),
WindowEventFilter::LeftMouseDown => Some(HoverEventFilter::LeftMouseDown),
WindowEventFilter::RightMouseDown => Some(HoverEventFilter::RightMouseDown),
WindowEventFilter::MiddleMouseDown => Some(HoverEventFilter::MiddleMouseDown),
WindowEventFilter::MouseUp => Some(HoverEventFilter::MouseUp),
WindowEventFilter::LeftMouseUp => Some(HoverEventFilter::LeftMouseUp),
WindowEventFilter::RightMouseUp => Some(HoverEventFilter::RightMouseUp),
WindowEventFilter::MiddleMouseUp => Some(HoverEventFilter::MiddleMouseUp),
WindowEventFilter::Scroll => Some(HoverEventFilter::Scroll),
WindowEventFilter::ScrollStart => Some(HoverEventFilter::ScrollStart),
WindowEventFilter::ScrollEnd => Some(HoverEventFilter::ScrollEnd),
WindowEventFilter::TextInput => Some(HoverEventFilter::TextInput),
WindowEventFilter::VirtualKeyDown => Some(HoverEventFilter::VirtualKeyDown),
WindowEventFilter::VirtualKeyUp => Some(HoverEventFilter::VirtualKeyUp),
WindowEventFilter::HoveredFile => Some(HoverEventFilter::HoveredFile),
WindowEventFilter::DroppedFile => Some(HoverEventFilter::DroppedFile),
WindowEventFilter::HoveredFileCancelled => Some(HoverEventFilter::HoveredFileCancelled),
WindowEventFilter::MouseEnter => None,
WindowEventFilter::MouseLeave => None,
WindowEventFilter::Resized => None,
WindowEventFilter::Moved => None,
WindowEventFilter::TouchStart => Some(HoverEventFilter::TouchStart),
WindowEventFilter::TouchMove => Some(HoverEventFilter::TouchMove),
WindowEventFilter::TouchEnd => Some(HoverEventFilter::TouchEnd),
WindowEventFilter::TouchCancel => Some(HoverEventFilter::TouchCancel),
WindowEventFilter::FocusReceived => None,
WindowEventFilter::FocusLost => None,
WindowEventFilter::CloseRequested => None,
WindowEventFilter::ThemeChanged => None,
WindowEventFilter::WindowFocusReceived => None, WindowEventFilter::WindowFocusLost => None, WindowEventFilter::PenDown => Some(HoverEventFilter::PenDown),
WindowEventFilter::PenMove => Some(HoverEventFilter::PenMove),
WindowEventFilter::PenUp => Some(HoverEventFilter::PenUp),
WindowEventFilter::PenEnter => Some(HoverEventFilter::PenEnter),
WindowEventFilter::PenLeave => Some(HoverEventFilter::PenLeave),
WindowEventFilter::PenSqueeze => Some(HoverEventFilter::PenSqueeze),
WindowEventFilter::PenDoubleTap => Some(HoverEventFilter::PenDoubleTap),
WindowEventFilter::PenHover => Some(HoverEventFilter::PenHover),
WindowEventFilter::GeolocationFix => Some(HoverEventFilter::GeolocationFix),
WindowEventFilter::GeolocationError => Some(HoverEventFilter::GeolocationError),
WindowEventFilter::SensorChanged => Some(HoverEventFilter::SensorChanged),
WindowEventFilter::GamepadInput => Some(HoverEventFilter::GamepadInput),
WindowEventFilter::DragStart => Some(HoverEventFilter::DragStart),
WindowEventFilter::Drag => Some(HoverEventFilter::Drag),
WindowEventFilter::DragEnd => Some(HoverEventFilter::DragEnd),
WindowEventFilter::DragEnter => Some(HoverEventFilter::DragEnter),
WindowEventFilter::DragOver => Some(HoverEventFilter::DragOver),
WindowEventFilter::DragLeave => Some(HoverEventFilter::DragLeave),
WindowEventFilter::Drop => Some(HoverEventFilter::Drop),
WindowEventFilter::DoubleClick => Some(HoverEventFilter::DoubleClick),
WindowEventFilter::LongPress => Some(HoverEventFilter::LongPress),
WindowEventFilter::SwipeLeft => Some(HoverEventFilter::SwipeLeft),
WindowEventFilter::SwipeRight => Some(HoverEventFilter::SwipeRight),
WindowEventFilter::SwipeUp => Some(HoverEventFilter::SwipeUp),
WindowEventFilter::SwipeDown => Some(HoverEventFilter::SwipeDown),
WindowEventFilter::PinchIn => Some(HoverEventFilter::PinchIn),
WindowEventFilter::PinchOut => Some(HoverEventFilter::PinchOut),
WindowEventFilter::RotateClockwise => Some(HoverEventFilter::RotateClockwise),
WindowEventFilter::RotateCounterClockwise => {
Some(HoverEventFilter::RotateCounterClockwise)
}
WindowEventFilter::DpiChanged => None,
WindowEventFilter::MonitorChanged => None,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ComponentEventFilter {
AfterMount,
BeforeUnmount,
NodeResized,
DefaultAction,
Selected,
Updated,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(C)]
pub enum ApplicationEventFilter {
DeviceConnected,
DeviceDisconnected,
MonitorConnected,
MonitorDisconnected,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum EventFilter {
Hover(HoverEventFilter),
Focus(FocusEventFilter),
Window(WindowEventFilter),
Component(ComponentEventFilter),
Application(ApplicationEventFilter),
}
impl EventFilter {
pub const fn is_focus_callback(&self) -> bool {
match self {
EventFilter::Focus(_) => true,
_ => false,
}
}
pub const fn is_window_callback(&self) -> bool {
match self {
EventFilter::Window(_) => true,
_ => false,
}
}
}
macro_rules! get_single_enum_type {
($fn_name:ident, $enum_name:ident:: $variant:ident($return_type:ty)) => {
pub fn $fn_name(&self) -> Option<$return_type> {
use self::$enum_name::*;
match self {
$variant(e) => Some(*e),
_ => None,
}
}
};
}
impl EventFilter {
get_single_enum_type!(as_hover_event_filter, EventFilter::Hover(HoverEventFilter));
get_single_enum_type!(as_focus_event_filter, EventFilter::Focus(FocusEventFilter));
get_single_enum_type!(
as_window_event_filter,
EventFilter::Window(WindowEventFilter)
);
}
impl From<On> for EventFilter {
fn from(input: On) -> EventFilter {
use crate::dom::On::*;
match input {
MouseOver => EventFilter::Hover(HoverEventFilter::MouseOver),
MouseDown => EventFilter::Hover(HoverEventFilter::MouseDown),
LeftMouseDown => EventFilter::Hover(HoverEventFilter::LeftMouseDown),
MiddleMouseDown => EventFilter::Hover(HoverEventFilter::MiddleMouseDown),
RightMouseDown => EventFilter::Hover(HoverEventFilter::RightMouseDown),
MouseUp => EventFilter::Hover(HoverEventFilter::MouseUp),
LeftMouseUp => EventFilter::Hover(HoverEventFilter::LeftMouseUp),
MiddleMouseUp => EventFilter::Hover(HoverEventFilter::MiddleMouseUp),
RightMouseUp => EventFilter::Hover(HoverEventFilter::RightMouseUp),
MouseEnter => EventFilter::Hover(HoverEventFilter::MouseEnter),
MouseLeave => EventFilter::Hover(HoverEventFilter::MouseLeave),
Scroll => EventFilter::Hover(HoverEventFilter::Scroll),
TextInput => EventFilter::Focus(FocusEventFilter::TextInput), VirtualKeyDown => EventFilter::Window(WindowEventFilter::VirtualKeyDown), VirtualKeyUp => EventFilter::Window(WindowEventFilter::VirtualKeyUp), HoveredFile => EventFilter::Hover(HoverEventFilter::HoveredFile),
DroppedFile => EventFilter::Hover(HoverEventFilter::DroppedFile),
HoveredFileCancelled => EventFilter::Hover(HoverEventFilter::HoveredFileCancelled),
FocusReceived => EventFilter::Focus(FocusEventFilter::FocusReceived), FocusLost => EventFilter::Focus(FocusEventFilter::FocusLost),
Default => EventFilter::Hover(HoverEventFilter::MouseUp), Collapse => EventFilter::Hover(HoverEventFilter::MouseUp), Expand => EventFilter::Hover(HoverEventFilter::MouseUp), Increment => EventFilter::Hover(HoverEventFilter::MouseUp), Decrement => EventFilter::Hover(HoverEventFilter::MouseUp), }
}
}
pub trait EventProvider {
fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent>;
}
pub fn deduplicate_synthetic_events(mut events: Vec<SyntheticEvent>) -> Vec<SyntheticEvent> {
if events.len() <= 1 {
return events;
}
events.sort_by_key(|e| (e.target.dom, e.target.node, e.event_type));
let mut result = Vec::with_capacity(events.len());
let mut iter = events.into_iter();
if let Some(mut prev) = iter.next() {
for curr in iter {
if prev.target == curr.target && prev.event_type == curr.event_type {
prev = if curr.timestamp > prev.timestamp {
curr
} else {
prev
};
} else {
result.push(prev);
prev = curr;
}
}
result.push(prev);
}
result
}
pub fn event_type_to_filters(event_type: EventType, event_data: &EventData) -> Vec<EventFilter> {
use EventFilter as EF;
use EventType as E;
use FocusEventFilter as F;
use HoverEventFilter as H;
use WindowEventFilter as W;
let button_specific_down = || -> Option<EventFilter> {
match event_data {
EventData::Mouse(m) => match m.button {
MouseButton::Left => Some(EF::Hover(H::LeftMouseDown)),
MouseButton::Right => Some(EF::Hover(H::RightMouseDown)),
MouseButton::Middle => Some(EF::Hover(H::MiddleMouseDown)),
MouseButton::Other(_) => None, },
_ => Some(EF::Hover(H::LeftMouseDown)), }
};
let button_specific_up = || -> Option<EventFilter> {
match event_data {
EventData::Mouse(m) => match m.button {
MouseButton::Left => Some(EF::Hover(H::LeftMouseUp)),
MouseButton::Right => Some(EF::Hover(H::RightMouseUp)),
MouseButton::Middle => Some(EF::Hover(H::MiddleMouseUp)),
MouseButton::Other(_) => None, },
_ => Some(EF::Hover(H::LeftMouseUp)), }
};
match event_type {
E::MouseDown => {
let mut v = vec![EF::Hover(H::MouseDown)];
if let Some(f) = button_specific_down() { v.push(f); }
v
}
E::MouseUp => {
let mut v = vec![EF::Hover(H::MouseUp)];
if let Some(f) = button_specific_up() { v.push(f); }
v
}
E::Click => vec![EF::Hover(H::LeftMouseDown)],
E::MouseOver => vec![EF::Hover(H::MouseOver)],
E::MouseEnter => vec![EF::Hover(H::MouseEnter)],
E::MouseLeave => vec![EF::Hover(H::MouseLeave)],
E::MouseOut => vec![EF::Hover(H::MouseOut)],
E::DoubleClick => vec![EF::Hover(H::DoubleClick), EF::Window(W::DoubleClick)],
E::ContextMenu => vec![EF::Hover(H::RightMouseDown)],
E::KeyDown => vec![EF::Focus(F::VirtualKeyDown)],
E::KeyUp => vec![EF::Focus(F::VirtualKeyUp)],
E::KeyPress => vec![EF::Focus(F::TextInput)],
E::CompositionStart => vec![EF::Hover(H::CompositionStart), EF::Focus(F::CompositionStart)],
E::CompositionUpdate => vec![EF::Hover(H::CompositionUpdate), EF::Focus(F::CompositionUpdate)],
E::CompositionEnd => vec![EF::Hover(H::CompositionEnd), EF::Focus(F::CompositionEnd)],
E::Focus => vec![EF::Focus(F::FocusReceived)],
E::Blur => vec![EF::Focus(F::FocusLost)],
E::FocusIn => vec![EF::Hover(H::FocusIn), EF::Focus(F::FocusIn)],
E::FocusOut => vec![EF::Hover(H::FocusOut), EF::Focus(F::FocusOut)],
E::Input | E::Change => vec![EF::Focus(F::TextInput)],
E::Scroll | E::ScrollStart | E::ScrollEnd => vec![EF::Hover(H::Scroll)],
E::DragStart => vec![EF::Hover(H::DragStart), EF::Window(W::DragStart)],
E::Drag => vec![EF::Hover(H::Drag), EF::Window(W::Drag)],
E::DragEnd => vec![EF::Hover(H::DragEnd), EF::Window(W::DragEnd)],
E::DragEnter => vec![EF::Hover(H::DragEnter), EF::Window(W::DragEnter)],
E::DragOver => vec![EF::Hover(H::DragOver), EF::Window(W::DragOver)],
E::DragLeave => vec![EF::Hover(H::DragLeave), EF::Window(W::DragLeave)],
E::Drop => vec![EF::Hover(H::Drop), EF::Window(W::Drop)],
E::TouchStart => vec![EF::Hover(H::TouchStart)],
E::TouchMove => vec![EF::Hover(H::TouchMove)],
E::TouchEnd => vec![EF::Hover(H::TouchEnd)],
E::TouchCancel => vec![EF::Hover(H::TouchCancel)],
E::WindowResize => vec![EF::Window(W::Resized)],
E::WindowMove => vec![EF::Window(W::Moved)],
E::WindowClose => vec![EF::Window(W::CloseRequested)],
E::WindowFocusIn => vec![EF::Window(W::WindowFocusReceived)],
E::WindowFocusOut => vec![EF::Window(W::WindowFocusLost)],
E::ThemeChange => vec![EF::Window(W::ThemeChanged)],
E::WindowDpiChanged => vec![EF::Window(W::DpiChanged)],
E::WindowMonitorChanged => vec![EF::Window(W::MonitorChanged)],
E::MonitorConnected => vec![EF::Application(ApplicationEventFilter::MonitorConnected)],
E::MonitorDisconnected => vec![EF::Application(ApplicationEventFilter::MonitorDisconnected)],
E::FileHover => vec![EF::Hover(H::HoveredFile)],
E::FileDrop => vec![EF::Hover(H::DroppedFile)],
E::FileHoverCancel => vec![EF::Hover(H::HoveredFileCancelled)],
E::Mount => vec![EF::Component(ComponentEventFilter::AfterMount)],
E::Unmount => vec![EF::Component(ComponentEventFilter::BeforeUnmount)],
E::Update => vec![EF::Component(ComponentEventFilter::Updated)],
E::Resize => vec![EF::Component(ComponentEventFilter::NodeResized)],
E::SensorChanged => vec![EF::Hover(H::SensorChanged), EF::Window(W::SensorChanged)],
E::GamepadInput => vec![EF::Hover(H::GamepadInput), EF::Window(W::GamepadInput)],
_ => vec![],
}
}
#[derive(Debug, Clone, PartialEq)]
#[must_use = "SystemChange must be processed through apply_system_change()"]
pub enum SystemChange {
TextSelectionClick {
position: LogicalPosition,
timestamp: Instant,
},
TextSelectionDrag {
start_position: LogicalPosition,
current_position: LogicalPosition,
},
ApplySelectionOp {
target: DomNodeId,
op: SelectionOp,
},
CopyToClipboard,
CutToClipboard { target: DomNodeId },
PasteFromClipboard,
SelectAllText,
UndoTextEdit { target: DomNodeId },
RedoTextEdit { target: DomNodeId },
AddCursorAtClick {
position: LogicalPosition,
},
SelectNextOccurrence {
target: DomNodeId,
},
ApplyPendingTextInput,
ApplyTextChangeset,
ActivateNodeDrag {
dom_id: crate::dom::DomId,
node_id: crate::id::NodeId,
},
ActivateWindowDrag,
InitDragVisualState,
SetDragOverState { target: DomNodeId, active: bool },
UpdateDropTarget { target: DomNodeId },
UpdateDragGpuTransform,
DeactivateDrag,
SetFocus {
new_focus: Option<DomNodeId>,
old_focus: Option<DomNodeId>,
},
ClearAllSelections,
FinalizePendingFocusChanges,
ScrollSelectionIntoView,
ScrollNodeIntoView { target: DomNodeId },
ScrollCursorIntoViewAfterTextInput,
StartAutoScrollTimer,
StopAutoScrollTimer,
}
impl_option!(
SystemChange,
OptionSystemChange,
copy = false,
clone = false,
[Debug, Clone, PartialEq]
);
impl_vec!(SystemChange, SystemChangeVec, SystemChangeVecDestructor, SystemChangeVecDestructorType, SystemChangeVecSlice, OptionSystemChange);
impl_vec_debug!(SystemChange, SystemChangeVec);
impl_vec_clone!(SystemChange, SystemChangeVec, SystemChangeVecDestructor);
impl_vec_partialeq!(SystemChange, SystemChangeVec);
#[derive(Debug, Clone, PartialEq)]
pub struct PreCallbackFilterResult {
pub system_changes: Vec<SystemChange>,
pub user_events: Vec<SyntheticEvent>,
}
#[derive(Debug, Clone)]
pub struct InputInterpreterState {
pub focused_node: Option<DomNodeId>,
pub click_count: u8,
pub drag_start_position: Option<LogicalPosition>,
pub has_selection: bool,
}
pub struct InputInterpreterInfo<'a> {
pub events: &'a [SyntheticEvent],
pub hit_test: Option<&'a FullHitTest>,
pub keyboard_state: &'a crate::window::KeyboardState,
pub mouse_state: &'a crate::window::MouseState,
pub state: InputInterpreterState,
}
pub type InputInterpreterCallbackType = extern "C" fn(
crate::refany::RefAny,
*const InputInterpreterInfo<'static>, ) -> PreCallbackFilterResult;
#[repr(C)]
pub struct InputInterpreterCallback {
pub cb: InputInterpreterCallbackType,
pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(InputInterpreterCallback, InputInterpreterCallbackType);
impl Default for InputInterpreterCallback {
fn default() -> Self {
Self {
cb: default_input_interpreter_extern,
ctx: crate::refany::OptionRefAny::None,
}
}
}
pub type PostFilterCallbackType = extern "C" fn(
crate::refany::RefAny,
bool, SystemChangeVecSlice, DomNodeId, DomNodeId, ) -> SystemChangeVec;
#[repr(C)]
pub struct PostFilterCallback {
pub cb: PostFilterCallbackType,
pub ctx: crate::refany::OptionRefAny,
}
impl_callback!(PostFilterCallback, PostFilterCallbackType);
impl Default for PostFilterCallback {
fn default() -> Self {
Self {
cb: default_post_filter_extern,
ctx: crate::refany::OptionRefAny::None,
}
}
}
pub type InputInterpreterFn = fn(
info: &InputInterpreterInfo,
) -> PreCallbackFilterResult;
pub type PostFilterFn = fn(
prevent_default: bool,
pre_changes: &[SystemChange],
old_focus: Option<DomNodeId>,
new_focus: Option<DomNodeId>,
) -> Vec<SystemChange>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseButtonState {
pub left_down: bool,
pub right_down: bool,
pub middle_down: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ArrowDirection {
Left,
Right,
Up,
Down,
LineStart,
LineEnd,
DocumentStart,
DocumentEnd,
}
impl ArrowDirection {
pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool) -> Option<Self> {
use crate::window::VirtualKeyCode::*;
Some(match vk {
Left => ArrowDirection::Left,
Right => ArrowDirection::Right,
Up => ArrowDirection::Up,
Down => ArrowDirection::Down,
Home if ctrl => ArrowDirection::DocumentStart,
Home => ArrowDirection::LineStart,
End if ctrl => ArrowDirection::DocumentEnd,
End => ArrowDirection::LineEnd,
_ => return None,
})
}
pub fn to_selection(self, ctrl: bool) -> (SelectionDirection, SelectionStep) {
match self {
ArrowDirection::Left if ctrl => (SelectionDirection::Backward, SelectionStep::Word),
ArrowDirection::Right if ctrl => (SelectionDirection::Forward, SelectionStep::Word),
ArrowDirection::Left => (SelectionDirection::Backward, SelectionStep::Character),
ArrowDirection::Right => (SelectionDirection::Forward, SelectionStep::Character),
ArrowDirection::Up => (SelectionDirection::Backward, SelectionStep::VisualLine),
ArrowDirection::Down => (SelectionDirection::Forward, SelectionStep::VisualLine),
ArrowDirection::LineStart => (SelectionDirection::Backward, SelectionStep::Line),
ArrowDirection::LineEnd => (SelectionDirection::Forward, SelectionStep::Line),
ArrowDirection::DocumentStart => (SelectionDirection::Backward, SelectionStep::Document),
ArrowDirection::DocumentEnd => (SelectionDirection::Forward, SelectionStep::Document),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionDirection {
Forward,
Backward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionStep {
Character,
Word,
Line,
VisualLine,
Document,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub enum SelectionMode {
Move,
Extend,
Delete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct SelectionOp {
pub direction: SelectionDirection,
pub step: SelectionStep,
pub mode: SelectionMode,
pub repeat: usize,
}
impl SelectionOp {
pub fn new(direction: SelectionDirection, step: SelectionStep, mode: SelectionMode) -> Self {
Self { direction, step, mode, repeat: 1 }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyboardShortcut {
Copy, Cut, Paste, SelectAll, Undo, Redo, }
impl KeyboardShortcut {
pub fn from_key(vk: crate::window::VirtualKeyCode, ctrl: bool, shift: bool) -> Option<Self> {
use crate::window::VirtualKeyCode::*;
if !ctrl {
return None;
}
Some(match vk {
C => KeyboardShortcut::Copy,
X => KeyboardShortcut::Cut,
V => KeyboardShortcut::Paste,
A => KeyboardShortcut::SelectAll,
Z if shift => KeyboardShortcut::Redo,
Z => KeyboardShortcut::Undo,
Y => KeyboardShortcut::Redo,
_ => return None,
})
}
}
pub extern "C" fn default_input_interpreter_extern(
_user_data: crate::refany::RefAny,
info_ptr: *const InputInterpreterInfo<'static>,
) -> PreCallbackFilterResult {
if info_ptr.is_null() {
return PreCallbackFilterResult {
system_changes: Vec::new(),
user_events: Vec::new(),
};
}
let info = unsafe { &*info_ptr };
default_input_interpreter(info)
}
pub extern "C" fn default_post_filter_extern(
_user_data: crate::refany::RefAny,
prevent_default: bool,
pre_changes: SystemChangeVecSlice,
old_focus: DomNodeId,
new_focus: DomNodeId,
) -> SystemChangeVec {
let pre_changes_slice = pre_changes.as_slice();
let old = old_focus.node.into_crate_internal().map(|_| old_focus);
let new = new_focus.node.into_crate_internal().map(|_| new_focus);
default_post_filter(prevent_default, pre_changes_slice, old, new).into()
}
pub fn default_input_interpreter(
info: &InputInterpreterInfo,
) -> PreCallbackFilterResult {
let ctx = FilterContext {
hit_test: info.hit_test,
keyboard_state: info.keyboard_state,
mouse_state: info.mouse_state,
click_count: info.state.click_count,
focused_node: info.state.focused_node,
drag_start_position: info.state.drag_start_position,
};
let (system_changes, user_events) = info.events.iter().fold(
(Vec::new(), Vec::new()),
|(mut internal, mut user), event| {
match process_event_for_internal(&ctx, event) {
Some(InternalEventAction::AddAndSkip(evt)) => {
internal.push(evt);
}
Some(InternalEventAction::AddAndPass(evt)) => {
internal.push(evt);
user.push(event.clone());
}
None => {
user.push(event.clone());
}
}
(internal, user)
},
);
PreCallbackFilterResult {
system_changes,
user_events,
}
}
pub fn pre_callback_filter_internal_events<SM, FM>(
events: &[SyntheticEvent],
hit_test: Option<&FullHitTest>,
keyboard_state: &crate::window::KeyboardState,
mouse_state: &crate::window::MouseState,
selection_manager: &SM,
focus_manager: &FM,
) -> PreCallbackFilterResult
where
SM: SelectionManagerQuery,
FM: FocusManagerQuery,
{
let info = InputInterpreterInfo {
events,
hit_test,
keyboard_state,
mouse_state,
state: InputInterpreterState {
focused_node: focus_manager.get_focused_node_id(),
click_count: selection_manager.get_click_count(),
drag_start_position: selection_manager.get_drag_start_position(),
has_selection: selection_manager.has_selection(),
},
};
default_input_interpreter(&info)
}
struct FilterContext<'a> {
hit_test: Option<&'a FullHitTest>,
keyboard_state: &'a crate::window::KeyboardState,
mouse_state: &'a crate::window::MouseState,
click_count: u8,
focused_node: Option<DomNodeId>,
drag_start_position: Option<LogicalPosition>,
}
fn process_event_for_internal(
ctx: &FilterContext<'_>,
event: &SyntheticEvent,
) -> Option<InternalEventAction> {
match event.event_type {
EventType::MouseDown => handle_mouse_down(event, ctx.hit_test, ctx.click_count, ctx.mouse_state, ctx.keyboard_state),
EventType::MouseOver => handle_mouse_over(
event,
ctx.hit_test,
ctx.mouse_state,
ctx.drag_start_position,
),
EventType::KeyDown => handle_key_down(
event,
ctx.keyboard_state,
ctx.focused_node,
),
_ => None,
}
}
enum InternalEventAction {
AddAndSkip(SystemChange),
AddAndPass(SystemChange),
}
fn get_first_hovered_node(hit_test: Option<&FullHitTest>) -> Option<DomNodeId> {
let ht = hit_test?;
let (dom_id, hit_data) = ht.hovered_nodes.iter().next()?;
let node_id = hit_data.regular_hit_test_nodes.keys().next()?;
Some(DomNodeId {
dom: *dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
})
}
fn get_mouse_position_with_fallback(
event: &SyntheticEvent,
mouse_state: &crate::window::MouseState,
) -> LogicalPosition {
match &event.data {
EventData::Mouse(mouse_data) => mouse_data.position,
_ => {
mouse_state.cursor_position.get_position().unwrap_or(LogicalPosition::zero())
}
}
}
fn handle_mouse_down(
event: &SyntheticEvent,
hit_test: Option<&FullHitTest>,
click_count: u8,
mouse_state: &crate::window::MouseState,
keyboard_state: &crate::window::KeyboardState,
) -> Option<InternalEventAction> {
let effective_click_count = if click_count == 0 { 1 } else { click_count };
if effective_click_count > 3 {
return None;
}
let _target = get_first_hovered_node(hit_test)?;
let position = get_mouse_position_with_fallback(event, mouse_state);
if keyboard_state.ctrl_down() && effective_click_count == 1 {
return Some(InternalEventAction::AddAndPass(
SystemChange::AddCursorAtClick { position },
));
}
Some(InternalEventAction::AddAndPass(
SystemChange::TextSelectionClick {
position,
timestamp: event.timestamp.clone(),
},
))
}
fn handle_mouse_over(
event: &SyntheticEvent,
hit_test: Option<&FullHitTest>,
mouse_state: &crate::window::MouseState,
drag_start_position: Option<LogicalPosition>,
) -> Option<InternalEventAction> {
if !mouse_state.left_down {
return None;
}
let start_position = drag_start_position?;
let _target = get_first_hovered_node(hit_test)?;
let current_position = get_mouse_position_with_fallback(event, mouse_state);
Some(InternalEventAction::AddAndPass(
SystemChange::TextSelectionDrag {
start_position,
current_position,
},
))
}
fn handle_key_down(
event: &SyntheticEvent,
keyboard_state: &crate::window::KeyboardState,
focused_node: Option<DomNodeId>,
) -> Option<InternalEventAction> {
use crate::window::VirtualKeyCode;
let target = focused_node?;
let EventData::Keyboard(_) = &event.data else {
return None;
};
let ctrl = keyboard_state.ctrl_down();
let shift = keyboard_state.shift_down();
let vk = keyboard_state.current_virtual_keycode.as_ref()?;
if ctrl {
if let Some(shortcut) = KeyboardShortcut::from_key(*vk, ctrl, shift) {
let change = match shortcut {
KeyboardShortcut::Copy => SystemChange::CopyToClipboard,
KeyboardShortcut::Cut => SystemChange::CutToClipboard { target },
KeyboardShortcut::Paste => SystemChange::PasteFromClipboard,
KeyboardShortcut::SelectAll => SystemChange::SelectAllText,
KeyboardShortcut::Undo => SystemChange::UndoTextEdit { target },
KeyboardShortcut::Redo => SystemChange::RedoTextEdit { target },
};
return Some(InternalEventAction::AddAndSkip(change));
}
if matches!(vk, VirtualKeyCode::D) {
return Some(InternalEventAction::AddAndSkip(
SystemChange::SelectNextOccurrence { target },
));
}
}
let mode_for_shift = if shift { SelectionMode::Extend } else { SelectionMode::Move };
let selection_op = if let Some(arrow) = ArrowDirection::from_key(*vk, ctrl) {
let (direction, step) = arrow.to_selection(ctrl);
SelectionOp::new(direction, step, mode_for_shift)
} else {
match vk {
VirtualKeyCode::Back => SelectionOp::new(
SelectionDirection::Backward,
if ctrl { SelectionStep::Word } else { SelectionStep::Character },
SelectionMode::Delete,
),
VirtualKeyCode::Delete => SelectionOp::new(
SelectionDirection::Forward,
if ctrl { SelectionStep::Word } else { SelectionStep::Character },
SelectionMode::Delete,
),
_ => return None,
}
};
Some(InternalEventAction::AddAndSkip(
SystemChange::ApplySelectionOp { target, op: selection_op },
))
}
pub trait SelectionManagerQuery {
fn get_click_count(&self) -> u8;
fn get_drag_start_position(&self) -> Option<LogicalPosition>;
fn has_selection(&self) -> bool;
}
pub trait FocusManagerQuery {
fn get_focused_node_id(&self) -> Option<DomNodeId>;
}
pub fn default_post_filter(
prevent_default: bool,
pre_changes: &[SystemChange],
old_focus: Option<DomNodeId>,
new_focus: Option<DomNodeId>,
) -> Vec<SystemChange> {
post_callback_filter_system_changes(prevent_default, pre_changes, old_focus, new_focus)
}
pub fn post_callback_filter_system_changes(
prevent_default: bool,
pre_changes: &[SystemChange],
old_focus: Option<DomNodeId>,
new_focus: Option<DomNodeId>,
) -> Vec<SystemChange> {
let mut changes = Vec::new();
if prevent_default {
if old_focus != new_focus {
changes.push(SystemChange::SetFocus { new_focus, old_focus });
}
return changes;
}
changes.push(SystemChange::ApplyPendingTextInput);
for change in pre_changes {
match change {
SystemChange::TextSelectionClick { .. }
| SystemChange::ApplySelectionOp { .. }
| SystemChange::AddCursorAtClick { .. }
| SystemChange::SelectNextOccurrence { .. } => {
changes.push(SystemChange::ScrollSelectionIntoView);
}
SystemChange::TextSelectionDrag { .. } => {
changes.push(SystemChange::StartAutoScrollTimer);
}
SystemChange::CutToClipboard { .. }
| SystemChange::PasteFromClipboard
| SystemChange::UndoTextEdit { .. }
| SystemChange::RedoTextEdit { .. }
| SystemChange::SelectAllText => {
changes.push(SystemChange::ScrollSelectionIntoView);
}
_ => {}
}
}
if old_focus != new_focus {
changes.push(SystemChange::SetFocus { new_focus, old_focus });
}
changes
}
#[cfg(test)]
mod tests {
use super::*;
use azul_css::AzString;
use crate::dom::{DomId, DomNodeId};
use crate::styled_dom::NodeHierarchyItemId;
use crate::id::NodeId;
use crate::window::{KeyboardState, MouseState, VirtualKeyCode, VirtualKeyCodeVec, OptionVirtualKeyCode};
use crate::geom::LogicalPosition;
use crate::task::{Instant, SystemTick};
struct MockSelectionManager {
click_count: u8,
has_sel: bool,
}
impl SelectionManagerQuery for MockSelectionManager {
fn get_click_count(&self) -> u8 { self.click_count }
fn get_drag_start_position(&self) -> Option<LogicalPosition> { None }
fn has_selection(&self) -> bool { self.has_sel }
}
struct MockFocusManager(Option<DomNodeId>);
impl FocusManagerQuery for MockFocusManager {
fn get_focused_node_id(&self) -> Option<DomNodeId> { self.0 }
}
fn focused_node(node_idx: usize) -> DomNodeId {
DomNodeId {
dom: DomId { inner: 0 },
node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_idx))),
}
}
fn make_keyboard_state(vk: VirtualKeyCode) -> KeyboardState {
let mut ks = KeyboardState::default();
ks.current_virtual_keycode = OptionVirtualKeyCode::Some(vk);
ks.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(vec![vk]);
ks
}
fn make_keydown_event(target: DomNodeId) -> SyntheticEvent {
SyntheticEvent::new(
EventType::KeyDown,
EventSource::User,
target,
Instant::Tick(SystemTick::new(0)),
EventData::Keyboard(KeyboardEventData {
key_code: VirtualKeyCode::Back as u32,
char_code: None,
modifiers: KeyModifiers::default(),
repeat: false,
}),
)
}
#[test]
fn backspace_generates_delete_text_selection() {
let target = focused_node(2);
let events = vec![make_keydown_event(target)];
let kb = make_keyboard_state(VirtualKeyCode::Back);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(
&events, None, &kb, &mouse, &sel, &focus,
);
let ops: Vec<_> = result.system_changes.iter()
.filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
.collect();
assert_eq!(ops.len(), 1, "Backspace should generate ApplySelectionOp");
match &ops[0] {
SystemChange::ApplySelectionOp { op, .. } => {
assert_eq!(op.direction, SelectionDirection::Backward);
assert_eq!(op.step, SelectionStep::Character);
assert_eq!(op.mode, SelectionMode::Delete);
}
_ => unreachable!(),
}
}
#[test]
fn delete_key_generates_forward_deletion() {
let target = focused_node(2);
let event = SyntheticEvent::new(
EventType::KeyDown, EventSource::User, target,
Instant::Tick(SystemTick::new(0)),
EventData::Keyboard(KeyboardEventData {
key_code: VirtualKeyCode::Delete as u32,
char_code: None, modifiers: KeyModifiers::default(), repeat: false,
}),
);
let kb = make_keyboard_state(VirtualKeyCode::Delete);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
let ops: Vec<_> = result.system_changes.iter()
.filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
.collect();
assert_eq!(ops.len(), 1);
match &ops[0] {
SystemChange::ApplySelectionOp { op, .. } => {
assert_eq!(op.direction, SelectionDirection::Forward);
assert_eq!(op.step, SelectionStep::Character);
assert_eq!(op.mode, SelectionMode::Delete);
}
_ => unreachable!(),
}
}
#[test]
fn arrow_left_generates_navigation() {
let target = focused_node(2);
let event = SyntheticEvent::new(
EventType::KeyDown, EventSource::User, target,
Instant::Tick(SystemTick::new(0)),
EventData::Keyboard(KeyboardEventData {
key_code: VirtualKeyCode::Left as u32,
char_code: None, modifiers: KeyModifiers::default(), repeat: false,
}),
);
let kb = make_keyboard_state(VirtualKeyCode::Left);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(&[event], None, &kb, &mouse, &sel, &focus);
let ops: Vec<_> = result.system_changes.iter()
.filter(|c| matches!(c, SystemChange::ApplySelectionOp { .. }))
.collect();
assert_eq!(ops.len(), 1, "Left arrow should generate ApplySelectionOp");
match &ops[0] {
SystemChange::ApplySelectionOp { op, .. } => {
assert_eq!(op.direction, SelectionDirection::Backward);
assert_eq!(op.step, SelectionStep::Character);
assert_eq!(op.mode, SelectionMode::Move);
}
_ => unreachable!(),
}
}
#[test]
fn no_focused_node_means_no_keyboard_system_changes() {
let target = focused_node(2);
let event = make_keydown_event(target);
let kb = make_keyboard_state(VirtualKeyCode::Back);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(None);
let result = pre_callback_filter_internal_events(
&[event], None, &kb, &mouse, &sel, &focus,
);
assert!(result.system_changes.is_empty(),
"No system changes should be generated without focused node");
}
#[test]
fn keydown_without_keyboard_data_generates_no_system_change() {
let target = focused_node(2);
let event = SyntheticEvent::new(
EventType::KeyDown,
EventSource::User,
target,
Instant::Tick(SystemTick::new(0)),
EventData::None, );
let kb = make_keyboard_state(VirtualKeyCode::Back);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(
&[event], None, &kb, &mouse, &sel, &focus,
);
assert!(result.system_changes.is_empty(),
"EventData::None should not generate system changes (documents the old bug)");
}
#[test]
fn ctrl_c_generates_copy() {
let target = focused_node(2);
let event = SyntheticEvent::new(
EventType::KeyDown,
EventSource::User,
target,
Instant::Tick(SystemTick::new(0)),
EventData::Keyboard(KeyboardEventData {
key_code: VirtualKeyCode::C as u32,
char_code: Some('c'),
modifiers: KeyModifiers { ctrl: true, shift: false, alt: false, meta: false },
repeat: false,
}),
);
let mut kb = make_keyboard_state(VirtualKeyCode::C);
kb.pressed_virtual_keycodes = VirtualKeyCodeVec::from_vec(
vec![VirtualKeyCode::C, VirtualKeyCode::LControl]
);
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 0, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(
&[event], None, &kb, &mouse, &sel, &focus,
);
let copy_changes: Vec<_> = result.system_changes.iter()
.filter(|c| matches!(c, SystemChange::CopyToClipboard))
.collect();
assert_eq!(copy_changes.len(), 1, "Ctrl+C should generate CopyToClipboard");
}
fn make_hit_test_with_node(node_idx: usize) -> crate::hit_test::FullHitTest {
use crate::hit_test::{FullHitTest, HitTest, HitTestItem};
use crate::dom::OptionDomNodeId;
use std::collections::BTreeMap;
let node_id = NodeId::new(node_idx);
let dom_id = DomId { inner: 0 };
let mut regular = BTreeMap::new();
regular.insert(node_id, HitTestItem {
point_in_viewport: LogicalPosition::new(100.0, 200.0),
point_relative_to_item: LogicalPosition::new(50.0, 30.0),
is_focusable: true,
is_virtual_view_hit: None,
hit_depth: 0,
});
let mut hovered = BTreeMap::new();
hovered.insert(dom_id, HitTest {
regular_hit_test_nodes: regular,
scroll_hit_test_nodes: BTreeMap::new(),
scrollbar_hit_test_nodes: BTreeMap::new(),
cursor_hit_test_nodes: BTreeMap::new(),
});
FullHitTest {
hovered_nodes: hovered,
focused_node: OptionDomNodeId::None,
}
}
#[test]
fn mousedown_generates_text_selection_click() {
let target = focused_node(2);
let event = SyntheticEvent::new(
EventType::MouseDown,
EventSource::User,
target,
Instant::Tick(SystemTick::new(0)),
EventData::Mouse(MouseEventData {
position: LogicalPosition::new(100.0, 200.0),
button: crate::events::MouseButton::Left,
buttons: 1,
modifiers: KeyModifiers::default(),
}),
);
let hit_test = make_hit_test_with_node(2);
let kb = KeyboardState::default();
let mouse = MouseState::default();
let sel = MockSelectionManager { click_count: 1, has_sel: false };
let focus = MockFocusManager(Some(target));
let result = pre_callback_filter_internal_events(
&[event], Some(&hit_test), &kb, &mouse, &sel, &focus,
);
let click_changes: Vec<_> = result.system_changes.iter()
.filter(|c| matches!(c, SystemChange::TextSelectionClick { .. }))
.collect();
assert_eq!(click_changes.len(), 1, "MouseDown with hit_test should generate TextSelectionClick");
}
#[test]
fn process_event_result_max_self_picks_higher_variant() {
let lo = ProcessEventResult::ShouldReRenderCurrentWindow;
let hi = ProcessEventResult::ShouldRegenerateDomCurrentWindow;
assert_eq!(lo.max_self(hi), hi);
assert_eq!(hi.max_self(lo), hi);
assert_eq!(lo.max_self(lo), lo);
}
#[test]
fn arrow_direction_from_key_maps_arrows_and_home_end() {
use crate::window::VirtualKeyCode::*;
assert_eq!(ArrowDirection::from_key(Left, false), Some(ArrowDirection::Left));
assert_eq!(ArrowDirection::from_key(Right, false), Some(ArrowDirection::Right));
assert_eq!(ArrowDirection::from_key(Up, false), Some(ArrowDirection::Up));
assert_eq!(ArrowDirection::from_key(Down, false), Some(ArrowDirection::Down));
assert_eq!(ArrowDirection::from_key(Home, false), Some(ArrowDirection::LineStart));
assert_eq!(ArrowDirection::from_key(End, false), Some(ArrowDirection::LineEnd));
assert_eq!(ArrowDirection::from_key(Home, true), Some(ArrowDirection::DocumentStart));
assert_eq!(ArrowDirection::from_key(End, true), Some(ArrowDirection::DocumentEnd));
assert_eq!(ArrowDirection::from_key(C, false), None);
}
#[test]
fn arrow_direction_to_selection_respects_ctrl() {
let (d, s) = ArrowDirection::Left.to_selection(false);
assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Character));
let (d, s) = ArrowDirection::Left.to_selection(true);
assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::Word));
let (d, s) = ArrowDirection::Up.to_selection(false);
assert_eq!((d, s), (SelectionDirection::Backward, SelectionStep::VisualLine));
let (d, s) = ArrowDirection::DocumentEnd.to_selection(false);
assert_eq!((d, s), (SelectionDirection::Forward, SelectionStep::Document));
}
#[test]
fn keyboard_shortcut_from_key_recognizes_editing_combos() {
use crate::window::VirtualKeyCode::*;
assert_eq!(KeyboardShortcut::from_key(C, true, false), Some(KeyboardShortcut::Copy));
assert_eq!(KeyboardShortcut::from_key(X, true, false), Some(KeyboardShortcut::Cut));
assert_eq!(KeyboardShortcut::from_key(V, true, false), Some(KeyboardShortcut::Paste));
assert_eq!(KeyboardShortcut::from_key(A, true, false), Some(KeyboardShortcut::SelectAll));
assert_eq!(KeyboardShortcut::from_key(Z, true, false), Some(KeyboardShortcut::Undo));
assert_eq!(KeyboardShortcut::from_key(Z, true, true), Some(KeyboardShortcut::Redo));
assert_eq!(KeyboardShortcut::from_key(Y, true, false), Some(KeyboardShortcut::Redo));
assert_eq!(KeyboardShortcut::from_key(C, false, false), None);
assert_eq!(KeyboardShortcut::from_key(D, true, false), None);
}
#[test]
fn mouse_button_state_round_trips_from_mouse_state() {
let mut ms = MouseState::default();
ms.left_down = true;
ms.middle_down = true;
let bs: MouseButtonState = (&ms).into();
assert!(bs.left_down);
assert!(!bs.right_down);
assert!(bs.middle_down);
assert!(bs.any_down());
let none = MouseButtonState { left_down: false, right_down: false, middle_down: false };
assert!(!none.any_down());
}
#[test]
fn callback_to_call_collects_hits_for_dom() {
let dom_id = DomId { inner: 0 };
let hit_test = make_hit_test_with_node(2);
let filter = EventFilter::Hover(HoverEventFilter::MouseDown);
let calls = CallbackToCall::from_hit_test(&hit_test, dom_id, filter.clone());
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].node_id, NodeId::new(2));
assert_eq!(calls[0].event_filter, filter);
assert!(calls[0].hit_test_item.is_some());
let other = CallbackToCall::from_hit_test(
&hit_test,
DomId { inner: 999 },
EventFilter::Hover(HoverEventFilter::MouseUp),
);
assert!(other.is_empty());
let direct = CallbackToCall::new(
NodeId::new(7),
None,
EventFilter::Focus(FocusEventFilter::FocusReceived),
);
assert_eq!(direct.node_id, NodeId::new(7));
assert!(direct.hit_test_item.is_none());
}
#[test]
fn restyle_relayout_aliases_are_btreemap_compatible() {
let restyle: RestyleNodes = BTreeMap::new();
let relayout: RelayoutNodes = BTreeMap::new();
assert!(restyle.is_empty());
assert!(relayout.is_empty());
let mut words: RelayoutWords = BTreeMap::new();
words.insert(NodeId::new(1), AzString::from_const_str("hello"));
assert_eq!(words.get(&NodeId::new(1)).map(|s| s.as_str()), Some("hello"));
}
#[test]
fn detect_lifecycle_events_with_reconciliation_is_callable() {
let dom_id = DomId { inner: 0 };
let old_data: Vec<crate::dom::NodeData> = Vec::new();
let new_data: Vec<crate::dom::NodeData> = Vec::new();
let old_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
let new_hier: Vec<crate::styled_dom::NodeHierarchyItem> = Vec::new();
let old_layout = OrderedMap::default();
let new_layout = OrderedMap::default();
let result: LifecycleEventResult = detect_lifecycle_events_with_reconciliation(
dom_id,
&old_data,
&new_data,
&old_hier,
&new_hier,
&old_layout,
&new_layout,
Instant::Tick(SystemTick::new(0)),
);
assert!(result.events.is_empty());
assert!(result.node_id_mapping.is_empty());
}
#[test]
fn nodedata_focusable_and_activation_traits_are_wired() {
use crate::dom::{NodeData, NodeType};
use crate::events::{ActivationBehavior as _, Focusable as _};
let btn = NodeData::create_node(NodeType::Button);
assert!(<NodeData as Focusable>::is_naturally_focusable(&btn));
assert!(<NodeData as Focusable>::is_focusable(&btn));
assert!(<NodeData as ActivationBehavior>::has_activation_behavior(&btn));
assert!(<NodeData as ActivationBehavior>::is_activatable(&btn));
let div = NodeData::create_node(NodeType::Div);
assert!(!<NodeData as Focusable>::is_naturally_focusable(&div));
assert!(!<NodeData as ActivationBehavior>::has_activation_behavior(&div));
let input = NodeData::create_node(NodeType::Input);
assert!(<NodeData as Focusable>::is_naturally_focusable(&input));
}
}