Current section
Files
Jump to
Current section
Files
lib/mgui_ex/server.ex
defmodule MguiEx.Scene.Server do
@moduledoc """
GenServer wrapper for a scene module.
Handles:
- Calling the scene's `init/1` to get initial state
- Calling `render/1` on state changes and sending the tree to Swift
- Routing UI events from Swift to `handle_event/3`
- Routing Erlang messages to `handle_info/2`
- Coalescing rapid state changes (only renders once per message batch)
Users don't interact with this directly — use `MguiEx.SceneManager`.
"""
use GenServer
require Logger
defstruct [:module, :state, :active]
# -------------------------------------------------------------------
# Public API (called by SceneManager)
# -------------------------------------------------------------------
def start_link({module, args}) do
GenServer.start_link(__MODULE__, {module, args}, name: module)
end
@doc "Send a UI event to this scene"
def send_event(scene, node_id, event) do
GenServer.cast(scene, {:ui_event, node_id, event})
end
@doc "Mark this scene as active (will render to popover)"
def activate(scene) do
GenServer.cast(scene, :activate)
end
@doc "Mark this scene as inactive (still running, won't render)"
def deactivate(scene) do
GenServer.cast(scene, :deactivate)
end
@doc "Get the current state (for debugging)"
def get_state(scene) do
GenServer.call(scene, :get_state)
end
# -------------------------------------------------------------------
# GenServer callbacks
# -------------------------------------------------------------------
@impl true
def init({module, args}) do
Logger.debug("Scene.Server: starting #{inspect(module)}")
state = module.init(args)
{:ok, %__MODULE__{module: module, state: state, active: false}}
end
@impl true
def handle_cast({:ui_event, node_id, event}, %{module: module, state: state} = s) do
new_state = module.handle_event(node_id, event, state)
if new_state == state do
{:noreply, s}
else
s = %{s | state: new_state}
maybe_render(s)
{:noreply, s}
end
end
def handle_cast(:activate, s) do
s = %{s | active: true}
do_render(s)
{:noreply, s}
end
def handle_cast(:deactivate, s) do
{:noreply, %{s | active: false}}
end
@impl true
def handle_call(:get_state, _from, s) do
{:reply, s.state, s}
end
@impl true
def handle_info(msg, %{module: module, state: state} = s) do
if function_exported?(module, :handle_info, 2) do
case module.handle_info(msg, state) do
{:noreply, new_state} ->
if new_state == state do
{:noreply, s}
else
s = %{s | state: new_state}
maybe_render(s)
{:noreply, s}
end
{:stop, reason, new_state} ->
{:stop, reason, %{s | state: new_state}}
other ->
Logger.warning("Scene.Server(#{inspect(module)}): handle_info returned unexpected: #{inspect(other)}")
{:noreply, s}
end
else
Logger.debug("Scene.Server(#{inspect(module)}): ignoring #{inspect(msg)}")
{:noreply, s}
end
end
# -------------------------------------------------------------------
# Private
# -------------------------------------------------------------------
defp maybe_render(%{active: true} = s), do: do_render(s)
defp maybe_render(_s), do: :ok
defp do_render(%{module: module, state: state}) do
MguiEx.View.reset_ids()
tree = module.render(state)
opts =
if function_exported?(module, :status_bar, 1) do
module.status_bar(state)
else
[]
end
MguiEx.SwiftPort.render(tree, opts)
end
end