Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/animation/gestures/gesture_server.ex
defmodule Raxol.Animation.Gestures.GestureServer do
@moduledoc """
GenServer implementation for Animation Gestures state management.
This server manages gesture state and animations without Process dictionary usage,
providing per-process gesture tracking and animation state management.
## Features
- Per-process gesture state tracking
- Touch/mouse event handling
- Gesture type detection and handling
- Physics-based animation management
- Automatic cleanup on process termination
"""
use GenServer
require Logger
alias Raxol.Animation.Physics.{PhysicsEngine, Vector}
# Gesture state structure
defmodule State do
@moduledoc false
defstruct [
:touch_start,
:touch_current,
:touch_history,
:start_time,
:current_time,
:active,
:gesture_type,
:velocity,
:handlers,
:active_animations
]
def new do
%__MODULE__{
touch_start: nil,
touch_current: nil,
touch_history: [],
start_time: nil,
current_time: nil,
active: false,
gesture_type: nil,
velocity: %Vector{},
handlers: %{},
active_animations: []
}
end
end
# Client API
@doc """
Starts the Animation Gestures server.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Returns a child specification for this server.
"""
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 5000
}
end
@doc """
Initializes gesture state for the calling process.
"""
def init_gestures(pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
GenServer.call(__MODULE__, {:init_gestures, pid})
end
@doc """
Registers a gesture handler for the calling process.
"""
def register_handler(gesture_type, handler, pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
GenServer.call(__MODULE__, {:register_handler, pid, gesture_type, handler})
end
@doc """
Handles a touch down event.
"""
def touch_down(position, time \\ nil, pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
time =
case time do
nil -> System.monotonic_time(:millisecond)
t -> t
end
GenServer.call(__MODULE__, {:touch_down, pid, position, time})
end
@doc """
Handles a touch move event.
"""
def touch_move(position, time \\ nil, pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
time =
case time do
nil -> System.monotonic_time(:millisecond)
t -> t
end
GenServer.call(__MODULE__, {:touch_move, pid, position, time})
end
@doc """
Handles a touch up event.
"""
def touch_up(position, time \\ nil, pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
time =
case time do
nil -> System.monotonic_time(:millisecond)
t -> t
end
GenServer.call(__MODULE__, {:touch_up, pid, position, time})
end
@doc """
Updates animations for the calling process.
"""
def update_animations(delta_time \\ nil, pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
GenServer.call(__MODULE__, {:update_animations, pid, delta_time})
end
@doc """
Gets animation objects for the calling process.
"""
def get_animation_objects(pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
GenServer.call(__MODULE__, {:get_animation_objects, pid})
end
@doc """
Gets gesture state for the calling process.
"""
def get_state(pid \\ nil) do
pid =
case pid do
nil -> self()
p -> p
end
GenServer.call(__MODULE__, {:get_state, pid})
end
# Server Callbacks
@impl true
def init(_opts) do
state = %{
# Map of pid -> gesture_state
process_states: %{},
# Map of pid -> monitor_ref
monitors: %{}
}
{:ok, state}
end
@impl true
def handle_call({:init_gestures, pid}, _from, state) do
gesture_state = State.new()
state = ensure_monitored(pid, state)
process_states = Map.put(state.process_states, pid, gesture_state)
{:reply, :ok, %{state | process_states: process_states}}
end
@impl true
def handle_call({:register_handler, pid, gesture_type, handler}, _from, state) do
state = ensure_monitored(pid, state)
updated_state =
update_process_state(state, pid, fn gesture_state ->
handlers =
Map.update(
gesture_state.handlers,
gesture_type,
[handler],
&[handler | &1]
)
%{gesture_state | handlers: handlers}
end)
{:reply, :ok, updated_state}
end
@impl true
def handle_call({:touch_down, pid, position, time}, _from, state) do
state = ensure_monitored(pid, state)
updated_state =
update_process_state(state, pid, fn gesture_state ->
%{
gesture_state
| touch_start: position,
touch_current: position,
touch_history: [position],
start_time: time,
current_time: time,
active: true,
gesture_type: nil
}
end)
{:reply, :ok, updated_state}
end
@impl true
def handle_call({:touch_move, pid, position, time}, _from, state) do
updated_state =
update_process_state(state, pid, fn gesture_state ->
case gesture_state.active do
true ->
# Calculate velocity
time_diff = max(1, time - gesture_state.current_time)
{prev_x, prev_y} = gesture_state.touch_current
{curr_x, curr_y} = position
velocity = %Vector{
x: (curr_x - prev_x) / time_diff * 1000,
y: (curr_y - prev_y) / time_diff * 1000
}
%{
gesture_state
| touch_current: position,
touch_history:
[position | gesture_state.touch_history] |> Enum.take(10),
current_time: time,
velocity: velocity,
gesture_type: detect_gesture_type(gesture_state, :move)
}
false ->
gesture_state
end
end)
{:reply, :ok, updated_state}
end
@impl true
def handle_call({:touch_up, pid, position, time}, _from, state) do
updated_state =
update_process_state(state, pid, fn gesture_state ->
case gesture_state.active do
true ->
# Finalize gesture and trigger handlers
final_gesture_type = detect_gesture_type(gesture_state, :up)
gesture_data = build_gesture_data(gesture_state, position, time)
# Call handlers asynchronously
call_handlers_async(
final_gesture_type,
gesture_data,
gesture_state.handlers
)
# Create animation if applicable
animations =
create_animation_if_applicable(
final_gesture_type,
gesture_data,
gesture_state.active_animations
)
%{
gesture_state
| touch_current: position,
current_time: time,
active: false,
gesture_type: final_gesture_type,
active_animations: animations
}
false ->
gesture_state
end
end)
{:reply, :ok, updated_state}
end
@impl true
def handle_call({:update_animations, pid, delta_time}, _from, state) do
# ~60fps
delta_time =
case delta_time do
nil -> 16.67
dt -> dt
end
updated_state =
update_process_state(state, pid, fn gesture_state ->
# Update physics worlds for all animations
updated_animations =
Enum.map(gesture_state.active_animations, fn animation ->
updated_world =
PhysicsEngine.step(animation.world, delta_time / 1000.0)
%{animation | world: updated_world}
end)
# Remove completed animations
active_animations =
Enum.reject(updated_animations, &animation_completed?/1)
%{gesture_state | active_animations: active_animations}
end)
{:reply, :ok, updated_state}
end
@impl true
def handle_call({:get_animation_objects, pid}, _from, state) do
gesture_state = get_process_state(state, pid)
objects = map_animation_objects(gesture_state.active_animations)
{:reply, {gesture_state, objects}, state}
end
@impl true
def handle_call({:get_state, pid}, _from, state) do
gesture_state = get_process_state(state, pid)
{:reply, gesture_state, state}
end
@impl true
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
# Clean up state for dead process
state = %{
state
| process_states: Map.delete(state.process_states, pid),
monitors: Map.delete(state.monitors, pid)
}
{:noreply, state}
end
# Private helpers
defp ensure_monitored(pid, state) do
case Map.has_key?(state.monitors, pid) do
true ->
state
false ->
ref = Process.monitor(pid)
%{state | monitors: Map.put(state.monitors, pid, ref)}
end
end
defp get_process_state(state, pid) do
Map.get(state.process_states, pid, State.new())
end
defp update_process_state(state, pid, fun) do
gesture_state = get_process_state(state, pid)
updated_gesture_state = fun.(gesture_state)
process_states = Map.put(state.process_states, pid, updated_gesture_state)
%{state | process_states: process_states}
end
defp detect_gesture_type(gesture_state, event_type) do
# Simple gesture detection logic
case event_type do
:move ->
case {gesture_state.gesture_type == nil,
Vector.magnitude(gesture_state.velocity) > 100} do
{true, true} -> :drag
_ -> gesture_state.gesture_type
end
:up ->
duration = gesture_state.current_time - gesture_state.start_time
distance =
calculate_distance(
gesture_state.touch_start,
gesture_state.touch_current
)
velocity_magnitude = Vector.magnitude(gesture_state.velocity)
case {velocity_magnitude > 200, duration > 500, distance > 10,
distance > 5} do
{true, _, true, _} -> :swipe
{_, true, false, false} -> :long_press
{_, _, _, true} -> :drag
_ -> :tap
end
end
end
defp calculate_distance({x1, y1}, {x2, y2}) do
:math.sqrt(:math.pow(x2 - x1, 2) + :math.pow(y2 - y1, 2))
end
defp build_gesture_data(gesture_state, position, time) do
%{
start_position: gesture_state.touch_start,
end_position: position,
velocity: gesture_state.velocity,
duration: time - gesture_state.start_time,
direction: calculate_direction(gesture_state.touch_start, position),
distance: calculate_distance(gesture_state.touch_start, position)
}
end
defp calculate_direction({x1, y1}, {x2, y2}) do
dx = x2 - x1
dy = y2 - y1
case abs(dx) > abs(dy) do
true ->
case dx > 0 do
true -> :right
false -> :left
end
false ->
case dy > 0 do
true -> :down
false -> :up
end
end
end
defp call_handlers_async(gesture_type, gesture_data, handlers) do
handler_list = Map.get(handlers, gesture_type, [])
# Execute handlers in separate task to avoid blocking
case length(handler_list) do
0 ->
:ok
_ ->
Task.start(fn ->
Enum.each(handler_list, fn handler ->
case Raxol.Core.ErrorHandling.safe_call(fn ->
handler.(gesture_data)
end) do
{:ok, _result} ->
:ok
{:error, reason} ->
Logger.warning("Gesture handler failed: #{inspect(reason)}")
end
end)
end)
end
end
defp create_animation_if_applicable(
gesture_type,
gesture_data,
current_animations
) do
case gesture_type do
:swipe ->
create_swipe_animation(gesture_data, current_animations)
:drag ->
create_drag_animation(gesture_data, current_animations)
_ ->
current_animations
end
end
defp create_swipe_animation(gesture_data, animations) do
# Create a simple physics world for swipe animation
world = PhysicsEngine.create_world()
# Add object representing the swiped element
world =
PhysicsEngine.add_object(world, "swipe_element", %{
position: gesture_data.start_position,
velocity: %Vector{
x: gesture_data.velocity.x * 0.5,
y: gesture_data.velocity.y * 0.5
},
mass: 1.0,
friction: 0.8
})
[
%{
id: "swipe_#{:erlang.unique_integer([:positive, :monotonic])}",
type: :swipe,
world: world,
start_time: System.monotonic_time(:millisecond),
duration: 1000,
data: gesture_data
}
| animations
]
end
defp create_drag_animation(gesture_data, animations) do
# Create physics world for drag animation
world = PhysicsEngine.create_world()
world =
PhysicsEngine.add_object(world, "drag_element", %{
position: gesture_data.end_position,
velocity: %Vector{
x: gesture_data.velocity.x * 0.3,
y: gesture_data.velocity.y * 0.3
},
mass: 1.2,
friction: 0.9
})
[
%{
id: "drag_#{:erlang.unique_integer([:positive, :monotonic])}",
type: :drag,
world: world,
start_time: System.monotonic_time(:millisecond),
duration: 800,
data: gesture_data
}
| animations
]
end
defp animation_completed?(animation) do
current_time = System.monotonic_time(:millisecond)
elapsed = current_time - animation.start_time
case elapsed > animation.duration do
true ->
true
false ->
# Check if all objects have essentially stopped moving
Enum.all?(animation.world.objects, fn {_, obj} ->
Vector.magnitude(obj.velocity) < 0.5
end)
end
end
defp map_animation_objects(animations) do
Enum.flat_map(animations, fn animation ->
Enum.map(animation.world.objects, fn {id, obj} ->
%{
id: "#{animation.id}_#{id}",
type: animation.type,
position: obj.position,
velocity: obj.velocity,
animation_data: animation.data
}
end)
end)
end
end