Current section
Files
Jump to
Current section
Files
lib/elementtui/parser.ex
defmodule ElementTui.Parser do
# Types returned by the parser
defmodule TBPut do
@moduledoc """
Minimal data needed for termbox2 puts function, to draw a line/string to the screen
"""
defstruct x: -1, y: -1, attr: 0, bg_attr: 0, line: ""
@type t :: %ElementTui.Parser.TBPut{
x: integer(),
y: integer(),
attr: integer(),
bg_attr: integer(),
line: String.t()
}
def new(x, y, attr, bg_attr, line) do
%ElementTui.Parser.TBPut{x: x, y: y, attr: attr, bg_attr: bg_attr, line: line}
end
end
defmodule ParseResult do
defstruct elements: [], width: 0, height: 0
@type t :: %ElementTui.Parser.ParseResult{
elements: [ElementTui.Parser.TBPut.t()],
width: integer(),
height: integer()
}
def new(elements, width, height) do
%ElementTui.Parser.ParseResult{elements: elements, width: width, height: height}
end
def merge(current, other, width, height) do
new(other.elements ++ current.elements, width, height)
end
end
alias ElementTui.TermBox2Ex
alias ElementTui.Element
def calculate_dim(_, w, _) when w <= 0 do
{0, 0, Keyword.new()}
end
def calculate_dim(_, _, h) when h <= 0 do
{0, 0, Keyword.new()}
end
def calculate_dim(%{properties: [:vfill | tl]} = element, width, height) do
# Return the minimum height needed, if we don't replicate it
el = element |> Map.put(:properties, tl)
{w, h, kw} = ElementTui.Parser.calculate_dim(el, width, height)
# {w, h, kw |> Keyword.put(:vflex, 1.0)}
{w, h, kw}
end
def calculate_dim(%{properties: [:hfill | tl]} = element, width, height) do
# Return the minimum width needed, if we don't replicate it
el = element |> Map.put(:properties, tl)
{w, h, kw} = ElementTui.Parser.calculate_dim(el, width, height)
# {w, h, kw |> Keyword.put(:hflex, 1.0)}
{w, h, kw}
end
def calculate_dim(%{properties: [:center | tl]} = element, width, height) do
el = element |> Map.put(:properties, tl)
ElementTui.Parser.calculate_dim(el, width, height)
end
def calculate_dim(%{properties: [{:width, mx} | tl]} = element, width, height) do
el = element |> Map.put(:properties, tl) |> Map.put(:width, width)
ElementTui.Parser.calculate_dim(el, min(width, mx), height)
end
def calculate_dim(%{properties: [{:height, mx, _} | tl]} = element, width, height) do
el = element |> Map.put(:properties, tl) |> Map.put(:height, height)
ElementTui.Parser.calculate_dim(el, width, min(height, mx))
end
def calculate_dim(%{width: w, height: h}, width, height) do
{min(w, width), min(h, height), Keyword.new()}
end
def calculate_dim(%{properties: [{:component, component}]}, width, height) do
ElementTui.Component.calculate_dim(component, width, height)
end
def calculate_dim(%{properties: [{:layerbox, xs}]}, width, height) do
{w, h} =
Enum.reduce(xs, {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)
{max(w, curr_w), max(curr_h, h)}
end)
{w, h, Keyword.new()}
end
def calculate_dim(%{line_width: w, height: h, properties: []}, width, height) do
{min(w, width), min(h, height), Keyword.new()}
end
def calculate_dim(%{line: _, line_width: w, properties: []}, width, _height) do
{min(w, width), 1, Keyword.new()}
end
def calculate_dim(%{text: _text, properties: []} = element, width, height) do
%{width: w, height: h} = parse_text(element, 0, 0, width, height)
{w, h, Keyword.new()}
end
def parse(%{properties: [:hfill | tl]} = element, x, y, width, height) do
el = element |> Map.put(:properties, tl)
{w, _, _} = ElementTui.Parser.calculate_dim(el, width, height)
n = div(width, w)
left_over_width = rem(width, w)
# TODO: Should have special case when the underlying thing is a line and we can just duplicate the line
# Use left over height to plot a partial one if needed
el =
if left_over_width > 0 do
[el |> Element.width(left_over_width) | List.duplicate(el, n)]
|> Enum.reverse()
|> Element.hbox()
else
List.duplicate(el, n) |> Element.hbox()
end
ElementTui.Parser.parse(el, x, y, width, height)
end
def parse(%{properties: [:vfill | tl]} = element, x, y, width, height) do
el = element |> Map.put(:properties, tl)
{_, h, _} = ElementTui.Parser.calculate_dim(el, width, height)
n = div(height, h)
left_over_height = rem(height, h)
# Use left over height to plot a partial one if needed
el =
if left_over_height > 0 do
[el |> Element.height(left_over_height) | List.duplicate(el, n)]
|> Enum.reverse()
|> Element.vbox()
else
List.duplicate(el, n) |> Element.vbox()
end
ElementTui.Parser.parse(el, x, y, width, height)
end
def parse(%{properties: [{:component, component}]}, x, y, width, height) do
ElementTui.Component.parse(component, x, y, width, height)
end
def parse(%{properties: [{:layerbox, xs}]}, x, y, width, height) do
[h | tl] = Enum.reverse(xs)
acc = ElementTui.Parser.parse(h, x, y, width, height)
tl
|> Enum.reduce(acc, fn el, acc ->
%{elements: elements, height: h, width: w} =
ElementTui.Parser.parse(el, x, y, width, height)
%{elements: elements ++ acc.elements, height: max(h, acc.height), width: max(w, acc.width)}
end)
end
def parse(%{properties: [:center | tl]} = element, x, y, width, height) do
el = element |> Map.put(:properties, tl)
# TODO: Could we do this without double parsing?
{w, h, _} = ElementTui.Parser.calculate_dim(el, width, height)
x = div(width, 2) + x - div(w, 2)
y = div(height, 2) + y - div(h, 2)
ElementTui.Parser.parse(el, x, y, w, h)
end
def parse(%{properties: [{:height, mx, func} | tl]} = element, x, y, width, height) do
# Do these checks on approx_height, and don't worry about height, because if height is less that probably
# means we are at the bottom of the screen.
# Parse element, see if it fits in the height provided
el = element |> Map.put(:properties, tl)
{_, h, _} = ElementTui.Parser.calculate_dim(el, width, mx + 1)
element =
case h > mx do
# If does not fit then call the height function. The height function should return a new element that fits
# We don't check that again
true ->
func.(el, mx)
false ->
el
end
ElementTui.Parser.parse(
element,
x,
y,
width,
min(height, mx)
)
end
def parse(
%{line: line, line_width: w, properties: [{:width, mx} | tl]} = element,
x,
y,
width,
height
)
when mx < w do
w = min(width, mx)
ElementTui.Parser.parse(
%{element | line: String.slice(line, 0, w), line_width: w, properties: tl},
x,
y,
w,
height
)
end
def parse(%{properties: [{:width, mx} | tl]} = element, x, y, width, height) do
el = element |> Map.put(:properties, tl)
ElementTui.Parser.parse(el, x, y, min(width, mx), height)
end
def parse(%{cursor: dx} = element, x, y, width, height) do
TermBox2Ex.set_cursor(x + dx, y)
element
|> Map.delete(:cursor)
|> ElementTui.Parser.parse(x, y, width, height)
end
def parse(%{line: line, line_width: width} = element, x, y, w, _) when width > w do
ElementTui.Parser.parse(
%{element | line_width: w, line: String.slice(line, 0, w)},
x,
y,
w,
1
)
end
def parse(%{line: line, pad: true, line_width: w} = element, x, y, width, _) when width > w do
ElementTui.Parser.parse(
%{element | line: String.pad_trailing(line, width), line_width: width}
|> Map.delete(:pad),
x,
y,
width,
1
)
end
def parse(%{line: _line} = element, x, y, width, _) do
# Return final results, We assume nothing really has to be done here
el =
Map.merge(element, %{x: x, y: y, width: width, height: 1})
|> Map.delete(:line_width)
%{elements: [el], height: 1, width: width}
end
def parse(%{text: _text} = element, x, y, width, height) do
parse_text(element, x, y, width, height)
end
defp parse_text(%{text: text, wrap: true} = element, x, y, width, height) do
xs = ElementTui.Text.wrap(text, width, height)
h = Enum.count(xs)
%{elements: es} =
xs
|> Enum.map(fn line ->
element
|> Map.delete(:wrap)
|> Map.delete(:text)
|> Map.delete(:pad)
|> Map.put(:line, String.pad_trailing(line, width))
|> Map.put(:width, width)
|> Map.put(:height, 1)
end)
|> Element.vbox()
|> ElementTui.Parser.parse(x, y, width, height)
%{elements: es, height: Kernel.min(height, h), width: width}
end
defp parse_text(%{text: text} = element, x, y, width, _height) do
# Parse a text element
line =
text
|> String.split("\n")
|> Kernel.hd()
|> String.slice(0, width)
el =
element
|> Map.delete(:text)
|> Map.put(:x, x)
|> Map.put(:y, y)
|> Map.put(:height, 1)
if Map.get(element, :pad, false) do
line = line |> String.pad_trailing(width)
# Should padding happening in the termbox2 put code?
%{
elements: [el |> Map.delete(:pad) |> Map.merge(%{width: width, line: line})],
height: 1,
width: width
}
else
width = String.length(line)
%{
elements: [el |> Map.merge(%{width: width, line: line})],
height: 1,
width: width
}
end
end
end