//// Wire event decoder for test backends. //// //// Decodes wire-format event maps (as received from the renderer) into //// typed Event values. Shared between all test backends that receive //// wire events, keeping decoding logic in one place. import gleam/dict.{type Dict} import gleam/dynamic.{type Dynamic} import gleam/dynamic/decode import gleam/list import gleam/option.{None, Some} import gleam/string import plushie/event.{type Event, type Modifiers, Modifiers} /// Decode a wire event map into a typed Event. /// Returns Ok(event) or Error(Nil) for unrecognised families. pub fn decode_test_event( family: String, id: String, data: Dict(String, Dynamic), ) -> Result(Event, Nil) { let #(local, scope) = split_scoped_id(id) case family { // -- Widget events ------------------------------------------------------- "click" -> Ok(event.WidgetClick(id: local, scope:)) "input" -> Ok(event.WidgetInput( id: local, scope:, value: get_string(data, "value", ""), )) "submit" -> Ok(event.WidgetSubmit( id: local, scope:, value: get_string(data, "value", ""), )) "toggle" -> Ok(event.WidgetToggle( id: local, scope:, value: get_bool(data, "value", False), )) "select" -> Ok(event.WidgetSelect( id: local, scope:, value: get_string(data, "value", ""), )) "slide" -> Ok(event.WidgetSlide( id: local, scope:, value: get_float(data, "value", 0.0), )) "slide_release" -> Ok(event.WidgetSlideRelease( id: local, scope:, value: get_float(data, "value", 0.0), )) "paste" -> Ok(event.WidgetPaste( id: local, scope:, value: get_string(data, "value", ""), )) "sort" -> Ok(event.WidgetSort( id: local, scope:, value: get_string(data, "column", ""), )) "open" -> Ok(event.WidgetOpen(id: local, scope:)) "close" -> Ok(event.WidgetClose(id: local, scope:)) "option_hovered" -> Ok(event.WidgetOptionHovered( id: local, scope:, value: get_string(data, "value", ""), )) "key_binding" -> Ok(event.WidgetKeyBinding( id: local, scope:, value: get_string(data, "value", ""), )) "scroll" -> { let scroll_data = event.ScrollData( absolute_x: get_float(data, "absolute_x", 0.0), absolute_y: get_float(data, "absolute_y", 0.0), relative_x: get_float(data, "relative_x", 0.0), relative_y: get_float(data, "relative_y", 0.0), bounds_width: get_float(data, "bounds_width", 0.0), bounds_height: get_float(data, "bounds_height", 0.0), content_width: get_float(data, "content_width", 0.0), content_height: get_float(data, "content_height", 0.0), ) Ok(event.WidgetScroll(id: local, scope:, data: scroll_data)) } // -- Key events ---------------------------------------------------------- "key_press" -> Ok(decode_key_press(data)) "key_release" -> Ok(decode_key_release(data)) // -- Mouse events (global subscriptions) --------------------------------- "cursor_moved" -> Ok(event.MouseMoved( x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "cursor_entered" -> Ok(event.MouseEntered(captured: False)) "cursor_left" -> Ok(event.MouseLeft(captured: False)) "button_pressed" -> Ok(event.MouseButtonPressed( button: decode_mouse_button(get_string(data, "button", "left")), captured: False, )) "button_released" -> Ok(event.MouseButtonReleased( button: decode_mouse_button(get_string(data, "button", "left")), captured: False, )) "wheel_scrolled" -> Ok(event.MouseWheelScrolled( delta_x: get_float(data, "delta_x", 0.0), delta_y: get_float(data, "delta_y", 0.0), unit: event.Line, captured: False, )) // -- Touch events -------------------------------------------------------- "finger_pressed" -> Ok(event.TouchPressed( finger_id: get_int(data, "finger_id", 0), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "finger_moved" -> Ok(event.TouchMoved( finger_id: get_int(data, "finger_id", 0), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "finger_lifted" -> Ok(event.TouchLifted( finger_id: get_int(data, "finger_id", 0), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "finger_lost" -> Ok(event.TouchLost( finger_id: get_int(data, "finger_id", 0), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) // -- Window events ------------------------------------------------------- "window_opened" -> Ok(event.WindowOpened( window_id: get_string(data, "window_id", ""), width: get_float(data, "width", 0.0), height: get_float(data, "height", 0.0), position_x: get_optional_float(data, "position_x"), position_y: get_optional_float(data, "position_y"), scale_factor: get_float(data, "scale_factor", 1.0), )) "window_closed" -> Ok(event.WindowClosed(window_id: get_string(data, "window_id", ""))) "window_close_requested" -> Ok( event.WindowCloseRequested(window_id: get_string(data, "window_id", "")), ) "window_moved" -> Ok(event.WindowMoved( window_id: get_string(data, "window_id", ""), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), )) "window_resized" -> Ok(event.WindowResized( window_id: get_string(data, "window_id", ""), width: get_float(data, "width", 0.0), height: get_float(data, "height", 0.0), )) "window_focused" -> Ok(event.WindowFocused(window_id: get_string(data, "window_id", ""))) "window_unfocused" -> Ok(event.WindowUnfocused(window_id: get_string(data, "window_id", ""))) "window_rescaled" -> Ok(event.WindowRescaled( window_id: get_string(data, "window_id", ""), scale_factor: get_float(data, "scale_factor", 1.0), )) "window_file_hovered" -> Ok(event.WindowFileHovered( window_id: get_string(data, "window_id", ""), path: get_string(data, "path", ""), )) "window_file_dropped" -> Ok(event.WindowFileDropped( window_id: get_string(data, "window_id", ""), path: get_string(data, "path", ""), )) "window_files_hovered_left" -> Ok( event.WindowFilesHoveredLeft(window_id: get_string( data, "window_id", "", )), ) // -- IME events ---------------------------------------------------------- "ime_opened" -> Ok(event.ImeOpened(captured: False)) "ime_preedit" -> Ok(event.ImePreedit( text: get_string(data, "text", ""), cursor: None, captured: False, )) "ime_commit" -> Ok(event.ImeCommit(text: get_string(data, "text", ""), captured: False)) "ime_closed" -> Ok(event.ImeClosed(captured: False)) // -- Modifiers changed --------------------------------------------------- "modifiers_changed" -> Ok(event.ModifiersChanged( modifiers: decode_modifiers(data), captured: False, )) // -- MouseArea events ---------------------------------------------------- "mouse_area_right_press" -> Ok(event.MouseAreaRightPress(id: local, scope:)) "mouse_area_right_release" -> Ok(event.MouseAreaRightRelease(id: local, scope:)) "mouse_area_middle_press" -> Ok(event.MouseAreaMiddlePress(id: local, scope:)) "mouse_area_middle_release" -> Ok(event.MouseAreaMiddleRelease(id: local, scope:)) "mouse_area_double_click" -> Ok(event.MouseAreaDoubleClick(id: local, scope:)) "mouse_area_enter" -> Ok(event.MouseAreaEnter(id: local, scope:)) "mouse_area_exit" -> Ok(event.MouseAreaExit(id: local, scope:)) "mouse_area_move" -> Ok(event.MouseAreaMove( id: local, scope:, x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), )) "mouse_area_scroll" -> Ok(event.MouseAreaScroll( id: local, scope:, delta_x: get_float(data, "delta_x", 0.0), delta_y: get_float(data, "delta_y", 0.0), )) // -- Canvas events ------------------------------------------------------- "canvas_press" -> Ok(event.CanvasPress( id: local, scope:, x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), button: get_string(data, "button", "left"), )) "canvas_release" -> Ok(event.CanvasRelease( id: local, scope:, x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), button: get_string(data, "button", "left"), )) "canvas_move" -> Ok(event.CanvasMove( id: local, scope:, x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), )) "canvas_scroll" -> Ok(event.CanvasScroll( id: local, scope:, x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), delta_x: get_float(data, "delta_x", 0.0), delta_y: get_float(data, "delta_y", 0.0), )) // -- Canvas shape events ------------------------------------------------- "canvas_element_enter" -> Ok(event.CanvasElementEnter( id: local, scope:, element_id: get_string(data, "element_id", ""), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "canvas_element_leave" -> Ok(event.CanvasElementLeave( id: local, scope:, element_id: get_string(data, "element_id", ""), captured: False, )) "canvas_element_click" -> Ok(event.CanvasElementClick( id: local, scope:, element_id: get_string(data, "element_id", ""), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), button: get_string(data, "button", "left"), captured: False, )) "canvas_element_drag" -> Ok(event.CanvasElementDrag( id: local, scope:, element_id: get_string(data, "element_id", ""), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), delta_x: get_float(data, "delta_x", 0.0), delta_y: get_float(data, "delta_y", 0.0), captured: False, )) "canvas_element_drag_end" -> Ok(event.CanvasElementDragEnd( id: local, scope:, element_id: get_string(data, "element_id", ""), x: get_float(data, "x", 0.0), y: get_float(data, "y", 0.0), captured: False, )) "canvas_element_focused" -> Ok(event.CanvasElementFocused( id: local, scope:, element_id: get_string(data, "element_id", ""), captured: False, )) "canvas_element_blurred" -> Ok(event.CanvasElementBlurred( id: local, scope:, element_id: get_string(data, "element_id", ""), )) "canvas_focused" -> Ok(event.CanvasFocused(id: local, scope:)) "canvas_blurred" -> Ok(event.CanvasBlurred(id: local, scope:)) "canvas_group_focused" -> Ok(event.CanvasGroupFocused( id: local, scope:, group_id: get_string(data, "group_id", ""), )) "canvas_group_blurred" -> Ok(event.CanvasGroupBlurred( id: local, scope:, group_id: get_string(data, "group_id", ""), )) "diagnostic" -> Ok(event.Diagnostic( level: get_string(data, "level", ""), element_id: get_string(data, "element_id", ""), code: get_string(data, "code", ""), message: get_string(data, "message", ""), )) // -- Pane events --------------------------------------------------------- "pane_resized" -> Ok(event.PaneResized( id: local, scope:, split: get_dynamic(data, "split"), ratio: get_float(data, "ratio", 0.5), )) "pane_dragged" -> Ok(event.PaneDragged( id: local, scope:, pane: get_dynamic(data, "pane"), target: get_dynamic(data, "target"), action: get_string(data, "action", ""), region: get_optional_string(data, "region"), edge: get_optional_string(data, "edge"), )) "pane_clicked" -> Ok(event.PaneClicked(id: local, scope:, pane: get_dynamic(data, "pane"))) "pane_focus_cycle" -> Ok(event.PaneFocusCycle( id: local, scope:, pane: get_dynamic(data, "pane"), )) // -- Sensor events ------------------------------------------------------- "sensor_resize" -> Ok(event.SensorResize( id: local, scope:, width: get_float(data, "width", 0.0), height: get_float(data, "height", 0.0), )) // -- System events ------------------------------------------------------- "animation_frame" -> Ok(event.AnimationFrame(timestamp: get_int(data, "timestamp", 0))) "theme_changed" -> Ok(event.ThemeChanged(theme: get_string(data, "theme", ""))) "all_windows_closed" -> Ok(event.AllWindowsClosed) _ -> Error(Nil) } } // -- Scoped ID splitting ----------------------------------------------------- fn split_scoped_id(id: String) -> #(String, List(String)) { case string.split(id, "/") { [] -> #(id, []) [single] -> #(single, []) segments -> { let assert Ok(local) = list.last(segments) let scope = list.take(segments, list.length(segments) - 1) #(local, scope) } } } // -- Key event decoding ------------------------------------------------------ fn decode_key_press(data: Dict(String, Dynamic)) -> Event { let key = parse_wire_key_name(get_string(data, "value", "")) let modifiers = decode_modifiers(data) let text = case string.length(key) == 1 { True -> Some(key) False -> None } event.KeyPress( key:, modified_key: key, modifiers:, physical_key: None, location: event.Standard, text:, repeat: False, captured: False, ) } fn decode_key_release(data: Dict(String, Dynamic)) -> Event { let key = parse_wire_key_name(get_string(data, "value", "")) let modifiers = decode_modifiers(data) let text = case string.length(key) == 1 { True -> Some(key) False -> None } event.KeyRelease( key:, modified_key: key, modifiers:, physical_key: None, location: event.Standard, text:, captured: False, ) } fn decode_modifiers(data: Dict(String, Dynamic)) -> Modifiers { case dict.get(data, "modifiers") { Ok(m) -> decode_modifiers_from_dynamic(m) Error(_) -> case dict.get(data, "data") { Ok(d) -> { // Try to extract "modifiers" sub-field from "data" case decode.run(d, decode.at(["modifiers"], decode.dynamic)) { Ok(m) -> decode_modifiers_from_dynamic(m) Error(_) -> modifiers_none() } } Error(_) -> modifiers_none() } } } fn decode_modifiers_from_dynamic(raw: Dynamic) -> Modifiers { let get_field = fn(name) { case decode.run(raw, decode.at([name], decode.bool)) { Ok(v) -> v Error(_) -> False } } let ctrl = get_field("ctrl") Modifiers( ctrl:, shift: get_field("shift"), alt: get_field("alt"), logo: get_field("logo"), command: ctrl, ) } fn modifiers_none() -> Modifiers { Modifiers(shift: False, ctrl: False, alt: False, logo: False, command: False) } /// Map wire key names to canonical key strings. fn parse_wire_key_name(name: String) -> String { case name { "enter" -> "Enter" "escape" -> "Escape" "tab" -> "Tab" "backspace" -> "Backspace" "space" -> " " "delete" -> "Delete" "up" -> "ArrowUp" "down" -> "ArrowDown" "left" -> "ArrowLeft" "right" -> "ArrowRight" "home" -> "Home" "end" -> "End" "page_up" -> "PageUp" "page_down" -> "PageDown" "f1" -> "F1" "f2" -> "F2" "f3" -> "F3" "f4" -> "F4" "f5" -> "F5" "f6" -> "F6" "f7" -> "F7" "f8" -> "F8" "f9" -> "F9" "f10" -> "F10" "f11" -> "F11" "f12" -> "F12" other -> other } } // -- Mouse button decoding --------------------------------------------------- fn decode_mouse_button(name: String) -> event.MouseButton { case name { "left" -> event.LeftButton "right" -> event.RightButton "middle" -> event.MiddleButton "back" -> event.BackButton "forward" -> event.ForwardButton other -> event.OtherButton(other) } } // -- Dict helpers ------------------------------------------------------------ fn get_string( data: Dict(String, Dynamic), key: String, default: String, ) -> String { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.string) { Ok(s) -> s Error(_) -> default } Error(_) -> default } } fn get_optional_string( data: Dict(String, Dynamic), key: String, ) -> option.Option(String) { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.string) { Ok(s) -> Some(s) Error(_) -> None } Error(_) -> None } } fn get_float(data: Dict(String, Dynamic), key: String, default: Float) -> Float { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.float) { Ok(f) -> f Error(_) -> case decode.run(val, decode.int) { Ok(i) -> int_to_float(i) Error(_) -> default } } Error(_) -> default } } fn get_optional_float( data: Dict(String, Dynamic), key: String, ) -> option.Option(Float) { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.float) { Ok(f) -> Some(f) Error(_) -> case decode.run(val, decode.int) { Ok(i) -> Some(int_to_float(i)) Error(_) -> None } } Error(_) -> None } } fn get_int(data: Dict(String, Dynamic), key: String, default: Int) -> Int { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.int) { Ok(i) -> i Error(_) -> default } Error(_) -> default } } fn get_bool(data: Dict(String, Dynamic), key: String, default: Bool) -> Bool { case dict.get(data, key) { Ok(val) -> case decode.run(val, decode.bool) { Ok(b) -> b Error(_) -> default } Error(_) -> default } } fn get_dynamic(data: Dict(String, Dynamic), key: String) -> Dynamic { case dict.get(data, key) { Ok(val) -> val Error(_) -> dynamic.nil() } } @external(erlang, "erlang", "float") fn int_to_float(i: Int) -> Float