From 5b19c51100fb094dbc4b28e893a377a4e9110da5 Mon Sep 17 00:00:00 2001 From: Dominic Letz Date: Thu, 23 Jul 2026 07:19:53 +0800 Subject: [PATCH] Add custom_event/2 and webview reload Platform APIs Provide Desktop.Platform.System.custom_event/2 for mobile bridge custom events (replacing Hex Bridge GenServer calls) and Desktop.Window.reload/1 plus Content.reload/1 as the backend-safe replacement for :wxWebView.reload/1. Co-authored-by: Cursor --- CHANGELOG.md | 2 ++ guides/faq.md | 10 ++++++ lib/desktop/backend/browser.ex | 6 ++++ lib/desktop/backend/json.ex | 17 ++++++++++ lib/desktop/backend/wx.ex | 11 +++++++ lib/desktop/bridge/mock.ex | 2 ++ lib/desktop/platform/content.ex | 10 ++++++ lib/desktop/platform/system.ex | 12 +++++++ lib/desktop/window.ex | 20 ++++++++++++ test/desktop/backend/browser_test.exs | 5 +++ test/desktop/backend/json_test.exs | 14 ++++++++ test/desktop/backend/wx_test.exs | 14 ++++++++ test/desktop/platform_system_test.exs | 32 +++++++++++++++++++ .../desktop/regression/beam_wx_calls_test.exs | 11 +++++++ test/support/guard_platform_abstraction.exs | 4 ++- 15 files changed, 169 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e74e6f..f986e47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ - Menu adapters: `Desktop.Menu.Adapter.Json` and `Desktop.Menu.Adapter.Browser` - Public `Desktop.*` APIs unchanged (`Desktop.Window`, `Desktop.Env`, `Desktop.Menu`, etc.) - `Desktop.Platform.System.os_description/0` — backend-safe replacement for `:wx_misc.getOsDescription/0` (Wx, Json bridge, Browser) +- `Desktop.Platform.System.custom_event/2` — mobile bridge custom events (replaces Hex `Bridge` `[:custom_event, …]` calls) +- `Desktop.Platform.Content.reload/1` and `Desktop.Window.reload/1` — backend-safe webview reload (replaces `:wxWebView.reload/1`) - Test suite: `mix test.fast`, `xvfb-run mix test.wx`, `mix test.guard` — see `docs/TEST_PLAN.md` - Compile without OTP `:wx`: conditional `erl_src_paths` and `Desktop.Wx` fallbacks (no `wx.hrl` required) diff --git a/guides/faq.md b/guides/faq.md index 1899dae..0b11c53 100644 --- a/guides/faq.md +++ b/guides/faq.md @@ -50,6 +50,8 @@ Desktop.Platform.backend() # e.g. Desktop.Backend.Wx Desktop.Platform.capabilities() # %{window: true, content: :webview, ...} Desktop.Platform.System.locale() Desktop.Platform.System.os_description() # replaces :wx_misc.getOsDescription/0 +Desktop.Platform.System.custom_event(:share, [path]) # mobile bridge custom events +Desktop.Window.reload(pid) # replaces :wxWebView.reload/1 ``` | Backend | `window` | `content` | `menu` | @@ -64,6 +66,14 @@ On Linux with DBus SNI available, Wx may use `:dbus` for the taskbar menu instea On mobile targets, `desktop` uses `Desktop.Backend.Json` instead of OTP `:wx`. The Elixir side speaks the legacy JSON protocol over TCP to your native host app. Set `BRIDGE_PORT` to the port your host app listens on. Transport is built in as `Desktop.Bridge.Transport` — the separate `bridge` hex package is no longer required. +App-level native events (share, save, restart, etc.) use: + +```elixir +Desktop.Platform.System.custom_event(:share, [path, label]) +``` + +This sends `[:custom_event, event, args]` over the same wire format the Hex `Bridge` GenServer used previously. + Override explicitly if needed: ```elixir diff --git a/lib/desktop/backend/browser.ex b/lib/desktop/backend/browser.ex index 50c2116..6e9e158 100644 --- a/lib/desktop/backend/browser.ex +++ b/lib/desktop/backend/browser.ex @@ -48,6 +48,9 @@ defmodule Desktop.Backend.Browser do @impl true def os_description, do: nil + @impl true + def custom_event(_event, _args), do: :ok + @impl true def activate_event_active?(_event), do: true @@ -121,6 +124,9 @@ defmodule Desktop.Backend.Browser do @impl true def rebuild(_frame, _url), do: nil + @impl true + def reload(_content), do: :ok + @impl true def put_webview_backend(name) do Desktop.Env.put(:webview_backend, name) diff --git a/lib/desktop/backend/json.ex b/lib/desktop/backend/json.ex index f8b4f30..78b66de 100644 --- a/lib/desktop/backend/json.ex +++ b/lib/desktop/backend/json.ex @@ -82,6 +82,15 @@ defmodule Desktop.Backend.Json do Protocol.call(:wx_misc, :getOsDescription, []) end + @impl true + def custom_event(event, args) do + if Process.whereis(Transport) do + Protocol.call(:custom_event, event, args) + end + + :ok + end + @impl true def activate_event_active?(_event), do: true @@ -218,6 +227,14 @@ defmodule Desktop.Backend.Json do :ok end + @impl true + def reload(nil), do: :ok + + def reload(webview) do + Protocol.call(:wxWebView, :reload, [webview]) + :ok + end + @impl true def current_url(nil, last_url), do: last_url diff --git a/lib/desktop/backend/wx.ex b/lib/desktop/backend/wx.ex index de077cf..721156a 100644 --- a/lib/desktop/backend/wx.ex +++ b/lib/desktop/backend/wx.ex @@ -80,6 +80,9 @@ defmodule Desktop.Backend.Wx do Null.wx_call(:wx_misc, :getOsDescription, []) end + @impl true + def custom_event(_event, _args), do: :ok + @impl true def activate_event_active?(event) do if function_exported?(:wxActivateEvent, :getActive, 1) do @@ -267,6 +270,14 @@ defmodule Desktop.Backend.Wx do :ok end + @impl true + def reload(nil), do: :ok + + def reload(webview) do + Null.wx_call(:wxWebView, :reload, [webview]) + :ok + end + @impl true def current_url(nil, last_url), do: last_url diff --git a/lib/desktop/bridge/mock.ex b/lib/desktop/bridge/mock.ex index 4573b42..c66dae5 100644 --- a/lib/desktop/bridge/mock.ex +++ b/lib/desktop/bridge/mock.ex @@ -15,6 +15,8 @@ defmodule Desktop.Bridge.Mock do def handle_method([:wxLocale | _]), do: ~c"en" def handle_method([:wx_misc, :launchDefaultBrowser | _]), do: :ok def handle_method([:wx_misc, :getOsDescription | _]), do: ~c"Mock OS" + def handle_method([:custom_event | _]), do: :ok + def handle_method([:wxWebView, :reload | _]), do: :ok def handle_method([type, :new | args]), do: [id: System.unique_integer([:positive]), type: type, args: args] diff --git a/lib/desktop/platform/content.ex b/lib/desktop/platform/content.ex index 28cd19a..89b9cea 100644 --- a/lib/desktop/platform/content.ex +++ b/lib/desktop/platform/content.ex @@ -14,6 +14,7 @@ defmodule Desktop.Platform.Content do only_open :: boolean() ) :: :ok @callback rebuild(frame :: term() | nil, last_url :: String.t() | nil) :: term() | nil + @callback reload(content :: term() | nil) :: :ok @callback put_webview_backend(name :: String.t()) :: :ok def attach(frame), do: Helpers.with_wx_env(fn -> impl().attach(frame) end) @@ -30,6 +31,15 @@ defmodule Desktop.Platform.Content do def rebuild(frame, last_url), do: Helpers.with_wx_env(fn -> impl().rebuild(frame, last_url) end) + @doc """ + Reloads the webview / native content handle. + + Replaces direct `:wxWebView.reload/1` so the call works on Wx and Json + (mobile bridge) backends. No-op when content is `nil` or on Browser. + """ + def reload(content), + do: Helpers.with_wx_env(fn -> impl().reload(content) end) + def put_webview_backend(name), do: Helpers.with_wx_env(fn -> impl().put_webview_backend(name) end) diff --git a/lib/desktop/platform/system.ex b/lib/desktop/platform/system.ex index 4f99537..08927a9 100644 --- a/lib/desktop/platform/system.ex +++ b/lib/desktop/platform/system.ex @@ -18,6 +18,7 @@ defmodule Desktop.Platform.System do @callback wx_available?() :: boolean() @callback open_external_url(String.t()) :: :ok @callback os_description() :: String.t() | charlist() | nil + @callback custom_event(event :: atom(), args :: list()) :: :ok @callback activate_event_active?(event :: term()) :: boolean() def init_env, do: impl().init_env() @@ -57,6 +58,17 @@ defmodule Desktop.Platform.System do end end + @doc """ + Sends a native-host custom event over the mobile bridge. + + Replaces direct `GenServer.call(Bridge, {:bridge_call, …})` with + `[:custom_event, event, args]` JSON. No-op (`:ok`) on Wx/Browser backends + or when the bridge transport is not running. + """ + def custom_event(event, args \\ []) when is_atom(event) and is_list(args) do + Helpers.with_wx_env(fn -> impl().custom_event(event, args) end) + end + def activate_event_active?(event), do: Helpers.with_wx_env(fn -> impl().activate_event_active?(event) end) diff --git a/lib/desktop/window.ex b/lib/desktop/window.ex index 17654bb..42754a7 100644 --- a/lib/desktop/window.ex +++ b/lib/desktop/window.ex @@ -271,6 +271,21 @@ defmodule Desktop.Window do GenServer.cast(pid, {:load_url, url}) end + @doc """ + Reload the Window webview / native content. + + Replaces direct `:wxWebView.reload/1` on the handle from `webview/1`. + + ## Examples + + iex> Desktop.Window.reload(pid) + :ok + + """ + def reload(pid) do + GenServer.cast(pid, :reload) + end + @doc """ Show the Window if not visible with the given url. @@ -634,6 +649,11 @@ defmodule Desktop.Window do {:noreply, %Window{ui | webview: Fallback.webview_rebuild(ui)}} end + def handle_cast(:reload, ui = %Window{webview: webview}) do + Platform.Content.reload(webview) + {:noreply, ui} + end + def handle_cast( {:show_notification, message, id, type, title, callback, timeout}, ui = %Window{notifications: noties, title: window_title} diff --git a/test/desktop/backend/browser_test.exs b/test/desktop/backend/browser_test.exs index 3e013e9..f9f027e 100644 --- a/test/desktop/backend/browser_test.exs +++ b/test/desktop/backend/browser_test.exs @@ -38,6 +38,11 @@ defmodule Desktop.Backend.BrowserTest do assert Browser.get_env() == nil assert Browser.locale() == nil assert Browser.os_description() == nil + assert :ok = Browser.custom_event(:share, []) refute Browser.wx_available?() end + + test "T-BRW: content reload is no-op" do + assert :ok = Browser.reload(nil) + end end diff --git a/test/desktop/backend/json_test.exs b/test/desktop/backend/json_test.exs index 04e5297..d4bb752 100644 --- a/test/desktop/backend/json_test.exs +++ b/test/desktop/backend/json_test.exs @@ -32,6 +32,20 @@ defmodule Desktop.Backend.JsonTest do assert Desktop.Platform.System.os_description() == "Mock OS" end + test "T-JSN: custom_event via bridge RPC" do + assert :ok = Json.custom_event(:share, ["/tmp/file"]) + assert :ok = Desktop.Platform.System.custom_event(:restart, []) + end + + test "T-JSN: content reload via bridge RPC" do + wx = Transport.ensure_started() + {:ok, _frame, webview} = Json.open(wx: wx, title: "t", size: {200, 200}, icon: nil) + + assert :ok = Json.reload(webview) + assert :ok = Desktop.Platform.Content.reload(webview) + assert :ok = Desktop.Platform.Content.reload(nil) + end + test "T-JSN: new frame handle shape" do wx = Transport.ensure_started() diff --git a/test/desktop/backend/wx_test.exs b/test/desktop/backend/wx_test.exs index 5fb6260..da7b40b 100644 --- a/test/desktop/backend/wx_test.exs +++ b/test/desktop/backend/wx_test.exs @@ -44,4 +44,18 @@ defmodule Desktop.Backend.WxTest do result = Desktop.Platform.System.os_description() assert result == nil or is_binary(result) end + + test "T-WX: custom_event is no-op" do + assert :ok = Desktop.Platform.System.custom_event(:share, []) + end + + @tag timeout: 10_000 + test "T-WX: content reload" do + wx = Desktop.Env.wx() + {:ok, frame, webview} = Wx.open(wx: wx, title: ~c"reload", size: {200, 200}, icon: nil) + + assert :ok = Desktop.Platform.Content.reload(webview) + assert :ok = Desktop.Platform.Content.reload(nil) + Wx.destroy_frame(frame) + end end diff --git a/test/desktop/platform_system_test.exs b/test/desktop/platform_system_test.exs index bbf0a12..31de39e 100644 --- a/test/desktop/platform_system_test.exs +++ b/test/desktop/platform_system_test.exs @@ -40,3 +40,35 @@ defmodule Desktop.Platform.SystemTest do assert PlatformSystem.os_description() == nil end end + +defmodule Desktop.Platform.SystemCustomEventTest do + use ExUnit.Case, async: false + + alias Desktop.Platform.System, as: PlatformSystem + + defmodule StubBackend do + def set_env(_env), do: :ok + + def custom_event(event, args) do + send(Process.get(:stub_test_pid), {:custom_event, event, args}) + :ok + end + end + + setup do + previous = Application.get_env(:desktop, :backend, :auto) + Application.put_env(:desktop, :backend, StubBackend) + Process.put(:stub_test_pid, self()) + + on_exit(fn -> + Application.put_env(:desktop, :backend, previous) + end) + + :ok + end + + test "custom_event delegates to backend" do + assert :ok = PlatformSystem.custom_event(:share, ["/tmp/a"]) + assert_receive {:custom_event, :share, ["/tmp/a"]} + end +end diff --git a/test/desktop/regression/beam_wx_calls_test.exs b/test/desktop/regression/beam_wx_calls_test.exs index 3de0832..f5cdfe2 100644 --- a/test/desktop/regression/beam_wx_calls_test.exs +++ b/test/desktop/regression/beam_wx_calls_test.exs @@ -36,6 +36,17 @@ defmodule Desktop.Regression.BeamWxCallsTest do assert PlatformSystem.os_description() == "Mock OS" end + test "custom_event uses bridge RPC not Hex Bridge GenServer" do + assert :ok = Json.custom_event(:open_with, ["/tmp/x"]) + assert :ok = PlatformSystem.custom_event(:message_read_up_to, ["0x01", 1]) + end + + test "content reload uses bridge RPC not OTP :wxWebView" do + wx = Transport.ensure_started() + {:ok, _frame, webview} = Json.open(wx: wx, title: "t", size: {100, 100}, icon: nil) + assert :ok = Desktop.Platform.Content.reload(webview) + end + test "open_external_url on Json uses bridge RPC" do assert :ok = Json.open_external_url("https://example.com") assert :ok = PlatformSystem.open_external_url("https://example.com") diff --git a/test/support/guard_platform_abstraction.exs b/test/support/guard_platform_abstraction.exs index 3b2ba0c..dca2b2e 100644 --- a/test/support/guard_platform_abstraction.exs +++ b/test/support/guard_platform_abstraction.exs @@ -19,7 +19,9 @@ forbidden_patterns = [ {~r/:wxLocale\.getSystemLanguage\s*\(/, "Json/mobile: pass :getSystemLanguage as a bridge atom to Protocol, never :wxLocale.getSystemLanguage/0"}, {~r/:wx_misc\.getOsDescription\s*\(/, - "use Desktop.Platform.System.os_description/0 instead of :wx_misc.getOsDescription/0"} + "use Desktop.Platform.System.os_description/0 instead of :wx_misc.getOsDescription/0"}, + {~r/:wxWebView\.reload\s*\(/, + "use Desktop.Window.reload/1 or Desktop.Platform.Content.reload/1 instead of :wxWebView.reload/1"} ] violations =