Packages
Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.
Current section
Files
Jump to
Current section
Files
lib/raxol/ui/layout/flexbox/positioner.ex
defmodule Raxol.UI.Layout.Flexbox.Positioner do
@moduledoc """
Main-axis and cross-axis positioning for flex items, including
justify-content and align-items logic.
"""
@doc "Position sized children along the main axis."
def position_main_axis(sized_children, space, flex_props, main_axis) do
total_size = sum_main_sizes(sized_children, main_axis)
gap_size = get_gap_size(flex_props.gap, main_axis)
total_gaps = gap_size * max(0, length(sized_children) - 1)
available_space = get_dimension(space, main_axis) - total_size - total_gaps
{start_pos, item_gap} =
calculate_justify_positioning(
flex_props.justify_content,
available_space,
length(sized_children),
gap_size
)
place_children_on_main_axis(
sized_children,
space,
main_axis,
get_coord(space, main_axis) + start_pos,
item_gap
)
end
defp sum_main_sizes(sized_children, main_axis) do
Enum.reduce(sized_children, 0, fn {_child, dims, _flex}, acc ->
acc + get_dimension(dims, main_axis)
end)
end
defp place_children_on_main_axis(
sized_children,
space,
main_axis,
start_coord,
item_gap
) do
{_, positioned} =
Enum.reduce(sized_children, {start_coord, []}, fn {child, dims, flex},
{current_pos, acc} ->
child_space = build_child_space(space, dims, main_axis, current_pos)
next_pos = current_pos + get_dimension(dims, main_axis) + item_gap
{next_pos, [{child, child_space, flex} | acc]}
end)
Enum.reverse(positioned)
end
@doc "Position children along the cross axis according to align-items."
def position_cross_axis(positioned_children, space, flex_props, cross_axis) do
Enum.map(positioned_children, fn {child, child_space, flex} ->
alignment = flex.align_self || flex_props.align_items
new_child_space = align_cross(child_space, space, cross_axis, alignment)
{child, new_child_space}
end)
end
def align_cross(child_space, space, cross_axis, alignment) do
origin = get_coord(space, cross_axis)
total = get_dimension(space, cross_axis)
child_size = get_dimension(child_space, cross_axis)
case alignment do
:flex_start ->
set_cross_coord(child_space, cross_axis, origin)
:flex_end ->
set_cross_coord(child_space, cross_axis, origin + total - child_size)
:center ->
set_cross_coord(
child_space,
cross_axis,
origin + div(total - child_size, 2)
)
:stretch ->
child_space
|> set_cross_coord(cross_axis, origin)
|> set_cross_dimension(cross_axis, total)
_ ->
child_space
end
end
# ---------------------------------------------------------------------------
# justify-content positioning
# ---------------------------------------------------------------------------
def calculate_justify_positioning(
:flex_start,
_available_space,
_item_count,
gap
) do
{0, gap}
end
def calculate_justify_positioning(
:flex_end,
available_space,
_item_count,
gap
) do
{available_space, gap}
end
def calculate_justify_positioning(:center, available_space, _item_count, gap) do
{div(available_space, 2), gap}
end
def calculate_justify_positioning(
:space_between,
available_space,
item_count,
_gap
)
when item_count > 1 do
{0, div(available_space, item_count - 1)}
end
def calculate_justify_positioning(
:space_around,
available_space,
item_count,
_gap
) do
space_per_item = div(available_space, item_count)
{div(space_per_item, 2), space_per_item}
end
def calculate_justify_positioning(
:space_evenly,
available_space,
item_count,
_gap
) do
space_per_gap = div(available_space, item_count + 1)
{space_per_gap, space_per_gap}
end
def calculate_justify_positioning(_, _available_space, _item_count, gap) do
{0, gap}
end
# ---------------------------------------------------------------------------
# Cross-axis line positioning (align-content)
# ---------------------------------------------------------------------------
@doc "Position wrapped lines along the cross axis."
def position_lines_cross_axis(
lines_with_layout,
space,
flex_props,
cross_axis
) do
line_heights = compute_line_heights(lines_with_layout, cross_axis)
total_line_height = Enum.sum(line_heights)
available_space = get_dimension(space, cross_axis) - total_line_height
gap_size = get_gap_size(flex_props.gap, cross_axis)
total_gaps = gap_size * max(0, length(lines_with_layout) - 1)
{start_pos, line_gap} =
calculate_align_content_positioning(
flex_props.align_content,
available_space - total_gaps,
length(lines_with_layout),
gap_size
)
place_lines_cross(
lines_with_layout,
line_heights,
cross_axis,
get_coord(space, cross_axis) + start_pos,
line_gap
)
end
defp compute_line_heights(lines_with_layout, cross_axis) do
Enum.map(lines_with_layout, fn line ->
Enum.reduce(line, 0, fn item, acc ->
max(acc, get_dimension(item_space(item), cross_axis))
end)
end)
end
defp place_lines_cross(
lines_with_layout,
line_heights,
cross_axis,
start_coord,
line_gap
) do
{_, positioned_lines} =
lines_with_layout
|> Enum.zip(line_heights)
|> Enum.reduce({start_coord, []}, fn {line, line_height},
{current_pos, acc} ->
positioned_line =
Enum.map(line, &set_item_cross_pos(&1, cross_axis, current_pos))
next_pos = current_pos + line_height + line_gap
{next_pos, positioned_line ++ acc}
end)
positioned_lines
end
def calculate_align_content_positioning(
:flex_start,
_available_space,
_line_count,
gap
) do
{0, gap}
end
def calculate_align_content_positioning(
:flex_end,
available_space,
_line_count,
gap
) do
{available_space, gap}
end
def calculate_align_content_positioning(
:center,
available_space,
_line_count,
gap
) do
{div(available_space, 2), gap}
end
def calculate_align_content_positioning(
:space_between,
available_space,
line_count,
_gap
)
when line_count > 1 do
{0, div(available_space, line_count - 1)}
end
def calculate_align_content_positioning(
:space_around,
available_space,
line_count,
_gap
) do
space_per_line = div(available_space, line_count)
{div(space_per_line, 2), space_per_line}
end
def calculate_align_content_positioning(_, _available_space, _line_count, gap) do
{0, gap}
end
# ---------------------------------------------------------------------------
# Coordinate helpers
# ---------------------------------------------------------------------------
def get_coord(space, :horizontal), do: space.x
def get_coord(space, :vertical), do: space.y
def get_dimension(dims, :horizontal), do: dims.width
def get_dimension(dims, :vertical), do: dims.height
def get_gap_size(gap, :horizontal), do: gap.column
def get_gap_size(gap, :vertical), do: gap.row
def set_cross_coord(space, :horizontal, pos), do: %{space | x: pos}
def set_cross_coord(space, :vertical, pos), do: %{space | y: pos}
def set_cross_dimension(space, :horizontal, val), do: %{space | width: val}
def set_cross_dimension(space, :vertical, val), do: %{space | height: val}
def build_child_space(space, dims, :horizontal, main_pos) do
%{x: main_pos, y: space.y, width: dims.width, height: dims.height}
end
def build_child_space(space, dims, :vertical, main_pos) do
%{x: space.x, y: main_pos, width: dims.width, height: dims.height}
end
def item_space({_child, child_space, _flex}), do: child_space
def item_space({_child, child_space}), do: child_space
def set_item_cross_pos({child, child_space}, cross_axis, pos) do
{child, set_cross_coord(child_space, cross_axis, pos)}
end
def set_item_cross_pos({child, child_space, _flex}, cross_axis, pos) do
{child, set_cross_coord(child_space, cross_axis, pos)}
end
end