use crate::debug::debug_message;
use crate::keys::KeyEventData;
use crate::keys::format_key_display;
use crate::message::{AsyncTaskRequest, CommandPaletteCommand, Message, MessageEvent};
use crate::node_id::{NodeId, node_id_to_ffi};
use crate::style::{Color, Scalar, Spacing, Tint};
use crate::worker::{CancellationToken, WorkerRequest, WorkerRequestPayload};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::collections::HashMap;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseDownEvent {
pub target: NodeId,
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseUpEvent {
pub target: Option<NodeId>,
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseMoveEvent {
pub target: NodeId,
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseScrollEvent {
pub target: Option<NodeId>,
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
pub delta_x: i32,
pub delta_y: i32,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseEnterEvent {
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseLeaveEvent {
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClickEvent {
pub screen_x: u16,
pub screen_y: u16,
pub x: u16,
pub y: u16,
pub button: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PasteEvent {
pub text: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MountEvent {
pub node: NodeId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnmountEvent {
pub node: NodeId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReadyEvent;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FocusEvent {
pub node: NodeId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlurEvent {
pub node: NodeId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationLevel {
None,
Basic,
Full,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnimationEase {
None,
Round,
Linear,
InOutCubic,
OutCubic,
InQuad,
OutQuad,
InOutQuad,
InCubic,
InQuart,
OutQuart,
InOutQuart,
InQuint,
OutQuint,
InOutQuint,
InExpo,
OutExpo,
InOutExpo,
InCirc,
OutCirc,
InOutCirc,
InBack,
OutBack,
InOutBack,
InBounce,
OutBounce,
InOutBounce,
InElastic,
OutElastic,
InOutElastic,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnimationRequest {
pub target: NodeId,
pub attribute: String,
pub start: f32,
pub end: f32,
pub duration: Duration,
pub delay: Duration,
pub ease: AnimationEase,
pub level: AnimationLevel,
}
impl AnimationRequest {
pub fn new(
target: NodeId,
attribute: impl Into<String>,
start: f32,
end: f32,
duration: Duration,
) -> Self {
Self {
target,
attribute: attribute.into(),
start,
end,
duration,
delay: Duration::ZERO,
ease: AnimationEase::InOutCubic,
level: AnimationLevel::Full,
}
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
pub fn with_ease(mut self, ease: AnimationEase) -> Self {
self.ease = ease;
self
}
pub fn with_level(mut self, level: AnimationLevel) -> Self {
self.level = level;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum StyleValue {
Color(Color),
Float(f32),
Scalar(Scalar),
Spacing(Spacing),
Tint(Tint),
}
#[derive(Debug, Clone, PartialEq)]
pub struct StyleAnimationRequest {
pub target: NodeId,
pub property: String,
pub from: StyleValue,
pub to: StyleValue,
pub duration: Duration,
pub delay: Duration,
pub ease: AnimationEase,
pub level: AnimationLevel,
}
impl StyleAnimationRequest {
pub fn new(
target: NodeId,
property: impl Into<String>,
from: StyleValue,
to: StyleValue,
duration: Duration,
) -> Self {
Self {
target,
property: property.into(),
from,
to,
duration,
delay: Duration::ZERO,
ease: AnimationEase::InOutCubic,
level: AnimationLevel::Full,
}
}
pub fn with_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}
pub fn with_ease(mut self, ease: AnimationEase) -> Self {
self.ease = ease;
self
}
pub fn with_level(mut self, level: AnimationLevel) -> Self {
self.level = level;
self
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AnimationValueEvent {
pub target: NodeId,
pub attribute: String,
pub value: f32,
pub done: bool,
}
#[derive(Debug, Clone)]
pub enum Event {
Key(KeyEventData),
Action(Action),
BindingsChanged(Vec<BindingHint>),
MouseDown(MouseDownEvent),
MouseUp(MouseUpEvent),
MouseMove(MouseMoveEvent),
MouseScroll(MouseScrollEvent),
Enter(MouseEnterEvent),
Leave(MouseLeaveEvent),
Click(ClickEvent),
Paste(PasteEvent),
Mount(MountEvent),
Unmount(UnmountEvent),
Ready(ReadyEvent),
Focus(FocusEvent),
Blur(BlurEvent),
AnimationValue(AnimationValueEvent),
AppFocus(bool),
Tick(u64),
Resize(u16, u16),
ScreenSuspend,
ScreenResume,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
FocusNext,
FocusPrev,
HelpQuit,
CopySelectedText,
ScrollHome,
ScrollEnd,
ScrollUp,
ScrollDown,
ScrollPageUp,
ScrollPageDown,
ScrollLeft,
ScrollRight,
ScrollPageLeft,
ScrollPageRight,
Toggle,
CommandPalette,
}
impl Action {
pub fn description(self) -> &'static str {
match self {
Action::FocusNext => "Focus next",
Action::FocusPrev => "Focus previous",
Action::HelpQuit => "Show quit help",
Action::CopySelectedText => "Copy selected text",
Action::ScrollHome => "Scroll home",
Action::ScrollEnd => "Scroll end",
Action::ScrollUp => "Scroll up",
Action::ScrollDown => "Scroll down",
Action::ScrollPageUp => "Page up",
Action::ScrollPageDown => "Page down",
Action::ScrollLeft => "Scroll left",
Action::ScrollRight => "Scroll right",
Action::ScrollPageLeft => "Page left",
Action::ScrollPageRight => "Page right",
Action::Toggle => "Toggle",
Action::CommandPalette => "Command palette",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyBind {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BindingHint {
pub key: String,
pub description: String,
pub tooltip: Option<String>,
pub namespace: Option<String>,
pub show: bool,
pub key_display: Option<String>,
pub group: Option<String>,
pub priority: bool,
pub system: bool,
pub action: Option<String>,
pub action_name: Option<String>,
pub action_parameters: Vec<String>,
pub enabled: Option<bool>,
}
impl BindingHint {
pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
Self {
key: key.into(),
description: description.into(),
tooltip: None,
namespace: None,
show: true,
key_display: None,
group: None,
priority: false,
system: false,
action: None,
action_name: None,
action_parameters: Vec::new(),
enabled: Some(true),
}
}
pub fn with_action(mut self, action: impl Into<String>) -> Self {
let action = action.into();
self.action = Some(action.clone());
if let Some(parsed) = crate::action::parse_action(&action) {
self.action_name = Some(parsed.name);
self.action_parameters = parsed.arguments;
} else {
self.action_name = Some(action);
self.action_parameters.clear();
}
self
}
pub fn hidden(mut self, hidden: bool) -> Self {
self.show = !hidden;
self
}
pub fn with_key_display(mut self, key_display: impl Into<String>) -> Self {
self.key_display = Some(key_display.into());
self
}
pub fn with_group(mut self, group: impl Into<String>) -> Self {
self.group = Some(group.into());
self
}
pub fn with_tooltip(mut self, tooltip: impl Into<String>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
self.namespace = Some(namespace.into());
self
}
pub fn with_priority(mut self, priority: bool) -> Self {
self.priority = priority;
self
}
pub fn with_system(mut self, system: bool) -> Self {
self.system = system;
self
}
}
impl KeyBind {
pub fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
Self { code, modifiers }
}
pub fn from_event(key: &KeyEventData) -> Self {
Self {
code: key.code,
modifiers: key.modifiers,
}
}
pub fn key_name(&self) -> String {
KeyEventData::from_crossterm(KeyEvent::new(self.code, self.modifiers)).key
}
pub fn display_key(&self) -> String {
format_key_display(&self.key_name())
}
}
#[derive(Debug, Default)]
pub struct ActionMap {
bindings: HashMap<KeyBind, Action>,
}
impl ActionMap {
pub fn new() -> Self {
Self::default()
}
pub fn bind(&mut self, key: KeyBind, action: Action) {
self.bindings.insert(key, action);
}
pub fn lookup(&self, key: &KeyBind) -> Option<Action> {
self.bindings.get(key).copied()
}
pub fn entries(&self) -> Vec<(KeyBind, Action)> {
self.bindings
.iter()
.map(|(bind, action)| (*bind, *action))
.collect()
}
}
#[derive(Debug, Default)]
pub struct EventCtx {
node_id: NodeId,
handled: bool,
repaint_requested: bool,
invalidation: InvalidationFlags,
stop_requested: bool,
messages: Vec<MessageEvent>,
animation_requests: Vec<AnimationRequest>,
style_animation_requests: Vec<StyleAnimationRequest>,
worker_requests: Vec<WorkerRequest>,
recompose_nodes: Vec<NodeId>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct InvalidationFlags {
pub content: bool,
pub style: bool,
pub layout: bool,
}
impl InvalidationFlags {
pub fn content() -> Self {
Self {
content: true,
style: false,
layout: false,
}
}
pub fn style() -> Self {
Self {
content: true,
style: true,
layout: false,
}
}
pub fn layout() -> Self {
Self {
content: true,
style: true,
layout: true,
}
}
pub fn merge(&mut self, other: Self) {
self.content |= other.content;
self.style |= other.style;
self.layout |= other.layout;
}
}
impl EventCtx {
pub fn node_id(&self) -> NodeId {
self.node_id
}
pub fn set_node_id(&mut self, id: NodeId) {
self.node_id = id;
}
pub fn handled(&self) -> bool {
self.handled
}
pub fn set_handled(&mut self) {
self.handled = true;
}
pub fn request_repaint(&mut self) {
self.repaint_requested = true;
self.invalidation.merge(InvalidationFlags::content());
}
pub fn repaint_requested(&self) -> bool {
self.repaint_requested
}
pub fn invalidation(&self) -> InvalidationFlags {
self.invalidation
}
pub fn request_style_invalidation(&mut self) {
self.repaint_requested = true;
self.invalidation.merge(InvalidationFlags::style());
}
pub fn request_layout_invalidation(&mut self) {
self.repaint_requested = true;
self.invalidation.merge(InvalidationFlags::layout());
}
pub fn request_recompose(&mut self) {
self.request_recompose_node(self.node_id);
}
pub fn request_recompose_node(&mut self, node_id: NodeId) {
if !self.recompose_nodes.contains(&node_id) {
self.recompose_nodes.push(node_id);
}
self.request_layout_invalidation();
}
pub fn request_stop(&mut self) {
self.stop_requested = true;
}
pub fn stop_requested(&self) -> bool {
self.stop_requested
}
pub fn post_message(&mut self, message: Message) {
debug_message(&format!(
"[post_message] sender={} payload={message:?}",
node_id_to_ffi(self.node_id)
));
self.messages.push(MessageEvent {
sender: self.node_id,
message,
control: Some(self.node_id),
});
}
pub fn spawn_async_task(&mut self, task_id: u64, target: NodeId, request: AsyncTaskRequest) {
self.post_message(Message::AsyncTaskSpawn(crate::message::AsyncTaskSpawn {
task_id,
target,
request,
}));
}
pub fn spawn_async_task_for(&mut self, task_id: u64, request: AsyncTaskRequest) {
let self_id = self.node_id;
self.spawn_async_task(task_id, self_id, request);
}
pub fn cancel_async_task(&mut self, task_id: u64) {
self.post_message(Message::AsyncTaskCancel(crate::message::AsyncTaskCancel {
task_id,
}));
}
pub fn cancel_async_tasks_for(&mut self, target: NodeId) {
self.post_message(Message::AsyncTaskCancelTarget(
crate::message::AsyncTaskCancelTarget { target },
));
}
pub fn schedule_timer(&mut self, timer_id: u64, target: NodeId, delay: Duration) {
self.post_message(Message::TimerSchedule(crate::message::TimerSchedule {
timer_id,
target,
delay,
}));
}
pub fn schedule_timer_for(&mut self, timer_id: u64, delay: Duration) {
let self_id = self.node_id;
self.schedule_timer(timer_id, self_id, delay);
}
pub fn cancel_timer(&mut self, timer_id: u64) {
self.post_message(Message::TimerCancel(crate::message::TimerCancel {
timer_id,
}));
}
pub fn set_overlay_visible(&mut self, overlay: NodeId, visible: bool) {
self.post_message(Message::OverlaySetVisible(
crate::message::OverlaySetVisible { overlay, visible },
));
}
pub fn show_overlay(&mut self, overlay: NodeId) {
self.set_overlay_visible(overlay, true);
}
pub fn hide_overlay(&mut self, overlay: NodeId) {
self.set_overlay_visible(overlay, false);
}
pub fn toggle_overlay(&mut self, overlay: NodeId) {
self.post_message(Message::OverlayToggle(crate::message::OverlayToggle {
overlay,
}));
}
pub fn dismiss_overlay(&mut self, overlay: Option<NodeId>) {
self.post_message(Message::OverlayDismissRequested(
crate::message::OverlayDismissRequested { overlay },
));
}
pub fn open_command_palette(&mut self) {
self.post_message(Message::CommandPaletteOpened(
crate::message::CommandPaletteOpened,
));
}
pub fn close_command_palette(&mut self) {
self.post_message(Message::CommandPaletteClosed(
crate::message::CommandPaletteClosed,
));
}
pub fn set_command_palette_commands(&mut self, commands: Vec<CommandPaletteCommand>) {
self.post_message(Message::CommandPaletteSetCommands(
crate::message::CommandPaletteSetCommands { commands },
));
}
pub fn select_command_palette_command(
&mut self,
id: impl Into<String>,
title: impl Into<String>,
) {
self.post_message(Message::CommandPaletteCommandSelected(
crate::message::CommandPaletteCommandSelected {
id: id.into(),
title: title.into(),
},
));
}
pub fn request_animation(&mut self, request: AnimationRequest) {
debug_message(&format!(
"[request_animation] target={} attribute={} start={} end={} duration_ms={} delay_ms={} ease={:?} level={:?}",
node_id_to_ffi(request.target),
request.attribute,
request.start,
request.end,
request.duration.as_millis(),
request.delay.as_millis(),
request.ease,
request.level
));
self.animation_requests.push(request);
}
pub fn animate_style(
&mut self,
target: NodeId,
property: impl Into<String>,
from: StyleValue,
to: StyleValue,
duration: Duration,
ease: AnimationEase,
) {
let request =
StyleAnimationRequest::new(target, property, from, to, duration).with_ease(ease);
self.request_style_animation(request);
}
pub fn request_style_animation(&mut self, request: StyleAnimationRequest) {
debug_message(&format!(
"[request_style_animation] target={} property={} duration_ms={} ease={:?}",
node_id_to_ffi(request.target),
request.property,
request.duration.as_millis(),
request.ease
));
self.style_animation_requests.push(request);
}
pub fn request_worker(&mut self, name: Option<&str>) {
self.request_worker_with_payload(name, WorkerRequestPayload::default());
}
pub fn request_worker_with_payload(
&mut self,
name: Option<&str>,
payload: WorkerRequestPayload,
) {
self.worker_requests.push(WorkerRequest {
owner: self.node_id,
exclusive_key: None,
name: name.map(|s| s.to_string()),
payload,
});
}
pub fn request_exclusive_worker(&mut self, key: &str, name: Option<&str>) {
self.request_exclusive_worker_with_payload(key, name, WorkerRequestPayload::default());
}
pub fn request_exclusive_worker_with_payload(
&mut self,
key: &str,
name: Option<&str>,
payload: WorkerRequestPayload,
) {
self.worker_requests.push(WorkerRequest {
owner: self.node_id,
exclusive_key: Some(key.to_string()),
name: name.map(|s| s.to_string()),
payload,
});
}
pub fn request_worker_task(
&mut self,
name: Option<&str>,
task: impl FnOnce(CancellationToken) -> Result<(), String> + Send + 'static,
) {
self.request_worker_with_payload(name, WorkerRequestPayload::task(task));
}
pub fn request_exclusive_worker_task(
&mut self,
key: &str,
name: Option<&str>,
task: impl FnOnce(CancellationToken) -> Result<(), String> + Send + 'static,
) {
self.request_exclusive_worker_with_payload(key, name, WorkerRequestPayload::task(task));
}
pub(crate) fn take_worker_requests(&mut self) -> Vec<WorkerRequest> {
std::mem::take(&mut self.worker_requests)
}
pub(crate) fn take_recompose_nodes(&mut self) -> Vec<NodeId> {
std::mem::take(&mut self.recompose_nodes)
}
pub(crate) fn merge_from(&mut self, mut other: EventCtx) {
if other.handled {
self.handled = true;
}
if other.repaint_requested {
self.repaint_requested = true;
}
self.invalidation.merge(other.invalidation);
if other.stop_requested {
self.stop_requested = true;
}
self.messages.append(&mut other.messages);
self.animation_requests
.append(&mut other.animation_requests);
self.style_animation_requests
.append(&mut other.style_animation_requests);
self.worker_requests.append(&mut other.worker_requests);
for node_id in other.recompose_nodes.drain(..) {
if !self.recompose_nodes.contains(&node_id) {
self.recompose_nodes.push(node_id);
}
}
}
pub(crate) fn take_messages(&mut self) -> Vec<MessageEvent> {
std::mem::take(&mut self.messages)
}
pub(crate) fn take_animation_requests(&mut self) -> Vec<AnimationRequest> {
std::mem::take(&mut self.animation_requests)
}
#[allow(dead_code)]
pub(crate) fn take_style_animation_requests(&mut self) -> Vec<StyleAnimationRequest> {
std::mem::take(&mut self.style_animation_requests)
}
}
#[derive(Debug)]
pub struct WidgetCtx<'a> {
node_id: NodeId,
event_ctx: &'a mut EventCtx,
}
impl<'a> WidgetCtx<'a> {
#[allow(dead_code)]
pub(crate) fn new(node_id: NodeId, event_ctx: &'a mut EventCtx) -> Self {
Self { node_id, event_ctx }
}
#[inline]
pub fn node_id(&self) -> NodeId {
self.node_id
}
#[inline]
pub fn event_ctx(&self) -> &EventCtx {
self.event_ctx
}
#[inline]
pub fn event_ctx_mut(&mut self) -> &mut EventCtx {
self.event_ctx
}
#[inline]
pub fn set_handled(&mut self) {
self.event_ctx.set_handled();
}
#[inline]
pub fn request_repaint(&mut self) {
self.event_ctx.request_repaint();
}
#[inline]
pub fn request_stop(&mut self) {
self.event_ctx.request_stop();
}
#[inline]
pub fn post_message(&mut self, message: Message) {
self.event_ctx.set_node_id(self.node_id);
self.event_ctx.post_message(message);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::message::{AsyncTaskRequest, CommandPaletteCommand, Message};
use crate::node_id::node_id_from_ffi;
use crate::style::{Color, Scalar, Spacing, Tint};
use std::time::Duration;
#[test]
fn helper_methods_emit_runtime_control_messages() {
let sender_id = node_id_from_ffi(12);
let mut ctx = EventCtx::default();
ctx.set_node_id(sender_id);
ctx.spawn_async_task_for(
5,
AsyncTaskRequest::Sleep {
duration: Duration::from_millis(10),
label: "work".to_string(),
},
);
ctx.schedule_timer_for(9, Duration::from_millis(25));
ctx.cancel_async_task(5);
ctx.cancel_timer(9);
let messages = ctx.take_messages();
assert_eq!(messages.len(), 4);
assert!(matches!(
&messages[0].message,
Message::AsyncTaskSpawn(crate::message::AsyncTaskSpawn {
task_id,
target,
request: AsyncTaskRequest::Sleep { label, .. },
}) if *task_id == 5 && *target == sender_id && label == "work"
));
assert!(matches!(
messages[1].message,
Message::TimerSchedule(crate::message::TimerSchedule {
timer_id,
target,
..
}) if timer_id == 9 && target == sender_id
));
assert!(matches!(
messages[2].message,
Message::AsyncTaskCancel(crate::message::AsyncTaskCancel { task_id }) if task_id == 5
));
assert!(matches!(
messages[3].message,
Message::TimerCancel(crate::message::TimerCancel { timer_id }) if timer_id == 9
));
}
#[test]
fn overlay_and_command_palette_helpers_emit_messages() {
let overlay_id = node_id_from_ffi(77);
let mut ctx = EventCtx::default();
ctx.set_node_id(node_id_from_ffi(5));
ctx.show_overlay(overlay_id);
ctx.hide_overlay(overlay_id);
ctx.toggle_overlay(overlay_id);
ctx.dismiss_overlay(Some(overlay_id));
ctx.open_command_palette();
ctx.set_command_palette_commands(vec![CommandPaletteCommand {
id: "open".to_string(),
title: "Open".to_string(),
help: "Open file".to_string(),
}]);
ctx.select_command_palette_command("open", "Open");
ctx.close_command_palette();
let messages = ctx.take_messages();
assert_eq!(messages.len(), 8);
assert!(matches!(
messages[0].message,
Message::OverlaySetVisible(crate::message::OverlaySetVisible {
overlay: target,
visible: true
}) if target == overlay_id
));
assert!(matches!(
messages[1].message,
Message::OverlaySetVisible(crate::message::OverlaySetVisible {
overlay: target,
visible: false
}) if target == overlay_id
));
assert!(matches!(
messages[2].message,
Message::OverlayToggle(crate::message::OverlayToggle { overlay: target }) if target == overlay_id
));
assert!(matches!(
messages[3].message,
Message::OverlayDismissRequested(crate::message::OverlayDismissRequested { overlay: Some(target) }) if target == overlay_id
));
assert!(matches!(
messages[4].message,
Message::CommandPaletteOpened(_)
));
assert!(matches!(
&messages[5].message,
Message::CommandPaletteSetCommands(crate::message::CommandPaletteSetCommands { commands })
if commands.len() == 1 && commands[0].id == "open"
));
assert!(matches!(
&messages[6].message,
Message::CommandPaletteCommandSelected(crate::message::CommandPaletteCommandSelected { id, title }) if id == "open" && title == "Open"
));
assert!(matches!(
messages[7].message,
Message::CommandPaletteClosed(_)
));
}
#[test]
fn mouse_enter_event_construction() {
let e = MouseEnterEvent {
x: 5,
y: 10,
screen_x: 20,
screen_y: 30,
};
assert_eq!(e.x, 5);
assert_eq!(e.y, 10);
assert_eq!(e.screen_x, 20);
assert_eq!(e.screen_y, 30);
let ev = Event::Enter(e);
assert!(matches!(
ev,
Event::Enter(MouseEnterEvent { x: 5, y: 10, .. })
));
}
#[test]
fn mouse_leave_event_construction() {
let e = MouseLeaveEvent {
x: 1,
y: 2,
screen_x: 3,
screen_y: 4,
};
let ev = Event::Leave(e);
assert!(matches!(
ev,
Event::Leave(MouseLeaveEvent {
x: 1,
y: 2,
screen_x: 3,
screen_y: 4
})
));
}
#[test]
fn click_event_construction() {
let e = ClickEvent {
x: 10,
y: 20,
screen_x: 50,
screen_y: 60,
button: 0,
};
assert_eq!(e.button, 0);
let ev = Event::Click(e);
assert!(matches!(ev, Event::Click(ClickEvent { button: 0, .. })));
}
#[test]
fn click_event_right_button() {
let e = ClickEvent {
x: 0,
y: 0,
screen_x: 0,
screen_y: 0,
button: 2,
};
assert_eq!(e.button, 2);
}
#[test]
fn paste_event_construction() {
let e = PasteEvent {
text: "hello world".to_string(),
};
assert_eq!(e.text, "hello world");
let ev = Event::Paste(e);
assert!(matches!(ev, Event::Paste(PasteEvent { .. })));
}
#[test]
fn paste_event_empty_text() {
let e = PasteEvent {
text: String::new(),
};
assert!(e.text.is_empty());
}
#[test]
fn mount_event_construction() {
let id = node_id_from_ffi(42);
let e = MountEvent { node: id };
assert_eq!(e.node, id);
let ev = Event::Mount(e);
assert!(matches!(ev, Event::Mount(MountEvent { node }) if node == id));
}
#[test]
fn unmount_event_construction() {
let id = node_id_from_ffi(7);
let e = UnmountEvent { node: id };
assert_eq!(e.node, id);
let ev = Event::Unmount(e);
assert!(matches!(ev, Event::Unmount(UnmountEvent { node }) if node == id));
}
#[test]
fn ready_event_construction() {
let e = ReadyEvent;
let ev = Event::Ready(e);
assert!(matches!(ev, Event::Ready(ReadyEvent)));
}
#[test]
fn focus_event_construction() {
let id = node_id_from_ffi(99);
let e = FocusEvent { node: id };
assert_eq!(e.node, id);
let ev = Event::Focus(e);
assert!(matches!(ev, Event::Focus(FocusEvent { node }) if node == id));
}
#[test]
fn blur_event_construction() {
let id = node_id_from_ffi(55);
let e = BlurEvent { node: id };
assert_eq!(e.node, id);
let ev = Event::Blur(e);
assert!(matches!(ev, Event::Blur(BlurEvent { node }) if node == id));
}
#[test]
fn style_value_color_construction() {
let v = StyleValue::Color(Color::rgb(10, 20, 30));
assert!(matches!(
v,
StyleValue::Color(Color {
r: 10,
g: 20,
b: 30,
a: 255
})
));
}
#[test]
fn style_value_float_construction() {
let v = StyleValue::Float(50.0);
assert!(matches!(v, StyleValue::Float(x) if (x - 50.0).abs() < 0.001));
}
#[test]
fn style_value_scalar_construction() {
let v = StyleValue::Scalar(Scalar::Cells(42));
assert!(matches!(v, StyleValue::Scalar(Scalar::Cells(42))));
}
#[test]
fn style_value_spacing_construction() {
let v = StyleValue::Spacing(Spacing::all(5));
if let StyleValue::Spacing(s) = v {
assert_eq!(s.top, 5);
assert_eq!(s.right, 5);
} else {
panic!("expected Spacing");
}
}
#[test]
fn style_value_tint_construction() {
let v = StyleValue::Tint(Tint::new(Color::rgb(255, 0, 0), 50));
if let StyleValue::Tint(t) = v {
assert_eq!(t.color, Color::rgb(255, 0, 0));
assert_eq!(t.percent, 50);
} else {
panic!("expected Tint");
}
}
#[test]
fn style_animation_request_builder() {
let target = node_id_from_ffi(10);
let req = StyleAnimationRequest::new(
target,
"bg",
StyleValue::Color(Color::rgb(0, 0, 0)),
StyleValue::Color(Color::rgb(255, 255, 255)),
Duration::from_millis(300),
)
.with_delay(Duration::from_millis(50))
.with_ease(AnimationEase::Linear)
.with_level(AnimationLevel::Basic);
assert_eq!(req.target, target);
assert_eq!(req.property, "bg");
assert_eq!(req.duration, Duration::from_millis(300));
assert_eq!(req.delay, Duration::from_millis(50));
assert_eq!(req.ease, AnimationEase::Linear);
assert_eq!(req.level, AnimationLevel::Basic);
}
#[test]
fn event_ctx_animate_style_populates_requests() {
let target = node_id_from_ffi(20);
let mut ctx = EventCtx::default();
ctx.set_node_id(target);
ctx.animate_style(
target,
"opacity",
StyleValue::Float(0.0),
StyleValue::Float(100.0),
Duration::from_millis(500),
AnimationEase::OutCubic,
);
let requests = ctx.take_style_animation_requests();
assert_eq!(requests.len(), 1);
assert_eq!(requests[0].property, "opacity");
assert_eq!(requests[0].ease, AnimationEase::OutCubic);
}
#[test]
fn event_ctx_merge_includes_style_animation_requests() {
let mut a = EventCtx::default();
a.set_node_id(node_id_from_ffi(1));
let mut b = EventCtx::default();
b.set_node_id(node_id_from_ffi(2));
let target = node_id_from_ffi(10);
a.request_style_animation(StyleAnimationRequest::new(
target,
"fg",
StyleValue::Color(Color::rgb(0, 0, 0)),
StyleValue::Color(Color::rgb(255, 0, 0)),
Duration::from_millis(200),
));
b.request_style_animation(StyleAnimationRequest::new(
target,
"bg",
StyleValue::Color(Color::rgb(0, 0, 0)),
StyleValue::Color(Color::rgb(0, 255, 0)),
Duration::from_millis(300),
));
a.merge_from(b);
let requests = a.take_style_animation_requests();
assert_eq!(requests.len(), 2);
assert_eq!(requests[0].property, "fg");
assert_eq!(requests[1].property, "bg");
}
#[test]
fn animation_ease_has_all_variants() {
let variants = [
AnimationEase::None,
AnimationEase::Round,
AnimationEase::Linear,
AnimationEase::InOutCubic,
AnimationEase::OutCubic,
AnimationEase::InQuad,
AnimationEase::OutQuad,
AnimationEase::InOutQuad,
AnimationEase::InCubic,
AnimationEase::InQuart,
AnimationEase::OutQuart,
AnimationEase::InOutQuart,
AnimationEase::InQuint,
AnimationEase::OutQuint,
AnimationEase::InOutQuint,
AnimationEase::InExpo,
AnimationEase::OutExpo,
AnimationEase::InOutExpo,
AnimationEase::InCirc,
AnimationEase::OutCirc,
AnimationEase::InOutCirc,
AnimationEase::InBack,
AnimationEase::OutBack,
AnimationEase::InOutBack,
AnimationEase::InBounce,
AnimationEase::OutBounce,
AnimationEase::InOutBounce,
AnimationEase::InElastic,
AnimationEase::OutElastic,
AnimationEase::InOutElastic,
];
assert_eq!(variants.len(), 30);
}
#[test]
fn event_ctx_request_worker() {
let owner = node_id_from_ffi(10);
let mut ctx = EventCtx::default();
ctx.set_node_id(owner);
ctx.request_worker(Some("bg-fetch"));
let reqs = ctx.take_worker_requests();
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].owner, owner);
assert!(reqs[0].exclusive_key.is_none());
assert_eq!(reqs[0].name.as_deref(), Some("bg-fetch"));
}
#[test]
fn event_ctx_request_exclusive_worker() {
let owner = node_id_from_ffi(11);
let mut ctx = EventCtx::default();
ctx.set_node_id(owner);
ctx.request_exclusive_worker("search", Some("search-worker"));
let reqs = ctx.take_worker_requests();
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].owner, owner);
assert_eq!(reqs[0].exclusive_key.as_deref(), Some("search"));
assert_eq!(reqs[0].name.as_deref(), Some("search-worker"));
}
#[test]
fn event_ctx_request_worker_with_payload() {
let owner = node_id_from_ffi(12);
let mut ctx = EventCtx::default();
ctx.set_node_id(owner);
ctx.request_worker_with_payload(
Some("digest"),
WorkerRequestPayload::ComputeDigest {
input: "abc".into(),
rounds: 2,
delay_per_round_ms: 0,
fail_with: None,
},
);
let reqs = ctx.take_worker_requests();
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].owner, owner);
assert!(matches!(
reqs[0].payload,
WorkerRequestPayload::ComputeDigest { rounds: 2, .. }
));
}
#[test]
fn event_ctx_request_worker_task_uses_task_payload() {
let mut ctx = EventCtx::default();
ctx.set_node_id(node_id_from_ffi(13));
ctx.request_worker_task(Some("task"), |_token| Ok(()));
let reqs = ctx.take_worker_requests();
assert_eq!(reqs.len(), 1);
assert!(matches!(reqs[0].payload, WorkerRequestPayload::Task(_)));
}
#[test]
fn event_ctx_take_worker_requests_drains() {
let mut ctx = EventCtx::default();
ctx.set_node_id(node_id_from_ffi(1));
ctx.request_worker(None);
ctx.request_worker(None);
let reqs = ctx.take_worker_requests();
assert_eq!(reqs.len(), 2);
let reqs2 = ctx.take_worker_requests();
assert!(reqs2.is_empty());
}
#[test]
fn event_ctx_merge_includes_worker_requests() {
let mut a = EventCtx::default();
a.set_node_id(node_id_from_ffi(1));
a.request_worker(Some("a"));
let mut b = EventCtx::default();
b.set_node_id(node_id_from_ffi(2));
b.request_worker(Some("b"));
a.merge_from(b);
let reqs = a.take_worker_requests();
assert_eq!(reqs.len(), 2);
assert_eq!(reqs[0].name.as_deref(), Some("a"));
assert_eq!(reqs[1].name.as_deref(), Some("b"));
}
#[test]
fn post_message_sets_control_to_sender() {
let sender_id = node_id_from_ffi(42);
let mut ctx = EventCtx::default();
ctx.set_node_id(sender_id);
ctx.post_message(Message::ClearRequested(crate::message::ClearRequested));
let messages = ctx.take_messages();
assert_eq!(messages.len(), 1);
assert_eq!(messages[0].sender, sender_id);
assert_eq!(
messages[0].control,
Some(sender_id),
"post_message should set control to Some(sender)"
);
}
}