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