Packages

Write native GUI directly from Elixir using declarative API.

Current section

Files

Jump to
emerge lib emerge engine diff_state.ex
Raw

lib/emerge/engine/diff_state.ex

defmodule Emerge.Engine.DiffState do
@moduledoc """
Stateful diff helper that keeps numeric id assignments stable.
"""
alias Emerge.Engine.Reconcile
alias Emerge.Engine.Tree.Nearby
alias Emerge.Engine.VNode
@type t :: %__MODULE__{
tree: Emerge.Engine.Element.t() | nil,
vdom: VNode.t() | nil,
event_registry: %{binary() => %{term() => {pid(), term()}}}
}
defstruct tree: nil, vdom: nil, event_registry: %{}
@doc """
Initialize diff state with an optional tree.
"""
def new(tree \\ nil)
def new(nil), do: %__MODULE__{}
def new(tree) do
{vdom, tree} = Reconcile.assign_ids(tree)
%__MODULE__{tree: tree, vdom: vdom, event_registry: build_event_registry(tree)}
end
@doc """
Compute patches for a new tree and return {patch_binary, updated_state, assigned_tree}.
"""
@spec diff_and_encode(t(), Emerge.Engine.Element.t()) ::
{binary(), t(), Emerge.Engine.Element.t()}
def diff_and_encode(%__MODULE__{} = state, tree) do
{vdom, patches, assigned} = Reconcile.reconcile(state.vdom, tree)
{
Emerge.Engine.Patch.encode(patches),
%__MODULE__{tree: assigned, vdom: vdom, event_registry: build_event_registry(assigned)},
assigned
}
end
@spec dispatch_click(t(), binary()) :: :ok
def dispatch_click(%__MODULE__{} = state, id_bin) when is_binary(id_bin) do
dispatch_event(state, id_bin, :click)
end
@spec dispatch_event(t(), binary(), term()) :: :ok
def dispatch_event(%__MODULE__{event_registry: registry}, id_bin, event)
when is_binary(id_bin) do
dispatch_event_with_payload(%__MODULE__{event_registry: registry}, id_bin, event, :no_payload)
end
@spec dispatch_event(t(), binary(), term(), term()) :: :ok
def dispatch_event(%__MODULE__{event_registry: registry}, id_bin, event, payload)
when is_binary(id_bin) do
dispatch_event_with_payload(
%__MODULE__{event_registry: registry},
id_bin,
event,
{:with_payload, payload}
)
end
defp dispatch_event_with_payload(%__MODULE__{event_registry: registry}, id_bin, event, payload) do
case lookup_event(%__MODULE__{event_registry: registry}, id_bin, event) do
{:ok, {pid, msg}} when is_pid(pid) ->
send(pid, dispatch_message(msg, payload))
:ok
_ ->
:ok
end
end
defp dispatch_message(msg, :no_payload), do: msg
defp dispatch_message(msg, {:with_payload, payload}) when is_tuple(msg),
do: Tuple.insert_at(msg, tuple_size(msg), payload)
defp dispatch_message(msg, {:with_payload, payload}), do: {msg, payload}
@spec lookup_event(t(), binary(), term()) :: {:ok, {pid(), term()}} | :error
def lookup_event(%__MODULE__{event_registry: registry}, id_bin, event)
when is_binary(id_bin) do
case Map.get(registry, id_bin, %{}) |> Map.get(event) do
{pid, msg} when is_pid(pid) -> {:ok, {pid, msg}}
_ -> :error
end
end
def build_event_registry(tree) do
tree
|> collect_event_handlers(%{})
end
defp collect_event_handlers(%Emerge.Engine.Element{} = element, acc) do
acc
|> register_event(element, :on_click, :click)
|> register_event(element, :on_press, :press)
|> register_event(element, :on_swipe_up, :swipe_up)
|> register_event(element, :on_swipe_down, :swipe_down)
|> register_event(element, :on_swipe_left, :swipe_left)
|> register_event(element, :on_swipe_right, :swipe_right)
|> register_event(element, :on_mouse_down, :mouse_down)
|> register_event(element, :on_mouse_up, :mouse_up)
|> register_event(element, :on_mouse_enter, :mouse_enter)
|> register_event(element, :on_mouse_leave, :mouse_leave)
|> register_event(element, :on_mouse_move, :mouse_move)
|> register_event(element, :on_change, :change)
|> register_event(element, :on_focus, :focus)
|> register_event(element, :on_blur, :blur)
|> register_virtual_key_hold_event(element)
|> register_key_events(element, :on_key_down, :key_down)
|> register_key_events(element, :on_key_up, :key_up)
|> register_key_events(element, :on_key_press, :key_press)
|> then(fn registry ->
registry =
Enum.reduce(element.children, registry, fn child, next_registry ->
collect_event_handlers(child, next_registry)
end)
Enum.reduce(Nearby.nearby_children(element), registry, fn {_slot, child}, next_registry ->
collect_event_handlers(child, next_registry)
end)
end)
end
defp register_event(acc, element, attr, event) do
case Map.get(element.attrs, attr) do
{pid, msg} when is_pid(pid) ->
id_bin = :erlang.term_to_binary(element.id)
Map.update(acc, id_bin, %{event => {pid, msg}}, fn events ->
Map.put(events, event, {pid, msg})
end)
_ ->
acc
end
end
defp register_key_events(acc, element, attr, event_type) do
case Map.get(element.attrs, attr) do
bindings when is_list(bindings) ->
Enum.reduce(bindings, acc, fn binding, next_acc ->
register_key_event(next_acc, element, event_type, binding)
end)
_ ->
acc
end
end
defp register_key_event(acc, element, event_type, %{route: route, payload: {pid, msg}})
when is_binary(route) and is_pid(pid) do
id_bin = :erlang.term_to_binary(element.id)
event = {event_type, route}
Map.update(acc, id_bin, %{event => {pid, msg}}, fn events ->
Map.put(events, event, {pid, msg})
end)
end
defp register_key_event(acc, _element, _event_type, _binding), do: acc
defp register_virtual_key_hold_event(acc, element) do
case Map.get(element.attrs, :virtual_key) do
%{hold: {:event, {pid, msg}}} when is_pid(pid) ->
id_bin = :erlang.term_to_binary(element.id)
Map.update(acc, id_bin, %{virtual_key_hold: {pid, msg}}, fn events ->
Map.put(events, :virtual_key_hold, {pid, msg})
end)
_ ->
acc
end
end
end