diff --git a/CHANGELOG.md b/CHANGELOG.md index f8bd4638b..2ab459281 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `ComputeParticlesDemo`: keeps drawing the last particle snapshot on frames where no new one has landed, so it renders on the Emscripten WebGPU backend instead of showing nothing. The status label reports the landed-snapshot count alongside the frame count - `Component`'s effect path now reuses its offscreen `GpuCanvas` across frames while the component size is unchanged, instead of allocating (and freeing) a full-size render target every frame. On a size change the outgoing canvas is released before the replacement is created, so its `RenderContext` lease returns to the pool rather than forcing a second context to be reserved permanently - `ComponentEffectsDemo`: shader effects now share a common base that compiles the pipeline at most once instead of retrying a failed compile on every frame, reports the compile error in the status label and on the console, and shows the CPU time spent applying the effect next to the paint time +- New `Layout` example (`examples/graphics`): exercises the FlexBox and Grid layout containers with pages for direction, wrap, justify-content, align-items, align-content, flex grow/shrink/basis, align-self, order, margins/gaps, percentage sizing, min/max constraints, grid track sizing (px/fr/auto), explicit placement and spans, auto placement, grid alignment, and nested flex/grid compositions #### Rive Runtime Bump @@ -155,6 +156,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Bug Fixes +- `FlexBox`: the `gap` is no longer applied after the last item on each line. It was added unconditionally after every item, so when items grew or shrank to fill the container exactly (e.g. `flexGrow` items with `gap`), the trailing gap pushed the final item past the container's main-axis edge and it got clipped (e.g. the last panel in each row of the `Layout` example). - iOS applications now use the `UIScene` lifecycle, removing UIKit's legacy lifecycle warning and ensuring SDL windows are created for the connected scene. - Offscreen GPU rendering now supports recursive targets on Metal, OpenGL/GLES, and D3D11, so Lottie alpha/luma mattes, isolated-opacity layers, and cached precomps retain GPU compositing when rendered into an `Image` or `GpuCanvas`. Each `RenderableTarget` leases a Rive render context exclusively for its lifetime and returns it to the pool when destroyed. Repeated Lottie matte and precomp renders now reuse their canvases rather than allocating GPU textures each frame. Metal child targets allocate only their Rive render-canvas output texture; the CPU readback staging texture is created only when pixels are requested. - Fixed undefined offscreen contents when nesting pooled render targets on all GPU backends. Render context slots were recycled whenever no frame was currently active, so two long-lived targets could share one slot; once their frames nested — which happens as Lottie matte and precomp layers cross their in/out points and the nesting order changes between frames — the inner target skipped `beginFrame` and was then flushed against the outer target's frame descriptor. @@ -330,6 +332,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Safe area support: `Component::getSafeAreaBounds()` and `safeAreaChanged()` virtual (backed by `ComponentNative::getSafeAreaBounds()`), so content can avoid display cutouts and system bars on mobile devices - High dpi support on Windows and Linux X11: window bounds, screen geometry and input coordinates are now logical points everywhere (converted at the SDL boundary), so windows and content scale with the display scale like on macOS; live display scale changes resize the native window keeping the logical size +#### Layout +- `FlexBox`: flex-shrink is now applied when a line overflows (deficit distributed by `flexShrink × size`), `wrapReverse` reverses the line order on the cross axis, and min/max constraints are now clamped for every `align-self` value instead of only `stretch` +- `FlexBox`/`Grid` items: unspecified sizes now fall back to the component's current bounds (intrinsic/measured sizing) instead of collapsing to zero +- Percentage sizing: `FlexItem::withWidthPercent()` / `withHeightPercent()` (resolved against the container) and `GridItem::withWidthPercent()` / `withHeightPercent()` (resolved against the cell) +- `FlexBox` baseline alignment: `AlignItems::baseline` / `AlignSelf::baseline` with `FlexItem::withBaseline()` aligning items on the line's shared baseline +- `GridItem`: new `width`/`height`/`minWidth`/`minHeight`/`maxWidth`/`maxHeight` fields with fluent setters, replacing the hardcoded 100×100 fallback for non-stretch items +- `Grid` auto-placement: items with unset `column`/`row` (-1) flow row by row into the first free cell, skipping cells occupied by explicit items and spans; implicit tracks are created as needed + #### Text - `TextEditor` and `Label` components ([#16](https://github.com/kunitoki/yup/pull/16), [#55](https://github.com/kunitoki/yup/pull/55)) - Improved fonts: better layouting, variable font axis manipulation, embedded fallback font ([#55](https://github.com/kunitoki/yup/pull/55)) diff --git a/examples/graphics/source/examples/Layout.h b/examples/graphics/source/examples/Layout.h new file mode 100644 index 000000000..aac1b2bbf --- /dev/null +++ b/examples/graphics/source/examples/Layout.h @@ -0,0 +1,1275 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +//============================================================================== +namespace +{ + +//============================================================================== +/** A small colored box used as a visual placeholder in the layout demos. */ +class LayoutCell : public yup::Component +{ +public: + LayoutCell (const yup::String& label, yup::Color color) + : Component ("layoutCell") + , cellLabel (label) + , cellColor (color) + , cellFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (11.0f)) + { + } + + void paint (yup::Graphics& g) override + { + auto bounds = getLocalBounds().reduced (1.0f); + + g.setFillColor (cellColor); + g.fillRoundedRect (bounds, 4.0f); + + g.setFillColor (yup::Colors::white); + g.fillFittedText (cellLabel, cellFont, bounds, yup::Justification::center); + } + +private: + yup::String cellLabel; + yup::Color cellColor; + yup::Font cellFont; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LayoutCell) +}; + +//============================================================================== +/** Cycles through a fixed palette of pleasant colors for the demo cells. */ +static yup::Color cellColor (int index) +{ + static const yup::Color palette[] = { + yup::Colors::tomato, + yup::Colors::dodgerblue, + yup::Colors::seagreen, + yup::Colors::goldenrod, + yup::Colors::orchid, + yup::Colors::teal, + yup::Colors::indianred, + yup::Colors::steelblue, + yup::Colors::peru, + yup::Colors::firebrick, + yup::Colors::mediumseagreen, + yup::Colors::darkslateblue, + }; + + return palette[index % yup::numElementsInArray (palette)]; +} + +//============================================================================== +/** Base class for a labeled panel that hosts a single layout demonstration. */ +class DemoPanel : public yup::Component +{ +public: + DemoPanel (const yup::String& title) + : Component ("layoutDemoPanel") + , panelTitle (title) + , captionFont (yup::ApplicationTheme::getGlobalTheme()->getDefaultFont().withHeight (13.0f)) + { + setOpaque (false); // translucent rounded-rect background + } + + void paint (yup::Graphics& g) override + { + auto bounds = getLocalBounds().reduced (0.5f); + + g.setFillColor (yup::Color (0x22ffffff)); + g.fillRoundedRect (bounds, 8.0f); + + g.setStrokeColor (yup::Color (0x55ffffff)); + g.strokeRoundedRect (bounds, 8.0f); + + g.setFillColor (yup::Colors::white); + g.fillFittedText (panelTitle, captionFont, bounds.removeFromTop (24.0f), yup::Justification::center); + } + + /** Creates a cell, makes it visible and returns it so it can be added to a layout. */ + LayoutCell* addCell (const yup::String& label, yup::Color color, float width = 56.0f, float height = 32.0f) + { + auto* cell = new LayoutCell (label, color); + cell->setSize (width, height); + cells.add (cell); + addAndMakeVisible (cell); + return cell; + } + +protected: + /** The area below the caption, where the layout is performed. */ + yup::Rectangle getContentArea() const + { + return getLocalBounds().withTrimmedTop (26.0f).reduced (8.0f); + } + +private: + yup::OwnedArray cells; + yup::String panelTitle; + yup::Font captionFont; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoPanel) +}; + +//============================================================================== +/** A panel that demonstrates a FlexBox layout. */ +class FlexPanel : public DemoPanel +{ +public: + using DemoPanel::DemoPanel; + + /** The FlexBox used by this panel. Configure it and add items to it. */ + yup::FlexBox& box() + { + return flexBox; + } + + /** Adds a cell as a flex item with the given main/cross base sizes. */ + yup::FlexItem& addItem (LayoutCell* cell, float width, float height) + { + flexBox.items.add (yup::FlexItem (*cell, width, height)); + return flexBox.items.getReference (flexBox.items.size() - 1); + } + + void resized() override + { + flexBox.performLayout (getContentArea()); + } + +private: + yup::FlexBox flexBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FlexPanel) +}; + +//============================================================================== +/** A panel that demonstrates a Grid layout. */ +class GridPanel : public DemoPanel +{ +public: + using DemoPanel::DemoPanel; + + /** The Grid used by this panel. Configure it and add items to it. */ + yup::Grid& grid() + { + return gridBox; + } + + /** Adds a cell as a grid item, optionally at an explicit column/row. */ + yup::GridItem& addItem (LayoutCell* cell, int column = -1, int row = -1) + { + gridBox.items.add (yup::GridItem (*cell)); + auto& item = gridBox.items.getReference (gridBox.items.size() - 1); + item.column = column; + item.row = row; + return item; + } + + void resized() override + { + gridBox.performLayout (getContentArea()); + } + +private: + yup::Grid gridBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GridPanel) +}; + +//============================================================================== +/** A page that wraps a set of panels using a responsive flexbox. */ +class DemoPage : public yup::Component +{ +public: + DemoPage() + : Component ("layoutDemoPage") + { + setOpaque (false); // transparent container, only panels are drawn + } + + /** Adds a panel with the given base width/height used by the wrapping layout. */ + void addPanel (DemoPanel* panel, float minWidth = 240.0f, float minHeight = 160.0f) + { + panels.add (panel); + panelWidths.add (minWidth); + panelHeights.add (minHeight); + addAndMakeVisible (panel); + } + + void resized() override + { + pageBox.flexDirection = yup::FlexBox::Direction::row; + pageBox.flexWrap = yup::FlexBox::Wrap::wrap; + pageBox.alignItems = yup::FlexBox::AlignItems::stretch; + pageBox.justifyContent = yup::FlexBox::JustifyContent::flexStart; + pageBox.alignContent = yup::FlexBox::AlignContent::flexStart; + pageBox.gap = 10.0f; + + pageBox.items.clear(); + + for (int i = 0; i < panels.size(); ++i) + { + auto* panel = panels.getUnchecked (i); + pageBox.items.add (yup::FlexItem (*panel, panelWidths.getUnchecked (i), panelHeights.getUnchecked (i)) + .withFlex (1.0f) + .withMinWidth (panelWidths.getUnchecked (i))); + } + + pageBox.performLayout (getLocalBounds()); + } + +private: + yup::OwnedArray panels; + yup::Array panelWidths; + yup::Array panelHeights; + yup::FlexBox pageBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoPage) +}; + +//============================================================================== +/** A component that hosts an inner flexbox, used to demonstrate nesting. */ +class InnerFlexHost : public yup::Component +{ +public: + InnerFlexHost() + : Component ("innerFlexHost") + { + setOpaque (false); // transparent container, only cells are drawn + + for (int i = 0; i < 3; ++i) + { + auto* cell = new LayoutCell (yup::String (i + 1), cellColor (i)); + cell->setSize (0, 0); + cells.add (cell); + addAndMakeVisible (cell); + } + } + + void resized() override + { + innerBox.flexDirection = yup::FlexBox::Direction::row; + innerBox.gap = 6.0f; + + innerBox.items.clear(); + innerBox.items.add (yup::FlexItem (*cells[0], 0, 0).withFlex (1)); + innerBox.items.add (yup::FlexItem (*cells[1], 0, 0).withFlex (1)); + innerBox.items.add (yup::FlexItem (*cells[2], 0, 0).withFlex (2)); + innerBox.performLayout (getLocalBounds().reduced (6.0f)); + } + +private: + yup::OwnedArray cells; + yup::FlexBox innerBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (InnerFlexHost) +}; + +//============================================================================== +/** Nested demo: a flex column with a flex row in the middle (holy grail). */ +class HolyGrailPanel : public DemoPanel +{ +public: + HolyGrailPanel() + : DemoPanel ("nested: flex column (holy grail)") + { + headerCell = addCell ("header", yup::Colors::steelblue); + sidebarCell = addCell ("sidebar", yup::Colors::tomato, 56.0f, 40.0f); + mainCell = addCell ("main", yup::Colors::seagreen); + footerCell = addCell ("footer", yup::Colors::steelblue); + + middleBox.flexDirection = yup::FlexBox::Direction::row; + middleBox.gap = 6.0f; + } + + void resized() override + { + auto area = getContentArea(); + + auto headerArea = area.removeFromTop (26.0f); + auto footerArea = area.removeFromBottom (26.0f); + auto middleArea = area.reduced (0.0f, 6.0f); + + headerCell->setBounds (headerArea); + footerCell->setBounds (footerArea); + + middleBox.items.clear(); + middleBox.items.add (yup::FlexItem (*sidebarCell, 56.0f, 0)); + middleBox.items.add (yup::FlexItem (*mainCell, 0, 0).withFlex (1)); + middleBox.performLayout (middleArea); + } + +private: + LayoutCell* headerCell = nullptr; + LayoutCell* sidebarCell = nullptr; + LayoutCell* mainCell = nullptr; + LayoutCell* footerCell = nullptr; + yup::FlexBox middleBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (HolyGrailPanel) +}; + +//============================================================================== +/** Nested demo: a flexbox hosted inside a grid cell. */ +class FlexInGridPanel : public DemoPanel +{ +public: + FlexInGridPanel() + : DemoPanel ("nested: flex inside grid") + { + leftCell = addCell ("left", yup::Colors::tomato); + rightCell = addCell ("right", yup::Colors::dodgerblue); + + innerHost = std::make_unique(); + addAndMakeVisible (innerHost.get()); + + gridBox.templateColumns.add (yup::Grid::TrackInfo::px (56.0f)); + gridBox.templateColumns.add (yup::Grid::TrackInfo::fr (1.0f)); + gridBox.templateColumns.add (yup::Grid::TrackInfo::px (56.0f)); + gridBox.templateRows.add (yup::Grid::TrackInfo::px (90.0f)); + + gridBox.items.add (yup::GridItem (*leftCell).withColumn (0).withRow (0)); + gridBox.items.add (yup::GridItem (*innerHost).withColumn (1).withRow (0)); + gridBox.items.add (yup::GridItem (*rightCell).withColumn (2).withRow (0)); + } + + void resized() override + { + gridBox.performLayout (getContentArea()); + } + +private: + LayoutCell* leftCell = nullptr; + LayoutCell* rightCell = nullptr; + std::unique_ptr innerHost; + yup::Grid gridBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FlexInGridPanel) +}; + +//============================================================================== +/** Nested demo: two grids hosted inside a flexbox row. */ +class GridInFlexPanel : public DemoPanel +{ +public: + GridInFlexPanel() + : DemoPanel ("nested: grid inside flex") + { + leftGrid = std::make_unique ("inner grid: fr tracks"); + leftGrid->grid().templateColumns.add (yup::Grid::TrackInfo::fr (1)); + leftGrid->grid().templateColumns.add (yup::Grid::TrackInfo::fr (2)); + leftGrid->grid().templateRows.add (yup::Grid::TrackInfo::px (56.0f)); + leftGrid->grid().columnGap = 6.0f; + leftGrid->addItem (leftGrid->addCell ("a", yup::Colors::seagreen), 0, 0); + leftGrid->addItem (leftGrid->addCell ("b", yup::Colors::seagreen), 1, 0); + addAndMakeVisible (leftGrid.get()); + + rightGrid = std::make_unique ("inner grid: 2x2"); + rightGrid->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + rightGrid->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + rightGrid->grid().templateRows.add (yup::Grid::TrackInfo::px (30.0f)); + rightGrid->grid().templateRows.add (yup::Grid::TrackInfo::px (30.0f)); + rightGrid->grid().rowGap = 6.0f; + for (int i = 0; i < 4; ++i) + { + auto& item = rightGrid->addItem (rightGrid->addCell (yup::String (i + 1), yup::Colors::orchid)); + item.column = i % 2; + item.row = i / 2; + } + addAndMakeVisible (rightGrid.get()); + + flexBox.flexDirection = yup::FlexBox::Direction::row; + flexBox.gap = 8.0f; + } + + void resized() override + { + flexBox.items.clear(); + flexBox.items.add (yup::FlexItem (*leftGrid, 0, 0).withFlex (1)); + flexBox.items.add (yup::FlexItem (*rightGrid, 0, 0).withFlex (1)); + flexBox.performLayout (getContentArea()); + } + +private: + std::unique_ptr leftGrid; + std::unique_ptr rightGrid; + yup::FlexBox flexBox; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (GridInFlexPanel) +}; + +//============================================================================== +/** Page: all four flex directions side by side. */ +static DemoPage* makeDirectionPage() +{ + auto* page = new DemoPage(); + + auto addDirectionPanel = [] (const char* title, yup::FlexBox::Direction direction) + { + auto* panel = new FlexPanel (title); + panel->box().flexDirection = direction; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 60.0f, 28.0f); + + return panel; + }; + + page->addPanel (addDirectionPanel ("flex-direction: row", yup::FlexBox::Direction::row)); + page->addPanel (addDirectionPanel ("flex-direction: row-reverse", yup::FlexBox::Direction::rowReverse)); + page->addPanel (addDirectionPanel ("flex-direction: column", yup::FlexBox::Direction::column)); + page->addPanel (addDirectionPanel ("flex-direction: column-reverse", yup::FlexBox::Direction::columnReverse)); + + return page; +} + +//============================================================================== +/** Page: no-wrap, wrap and wrap-reverse. */ +static DemoPage* makeWrapPage() +{ + auto* page = new DemoPage(); + + auto addWrapPanel = [] (const char* title, yup::FlexBox::Wrap wrap) + { + auto* panel = new FlexPanel (title); + panel->box().flexWrap = wrap; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + panel->box().gap = 6.0f; + + for (int i = 0; i < 5; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 70.0f, 28.0f); + + return panel; + }; + + page->addPanel (addWrapPanel ("flex-wrap: no-wrap", yup::FlexBox::Wrap::noWrap)); + page->addPanel (addWrapPanel ("flex-wrap: wrap", yup::FlexBox::Wrap::wrap)); + page->addPanel (addWrapPanel ("flex-wrap: wrap-reverse", yup::FlexBox::Wrap::wrapReverse)); + + return page; +} + +//============================================================================== +/** Page: every justify-content value. */ +static DemoPage* makeJustifyContentPage() +{ + auto* page = new DemoPage(); + + auto addJustifyPanel = [] (const char* title, yup::FlexBox::JustifyContent value) + { + auto* panel = new FlexPanel (title); + panel->box().justifyContent = value; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 56.0f, 28.0f); + + return panel; + }; + + page->addPanel (addJustifyPanel ("justify-content: flex-start", yup::FlexBox::JustifyContent::flexStart)); + page->addPanel (addJustifyPanel ("justify-content: flex-end", yup::FlexBox::JustifyContent::flexEnd)); + page->addPanel (addJustifyPanel ("justify-content: center", yup::FlexBox::JustifyContent::center)); + page->addPanel (addJustifyPanel ("justify-content: space-between", yup::FlexBox::JustifyContent::spaceBetween)); + page->addPanel (addJustifyPanel ("justify-content: space-around", yup::FlexBox::JustifyContent::spaceAround)); + + return page; +} + +//============================================================================== +/** Page: every align-items value. */ +static DemoPage* makeAlignItemsPage() +{ + auto* page = new DemoPage(); + + auto addAlignPanel = [] (const char* title, yup::FlexBox::AlignItems value) + { + auto* panel = new FlexPanel (title); + panel->box().alignItems = value; + + const float heights[] = { 24.0f, 36.0f, 28.0f }; + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 64.0f, heights[i]); + + return panel; + }; + + page->addPanel (addAlignPanel ("align-items: flex-start", yup::FlexBox::AlignItems::flexStart)); + page->addPanel (addAlignPanel ("align-items: flex-end", yup::FlexBox::AlignItems::flexEnd)); + page->addPanel (addAlignPanel ("align-items: center", yup::FlexBox::AlignItems::center)); + page->addPanel (addAlignPanel ("align-items: stretch", yup::FlexBox::AlignItems::stretch)); + + { + auto* panel = new FlexPanel ("align-items: baseline"); + panel->box().alignItems = yup::FlexBox::AlignItems::baseline; + + auto& item1 = panel->addItem (panel->addCell ("1", cellColor (0)), 64.0f, 24.0f); + item1.baseline = 18.0f; + + auto& item2 = panel->addItem (panel->addCell ("2", cellColor (1)), 64.0f, 36.0f); + item2.baseline = 26.0f; + + auto& item3 = panel->addItem (panel->addCell ("3", cellColor (2)), 64.0f, 28.0f); + item3.baseline = 22.0f; + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: every align-content value, with wrapping enabled. */ +static DemoPage* makeAlignContentPage() +{ + auto* page = new DemoPage(); + + auto addAlignContentPanel = [] (const char* title, yup::FlexBox::AlignContent value) + { + auto* panel = new FlexPanel (title); + panel->box().flexWrap = yup::FlexBox::Wrap::wrap; + panel->box().alignContent = value; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + panel->box().gap = 6.0f; + + for (int i = 0; i < 6; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 64.0f, 30.0f); + + return panel; + }; + + page->addPanel (addAlignContentPanel ("align-content: flex-start", yup::FlexBox::AlignContent::flexStart), 210.0f); + page->addPanel (addAlignContentPanel ("align-content: flex-end", yup::FlexBox::AlignContent::flexEnd), 210.0f); + page->addPanel (addAlignContentPanel ("align-content: center", yup::FlexBox::AlignContent::center), 210.0f); + page->addPanel (addAlignContentPanel ("align-content: space-between", yup::FlexBox::AlignContent::spaceBetween), 210.0f); + page->addPanel (addAlignContentPanel ("align-content: space-around", yup::FlexBox::AlignContent::spaceAround), 210.0f); + page->addPanel (addAlignContentPanel ("align-content: stretch", yup::FlexBox::AlignContent::stretch), 210.0f); + + return page; +} + +//============================================================================== +/** Page: flex-grow, flex-shrink and flex-basis. */ +static DemoPage* makeGrowShrinkBasisPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new FlexPanel ("flex-grow: 1, 1, 2"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + auto& grow1 = panel->addItem (panel->addCell ("grow 1", cellColor (0)), 50.0f, 28.0f); + grow1.flexGrow = 1.0f; + + auto& grow2 = panel->addItem (panel->addCell ("grow 1", cellColor (1)), 50.0f, 28.0f); + grow2.flexGrow = 1.0f; + + auto& grow3 = panel->addItem (panel->addCell ("grow 2", cellColor (2)), 50.0f, 28.0f); + grow3.flexGrow = 2.0f; + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("flex-shrink: 1, 3, 0"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + auto& item1 = panel->addItem (panel->addCell ("shrink 1", cellColor (0)), 100.0f, 28.0f); + item1.flexShrink = 1.0f; + + auto& item2 = panel->addItem (panel->addCell ("shrink 3", cellColor (1)), 100.0f, 28.0f); + item2.flexShrink = 3.0f; + + auto& item3 = panel->addItem (panel->addCell ("shrink 0", cellColor (2)), 100.0f, 28.0f); + item3.flexShrink = 0.0f; + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("flex-basis: 50, 70, 60 + grow 1"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + const float basis[] = { 50.0f, 70.0f, 60.0f }; + + for (int i = 0; i < 3; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (basis[i]), cellColor (i)), 50.0f, 28.0f); + item.flexGrow = 1.0f; + item.flexBasis = basis[i]; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: align-self and order. */ +static DemoPage* makeAlignSelfOrderPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new FlexPanel ("align-self: start / end / center / stretch"); + panel->box().alignItems = yup::FlexBox::AlignItems::center; + + auto& item1 = panel->addItem (panel->addCell ("start", cellColor (0)), 56.0f, 40.0f); + item1.alignSelf = yup::FlexItem::AlignSelf::flexStart; + + auto& item2 = panel->addItem (panel->addCell ("end", cellColor (1)), 56.0f, 40.0f); + item2.alignSelf = yup::FlexItem::AlignSelf::flexEnd; + + auto& item3 = panel->addItem (panel->addCell ("center", cellColor (2)), 56.0f, 40.0f); + item3.alignSelf = yup::FlexItem::AlignSelf::center; + + auto& item4 = panel->addItem (panel->addCell ("stretch", cellColor (3)), 56.0f, 40.0f); + item4.alignSelf = yup::FlexItem::AlignSelf::stretch; + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("align-self: baseline"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + auto& item1 = panel->addItem (panel->addCell ("1", cellColor (0)), 64.0f, 40.0f); + item1.alignSelf = yup::FlexItem::AlignSelf::baseline; + item1.baseline = 20.0f; + + auto& item2 = panel->addItem (panel->addCell ("2", cellColor (1)), 64.0f, 56.0f); + item2.alignSelf = yup::FlexItem::AlignSelf::baseline; + item2.baseline = 30.0f; + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("order: 3, 1, 0, 2"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + const int orders[] = { 3, 1, 0, 2 }; + + for (int i = 0; i < 4; ++i) + { + auto& item = panel->addItem (panel->addCell ("order " + yup::String (orders[i]), cellColor (i)), 56.0f, 28.0f); + item.order = orders[i]; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: margins and gap. */ +static DemoPage* makeMarginsGapPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new FlexPanel ("margins: 8px"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + for (int i = 0; i < 3; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 56.0f, 28.0f); + item.marginLeft = item.marginRight = item.marginTop = item.marginBottom = 8.0f; + } + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("gap: 14px"); + panel->box().gap = 14.0f; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 56.0f, 28.0f); + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("margins + gap"); + panel->box().gap = 8.0f; + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + for (int i = 0; i < 3; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), 56.0f, 28.0f); + item.marginLeft = item.marginRight = item.marginTop = item.marginBottom = 6.0f; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: percentage sizing and min/max constraints on flex items. */ +static DemoPage* makeFlexPercentMinMaxPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new FlexPanel ("width/height percent"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + auto& item1 = panel->addItem (panel->addCell ("30%", cellColor (0)), 0, 0); + item1.widthPercent = 30.0f; + item1.heightPercent = 50.0f; + + auto& item2 = panel->addItem (panel->addCell ("40%", cellColor (1)), 0, 0); + item2.widthPercent = 40.0f; + item2.heightPercent = 50.0f; + + auto& item3 = panel->addItem (panel->addCell ("30%", cellColor (2)), 0, 0); + item3.widthPercent = 30.0f; + item3.heightPercent = 50.0f; + + page->addPanel (panel); + } + + { + auto* panel = new FlexPanel ("min/max constraints"); + panel->box().alignItems = yup::FlexBox::AlignItems::flexStart; + + auto& item1 = panel->addItem (panel->addCell ("min-w 80", cellColor (0)), 40.0f, 28.0f); + item1.flexGrow = 1.0f; + item1.minWidth = 80.0f; + + auto& item2 = panel->addItem (panel->addCell ("max-w 60", cellColor (1)), 40.0f, 28.0f); + item2.flexGrow = 1.0f; + item2.maxWidth = 60.0f; + + auto& item3 = panel->addItem (panel->addCell ("min-h 70", cellColor (2)), 40.0f, 60.0f); + item3.minHeight = 70.0f; + + auto& item4 = panel->addItem (panel->addCell ("max-h 40", cellColor (3)), 40.0f, 60.0f); + item4.maxHeight = 40.0f; + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: grid track sizing with px, fr and auto tracks. */ +static DemoPage* makeGridTracksPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("tracks: px(70), fr(1), fr(2)"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (70.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::fr (1.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::fr (2.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (60.0f)); + + for (int i = 0; i < 3; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + item.column = i; + item.row = 0; + } + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("tracks: auto, fr(1), fr(2)"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::auto_()); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::fr (1.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::fr (2.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (60.0f)); + + for (int i = 0; i < 3; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + item.column = i; + item.row = 0; + } + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("implicit tracks: autoColumns 100"); + panel->grid().autoColumns = 100.0f; + panel->grid().autoRows = 56.0f; + + for (int i = 0; i < 2; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: explicit grid placement and spans. */ +static DemoPage* makeGridPlacementPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("explicit placement: 2x2"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (80.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (80.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (44.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (44.0f)); + + for (int i = 0; i < 4; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + item.column = i % 2; + item.row = i / 2; + } + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("spans: 2x2, 3 cols, 3 rows"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (32.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (32.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (32.0f)); + panel->grid().rowGap = 6.0f; + + { + auto& item = panel->addItem (panel->addCell ("A", cellColor (0)), 0, 0); + item.columnSpan = 2; + item.rowSpan = 2; + } + { + auto& item = panel->addItem (panel->addCell ("B", cellColor (1))); + item.column = 2; + item.row = 0; + } + { + auto& item = panel->addItem (panel->addCell ("C", cellColor (2))); + item.column = 2; + item.row = 1; + } + { + auto& item = panel->addItem (panel->addCell ("D", cellColor (3))); + item.column = 0; + item.row = 2; + item.columnSpan = 2; + } + { + auto& item = panel->addItem (panel->addCell ("E", cellColor (4))); + item.column = 2; + item.row = 2; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: auto placement flowing into template and implicit tracks. */ +static DemoPage* makeGridAutoPlacementPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("auto flow: 2 template columns"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (70.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (70.0f)); + panel->grid().autoRows = 36.0f; + + for (int i = 0; i < 5; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("explicit + auto placement"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().autoRows = 40.0f; + + { + auto& item = panel->addItem (panel->addCell ("A", cellColor (0))); + item.column = 0; + item.row = 0; + item.columnSpan = 2; + } + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i + 1))); + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("implicit growth: no template"); + panel->grid().autoColumns = 70.0f; + panel->grid().autoRows = 44.0f; + + for (int i = 0; i < 3; ++i) + panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: grid gaps and item margins. */ +static DemoPage* makeGridGapsMarginsPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("columnGap 14 / rowGap 10"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (64.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (64.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (44.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (44.0f)); + panel->grid().columnGap = 14.0f; + panel->grid().rowGap = 10.0f; + + for (int i = 0; i < 4; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i))); + item.column = i % 2; + item.row = i / 2; + } + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("item margins: 8px"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (80.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (80.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (44.0f)); + + for (int i = 0; i < 2; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), i, 0); + item.marginLeft = item.marginRight = item.marginTop = item.marginBottom = 8.0f; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: grid item alignment, both container defaults and per-item overrides. */ +static DemoPage* makeGridAlignmentPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("justifyItems center / alignItems flex-end"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (90.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (90.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (60.0f)); + panel->grid().justifyItems = yup::Grid::AlignItems::center; + panel->grid().alignItems = yup::Grid::AlignItems::flexEnd; + + for (int i = 0; i < 2; ++i) + { + auto& item = panel->addItem (panel->addCell (yup::String (i + 1), cellColor (i)), i, 0); + item.width = 44.0f; + item.height = 22.0f; + } + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("justify-self / align-self overrides"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (90.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (90.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (56.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (56.0f)); + + const auto start = yup::GridItem::AlignSelf::flexStart; + const auto end = yup::GridItem::AlignSelf::flexEnd; + const auto center = yup::GridItem::AlignSelf::center; + const auto stretch = yup::GridItem::AlignSelf::stretch; + + { + auto& item = panel->addItem (panel->addCell ("start", cellColor (0)), 0, 0); + item.width = 40.0f; + item.height = 20.0f; + item.justifySelf = start; + item.alignSelf = start; + } + { + auto& item = panel->addItem (panel->addCell ("end", cellColor (1)), 1, 0); + item.width = 40.0f; + item.height = 20.0f; + item.justifySelf = end; + item.alignSelf = end; + } + { + auto& item = panel->addItem (panel->addCell ("center", cellColor (2)), 0, 1); + item.width = 40.0f; + item.height = 20.0f; + item.justifySelf = center; + item.alignSelf = center; + } + { + auto& item = panel->addItem (panel->addCell ("stretch", cellColor (3)), 1, 1); + item.width = 40.0f; + item.height = 20.0f; + item.justifySelf = stretch; + item.alignSelf = stretch; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: percentage sizing and min/max clamps on grid items. */ +static DemoPage* makeGridPercentMinMaxPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new GridPanel ("percentage of cell: 60% / 50%"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::fr (1.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (90.0f)); + + auto& item = panel->addItem (panel->addCell ("60% x 50%", cellColor (0)), 0, 0); + item.widthPercent = 60.0f; + item.heightPercent = 50.0f; + item.justifySelf = yup::GridItem::AlignSelf::center; + item.alignSelf = yup::GridItem::AlignSelf::center; + + page->addPanel (panel); + } + + { + auto* panel = new GridPanel ("min/max clamps"); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (100.0f)); + panel->grid().templateColumns.add (yup::Grid::TrackInfo::px (100.0f)); + panel->grid().templateRows.add (yup::Grid::TrackInfo::px (70.0f)); + + { + auto& item = panel->addItem (panel->addCell ("min 60x40", cellColor (0)), 0, 0); + item.width = 40.0f; + item.height = 30.0f; + item.minWidth = 60.0f; + item.minHeight = 40.0f; + item.justifySelf = yup::GridItem::AlignSelf::center; + item.alignSelf = yup::GridItem::AlignSelf::center; + } + { + auto& item = panel->addItem (panel->addCell ("max 90x60", cellColor (1)), 1, 0); + item.width = 120.0f; + item.height = 80.0f; + item.maxWidth = 90.0f; + item.maxHeight = 60.0f; + item.justifySelf = yup::GridItem::AlignSelf::center; + item.alignSelf = yup::GridItem::AlignSelf::center; + } + + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Page: nested flex/grid layouts. */ +static DemoPage* makeNestedPage() +{ + auto* page = new DemoPage(); + + { + auto* panel = new HolyGrailPanel(); + page->addPanel (panel, 240.0f, 220.0f); + } + + { + auto* panel = new FlexInGridPanel(); + page->addPanel (panel); + } + + { + auto* panel = new GridInFlexPanel(); + page->addPanel (panel); + } + + return page; +} + +//============================================================================== +/** Creates the page for the given selector index. */ +static DemoPage* createLayoutPage (int index) +{ + switch (index) + { + case 0: + return makeDirectionPage(); + case 1: + return makeWrapPage(); + case 2: + return makeJustifyContentPage(); + case 3: + return makeAlignItemsPage(); + case 4: + return makeAlignContentPage(); + case 5: + return makeGrowShrinkBasisPage(); + case 6: + return makeAlignSelfOrderPage(); + case 7: + return makeMarginsGapPage(); + case 8: + return makeFlexPercentMinMaxPage(); + case 9: + return makeGridTracksPage(); + case 10: + return makeGridPlacementPage(); + case 11: + return makeGridAutoPlacementPage(); + case 12: + return makeGridGapsMarginsPage(); + case 13: + return makeGridAlignmentPage(); + case 14: + return makeGridPercentMinMaxPage(); + case 15: + return makeNestedPage(); + default: + return new DemoPage(); + } +} + +} // namespace + +//============================================================================== +/** + Demonstrates the CSS-style FlexBox and Grid layout containers available in + yup_gui, exercising every layout property: flex direction, wrapping, + justification, cross-axis alignment, flex grow/shrink/basis, align-self, + order, margins, gaps, percentage sizing, min/max constraints, grid track + sizing (px / fr / auto), explicit placement, spans, auto placement, grid + alignment and nested flex/grid compositions. + + Use the combo box at the top to switch between the different pages. +*/ +class LayoutExample : public yup::Component +{ +public: + LayoutExample() + : Component ("LayoutExample") + { + setupSelector(); + showPage (0); + } + + void resized() override + { + auto bounds = getLocalBounds().reduced (10.0f); + + selector->setBounds (bounds.removeFromTop (30.0f)); + + bounds.removeFromTop (10.0f); + + if (page != nullptr) + page->setBounds (bounds); + } + + void paint (yup::Graphics& g) override + { + g.setFillColor (findColor (yup::DocumentWindow::Style::backgroundColorId).value_or (yup::Colors::dimgray)); + g.fillAll(); + } + +private: + void setupSelector() + { + selector = std::make_unique ("layoutPageSelector"); + + selector->addItem ("Flex Direction", 1); + selector->addItem ("Flex Wrap", 2); + selector->addItem ("Justify Content", 3); + selector->addItem ("Align Items", 4); + selector->addItem ("Align Content", 5); + selector->addItem ("Grow / Shrink / Basis", 6); + selector->addItem ("Align Self & Order", 7); + selector->addItem ("Margins & Gap", 8); + selector->addItem ("Percent & Min/Max", 9); + selector->addItem ("Grid Tracks", 10); + selector->addItem ("Grid Placement & Spans", 11); + selector->addItem ("Grid Auto Placement", 12); + selector->addItem ("Grid Gaps & Margins", 13); + selector->addItem ("Grid Alignment", 14); + selector->addItem ("Grid Percent & Min/Max", 15); + selector->addItem ("Nested Layout", 16); + + selector->setSelectedId (1, yup::dontSendNotification); + selector->onSelectedItemChanged = [this] + { + showPage (selector->getSelectedId() - 1); + }; + + addAndMakeVisible (selector.get()); + } + + void showPage (int index) + { + if (page != nullptr) + removeChildComponent (page.get()); + + page.reset (createLayoutPage (index)); + page->setBounds (getLocalBounds()); + addAndMakeVisible (page.get()); + resized(); + } + + std::unique_ptr selector; + std::unique_ptr page; + + YUP_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (LayoutExample) +}; diff --git a/examples/graphics/source/main.cpp b/examples/graphics/source/main.cpp index b9b2de1a2..33e89df8a 100644 --- a/examples/graphics/source/main.cpp +++ b/examples/graphics/source/main.cpp @@ -76,6 +76,7 @@ inline yup::File getAssetPath (yup::StringRef subPath = {}) #include "examples/FilterDemo.h" #include "examples/GpuAudioProcessingDemo.h" #include "examples/Images.h" +#include "examples/Layout.h" #include "examples/LayoutFonts.h" #include "examples/LottieDemo.h" #include "examples/OffscreenRenderDemo.h" @@ -187,6 +188,7 @@ class CustomWindow addDemo ("Filter Demo", [] { return std::make_unique(); }); addDemo ("GPU Audio", [] { return std::make_unique(); }); addDemo ("Images", [] { return std::make_unique(); }); + addDemo ("Layout", [] { return std::make_unique(); }); addDemo ("Layout Fonts", [] { return std::make_unique(); }); addDemo ("Lottie", [] { return std::make_unique(); }); addDemo ("Offscreen Render", [] { return std::make_unique(); }); diff --git a/modules/yup_gui/layout/yup_FlexBox.cpp b/modules/yup_gui/layout/yup_FlexBox.cpp new file mode 100644 index 000000000..a0b785b57 --- /dev/null +++ b/modules/yup_gui/layout/yup_FlexBox.cpp @@ -0,0 +1,444 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +FlexBox::FlexBox (Direction d) + : flexDirection (d) +{ +} + +FlexBox::FlexBox (Direction d, Wrap w, AlignItems ai, JustifyContent jc, AlignContent ac) + : flexDirection (d) + , flexWrap (w) + , alignItems (ai) + , justifyContent (jc) + , alignContent (ac) +{ +} + +//============================================================================== +static bool isRowDirection (FlexBox::Direction direction) +{ + return direction == FlexBox::Direction::row || direction == FlexBox::Direction::rowReverse; +} + +static bool isReverseDirection (FlexBox::Direction direction) +{ + return direction == FlexBox::Direction::rowReverse || direction == FlexBox::Direction::columnReverse; +} + +//============================================================================== +// Resolves the item's main-axis size, honoring (in priority order) flex-basis, +// percentage sizing, fixed sizing, and finally the component's current size. +static float resolveItemMainSize (const FlexItem& item, bool isRow, float containerMainSize) +{ + if (item.flexBasis > 0.0f) + return item.flexBasis; + + const float mainPercent = isRow ? item.widthPercent : item.heightPercent; + if (mainPercent >= 0.0f) + return containerMainSize * mainPercent / 100.0f; + + const float mainSize = isRow ? item.width : item.height; + if (mainSize > 0.0f) + return mainSize; + + if (item.associatedComponent != nullptr) + return isRow ? item.associatedComponent->getWidth() + : item.associatedComponent->getHeight(); + + return 0.0f; +} + +// Resolves the item's cross-axis size, honoring (in priority order) percentage +// sizing, fixed sizing, and finally the component's current size. +static float resolveItemCrossSize (const FlexItem& item, bool isRow, float containerCrossSize) +{ + const float crossPercent = isRow ? item.heightPercent : item.widthPercent; + if (crossPercent >= 0.0f) + return containerCrossSize * crossPercent / 100.0f; + + const float crossSize = isRow ? item.height : item.width; + if (crossSize > 0.0f) + return crossSize; + + if (item.associatedComponent != nullptr) + return isRow ? item.associatedComponent->getHeight() + : item.associatedComponent->getWidth(); + + return 0.0f; +} + +// Resolves the item's cross-axis alignment, mapping the container's align-items +// value onto the item's align-self when the item uses auto alignment. +static FlexItem::AlignSelf resolveItemAlign (const FlexItem& item, FlexBox::AlignItems alignItems) +{ + if (item.alignSelf != FlexItem::AlignSelf::autoAlign) + return item.alignSelf; + + switch (alignItems) + { + case FlexBox::AlignItems::flexStart: + return FlexItem::AlignSelf::flexStart; + case FlexBox::AlignItems::flexEnd: + return FlexItem::AlignSelf::flexEnd; + case FlexBox::AlignItems::center: + return FlexItem::AlignSelf::center; + case FlexBox::AlignItems::stretch: + return FlexItem::AlignSelf::stretch; + case FlexBox::AlignItems::baseline: + return FlexItem::AlignSelf::baseline; + } + + return FlexItem::AlignSelf::autoAlign; +} + +//============================================================================== +void FlexBox::performLayout (Rectangle targetArea) +{ + if (items.size() == 0) + return; + + // Sort items by order + Array sortedItems; + sortedItems.ensureStorageAllocated (items.size()); + + for (auto& item : items) + sortedItems.add (&item); + + std::sort (sortedItems.begin(), sortedItems.end(), [] (const FlexItem* a, const FlexItem* b) + { + return a->order < b->order; + }); + + const bool isRow = isRowDirection (flexDirection); + const bool isReverse = isReverseDirection (flexDirection); + + const float containerMainSize = isRow ? targetArea.getWidth() : targetArea.getHeight(); + const float containerCrossSize = isRow ? targetArea.getHeight() : targetArea.getWidth(); + const float containerMainStart = isRow ? targetArea.getX() : targetArea.getY(); + const float containerCrossStart = isRow ? targetArea.getY() : targetArea.getX(); + + // Build lines + Array lines; + FlexBox::LineInfo currentLine; + float currentMainSize = 0.0f; + + for (int i = 0; i < sortedItems.size(); ++i) + { + auto* item = sortedItems.getUnchecked (i); + + float itemMainSize = resolveItemMainSize (*item, isRow, containerMainSize); + float itemMainMarginStart = isRow ? item->marginLeft : item->marginTop; + float itemMainMarginEnd = isRow ? item->marginRight : item->marginBottom; + + const float totalItemMainSize = itemMainSize + itemMainMarginStart + itemMainMarginEnd; + + if (flexWrap != Wrap::noWrap && ! currentLine.items.isEmpty() + && currentMainSize + totalItemMainSize + gap > containerMainSize) + { + // Start a new line + lines.add (currentLine); + currentLine = {}; + currentMainSize = 0.0f; + } + + currentLine.items.add (item); + currentMainSize += totalItemMainSize; + } + + if (! currentLine.items.isEmpty()) + lines.add (currentLine); + + // Reverse the line order for wrap-reverse, so the first line is laid out at + // the end of the cross axis instead of the start. + if (flexWrap == Wrap::wrapReverse) + std::reverse (lines.begin(), lines.end()); + + // Calculate cross sizes for lines + for (auto& line : lines) + { + float maxCrossSize = 0.0f; + + for (auto* item : line.items) + { + float itemCrossSize = resolveItemCrossSize (*item, isRow, containerCrossSize); + float crossMarginStart = isRow ? item->marginTop : item->marginLeft; + float crossMarginEnd = isRow ? item->marginBottom : item->marginRight; + maxCrossSize = std::max (maxCrossSize, itemCrossSize + crossMarginStart + crossMarginEnd); + } + + line.crossSize = maxCrossSize; + line.totalMainSize = 0.0f; + + for (auto* item : line.items) + { + float itemMainSize = resolveItemMainSize (*item, isRow, containerMainSize); + float mainMarginStart = isRow ? item->marginLeft : item->marginTop; + float mainMarginEnd = isRow ? item->marginRight : item->marginBottom; + line.totalMainSize += itemMainSize + mainMarginStart + mainMarginEnd; + } + } + + // Calculate total cross size + float totalCrossSize = 0.0f; + for (const auto& line : lines) + totalCrossSize += line.crossSize + gap; + + if (lines.size() > 0) + totalCrossSize -= gap; + + // Align lines on cross axis + float crossOffset; + + switch (alignContent) + { + case AlignContent::flexStart: + crossOffset = 0.0f; + break; + case AlignContent::flexEnd: + crossOffset = containerCrossSize - totalCrossSize; + break; + case AlignContent::center: + crossOffset = (containerCrossSize - totalCrossSize) / 2.0f; + break; + case AlignContent::spaceBetween: + crossOffset = 0.0f; + break; + case AlignContent::spaceAround: + crossOffset = (containerCrossSize - totalCrossSize) / (float) (lines.size() + 1); + break; + case AlignContent::stretch: + crossOffset = 0.0f; + break; + } + + // Position items + float currentCrossPos = containerCrossStart + crossOffset; + + for (int lineIdx = 0; lineIdx < lines.size(); ++lineIdx) + { + auto& line = lines.getReference (lineIdx); + + // Calculate cross size for this line + float lineCrossSize = line.crossSize; + + if (alignContent == AlignContent::stretch && lines.size() > 1) + lineCrossSize = (containerCrossSize - totalCrossSize) / (float) lines.size() + line.crossSize; + + if (alignContent == AlignContent::spaceBetween && lines.size() > 1) + { + if (lineIdx == 0) + currentCrossPos = containerCrossStart; + else if (lineIdx == lines.size() - 1) + currentCrossPos = containerCrossStart + containerCrossSize - lineCrossSize; + else + currentCrossPos = containerCrossStart + (containerCrossSize - totalCrossSize) * (float) lineIdx / (float) (lines.size() - 1); + } + + // Pre-compute the shared baseline for baseline-aligned items + float lineBaseline = 0.0f; + + for (auto* item : line.items) + { + if (resolveItemAlign (*item, alignItems) == FlexItem::AlignSelf::baseline) + { + float crossMarginStart = isRow ? item->marginTop : item->marginLeft; + float itemBaseline = item->baseline >= 0.0f ? item->baseline + : resolveItemCrossSize (*item, isRow, containerCrossSize); + lineBaseline = std::max (lineBaseline, crossMarginStart + itemBaseline); + } + } + + // Calculate flex-grow / flex-shrink weights + float totalFlexGrow = 0.0f; + float totalShrinkWeight = 0.0f; + float totalFixedSize = 0.0f; + + for (auto* item : line.items) + { + float itemMainSize = resolveItemMainSize (*item, isRow, containerMainSize); + float mainMarginStart = isRow ? item->marginLeft : item->marginTop; + float mainMarginEnd = isRow ? item->marginRight : item->marginBottom; + + totalFixedSize += itemMainSize + mainMarginStart + mainMarginEnd; + + if (item->flexGrow > 0.0f) + totalFlexGrow += item->flexGrow; + + if (item->flexShrink > 0.0f) + totalShrinkWeight += item->flexShrink * itemMainSize; + } + + const float interItemGapTotal = line.items.size() > 1 ? (line.items.size() - 1) * gap : 0.0f; + + float extraSpace = containerMainSize - totalFixedSize - interItemGapTotal; + + // Calculate main axis offset + float mainOffset; + + switch (justifyContent) + { + case JustifyContent::flexStart: + mainOffset = 0.0f; + break; + case JustifyContent::flexEnd: + mainOffset = extraSpace; + break; + case JustifyContent::center: + mainOffset = extraSpace / 2.0f; + break; + case JustifyContent::spaceBetween: + mainOffset = 0.0f; + break; + case JustifyContent::spaceAround: + mainOffset = extraSpace / (float) (line.items.size() + 1); + break; + } + + float currentMainPos = containerMainStart + mainOffset; + int gapCount = 0; + + for (auto* item : line.items) + { + float itemMainSize = resolveItemMainSize (*item, isRow, containerMainSize); + float itemCrossSize = resolveItemCrossSize (*item, isRow, containerCrossSize); + const float resolvedItemCrossSize = itemCrossSize; + float mainMarginStart = isRow ? item->marginLeft : item->marginTop; + float mainMarginEnd = isRow ? item->marginRight : item->marginBottom; + float crossMarginStart = isRow ? item->marginTop : item->marginLeft; + float crossMarginEnd = isRow ? item->marginBottom : item->marginRight; + + // Apply flex-grow when there is extra space, or flex-shrink when + // the line overflows. The shrink deficit is distributed across the + // shrinkable items weighted by flexShrink * base main size. + if (extraSpace > 0.0f && totalFlexGrow > 0.0f && item->flexGrow > 0.0f) + itemMainSize += extraSpace * item->flexGrow / totalFlexGrow; + else if (extraSpace < 0.0f && totalShrinkWeight > 0.0f && item->flexShrink > 0.0f) + itemMainSize -= (-extraSpace) * item->flexShrink * itemMainSize / totalShrinkWeight; + + // Apply min/max constraints on the main axis + float constraintMinMainSize = isRow ? item->minWidth : item->minHeight; + float constraintMaxMainSize = isRow ? item->maxWidth : item->maxHeight; + + if (constraintMinMainSize >= 0) + itemMainSize = std::max (itemMainSize, constraintMinMainSize); + if (constraintMaxMainSize >= 0) + itemMainSize = std::min (itemMainSize, constraintMaxMainSize); + + // Resolve alignment on the cross axis + FlexItem::AlignSelf align = resolveItemAlign (*item, alignItems); + + // Apply min/max constraints on the cross axis for every alignment + float constraintMinCrossSize = isRow ? item->minHeight : item->minWidth; + float constraintMaxCrossSize = isRow ? item->maxHeight : item->maxWidth; + + if (align == FlexItem::AlignSelf::stretch) + itemCrossSize = lineCrossSize - crossMarginStart - crossMarginEnd; + + if (constraintMinCrossSize >= 0) + itemCrossSize = std::max (itemCrossSize, constraintMinCrossSize); + if (constraintMaxCrossSize >= 0) + itemCrossSize = std::min (itemCrossSize, constraintMaxCrossSize); + + float itemCrossPos; + + switch (align) + { + case FlexItem::AlignSelf::stretch: + case FlexItem::AlignSelf::flexStart: + itemCrossPos = currentCrossPos + crossMarginStart; + break; + case FlexItem::AlignSelf::flexEnd: + itemCrossPos = currentCrossPos + lineCrossSize - itemCrossSize - crossMarginEnd; + break; + case FlexItem::AlignSelf::baseline: + { + float itemBaseline = item->baseline >= 0.0f ? item->baseline : resolvedItemCrossSize; + itemCrossPos = currentCrossPos + lineBaseline - crossMarginStart - itemBaseline; + break; + } + case FlexItem::AlignSelf::center: + default: + itemCrossPos = currentCrossPos + (lineCrossSize - itemCrossSize) / 2.0f; + break; + } + + // Apply spacing + if (justifyContent == JustifyContent::spaceBetween && line.items.size() > 1 && gapCount > 0) + currentMainPos += extraSpace / (float) (line.items.size() - 1); + else if (justifyContent == JustifyContent::spaceAround && gapCount > 0) + currentMainPos += mainOffset; + + currentMainPos += mainMarginStart; + + // Set bounds + if (item->associatedComponent != nullptr) + { + float x, y, w, h; + + if (isRow) + { + x = currentMainPos; + y = itemCrossPos; + w = itemMainSize; + h = itemCrossSize; + } + else + { + x = itemCrossPos; + y = currentMainPos; + w = itemCrossSize; + h = itemMainSize; + } + + if (isReverse) + { + if (isRow) + x = targetArea.getRight() - (x - targetArea.getX()) - w; + else + y = targetArea.getBottom() - (y - targetArea.getY()) - h; + } + + item->associatedComponent->setBounds (Rectangle (x, y, w, h).toNearestInt()); + } + + currentMainPos += itemMainSize + mainMarginEnd; + + if (gapCount < line.items.size() - 1) + currentMainPos += gap; + + ++gapCount; + } + + currentCrossPos += lineCrossSize + gap; + } +} + +void FlexBox::performLayout (Rectangle targetArea) +{ + performLayout (targetArea.to()); +} + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_FlexBox.h b/modules/yup_gui/layout/yup_FlexBox.h new file mode 100644 index 000000000..69f79755e --- /dev/null +++ b/modules/yup_gui/layout/yup_FlexBox.h @@ -0,0 +1,161 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A CSS-flexbox-style layout container for arranging components. + + FlexBox provides a flexible layout system for positioning child components + within a given area. It follows the CSS Flexbox layout algorithm, supporting + row/column direction, wrapping, alignment, and flexible sizing via FlexItem. + + Components can be added directly (implicitly converted to FlexItem), or as + FlexItem objects with specific layout properties. + + Usage: + @code + FlexBox fb; + fb.flexDirection = FlexBox::Direction::row; + fb.justifyContent = FlexBox::JustifyContent::spaceBetween; + fb.items.add (component1.withFlex (1)); + fb.items.add (component2.withFlex (2)); + fb.performLayout (getLocalBounds()); + @endcode + + @see FlexItem + + @tags{GUI} +*/ +class YUP_API FlexBox +{ +public: + //============================================================================== + /** Direction of the flex layout. */ + enum class Direction + { + row, /**< Left to right */ + rowReverse, /**< Right to left */ + column, /**< Top to bottom */ + columnReverse /**< Bottom to top */ + }; + + /** Wrapping behavior for items that overflow. */ + enum class Wrap + { + noWrap, /**< All items on one line */ + wrap, /**< Wrap to next line */ + wrapReverse /**< Wrap to next line in reverse */ + }; + + /** Alignment of items along the main axis. */ + enum class JustifyContent + { + flexStart, /**< Pack at start */ + flexEnd, /**< Pack at end */ + center, /**< Pack centered */ + spaceBetween, /**< Even spacing between items */ + spaceAround /**< Even spacing around items */ + }; + + /** Alignment of items along the cross axis. */ + enum class AlignItems + { + flexStart, /**< Align to start */ + flexEnd, /**< Align to end */ + center, /**< Center */ + stretch, /**< Stretch to fill */ + baseline /**< Align by the items' baselines */ + }; + + /** Alignment of lines when there is extra space on the cross axis. */ + enum class AlignContent + { + flexStart, /**< Pack at start */ + flexEnd, /**< Pack at end */ + center, /**< Pack centered */ + spaceBetween, /**< Even spacing between lines */ + spaceAround, /**< Even spacing around lines */ + stretch /**< Stretch lines to fill */ + }; + + //============================================================================== + FlexBox() = default; + + /** Creates a FlexBox with a direction. */ + explicit FlexBox (Direction direction); + + /** Creates a FlexBox with a direction, wrap and alignment settings. */ + FlexBox (Direction direction, Wrap wrap, AlignItems alignItems, JustifyContent justifyContent, AlignContent alignContent); + + //============================================================================== + /** The flex direction. Default is row. */ + Direction flexDirection = Direction::row; + + /** The wrap mode. Default is no wrap. */ + Wrap flexWrap = Wrap::noWrap; + + /** How items are aligned on the cross axis. Default is stretch. */ + AlignItems alignItems = AlignItems::stretch; + + /** How items are justified on the main axis. Default is flex-start. */ + JustifyContent justifyContent = JustifyContent::flexStart; + + /** How wrapped lines are aligned on the cross axis. Default is stretch. */ + AlignContent alignContent = AlignContent::stretch; + + /** The gap between items on the main axis. */ + float gap = 0.0f; + + //============================================================================== + /** The items to be laid out. */ + Array items; + + //============================================================================== + /** + Performs the flexbox layout, positioning child components within the + given rectangle. + + @param targetArea the area in which to lay out the items. + */ + void performLayout (Rectangle targetArea); + + /** + Performs the flexbox layout using an integer rectangle. + @param targetArea the area in which to lay out the items. + */ + void performLayout (Rectangle targetArea); + +private: + //============================================================================== + struct LineInfo + { + Array items; + float totalMainSize; + float crossSize; + }; + + void calculateLayout (Rectangle targetArea, Array& lines); +}; + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_FlexItem.cpp b/modules/yup_gui/layout/yup_FlexItem.cpp new file mode 100644 index 000000000..d029cd66b --- /dev/null +++ b/modules/yup_gui/layout/yup_FlexItem.cpp @@ -0,0 +1,150 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +FlexItem::FlexItem (Component& c) + : associatedComponent (&c) +{ +} + +FlexItem::FlexItem (Component* c) + : associatedComponent (c) +{ +} + +FlexItem::FlexItem (float w, float h) + : width (w) + , height (h) +{ +} + +FlexItem::FlexItem (Component& c, float w, float h) + : associatedComponent (&c) + , width (w) + , height (h) +{ +} + +FlexItem::FlexItem (Component* c, float w, float h) + : associatedComponent (c) + , width (w) + , height (h) +{ +} + +FlexItem FlexItem::withFlex (float newFlexGrow) const +{ + auto copy = *this; + copy.flexGrow = newFlexGrow; + return copy; +} + +FlexItem FlexItem::withWidth (float newWidth) const +{ + auto copy = *this; + copy.width = newWidth; + return copy; +} + +FlexItem FlexItem::withHeight (float newHeight) const +{ + auto copy = *this; + copy.height = newHeight; + return copy; +} + +FlexItem FlexItem::withWidthPercent (float newWidthPercent) const +{ + auto copy = *this; + copy.widthPercent = newWidthPercent; + return copy; +} + +FlexItem FlexItem::withHeightPercent (float newHeightPercent) const +{ + auto copy = *this; + copy.heightPercent = newHeightPercent; + return copy; +} + +FlexItem FlexItem::withMinWidth (float newMinWidth) const +{ + auto copy = *this; + copy.minWidth = newMinWidth; + return copy; +} + +FlexItem FlexItem::withMinHeight (float newMinHeight) const +{ + auto copy = *this; + copy.minHeight = newMinHeight; + return copy; +} + +FlexItem FlexItem::withMaxWidth (float newMaxWidth) const +{ + auto copy = *this; + copy.maxWidth = newMaxWidth; + return copy; +} + +FlexItem FlexItem::withMaxHeight (float newMaxHeight) const +{ + auto copy = *this; + copy.maxHeight = newMaxHeight; + return copy; +} + +FlexItem FlexItem::withMargin (float newMargin) const +{ + auto copy = *this; + copy.marginLeft = newMargin; + copy.marginRight = newMargin; + copy.marginTop = newMargin; + copy.marginBottom = newMargin; + return copy; +} + +FlexItem FlexItem::withAlignSelf (AlignSelf newAlignSelf) const +{ + auto copy = *this; + copy.alignSelf = newAlignSelf; + return copy; +} + +FlexItem FlexItem::withBaseline (float newBaseline) const +{ + auto copy = *this; + copy.baseline = newBaseline; + return copy; +} + +FlexItem FlexItem::withOrder (int newOrder) const +{ + auto copy = *this; + copy.order = newOrder; + return copy; +} + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_FlexItem.h b/modules/yup_gui/layout/yup_FlexItem.h new file mode 100644 index 000000000..055ba3486 --- /dev/null +++ b/modules/yup_gui/layout/yup_FlexItem.h @@ -0,0 +1,173 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Describes the layout properties of a single item inside a FlexBox container. + + Each FlexItem wraps a Component and specifies how it should be sized and + positioned within the flex layout. Properties include flex-grow, flex-shrink, + min/max sizes, alignment, margins, and order. + + A Component* can be implicitly converted to a FlexItem, allowing components + to be added to a FlexBox directly. + + @see FlexBox + + @tags{GUI} +*/ +class YUP_API FlexItem +{ +public: + //============================================================================== + /** Creates a FlexItem with no associated component. */ + FlexItem() = default; + + /** Creates a FlexItem that controls the layout of the given component. */ + FlexItem (Component& component); + + /** Creates a FlexItem that controls the layout of the given component. */ + FlexItem (Component* component); + + /** Creates a FlexItem with a fixed width and height. */ + FlexItem (float width, float height); + + /** Creates a FlexItem with a fixed width and height, and the given component. */ + FlexItem (Component& component, float width, float height); + + /** Creates a FlexItem with a fixed width and height, and the given component. */ + FlexItem (Component* component, float width, float height); + + //============================================================================== + /** The component associated with this flex item, or nullptr. */ + Component* associatedComponent = nullptr; + + /** The flex-grow factor. Controls how this item grows relative to others. */ + float flexGrow = 0.0f; + + /** The flex-shrink factor. Controls how this item shrinks relative to others. */ + float flexShrink = 1.0f; + + /** The flex-basis value: the initial main size before growing/shrinking. */ + float flexBasis = 0.0f; + + //============================================================================== + /** Minimum width constraint. -1 means no constraint. */ + float minWidth = -1.0f; + + /** Minimum height constraint. -1 means no constraint. */ + float minHeight = -1.0f; + + /** Maximum width constraint. -1 means no constraint. */ + float maxWidth = -1.0f; + + /** Maximum height constraint. -1 means no constraint. */ + float maxHeight = -1.0f; + + //============================================================================== + /** The width of the item. A value of 0 means the component's current + width is used (auto/intrinsic sizing). */ + float width = 0.0f; + + /** The height of the item. A value of 0 means the component's current + height is used (auto/intrinsic sizing). */ + float height = 0.0f; + + /** The width of the item expressed as a percentage of the container's + width (row layouts) or height (column layouts). -1 means not set. + When set, it overrides the fixed width. */ + float widthPercent = -1.0f; + + /** The height of the item expressed as a percentage of the container's + height (row layouts) or width (column layouts). -1 means not set. + When set, it overrides the fixed height. */ + float heightPercent = -1.0f; + + //============================================================================== + /** Enumeration of possible alignment values for the cross-axis. */ + enum class AlignSelf + { + autoAlign, /**< Use the container's align-items value */ + flexStart, /**< Align to the start of the cross axis */ + flexEnd, /**< Align to the end of the cross axis */ + center, /**< Center along the cross axis */ + stretch, /**< Stretch to fill the cross axis */ + baseline /**< Align so the item's baseline lines up with the line's baseline */ + }; + + /** The alignment of this item on the cross axis. */ + AlignSelf alignSelf = AlignSelf::autoAlign; + + /** The baseline of this item, as an offset from its top edge. Only used + with AlignSelf::baseline. -1 means the item's own height is used. */ + float baseline = -1.0f; + + //============================================================================== + /** Margin values for the item (in pixels). */ + float marginLeft = 0.0f; + float marginRight = 0.0f; + float marginTop = 0.0f; + float marginBottom = 0.0f; + + /** Order value. Items with lower order are laid out first. */ + int order = 0; + + //============================================================================== + /** Returns a copy of this FlexItem with a different flex-grow. */ + FlexItem withFlex (float newFlexGrow) const; + + /** Returns a copy of this FlexItem with a different width. */ + FlexItem withWidth (float newWidth) const; + + /** Returns a copy of this FlexItem with a different height. */ + FlexItem withHeight (float newHeight) const; + + /** Returns a copy of this FlexItem with a different width percentage. */ + FlexItem withWidthPercent (float newWidthPercent) const; + + /** Returns a copy of this FlexItem with a different height percentage. */ + FlexItem withHeightPercent (float newHeightPercent) const; + + /** Returns a copy of this FlexItem with different minimum dimensions. */ + FlexItem withMinWidth (float newMinWidth) const; + FlexItem withMinHeight (float newMinHeight) const; + + /** Returns a copy of this FlexItem with different maximum dimensions. */ + FlexItem withMaxWidth (float newMaxWidth) const; + FlexItem withMaxHeight (float newMaxHeight) const; + + /** Returns a copy of this FlexItem with different margins. */ + FlexItem withMargin (float newMargin) const; + + /** Returns a copy of this FlexItem with a different align-self value. */ + FlexItem withAlignSelf (AlignSelf newAlignSelf) const; + + /** Returns a copy of this FlexItem with a different baseline offset. */ + FlexItem withBaseline (float newBaseline) const; + + /** Returns a copy of this FlexItem with a different order. */ + FlexItem withOrder (int newOrder) const; +}; + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_Grid.cpp b/modules/yup_gui/layout/yup_Grid.cpp new file mode 100644 index 000000000..6fd0300cc --- /dev/null +++ b/modules/yup_gui/layout/yup_Grid.cpp @@ -0,0 +1,380 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +Grid::TrackInfo Grid::TrackInfo::px (float pixelSize) +{ + TrackInfo t; + t.pixelSize = pixelSize; + return t; +} + +Grid::TrackInfo Grid::TrackInfo::fr (float fraction) +{ + TrackInfo t; + t.fraction = fraction; + return t; +} + +Grid::TrackInfo Grid::TrackInfo::auto_() +{ + TrackInfo t; + t.isAuto = true; + return t; +} + +//============================================================================== +Array Grid::calculateTrackSizes (const Array& tracks, + float totalSize, + float defaultSize) +{ + Array sizes; + float usedSize = 0.0f; + float totalFr = 0.0f; + + if (tracks.isEmpty()) + { + // Auto-track mode: one column per item + return sizes; // will be handled by the caller + } + + sizes.resize (tracks.size()); + + // First pass: allocate fixed and count fr units + for (int i = 0; i < tracks.size(); ++i) + { + const auto& track = tracks.getReference (i); + + if (track.isAuto) + { + sizes.set (i, defaultSize); + usedSize += defaultSize; + } + else if (track.fraction > 0.0f) + { + totalFr += track.fraction; + sizes.set (i, 0.0f); + } + else + { + sizes.set (i, track.pixelSize); + usedSize += track.pixelSize; + } + } + + // Distribute remaining space by fr units + if (totalFr > 0.0f) + { + float remaining = std::max (0.0f, totalSize - usedSize); + + for (int i = 0; i < tracks.size(); ++i) + { + const auto& track = tracks.getReference (i); + + if (track.fraction > 0.0f) + { + sizes.set (i, remaining * track.fraction / totalFr); + } + } + } + + return sizes; +} + +//============================================================================== +namespace +{ + +// A resolved grid cell (position + span) used for auto-placement bookkeeping. +struct PlacedCell +{ + int row = 0; + int col = 0; + int rowSpan = 1; + int colSpan = 1; +}; + +bool overlapsPlacedCell (const PlacedCell& cell, int row, int col, int rowSpan, int colSpan) +{ + return row < cell.row + cell.rowSpan && row + rowSpan > cell.row + && col < cell.col + cell.colSpan && col + colSpan > cell.col; +} + +bool isCellFree (const Array& placed, int row, int col, int rowSpan, int colSpan) +{ + for (const auto& cell : placed) + { + if (overlapsPlacedCell (cell, row, col, rowSpan, colSpan)) + return false; + } + + return true; +} + +// Sparse auto-placement: scans from the cursor position onward, row by row, +// and places the item in the first free cell. When maxCol is >= 0 (an explicit +// column template exists) the scan wraps to the next row at that column count; +// otherwise implicit tracks grow without bound. +void findAutoPlacementCell (int& outRow, int& outCol, int& cursorRow, int& cursorCol, int rowSpan, int colSpan, int maxCol, const Array& placed) +{ + for (int row = cursorRow;; ++row) + { + const int startCol = (row == cursorRow) ? cursorCol : 0; + + if (maxCol >= 0) + { + for (int col = startCol; col < maxCol; ++col) + { + if (isCellFree (placed, row, col, rowSpan, colSpan)) + { + outRow = row; + outCol = col; + cursorRow = row; + cursorCol = col + colSpan; + return; + } + } + + // Row exhausted: advance the cursor to the next row. + cursorRow = row + 1; + cursorCol = 0; + } + else + { + for (int col = startCol;; ++col) + { + if (isCellFree (placed, row, col, rowSpan, colSpan)) + { + outRow = row; + outCol = col; + cursorRow = row; + cursorCol = col + colSpan; + return; + } + } + } + } +} + +} // namespace + +//============================================================================== +void Grid::performLayout (Rectangle targetArea) +{ + if (items.isEmpty()) + return; + + // Column track sizes: from template definitions, or grown implicitly. + Array columnWidths; + + if (! templateColumns.isEmpty()) + columnWidths = calculateTrackSizes (templateColumns, targetArea.getWidth(), autoColumns); + + // Row track sizes: from template definitions, or grown implicitly. + Array rowHeights; + + if (! templateRows.isEmpty()) + rowHeights = calculateTrackSizes (templateRows, targetArea.getHeight(), autoRows); + + // Resolve each item's cell (explicit or auto-placed) and grow the implicit + // tracks so every item's span is covered. + const int maxAutoColumn = templateColumns.isEmpty() ? -1 : templateColumns.size(); + + Array placed; + placed.ensureStorageAllocated (items.size()); + + int cursorRow = 0; + int cursorCol = 0; + + for (const auto& item : items) + { + int col = item.column; + int row = item.row; + + // Defensively cap spans so a pathological value cannot make the + // implicit-track growth allocate without bound. + const int colSpan = std::clamp (item.columnSpan, 1, 10000); + const int rowSpan = std::clamp (item.rowSpan, 1, 10000); + + if (col < 0 || row < 0) + findAutoPlacementCell (row, col, cursorRow, cursorCol, rowSpan, colSpan, maxAutoColumn, placed); + + while (col + colSpan > columnWidths.size()) + columnWidths.add (autoColumns); + + while (row + rowSpan > rowHeights.size()) + rowHeights.add (autoRows); + + placed.add ({ row, col, rowSpan, colSpan }); + } + + // Calculate cell positions + Array columnPositions; + float currentX = targetArea.getX(); + + for (auto width : columnWidths) + { + columnPositions.add (currentX); + currentX += width + columnGap; + } + + Array rowPositions; + float currentY = targetArea.getY(); + + for (auto height : rowHeights) + { + rowPositions.add (currentY); + currentY += height + rowGap; + } + + // Position each item + for (int i = 0; i < items.size(); ++i) + { + const auto& item = items.getReference (i); + + if (item.associatedComponent == nullptr) + continue; + + const auto& cell = placed.getReference (i); + const int col = cell.col; + const int row = cell.row; + const int colSpan = cell.colSpan; + const int rowSpan = cell.rowSpan; + + // Calculate cell bounds + float cellX = columnPositions.getUnchecked (col); + float cellY = rowPositions.getUnchecked (row); + float cellW = columnWidths.getUnchecked (col); + float cellH = rowHeights.getUnchecked (row); + + if (col + colSpan <= columnPositions.size()) + { + float endX = columnPositions.getUnchecked (col + colSpan - 1) + columnWidths.getUnchecked (col + colSpan - 1); + cellW = endX - cellX; + } + + if (row + rowSpan <= rowPositions.size()) + { + float endY = rowPositions.getUnchecked (row + rowSpan - 1) + rowHeights.getUnchecked (row + rowSpan - 1); + cellH = endY - cellY; + } + + // Apply margins + cellX += item.marginLeft; + cellY += item.marginTop; + cellW -= item.marginLeft + item.marginRight; + cellH -= item.marginTop + item.marginBottom; + + // Apply alignment + GridItem::AlignSelf hAlign = item.justifySelf; + GridItem::AlignSelf vAlign = item.alignSelf; + + if (hAlign == GridItem::AlignSelf::autoAlign) + { + switch (justifyItems) + { + case AlignItems::flexStart: + hAlign = GridItem::AlignSelf::flexStart; + break; + case AlignItems::flexEnd: + hAlign = GridItem::AlignSelf::flexEnd; + break; + case AlignItems::center: + hAlign = GridItem::AlignSelf::center; + break; + case AlignItems::stretch: + hAlign = GridItem::AlignSelf::stretch; + break; + } + } + + if (vAlign == GridItem::AlignSelf::autoAlign) + { + switch (alignItems) + { + case AlignItems::flexStart: + vAlign = GridItem::AlignSelf::flexStart; + break; + case AlignItems::flexEnd: + vAlign = GridItem::AlignSelf::flexEnd; + break; + case AlignItems::center: + vAlign = GridItem::AlignSelf::center; + break; + case AlignItems::stretch: + vAlign = GridItem::AlignSelf::stretch; + break; + } + } + + // Resolve the item size: percentage > explicit size > fill the cell. + float itemW = cellW; + + if (item.widthPercent >= 0.0f) + itemW = cellW * item.widthPercent / 100.0f; + else if (item.width >= 0.0f) + itemW = item.width; + + float itemH = cellH; + + if (item.heightPercent >= 0.0f) + itemH = cellH * item.heightPercent / 100.0f; + else if (item.height >= 0.0f) + itemH = item.height; + + // Apply min/max constraints + if (item.minWidth >= 0.0f) + itemW = std::max (itemW, item.minWidth); + if (item.maxWidth >= 0.0f) + itemW = std::min (itemW, item.maxWidth); + if (item.minHeight >= 0.0f) + itemH = std::max (itemH, item.minHeight); + if (item.maxHeight >= 0.0f) + itemH = std::min (itemH, item.maxHeight); + + float itemX = cellX; + float itemY = cellY; + + // Apply horizontal alignment + if (hAlign == GridItem::AlignSelf::center) + itemX = cellX + (cellW - itemW) / 2.0f; + else if (hAlign == GridItem::AlignSelf::flexEnd) + itemX = cellX + cellW - itemW; + + // Apply vertical alignment + if (vAlign == GridItem::AlignSelf::center) + itemY = cellY + (cellH - itemH) / 2.0f; + else if (vAlign == GridItem::AlignSelf::flexEnd) + itemY = cellY + cellH - itemH; + + item.associatedComponent->setBounds (Rectangle (itemX, itemY, itemW, itemH).toNearestInt()); + } +} + +void Grid::performLayout (Rectangle targetArea) +{ + performLayout (targetArea.to()); +} + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_Grid.h b/modules/yup_gui/layout/yup_Grid.h new file mode 100644 index 000000000..479aa0a67 --- /dev/null +++ b/modules/yup_gui/layout/yup_Grid.h @@ -0,0 +1,141 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + A CSS-grid-style layout container for arranging components. + + Grid provides a two-dimensional layout system based on rows and columns. + Items are placed using explicit row/column positions with spans, similar + to CSS Grid Layout. Items whose column/row are left unset (-1) are placed + automatically, flowing row by row into the first free cell; implicit + tracks are created as needed to fit every item. + + Usage: + @code + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::fr (1)); + grid.templateColumns.add (Grid::TrackInfo::fr (2)); + grid.templateRows.add (Grid::TrackInfo::px (50)); + grid.templateRows.add (Grid::TrackInfo::fr (1)); + grid.items.add (component1.withColumn (0).withRow (0)); + grid.items.add (component2.withColumn (1).withRow (0).withRowSpan (2)); + grid.performLayout (getLocalBounds()); + @endcode + + @see GridItem + + @tags{GUI} +*/ +class YUP_API Grid +{ +public: + //============================================================================== + /** Represents a grid track (row or column) sizing specification. */ + struct YUP_API TrackInfo + { + /** Creates a track with a fixed pixel size. */ + static TrackInfo px (float pixelSize); + + /** Creates a track with a fractional size (fr unit). */ + static TrackInfo fr (float fraction); + + /** Creates a track with auto sizing. */ + static TrackInfo auto_(); + + /** The pixel size for fixed tracks. */ + float pixelSize = 0.0f; + + /** The fractional size (fr units). */ + float fraction = 0.0f; + + /** Whether this track uses auto-sizing. */ + bool isAuto = false; + + private: + TrackInfo() = default; + }; + + //============================================================================== + /** Alignment of items within cells. */ + enum class AlignItems + { + flexStart, + flexEnd, + center, + stretch + }; + + //============================================================================== + Grid() = default; + + //============================================================================== + /** Column track definitions. */ + Array templateColumns; + + /** Row track definitions. */ + Array templateRows; + + /** Auto-generated row height (when templateRows is empty). */ + float autoRows = 40.0f; + + /** Auto-generated column width (when templateColumns is empty). */ + float autoColumns = 100.0f; + + /** Gap between columns. */ + float columnGap = 0.0f; + + /** Gap between rows. */ + float rowGap = 0.0f; + + /** Default horizontal alignment for items. */ + AlignItems justifyItems = AlignItems::stretch; + + /** Default vertical alignment for items. */ + AlignItems alignItems = AlignItems::stretch; + + //============================================================================== + /** The items to be laid out. */ + Array items; + + //============================================================================== + /** + Performs the grid layout, positioning child components within the + given rectangle. + + @param targetArea the area in which to lay out the items. + */ + void performLayout (Rectangle targetArea); + + /** + Performs the grid layout using an integer rectangle. + */ + void performLayout (Rectangle targetArea); + +private: + //============================================================================== + Array calculateTrackSizes (const Array& tracks, float totalSize, float defaultSize); +}; + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_GridItem.cpp b/modules/yup_gui/layout/yup_GridItem.cpp new file mode 100644 index 000000000..32f1a08d2 --- /dev/null +++ b/modules/yup_gui/layout/yup_GridItem.cpp @@ -0,0 +1,143 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +GridItem::GridItem (Component& c) + : associatedComponent (&c) +{ +} + +GridItem::GridItem (Component* c) + : associatedComponent (c) +{ +} + +GridItem GridItem::withColumn (int newColumn) const +{ + auto copy = *this; + copy.column = newColumn; + return copy; +} + +GridItem GridItem::withRow (int newRow) const +{ + auto copy = *this; + copy.row = newRow; + return copy; +} + +GridItem GridItem::withColumnSpan (int newSpan) const +{ + auto copy = *this; + copy.columnSpan = newSpan; + return copy; +} + +GridItem GridItem::withRowSpan (int newSpan) const +{ + auto copy = *this; + copy.rowSpan = newSpan; + return copy; +} + +GridItem GridItem::withMargin (float newMargin) const +{ + auto copy = *this; + copy.marginLeft = newMargin; + copy.marginRight = newMargin; + copy.marginTop = newMargin; + copy.marginBottom = newMargin; + return copy; +} + +GridItem GridItem::withWidth (float newWidth) const +{ + auto copy = *this; + copy.width = newWidth; + return copy; +} + +GridItem GridItem::withHeight (float newHeight) const +{ + auto copy = *this; + copy.height = newHeight; + return copy; +} + +GridItem GridItem::withWidthPercent (float newWidthPercent) const +{ + auto copy = *this; + copy.widthPercent = newWidthPercent; + return copy; +} + +GridItem GridItem::withHeightPercent (float newHeightPercent) const +{ + auto copy = *this; + copy.heightPercent = newHeightPercent; + return copy; +} + +GridItem GridItem::withMinWidth (float newMinWidth) const +{ + auto copy = *this; + copy.minWidth = newMinWidth; + return copy; +} + +GridItem GridItem::withMinHeight (float newMinHeight) const +{ + auto copy = *this; + copy.minHeight = newMinHeight; + return copy; +} + +GridItem GridItem::withMaxWidth (float newMaxWidth) const +{ + auto copy = *this; + copy.maxWidth = newMaxWidth; + return copy; +} + +GridItem GridItem::withMaxHeight (float newMaxHeight) const +{ + auto copy = *this; + copy.maxHeight = newMaxHeight; + return copy; +} + +GridItem GridItem::withJustifySelf (AlignSelf newJustifySelf) const +{ + auto copy = *this; + copy.justifySelf = newJustifySelf; + return copy; +} + +GridItem GridItem::withAlignSelf (AlignSelf newAlignSelf) const +{ + auto copy = *this; + copy.alignSelf = newAlignSelf; + return copy; +} + +} // namespace yup diff --git a/modules/yup_gui/layout/yup_GridItem.h b/modules/yup_gui/layout/yup_GridItem.h new file mode 100644 index 000000000..cd7433230 --- /dev/null +++ b/modules/yup_gui/layout/yup_GridItem.h @@ -0,0 +1,138 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace yup +{ + +//============================================================================== +/** + Describes the layout properties of a single item inside a Grid container. + + Each GridItem wraps a Component and specifies its placement within the grid + using row/column positions, spans, and alignment properties. + + A Component* can be implicitly converted to a GridItem. + + @see Grid + + @tags{GUI} +*/ +class YUP_API GridItem +{ +public: + //============================================================================== + /** Creates a GridItem with no associated component. */ + GridItem() = default; + + /** Creates a GridItem that controls the layout of the given component. */ + GridItem (Component& component); + + /** Creates a GridItem that controls the layout of the given component. */ + GridItem (Component* component); + + //============================================================================== + /** The component associated with this grid item, or nullptr. */ + Component* associatedComponent = nullptr; + + //============================================================================== + /** The column position (0-based). -1 places the item automatically. */ + int column = -1; + + /** The row position (0-based). -1 places the item automatically. */ + int row = -1; + + /** Number of columns this item spans. */ + int columnSpan = 1; + + /** Number of rows this item spans. */ + int rowSpan = 1; + + //============================================================================== + /** The width of the item. -1 means the cell's width is used (fill). */ + float width = -1.0f; + + /** The height of the item. -1 means the cell's height is used (fill). */ + float height = -1.0f; + + /** The width of the item expressed as a percentage of the cell's width. + -1 means not set. When set, it overrides the fixed width. */ + float widthPercent = -1.0f; + + /** The height of the item expressed as a percentage of the cell's height. + -1 means not set. When set, it overrides the fixed height. */ + float heightPercent = -1.0f; + + /** Minimum width constraint. -1 means no constraint. */ + float minWidth = -1.0f; + + /** Minimum height constraint. -1 means no constraint. */ + float minHeight = -1.0f; + + /** Maximum width constraint. -1 means no constraint. */ + float maxWidth = -1.0f; + + /** Maximum height constraint. -1 means no constraint. */ + float maxHeight = -1.0f; + + //============================================================================== + /** Enumeration of alignment values. */ + enum class AlignSelf + { + autoAlign, /**< Use the container's default */ + flexStart, /**< Align to start */ + flexEnd, /**< Align to end */ + center, /**< Center */ + stretch /**< Stretch to fill */ + }; + + /** Horizontal alignment within the cell. */ + AlignSelf justifySelf = AlignSelf::autoAlign; + + /** Vertical alignment within the cell. */ + AlignSelf alignSelf = AlignSelf::autoAlign; + + //============================================================================== + /** Margin values for the item (in pixels). */ + float marginLeft = 0.0f; + float marginRight = 0.0f; + float marginTop = 0.0f; + float marginBottom = 0.0f; + + //============================================================================== + /** Returns a copy of this GridItem with the given column/row. */ + GridItem withColumn (int newColumn) const; + GridItem withRow (int newRow) const; + GridItem withColumnSpan (int newSpan) const; + GridItem withRowSpan (int newSpan) const; + GridItem withMargin (float newMargin) const; + GridItem withWidth (float newWidth) const; + GridItem withHeight (float newHeight) const; + GridItem withWidthPercent (float newWidthPercent) const; + GridItem withHeightPercent (float newHeightPercent) const; + GridItem withMinWidth (float newMinWidth) const; + GridItem withMinHeight (float newMinHeight) const; + GridItem withMaxWidth (float newMaxWidth) const; + GridItem withMaxHeight (float newMaxHeight) const; + GridItem withJustifySelf (AlignSelf newJustifySelf) const; + GridItem withAlignSelf (AlignSelf newAlignSelf) const; +}; + +} // namespace yup diff --git a/modules/yup_gui/yup_gui.cpp b/modules/yup_gui/yup_gui.cpp index da1261bc2..ac67dde5a 100644 --- a/modules/yup_gui/yup_gui.cpp +++ b/modules/yup_gui/yup_gui.cpp @@ -160,6 +160,10 @@ #include "artboard/yup_Artboard.cpp" #include "windowing/yup_DocumentWindow.cpp" #include "dialogs/yup_FileChooser.cpp" +#include "layout/yup_FlexItem.cpp" +#include "layout/yup_FlexBox.cpp" +#include "layout/yup_GridItem.cpp" +#include "layout/yup_Grid.cpp" #include "themes/yup_ApplicationTheme.cpp" //============================================================================== diff --git a/modules/yup_gui/yup_gui.h b/modules/yup_gui/yup_gui.h index 0db93dcdd..723ca53b2 100644 --- a/modules/yup_gui/yup_gui.h +++ b/modules/yup_gui/yup_gui.h @@ -162,6 +162,13 @@ //============================================================================== +#include "layout/yup_FlexItem.h" +#include "layout/yup_FlexBox.h" +#include "layout/yup_GridItem.h" +#include "layout/yup_Grid.h" + +//============================================================================== + #include "profiling/yup_PaintProfiler.h" //============================================================================== diff --git a/tests/yup_gui.cpp b/tests/yup_gui.cpp index 43fb36ada..c94e0922e 100644 --- a/tests/yup_gui.cpp +++ b/tests/yup_gui.cpp @@ -31,6 +31,8 @@ #include "yup_gui/yup_Desktop.cpp" #include "yup_gui/yup_DragAndDropData.cpp" #include "yup_gui/yup_FileChooser.cpp" +#include "yup_gui/yup_FlexBox.cpp" +#include "yup_gui/yup_Grid.cpp" #include "yup_gui/yup_KeyModifiers.cpp" #include "yup_gui/yup_KeyPress.cpp" #include "yup_gui/yup_Label.cpp" diff --git a/tests/yup_gui/yup_FlexBox.cpp b/tests/yup_gui/yup_FlexBox.cpp new file mode 100644 index 000000000..89b222fb3 --- /dev/null +++ b/tests/yup_gui/yup_FlexBox.cpp @@ -0,0 +1,764 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +void expectFlexBounds (const Component& component, float x, float y, float width, float height) +{ + auto bounds = component.getBounds(); + EXPECT_FLOAT_EQ (x, bounds.getX()); + EXPECT_FLOAT_EQ (y, bounds.getY()); + EXPECT_FLOAT_EQ (width, bounds.getWidth()); + EXPECT_FLOAT_EQ (height, bounds.getHeight()); +} + +} // namespace + +// ============================================================================= +// Direction +// ============================================================================= + +TEST (FlexBoxTests, RowPlacesItemsLeftToRight) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); +} + +TEST (FlexBoxTests, ColumnPlacesItemsTopToBottom) +{ + Component c1, c2; + + FlexBox box; + box.flexDirection = FlexBox::Direction::column; + box.items.add (FlexItem (c1, 50, 100)); + box.items.add (FlexItem (c2, 50, 100)); + box.performLayout (Rectangle (0, 0, 100, 300)); + + expectFlexBounds (c1, 0, 0, 50, 100); + expectFlexBounds (c2, 0, 100, 50, 100); +} + +TEST (FlexBoxTests, RowReverseMirrorsItemsOnTheMainAxis) +{ + Component c1, c2; + + FlexBox box; + box.flexDirection = FlexBox::Direction::rowReverse; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 200, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); +} + +TEST (FlexBoxTests, ColumnReverseMirrorsItemsOnTheMainAxis) +{ + Component c1, c2; + + FlexBox box; + box.flexDirection = FlexBox::Direction::columnReverse; + box.items.add (FlexItem (c1, 50, 100)); + box.items.add (FlexItem (c2, 50, 100)); + box.performLayout (Rectangle (0, 0, 100, 300)); + + expectFlexBounds (c1, 0, 200, 50, 100); + expectFlexBounds (c2, 0, 100, 50, 100); +} + +// ============================================================================= +// justify-content +// ============================================================================= + +TEST (FlexBoxTests, JustifyContentFlexEndPacksItemsAtTheEnd) +{ + Component c1, c2; + + FlexBox box; + box.justifyContent = FlexBox::JustifyContent::flexEnd; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 100, 0, 100, 50); + expectFlexBounds (c2, 200, 0, 100, 50); +} + +TEST (FlexBoxTests, JustifyContentCenterCentersItemsOnTheMainAxis) +{ + Component c1, c2; + + FlexBox box; + box.justifyContent = FlexBox::JustifyContent::center; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 50, 0, 100, 50); + expectFlexBounds (c2, 150, 0, 100, 50); +} + +TEST (FlexBoxTests, JustifyContentSpaceBetweenDistributesSpaceBetweenItems) +{ + Component c1, c2; + + FlexBox box; + box.justifyContent = FlexBox::JustifyContent::spaceBetween; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 200, 0, 100, 50); +} + +TEST (FlexBoxTests, JustifyContentSpaceBetweenWithThreeItems) +{ + Component c1, c2, c3; + + FlexBox box; + box.justifyContent = FlexBox::JustifyContent::spaceBetween; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 400, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 150, 0, 100, 50); + expectFlexBounds (c3, 300, 0, 100, 50); +} + +TEST (FlexBoxTests, JustifyContentSpaceAroundDistributesSpaceAroundItems) +{ + Component c1, c2; + + FlexBox box; + box.justifyContent = FlexBox::JustifyContent::spaceAround; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // extra = 100, mainOffset = 100 / 3 per gap + expectFlexBounds (c1, 33, 0, 100, 50); + expectFlexBounds (c2, 167, 0, 100, 50); +} + +// ============================================================================= +// align-items / align-self +// ============================================================================= + +TEST (FlexBoxTests, AlignItemsFlexStartAlignsItemsToTheCrossStart) +{ + Component c1, c2; + + FlexBox box; + box.alignItems = FlexBox::AlignItems::flexStart; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 80)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 80); +} + +TEST (FlexBoxTests, AlignItemsFlexEndAlignsItemsToTheCrossEnd) +{ + Component c1, c2; + + FlexBox box; + box.alignItems = FlexBox::AlignItems::flexEnd; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 80)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 30, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 80); +} + +TEST (FlexBoxTests, AlignItemsCenterCentersItemsOnTheCrossAxis) +{ + Component c1, c2; + + FlexBox box; + box.alignItems = FlexBox::AlignItems::center; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 80)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 15, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 80); +} + +TEST (FlexBoxTests, AlignSelfOverridesTheContainerAlignment) +{ + Component c1, c2; + + FlexBox box; + box.alignItems = FlexBox::AlignItems::center; + box.items.add (FlexItem (c1, 100, 80).withAlignSelf (FlexItem::AlignSelf::flexStart)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 100, 80); + expectFlexBounds (c2, 100, 15, 100, 50); +} + +// ============================================================================= +// flex-grow +// ============================================================================= + +TEST (FlexBoxTests, FlexGrowDistributesExtraSpaceEqually) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 50, 50).withFlex (1)); + box.items.add (FlexItem (c2, 50, 50).withFlex (1)); + box.performLayout (Rectangle (0, 0, 200, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); +} + +TEST (FlexBoxTests, FlexGrowDistributesExtraSpaceProportionally) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 50, 50).withFlex (1)); + box.items.add (FlexItem (c2, 50, 50).withFlex (2)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 117, 50); // 50 + 200 / 3 + expectFlexBounds (c2, 117, 0, 183, 50); // 50 + 400 / 3 +} + +TEST (FlexBoxTests, FlexGrowRespectsMaxWidth) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 50, 50).withFlex (1).withMaxWidth (60)); + box.items.add (FlexItem (c2, 50, 50).withFlex (1)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // Both would grow to 150, but c1 clamps at its 60 maximum + expectFlexBounds (c1, 0, 0, 60, 50); + expectFlexBounds (c2, 60, 0, 150, 50); +} + +TEST (FlexBoxTests, FlexGrowInColumnDirectionGrowsTheMainAxisHeight) +{ + Component c1, c2; + + FlexBox box; + box.flexDirection = FlexBox::Direction::column; + box.items.add (FlexItem (c1, 50, 50).withFlex (1)); + box.items.add (FlexItem (c2, 50, 50).withFlex (1)); + box.performLayout (Rectangle (0, 0, 100, 300)); + + expectFlexBounds (c1, 0, 0, 50, 150); + expectFlexBounds (c2, 0, 150, 50, 150); +} + +TEST (FlexBoxTests, IntrinsicSizeParticipatesInFlexGrow) +{ + Component c1, c2; + c1.setBounds (0, 0, 60, 50); + c2.setBounds (0, 0, 60, 50); + + FlexBox box; + box.items.add (FlexItem (c1).withFlex (1)); + box.items.add (FlexItem (c2).withFlex (1)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // Base sizes 60 each, extra 180 split equally + expectFlexBounds (c1, 0, 0, 150, 50); + expectFlexBounds (c2, 150, 0, 150, 50); +} + +// ============================================================================= +// flex-shrink +// ============================================================================= + +TEST (FlexBoxTests, FlexShrinkReducesSizesWhenLineOverflows) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 80, 50)); + box.items.add (FlexItem (c2, 80, 50)); + box.performLayout (Rectangle (0, 0, 100, 50)); + + // Deficit of 60 distributed equally (equal sizes and shrink factors) + expectFlexBounds (c1, 0, 0, 50, 50); + expectFlexBounds (c2, 50, 0, 50, 50); +} + +TEST (FlexBoxTests, FlexShrinkIsWeightedByShrinkFactorAndSize) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50).withAlignSelf (FlexItem::AlignSelf::stretch)); + box.items.add (FlexItem (c2, 100, 50).withAlignSelf (FlexItem::AlignSelf::stretch)); + + box.items.getReference (0).flexShrink = 1.0f; + box.items.getReference (1).flexShrink = 3.0f; + + box.performLayout (Rectangle (0, 0, 120, 50)); + + // Deficit of 80: c1 shrinks 80*100/400 = 20, c2 shrinks 80*300/400 = 60 + expectFlexBounds (c1, 0, 0, 80, 50); + expectFlexBounds (c2, 80, 0, 40, 50); +} + +TEST (FlexBoxTests, FlexShrinkRespectsMinWidth) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50).withMinWidth (70)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 120, 50)); + + // Without the clamp each item would shrink by 40 (deficit 80 split + // equally); c1 then clamps at its 70 minimum, leaving the residual + // overflow (simplified single-pass shrink). + expectFlexBounds (c1, 0, 0, 70, 50); + expectFlexBounds (c2, 70, 0, 60, 50); +} + +TEST (FlexBoxTests, FlexShrinkUsesFlexBasisAsTheBaseSize) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 200, 50)); + box.items.add (FlexItem (c2, 50, 50)); + box.items.getReference (0).flexBasis = 100.0f; + box.performLayout (Rectangle (0, 0, 100, 50)); + + // Base sizes 100 and 50, deficit 50: c1 shrinks 50*100/150, c2 50*50/150 + expectFlexBounds (c1, 0, 0, 67, 50); + expectFlexBounds (c2, 67, 0, 33, 50); +} + +TEST (FlexBoxTests, FlexShrinkInColumnDirectionShrinksTheMainAxisHeight) +{ + Component c1, c2; + + FlexBox box; + box.flexDirection = FlexBox::Direction::column; + box.items.add (FlexItem (c1, 50, 80)); + box.items.add (FlexItem (c2, 50, 80)); + box.performLayout (Rectangle (0, 0, 100, 120)); + + expectFlexBounds (c1, 0, 0, 50, 60); + expectFlexBounds (c2, 0, 60, 50, 60); +} + +// ============================================================================= +// flex-basis +// ============================================================================= + +TEST (FlexBoxTests, FlexBasisOverridesTheExplicitWidth) +{ + Component c1; + + FlexBox box; + box.items.add (FlexItem (c1, 50, 50)); + box.items.getReference (0).flexBasis = 120.0f; + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 120, 50); +} + +// ============================================================================= +// wrap / wrap-reverse +// ============================================================================= + +TEST (FlexBoxTests, WrapCreatesMultipleLines) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); + expectFlexBounds (c3, 0, 50, 100, 50); +} + +TEST (FlexBoxTests, WrapReverseReversesLineOrder) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrapReverse; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 100)); + + // The second line is laid out first, so the first line ends up at the bottom + expectFlexBounds (c1, 0, 50, 100, 50); + expectFlexBounds (c2, 100, 50, 100, 50); + expectFlexBounds (c3, 0, 0, 100, 50); +} + +TEST (FlexBoxTests, WrapTakesTheGapIntoAccount) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.gap = 50.0f; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + // 100 + gap 50 + 100 = 250 fits, the third item wraps to a new line + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 150, 0, 100, 50); + expectFlexBounds (c3, 0, 100, 100, 50); +} + +// ============================================================================= +// align-content +// ============================================================================= + +TEST (FlexBoxTests, AlignContentCenterCentersLinesOnTheCrossAxis) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.alignContent = FlexBox::AlignContent::center; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + // Two lines of 50 in a 150 tall container: offset (150-100)/2 = 25 + expectFlexBounds (c1, 0, 25, 100, 50); + expectFlexBounds (c2, 100, 25, 100, 50); + expectFlexBounds (c3, 0, 75, 100, 50); +} + +TEST (FlexBoxTests, AlignContentFlexEndPacksLinesAtTheCrossEnd) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.alignContent = FlexBox::AlignContent::flexEnd; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + expectFlexBounds (c1, 0, 50, 100, 50); + expectFlexBounds (c2, 100, 50, 100, 50); + expectFlexBounds (c3, 0, 100, 100, 50); +} + +TEST (FlexBoxTests, AlignContentSpaceBetweenDistributesLines) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.alignContent = FlexBox::AlignContent::spaceBetween; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); + expectFlexBounds (c3, 0, 100, 100, 50); +} + +TEST (FlexBoxTests, AlignContentSpaceAroundDistributesLines) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.alignContent = FlexBox::AlignContent::spaceAround; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + // offset (150-100)/3 per gap: first line at 16.67, second at 66.67 + expectFlexBounds (c1, 0, 17, 100, 50); + expectFlexBounds (c2, 100, 17, 100, 50); + expectFlexBounds (c3, 0, 67, 100, 50); +} + +TEST (FlexBoxTests, AlignContentStretchGrowsLinesToFillTheCrossAxis) +{ + Component c1, c2, c3; + + FlexBox box; + box.flexWrap = FlexBox::Wrap::wrap; + box.alignContent = FlexBox::AlignContent::stretch; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.items.add (FlexItem (c3, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 150)); + + // Each line grows by (150-100)/2 = 25 + expectFlexBounds (c1, 0, 0, 100, 75); + expectFlexBounds (c2, 100, 0, 100, 75); + expectFlexBounds (c3, 0, 75, 100, 75); +} + +// ============================================================================= +// min/max constraints +// ============================================================================= + +TEST (FlexBoxTests, MinMaxClampsApplyOnTheMainAxis) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 50, 50).withMinWidth (80)); + box.items.add (FlexItem (c2, 200, 50).withMaxWidth (120)); + box.performLayout (Rectangle (0, 0, 400, 100)); + + expectFlexBounds (c1, 0, 0, 80, 50); + expectFlexBounds (c2, 80, 0, 120, 50); +} + +TEST (FlexBoxTests, MinMaxClampsApplyForEveryAlignSelf) +{ + struct AlignSelfCase + { + FlexItem::AlignSelf align; + float expectedY; + }; + + const AlignSelfCase cases[] = { + { FlexItem::AlignSelf::flexStart, 0.0f }, + { FlexItem::AlignSelf::flexEnd, 10.0f }, + { FlexItem::AlignSelf::center, 5.0f }, + { FlexItem::AlignSelf::stretch, 0.0f }, + }; + + for (const auto& testCase : cases) + { + Component component; + + FlexBox box; + box.items.add (FlexItem (component, 100, 60) + .withMinWidth (120) + .withMaxWidth (140) + .withMinHeight (40) + .withMaxHeight (50) + .withAlignSelf (testCase.align)); + box.performLayout (Rectangle (0, 0, 200, 100)); + + // Main axis clamped from 100 to 120, cross axis clamped from 60 to 50 + expectFlexBounds (component, 0, testCase.expectedY, 120, 50); + } +} + +// ============================================================================= +// percentage sizing +// ============================================================================= + +TEST (FlexBoxTests, PercentageSizingResolvesAgainstContainer) +{ + Component c1; + + FlexBox box; + box.items.add (FlexItem (c1).withWidthPercent (50).withHeightPercent (25)); + box.performLayout (Rectangle (0, 0, 200, 100)); + + expectFlexBounds (c1, 0, 0, 100, 25); +} + +TEST (FlexBoxTests, PercentageSizingInColumnDirection) +{ + Component c1; + + FlexBox box; + box.flexDirection = FlexBox::Direction::column; + box.items.add (FlexItem (c1).withWidthPercent (50).withHeightPercent (50)); + box.performLayout (Rectangle (0, 0, 200, 100)); + + // widthPercent resolves against the container width (cross), heightPercent + // against the container height (main) + expectFlexBounds (c1, 0, 0, 100, 50); +} + +// ============================================================================= +// intrinsic / measured sizing +// ============================================================================= + +TEST (FlexBoxTests, UnspecifiedSizeUsesComponentCurrentBounds) +{ + Component c1; + c1.setBounds (0, 0, 80, 30); + + FlexBox box; + box.items.add (FlexItem (c1)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 80, 30); +} + +// ============================================================================= +// baseline alignment +// ============================================================================= + +TEST (FlexBoxTests, BaselineAlignsItemsByTheirBaseline) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50).withAlignSelf (FlexItem::AlignSelf::baseline).withBaseline (20)); + box.items.add (FlexItem (c2, 100, 60).withAlignSelf (FlexItem::AlignSelf::baseline).withBaseline (30)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // Both baselines land at y = 30 within the line + expectFlexBounds (c1, 0, 10, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 60); +} + +TEST (FlexBoxTests, ContainerBaselineAlignmentUsesItemHeightAsDefaultBaseline) +{ + Component c1, c2; + + FlexBox box; + box.alignItems = FlexBox::AlignItems::baseline; + box.items.add (FlexItem (c1, 100, 40)); + box.items.add (FlexItem (c2, 100, 60)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // Default baseline = item height: line baseline = 60, c1 top at 60 - 40 = 20 + expectFlexBounds (c1, 0, 20, 100, 40); + expectFlexBounds (c2, 100, 0, 100, 60); +} + +TEST (FlexBoxTests, BaselineAlignedItemsKeepTheirOwnSize) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 30).withAlignSelf (FlexItem::AlignSelf::baseline).withBaseline (25)); + box.items.add (FlexItem (c2, 100, 70).withAlignSelf (FlexItem::AlignSelf::baseline).withBaseline (40)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + // Line baseline = max(25, 40) = 40; c1 top = 40 - 25 = 15 + expectFlexBounds (c1, 0, 15, 100, 30); + expectFlexBounds (c2, 100, 0, 100, 70); +} + +// ============================================================================= +// margins / gap / order +// ============================================================================= + +TEST (FlexBoxTests, MarginsAreRespectedOnBothAxes) +{ + Component c1; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50).withMargin (10)); + box.performLayout (Rectangle (0, 0, 320, 120)); + + expectFlexBounds (c1, 10, 10, 100, 50); +} + +TEST (FlexBoxTests, GapIsAddedBetweenItemsOnTheMainAxis) +{ + Component c1, c2; + + FlexBox box; + box.gap = 20.0f; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 250, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 120, 0, 100, 50); +} + +TEST (FlexBoxTests, OrderSortsItemsBeforeLayout) +{ + Component c1, c2, c3; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50).withOrder (2)); + box.items.add (FlexItem (c2, 100, 50).withOrder (1)); + box.items.add (FlexItem (c3, 100, 50).withOrder (0)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c3, 0, 0, 100, 50); + expectFlexBounds (c2, 100, 0, 100, 50); + expectFlexBounds (c1, 200, 0, 100, 50); +} + +// ============================================================================= +// edge cases +// ============================================================================= + +TEST (FlexBoxTests, EmptyItemListDoesNothing) +{ + FlexBox box; + EXPECT_NO_THROW (box.performLayout (Rectangle (0, 0, 100, 100))); +} + +TEST (FlexBoxTests, ItemsWithoutComponentAreSkipped) +{ + Component c1, c2; + + FlexBox box; + box.items.add (FlexItem (c1, 100, 50)); + box.items.add (FlexItem (nullptr, 100, 50)); + box.items.add (FlexItem (c2, 100, 50)); + box.performLayout (Rectangle (0, 0, 300, 100)); + + expectFlexBounds (c1, 0, 0, 100, 50); + expectFlexBounds (c2, 200, 0, 100, 50); +} diff --git a/tests/yup_gui/yup_Grid.cpp b/tests/yup_gui/yup_Grid.cpp new file mode 100644 index 000000000..8ca56b110 --- /dev/null +++ b/tests/yup_gui/yup_Grid.cpp @@ -0,0 +1,536 @@ +/* + ============================================================================== + + This file is part of the YUP library. + Copyright (c) 2026 - kunitoki@gmail.com + + YUP is an open source library subject to open-source licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + to use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + YUP IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#include + +#include + +using namespace yup; + +namespace +{ + +void expectGridBounds (const Component& component, float x, float y, float width, float height) +{ + auto bounds = component.getBounds(); + EXPECT_FLOAT_EQ (x, bounds.getX()); + EXPECT_FLOAT_EQ (y, bounds.getY()); + EXPECT_FLOAT_EQ (width, bounds.getWidth()); + EXPECT_FLOAT_EQ (height, bounds.getHeight()); +} + +Grid makeTwoColumnGrid() +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (200)); + grid.templateColumns.add (Grid::TrackInfo::px (200)); + grid.templateRows.add (Grid::TrackInfo::px (100)); + return grid; +} + +} // namespace + +// ============================================================================= +// stretch (existing behavior) +// ============================================================================= + +TEST (GridTests, StretchFillsTheCell) +{ + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c).withColumn (0).withRow (0)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c, 0, 0, 200, 100); +} + +// ============================================================================= +// explicit width / height with every alignment +// ============================================================================= + +TEST (GridTests, ExplicitWidthAndHeightAreHonoredForEveryJustifySelf) +{ + struct JustifyCase + { + GridItem::AlignSelf justify; + float expectedX; + }; + + const JustifyCase cases[] = { + { GridItem::AlignSelf::flexStart, 0.0f }, + { GridItem::AlignSelf::center, 75.0f }, + { GridItem::AlignSelf::flexEnd, 150.0f }, + { GridItem::AlignSelf::stretch, 0.0f }, + }; + + for (const auto& testCase : cases) + { + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c) + .withColumn (0) + .withRow (0) + .withWidth (50) + .withHeight (30) + .withAlignSelf (GridItem::AlignSelf::stretch)); + grid.items.getReference (0).justifySelf = testCase.justify; + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c, testCase.expectedX, 0, 50, 30); + } +} + +TEST (GridTests, ExplicitWidthAndHeightAreHonoredForEveryAlignSelf) +{ + struct AlignCase + { + GridItem::AlignSelf align; + float expectedY; + }; + + const AlignCase cases[] = { + { GridItem::AlignSelf::flexStart, 0.0f }, + { GridItem::AlignSelf::center, 35.0f }, + { GridItem::AlignSelf::flexEnd, 70.0f }, + { GridItem::AlignSelf::stretch, 0.0f }, + }; + + for (const auto& testCase : cases) + { + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c) + .withColumn (0) + .withRow (0) + .withWidth (50) + .withHeight (30) + .withJustifySelf (GridItem::AlignSelf::stretch)); + grid.items.getReference (0).alignSelf = testCase.align; + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c, 0, testCase.expectedY, 50, 30); + } +} + +TEST (GridTests, JustifyItemsContainerDefaultAppliesToAutoAlignedItems) +{ + auto grid = makeTwoColumnGrid(); + grid.justifyItems = Grid::AlignItems::center; + + Component c1, c2; + grid.items.add (GridItem (c1).withColumn (0).withRow (0).withWidth (50).withHeight (30)); + grid.items.add (GridItem (c2) + .withColumn (1) + .withRow (0) + .withWidth (50) + .withHeight (30) + .withJustifySelf (GridItem::AlignSelf::flexStart)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c1, 75, 0, 50, 30); + expectGridBounds (c2, 200, 0, 50, 30); +} + +TEST (GridTests, AlignItemsContainerDefaultAppliesToAutoAlignedItems) +{ + auto grid = makeTwoColumnGrid(); + grid.alignItems = Grid::AlignItems::flexEnd; + + Component c; + grid.items.add (GridItem (c).withColumn (0).withRow (0).withWidth (50).withHeight (30)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c, 0, 70, 50, 30); +} + +// ============================================================================= +// min/max clamps +// ============================================================================= + +TEST (GridTests, MinMaxClampsAreAppliedToExplicitSizes) +{ + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c) + .withColumn (0) + .withRow (0) + .withWidth (150) + .withHeight (60) + .withMinWidth (180) + .withMinHeight (70) + .withMaxWidth (190) + .withMaxHeight (80) + .withJustifySelf (GridItem::AlignSelf::center) + .withAlignSelf (GridItem::AlignSelf::center)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + // Width clamped from 150 to 180, height from 60 to 70, then centered + expectGridBounds (c, 10, 15, 180, 70); +} + +TEST (GridTests, PercentageSizingIsClampedByMinMax) +{ + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c) + .withColumn (0) + .withRow (0) + .withWidthPercent (50) + .withHeightPercent (50) + .withMinWidth (120) + .withMinHeight (60) + .withJustifySelf (GridItem::AlignSelf::center) + .withAlignSelf (GridItem::AlignSelf::center)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + // 50% of 200x100 = 100x50, clamped up to 120x60, then centered + expectGridBounds (c, 40, 20, 120, 60); +} + +// ============================================================================= +// spans +// ============================================================================= + +TEST (GridTests, ColumnAndRowSpansCoverMultipleTracks) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateRows.add (Grid::TrackInfo::px (50)); + grid.templateRows.add (Grid::TrackInfo::px (50)); + + Component c1, c2; + grid.items.add (GridItem (c1).withColumn (0).withRow (0).withColumnSpan (2).withRowSpan (2)); + grid.items.add (GridItem (c2).withColumn (2).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + expectGridBounds (c1, 0, 0, 200, 100); + expectGridBounds (c2, 200, 0, 100, 50); +} + +// ============================================================================= +// percentage sizing +// ============================================================================= + +TEST (GridTests, PercentageSizingResolvesAgainstTheCell) +{ + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c) + .withColumn (0) + .withRow (0) + .withWidthPercent (50) + .withHeightPercent (50) + .withJustifySelf (GridItem::AlignSelf::center) + .withAlignSelf (GridItem::AlignSelf::center)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + // 50% of the 200x100 cell, centered within it + expectGridBounds (c, 50, 25, 100, 50); +} + +// ============================================================================= +// track sizing +// ============================================================================= + +TEST (GridTests, FrTracksShareTheRemainingSpace) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::fr (1)); + grid.templateColumns.add (Grid::TrackInfo::fr (2)); + grid.templateRows.add (Grid::TrackInfo::px (100)); + + Component c1, c2; + grid.items.add (GridItem (c1).withColumn (0).withRow (0)); + grid.items.add (GridItem (c2).withColumn (1).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + expectGridBounds (c1, 0, 0, 100, 100); + expectGridBounds (c2, 100, 0, 200, 100); +} + +TEST (GridTests, FixedTracksTakePriorityOverFrTracks) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (50)); + grid.templateColumns.add (Grid::TrackInfo::fr (1)); + grid.templateColumns.add (Grid::TrackInfo::fr (1)); + grid.templateRows.add (Grid::TrackInfo::px (100)); + + Component c1, c2, c3; + grid.items.add (GridItem (c1).withColumn (0).withRow (0)); + grid.items.add (GridItem (c2).withColumn (1).withRow (0)); + grid.items.add (GridItem (c3).withColumn (2).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + // 50 fixed, remaining 250 split equally between the two fr tracks + expectGridBounds (c1, 0, 0, 50, 100); + expectGridBounds (c2, 50, 0, 125, 100); + expectGridBounds (c3, 175, 0, 125, 100); +} + +TEST (GridTests, AutoTracksUseTheDefaultTrackSizes) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::auto_()); + grid.templateColumns.add (Grid::TrackInfo::auto_()); + grid.templateRows.add (Grid::TrackInfo::auto_()); + grid.autoRows = 60.0f; + + Component c1, c2; + grid.items.add (GridItem (c1).withColumn (0).withRow (0)); + grid.items.add (GridItem (c2).withColumn (1).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + expectGridBounds (c1, 0, 0, 100, 60); + expectGridBounds (c2, 100, 0, 100, 60); +} + +TEST (GridTests, MixedTrackTypesResolveInOrder) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::auto_()); + grid.templateColumns.add (Grid::TrackInfo::fr (1)); + grid.templateRows.add (Grid::TrackInfo::px (100)); + + Component c1, c2; + grid.items.add (GridItem (c1).withColumn (0).withRow (0)); + grid.items.add (GridItem (c2).withColumn (1).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + // auto track gets 100, the fr track gets the remaining 200 + expectGridBounds (c1, 0, 0, 100, 100); + expectGridBounds (c2, 100, 0, 200, 100); +} + +TEST (GridTests, CustomAutoTrackSizesAreUsedForImplicitTracks) +{ + Grid grid; + grid.autoColumns = 50.0f; + grid.autoRows = 30.0f; + + Component c1, c2; + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (c2)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c1, 0, 0, 50, 30); + expectGridBounds (c2, 50, 0, 50, 30); +} + +TEST (GridTests, ExplicitPlacementBeyondTheTemplateCreatesImplicitTracks) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c; + grid.items.add (GridItem (c).withColumn (3).withRow (0)); + grid.performLayout (Rectangle (0, 0, 300, 100)); + + // Columns 1..3 are created implicitly with autoColumns, no gaps + expectGridBounds (c, 300, 0, 100, 40); +} + +// ============================================================================= +// gaps and margins +// ============================================================================= + +TEST (GridTests, ColumnGapAndRowGapAreRespected) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateRows.add (Grid::TrackInfo::px (50)); + grid.templateRows.add (Grid::TrackInfo::px (50)); + grid.columnGap = 10.0f; + grid.rowGap = 20.0f; + + Component c1, c2, c3; + grid.items.add (GridItem (c1).withColumn (0).withRow (0)); + grid.items.add (GridItem (c2).withColumn (1).withRow (0)); + grid.items.add (GridItem (c3).withColumn (0).withRow (1)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + expectGridBounds (c1, 0, 0, 100, 50); + expectGridBounds (c2, 110, 0, 100, 50); + expectGridBounds (c3, 0, 70, 100, 50); +} + +TEST (GridTests, GridItemMarginsShrinkTheAvailableCell) +{ + auto grid = makeTwoColumnGrid(); + + Component c; + grid.items.add (GridItem (c).withColumn (0).withRow (0).withMargin (10)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + expectGridBounds (c, 10, 10, 180, 80); +} + +// ============================================================================= +// auto-placement +// ============================================================================= + +TEST (GridTests, AutoPlacementFlowsItemsIntoImplicitTracks) +{ + Grid grid; + + Component c1, c2, c3; + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (c2)); + grid.items.add (GridItem (c3)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + // No column template: items flow left to right in a growing single row + expectGridBounds (c1, 0, 0, 100, 40); + expectGridBounds (c2, 100, 0, 100, 40); + expectGridBounds (c3, 200, 0, 100, 40); +} + +TEST (GridTests, AutoPlacementWrapsAtTheTemplateColumnCount) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c1, c2, c3; + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (c2)); + grid.items.add (GridItem (c3)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + expectGridBounds (c1, 0, 0, 100, 40); + expectGridBounds (c2, 100, 0, 100, 40); + expectGridBounds (c3, 0, 40, 100, 40); +} + +TEST (GridTests, AutoPlacementAvoidsExplicitlyPlacedItems) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c0, c1, c2; + grid.items.add (GridItem (c0).withColumn (0).withRow (0)); + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (c2)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + expectGridBounds (c0, 0, 0, 100, 40); + expectGridBounds (c1, 100, 0, 100, 40); + expectGridBounds (c2, 200, 0, 100, 40); +} + +TEST (GridTests, AutoPlacementSkipsCellsOccupiedBySpans) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c0, c1; + grid.items.add (GridItem (c0).withColumn (0).withRow (0).withColumnSpan (2)); + grid.items.add (GridItem (c1)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + expectGridBounds (c0, 0, 0, 200, 40); + expectGridBounds (c1, 200, 0, 100, 40); +} + +TEST (GridTests, AutoPlacementRespectsRowSpans) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c0, c1; + grid.items.add (GridItem (c0).withRowSpan (2)); + grid.items.add (GridItem (c1)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + // c0 occupies rows 0-1 of column 0, so c1 lands in column 1 + expectGridBounds (c0, 0, 0, 100, 80); + expectGridBounds (c1, 100, 0, 100, 40); +} + +TEST (GridTests, AutoPlacementContinuesOnANewRowWhenTheRowIsFull) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c0, c1; + grid.items.add (GridItem (c0).withColumn (0).withRow (0).withColumnSpan (2)); + grid.items.add (GridItem (c1)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + expectGridBounds (c0, 0, 0, 200, 40); + expectGridBounds (c1, 0, 40, 100, 40); +} + +TEST (GridTests, AutoPlacementWrapsWithSingleColumnTemplate) +{ + Grid grid; + grid.templateColumns.add (Grid::TrackInfo::px (100)); + + Component c1, c2; + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (c2)); + grid.performLayout (Rectangle (0, 0, 300, 200)); + + // With a single template column, auto items stack into successive rows + expectGridBounds (c1, 0, 0, 100, 40); + expectGridBounds (c2, 0, 40, 100, 40); +} + +// ============================================================================= +// edge cases +// ============================================================================= + +TEST (GridTests, NullComponentItemsStillOccupyCells) +{ + Grid grid; + + Component c1, c2; + grid.items.add (GridItem (c1)); + grid.items.add (GridItem (nullptr)); + grid.items.add (GridItem (c2)); + grid.performLayout (Rectangle (0, 0, 400, 200)); + + // The null item occupies the middle cell, pushing c2 to the third column + expectGridBounds (c1, 0, 0, 100, 40); + expectGridBounds (c2, 200, 0, 100, 40); +} + +TEST (GridTests, EmptyItemListDoesNothing) +{ + Grid grid; + EXPECT_NO_THROW (grid.performLayout (Rectangle (0, 0, 100, 100))); +}