Current section

Files

Jump to
elementtui lib component box.ex
Raw

lib/component/box.ex

defmodule ElementTui.Component.Box do
defstruct direction: {0, 1}, elements: []
def new(elements, direction) do
%ElementTui.Component.Box{elements: elements, direction: direction}
end
end
defimpl ElementTui.Component, for: ElementTui.Component.Box do
alias ElementTui.Component.Box
alias ElementTui.FocusStream
def calculate_dim(%Box{elements: %FocusStream{} = fs, direction: dir}, width, height) do
{xs, ys} = fs_helper(fs, dir, width, height)
calculate_dim(Box.new(Enum.reverse(xs) ++ ys, dir), width, height)
end
def calculate_dim(%Box{elements: es, direction: {1, 0}}, width, height) do
{total_width, approx_height} =
Enum.reduce(es, {0, 0}, fn el, {curr_w, curr_h} ->
# TODO: should we carry over hflex and/or vflex?
{w, h, _kw} = ElementTui.Parser.calculate_dim(el, width - curr_w, height)
{w + curr_w, max(curr_h, h)}
end)
{total_width, approx_height, Keyword.new()}
end
def calculate_dim(%Box{elements: es, direction: {0, 1}}, width, height) do
{max_width, total_height} =
Enum.reduce(es, {0, 0}, fn el, {curr_w, curr_h} ->
# TODO: should we carry over hflex and/or vflex?
{w, h, _kw} = ElementTui.Parser.calculate_dim(el, width, height - curr_h)
{max(curr_w, w), h + curr_h}
end)
{max_width, total_height, Keyword.new()}
end
def parse(
%Box{elements: %FocusStream{} = fs, direction: dir},
x,
y,
width,
height
) do
{xs, ys} = fs_helper(fs, dir, width, height)
Box.new(Enum.reverse(xs) ++ ys, dir)
|> parse(x, y, width, height)
end
def parse(%Box{elements: es, direction: {1, 0}}, x, y, width, height) do
# Map over it, while tracking the curr_w so we can calculate the total width
# and find the maximum height needed by any element
{dims, {total_width, flex}} =
Enum.map_reduce(es, {0, false}, fn el, {curr_w, flex} = acc ->
if curr_w >= width do
{nil, acc}
else
{w, h, kw} = ElementTui.Parser.calculate_dim(el, width - curr_w, height)
acc =
case Keyword.get(kw, :hflex, nil) do
nil ->
{w + curr_w, flex}
_ ->
{curr_w, true}
end
{{w, h, kw}, acc}
end
end)
# Filter out the values not needed
dims = dims |> Enum.take_while(fn x -> x end)
ws =
case {width - total_width, flex} do
{x, flex} when x <= 0 or flex == false ->
dims
|> Enum.map(fn {w, _, _} -> w end)
{x, _} ->
dims
|> Enum.map(fn {w, _, kw} ->
{w, [flex: Keyword.get(kw, :hflex, nil)]}
end)
|> ElementTui.Component.Flex.distribute(x)
end
{xs, _} =
ws
|> Enum.map_reduce(x, fn w, acc ->
{acc, acc + w}
end)
Enum.zip([xs, es, ws])
|> Enum.reduce(
%{elements: [], width: 0, height: 0},
fn {x, el, w},
%{
elements: es_acc,
height: h_acc,
width: w_acc
} = acc ->
%{elements: elements, height: curr_h, width: curr_w} =
ElementTui.Parser.parse(el, x, y, w, height)
if curr_w != w do
# {:ok, path} = Briefly.create()
# File.write!(path <> ".ex", "#{inspect({element, x, y, width, height})}")
# raise "hbox: width should be equal, got #{curr_w} and #{w}, #{path}"
raise "hbox: width should be equal, got #{curr_w} and #{w}"
end
%{acc | elements: es_acc ++ elements, height: max(curr_h, h_acc), width: curr_w + w_acc}
end
)
end
def parse(%Box{elements: es, direction: {0, 1}}, x, y, width, height) do
# NOTE: Logic here is the same as the hbox case, but with the width and height swapped
{dims, {total_height, flex}} =
Enum.map_reduce(es, {0, false}, fn el, {curr_h, flex} = acc ->
if curr_h >= height do
{nil, acc}
else
{w, h, kw} = ElementTui.Parser.calculate_dim(el, width, height - curr_h)
acc =
case Keyword.get(kw, :vflex, nil) do
nil ->
{curr_h + h, flex}
_ ->
# {curr_h + h, max(flex_height, 0) + h}
{curr_h, true}
end
{{w, h, kw}, acc}
end
end)
# Filter out the values not needed
dims = dims |> Enum.take_while(fn x -> x end)
hs =
case {height - total_height, flex} do
{x, flex} when x <= 0 or flex == false ->
dims |> Enum.map(fn {_, h, _} -> h end)
{x, _} ->
dims
|> Enum.map(fn {_, x, kw} ->
{x, [flex: Keyword.get(kw, :vflex, nil)]}
end)
|> ElementTui.Component.Flex.distribute(x)
end
{ys, _} =
hs
|> Enum.map_reduce(y, fn h, acc ->
{acc, acc + h}
end)
alias ElementTui.Parser.ParseResult
Enum.zip([ys, es, hs])
|> Enum.reduce(
ParseResult.new([], 0, 0),
fn {y, el, h}, acc ->
parsed =
ElementTui.Parser.parse(el, x, y, width, h)
%{height: curr_h, width: curr_w} = parsed
if curr_h != h do
raise "vbox: height should be equal, got #{curr_h} and #{h}}"
end
ParseResult.merge(parsed, acc, max(curr_w, acc.width), acc.height + curr_h)
end
)
end
def horizontal?(dir) do
dir == {1, 0}
end
# FocusStream/scroll helpers
defp fs_helper(%{pre: pre, post: post}, dir, available_width, available_height) do
# TODO: Return the used width and height, note that this is not easy to do while also be flexible on the direction
# because for the non changing direction we need to track both the maximum used by any element, while always passing
# the original on
next = Enum.at(post, 0)
{ys, {cond_y, available_width, available_height}} =
case next do
nil ->
{[], {:cont, available_width, available_height}}
el ->
wh = ElementTui.Parser.calculate_dim(el, available_width, available_height)
{[el], fs_update_dim(wh, dir, available_width, available_height)}
end
cond do
cond_y != :cont ->
{[], ys}
true ->
next = Enum.at(pre, 0)
{xs, {cond_x, available_width_x, available_height_x}} =
case next do
nil ->
{[], {:cont, available_width, available_height}}
el ->
# NOTE: we check if the element fits in the available space, note that when calling it we add 1 to the width/height
# this way we will know if it rather takes more space.
# TODO: Consider if calculate_dim should return the actual minimum space it needs, then we would not need to provide
# the 1.
wh =
case horizontal?(dir) do
true ->
ElementTui.Parser.calculate_dim(el, available_width + 1, available_height)
false ->
ElementTui.Parser.calculate_dim(el, available_width, available_height + 1)
end
{[el], fs_update_dim(wh, dir, available_width, available_height)}
end
cond do
# Empty, so we can return!
ys == [] && xs == [] ->
{xs, ys}
# Fits exactly, so we can return!
cond_x == :eql ->
{xs, ys}
cond_x == :halt ->
# Can we fit another item at the bottom instead?
{_, nys} =
fs_helper(
%{pre: [], post: Stream.drop(post, 1)},
dir,
available_width,
available_height
)
{[], ys ++ nys}
true ->
{nxs, nys} =
fs_helper(
%{pre: Stream.drop(pre, 1), post: Stream.drop(post, 1)},
dir,
available_width_x,
available_height_x
)
{xs ++ nxs, ys ++ nys}
end
end
end
defp fs_update_dim({_, height, _}, {0, 1}, tracked_width, tracked_height) do
{x, y} = {tracked_width, tracked_height - height}
cond do
y == 0 -> {:eql, x, y}
y < 0 -> {:halt, x, y}
true -> {:cont, x, y}
end
end
defp fs_update_dim({width, _, _}, {1, 0}, tracked_width, tracked_height) do
{x, y} = {tracked_width - width, tracked_height}
cond do
x == 0 -> {:eql, x, y}
x < 0 -> {:halt, x, y}
true -> {:cont, x, y}
end
end
end