use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq)]
pub enum CoalesceHint {
Replace,
Accumulate(Vec<String>),
}
#[derive(Debug, Serialize)]
pub struct OutgoingEvent {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub family: String,
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub modifiers: Option<KeyModifiers>,
#[serde(skip_serializing_if = "Option::is_none")]
pub captured: Option<bool>,
#[serde(skip)]
pub(crate) coalesce: Option<CoalesceHint>,
}
impl OutgoingEvent {
pub fn with_captured(mut self, captured: bool) -> Self {
self.captured = Some(captured);
self
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
pub fn with_coalesce(mut self, hint: CoalesceHint) -> Self {
self.coalesce = Some(hint);
self
}
pub fn coalesce_hint(&self) -> Option<&CoalesceHint> {
self.coalesce.as_ref()
}
pub fn take_coalesce(&mut self) -> Option<CoalesceHint> {
self.coalesce.take()
}
pub fn with_value(mut self, value: Value) -> Self {
self.value = Some(value);
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct KeyModifiers {
#[serde(default)]
pub shift: bool,
#[serde(default)]
pub ctrl: bool,
#[serde(default)]
pub alt: bool,
#[serde(default)]
pub logo: bool,
#[serde(default)]
pub command: bool,
}
impl OutgoingEvent {
fn bare(family: impl Into<String>, id: impl Into<String>) -> Self {
Self {
message_type: "event",
session: String::new(),
family: family.into(),
id: id.into(),
value: None,
tag: None,
modifiers: None,
captured: None,
coalesce: None,
}
}
pub fn tagged(family: impl Into<String>, tag: impl Into<String>) -> Self {
Self {
message_type: "event",
session: String::new(),
family: family.into(),
id: String::new(),
value: None,
tag: Some(tag.into()),
modifiers: None,
captured: None,
coalesce: None,
}
}
pub fn generic(family: impl Into<String>, id: impl Into<String>, value: Option<Value>) -> Self {
Self {
value,
..Self::bare(family, id)
}
}
pub fn widget_event(
family: impl Into<String>,
id: impl Into<String>,
value: Option<Value>,
) -> Self {
let family = family.into();
crate::EventType::assert_custom_family(&family);
Self::generic(family, id, value)
}
pub fn click(id: impl Into<String>) -> Self {
Self::bare("click", id)
}
pub fn input(id: impl Into<String>, value: impl Into<String>) -> Self {
Self {
value: Some(Value::String(value.into())),
..Self::bare("input", id)
}
}
pub fn submit(id: impl Into<String>, value: impl Into<String>) -> Self {
Self {
value: Some(Value::String(value.into())),
..Self::bare("submit", id)
}
}
pub fn toggle(id: impl Into<String>, checked: bool) -> Self {
Self {
value: Some(Value::Bool(checked)),
..Self::bare("toggle", id)
}
}
pub fn slide(id: impl Into<String>, value: f64) -> Self {
Self {
value: Some(serde_json::json!(sanitize_f64(value))),
coalesce: Some(CoalesceHint::Replace),
..Self::bare("slide", id)
}
}
pub fn slide_release(id: impl Into<String>, value: f64) -> Self {
Self {
value: Some(serde_json::json!(sanitize_f64(value))),
..Self::bare("slide_release", id)
}
}
pub fn select(id: impl Into<String>, value: impl Into<String>) -> Self {
Self {
value: Some(Value::String(value.into())),
..Self::bare("select", id)
}
}
pub fn modifiers_changed(tag: impl Into<String>, modifiers: KeyModifiers) -> Self {
Self {
modifiers: Some(modifiers),
coalesce: Some(CoalesceHint::Replace),
..Self::tagged("modifiers_changed", tag)
}
}
pub fn cursor_moved(tag: impl Into<String>, x: f32, y: f32) -> Self {
Self {
value: Some(serde_json::json!({"x": sanitize_f32(x), "y": sanitize_f32(y)})),
coalesce: Some(CoalesceHint::Replace),
..Self::tagged("cursor_moved", tag)
}
}
pub fn cursor_entered(tag: impl Into<String>) -> Self {
Self::tagged("cursor_entered", tag)
}
pub fn cursor_left(tag: impl Into<String>) -> Self {
Self::tagged("cursor_left", tag)
}
pub fn button_pressed(tag: impl Into<String>, button: impl Into<String>) -> Self {
Self {
value: Some(Value::String(button.into())),
..Self::tagged("button_pressed", tag)
}
}
pub fn button_released(tag: impl Into<String>, button: impl Into<String>) -> Self {
Self {
value: Some(Value::String(button.into())),
..Self::tagged("button_released", tag)
}
}
pub fn wheel_scrolled(tag: impl Into<String>, delta_x: f32, delta_y: f32, unit: &str) -> Self {
Self {
value: Some(serde_json::json!({
"delta_x": sanitize_f32(delta_x),
"delta_y": sanitize_f32(delta_y),
"unit": unit,
})),
coalesce: Some(CoalesceHint::Accumulate(vec![
"delta_x".into(),
"delta_y".into(),
])),
..Self::tagged("wheel_scrolled", tag)
}
}
fn touch_event(family: &str, tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
Self {
value: Some(serde_json::json!({
"id": finger_id,
"x": sanitize_f32(x),
"y": sanitize_f32(y),
})),
..Self::tagged(family, tag)
}
}
pub fn finger_pressed(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
Self::touch_event("finger_pressed", tag, finger_id, x, y)
}
pub fn finger_moved(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
Self {
coalesce: Some(CoalesceHint::Replace),
..Self::touch_event("finger_moved", tag, finger_id, x, y)
}
}
pub fn finger_lifted(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
Self::touch_event("finger_lifted", tag, finger_id, x, y)
}
pub fn finger_lost(tag: impl Into<String>, finger_id: u64, x: f32, y: f32) -> Self {
Self::touch_event("finger_lost", tag, finger_id, x, y)
}
pub fn ime_opened(tag: impl Into<String>) -> Self {
Self::tagged("ime_opened", tag)
}
pub fn ime_preedit(
tag: impl Into<String>,
text: impl Into<String>,
cursor: Option<std::ops::Range<usize>>,
) -> Self {
let cursor_val = cursor
.map(|r| serde_json::json!({"start": r.start, "end": r.end}))
.unwrap_or(serde_json::Value::Null);
Self {
value: Some(serde_json::json!({"text": text.into(), "cursor": cursor_val})),
..Self::tagged("ime_preedit", tag)
}
}
pub fn ime_commit(tag: impl Into<String>, text: impl Into<String>) -> Self {
Self {
value: Some(serde_json::json!({"text": text.into()})),
..Self::tagged("ime_commit", tag)
}
}
pub fn ime_closed(tag: impl Into<String>) -> Self {
Self::tagged("ime_closed", tag)
}
pub fn window_opened(
tag: impl Into<String>,
window_id: impl Into<String>,
position: Option<(f32, f32)>,
width: f32,
height: f32,
scale_factor: f32,
) -> Self {
let mut value = serde_json::json!({
"window_id": window_id.into(),
"width": sanitize_f32(width),
"height": sanitize_f32(height),
"scale_factor": sanitize_f32(scale_factor),
});
if let Some((x, y)) = position {
value["x"] = serde_json::json!(sanitize_f32(x));
value["y"] = serde_json::json!(sanitize_f32(y));
}
Self {
value: Some(value),
..Self::tagged("window_opened", tag)
}
}
fn window_event(family: &str, tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self {
value: Some(serde_json::json!({"window_id": window_id.into()})),
..Self::tagged(family, tag)
}
}
pub fn window_closed(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self::window_event("window_closed", tag, window_id)
}
pub fn window_close_requested(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self::window_event("window_close_requested", tag, window_id)
}
pub fn window_moved(
tag: impl Into<String>,
window_id: impl Into<String>,
x: f32,
y: f32,
) -> Self {
Self {
value: Some(serde_json::json!({
"window_id": window_id.into(),
"x": sanitize_f32(x),
"y": sanitize_f32(y),
})),
..Self::tagged("window_moved", tag)
}
}
pub fn window_resized(
tag: impl Into<String>,
window_id: impl Into<String>,
width: f32,
height: f32,
) -> Self {
Self {
value: Some(serde_json::json!({
"window_id": window_id.into(),
"width": sanitize_f32(width),
"height": sanitize_f32(height),
})),
..Self::tagged("window_resized", tag)
}
}
pub fn window_focused(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self::window_event("window_focused", tag, window_id)
}
pub fn window_unfocused(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self::window_event("window_unfocused", tag, window_id)
}
pub fn window_rescaled(
tag: impl Into<String>,
window_id: impl Into<String>,
scale_factor: f32,
) -> Self {
Self {
value: Some(serde_json::json!({
"window_id": window_id.into(),
"scale_factor": sanitize_f32(scale_factor),
})),
..Self::tagged("window_rescaled", tag)
}
}
pub fn file_hovered(
tag: impl Into<String>,
window_id: impl Into<String>,
path: impl Into<String>,
) -> Self {
Self {
value: Some(serde_json::json!({
"window_id": window_id.into(),
"path": path.into(),
})),
..Self::tagged("file_hovered", tag)
}
}
pub fn file_dropped(
tag: impl Into<String>,
window_id: impl Into<String>,
path: impl Into<String>,
) -> Self {
Self {
value: Some(serde_json::json!({
"window_id": window_id.into(),
"path": path.into(),
})),
..Self::tagged("file_dropped", tag)
}
}
pub fn files_hovered_left(tag: impl Into<String>, window_id: impl Into<String>) -> Self {
Self::window_event("files_hovered_left", tag, window_id)
}
pub fn animation_frame(tag: impl Into<String>, timestamp_millis: u64) -> Self {
Self {
value: Some(serde_json::json!({"timestamp": timestamp_millis})),
coalesce: Some(CoalesceHint::Replace),
..Self::tagged("animation_frame", tag)
}
}
pub fn theme_changed(tag: impl Into<String>, mode: impl Into<String>) -> Self {
Self {
value: Some(Value::String(mode.into())),
coalesce: Some(CoalesceHint::Replace),
..Self::tagged("theme_changed", tag)
}
}
pub fn diagnostic(
canvas_id: impl Into<String>,
element_id: Option<String>,
level: &str,
code: &str,
message: &str,
) -> Self {
Self {
value: Some(serde_json::json!({
"level": level,
"element_id": element_id,
"code": code,
"message": message,
})),
..Self::bare("diagnostic", canvas_id)
}
}
pub fn pane_resized(id: impl Into<String>, split: impl Into<String>, ratio: f32) -> Self {
Self {
value: Some(serde_json::json!({"split": split.into(), "ratio": sanitize_f32(ratio)})),
coalesce: Some(CoalesceHint::Replace),
..Self::bare("pane_resized", id)
}
}
pub fn pane_dragged(
id: impl Into<String>,
kind: &str,
pane: impl Into<String>,
target: Option<String>,
region: Option<&str>,
edge: Option<&str>,
) -> Self {
let mut val = serde_json::json!({"action": kind, "pane": pane.into()});
if let Some(t) = target {
val["target"] = serde_json::json!(t);
}
if let Some(r) = region {
val["region"] = serde_json::json!(r);
}
if let Some(e) = edge {
val["edge"] = serde_json::json!(e);
}
Self {
value: Some(val),
..Self::bare("pane_dragged", id)
}
}
pub fn pane_clicked(id: impl Into<String>, pane: impl Into<String>) -> Self {
Self {
value: Some(serde_json::json!({"pane": pane.into()})),
..Self::bare("pane_clicked", id)
}
}
pub fn pane_focus_cycle(id: impl Into<String>, pane: impl Into<String>) -> Self {
Self {
value: Some(serde_json::json!({"pane": pane.into()})),
..Self::bare("pane_focus_cycle", id)
}
}
pub fn paste(id: impl Into<String>, text: impl Into<String>) -> Self {
Self {
value: Some(Value::String(text.into())),
..Self::bare("paste", id)
}
}
pub fn scripting_key_press(key: impl Into<String>, modifiers_json: Value) -> Self {
let mods: KeyModifiers =
serde_json::from_value(modifiers_json).unwrap_or(KeyModifiers::default());
Self {
modifiers: Some(mods),
value: Some(serde_json::json!({"key": key.into()})),
..Self::bare("key_press", String::new())
}
}
pub fn scripting_key_release(key: impl Into<String>, modifiers_json: Value) -> Self {
let mods: KeyModifiers =
serde_json::from_value(modifiers_json).unwrap_or(KeyModifiers::default());
Self {
modifiers: Some(mods),
value: Some(serde_json::json!({"key": key.into()})),
..Self::bare("key_release", String::new())
}
}
pub fn scripting_cursor_moved(x: f32, y: f32) -> Self {
Self {
value: Some(serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
})),
..Self::bare("cursor_moved", String::new())
}
}
pub fn scripting_scroll(delta_x: f32, delta_y: f32) -> Self {
Self {
value: Some(serde_json::json!({
"delta_x": sanitize_f32(delta_x),
"delta_y": sanitize_f32(delta_y),
"unit": "pixel",
})),
..Self::bare("wheel_scrolled", String::new())
}
}
pub fn option_hovered(id: impl Into<String>, value: impl Into<String>) -> Self {
Self {
value: Some(Value::String(value.into())),
..Self::bare("option_hovered", id)
}
}
#[allow(clippy::too_many_arguments)]
pub fn scroll(
id: impl Into<String>,
abs_x: f32,
abs_y: f32,
rel_x: f32,
rel_y: f32,
bounds_w: f32,
bounds_h: f32,
content_w: f32,
content_h: f32,
) -> Self {
Self {
value: Some(serde_json::json!({
"absolute_x": sanitize_f32(abs_x), "absolute_y": sanitize_f32(abs_y),
"relative_x": sanitize_f32(rel_x), "relative_y": sanitize_f32(rel_y),
"bounds_width": sanitize_f32(bounds_w), "bounds_height": sanitize_f32(bounds_h),
"content_width": sanitize_f32(content_w), "content_height": sanitize_f32(content_h),
})),
coalesce: Some(CoalesceHint::Replace),
..Self::bare("scrolled", id)
}
}
fn modifiers_data(modifiers: &KeyModifiers) -> serde_json::Value {
serde_json::json!({
"shift": modifiers.shift,
"ctrl": modifiers.ctrl,
"alt": modifiers.alt,
"logo": modifiers.logo,
"command": modifiers.command,
})
}
pub fn pointer_press(
id: impl Into<String>,
x: f32,
y: f32,
button: &str,
pointer_type: &str,
finger: Option<u64>,
modifiers: KeyModifiers,
) -> Self {
let mut val = serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
"button": button,
"pointer": pointer_type,
"modifiers": Self::modifiers_data(&modifiers),
});
if let Some(f) = finger {
val["finger"] = serde_json::json!(f);
}
Self {
value: Some(val),
..Self::bare("press", id)
}
}
pub fn pointer_release(
id: impl Into<String>,
x: f32,
y: f32,
button: &str,
pointer_type: &str,
finger: Option<u64>,
modifiers: KeyModifiers,
) -> Self {
let mut val = serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
"button": button,
"pointer": pointer_type,
"modifiers": Self::modifiers_data(&modifiers),
});
if let Some(f) = finger {
val["finger"] = serde_json::json!(f);
}
Self {
value: Some(val),
..Self::bare("release", id)
}
}
pub fn pointer_move(
id: impl Into<String>,
x: f32,
y: f32,
pointer_type: &str,
finger: Option<u64>,
modifiers: KeyModifiers,
) -> Self {
let mut val = serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
"pointer": pointer_type,
"modifiers": Self::modifiers_data(&modifiers),
});
if let Some(f) = finger {
val["finger"] = serde_json::json!(f);
}
Self {
value: Some(val),
coalesce: Some(CoalesceHint::Replace),
..Self::bare("move", id)
}
}
pub fn pointer_scroll(
id: impl Into<String>,
x: f32,
y: f32,
delta_x: f32,
delta_y: f32,
pointer_type: &str,
modifiers: KeyModifiers,
) -> Self {
Self {
value: Some(serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
"delta_x": sanitize_f32(delta_x),
"delta_y": sanitize_f32(delta_y),
"pointer": pointer_type,
"modifiers": Self::modifiers_data(&modifiers),
})),
coalesce: Some(CoalesceHint::Accumulate(vec![
"delta_x".into(),
"delta_y".into(),
])),
..Self::bare("scroll", id)
}
}
pub fn pointer_enter(id: impl Into<String>) -> Self {
Self::bare("enter", id)
}
pub fn pointer_exit(id: impl Into<String>) -> Self {
Self::bare("exit", id)
}
pub fn pointer_double_click(
id: impl Into<String>,
x: f32,
y: f32,
pointer_type: &str,
modifiers: KeyModifiers,
) -> Self {
Self {
value: Some(serde_json::json!({
"x": sanitize_f32(x),
"y": sanitize_f32(y),
"pointer": pointer_type,
"modifiers": Self::modifiers_data(&modifiers),
})),
..Self::bare("double_click", id)
}
}
pub fn resize(id: impl Into<String>, width: f32, height: f32) -> Self {
Self {
value: Some(serde_json::json!({
"width": sanitize_f32(width),
"height": sanitize_f32(height),
})),
coalesce: Some(CoalesceHint::Replace),
..Self::bare("resize", id)
}
}
}
fn sanitize_f32(v: f32) -> Value {
if v.is_finite() {
serde_json::json!(v)
} else {
log::warn!("non-finite f32 ({v}) replaced with null in outgoing event");
Value::Null
}
}
fn sanitize_f64(v: f64) -> Value {
if v.is_finite() {
serde_json::json!(v)
} else {
log::warn!("non-finite f64 ({v}) replaced with null in outgoing event");
Value::Null
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DiagnosticLevel {
Info,
Warn,
Error,
}
impl std::fmt::Display for DiagnosticLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => f.write_str("info"),
Self::Warn => f.write_str("warn"),
Self::Error => f.write_str("error"),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DiagnosticMessage {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub level: DiagnosticLevel,
pub diagnostic: crate::Diagnostic,
}
impl DiagnosticMessage {
pub fn new(level: DiagnosticLevel, diagnostic: crate::Diagnostic) -> Self {
Self {
message_type: "diagnostic",
session: String::new(),
level,
diagnostic,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct EffectResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
impl EffectResponse {
pub fn ok(id: String, result: Value) -> Self {
Self {
message_type: "effect_response",
session: String::new(),
id,
status: "ok",
result: Some(result),
error: None,
}
}
pub fn error(id: String, reason: String) -> Self {
Self {
message_type: "effect_response",
session: String::new(),
id,
status: "error",
result: None,
error: Some(reason),
}
}
pub fn unsupported(id: String) -> Self {
Self {
message_type: "effect_response",
session: String::new(),
id,
status: "unsupported",
result: None,
error: None,
}
}
pub fn cancelled(id: String) -> Self {
Self {
message_type: "effect_response",
session: String::new(),
id,
status: "cancelled",
result: None,
error: None,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct EffectStubAck {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub kind: String,
pub status: &'static str,
}
impl EffectStubAck {
pub fn registered(kind: String) -> Self {
Self {
message_type: "effect_stub_register_ack",
session: String::new(),
kind,
status: "registered",
}
}
pub fn register_error(kind: String) -> Self {
Self {
message_type: "effect_stub_register_ack",
session: String::new(),
kind,
status: "error",
}
}
pub fn unregistered(kind: String) -> Self {
Self {
message_type: "effect_stub_unregister_ack",
session: String::new(),
kind,
status: "unregistered",
}
}
pub fn unregister_error(kind: String) -> Self {
Self {
message_type: "effect_stub_unregister_ack",
session: String::new(),
kind,
status: "error",
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct QueryResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub target: String,
pub data: Value,
}
impl QueryResponse {
pub fn new(id: String, target: String, data: Value) -> Self {
Self {
message_type: "query_response",
session: String::new(),
id,
target,
data,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct InteractResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub events: Vec<OutgoingEvent>,
}
impl InteractResponse {
pub fn new(id: String, events: Vec<OutgoingEvent>) -> Self {
Self {
message_type: "interact_response",
session: String::new(),
id,
events,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
let session = session.into();
for event in &mut self.events {
event.session.clone_from(&session);
}
self.session = session;
self
}
}
#[derive(Debug, Serialize)]
#[allow(dead_code)]
pub struct TreeHashResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub name: String,
pub hash: String,
}
#[allow(dead_code)]
impl TreeHashResponse {
pub fn new(id: String, name: String, hash: String) -> Self {
Self {
message_type: "tree_hash_response",
session: String::new(),
id,
name,
hash,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct ScreenshotResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub name: String,
pub hash: String,
pub width: u32,
pub height: u32,
}
impl ScreenshotResponse {
pub fn new(id: String, name: String, hash: String, width: u32, height: u32) -> Self {
Self {
message_type: "screenshot_response",
session: String::new(),
id,
name,
hash,
width,
height,
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[derive(Debug, Serialize)]
pub struct ResetResponse {
#[serde(rename = "type")]
pub message_type: &'static str,
pub session: String,
pub id: String,
pub status: &'static str,
}
impl ResetResponse {
pub fn ok(id: String) -> Self {
Self {
message_type: "reset_response",
session: String::new(),
id,
status: "ok",
}
}
pub fn with_session(mut self, session: impl Into<String>) -> Self {
self.session = session.into();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn effect_stub_register_ack_includes_status() {
let ack = EffectStubAck::registered("file_open".to_string()).with_session("s1");
assert_eq!(
serde_json::to_value(ack).unwrap(),
json!({
"type": "effect_stub_register_ack",
"session": "s1",
"kind": "file_open",
"status": "registered",
})
);
}
#[test]
fn effect_stub_register_error_ack_includes_status() {
let ack = EffectStubAck::register_error("not_real".to_string()).with_session("s1");
assert_eq!(
serde_json::to_value(ack).unwrap(),
json!({
"type": "effect_stub_register_ack",
"session": "s1",
"kind": "not_real",
"status": "error",
})
);
}
#[test]
fn effect_stub_unregister_ack_includes_status() {
let ack = EffectStubAck::unregistered("file_open".to_string()).with_session("s1");
assert_eq!(
serde_json::to_value(ack).unwrap(),
json!({
"type": "effect_stub_unregister_ack",
"session": "s1",
"kind": "file_open",
"status": "unregistered",
})
);
}
#[test]
fn effect_stub_unregister_error_ack_includes_status() {
let ack = EffectStubAck::unregister_error("not_real".to_string()).with_session("s1");
assert_eq!(
serde_json::to_value(ack).unwrap(),
json!({
"type": "effect_stub_unregister_ack",
"session": "s1",
"kind": "not_real",
"status": "error",
})
);
}
#[test]
fn screenshot_response_serializes_structured_fields() {
let response = ScreenshotResponse::new(
"sc1".to_string(),
"homepage".to_string(),
"d4e5f6".to_string(),
1024,
768,
)
.with_session("s1");
assert_eq!(
serde_json::to_value(response).unwrap(),
json!({
"type": "screenshot_response",
"session": "s1",
"id": "sc1",
"name": "homepage",
"hash": "d4e5f6",
"width": 1024,
"height": 768,
})
);
}
#[test]
fn animation_frame_serializes_timestamp_object() {
let event = OutgoingEvent::animation_frame("anim", 16_000).with_session("s1");
assert_eq!(
serde_json::to_value(event).unwrap(),
json!({
"type": "event",
"session": "s1",
"family": "animation_frame",
"id": "",
"tag": "anim",
"value": {
"timestamp": 16_000,
},
})
);
}
#[test]
fn widget_event_accepts_custom_family() {
let event =
OutgoingEvent::widget_event("star_rating:select", "rating", Some(json!({"value": 5})));
assert_eq!(event.family, "star_rating:select");
assert_eq!(event.id, "rating");
}
#[test]
#[should_panic(
expected = "custom event family \"click\" collides with a built-in event family"
)]
fn widget_event_rejects_builtin_family() {
let _ = OutgoingEvent::widget_event("click", "button", None);
}
#[test]
fn generic_allows_builtin_renderer_events() {
let event = OutgoingEvent::generic("click", "button", None);
assert_eq!(event.family, "click");
assert_eq!(event.id, "button");
}
#[test]
fn cursor_moved_serializes_with_position_value_and_replace_hint() {
let event = OutgoingEvent::cursor_moved("mouse", 10.0, 20.0).with_session("s1");
assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
assert_eq!(
serde_json::to_value(event).unwrap(),
json!({
"type": "event",
"session": "s1",
"family": "cursor_moved",
"id": "",
"tag": "mouse",
"value": {"x": 10.0, "y": 20.0},
})
);
}
#[test]
fn cursor_entered_left_serialize_without_value() {
let entered = OutgoingEvent::cursor_entered("mouse").with_session("s1");
let left = OutgoingEvent::cursor_left("mouse").with_session("s1");
for (event, family) in [(entered, "cursor_entered"), (left, "cursor_left")] {
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], family);
assert_eq!(val["tag"], "mouse");
assert!(val.get("value").is_none(), "{family} should omit value");
}
}
#[test]
fn button_pressed_released_carry_button_string() {
let pressed = OutgoingEvent::button_pressed("mouse", "left");
assert_eq!(
serde_json::to_value(pressed).unwrap()["value"],
json!("left"),
);
let released = OutgoingEvent::button_released("mouse", "right");
assert_eq!(
serde_json::to_value(released).unwrap()["family"],
"button_released",
);
}
#[test]
fn wheel_scrolled_serializes_with_accumulate_hint() {
let event = OutgoingEvent::wheel_scrolled("mouse", 1.0, 2.0, "pixel");
match event.coalesce_hint() {
Some(CoalesceHint::Accumulate(fields)) => {
assert_eq!(fields, &vec!["delta_x".to_string(), "delta_y".to_string()]);
}
other => panic!("expected Accumulate hint, got {other:?}"),
}
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], "wheel_scrolled");
assert_eq!(val["value"]["delta_x"], 1.0);
assert_eq!(val["value"]["delta_y"], 2.0);
assert_eq!(val["value"]["unit"], "pixel");
}
#[test]
fn modifiers_changed_carries_modifiers_with_replace_hint() {
let event = OutgoingEvent::modifiers_changed(
"kbd",
KeyModifiers {
shift: true,
ctrl: true,
alt: false,
logo: false,
command: false,
},
);
assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], "modifiers_changed");
assert_eq!(val["modifiers"]["shift"], true);
assert_eq!(val["modifiers"]["ctrl"], true);
assert_eq!(val["modifiers"]["alt"], false);
}
#[test]
fn theme_changed_serializes_with_string_mode_and_replace_hint() {
let event = OutgoingEvent::theme_changed("theme", "dark");
assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], "theme_changed");
assert_eq!(val["value"], "dark");
}
#[test]
fn window_event_constructors_share_window_event_shape() {
for (event, family) in [
(OutgoingEvent::window_closed("win", "main"), "window_closed"),
(
OutgoingEvent::window_close_requested("win", "main"),
"window_close_requested",
),
(
OutgoingEvent::window_focused("win", "main"),
"window_focused",
),
(
OutgoingEvent::window_unfocused("win", "main"),
"window_unfocused",
),
(
OutgoingEvent::files_hovered_left("win", "main"),
"files_hovered_left",
),
] {
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], family);
assert_eq!(val["tag"], "win");
assert_eq!(val["value"]["window_id"], "main", "for {family}");
}
}
#[test]
fn window_opened_includes_position_when_provided() {
let with_pos =
OutgoingEvent::window_opened("win", "main", Some((50.0, 75.0)), 800.0, 600.0, 2.0);
let val = serde_json::to_value(with_pos).unwrap();
assert_eq!(val["family"], "window_opened");
assert_eq!(val["value"]["window_id"], "main");
assert_eq!(val["value"]["width"], 800.0);
assert_eq!(val["value"]["height"], 600.0);
assert_eq!(val["value"]["scale_factor"], 2.0);
assert_eq!(val["value"]["x"], 50.0);
assert_eq!(val["value"]["y"], 75.0);
}
#[test]
fn window_opened_omits_position_when_absent() {
let without_pos = OutgoingEvent::window_opened("win", "main", None, 800.0, 600.0, 1.0);
let val = serde_json::to_value(without_pos).unwrap();
assert!(val["value"].get("x").is_none());
assert!(val["value"].get("y").is_none());
}
#[test]
fn finger_event_constructors_share_payload_shape() {
for (event, family, expects_replace) in [
(
OutgoingEvent::finger_pressed("touch", 7, 1.0, 2.0),
"finger_pressed",
false,
),
(
OutgoingEvent::finger_moved("touch", 7, 1.0, 2.0),
"finger_moved",
true,
),
(
OutgoingEvent::finger_lifted("touch", 7, 1.0, 2.0),
"finger_lifted",
false,
),
(
OutgoingEvent::finger_lost("touch", 7, 1.0, 2.0),
"finger_lost",
false,
),
] {
let has_replace = matches!(event.coalesce_hint(), Some(CoalesceHint::Replace));
assert_eq!(
has_replace, expects_replace,
"{family} coalesce hint mismatch (only finger_moved should coalesce)"
);
let val = serde_json::to_value(event).unwrap();
assert_eq!(val["family"], family);
assert_eq!(val["tag"], "touch");
assert_eq!(val["value"]["id"], 7);
assert_eq!(val["value"]["x"], 1.0);
assert_eq!(val["value"]["y"], 2.0);
}
}
#[test]
fn pointer_scroll_carries_accumulate_hint_for_deltas() {
let event = OutgoingEvent::pointer_scroll(
"scroller",
5.0,
10.0,
1.5,
-2.5,
"mouse",
KeyModifiers::default(),
);
match event.coalesce_hint() {
Some(CoalesceHint::Accumulate(fields)) => {
let mut sorted: Vec<&str> = fields.iter().map(String::as_str).collect();
sorted.sort();
assert_eq!(sorted, vec!["delta_x", "delta_y"]);
}
other => panic!("expected Accumulate hint, got {other:?}"),
}
}
#[test]
fn coalesce_hint_persists_after_with_session() {
let event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0).with_session("s1");
assert!(matches!(event.coalesce_hint(), Some(CoalesceHint::Replace)));
}
#[test]
fn take_coalesce_consumes_hint_only_once() {
let mut event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0);
assert!(event.take_coalesce().is_some());
assert!(event.take_coalesce().is_none());
}
#[test]
fn coalesce_hint_field_is_skipped_on_serialization() {
let event = OutgoingEvent::cursor_moved("mouse", 0.0, 0.0);
let val = serde_json::to_value(event).unwrap();
assert!(val.get("coalesce").is_none());
let object = val.as_object().unwrap();
assert!(!object.contains_key("coalesce_hint"));
}
}