defmodule FloUI.Grid do @moduledoc """ ## Usage in SnapFramework Render this with children passed to it to automatically lay the children out in the grid. The children must be given width and height styles for it to work. Inspired by https://github.com/BWheatie/scenic_layout_o_matic data is a map in the form of ` elixir %{start_xy: {0, 0}, max_xy: {100, 100}}` ``` elixir <%= component FloUI.Grid, %{ start_xy: {0, 0}, max_xy: {48 * 3, 48} }, translate: {20, 120} do %> <%= component FloUI.Icon.Button, "Close", id: :icon_button, width: 48, height: 48, translate: {20, 120} do %> <%= component FloUI.Icon, {:flo_ui, "icons/clear_white.png"} %> <% end %> <%= component FloUI.Icon.Button, "Close", id: :icon_button, width: 48, height: 48, translate: {20, 120} do %> <%= component FloUI.Icon, {:flo_ui, "icons/clear_white.png"} %> <% end %> <%= component FloUI.Icon.Button, "Close", id: :icon_button, width: 48, height: 48, translate: {20, 120} do %> <%= component FloUI.Icon, {:flo_ui, "icons/clear_white.png"} %> <% end %> <% end %> ``` """ use SnapFramework.Component, name: :grid, template: "lib/layout/grid.eex", controller: :none, assigns: [last_height: 0, component_xy: {0, 0}, start_xy: {0, 0}, grid_xy: {0, 0}, max_xy: {0, 0}], opts: [] defcomponent :grid, :map def setup(%{assigns: %{data: %{start_xy: start_xy, max_xy: max_xy}} = assigns} = scene) do assigns = %{assigns | component_xy: start_xy, start_xy: start_xy, grid_xy: start_xy, max_xy: max_xy} {_layout, children} = Enum.reduce(Enum.with_index(assigns.children), {assigns, []}, fn child, acc -> do_layout(child, acc) end) assign(scene, children: children) end def process_info(info, scene) do send_parent(scene, info) {:noreply, scene} end def process_update(data, _opts, scene) do assigns = %{scene.assigns | component_xy: scene.assigns.start_xy, start_xy: scene.assigns.start_xy, grid_xy: scene.assigns.start_xy, max_xy: scene.assigns.max_xy} {_layout, children} = Enum.reduce(Enum.with_index(assigns.children), {assigns, []}, fn child, acc -> do_layout(child, acc) end) {:noreply, assign(scene, children: children)} end defp do_layout({[ type: _, module: _, data: _, opts: _ ] = child, i}, {layout, child_list}) when is_list(child) do case translate(child, layout) do {:error, error} -> {:error, error} new_layout -> translate = new_layout.component_xy updated_child = [ type: child[:type], module: child[:module], data: child[:data], opts: Keyword.put(child[:opts], :translate, translate) ] {new_layout, List.insert_at(child_list, i, updated_child)} # Map.put(new_layout, :children, List.replace_at(new_layout.children, i, updated_child)) end end defp do_layout({[ type: _, module: _, data: _, opts: _, children: _, ] = child, i}, {layout, child_list}) when is_list(child) do case translate(child, layout) do {:error, error} -> {:error, error} new_layout -> translate = new_layout.component_xy updated_child = [ type: child[:type], module: child[:module], data: child[:data], opts: Keyword.put(child[:opts], :translate, translate), children: child[:children] ] {new_layout, List.insert_at(child_list, i, updated_child)} # Map.put(new_layout, :children, List.replace_at(new_layout.children, i, updated_child)) end end defp do_layout({[ type: _, module: _, data: _, children: _, opts: _, ] = child, i}, {layout, child_list}) when is_list(child) do case translate(child, layout) do {:error, error} -> {:error, error} new_layout -> translate = new_layout.component_xy updated_child = [ type: child[:type], module: child[:module], data: child[:data], children: child[:children], opts: Keyword.put(child[:opts], :translate, translate) ] {new_layout, List.insert_at(child_list, i, updated_child)} # Map.put(new_layout, :children, List.replace_at(new_layout.children, i, updated_child)) end end defp do_layout({child, _i}, {layout, child_list}) when is_list(child) do Enum.reduce(Enum.with_index(child), {layout, child_list}, fn nchild, acc -> do_layout(nchild, acc) end) end defp do_layout(_, layout) do layout end defp translate( child, %{ last_height: last_height, start_xy: start_xy, grid_xy: grid_xy, max_xy: max_xy } = layout ) do width = child[:opts][:width] height = child[:opts][:height] {grid_x, _grid_y} = grid_xy {start_x, start_y} = start_xy new_x = start_x + width case start_xy == max_xy do true -> layout |> Map.put(:start_xy, {start_x, start_y}) |> Map.put(:last_height, height) false -> # already in a new group, use start_xy case fits_in_x?(new_x, max_xy) do # fits in x true -> # fit in y? case fits_in_y?(start_y, max_xy) do true -> # fits layout |> Map.put(:start_xy, {new_x, start_y}) |> Map.put(:component_xy, {start_x, start_y}) |> Map.put(:last_height, height) # Does not fit false -> {:error, "Does not fit in grid"} end # doesnt fit in x false -> # fit in new y? new_y = start_y + last_height case fits_in_y?(new_y, max_xy) do true -> layout |> Map.put(:start_xy, {grid_x + width, new_y}) |> Map.put(:component_xy, {grid_x, new_y}) |> Map.put(:last_height, height) false -> {:error, "Does not fit in the grid"} end end end end defp fits_in_x?(potential_x, {max_x, _}), do: potential_x <= max_x defp fits_in_y?(potential_y, {_, max_y}), do: potential_y <= max_y end