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