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/panels.ex
defmodule Raxol.UI.Layout.Panels do
@moduledoc """
Handles layout calculations for panel UI elements.
This module is responsible for:
* Panel border rendering
* Panel content layout
* Panel title positioning
* Panel-specific spacing and constraints
"""
alias Raxol.UI.Layout.Engine
require Logger
@doc """
Processes a panel element, calculating layout for it and its children.
## Parameters
* `panel` - The panel element to process
* `space` - The available space for the panel
* `acc` - The accumulator for rendered elements
## Returns
A list of positioned elements with absolute coordinates.
"""
def process(
%{type: :panel, attrs: attrs, children: children_input} = panel_element,
space,
acc
) do
# Measure the panel first to get its actual dimensions
panel_dimensions = measure_panel(panel_element, space)
# Original attributes from the panel element
panel_input_attrs = attrs
actual_border_style = Map.get(panel_input_attrs, :border, :single)
# Construct final attributes for the panel's box representation
final_box_attrs =
panel_input_attrs
# Ensure border_style is correctly set
|> Map.put(:border_style, actual_border_style)
|> then(fn current_attrs ->
if actual_border_style == :none do
# For test: attrs.border should be nil for :none style
Map.put(current_attrs, :border, nil)
else
# For other styles, ensure :border attribute also reflects the style
Map.put(current_attrs, :border, actual_border_style)
end
end)
# Create the main panel box element
panel_box = %{
type: :box,
x: space.x,
y: space.y,
width: panel_dimensions.width,
height: panel_dimensions.height,
# Use the constructed attributes
attrs: final_box_attrs
}
elements = [panel_box]
# Create title element if present
title_elements =
case Map.get(attrs, :title) do
nil ->
[]
"" ->
[]
title_text ->
[
%{
type: :text,
# Position inside left border, after a space
x: space.x + 2,
# On the top border line
y: space.y,
# Add padding around title
text: " #{title_text} ",
# Allow styling title
attrs: Map.get(attrs, :title_attrs, %{})
}
]
end
# Define inner space for children (inside borders)
inner_space = %{
x: space.x + 1,
y: space.y + 1,
width: max(0, panel_dimensions.width - 2),
height: max(0, panel_dimensions.height - 2)
}
# Ensure children_input is a list for Enum.map
children_to_process =
case children_input do
nil ->
[]
c when is_list(c) ->
c
# Wrap single child map in a list
c when is_map(c) ->
[c]
_ ->
# Should not happen based on Panel definition if type specs are followed
# but good to be defensive.
Logger.warning(
"Panels.process received unexpected children format: #{inspect(children_input)}",
[]
)
[]
end
# Process children by calling Engine.process_element for each one
processed_children_elements =
children_to_process
|> Enum.map(fn child_element ->
# Pass an empty accumulator for each child, as process_element appends to it.
Engine.process_element(child_element, inner_space, [])
end)
|> List.flatten()
# Combine all elements and add to accumulator
elements ++ title_elements ++ processed_children_elements ++ acc
end
@doc """
Measures the space required by a panel element.
## Parameters
* `panel` - The panel element to measure
* `available_space` - The available space for the panel
## Returns
The dimensions of the panel: %{width: w, height: h}
"""
def measure_panel(
%{type: :panel, attrs: attrs, children: children},
available_space
) do
# Calculate space available for content (inside borders)
content_available_space = %{
available_space
| # 1 cell border left/right
width: max(0, available_space.width - 2),
# 1 cell border top/bottom
height: max(0, available_space.height - 2)
}
# Measure children content size (treat as a column for measurement)
# Ensure the map has :attrs for measure_element pattern matching
column_for_measurement = %{type: :column, attrs: %{}, children: children}
children_size =
Engine.measure_element(column_for_measurement, content_available_space)
# Determine base width/height from content + borders
# Add 2 for left/right borders
content_width = children_size.width + 2
# Add 2 for top/bottom borders
content_height = children_size.height + 2
# Use explicit width/height if provided, otherwise use content size or available if no content
explicit_width = Map.get(attrs, :width)
explicit_height = Map.get(attrs, :height)
# Default to available space if no explicit size and no (or zero-sized) children
base_width =
if children_size.width == 0 and is_nil(explicit_width) do
available_space.width
else
content_width
end
base_height =
if children_size.height == 0 and is_nil(explicit_height) do
available_space.height
else
content_height
end
width = explicit_width || base_width
height = explicit_height || base_height
# Ensure minimum dimensions (e.g., for borders and minimal content)
min_width = 4
min_height = 3
width = max(width, min_width)
height = max(height, min_height)
# Return dimensions constrained to available space
%{
width: min(width, available_space.width),
height: min(height, available_space.height)
}
end
# Private helpers
# Unused
# defp calculate_inner_space(space, attrs) do
# ...
# end
# Unused
# defp get_border_width(attrs) do
# ...
# end
# Unused
# defp create_panel_elements(space, attrs) do
# ...
# end
# Unused
# defp get_border_chars(:none), do: nil
# defp get_border_chars(style) do
# ...
# end
end