Current section
Files
Jump to
Current section
Files
src/plushie/protocol/decode.gleam
//// Decode inbound wire messages into typed Gleam values.
////
//// Supports both MessagePack and JSON wire formats. The Rust binary sends
//// three top-level message types: "hello" (handshake), "event" (user
//// interaction dispatched by family), and "effect_response" (platform
//// result). Additionally, "op_query_response" messages carry system
//// query results.
import gleam/bit_array
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/dynamic/decode
import gleam/float
import gleam/int
import gleam/json
import gleam/list
import gleam/option.{type Option, None, Some}
import gleam/result
import gleam/string
@target(erlang)
import glepack
@target(erlang)
import glepack/data
@target(erlang)
import glepack/error as glepack_error
import plushie/event.{
type Event, type EventTarget, type KeyLocation, type Modifiers,
type MouseButton, type PointerType, BackButton, EventTarget, ForwardButton,
LeftButton, LeftSide, Line, MiddleButton, Mouse, Numpad, OtherButton, Pixel,
RightButton, RightSide, Standard, Touch,
}
import plushie/prop/pointer
import plushie/protocol
// ---------------------------------------------------------------------------
// Public types
// ---------------------------------------------------------------------------
/// A decoded inbound message from the Rust binary.
pub type InboundMessage {
/// Handshake sent by the Rust binary on startup.
Hello(
protocol: Int,
version: String,
name: String,
mode: String,
backend: String,
transport: String,
native_widgets: List(String),
widgets: List(String),
)
/// A user interaction or system event.
EventMessage(Event)
/// Acknowledgement that an effect stub was registered or unregistered.
EffectStubAck(kind: String)
/// Raw effect response from the renderer. Carries the wire ID (not
/// the app-facing tag), the wire status string, and the dynamic
/// payload. The runtime maps the wire ID to (tag, kind) and then
/// decodes the typed `EffectResult` using the tracked kind.
EffectResponseRaw(wire_id: String, status: String, payload: Dynamic)
/// Intermediate step during an interact request. Contains events
/// generated by the interaction that should be processed as a batch.
InteractStep(id: String, events: List(Event))
/// Final response to an interact request. Contains any remaining
/// events to process.
InteractResponse(id: String, events: List(Event))
}
// ---------------------------------------------------------------------------
// PropValue: internal intermediate representation
// ---------------------------------------------------------------------------
/// Internal JSON-like value used as the common representation after
/// deserializing either MessagePack or JSON wire data.
type PropValue {
PString(String)
PInt(Int)
PFloat(Float)
PBool(Bool)
PNull
PList(List(PropValue))
PMap(Dict(String, PropValue))
}
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/// Decode raw wire bytes into an InboundMessage.
pub fn decode_message(
data: BitArray,
format: protocol.Format,
) -> Result(InboundMessage, protocol.DecodeError) {
use map <- result.try(deserialize(data, format))
dispatch(map)
}
/// Decode an already-deserialized Dynamic value into an InboundMessage.
///
/// For use by test infrastructure that has already deserialized from wire
/// format for multiplexing purposes but still needs to route through the
/// production decoder rather than a parallel implementation.
pub fn decode_from_dynamic(
data: Dynamic,
) -> Result(InboundMessage, protocol.DecodeError) {
case dynamic_to_prop(data) {
PMap(map) -> dispatch(map)
_ -> Error(protocol.DeserializationFailed("expected map"))
}
}
// ---------------------------------------------------------------------------
// Deserialization: wire bytes to Dict(String, PropValue)
// ---------------------------------------------------------------------------
fn deserialize(
data: BitArray,
format: protocol.Format,
) -> Result(Dict(String, PropValue), protocol.DecodeError) {
case format {
protocol.Msgpack -> deserialize_msgpack(data)
protocol.Json -> deserialize_json(data)
}
}
@target(erlang)
fn deserialize_msgpack(
data: BitArray,
) -> Result(Dict(String, PropValue), protocol.DecodeError) {
case glepack.unpack_exact(data) {
Ok(value) ->
case msgpack_to_prop(value) {
PMap(map) -> Ok(map)
_ ->
Error(protocol.DeserializationFailed("top-level value is not a map"))
}
Error(e) ->
Error(protocol.DeserializationFailed(glepack_error.to_string(e)))
}
}
@target(javascript)
fn deserialize_msgpack(
_data: BitArray,
) -> Result(Dict(String, PropValue), protocol.DecodeError) {
Error(protocol.DeserializationFailed(
"MessagePack not available on JavaScript target",
))
}
fn deserialize_json(
data: BitArray,
) -> Result(Dict(String, PropValue), protocol.DecodeError) {
case bit_array.to_string(data) {
Error(_) -> Error(protocol.DeserializationFailed("invalid UTF-8"))
Ok(text) -> {
let text = string.trim_end(text)
case json.parse(text, decode.dynamic) {
Ok(dyn) ->
case dynamic_to_prop(dyn) {
PMap(map) -> Ok(map)
_ ->
Error(protocol.DeserializationFailed(
"top-level value is not a map",
))
}
Error(_) -> Error(protocol.DeserializationFailed("invalid JSON"))
}
}
}
}
// ---------------------------------------------------------------------------
// MessagePack data.Value -> PropValue
// ---------------------------------------------------------------------------
@target(erlang)
fn msgpack_to_prop(value: data.Value) -> PropValue {
case value {
data.Nil -> PNull
data.Boolean(b) -> PBool(b)
data.Integer(n) -> PInt(n)
data.Float(f) -> PFloat(f)
data.String(s) -> PString(s)
data.Binary(b) -> PString(bit_array.base64_encode(b, True))
data.Array(items) -> PList(list.map(items, msgpack_to_prop))
data.Map(entries) -> PMap(msgpack_map_to_string_dict(entries))
data.Extension(_, _) -> PNull
}
}
@target(erlang)
fn msgpack_map_to_string_dict(
entries: Dict(data.Value, data.Value),
) -> Dict(String, PropValue) {
dict.fold(entries, dict.new(), fn(acc, key, value) {
case key {
data.String(k) -> dict.insert(acc, k, msgpack_to_prop(value))
_ -> acc
}
})
}
// ---------------------------------------------------------------------------
// Dynamic (from JSON) -> PropValue
// ---------------------------------------------------------------------------
fn dynamic_to_prop(dyn: Dynamic) -> PropValue {
// Try each type in order. Gleam's dynamic decoders return Error
// on type mismatch, so we chain through until one matches.
case decode.run(dyn, decode.string) {
Ok(s) -> PString(s)
Error(_) ->
case decode.run(dyn, decode.bool) {
Ok(b) -> PBool(b)
Error(_) ->
case decode.run(dyn, decode.int) {
Ok(n) -> PInt(n)
Error(_) ->
case decode.run(dyn, decode.float) {
Ok(f) -> PFloat(f)
Error(_) ->
case decode.run(dyn, decode.list(decode.dynamic)) {
Ok(items) -> PList(list.map(items, dynamic_to_prop))
Error(_) ->
case
decode.run(
dyn,
decode.dict(decode.string, decode.dynamic),
)
{
Ok(entries) ->
PMap(
dict.map_values(entries, fn(_k, v) {
dynamic_to_prop(v)
}),
)
Error(_) ->
// null / nil
PNull
}
}
}
}
}
}
}
// ---------------------------------------------------------------------------
// Map accessor helpers
// ---------------------------------------------------------------------------
fn get_string(
map: Dict(String, PropValue),
key: String,
) -> Result(String, protocol.DecodeError) {
case dict.get(map, key) {
Ok(PString(s)) -> Ok(s)
Ok(_) ->
Error(protocol.MalformedEvent("expected string for \"" <> key <> "\""))
Error(_) ->
Error(protocol.MalformedEvent("missing field \"" <> key <> "\""))
}
}
fn get_int(
map: Dict(String, PropValue),
key: String,
) -> Result(Int, protocol.DecodeError) {
case dict.get(map, key) {
Ok(PInt(n)) -> Ok(n)
Ok(PFloat(f)) -> Ok(float.truncate(f))
Ok(_) ->
Error(protocol.MalformedEvent("expected int for \"" <> key <> "\""))
Error(_) ->
Error(protocol.MalformedEvent("missing field \"" <> key <> "\""))
}
}
fn get_float(
map: Dict(String, PropValue),
key: String,
) -> Result(Float, protocol.DecodeError) {
case dict.get(map, key) {
Ok(PFloat(f)) -> Ok(f)
Ok(PInt(n)) -> Ok(int.to_float(n))
Ok(_) ->
Error(protocol.MalformedEvent("expected float for \"" <> key <> "\""))
Error(_) ->
Error(protocol.MalformedEvent("missing field \"" <> key <> "\""))
}
}
fn get_bool(
map: Dict(String, PropValue),
key: String,
) -> Result(Bool, protocol.DecodeError) {
case dict.get(map, key) {
Ok(PBool(b)) -> Ok(b)
Ok(_) ->
Error(protocol.MalformedEvent("expected bool for \"" <> key <> "\""))
Error(_) ->
Error(protocol.MalformedEvent("missing field \"" <> key <> "\""))
}
}
fn get_optional_string(
map: Dict(String, PropValue),
key: String,
) -> Option(String) {
case dict.get(map, key) {
Ok(PString(s)) -> Some(s)
_ -> None
}
}
fn get_optional_float(
map: Dict(String, PropValue),
key: String,
) -> Option(Float) {
case dict.get(map, key) {
Ok(PFloat(f)) -> Some(f)
Ok(PInt(n)) -> Some(int.to_float(n))
_ -> None
}
}
fn get_string_or(
map: Dict(String, PropValue),
key: String,
default: String,
) -> String {
case dict.get(map, key) {
Ok(PString(s)) -> s
_ -> default
}
}
fn get_bool_or(map: Dict(String, PropValue), key: String, default: Bool) -> Bool {
case dict.get(map, key) {
Ok(PBool(b)) -> b
_ -> default
}
}
fn get_optional_bool(map: Dict(String, PropValue), key: String) -> Option(Bool) {
case dict.get(map, key) {
Ok(PBool(b)) -> Some(b)
_ -> None
}
}
fn get_float_or(
map: Dict(String, PropValue),
key: String,
default: Float,
) -> Float {
case dict.get(map, key) {
Ok(PFloat(f)) -> f
Ok(PInt(n)) -> int.to_float(n)
_ -> default
}
}
fn get_int_or(map: Dict(String, PropValue), key: String, default: Int) -> Int {
case dict.get(map, key) {
Ok(PInt(n)) -> n
Ok(PFloat(f)) -> float.truncate(f)
_ -> default
}
}
fn get_map(map: Dict(String, PropValue), key: String) -> Dict(String, PropValue) {
case dict.get(map, key) {
Ok(PMap(m)) -> m
_ -> dict.new()
}
}
/// Decode the events list inside an interact_step or interact_response.
/// Each event is a PMap sub-message that goes through the normal dispatch.
fn decode_interact_events(map: Dict(String, PropValue)) -> List(Event) {
case dict.get(map, "events") {
Ok(PList(items)) ->
list.filter_map(items, fn(item) {
case item {
PMap(event_map) ->
case dispatch(event_map) {
Ok(EventMessage(ev)) -> Ok(ev)
_ -> Error(Nil)
}
_ -> Error(Nil)
}
})
_ -> []
}
}
fn get_string_list(map: Dict(String, PropValue), key: String) -> List(String) {
case dict.get(map, key) {
Ok(PList(items)) ->
list.filter_map(items, fn(item) {
case item {
PString(s) -> Ok(s)
_ -> Error(Nil)
}
})
_ -> []
}
}
fn prop_to_dynamic(value: PropValue) -> Dynamic {
case value {
PString(s) -> dynamic.string(s)
PInt(n) -> dynamic.int(n)
PFloat(f) -> dynamic.float(f)
PBool(b) -> dynamic.bool(b)
PNull -> dynamic.nil()
PList(items) -> dynamic.list(list.map(items, prop_to_dynamic))
PMap(entries) ->
dynamic.properties(
dict.to_list(entries)
|> list.map(fn(pair) {
#(dynamic.string(pair.0), prop_to_dynamic(pair.1))
}),
)
}
}
// ---------------------------------------------------------------------------
// Dispatch: route on "type" field
// ---------------------------------------------------------------------------
fn dispatch(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use msg_type <- result.try(get_string(map, "type"))
case msg_type {
"hello" -> decode_hello(map)
"event" -> decode_event(map)
"diagnostic" -> decode_diagnostic_message(map)
"effect_response" -> decode_effect_response(map)
"op_query_response" -> decode_op_query_response(map)
"effect_stub_register_ack" | "effect_stub_unregister_ack" -> {
use kind <- result.try(get_string(map, "kind"))
Ok(EffectStubAck(kind:))
}
"interact_step" -> {
use id <- result.try(get_string(map, "id"))
let events = decode_interact_events(map)
Ok(InteractStep(id:, events:))
}
"interact_response" -> {
use id <- result.try(get_string(map, "id"))
let events = decode_interact_events(map)
Ok(InteractResponse(id:, events:))
}
_ -> Error(protocol.UnknownMessageType(msg_type))
}
}
// ---------------------------------------------------------------------------
// Hello
// ---------------------------------------------------------------------------
fn decode_hello(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use proto <- result.try(get_int(map, "protocol"))
use version <- result.try(get_string(map, "version"))
use name <- result.try(get_string(map, "name"))
let mode = get_string_or(map, "mode", "windowed")
let backend = get_string_or(map, "backend", "unknown")
let transport = get_string_or(map, "transport", "stdio")
let native_widgets = get_string_list(map, "native_widgets")
let widgets = get_string_list(map, "widgets")
Ok(Hello(
protocol: proto,
version:,
name:,
mode:,
backend:,
transport:,
native_widgets:,
widgets:,
))
}
// ---------------------------------------------------------------------------
// Effect response
// ---------------------------------------------------------------------------
fn decode_effect_response(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use wire_id <- result.try(get_string(map, "id"))
use status <- result.try(get_string(map, "status"))
let payload = case status {
"ok" ->
case dict.get(map, "result") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
"error" ->
case dict.get(map, "error") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
_ -> dynamic.nil()
}
Ok(EffectResponseRaw(wire_id:, status:, payload:))
}
// ---------------------------------------------------------------------------
// Op query response
// ---------------------------------------------------------------------------
fn decode_op_query_response(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use kind <- result.try(get_string(map, "kind"))
let tag = get_string_or(map, "tag", "")
let data_val = case dict.get(map, "data") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
let sys_evt = case kind {
"system_info" -> event.SystemInfo(tag:, value: data_val)
"system_theme" -> {
let theme = case dict.get(map, "data") {
Ok(PString(s)) -> s
_ -> "unknown"
}
event.SystemTheme(tag:, theme:)
}
"list_images" ->
event.ImageList(
tag:,
handles: get_string_list(get_map(map, "data"), "handles"),
)
"tree_hash" -> {
let data_map = get_map(map, "data")
let hash = get_string_or(data_map, "hash", "")
event.TreeHash(tag:, hash:)
}
"find_focused" -> {
let data_map = get_map(map, "data")
let widget_id = get_optional_string(data_map, "focused")
event.FocusedWidget(tag:, widget_id:)
}
"screenshot" -> {
let data_map = get_map(map, "data")
let hash = get_string_or(data_map, "hash", "")
let width = get_int_or(data_map, "width", 0)
let height = get_int_or(data_map, "height", 0)
let pixels = case dict.get(data_map, "rgba") {
Ok(PString(b64)) ->
case bit_array.base64_decode(b64) {
Ok(bytes) -> bytes
Error(_) -> <<>>
}
_ -> <<>>
}
event.ScreenshotData(tag:, hash:, width:, height:, pixels:)
}
_ -> event.SystemInfo(tag:, value: data_val)
}
Ok(EventMessage(event.System(sys_evt)))
}
// ---------------------------------------------------------------------------
// Event dispatch: route on "family" field
// ---------------------------------------------------------------------------
fn decode_event(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use family <- result.try(get_string(map, "family"))
case family {
// Widget events
"click" -> decode_widget_click(map)
"input" -> decode_widget_string_value(map, event.Input)
"submit" -> decode_widget_string_value(map, event.Submit)
"toggle" -> decode_widget_toggle(map)
"select" -> decode_widget_string_value(map, event.Select)
"slide" -> decode_widget_float_value(map, event.Slide)
"slide_release" -> decode_widget_float_value(map, event.SlideRelease)
"paste" -> decode_widget_string_value(map, event.Paste)
"open" -> decode_widget_no_value(map, event.Open)
"close" -> decode_widget_no_value(map, event.Close)
"option_hovered" -> decode_widget_string_value(map, event.OptionHovered)
"sort" -> decode_widget_sort(map)
"key_binding" -> decode_widget_key_binding(map)
"link_click" -> decode_widget_link_click(map)
"scrolled" -> decode_widget_scrolled(map)
// Key events (global subscription)
"key_press" ->
case get_string_or(map, "id", "") {
"" -> decode_key_press(map)
_ -> decode_widget_key_press(map)
}
"key_release" ->
case get_string_or(map, "id", "") {
"" -> decode_key_release(map)
_ -> decode_widget_key_release(map)
}
"modifiers_changed" -> decode_modifiers_changed(map)
// Window events
"window_opened" -> decode_window_opened(map)
"window_closed" -> decode_window_id_event(map, event.Closed)
"window_close_requested" ->
decode_window_id_event(map, event.CloseRequested)
"window_resized" -> decode_window_resized(map)
"window_moved" -> decode_window_moved(map)
"window_focused" -> decode_window_id_event(map, event.WindowFocused)
"window_unfocused" -> decode_window_id_event(map, event.WindowUnfocused)
"window_rescaled" -> decode_window_rescaled(map)
"file_hovered" -> decode_window_file(map, event.FileHovered)
"file_dropped" -> decode_window_file(map, event.FileDropped)
"files_hovered_left" -> decode_window_id_event(map, event.FilesHoveredLeft)
// Unified pointer events (from widgets AND subscriptions)
"press" -> decode_pointer_press(map)
"release" -> decode_pointer_release(map)
"move" -> decode_pointer_move(map)
"scroll" -> decode_pointer_scroll(map)
"enter" -> decode_enter_exit(map, event.Enter)
"exit" -> decode_enter_exit(map, event.Exit)
"double_click" -> decode_pointer_double_click(map)
"resize" -> decode_widget_resize(map)
// Generic element events (focus, drag, key on scoped elements)
"focused" -> decode_widget_no_value(map, event.Focused)
"blurred" -> decode_widget_no_value(map, event.Blurred)
"drag" -> decode_widget_drag(map)
"drag_end" -> decode_widget_drag_end(map)
// Subscription pointer events (wire families from renderer subscriptions)
"cursor_moved" -> decode_sub_cursor_moved(map)
"cursor_entered" -> decode_sub_cursor_entered(map)
"cursor_left" -> decode_sub_cursor_left(map)
"button_pressed" -> decode_sub_button_pressed(map)
"button_released" -> decode_sub_button_released(map)
"wheel_scrolled" -> decode_sub_wheel_scrolled(map)
"finger_pressed" -> decode_sub_touch_press(map)
"finger_moved" -> decode_sub_touch_move(map)
"finger_lifted" -> decode_sub_touch_release(map, False)
"finger_lost" -> decode_sub_touch_release(map, True)
// IME events
"ime_opened" -> decode_ime_opened(map)
"ime_preedit" -> decode_ime_preedit(map)
"ime_commit" -> decode_ime_commit(map)
"ime_closed" -> decode_ime_closed(map)
// Transition complete
"transition_complete" -> decode_transition_complete(map)
// Pane events
"pane_resized" -> decode_pane_resized(map)
"pane_dragged" -> decode_pane_dragged(map)
"pane_clicked" -> decode_pane_simple(map, event.PaneClicked)
"pane_focus_cycle" -> decode_pane_simple(map, event.PaneFocusCycle)
// System events
"animation_frame" -> decode_animation_frame(map)
"theme_changed" -> decode_theme_changed(map)
"all_windows_closed" ->
Ok(EventMessage(event.System(event.AllWindowsClosed)))
// Announce (headless/mock: screen reader announcements)
"announce" -> decode_announce(map)
// Session lifecycle events (multiplexed mode)
"session_error" -> decode_session_error(map)
"session_closed" -> decode_session_closed(map)
// Error events
"error" -> decode_error_event(map)
// Prop validation warnings
"prop_validation" -> decode_prop_validation(map)
_ ->
case is_valid_custom_event_family(family) {
True -> decode_generic_widget_event(map, family)
False -> Error(protocol.UnknownEventFamily(family))
}
}
}
fn is_valid_custom_event_family(family: String) -> Bool {
case string.to_graphemes(family) {
[] -> False
[first, ..rest] ->
is_family_name_start(first) && list.all(rest, is_family_name_rest)
}
}
fn is_family_name_start(char: String) -> Bool {
list.contains(lowercase_ascii_letters, char)
}
fn is_family_name_rest(char: String) -> Bool {
is_family_name_start(char)
|| list.contains(ascii_digits, char)
|| list.contains(["_", ":"], char)
}
const lowercase_ascii_letters = [
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
"q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
]
const ascii_digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
// ---------------------------------------------------------------------------
// Modifier parsing
// ---------------------------------------------------------------------------
fn parse_modifiers(map: Dict(String, PropValue)) -> Modifiers {
let mods = get_map(map, "modifiers")
event.Modifiers(
shift: get_bool_or(mods, "shift", False),
ctrl: get_bool_or(mods, "ctrl", False),
alt: get_bool_or(mods, "alt", False),
logo: get_bool_or(mods, "logo", False),
command: get_bool_or(mods, "command", False),
)
}
fn parse_key_location(loc: String) -> KeyLocation {
case loc {
"left" -> LeftSide
"right" -> RightSide
"numpad" -> Numpad
_ -> Standard
}
}
fn parse_mouse_button(value: String) -> MouseButton {
case value {
"left" -> LeftButton
"right" -> RightButton
"middle" -> MiddleButton
"back" -> BackButton
"forward" -> ForwardButton
other -> OtherButton(other)
}
}
// ---------------------------------------------------------------------------
// Widget event decoders
// ---------------------------------------------------------------------------
fn decode_widget_click(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
Ok(EventMessage(event.Widget(event.Click(target:))))
}
fn decode_widget_string_value(
map: Dict(String, PropValue),
constructor: fn(EventTarget, String) -> event.WidgetEvent,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
use value <- result.try(get_string(map, "value"))
Ok(EventMessage(event.Widget(constructor(target, value))))
}
fn decode_widget_toggle(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
use value <- result.try(get_bool(map, "value"))
Ok(EventMessage(event.Widget(event.Toggle(target:, value:))))
}
fn decode_widget_float_value(
map: Dict(String, PropValue),
constructor: fn(EventTarget, Float) -> event.WidgetEvent,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
use value <- result.try(get_float(map, "value"))
Ok(EventMessage(event.Widget(constructor(target, value))))
}
fn decode_widget_no_value(
map: Dict(String, PropValue),
constructor: fn(EventTarget) -> event.WidgetEvent,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
Ok(EventMessage(event.Widget(constructor(target))))
}
fn decode_enter_exit(
map: Dict(String, PropValue),
constructor: fn(EventTarget, Option(Float), Option(Float)) ->
event.WidgetEvent,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let x = get_optional_float(data, "x")
let y = get_optional_float(data, "y")
Ok(EventMessage(event.Widget(constructor(target, x, y))))
}
fn decode_widget_sort(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let column = get_string_or(data, "column", "")
Ok(EventMessage(event.Widget(event.Sort(target:, value: column))))
}
fn decode_widget_key_binding(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let binding = case dict.get(data, "binding") {
Ok(PString(s)) -> s
_ ->
case dict.get(map, "value") {
Ok(PString(s)) -> s
_ -> ""
}
}
Ok(EventMessage(event.Widget(event.KeyBinding(target:, value: binding))))
}
fn decode_widget_link_click(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let link = get_string_or(data, "link", "")
Ok(EventMessage(event.Widget(event.LinkClicked(target:, link:))))
}
fn decode_widget_scrolled(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let scroll_data =
event.ScrollData(
absolute_x: get_float_or(data, "absolute_x", 0.0),
absolute_y: get_float_or(data, "absolute_y", 0.0),
relative_x: get_float_or(data, "relative_x", 0.0),
relative_y: get_float_or(data, "relative_y", 0.0),
bounds_width: get_float_or(data, "bounds_width", 0.0),
bounds_height: get_float_or(data, "bounds_height", 0.0),
content_width: get_float_or(data, "content_width", 0.0),
content_height: get_float_or(data, "content_height", 0.0),
)
Ok(EventMessage(event.Widget(event.Scrolled(target:, data: scroll_data))))
}
fn decode_prop_validation(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = case dict.get(map, "value") {
Ok(PMap(d)) -> d
_ -> dict.new()
}
let node_id = get_string_or(data, "node_id", "")
let node_type = get_string_or(data, "node_type", "")
let warnings = case dict.get(data, "warnings") {
Ok(PList(items)) ->
list.filter_map(items, fn(item) {
case item {
PString(s) -> Ok(s)
_ -> Error(Nil)
}
})
_ -> []
}
Ok(
EventMessage(
event.Error(event.PropValidation(node_id:, node_type:, warnings:)),
),
)
}
fn decode_generic_widget_event(
map: Dict(String, PropValue),
family: String,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let value = case dict.get(map, "value") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
let data = case dict.get(map, "value") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
Ok(
EventMessage(
event.Widget(event.CustomWidget(kind: family, target:, value:, data:)),
),
)
}
// ---------------------------------------------------------------------------
// Key event decoders
// ---------------------------------------------------------------------------
fn decode_key_press(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let key = get_string_or(data, "key", "")
let modified_key = get_string_or(data, "modified_key", key)
let modifiers = parse_modifiers(map)
let physical_key = get_optional_string(data, "physical_key")
let location = parse_key_location(get_string_or(data, "location", "standard"))
let text = get_optional_string(data, "text")
let repeat = get_bool_or(data, "repeat", False)
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Key(event.KeyEvent(
event_type: event.KeyPressed,
window_id:,
key:,
modified_key:,
modifiers:,
physical_key:,
location:,
text:,
repeat:,
captured:,
)),
),
)
}
fn decode_widget_key_press(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let key = get_string_or(data, "key", "")
let modified_key = get_string_or(data, "modified_key", key)
let modifiers = decode_modifiers_from_data(data)
let physical_key = get_optional_string(data, "physical_key")
let location = parse_key_location(get_string_or(data, "location", "standard"))
let text = get_optional_string(data, "text")
let repeat = get_bool_or(data, "repeat", False)
Ok(
EventMessage(
event.Widget(event.WidgetKeyPress(
target:,
key:,
modified_key:,
physical_key:,
modifiers:,
location:,
text:,
repeat:,
)),
),
)
}
fn decode_widget_key_release(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let key = get_string_or(data, "key", "")
let modified_key = get_string_or(data, "modified_key", key)
let modifiers = decode_modifiers_from_data(data)
let physical_key = get_optional_string(data, "physical_key")
let location = parse_key_location(get_string_or(data, "location", "standard"))
let text = get_optional_string(data, "text")
Ok(
EventMessage(
event.Widget(event.WidgetKeyRelease(
target:,
key:,
modified_key:,
physical_key:,
modifiers:,
location:,
text:,
)),
),
)
}
fn decode_key_release(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let key = get_string_or(data, "key", "")
let modified_key = get_string_or(data, "modified_key", key)
let modifiers = parse_modifiers(map)
let physical_key = get_optional_string(data, "physical_key")
let location = parse_key_location(get_string_or(data, "location", "standard"))
let text = get_optional_string(data, "text")
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Key(event.KeyEvent(
event_type: event.KeyReleased,
window_id:,
key:,
modified_key:,
modifiers:,
physical_key:,
location:,
text:,
repeat: False,
captured:,
)),
),
)
}
fn decode_modifiers_changed(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let modifiers = parse_modifiers(map)
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.ModifiersChanged(event.ModifiersEvent(
window_id:,
modifiers:,
captured:,
)),
),
)
}
// ---------------------------------------------------------------------------
// Window event decoders
// ---------------------------------------------------------------------------
fn decode_window_opened(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
use width <- result.try(get_float(data, "width"))
use height <- result.try(get_float(data, "height"))
// Position lives at top level alongside width/height, mirroring
// window_moved and window_resized. When absent/null, use None.
let px = get_optional_float(data, "x")
let py = get_optional_float(data, "y")
let scale_factor = get_float_or(data, "scale_factor", 1.0)
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type: event.Opened,
window_id:,
width: Some(width),
height: Some(height),
x: px,
y: py,
scale_factor: Some(scale_factor),
path: None,
)),
),
)
}
fn decode_window_id_event(
map: Dict(String, PropValue),
event_type: event.WindowEventType,
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type:,
window_id:,
width: None,
height: None,
x: None,
y: None,
scale_factor: None,
path: None,
)),
),
)
}
fn decode_window_resized(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
use width <- result.try(get_float(data, "width"))
use height <- result.try(get_float(data, "height"))
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type: event.Resized,
window_id:,
width: Some(width),
height: Some(height),
x: None,
y: None,
scale_factor: None,
path: None,
)),
),
)
}
fn decode_window_moved(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
use x <- result.try(get_float(data, "x"))
use y <- result.try(get_float(data, "y"))
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type: event.Moved,
window_id:,
width: None,
height: None,
x: Some(x),
y: Some(y),
scale_factor: None,
path: None,
)),
),
)
}
fn decode_window_rescaled(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
use scale_factor <- result.try(get_float(data, "scale_factor"))
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type: event.Rescaled,
window_id:,
width: None,
height: None,
x: None,
y: None,
scale_factor: Some(scale_factor),
path: None,
)),
),
)
}
fn decode_window_file(
map: Dict(String, PropValue),
event_type: event.WindowEventType,
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use window_id <- result.try(get_string(data, "window_id"))
use path <- result.try(get_string(data, "path"))
Ok(
EventMessage(
event.Window(event.WindowEvent(
event_type:,
window_id:,
width: None,
height: None,
x: None,
y: None,
scale_factor: None,
path: Some(path),
)),
),
)
}
// ---------------------------------------------------------------------------
// Unified pointer event decoders
// ---------------------------------------------------------------------------
fn decode_modifiers_from_data(data: Dict(String, PropValue)) -> Modifiers {
let mods = get_map(data, "modifiers")
event.Modifiers(
shift: get_bool_or(mods, "shift", False),
ctrl: get_bool_or(mods, "ctrl", False),
alt: get_bool_or(mods, "alt", False),
logo: get_bool_or(mods, "logo", False),
command: get_bool_or(mods, "command", False),
)
}
fn decode_pointer_type_from_data(data: Dict(String, PropValue)) -> PointerType {
case dict.get(data, "pointer") {
Ok(PString(s)) -> pointer.parse_pointer(s)
_ -> Mouse
}
}
fn decode_finger_from_data(data: Dict(String, PropValue)) -> Option(Int) {
case dict.get(data, "finger") {
Ok(PInt(n)) -> Some(n)
Ok(PFloat(f)) -> Some(float.round(f))
_ -> None
}
}
fn decode_pointer_press(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let button = case dict.get(data, "button") {
Ok(PString(s)) -> pointer.parse_button(s)
_ -> LeftButton
}
Ok(
EventMessage(
event.Widget(event.Press(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
button:,
pointer: decode_pointer_type_from_data(data),
finger: decode_finger_from_data(data),
modifiers: decode_modifiers_from_data(data),
captured: get_bool_or(map, "captured", False)
|| get_bool_or(data, "captured", False),
)),
),
)
}
fn decode_pointer_release(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let button = case dict.get(data, "button") {
Ok(PString(s)) -> pointer.parse_button(s)
_ -> LeftButton
}
Ok(
EventMessage(
event.Widget(event.Release(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
button:,
pointer: decode_pointer_type_from_data(data),
finger: decode_finger_from_data(data),
modifiers: decode_modifiers_from_data(data),
captured: get_bool_or(map, "captured", False)
|| get_bool_or(data, "captured", False),
lost: get_optional_bool(data, "lost"),
)),
),
)
}
fn decode_pointer_move(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.Move(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
pointer: decode_pointer_type_from_data(data),
finger: decode_finger_from_data(data),
modifiers: decode_modifiers_from_data(data),
captured: get_bool_or(map, "captured", False)
|| get_bool_or(data, "captured", False),
)),
),
)
}
fn decode_pointer_scroll(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.Scroll(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
delta_x: get_float_or(data, "delta_x", 0.0),
delta_y: get_float_or(data, "delta_y", 0.0),
pointer: decode_pointer_type_from_data(data),
modifiers: decode_modifiers_from_data(data),
unit: None,
captured: get_bool_or(map, "captured", False)
|| get_bool_or(data, "captured", False),
)),
),
)
}
fn decode_pointer_double_click(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.DoubleClick(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
pointer: decode_pointer_type_from_data(data),
modifiers: decode_modifiers_from_data(data),
)),
),
)
}
fn decode_widget_resize(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.Resize(
target:,
width: get_float_or(data, "width", 0.0),
height: get_float_or(data, "height", 0.0),
)),
),
)
}
fn decode_widget_drag(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.Drag(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
delta_x: get_float_or(data, "delta_x", 0.0),
delta_y: get_float_or(data, "delta_y", 0.0),
)),
),
)
}
fn decode_widget_drag_end(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
Ok(
EventMessage(
event.Widget(event.DragEnd(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
)),
),
)
}
fn decode_transition_complete(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let tag = get_string_or(data, "tag", "")
let prop = get_string_or(data, "prop", "")
Ok(EventMessage(event.Widget(event.TransitionComplete(target:, tag:, prop:))))
}
// ---------------------------------------------------------------------------
// Subscription pointer event decoders
// ---------------------------------------------------------------------------
fn decode_sub_cursor_moved(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Move(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
pointer: Mouse,
finger: None,
modifiers: decode_modifiers_from_data(data),
captured:,
)),
),
)
}
fn decode_sub_cursor_entered(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(EventMessage(event.Widget(event.Enter(target:, x: None, y: None))))
}
fn decode_sub_cursor_left(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(EventMessage(event.Widget(event.Exit(target:, x: None, y: None))))
}
fn decode_sub_button_pressed(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let button_str = get_string_or(map, "value", "left")
let button = parse_mouse_button(button_str)
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Press(
target:,
x: 0.0,
y: 0.0,
button:,
pointer: Mouse,
finger: None,
modifiers: event.modifiers_none(),
captured:,
)),
),
)
}
fn decode_sub_button_released(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let button_str = get_string_or(map, "value", "left")
let button = parse_mouse_button(button_str)
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Release(
target:,
x: 0.0,
y: 0.0,
button:,
pointer: Mouse,
finger: None,
modifiers: event.modifiers_none(),
captured:,
lost: None,
)),
),
)
}
fn decode_sub_wheel_scrolled(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let delta_x = get_float_or(data, "delta_x", 0.0)
let delta_y = get_float_or(data, "delta_y", 0.0)
let unit_str = get_string_or(data, "unit", "line")
let unit = case unit_str {
"pixel" -> Pixel
_ -> Line
}
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Scroll(
target:,
x: 0.0,
y: 0.0,
delta_x:,
delta_y:,
pointer: Mouse,
modifiers: decode_modifiers_from_data(data),
unit: Some(unit),
captured:,
)),
),
)
}
fn decode_sub_touch_press(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let finger_id = decode_touch_finger(data)
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Press(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
button: LeftButton,
pointer: Touch,
finger: Some(finger_id),
modifiers: event.modifiers_none(),
captured:,
)),
),
)
}
fn decode_sub_touch_release(
map: Dict(String, PropValue),
lost: Bool,
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let finger_id = decode_touch_finger(data)
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Release(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
button: LeftButton,
pointer: Touch,
finger: Some(finger_id),
modifiers: event.modifiers_none(),
captured:,
lost: Some(lost),
)),
),
)
}
fn decode_touch_finger(data: Dict(String, PropValue)) -> Int {
case dict.get(data, "id") {
Ok(PInt(n)) -> n
Ok(PFloat(f)) -> float.round(f)
_ -> 0
}
}
fn decode_sub_touch_move(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let wid = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let finger_id = case dict.get(data, "id") {
Ok(PInt(n)) -> n
Ok(PFloat(f)) -> float.round(f)
_ -> 0
}
let captured = get_bool_or(map, "captured", False)
let target = EventTarget(window_id: wid, id: wid, scope: [], full: wid)
Ok(
EventMessage(
event.Widget(event.Move(
target:,
x: get_float_or(data, "x", 0.0),
y: get_float_or(data, "y", 0.0),
pointer: Touch,
finger: Some(finger_id),
modifiers: event.modifiers_none(),
captured:,
)),
),
)
}
// ---------------------------------------------------------------------------
// IME event decoders
// ---------------------------------------------------------------------------
fn decode_ime_opened(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Ime(event.ImeEvent(
event_type: event.ImeOpened,
window_id:,
text: None,
cursor: None,
captured:,
)),
),
)
}
fn decode_ime_preedit(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let text = get_string_or(data, "text", "")
let cursor = case dict.get(data, "cursor") {
Ok(PMap(c)) -> {
let start = get_int_or(c, "start", 0)
let end = get_int_or(c, "end", 0)
Some(#(start, end))
}
_ -> None
}
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Ime(event.ImeEvent(
event_type: event.ImePreedit,
window_id:,
text: Some(text),
cursor:,
captured:,
)),
),
)
}
fn decode_ime_commit(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let data = get_map(map, "value")
let text = get_string_or(data, "text", "")
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Ime(event.ImeEvent(
event_type: event.ImeCommit,
window_id:,
text: Some(text),
cursor: None,
captured:,
)),
),
)
}
fn decode_ime_closed(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let window_id = get_string_or(map, "window_id", "")
let captured = get_bool_or(map, "captured", False)
Ok(
EventMessage(
event.Ime(event.ImeEvent(
event_type: event.ImeClosed,
window_id:,
text: None,
cursor: None,
captured:,
)),
),
)
}
// ---------------------------------------------------------------------------
// Diagnostic decoder
// ---------------------------------------------------------------------------
/// Decode a top-level `diagnostic` message. Wire shape:
///
/// ```json
/// {"type":"diagnostic","session":"...","level":"warn",
/// "diagnostic":{"kind":"...",...}}
/// ```
///
/// Dispatches on the `kind` field to produce a typed
/// [`event.Diagnostic`] variant. Unknown kinds surface as a
/// `protocol.DecodeError` so version skew is loud, not silent.
fn decode_diagnostic_message(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let session = get_string_or(map, "session", "")
let level = get_string_or(map, "level", "warn")
let payload = get_map(map, "diagnostic")
let kind = get_string_or(payload, "kind", "")
use diag <- result.try(decode_diagnostic_payload(kind, payload))
Ok(
EventMessage(event.Error(event.Diagnostic(session:, level:, payload: diag))),
)
}
fn decode_diagnostic_payload(
kind: String,
payload: Dict(String, PropValue),
) -> Result(event.Diagnostic, protocol.DecodeError) {
case kind {
"duplicate_id" ->
Ok(event.DuplicateId(
id: get_string_or(payload, "id", ""),
window_id: get_optional_string(payload, "window_id"),
))
"empty_id" ->
Ok(event.EmptyId(type_name: get_string_or(payload, "type_name", "")))
"multiple_top_level_windows" ->
Ok(
event.MultipleTopLevelWindows(window_ids: get_string_list(
payload,
"window_ids",
)),
)
"unknown_window" ->
Ok(event.UnknownWindow(
window_id: get_string_or(payload, "window_id", ""),
subscription_tag: get_string_or(payload, "subscription_tag", ""),
))
"unrecognized_widget_placeholder" ->
Ok(
event.UnrecognizedWidgetPlaceholder(id: get_string_or(payload, "id", "")),
)
"tree_depth_exceeded" ->
Ok(event.TreeDepthExceeded(
id: get_string_or(payload, "id", ""),
max_depth: get_int_or(payload, "max_depth", 0),
))
"too_many_duplicates" ->
Ok(event.TooManyDuplicates(limit: get_int_or(payload, "limit", 0)))
"widget_id_invalid" ->
Ok(event.WidgetIdInvalid(
reason: get_string_or(payload, "reason", ""),
type_name: get_string_or(payload, "type_name", ""),
id: get_string_or(payload, "id", ""),
detail: get_string_or(payload, "detail", ""),
))
"missing_accessible_name" ->
Ok(event.MissingAccessibleName(
type_name: get_string_or(payload, "type_name", ""),
id: get_string_or(payload, "id", ""),
))
"a11y_ref_unresolved" ->
Ok(event.A11yRefUnresolved(
id: get_string_or(payload, "id", ""),
key: get_string_or(payload, "key", ""),
value: get_string_or(payload, "value", ""),
is_member: get_bool_or(payload, "is_member", False),
))
"prop_range_exceeded" ->
Ok(event.PropRangeExceeded(
id: get_string_or(payload, "id", ""),
type_name: get_string_or(payload, "type_name", ""),
prop: get_string_or(payload, "prop", ""),
raw: get_float_or(payload, "raw", 0.0),
clamped: get_float_or(payload, "clamped", 0.0),
non_finite: get_bool_or(payload, "non_finite", False),
))
"prop_type_mismatch" ->
Ok(event.PropTypeMismatch(
id: get_string_or(payload, "id", ""),
type_name: get_string_or(payload, "type_name", ""),
prop: get_string_or(payload, "prop", ""),
value_debug: get_string_or(payload, "value_debug", ""),
expected_debug: get_string_or(payload, "expected_debug", ""),
))
"prop_unknown" ->
Ok(event.PropUnknown(
id: get_string_or(payload, "id", ""),
type_name: get_string_or(payload, "type_name", ""),
prop: get_string_or(payload, "prop", ""),
known_debug: get_string_or(payload, "known_debug", ""),
))
"content_length_exceeded" ->
Ok(event.ContentLengthExceeded(
id: get_string_or(payload, "id", ""),
field: get_string_or(payload, "field", ""),
actual: get_int_or(payload, "actual", 0),
cap: get_int_or(payload, "cap", 0),
truncated: get_int_or(payload, "truncated", 0),
))
"font_cache_cap_exceeded" ->
Ok(event.FontCacheCapExceeded(max: get_int_or(payload, "max", 0)))
"font_cap_exceeded" ->
Ok(event.FontCapExceeded(
max: get_int_or(payload, "max", 0),
requested: get_int_or(payload, "requested", 0),
granted: get_int_or(payload, "granted", 0),
dropped: get_int_or(payload, "dropped", 0),
))
"font_family_not_found" ->
Ok(event.FontFamilyNotFound(family: get_string_or(payload, "family", "")))
"invalid_settings" ->
Ok(event.InvalidSettings(detail: get_string_or(payload, "detail", "")))
"required_widgets_missing" ->
Ok(
event.RequiredWidgetsMissing(missing: get_string_list(
payload,
"missing",
)),
)
"widget_panic" ->
Ok(event.WidgetPanic(
id: get_string_or(payload, "id", ""),
type_name: get_string_or(payload, "type_name", ""),
label: get_string_or(payload, "label", ""),
))
"svg_parse_error" ->
Ok(event.SvgParseError(
id: get_string_or(payload, "id", ""),
source: get_string_or(payload, "source", ""),
detail: get_string_or(payload, "detail", ""),
))
"svg_decode_timeout" ->
Ok(event.SvgDecodeTimeout(
id: get_string_or(payload, "id", ""),
source: get_string_or(payload, "source", ""),
deadline_debug: get_string_or(payload, "deadline_debug", ""),
))
"dash_cache_cap_exceeded" ->
Ok(event.DashCacheCapExceeded(max: get_int_or(payload, "max", 0)))
"emitter_coalesce_cap_exceeded" ->
Ok(event.EmitterCoalesceCapExceeded(cap: get_int_or(payload, "cap", 0)))
"widget_id_type_collision" ->
Ok(event.WidgetIdTypeCollision(
id: get_string_or(payload, "id", ""),
existing_type: get_string_or(payload, "existing_type", ""),
incoming_type: get_string_or(payload, "incoming_type", ""),
))
"view_panicked" ->
Ok(event.ViewPanicked(
consecutive: get_int_or(payload, "consecutive", 0),
message: get_string_or(payload, "message", ""),
))
"update_panicked" ->
Ok(event.UpdatePanicked(
consecutive: get_int_or(payload, "consecutive", 0),
message: get_string_or(payload, "message", ""),
))
"unknown_message_type" ->
Ok(
event.UnknownMessageType(msg_type: get_string_or(
payload,
"msg_type",
"",
)),
)
"dispatch_loop_exceeded" ->
Ok(event.DispatchLoopExceeded(
depth: get_int_or(payload, "depth", 0),
limit: get_int_or(payload, "limit", 0),
))
"buffer_overflow" ->
Ok(event.BufferOverflow(
size: get_int_or(payload, "size", 0),
limit: get_int_or(payload, "limit", 0),
))
other ->
Error(protocol.MalformedEvent(
"unknown diagnostic kind: \""
<> other
<> "\"; SDK and renderer versions may be skewed",
))
}
}
// ---------------------------------------------------------------------------
// Pane event decoders
// ---------------------------------------------------------------------------
fn decode_pane_resized(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let split = case dict.get(data, "split") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
let ratio = get_float_or(data, "ratio", 0.5)
Ok(EventMessage(event.Widget(event.PaneResized(target:, split:, ratio:))))
}
fn decode_pane_dragged(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let pane = case dict.get(data, "pane") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
let drop_target = case dict.get(data, "target") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
let action = get_string_or(data, "action", "")
let region = get_optional_string(data, "region")
let edge = get_optional_string(data, "edge")
Ok(
EventMessage(
event.Widget(event.PaneDragged(
target:,
pane:,
drop_target:,
action:,
region:,
edge:,
)),
),
)
}
fn decode_pane_simple(
map: Dict(String, PropValue),
constructor: fn(EventTarget, Dynamic) -> event.WidgetEvent,
) -> Result(InboundMessage, protocol.DecodeError) {
use target <- result.try(decode_windowed_target(map))
let data = get_map(map, "value")
let pane = case dict.get(data, "pane") {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
Ok(EventMessage(event.Widget(constructor(target, pane))))
}
fn decode_windowed_target(
map: Dict(String, PropValue),
) -> Result(EventTarget, protocol.DecodeError) {
use id <- result.try(get_string(map, "id"))
// Window is encoded in the ID via # prefix (e.g. "main#form/email").
Ok(event.make_target(id, ""))
}
// ---------------------------------------------------------------------------
// System event decoders
// ---------------------------------------------------------------------------
fn decode_animation_frame(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
use timestamp <- result.try(get_int(data, "timestamp"))
Ok(EventMessage(event.System(event.AnimationFrame(timestamp:))))
}
fn decode_theme_changed(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
use theme <- result.try(get_string(map, "value"))
Ok(EventMessage(event.System(event.ThemeChanged(theme:))))
}
// ---------------------------------------------------------------------------
// Announce event decoder
// ---------------------------------------------------------------------------
fn decode_announce(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let data = get_map(map, "value")
let text = get_string_or(data, "text", "")
Ok(EventMessage(event.System(event.Announce(text:))))
}
// ---------------------------------------------------------------------------
// Session lifecycle decoders
// ---------------------------------------------------------------------------
fn decode_session_error(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let session = get_string_or(map, "session", "")
let data = get_map(map, "value")
let code = get_string_or(data, "code", "")
let error = get_string_or(data, "error", "")
Ok(EventMessage(event.Session(event.SessionError(session:, code:, error:))))
}
fn decode_session_closed(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let session = get_string_or(map, "session", "")
let data = get_map(map, "value")
let reason = get_string_or(data, "reason", "")
Ok(EventMessage(event.Session(event.SessionClosed(session:, reason:))))
}
// ---------------------------------------------------------------------------
// Error event decoder
// ---------------------------------------------------------------------------
fn decode_error_event(
map: Dict(String, PropValue),
) -> Result(InboundMessage, protocol.DecodeError) {
let error_id = get_string_or(map, "id", "")
case error_id {
"duplicate_node_ids" -> {
let details = case get_value_or_data(map) {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
Ok(EventMessage(event.Error(event.DuplicateNodeIds(details:))))
}
"command" | "widget_command" | "extension_command" -> {
let value = case get_value_or_data(map) {
Ok(PMap(m)) -> m
_ -> dict.new()
}
Ok(
EventMessage(
event.Error(event.CommandError(
reason: get_string_or(value, "reason", ""),
id: get_optional_string(value, "id"),
family: get_optional_string(value, "family"),
widget_type: get_optional_string(value, "widget_type"),
message: get_optional_string(value, "message"),
)),
),
)
}
_ -> {
let data = case get_value_or_data(map) {
Ok(v) -> prop_to_dynamic(v)
Error(_) -> dynamic.nil()
}
Ok(EventMessage(event.Error(event.RendererError(id: error_id, data:))))
}
}
}
/// Read event payload from "value" (preferred) or "data" (fallback).
fn get_value_or_data(map: Dict(String, PropValue)) -> Result(PropValue, Nil) {
case dict.get(map, "value") {
Ok(v) -> Ok(v)
Error(_) -> dict.get(map, "data")
}
}