diff --git a/CMakeLists.txt b/CMakeLists.txt index b69ae79..a33b3b3 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,6 +87,7 @@ list(APPEND OGFX_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/ContextMenu.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/MenuBar.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/ToolBar.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/Layout.cpp # gui/widgets ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Widget.cpp diff --git a/other/TODO.txt b/other/TODO.txt index 2037977..96dadfd 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -23,7 +23,6 @@ ***Add Dark Mode Default theme ***FIX: Optimize ListView ***FIX: Limit FPS even when continuous events happen -Add theme caching Implement cursors in Stylesheet FIX: Window getting exponentially bigger when Desktop scale changes FIX: Refreshing scroll when content extent changes @@ -47,6 +46,8 @@ Add Hex Editor widget Add Message Boxes FIX: Widget Content extents increasing negatively not showing scrollbars FIX: Button state getting hung after showing an openFileDialog +FIX: ListView last item is covered by scrollbar +FIX: ListView last item not clickable even when no horizontal scrollbar present @@ -73,13 +74,13 @@ Implement following widgets: ***MenuBar ***Toolbar ***StatusBar + ***Layouts + ***Vertical + ***Horizontal + ***Grid + ***Fill ComboBox TreeView - Layouts - Vertical - Horizontal - Grid - Fill Text Input Text Area Numeric Field diff --git a/src/ogfx/gui/Layout.cpp b/src/ogfx/gui/Layout.cpp new file mode 100644 index 0000000..e1f0c32 --- /dev/null +++ b/src/ogfx/gui/Layout.cpp @@ -0,0 +1,332 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . +*/ + +#include "Layout.hpp" +#include "widgets/Widget.hpp" + +#include + +namespace ogfx +{ + namespace gui + { + // ============================================================================ + // Layout — shared helpers + // ============================================================================ + + Vec2 Layout::resolvePreferred(const Widget& child) + { + const auto& h = child.layoutHint(); + Vec2 pref; + pref.x = (h.preferred.x < 0) ? child.getw() : h.preferred.x; + pref.y = (h.preferred.y < 0) ? child.geth() : h.preferred.y; + return pref; + } + + f32 Layout::applyMinMax(f32 value, f32 minV, f32 maxV) + { + if (value < minV) value = minV; + if (maxV >= 0 && value > maxV) value = maxV; + return value; + } + + f32 Layout::alignOffset(eAlign a, f32 available, f32 size) + { + switch (a) + { + case eAlign::Center: return (available - size) * 0.5f; + case eAlign::End: return available - size; + case eAlign::Start: + case eAlign::Stretch: // when stretched the caller already used `available` + default: return 0.0f; + } + } + + // Iterate the children we actually care about: visible, valid, not ignored, + // not "ignore-scroll" (scrollbars and friends), and not manually drawn. + // This is a small inline helper to avoid repeating the predicate across + // every layout subclass below. + namespace + { + template + void for_each_laid_out(const Widget& parent, F&& fn) + { + auto& mgr = const_cast(parent).getChildren(); + for (auto* c : mgr.getWidgets()) + { + if (!c) continue; + if (c->isInvalid()) continue; + if (!c->isVisible()) continue; + if (c->isIgnoreScrollEnabled()) continue; // scrollbars etc. + if (c->layoutHint().ignored) continue; + fn(*c); + } + } + } + + + // ============================================================================ + // FillLayout + // ============================================================================ + + void FillLayout::arrange(Widget& parent) + { + // IMPORTANT: child positions are LOCAL to the parent's content frame. + // getGlobalPosition() automatically adds the parent's padding and + // contentOffset, so we must NOT include them here or they'd be + // applied twice. We only use the SIZE from getContentBounds(). + const Vec2 size = parent.getContentBounds().getSize(); + + for_each_laid_out(parent, [&](Widget& c) { + c.setPosition({ 0, 0 }); + c.setSize(size); + c.onBoundsChanged(size); + }); + } + + Vec2 FillLayout::measure(const Widget& parent) const + { + // Fill wants whatever the parent gives it; report the largest child + // preferred size as a hint for nested measure passes. + Vec2 result { 0, 0 }; + for_each_laid_out(parent, [&](Widget& c) { + Vec2 pref = resolvePreferred(c); + result.x = std::max(result.x, pref.x); + result.y = std::max(result.y, pref.y); + }); + return result; + } + + + // ============================================================================ + // BoxLayout + // ============================================================================ + + void BoxLayout::arrange(Widget& parent) + { + const bool horiz = (m_orientation == Orientation::Horizontal); + // IMPORTANT: child positions are LOCAL to the parent's content frame. + // getGlobalPosition() auto-adds parent's padding and contentOffset, + // so we use only the SIZE from getContentBounds() and start child + // positions at (0, 0). Adding area.x/area.y here would double-count. + const Vec2 areaSize = parent.getContentBounds().getSize(); + const f32 totalMain = horiz ? areaSize.x : areaSize.y; + const f32 totalCross = horiz ? areaSize.y : areaSize.x; + + // --- Pass 1: gather children, classify as "fixed" vs "stretching", + // and accumulate fixed-size + total stretch weight. + struct Slot { Widget* w; f32 main; f32 cross; bool stretches; f32 stretch; + f32 mainMin, mainMax; f32 crossMin, crossMax; }; + stdvec slots; + slots.reserve(8); + + f32 fixedMain = 0.0f; + f32 stretchTotal = 0.0f; + + for_each_laid_out(parent, [&](Widget& c) { + const auto& h = c.layoutHint(); + const Vec2 pref = resolvePreferred(c); + + Slot s; + s.w = &c; + s.main = horiz ? pref.x : pref.y; + s.cross = horiz ? pref.y : pref.x; + s.stretches = (h.stretch > 0.0f); + s.stretch = h.stretch; + s.mainMin = horiz ? h.minSize.x : h.minSize.y; + s.mainMax = horiz ? h.maxSize.x : h.maxSize.y; + s.crossMin = horiz ? h.minSize.y : h.minSize.x; + s.crossMax = horiz ? h.maxSize.y : h.maxSize.x; + + if (!s.stretches) + { + s.main = applyMinMax(s.main, s.mainMin, s.mainMax); + fixedMain += s.main; + } + else + { + stretchTotal += s.stretch; + } + slots.push_back(s); + }); + + if (slots.empty()) + return; + + const f32 spacingTotal = m_spacing * std::max(0, (f32)slots.size() - 1); + f32 leftover = std::max(0.0f, totalMain - fixedMain - spacingTotal); + + // --- Pass 2: distribute leftover among stretching children. + // We may need multiple rounds: if a stretcher hits its maxSize cap, + // we lock it and redistribute the residual among the rest. + stdvec locked(slots.size(), false); + for (size_t i = 0; i < slots.size(); ++i) + if (!slots[i].stretches) locked[i] = true; + + while (stretchTotal > 0.0f && leftover > 0.0f) + { + bool changed = false; + f32 newLeftover = leftover; + f32 newStretchTotal = stretchTotal; + + for (size_t i = 0; i < slots.size(); ++i) + { + if (locked[i]) continue; + f32 share = leftover * (slots[i].stretch / stretchTotal); + f32 capped = applyMinMax(share, slots[i].mainMin, slots[i].mainMax); + if (capped < share) // hit the cap + { + slots[i].main = capped; + newLeftover -= capped; + newStretchTotal -= slots[i].stretch; + locked[i] = true; + changed = true; + } + else + { + slots[i].main = share; // tentative; may be re-revised next round + } + } + + if (!changed) break; // no caps hit -> stable + leftover = newLeftover; + stretchTotal = newStretchTotal; + } + + // --- Pass 3: place children. + f32 cursor = 0.0f; // local to parent's content frame + for (auto& s : slots) + { + const auto& h = s.w->layoutHint(); + + // Cross size: stretch fills, otherwise use child preferred (clamped). + f32 crossSize; + if (h.alignCross == eAlign::Stretch) + crossSize = totalCross; + else + crossSize = applyMinMax(s.cross, s.crossMin, s.crossMax); + crossSize = std::min(crossSize, totalCross); + + const f32 crossOff = alignOffset(h.alignCross, totalCross, crossSize); + + if (horiz) + { + s.w->setPosition(cursor, crossOff); + s.w->setSize(s.main, crossSize); + } + else + { + s.w->setPosition(crossOff, cursor); + s.w->setSize(crossSize, s.main); + } + s.w->onBoundsChanged(s.w->getSize()); + + cursor += s.main + m_spacing; + } + } + + Vec2 BoxLayout::measure(const Widget& parent) const + { + const bool horiz = (m_orientation == Orientation::Horizontal); + f32 mainSum = 0, crossMax = 0; + i32 count = 0; + + for_each_laid_out(parent, [&](Widget& c) { + Vec2 pref = resolvePreferred(c); + f32 m = horiz ? pref.x : pref.y; + f32 cr = horiz ? pref.y : pref.x; + mainSum += m; + crossMax = std::max(crossMax, cr); + ++count; + }); + mainSum += m_spacing * std::max(0, count - 1); + return horiz ? Vec2 { mainSum, crossMax } : Vec2 { crossMax, mainSum }; + } + + + // ============================================================================ + // GridLayout + // ============================================================================ + + void GridLayout::arrange(Widget& parent) + { + if (m_rows == 0 || m_cols == 0) return; + + // IMPORTANT: child positions are LOCAL to the parent's content frame. + // getGlobalPosition() auto-adds parent's padding and contentOffset, + // so we use only the SIZE from getContentBounds() here. + const Vec2 areaSize = parent.getContentBounds().getSize(); + const f32 cellW = (areaSize.x - m_spacing * (m_cols - 1)) / (f32)m_cols; + const f32 cellH = (areaSize.y - m_spacing * (m_rows - 1)) / (f32)m_rows; + + u32 cellIndex = 0; + const u32 maxCells = m_rows * m_cols; + + for_each_laid_out(parent, [&](Widget& c) { + if (cellIndex >= maxCells) return; // overflow — drop + const auto& h = c.layoutHint(); + + const u32 col = cellIndex % m_cols; + const u32 row = cellIndex / m_cols; + + const u32 colSpan = std::max(1, h.colSpan); + const u32 rowSpan = std::max(1, h.rowSpan); + + const f32 slotW = cellW * colSpan + m_spacing * (colSpan - 1); + const f32 slotH = cellH * rowSpan + m_spacing * (rowSpan - 1); + + const f32 slotX = col * (cellW + m_spacing); // local to content frame + const f32 slotY = row * (cellH + m_spacing); + + // Apply alignment within the cell slot if the child isn't stretching. + Vec2 size { slotW, slotH }; + if (h.alignMain != eAlign::Stretch || h.alignCross != eAlign::Stretch) + { + Vec2 pref = resolvePreferred(c); + if (h.alignMain != eAlign::Stretch) size.x = std::min(pref.x, slotW); + if (h.alignCross != eAlign::Stretch) size.y = std::min(pref.y, slotH); + } + + const f32 offX = (h.alignMain == eAlign::Stretch) ? 0 : alignOffset(h.alignMain, slotW, size.x); + const f32 offY = (h.alignCross == eAlign::Stretch) ? 0 : alignOffset(h.alignCross, slotH, size.y); + + c.setPosition(slotX + offX, slotY + offY); + c.setSize(size); + c.onBoundsChanged(size); + + cellIndex += colSpan; + }); + } + + Vec2 GridLayout::measure(const Widget& parent) const + { + // Find the largest preferred size among children; report grid total. + f32 maxW = 0, maxH = 0; + for_each_laid_out(parent, [&](Widget& c) { + Vec2 pref = resolvePreferred(c); + maxW = std::max(maxW, pref.x); + maxH = std::max(maxH, pref.y); + }); + const f32 totalW = maxW * m_cols + m_spacing * (m_cols - 1); + const f32 totalH = maxH * m_rows + m_spacing * (m_rows - 1); + return { totalW, totalH }; + } + } +} diff --git a/src/ogfx/gui/Layout.hpp b/src/ogfx/gui/Layout.hpp new file mode 100644 index 0000000..b6ec96c --- /dev/null +++ b/src/ogfx/gui/Layout.hpp @@ -0,0 +1,145 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . +*/ + +#pragma once + +#include +#include +#include + +namespace ogfx +{ + namespace gui + { + class Widget; + + // ============================================================================ + // Layout — abstract base. + // + // A Layout is owned by a Widget (the "container") and is responsible for + // positioning and sizing that widget's children inside the container's + // content bounds (i.e. inside padding, after content offset). + // + // Two responsibilities: + // - arrange(parent): set position + size on every laid-out child. + // - measure(parent): report how much space this layout *wants* given + // its current children. Used so a Layout can drive + // a parent's contentExtents (and therefore scrollbars), + // and so a parent Layout can ask a child container + // "how big would you like to be?". + // ============================================================================ + class Layout + { + public: + virtual ~Layout(void) = default; + + // Place children. Called from Widget::relayout(). + virtual void arrange(Widget& parent) = 0; + + // Compute desired size of this layout's content. Should NOT mutate + // children. Used by Widget::getContentExtents() override below and + // by parent layouts asking for preferred size. + virtual Vec2 measure(const Widget& parent) const = 0; + + // Spacing between adjacent items along the main axis (Box) or + // between cells (Grid). FillLayout ignores it. + inline f32 getSpacing(void) const { return m_spacing; } + inline void setSpacing(f32 s) { m_spacing = s; } + + protected: + // Helpers used by concrete layouts to honor a child's hint + // without each subclass re-implementing the same min/max/preferred + // resolution logic. + static Vec2 resolvePreferred(const Widget& child); + static f32 applyMinMax(f32 value, f32 minV, f32 maxV); + static f32 alignOffset(eAlign a, f32 available, f32 size); + + f32 m_spacing { 0.0f }; + }; + + + // ============================================================================ + // FillLayout — every laid-out child is sized to fill the parent's content + // area. Useful as a default (a "single slot" container) and as a building + // block for things like SplitView panels. + // ============================================================================ + class FillLayout : public Layout + { + public: + void arrange(Widget& parent) override; + Vec2 measure(const Widget& parent) const override; + }; + + + // ============================================================================ + // BoxLayout — arrange children in a single row (Horizontal) or column + // (Vertical) with stretch factors and spacing. + // + // Algorithm (per axis = "main"): + // 1. Sum preferred sizes of non-stretching children + spacing. + // 2. The remainder is distributed among stretching children proportional + // to their stretch factor. + // 3. min/max are clamped per child; if a stretching child hits its max, + // the leftover is redistributed among the rest in a second pass. + // + // Cross axis: each child either fills (Stretch, default) or uses its + // preferred size aligned per alignCross. + // ============================================================================ + class BoxLayout : public Layout + { + public: + enum class Orientation { Horizontal, Vertical }; + + explicit BoxLayout(Orientation orientation) : m_orientation(orientation) {} + + void arrange(Widget& parent) override; + Vec2 measure(const Widget& parent) const override; + + inline Orientation getOrientation(void) const { return m_orientation; } + inline void setOrientation(Orientation o) { m_orientation = o; } + + private: + Orientation m_orientation; + }; + + + // ============================================================================ + // GridLayout — fixed rows × cols. Cell sizes are uniform (rows = h/N, + // cols = w/M). Children are placed in row-major insertion order. + // LayoutHint::colSpan / rowSpan let a child cover multiple cells. + // ============================================================================ + class GridLayout : public Layout + { + public: + GridLayout(u32 rows, u32 cols) : m_rows(rows), m_cols(cols) {} + + void arrange(Widget& parent) override; + Vec2 measure(const Widget& parent) const override; + + inline u32 getRows(void) const { return m_rows; } + inline u32 getCols(void) const { return m_cols; } + inline void setDimensions(u32 rows, u32 cols) { m_rows = rows; m_cols = cols; } + + private: + u32 m_rows; + u32 m_cols; + }; + } +} diff --git a/src/ogfx/gui/LayoutHint.hpp b/src/ogfx/gui/LayoutHint.hpp new file mode 100644 index 0000000..19ff726 --- /dev/null +++ b/src/ogfx/gui/LayoutHint.hpp @@ -0,0 +1,80 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2025 OmniaX-Dev + + This file is part of OmniaFramework. + + OmniaFramework is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OmniaFramework is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OmniaFramework. If not, see . +*/ + +#pragma once + +#include +#include + +namespace ogfx +{ + namespace gui + { + // How a child wants to align itself within the slot a Layout gives it. + enum class eAlign : u8 + { + Start, // top / left + Center, // center along the relevant axis + End, // bottom / right + Stretch // fill the available space along that axis + }; + + // Per-child metadata read by the parent's Layout when it arranges children. + // All fields have sane defaults so unset hints behave like "use my current size, + // no stretching, top-left, span 1". + struct LayoutHint + { + // 0 = use the widget's current/preferred size on the main axis. + // > 0 = take a share of leftover space proportional to this value. + // e.g. two children with stretch=1.0 split leftover space 50/50; + // stretch=2 vs stretch=1 splits it 2/3 vs 1/3. + f32 stretch { 0.0f }; + + // Alignment along the layout's main axis (only meaningful when the child + // is smaller than the slot the layout assigned it — i.e. when stretch=0 + // and the slot ended up larger than preferred). Mostly relevant for Grid + // and for FillLayout in single-child mode. + eAlign alignMain { eAlign::Start }; + + // Alignment perpendicular to the main axis. Stretch (default) makes the + // child fill the cross dimension of its slot — usually what you want. + eAlign alignCross { eAlign::Stretch }; + + // Hard limits the layout will respect. minSize is honored even if it + // breaks stretch ratios; maxSize component < 0 means "unbounded". + Vec2 minSize { 0, 0 }; + Vec2 maxSize { -1, -1 }; + + // If a component is < 0, the layout uses the widget's current size + // (getw()/geth()) for that axis. Otherwise this overrides it. + Vec2 preferred { -1, -1 }; + + // Grid-only. How many cells the child occupies. Ignored by Box/Fill. + u8 colSpan { 1 }; + u8 rowSpan { 1 }; + + // If true, the layout skips this child entirely. Useful for floating + // overlays, drag-ghosts, or scrollbars (which manage their own bounds). + // Note: ScrollableWidget already excludes its scrollbars via the + // IgnoreScroll flag, but this gives finer control for other cases. + bool ignored { false }; + }; + } +} diff --git a/src/ogfx/gui/ToolBar.cpp b/src/ogfx/gui/ToolBar.cpp index 5ce2fd0..6897a87 100644 --- a/src/ogfx/gui/ToolBar.cpp +++ b/src/ogfx/gui/ToolBar.cpp @@ -35,7 +35,7 @@ namespace ogfx { setRootChild(); setStylesheetCategoryName("toolbar"); - setTypeName("ogfx::gui::widgets::ToolBar"); + setTypeName("ogfx::gui::ToolBar"); auto& win = cast(getWindow()); w = win.getWindowWidth(); h = m_height; @@ -51,19 +51,18 @@ namespace ogfx return *this; } - widgets::Button& ToolBar::addButton(const String& iconPath, const String& text, EventCallback callback) + Button& ToolBar::addButton(const String& iconPath, const String& text, EventCallback callback) { - static f32 pad = 0; m_buttons.push_back({ getWindow() }); auto& btn = m_buttons.back(); btn.addThemeID("tool_button"); btn.setIcon(iconPath); - btn.setIconSize({ m_height, m_height }); btn.setText(text); + btn.setSizeMode(eSizeMode::LayoutManaged); btn.setCallback(ogfx::gui::Widget::eCallback::ActionPerformed, callback); btn.enableIconOnly(); - addWidget(btn, { m_height * (m_buttons.size() - 1) + pad, 0 }); - pad += 15; + btn.layoutHint().preferred = { m_height, m_height }; + addWidget(btn); return btn; } diff --git a/src/ogfx/gui/ToolBar.hpp b/src/ogfx/gui/ToolBar.hpp index 4c01297..1f04d38 100644 --- a/src/ogfx/gui/ToolBar.hpp +++ b/src/ogfx/gui/ToolBar.hpp @@ -34,7 +34,7 @@ namespace ogfx public: ToolBar(Window& window, bool statusbar = false); ToolBar& create(bool statusbar = false); - widgets::Button& addButton(const String& iconPath, const String& text = "", EventCallback callback = nullptr); + Button& addButton(const String& iconPath, const String& text = "", EventCallback callback = nullptr); void onWindowResized(const Event& event) override; void applyTheme(const ostd::Stylesheet& theme) override; void onDraw(BasicRenderer2D& gfx) override; @@ -59,7 +59,7 @@ namespace ogfx private: bool m_disableButtonText { true }; - std::deque m_buttons; + std::deque