NAME

Layout::Flex - CSS flexbox layout engine with wrap, gap, margin, and content driven sizing

VERSION

Version 0.02

SYNOPSIS

use Layout::Flex;

# Explicit sizing
my @rects = Layout::Flex->compute(
    main_size     => 400,
    cross_size    => 300,
    direction     => 'row',
    justify       => 'space-between',
    align         => 'stretch',
    wrap          => 'wrap',
    align_content => 'space-around',
    gap           => 10,
    items => [
        { basis => 150, grow => 1, cross => 80, margin => 5 },
        { basis => 150, grow => 1, cross => 60, margin => 5 },
    ],
);

# Content driven sizing with a measure callback
my @rects = Layout::Flex->compute(
    main_size  => 400,
    cross_size => 200,
    measure    => sub {
        my ($item, $avail_w) = @_;
        # $item hashref has text, font_size, and any custom keys you set
        # $avail_w is defined only during the wrap second pass
        my ($w, $h) = your_font_metrics($item->{text}, $item->{font_size});
        return ($w, $h);
    },
    items => [
        { text => 'Hello',     font_size => 14, grow => 1 },
        { text => 'Paragraph', font_size => 12, grow => 2, wrap_text => 1 },
    ],
);

for my $r (@rects) {
    my ($x, $y, $w, $h) = @$r;
}

DESCRIPTION

Layout::Flex implements the CSS Flexible Box layout algorithm in C/XS. Given a container size and a list of flex items it computes the exact position and size of each item.

Both single line (nowrap, default) and multi line (wrap, wrap-reverse) modes are supported. In multi line mode, items that overflow the main axis are collected onto new lines; each line runs an independent grow/shrink pass and lines are then distributed across the cross axis according to align_content.

gap (or main_gap/cross_gap) inserts fixed space between items and between lines before justify/align_content distributes any remaining free space. Per-item margins (margin, margin_top, margin_right, margin_bottom, margin_left) offset items within their slot; the output rect is always the content box, excluding margins.

Content-driven sizing is supported via a measure callback. Items may carry a text key (and font_size, plus any custom keys your callback needs); the engine calls the callback to derive basis and cross before layout runs. Items with wrap_text => 1 trigger a second measurement pass once their resolved width is known, allowing line-breaking callbacks to return the correct wrapped height.

METHODS

compute

my @rects = Layout::Flex->compute(%args);

Compute layout. Returns a list of [$x, $y, $w, $h] array references, one per item, in the same order as items.

Container options

main_size

Size of the container along the main axis: width for row, height for column.

cross_size

Size of the container along the cross axis: height for row, width for column.

direction

row (default) — items flow left to right. column — items flow top to bottom.

justify

Distributes free space along the main axis. One of:

start (default) — pack items at the start
end — pack items at the end
center — centre items
space-between — equal gaps between items, none at edges
space-around — equal gaps around items (half-gap at edges)
space-evenly — equal gaps between items and edges
align

Sizes and positions items along the cross axis within their line. One of:

stretch (default) — items fill the line's cross size (clamped to their constraints)
start — items align to the start of the cross axis
end — items align to the end of the cross axis
center — items are centred on the cross axis
wrap

Controls whether items may wrap onto multiple lines. One of:

nowrap (default) — all items on one line; may overflow
wrap — items wrap onto additional lines when they exceed main_size
wrap-reverse — same as wrap but lines are stacked in reverse cross-axis order
align_content

Distributes lines across the cross axis when wrap produces more than one line. Has no effect in single-line layouts. One of:

stretch (default) — lines stretch equally to fill the container cross size
start — lines packed at the start of the cross axis
end — lines packed at the end
center — lines centred
space-between — equal gaps between lines, none at edges
space-around — equal gaps around lines (half-gap at edges)
space-evenly — equal gaps between lines and edges
gap

Shorthand that sets both main_gap and cross_gap to the same value.

main_gap

Fixed space inserted between items within each line on the main axis. Reduces free space before justify distributes any remainder. Default 0.

cross_gap

Fixed space inserted between lines on the cross axis. Reduces free space before align_content distributes any remainder. Has no effect in single-line layouts. Default 0.

Item options

Each entry in items is a hashref with:

basis

Initial main-axis size before grow/shrink is applied. Default 0.

grow

flex-grow factor. The item receives this fraction of positive free space relative to the sum of all grow values. Default 0.

shrink

flex-shrink factor. When items overflow the container this controls how much each item gives back. Default 1.

min_main, max_main

Clamp the resolved main-axis size. max_main of 0 means unconstrained. Default 0 for both (unconstrained minimum).

cross

Natural cross-axis size. Used when align or align_self is start, end, or center. Ignored under stretch. Default 0.

min_cross, max_cross

Clamp the resolved cross-axis size. max_cross of 0 means unconstrained.

align_self

Per-item override for align. Accepts the same values (stretch, start, end, center). Omit or leave undefined to inherit the container's align.

margin

Shorthand that sets all four margins to the same value.

margin_top, margin_right, margin_bottom, margin_left

Per-side margins. These are direction-aware: in row layout, margin_left/margin_right affect the main axis and margin_top/margin_bottom affect the cross axis; in column layout the axes are swapped.

Margins reduce the free space available for grow/shrink and count toward line-breaking decisions. The output rect is always the content box — margins are factored into spacing but are not included in x/y/w/h.

Content sizing

When a measure callback is supplied on the container, items that carry a text key are automatically sized before layout runs. The following item options control this behaviour.

text

The text content of the item. When measure is set and this key is present, the engine calls the measurer to determine basis (main-axis natural size) and cross (cross-axis natural size). Any explicit basis or cross on the item takes precedence and suppresses the corresponding measurement.

font_size

Font size in points, passed to the measurer. Default 12. The built-in 'simple' measurer uses font_size * 0.6 as character width and font_size * 1.4 as line height.

wrap_text

Boolean. When true, the item participates in a second measurement pass after lf_compute has resolved final widths. The measurer is called again with the resolved width as a second argument $avail_w, allowing it to return a line-wrapped height. A second lf_compute then runs to apply the new cross sizes.

For row layouts the resolved width is $rect->{w}; for column layouts it is $rect->{h}. The built-in 'simple' measurer calculates ceil(len / floor(avail_w / char_w)) lines and multiplies by line height.

A code-ref measurer receives ($item, $avail_w) on the second pass and should return ($w, $h); the engine uses the returned $h (and $w if provided) to update the item's cross size.

Items without wrap_text are measured only once; they keep their naturally resolved dimensions.

Container measure option

measure

One of:

'simple'

Built-in proportional approximation. Width = length($text) * $font_size * 0.6. Height = $font_size * 1.4. Useful for fast layout estimates without a real font engine.

A code reference
measure => sub {
    my ($item, $avail_w) = @_;
    # $avail_w is undef on the first pass; defined on the wrap_text second-pass
    ...
    return ($w, $h);
}

Called once per item that has a text key. On the first pass $avail_w is undef; on the wrap_text second pass it is the resolved main-axis size. The item hashref contains text, font_size, and any other keys you set on the item.

AUTHOR

LNATION <email@lnation.org>

BUGS

Please report bugs at https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Layout-Flex.

LICENSE AND COPYRIGHT

This software is Copyright (c) 2026 by LNATION <email@lnation.org>.

This is free software, licensed under:

The Artistic License 2.0 (GPL Compatible)