% % This file is part of AtomVM. % % Copyright 2021-2022 Davide Bettio % % Licensed under the Apache License, Version 2.0 (the "License"); % you may not use this file except in compliance with the License. % You may obtain a copy of the License at % % http://www.apache.org/licenses/LICENSE-2.0 % % Unless required by applicable law or agreed to in writing, software % distributed under the License is distributed on an "AS IS" BASIS, % WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. % See the License for the specific language governing permissions and % limitations under the License. % % SPDX-License-Identifier: Apache-2.0 % %% @doc A GenServer wrapper that adds graphical display capabilities. %% %% `avm_scene' extends `gen_server' behavior by automatically handling display updates %% when callbacks return `[{push, DisplayList}]'. %% It manages the connection to a display driver and optionally subscribes to input events. %% %% This module acts as a layer between your scene logic and the display driver, %% intercepting GenServer callback return values to check for display update commands. %% When a callback returns `[{push, DisplayList}]' as the last element of its return %% tuple, the display list is automatically sent to the configured display driver. %% %% For more information about display lists and supported primitives, see %% AtomGL documentation. %% %% == Example Usage (Erlang) == %% %% ``` %% -module(my_scene). %% -export([start_link/2, init/1, handle_info/2]). %% %% start_link(Args, Opts) -> %% avm_scene:start_link(?MODULE, Args, Opts). %% %% init(_Args) -> %% erlang:send_after(100, self(), update_display), %% {ok, #{width => 320, height => 240}}. %% %% handle_info(update_display, State = #{width := Width, height := Height}) -> %% Items = [ %% {text, 10, 20, default16px, 16#000000, 16#FFFFFF, "Hello, World!"}, %% {rect, 0, 0, Width, Height, 16#FFFFFF} %% ], %% {noreply, State, [{push, Items}]}. %% ''' %% %% == Example Usage (Elixir) == %% %% ``` %% defmodule MyScene do %% def start_link(args, opts) do %% :avm_scene.start_link(__MODULE__, args, opts) %% end %% %% def init(_args) do %% :erlang.send_after(100, self(), :update_display) %% {:ok, %{width: 320, height: 240}} %% end %% %% def handle_info(:update_display, %{width: width, height: height} = state) do %% items = [ %% {:text, 10, 20, :default16px, 0x000000, 0xFFFFFF, "Hello, World!"}, %% {:rect, 0, 0, width, height, 0xFFFFFF} %% ] %% {:noreply, state, [{:push, items}]} %% end %% %% # Optional: handle input events %% def handle_input(event_data, timestamp, pid, state) do %% IO.puts("Input event: #{inspect(event_data)}") %% {:noreply, state} %% end %% end %% ''' %% %% == Starting a Scene == %% %% Erlang: %% ``` %% %% Open display port %% Display = erlang:open_port({spawn, "display"}, DisplayOpts), %% %% %% Start scene with display %% {ok, Pid} = my_scene:start_link([], [ %% {display_server, {port, Display}}, %% {input_server, InputPid} % optional %% ]). %% ''' %% %% Elixir: %% ``` %% # Open display port %% display = :erlang.open_port({:spawn, "display"}, display_opts) %% %% # Start scene with display %% {:ok, pid} = MyScene.start_link([], [ %% display_server: {:port, display}, %% input_server: input_pid # optional %% ]) %% ''' %% %% == Options == %% %% %% %% == Callbacks == %% %% Your scene module should implement standard GenServer callbacks plus optionally: %% %% %% %% All callbacks can return an additional `[{push, DisplayList}]' element to trigger %% a display update. %% %% @end -module(avm_scene). -behavior(gen_server). -export([ start/3, start_link/3, start_monitor/3 ]). -export([ init/1, handle_call/3, handle_cast/2, handle_info/2 ]). -record(state, { module = undefined, display_module = undefined, display = undefined, scene_state = undefined }). %% @doc Starts an avm_scene process. %% %% @param Module The callback module implementing your scene logic %% @param Args Arguments passed to your module's `init/1' callback %% @param Options Options including `display_server' (required) and `input_server' (optional), %% plus any standard GenServer options %% @returns Same as `gen_server:start/3' %% %% @see start_link/3 %% @see start_monitor/3 %% @end -spec start(Module :: module(), Args :: any(), Options :: list()) -> gen_server:start_ret(). start(Module, Args, Options) -> NoDisplayOptions = lists:keydelete(display_server, 1, Options), GenServerOptions = lists:keydelete(input_server, 1, NoDisplayOptions), gen_server:start(?MODULE, [{Module, Options} | Args], GenServerOptions). %% @doc Starts an avm_scene process linked to the current process. %% %% @param Module The callback module implementing your scene logic %% @param Args Arguments passed to your module's `init/1' callback %% @param Options Options including `display_server' (required) and `input_server' (optional), %% plus any standard GenServer options %% @returns Same as `gen_server:start_link/3' %% %% Erlang Example: %% ``` %% avm_scene:start_link(my_scene, [], [ %% {display_server, {port, Display}}, %% {name, {local, my_scene}} %% ]). %% ''' %% %% Elixir Example: %% ``` %% :avm_scene.start_link(MyScene, [], [ %% display_server: {:port, display}, %% name: {:local, :my_scene} %% ]) %% ''' %% %% @see start/3 %% @see start_monitor/3 %% @end -spec start_link(Module :: module(), Args :: any(), Options :: list()) -> gen_server:start_ret(). start_link(Module, Args, Options) -> NoDisplayOptions = lists:keydelete(display_server, 1, Options), GenServerOptions = lists:keydelete(input_server, 1, NoDisplayOptions), gen_server:start_link(?MODULE, [{Module, Options} | Args], GenServerOptions). %% @doc Starts an avm_scene process and returns both PID and monitor reference. %% %% @param Module The callback module implementing your scene logic %% @param Args Arguments passed to your module's `init/1' callback %% @param Options Options including `display_server' (required) and `input_server' (optional), %% plus any standard GenServer options %% @returns `{ok, {Pid, MonitorRef}}' or `{error, Reason}' %% %% @see start/3 %% @see start_link/3 %% @end -spec start_monitor(Module :: module(), Args :: any(), Options :: list()) -> {ok, {pid(), reference()}} | {error, any()}. start_monitor(Module, Args, Options) -> NoDisplayOptions = lists:keydelete(display_server, 1, Options), GenServerOptions = lists:keydelete(input_server, 1, NoDisplayOptions), gen_server:start_monitor(?MODULE, [{Module, Options} | Args], GenServerOptions). %% @private %% @doc GenServer callback - initializes the scene state. %% @end init([{Module, Options} | Args]) -> DisplayServer = proplists:get_value(display_server, Options), case DisplayServer of {DisplayModule, Display} when is_atom(DisplayModule) -> InputServer = proplists:get_value(input_server, Options), maybe_subscribe_input(InputServer), {ok, SceneState} = Module:init(Args), {ok, #state{ module = Module, display_module = DisplayModule, display = Display, scene_state = SceneState }}; _Invalid -> {stop, {error, invalid_display}} end. %% @private maybe_subscribe_input(InputServer) when is_pid(InputServer) -> gen_server:call(InputServer, {subscribe_input}); maybe_subscribe_input(_NoInput) -> ok. %% @private %% @doc GenServer callback - handles synchronous calls. %% @end handle_call(Msg, From, State) -> Module = State#state.module, Result = Module:handle_call(Msg, From, State#state.scene_state), maybe_update_scene(Result, State). %% @private %% @doc GenServer callback - handles asynchronous casts. %% @end handle_cast(Msg, State) -> Module = State#state.module, Result = Module:handle_cast(Msg, State#state.scene_state), maybe_update_scene(Result, State). %% @private %% @doc GenServer callback - handles info messages and input events. %% %% This callback specially handles `{input_event, Pid, Timestamp, EventData}' messages %% by forwarding them to the scene module's `handle_input/4' callback if it exists. %% @end handle_info({input_event, Pid, Ts, EventData}, State) when is_pid(Pid) and is_integer(Ts) -> Module = State#state.module, case erlang:function_exported(Module, handle_input, 4) of true -> Result = Module:handle_input(EventData, Ts, Pid, State#state.scene_state), maybe_update_scene(Result, State); false -> {noreply, State} end; handle_info(Msg, State) -> Module = State#state.module, Result = Module:handle_info(Msg, State#state.scene_state), maybe_update_scene(Result, State). %% @private %% @doc Processes callback results and triggers display updates when needed. %% %% Checks if the callback returned a `[{push, Scene}]' directive and if so, %% sends the scene to the display driver via `DisplayModule:call(Display, {update, Scene})'. %% @end maybe_update_scene(Result, State) -> case Result of {Action, Reply, NewSceneState, [{push, Scene}]} -> DisplayModule = State#state.display_module, DisplayModule:call(State#state.display, {update, Scene}), {Action, Reply, State#state{scene_state = NewSceneState}}; {Action, NewSceneState, [{push, Scene}]} -> DisplayModule = State#state.display_module, DisplayModule:call(State#state.display, {update, Scene}), {Action, State#state{scene_state = NewSceneState}}; {Action, Reply, NewSceneState} -> {Action, Reply, State#state{scene_state = NewSceneState}}; {Action, NewSceneState} -> {Action, State#state{scene_state = NewSceneState}} end.