Skip to main content

src/etui@geometry.erl

-module(etui@geometry).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/etui/geometry.gleam").
-export([rect_new/4, rect_zero/0, right/1, bottom/1, area/1, contains/2, hit_test/3, intersect/2, union/2, resolve_sizes/2, split/3, split_h/2, split_v/2, centered_rect/3, percent_rect/3, split_with_spacing/4, split_flex/5, split_responsive/2]).
-export_type([position/0, size/0, rect/0, direction/0, constraint/0, flex_justify/0, breakpoint/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

-type position() :: {position, integer(), integer()}.

-type size() :: {size, integer(), integer()}.

-type rect() :: {rect, position(), size()}.

-type direction() :: horizontal | vertical.

-type constraint() :: {length, integer()} |
    {min, integer()} |
    {max, integer()} |
    {percentage, integer()} |
    {ratio, integer(), integer()} |
    fill.

-type flex_justify() :: flex_start |
    flex_end |
    flex_center |
    flex_between |
    flex_around.

-type breakpoint() :: {breakpoint, integer(), list(constraint())}.

-file("src/etui/geometry.gleam", 53).
?DOC(" Create a Rect with clamped width/height to non-negative.\n").
-spec rect_new(integer(), integer(), integer(), integer()) -> rect().
rect_new(X, Y, Width, Height) ->
    {rect,
        {position, X, Y},
        {size, gleam@int:max(0, Width), gleam@int:max(0, Height)}}.

-file("src/etui/geometry.gleam", 61).
?DOC(" Zero-sized rect at origin.\n").
-spec rect_zero() -> rect().
rect_zero() ->
    {rect, {position, 0, 0}, {size, 0, 0}}.

-file("src/etui/geometry.gleam", 69).
?DOC(" X coordinate of the right edge (exclusive: x + width).\n").
-spec right(rect()) -> integer().
right(Rect) ->
    erlang:element(2, erlang:element(2, Rect)) + erlang:element(
        2,
        erlang:element(3, Rect)
    ).

-file("src/etui/geometry.gleam", 74).
?DOC(" Y coordinate of the bottom edge (exclusive: y + height).\n").
-spec bottom(rect()) -> integer().
bottom(Rect) ->
    erlang:element(3, erlang:element(2, Rect)) + erlang:element(
        3,
        erlang:element(3, Rect)
    ).

-file("src/etui/geometry.gleam", 79).
?DOC(" Area in cells.\n").
-spec area(rect()) -> integer().
area(Rect) ->
    erlang:element(2, erlang:element(3, Rect)) * erlang:element(
        3,
        erlang:element(3, Rect)
    ).

-file("src/etui/geometry.gleam", 84).
?DOC(" Check if a position is inside the rect (inclusive of edges).\n").
-spec contains(rect(), position()) -> boolean().
contains(Rect, Pos) ->
    (((erlang:element(2, Pos) >= erlang:element(2, erlang:element(2, Rect)))
    andalso (erlang:element(2, Pos) < right(Rect)))
    andalso (erlang:element(3, Pos) >= erlang:element(
        3,
        erlang:element(2, Rect)
    )))
    andalso (erlang:element(3, Pos) < bottom(Rect)).

-file("src/etui/geometry.gleam", 93).
?DOC(
    " True if terminal cell `(x, y)` is inside `rect`.\n"
    " Convenience wrapper over `contains` for use with mouse event coordinates.\n"
).
-spec hit_test(rect(), integer(), integer()) -> boolean().
hit_test(Rect, X, Y) ->
    contains(Rect, {position, X, Y}).

-file("src/etui/geometry.gleam", 98).
?DOC(" Intersection of two rects. Returns the overlapping rect if any.\n").
-spec intersect(rect(), rect()) -> {ok, rect()} | {error, nil}.
intersect(A, B) ->
    Left = gleam@int:max(
        erlang:element(2, erlang:element(2, A)),
        erlang:element(2, erlang:element(2, B))
    ),
    Top = gleam@int:max(
        erlang:element(3, erlang:element(2, A)),
        erlang:element(3, erlang:element(2, B))
    ),
    Right_edge = gleam@int:min(right(A), right(B)),
    Bottom_edge = gleam@int:min(bottom(A), bottom(B)),
    case (Left < Right_edge) andalso (Top < Bottom_edge) of
        true ->
            {ok,
                {rect,
                    {position, Left, Top},
                    {size, Right_edge - Left, Bottom_edge - Top}}};

        false ->
            {error, nil}
    end.

-file("src/etui/geometry.gleam", 115).
?DOC(" Union of two rects: smallest rect that contains both.\n").
-spec union(rect(), rect()) -> rect().
union(A, B) ->
    Left = gleam@int:min(
        erlang:element(2, erlang:element(2, A)),
        erlang:element(2, erlang:element(2, B))
    ),
    Top = gleam@int:min(
        erlang:element(3, erlang:element(2, A)),
        erlang:element(3, erlang:element(2, B))
    ),
    Right_edge = gleam@int:max(right(A), right(B)),
    Bottom_edge = gleam@int:max(bottom(A), bottom(B)),
    {rect, {position, Left, Top}, {size, Right_edge - Left, Bottom_edge - Top}}.

-file("src/etui/geometry.gleam", 395).
-spec pick_size(
    constraint(),
    list(integer()),
    list(integer()),
    list(integer()),
    list(integer())
) -> integer().
pick_size(Constraint, Lengths, Pcts, Ratios, Flexes) ->
    case Constraint of
        {length, _} ->
            case Lengths of
                [H | _] ->
                    H;

                _ ->
                    0
            end;

        {percentage, _} ->
            case Pcts of
                [H@1 | _] ->
                    H@1;

                _ ->
                    0
            end;

        {ratio, _, _} ->
            case Ratios of
                [H@2 | _] ->
                    H@2;

                _ ->
                    0
            end;

        fill ->
            case Flexes of
                [H@3 | _] ->
                    H@3;

                _ ->
                    0
            end;

        {min, _} ->
            case Flexes of
                [H@3 | _] ->
                    H@3;

                _ ->
                    0
            end;

        {max, _} ->
            case Flexes of
                [H@3 | _] ->
                    H@3;

                _ ->
                    0
            end
    end.

-file("src/etui/geometry.gleam", 362).
-spec assemble_sizes(
    list(constraint()),
    list(integer()),
    list(integer()),
    list(integer()),
    list(integer()),
    list(integer())
) -> list(integer()).
assemble_sizes(
    Constraints,
    Length_sizes,
    Pct_sizes,
    Ratio_sizes,
    Flex_sizes,
    Acc
) ->
    case Constraints of
        [] ->
            lists:reverse(Acc);

        [C | Cs] ->
            Size = pick_size(
                C,
                Length_sizes,
                Pct_sizes,
                Ratio_sizes,
                Flex_sizes
            ),
            Ls = case Length_sizes of
                [_ | T] ->
                    T;

                _ ->
                    []
            end,
            Ps = case Pct_sizes of
                [_ | T@1] ->
                    T@1;

                _ ->
                    []
            end,
            Rs = case Ratio_sizes of
                [_ | T@2] ->
                    T@2;

                _ ->
                    []
            end,
            Fs = case Flex_sizes of
                [_ | T@3] ->
                    T@3;

                _ ->
                    []
            end,
            assemble_sizes(Cs, Ls, Ps, Rs, Fs, [Size | Acc])
    end.

-file("src/etui/geometry.gleam", 303).
-spec phase_flex(
    list(constraint()),
    integer(),
    integer(),
    integer(),
    list(integer())
) -> list(integer()).
phase_flex(Constraints, Flex_count, Budget, _, _) ->
    case Flex_count of
        0 ->
            gleam@list:map(Constraints, fun(_) -> 0 end);

        _ ->
            Base = case Flex_count of
                0 -> 0;
                Gleam@denominator -> Budget div Gleam@denominator
            end,
            Min_max_used = gleam@list:fold(
                Constraints,
                0,
                fun(Acc, C) -> case C of
                        {min, N} ->
                            Acc + gleam@int:max(N, Base);

                        {max, N@1} ->
                            Acc + gleam@int:min(N@1, Base);

                        _ ->
                            Acc
                    end end
            ),
            Fill_count = gleam@list:count(Constraints, fun(C@1) -> case C@1 of
                        fill ->
                            true;

                        _ ->
                            false
                    end end),
            Fill_budget = gleam@int:max(0, Budget - Min_max_used),
            Fill_base = case Fill_count of
                0 ->
                    0;

                _ ->
                    case Fill_count of
                        0 -> 0;
                        Gleam@denominator@1 -> Fill_budget div Gleam@denominator@1
                    end
            end,
            Fill_rem = case Fill_count of
                0 ->
                    0;

                _ ->
                    case Fill_count of
                        0 -> 0;
                        Gleam@denominator@2 -> Fill_budget rem Gleam@denominator@2
                    end
            end,
            {Sizes, _} = gleam@list:fold(
                Constraints,
                {[], 0},
                fun(State, C@2) ->
                    {Acc@1, Fill_idx} = State,
                    {Size, New_fill_idx} = case C@2 of
                        fill ->
                            S = case Fill_idx < Fill_rem of
                                true ->
                                    Fill_base + 1;

                                false ->
                                    Fill_base
                            end,
                            {S, Fill_idx + 1};

                        {min, N@2} ->
                            {gleam@int:max(N@2, Base), Fill_idx};

                        {max, N@3} ->
                            {gleam@int:min(N@3, Base), Fill_idx};

                        _ ->
                            {0, Fill_idx}
                    end,
                    {[Size | Acc@1], New_fill_idx}
                end
            ),
            lists:reverse(Sizes)
    end.

-file("src/etui/geometry.gleam", 269).
-spec phase_proportional(
    list(integer()),
    integer(),
    integer(),
    integer(),
    integer(),
    list(integer())
) -> {list(integer()), integer()}.
phase_proportional(Demands, Budget, Total_demand, Cumsum, Prev_target, Acc) ->
    case Demands of
        [] ->
            {lists:reverse(Acc), Prev_target};

        [D | Rest] ->
            New_cumsum = Cumsum + D,
            Target = case Total_demand of
                0 ->
                    0;

                _ ->
                    case Total_demand =< Budget of
                        true ->
                            New_cumsum;

                        false ->
                            case Total_demand of
                                0 -> 0;
                                Gleam@denominator -> Budget * New_cumsum div Gleam@denominator
                            end
                    end
            end,
            Size = Target - Prev_target,
            phase_proportional(
                Rest,
                Budget,
                Total_demand,
                New_cumsum,
                Target,
                [Size | Acc]
            )
    end.

-file("src/etui/geometry.gleam", 238).
-spec phase_percentage(
    list(constraint()),
    integer(),
    integer(),
    integer(),
    integer(),
    list(integer())
) -> {list(integer()), integer()}.
phase_percentage(Constraints, Denom, Base, Acc_pct, Prev_target, Acc) ->
    case Constraints of
        [] ->
            {lists:reverse(Acc), Prev_target};

        [C | Rest] ->
            {Size, New_acc, New_target} = case C of
                {percentage, P} ->
                    New_acc_pct = Acc_pct + P,
                    Target = case Denom of
                        0 ->
                            0;

                        _ ->
                            case Denom of
                                0 -> 0;
                                Gleam@denominator -> Base * New_acc_pct div Gleam@denominator
                            end
                    end,
                    S = Target - Prev_target,
                    {S, New_acc_pct, Target};

                _ ->
                    {0, Acc_pct, Prev_target}
            end,
            phase_percentage(
                Rest,
                Denom,
                Base,
                New_acc,
                New_target,
                [Size | Acc]
            )
    end.

-file("src/etui/geometry.gleam", 215).
-spec phase_length(list(constraint()), integer(), integer(), list(integer())) -> {list(integer()),
    integer()}.
phase_length(Constraints, Total, Used, Acc) ->
    case Constraints of
        [] ->
            {lists:reverse(Acc), Used};

        [C | Rest] ->
            {Size, New_used} = case C of
                {length, V} ->
                    Take = gleam@int:min(V, gleam@int:max(0, Total - Used)),
                    {Take, Used + Take};

                _ ->
                    {0, Used}
            end,
            phase_length(Rest, Total, New_used, [Size | Acc])
    end.

-file("src/etui/geometry.gleam", 148).
-spec resolve_sizes_impl(integer(), list(constraint())) -> list(integer()).
resolve_sizes_impl(Total, Constraints) ->
    {Length_sizes, Length_used} = phase_length(Constraints, Total, 0, []),
    Prop_budget = gleam@int:max(0, Total - Length_used),
    Pct_total_pct = gleam@list:fold(Constraints, 0, fun(Acc, C) -> case C of
                {percentage, P} ->
                    Acc + P;

                _ ->
                    Acc
            end end),
    {Denom, Pct_base} = case (Total * Pct_total_pct) > (Prop_budget * 100) of
        true ->
            {Pct_total_pct, Prop_budget};

        false ->
            {100, Total}
    end,
    {Pct_sizes, Pct_used} = phase_percentage(
        Constraints,
        Denom,
        Pct_base,
        0,
        0,
        []
    ),
    Ratio_budget = gleam@int:max(0, Prop_budget - Pct_used),
    Ratio_demands = gleam@list:map(Constraints, fun(C@1) -> case C@1 of
                {ratio, A, B} ->
                    case B of
                        0 ->
                            0;

                        _ ->
                            case B of
                                0 -> 0;
                                Gleam@denominator -> Total * A div Gleam@denominator
                            end
                    end;

                _ ->
                    0
            end end),
    Total_ratio_demand = gleam@list:fold(
        Ratio_demands,
        0,
        fun(Acc@1, D) -> Acc@1 + D end
    ),
    {Ratio_sizes, Ratio_used} = phase_proportional(
        Ratio_demands,
        Ratio_budget,
        Total_ratio_demand,
        0,
        0,
        []
    ),
    Flex_budget = gleam@int:max(
        0,
        ((Total - Length_used) - Pct_used) - Ratio_used
    ),
    Flex_count = gleam@list:count(Constraints, fun(C@2) -> case C@2 of
                fill ->
                    true;

                {min, _} ->
                    true;

                {max, _} ->
                    true;

                _ ->
                    false
            end end),
    Flex_sizes = phase_flex(Constraints, Flex_count, Flex_budget, 0, []),
    assemble_sizes(
        Constraints,
        Length_sizes,
        Pct_sizes,
        Ratio_sizes,
        Flex_sizes,
        []
    ).

-file("src/etui/geometry.gleam", 141).
?DOC(
    " Distribute total space among constraints.\n"
    "\n"
    " Returns a list of sizes (one per constraint) that sum to ≤ total.\n"
    " Sum equals total when Fill (or Min/Max) is present or constraints saturate.\n"
    "\n"
    " Algorithm (three-phase Discrete Cumulative Allocation):\n"
    " 1. Length, exact, allocated first. Clamped to remaining budget in order.\n"
    " 2. Percentage + Ratio, proportional from total. Cumulative to prevent jitter.\n"
    "    Scaled proportionally if combined demand exceeds available budget.\n"
    " 3. Fill + Min + Max, divide remaining equally.\n"
    "    Min applies a floor; Max applies a ceiling. Fill gets equal share.\n"
).
-spec resolve_sizes(integer(), list(constraint())) -> list(integer()).
resolve_sizes(Total, Constraints) ->
    case Total < 0 of
        true ->
            gleam@list:map(Constraints, fun(_) -> 0 end);

        false ->
            resolve_sizes_impl(Total, Constraints)
    end.

-file("src/etui/geometry.gleam", 481).
-spec build_rects(direction(), rect(), list(integer()), integer(), list(rect())) -> list(rect()).
build_rects(Direction, Area, Sizes, Cursor, Acc) ->
    Limit = case Direction of
        vertical ->
            erlang:element(3, erlang:element(3, Area));

        horizontal ->
            erlang:element(2, erlang:element(3, Area))
    end,
    case Sizes of
        [] ->
            lists:reverse(Acc);

        [Size | Rest] ->
            Start = gleam@int:min(Cursor, Limit),
            Clamped = gleam@int:min(Size, gleam@int:max(0, Limit - Start)),
            Rect = case Direction of
                vertical ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)),
                            erlang:element(3, erlang:element(2, Area)) + Start},
                        {size,
                            erlang:element(2, erlang:element(3, Area)),
                            Clamped}};

                horizontal ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)) + Start,
                            erlang:element(3, erlang:element(2, Area))},
                        {size,
                            Clamped,
                            erlang:element(3, erlang:element(3, Area))}}
            end,
            build_rects(Direction, Area, Rest, Start + Clamped, [Rect | Acc])
    end.

-file("src/etui/geometry.gleam", 466).
?DOC(" Split a rect along a direction by applying constraints.\n").
-spec split(direction(), rect(), list(constraint())) -> list(rect()).
split(Direction, Area, Constraints) ->
    Total = case Direction of
        vertical ->
            erlang:element(3, erlang:element(3, Area));

        horizontal ->
            erlang:element(2, erlang:element(3, Area))
    end,
    Sizes = resolve_sizes(Total, Constraints),
    build_rects(Direction, Area, Sizes, 0, []).

-file("src/etui/geometry.gleam", 430).
?DOC(" Split horizontally (columns side-by-side). Shorthand for `split(Horizontal, ...)`.\n").
-spec split_h(rect(), list(constraint())) -> list(rect()).
split_h(Area, Constraints) ->
    split(horizontal, Area, Constraints).

-file("src/etui/geometry.gleam", 435).
?DOC(" Split vertically (rows stacked). Shorthand for `split(Vertical, ...)`.\n").
-spec split_v(rect(), list(constraint())) -> list(rect()).
split_v(Area, Constraints) ->
    split(vertical, Area, Constraints).

-file("src/etui/geometry.gleam", 445).
?DOC(
    " Center a rect of `width × height` within `area`.\n"
    " Clamps to area bounds. Common for popup placement.\n"
    "\n"
    " ```gleam\n"
    " let popup_area = geometry.centered_rect(60, 20, screen)\n"
    " ```\n"
).
-spec centered_rect(integer(), integer(), rect()) -> rect().
centered_rect(Width, Height, Area) ->
    W = gleam@int:min(Width, erlang:element(2, erlang:element(3, Area))),
    H = gleam@int:min(Height, erlang:element(3, erlang:element(3, Area))),
    X = erlang:element(2, erlang:element(2, Area)) + ((erlang:element(
        2,
        erlang:element(3, Area)
    )
    - W)
    div 2),
    Y = erlang:element(3, erlang:element(2, Area)) + ((erlang:element(
        3,
        erlang:element(3, Area)
    )
    - H)
    div 2),
    {rect, {position, X, Y}, {size, W, H}}.

-file("src/etui/geometry.gleam", 459).
?DOC(
    " Center a rect sized as a percentage of `area` (`pct_w` and `pct_h` are 0–100).\n"
    " Useful for responsive popup sizing:\n"
    "\n"
    " ```gleam\n"
    " let popup_area = geometry.percent_rect(60, 40, screen)  // 60% wide, 40% tall\n"
    " ```\n"
).
-spec percent_rect(integer(), integer(), rect()) -> rect().
percent_rect(Pct_w, Pct_h, Area) ->
    W = (erlang:element(2, erlang:element(3, Area)) * gleam@int:clamp(
        Pct_w,
        0,
        100
    ))
    div 100,
    H = (erlang:element(3, erlang:element(3, Area)) * gleam@int:clamp(
        Pct_h,
        0,
        100
    ))
    div 100,
    centered_rect(W, H, Area).

-file("src/etui/geometry.gleam", 548).
-spec build_rects_spaced(
    direction(),
    rect(),
    list(integer()),
    integer(),
    integer(),
    list(rect())
) -> list(rect()).
build_rects_spaced(Direction, Area, Sizes, Spacing, Cursor, Acc) ->
    Limit = case Direction of
        vertical ->
            erlang:element(3, erlang:element(3, Area));

        horizontal ->
            erlang:element(2, erlang:element(3, Area))
    end,
    case Sizes of
        [] ->
            lists:reverse(Acc);

        [Size | Rest] ->
            Start = gleam@int:min(Cursor, Limit),
            Clamped = gleam@int:min(Size, gleam@int:max(0, Limit - Start)),
            Rect = case Direction of
                vertical ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)),
                            erlang:element(3, erlang:element(2, Area)) + Start},
                        {size,
                            erlang:element(2, erlang:element(3, Area)),
                            Clamped}};

                horizontal ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)) + Start,
                            erlang:element(3, erlang:element(2, Area))},
                        {size,
                            Clamped,
                            erlang:element(3, erlang:element(3, Area))}}
            end,
            Next_cursor = case Rest of
                [] ->
                    Start + Clamped;

                _ ->
                    (Start + Clamped) + Spacing
            end,
            build_rects_spaced(
                Direction,
                Area,
                Rest,
                Spacing,
                Next_cursor,
                [Rect | Acc]
            )
    end.

-file("src/etui/geometry.gleam", 526).
?DOC(
    " Split a rect with `spacing` cells of gap between each child.\n"
    " Gap cells are taken from the total before distributing to constraints.\n"
    "\n"
    " ```gleam\n"
    " // Two columns with a 1-cell gap\n"
    " split_with_spacing(Horizontal, area, [Fill, Fill], 1)\n"
    " ```\n"
).
-spec split_with_spacing(direction(), rect(), list(constraint()), integer()) -> list(rect()).
split_with_spacing(Direction, Area, Constraints, Spacing) ->
    N = erlang:length(Constraints),
    case N =< 1 of
        true ->
            split(Direction, Area, Constraints);

        false ->
            Gap_total = gleam@int:max(0, Spacing) * (N - 1),
            Total = case Direction of
                vertical ->
                    erlang:element(3, erlang:element(3, Area));

                horizontal ->
                    erlang:element(2, erlang:element(3, Area))
            end,
            Available = gleam@int:max(0, Total - Gap_total),
            Sizes = resolve_sizes(Available, Constraints),
            build_rects_spaced(
                Direction,
                Area,
                Sizes,
                gleam@int:max(0, Spacing),
                0,
                []
            )
    end.

-file("src/etui/geometry.gleam", 728).
-spec build_flex_rects(
    direction(),
    rect(),
    list(integer()),
    list(integer()),
    list(rect())
) -> list(rect()).
build_flex_rects(Direction, Area, Sizes, Offsets, Acc) ->
    case {Sizes, Offsets} of
        {[], _} ->
            lists:reverse(Acc);

        {_, []} ->
            lists:reverse(Acc);

        {[Size | Rest_s], [Offset | Rest_o]} ->
            Rect = case Direction of
                vertical ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)),
                            erlang:element(3, erlang:element(2, Area)) + Offset},
                        {size, erlang:element(2, erlang:element(3, Area)), Size}};

                horizontal ->
                    {rect,
                        {position,
                            erlang:element(2, erlang:element(2, Area)) + Offset,
                            erlang:element(3, erlang:element(2, Area))},
                        {size, Size, erlang:element(3, erlang:element(3, Area))}}
            end,
            build_flex_rects(Direction, Area, Rest_s, Rest_o, [Rect | Acc])
    end.

-file("src/etui/geometry.gleam", 706).
-spec around_offsets(
    list(integer()),
    integer(),
    integer(),
    integer(),
    list(integer())
) -> list(integer()).
around_offsets(Sizes, Leftover, N, Cursor, Acc) ->
    Slot = case N of
        0 ->
            0;

        _ ->
            case N of
                0 -> 0;
                Gleam@denominator -> Leftover div Gleam@denominator
            end
    end,
    Half = Slot div 2,
    case Sizes of
        [] ->
            lists:reverse(Acc);

        [S | Rest] ->
            Pos = Cursor + Half,
            Next = ((Pos + S) + Half) + (Slot rem 2),
            around_offsets(Rest, Leftover, N, Next, [Pos | Acc])
    end.

-file("src/etui/geometry.gleam", 685).
-spec between_offsets(
    list(integer()),
    integer(),
    integer(),
    integer(),
    list(integer())
) -> list(integer()).
between_offsets(Sizes, Leftover, N, Cursor, Acc) ->
    Gaps = gleam@int:max(1, N - 1),
    Gap_size = case Gaps of
        0 ->
            0;

        _ ->
            case Gaps of
                0 -> 0;
                Gleam@denominator -> Leftover div Gleam@denominator
            end
    end,
    case Sizes of
        [] ->
            lists:reverse(Acc);

        [S | Rest] ->
            Next = (Cursor + S) + Gap_size,
            between_offsets(Rest, Leftover, N, Next, [Cursor | Acc])
    end.

-file("src/etui/geometry.gleam", 670).
-spec start_offsets(list(integer()), integer(), integer(), list(integer())) -> list(integer()).
start_offsets(Sizes, Gap, Start, Acc) ->
    case Sizes of
        [] ->
            lists:reverse(Acc);

        [S | Rest] ->
            Next = (Start + S) + Gap,
            start_offsets(Rest, Gap, Next, [Start | Acc])
    end.

-file("src/etui/geometry.gleam", 654).
-spec flex_offsets(
    list(integer()),
    flex_justify(),
    integer(),
    integer(),
    integer()
) -> list(integer()).
flex_offsets(Sizes, Justify, Gap, Leftover, N) ->
    case Justify of
        flex_start ->
            start_offsets(Sizes, Gap, 0, []);

        flex_end ->
            start_offsets(Sizes, Gap, Leftover, []);

        flex_center ->
            start_offsets(Sizes, Gap, Leftover div 2, []);

        flex_between ->
            between_offsets(Sizes, Leftover, N, 0, []);

        flex_around ->
            around_offsets(Sizes, Leftover, N, 0, [])
    end.

-file("src/etui/geometry.gleam", 627).
?DOC(
    " Flex layout: children have fixed sizes (from constraints), leftover space\n"
    " distributed according to `justify`. Use for toolbars, status bars, centering\n"
    " a widget in a larger area, or equal-gap grids.\n"
    "\n"
    " `gap` is the minimum gap between children (cells). Ignored when `justify`\n"
    " provides its own spacing (Between/Around). With `FlexStart`/`End`/`Center`,\n"
    " `gap` acts like `split_with_spacing`'s spacing parameter.\n"
    "\n"
    " ```gleam\n"
    " // Center a 20-wide widget in a 80-wide area:\n"
    " split_flex(Horizontal, area, [Length(20)], FlexCenter, 0)\n"
    "\n"
    " // Three buttons with 2-cell gap between:\n"
    " split_flex(Horizontal, area, [Length(10), Length(10), Length(10)], FlexStart, 2)\n"
    "\n"
    " // Toolbar: left item + right item, space between:\n"
    " split_flex(Horizontal, area, [Length(10), Length(10)], FlexBetween, 0)\n"
    " ```\n"
).
-spec split_flex(
    direction(),
    rect(),
    list(constraint()),
    flex_justify(),
    integer()
) -> list(rect()).
split_flex(Direction, Area, Constraints, Justify, Gap) ->
    N = erlang:length(Constraints),
    case N =:= 0 of
        true ->
            [];

        false ->
            Total = case Direction of
                vertical ->
                    erlang:element(3, erlang:element(3, Area));

                horizontal ->
                    erlang:element(2, erlang:element(3, Area))
            end,
            Gap_cells = gleam@int:max(0, Gap) * gleam@int:max(0, N - 1),
            Available = gleam@int:max(0, Total - Gap_cells),
            Sizes = resolve_sizes(Available, Constraints),
            Content_width = gleam@list:fold(
                Sizes,
                0,
                fun(Acc, S) -> Acc + S end
            )
            + Gap_cells,
            Leftover = gleam@int:max(0, Total - Content_width),
            Offsets = flex_offsets(Sizes, Justify, Gap, Leftover, N),
            build_flex_rects(Direction, Area, Sizes, Offsets, [])
    end.

-file("src/etui/geometry.gleam", 791).
-spec pick_breakpoint(list(breakpoint()), integer()) -> list(constraint()).
pick_breakpoint(Sorted_desc, Width) ->
    case Sorted_desc of
        [] ->
            [];

        [Bp] ->
            erlang:element(3, Bp);

        [Bp@1 | Rest] ->
            case Width >= erlang:element(2, Bp@1) of
                true ->
                    erlang:element(3, Bp@1);

                false ->
                    pick_breakpoint(Rest, Width)
            end
    end.

-file("src/etui/geometry.gleam", 774).
?DOC(
    " Split `area` horizontally using the first breakpoint whose `min_width` <=\n"
    " `area.size.width`, evaluated in descending order. Falls back to the last\n"
    " breakpoint (assumed smallest). Returns `[area]` if `breakpoints` is empty.\n"
    "\n"
    " Example, two columns on wide screens, stacked on narrow:\n"
    " ```gleam\n"
    " geometry.split_responsive(area, [\n"
    "   geometry.Breakpoint(80, [Percentage(50), Percentage(50)]),\n"
    "   geometry.Breakpoint(0,  [Percentage(100)]),\n"
    " ])\n"
    " ```\n"
).
-spec split_responsive(rect(), list(breakpoint())) -> list(rect()).
split_responsive(Area, Breakpoints) ->
    case Breakpoints of
        [] ->
            [Area];

        _ ->
            Sorted = gleam@list:sort(
                Breakpoints,
                fun(A, B) ->
                    gleam@int:compare(
                        erlang:element(2, B),
                        erlang:element(2, A)
                    )
                end
            ),
            Chosen = pick_breakpoint(
                Sorted,
                erlang:element(2, erlang:element(3, Area))
            ),
            split_h(Area, Chosen)
    end.