From 796b03f08c0455a889ab4fa35b7dc5c56b9a47e6 Mon Sep 17 00:00:00 2001 From: OmniaX-Dev Date: Fri, 1 May 2026 05:06:21 +0200 Subject: [PATCH] Added TreeView widget --- CMakeLists.txt | 2 +- extra/DefaultTheme.oss | 19 - extra/DefaultThemeDark.oss | 45 ++- other/TODO.txt | 12 +- src/ogfx/gui/Widgets.hpp | 10 +- src/ogfx/gui/widgets/ListView.cpp | 272 -------------- src/ogfx/gui/widgets/ListView.hpp | 116 ------ src/ogfx/gui/widgets/TreeView.cpp | 573 ++++++++++++++++++++++++++++++ src/ogfx/gui/widgets/TreeView.hpp | 161 +++++++++ src/ogfx/utils/Animation.hpp | 10 + src/test/GuiTest.cpp | 65 +++- 11 files changed, 832 insertions(+), 453 deletions(-) delete mode 100644 src/ogfx/gui/widgets/ListView.cpp delete mode 100644 src/ogfx/gui/widgets/ListView.hpp create mode 100644 src/ogfx/gui/widgets/TreeView.cpp create mode 100644 src/ogfx/gui/widgets/TreeView.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 73a1d49..49aef5e 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,8 +100,8 @@ list(APPEND OGFX_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/RadioButton.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ProgressBar.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Slider.cpp - ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ListView.cpp ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ComboBox.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/TreeView.cpp # render ${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/BasicRenderer.cpp diff --git a/extra/DefaultTheme.oss b/extra/DefaultTheme.oss index 4df966e..bdfd528 100644 --- a/extra/DefaultTheme.oss +++ b/extra/DefaultTheme.oss @@ -258,25 +258,6 @@ const $accentColorDark = #6B0A1DFF -% ====== ListView ====== -(list) { - (item) { - defaultTextColor = #202020FF - defaultFontSize = 16 - defaultPadding = rect(5, 15, 20, 40) - defaultSelectionColor = $accentColor - defaultSelectionTextColor = color_white - } - backgroundColor = rgb(160, 160, 160) - borderColor = rgb(50, 50, 50) - separatorLineColor = rgb(40, 40, 40) - showSeparatorLine = true - -} -% ====================== - - - % ====== Context Menu ====== (context) { padding = rect(16, 0, 16, 0) diff --git a/extra/DefaultThemeDark.oss b/extra/DefaultThemeDark.oss index ee29b94..7e22ec5 100644 --- a/extra/DefaultThemeDark.oss +++ b/extra/DefaultThemeDark.oss @@ -254,25 +254,6 @@ const $backgroundColor = rgba(30, 30, 30, 255) -% ====== ListView ====== -(list) { - (item) { - defaultTextColor = $textColor - defaultFontSize = 16 - defaultPadding = rect(5, 5, 20, 0) - defaultSelectionColor = $accentColor - defaultSelectionTextColor = color_white - } - backgroundColor = $darkColor - borderColor = $borderColor - separatorLineColor = $borderColor - showSeparatorLine = true - -} -% ====================== - - - % ====== Context Menu ====== (context) { padding = rect(16, 0, 16, 0) @@ -376,3 +357,29 @@ const $backgroundColor = rgba(30, 30, 30, 255) borderColor = $accentColorLight } % ====================== + + + +% ====== TreeView ====== +(tree) { + linePadding = rect(5, 5, 20, 0) + selectionColor = $accentColor + selectionTextColor = color_white + iconSpacing = 0.0 + iconInset = 4.0 + showIcons = true + fontSize = 16 + textColor = $textColor + backgroundColor = $darkColor + borderColor = $borderColor + separatorLineColor = $borderColor + showSeparatorLine = true + iconTintColor = $accentColorLight + indentWidth = 16.0 + chevronColor = $accentColorDark + chevronInset = 4.0 + branchLineColor = $textColor + showBranchLines = true + branchLineThickness = 1.0 +} +% ====================== diff --git a/other/TODO.txt b/other/TODO.txt index 319478a..102b24a 100644 --- a/other/TODO.txt +++ b/other/TODO.txt @@ -31,10 +31,8 @@ FIX: Float values in stylesheet needing to have decimal point to be read as floa Make parsing of vec2 and rect in stylesheet, consistent with other value functions like gradients and animations Add buttons to scroll tabs in TabPanel Add Font class, to handle different font attributes -Add multi-selection to ListView +Add multi-selection to TreeView Cache getStringDimensions() call in TabPanel::draw_tabs -FIX: Padding for ListView items -Add icons to ListView Add gradient to listbox selection Add gradient to menubar background and selection Add cursor stack @@ -46,11 +44,13 @@ 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 +FIX: TreeView last item is covered by scrollbar +FIX: TreeView last item not clickable even when no horizontal scrollbar present FIX: Refresh display text in ComboBox when font size changes and also on theme reload Add setSelectedMenuItem() function to ComboBox FIX: ContextMenu rendering partly outside the screen when showing it close to the right Window border +Update theme for visible TreeView lines +Add ToolBar::addButton overload that takes a ogfx::Icon wrapper @@ -83,7 +83,7 @@ Implement following widgets: ***Grid ***Fill ***ComboBox - TreeView + ***TreeView Text Input Text Area Numeric Field diff --git a/src/ogfx/gui/Widgets.hpp b/src/ogfx/gui/Widgets.hpp index 0b0e26e..5b82817 100644 --- a/src/ogfx/gui/Widgets.hpp +++ b/src/ogfx/gui/Widgets.hpp @@ -28,5 +28,13 @@ #include #include #include -#include #include +#include + +namespace ogfx +{ + namespace gui + { + using ListView = TreeView; + } +} diff --git a/src/ogfx/gui/widgets/ListView.cpp b/src/ogfx/gui/widgets/ListView.cpp deleted file mode 100644 index db3c3c4..0000000 --- a/src/ogfx/gui/widgets/ListView.cpp +++ /dev/null @@ -1,272 +0,0 @@ -/* - 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 "ListView.hpp" -#include "../../render/BasicRenderer.hpp" -#include "../Window.hpp" -#include "../../../ostd/io/Memory.hpp" - -namespace ogfx -{ - namespace gui - { - void ListView::Item::setText(const String& text) - { - if (m_text == text) - return; - m_text = text; - update_dimensions(); - if (m_parent) m_parent->m_extentsDirty = true; - } - - void ListView::Item::setFontSize(i32 fontSize) - { - if (m_fontSize == fontSize) - return; - m_fontSize = fontSize; - update_dimensions(); - if (m_parent) m_parent->m_extentsDirty = true; - } - - void ListView::Item::update_dimensions(void) - { - if (!isValid()) - return; - m_dimensions = m_parent->getWindow().getGFX().getStringDimensions(m_text, m_fontSize); - m_dimensions += Vec2 { m_padding.x + m_padding.w, m_padding.y + m_padding.h }; - } - - void ListView::Item::set_selected(stdvec& selectionList) - { - for (auto& sel : selectionList) - sel->m_selected = false; - selectionList.clear(); - selectionList.push_back(this); - m_selected = true; - } - - void ListView::Item::add_selected(stdvec& selectionList) - { - if (STDVEC_CONTAINS(selectionList, this)) - return; - selectionList.push_back(this); - m_selected = true; - } - - void ListView::Item::remove_selected(stdvec& selectionList) - { - if (!STDVEC_CONTAINS(selectionList, this)) - return; - STDVEC_REMOVE(selectionList, this); - m_selected = false; - } - - - - - ListView& ListView::create(void) - { - setPadding({ 0, 0, 0, 0 }); - setTypeName("ogfx::gui::ListView"); - enableStopEvents(); - enableBackground(); - enableBorder(); - setBackgroundColor({ 160, 160, 160 }); - setBorderColor({ 50, 50, 50 }); - setStylesheetCategoryName("list"); - validate(); - return *this; - } - - void ListView::applyTheme(const ostd::Stylesheet& theme) - { - setSeparatorLineColor(getThemeValue(theme, "separatorLineColor", getSeparatorLineColor())); - enableShowSeparatorLine(getThemeValue(theme, "showSeparatorLine", isShowSeparatorLineEnabled())); - setDefaultLineTextColor(getThemeValue(theme, "item.defaultTextColor", getDefaultLineTextColor())); - setDefaultLineFontSize(getThemeValue(theme, "item.defaultFontSize", getDefaultLineFontSize())); - setDefaultLinePadding(getThemeValue(theme, "item.defaultPadding", getDefaultLinePadding())); - setDefaultLineSelectionColor(getThemeValue(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor())); - setDefaultLineSelectionTextColor(getThemeValue(theme, "item.defaultSelectionTextColor", getDefaultLineSelectionTextColor())); - } - - void ListView::onDraw(ogfx::BasicRenderer2D& gfx) - { - const auto& bounds = getGlobalBounds(); - const auto& content = getContentExtents(); - const f32 scrollY = -getScrollOffset().y; - const f32 visibleH = getContentBounds().h; - const f32 visibleStart = scrollY; - const f32 visibleEnd = scrollY + visibleH; - Color textColor; - - f32 y = 0; - i32 startIdx = 0; - for (auto& item : m_list) - { - if (!item.isValid()) { startIdx++; continue; } - f32 itemH = item.getDimensions().y; - if (y + itemH > visibleStart) break; - y += itemH; - startIdx++; - } - - for (i32 i = startIdx; i < (i32)m_list.size(); i++) - { - auto& item = m_list[i]; - if (!item.isValid()) continue; - f32 itemH = item.getDimensions().y; - if (y > visibleEnd) break; - f32 lineW = content.w; - if (content.w < bounds.w) - lineW = bounds.w; - if (item.isSelected()) - { - textColor = item.getSelectedTextColor(); - gfx.fillRect({ Vec2 { bounds.x, bounds.y + y + item.getPadding().h } + getScrollOffset(), { lineW, itemH } }, item.getSelectedColor()); - } - else - textColor = item.getTextColor(); - gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + item.getPadding().h } + getScrollOffset() }, getSeparatorLineColor(), 1); - gfx.drawString(item, bounds.getPosition() + Vec2 { 0, y } + getScrollOffset() + item.getPadding().getPosition(), textColor, item.getFontSize()); - y += itemH; - } - } - - void ListView::afterDraw(ogfx::BasicRenderer2D& gfx) - { - drawScrollbars(gfx); - } - - void ListView::onMouseReleased(const Event& event) - { - if (!isMouseInside()) - return; - if (event.mouse->button != ogfx::MouseEventData::eButton::Left) - return; - if (isMouseInsideAnyScrollbar()) - return; - - const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; - const Vec2 origin = getGlobalBounds().getPosition(); - const Vec2 scroll = getScrollOffset(); - const f32 localY = mousePos.y - origin.y - scroll.y; - - f32 y = 0; - for (auto& item : m_list) - { - if (!item.isValid()) continue; - const f32 itemH = item.getDimensions().y; - if (localY >= y && localY < y + itemH) - { - bool wasSelected = item.isSelected(); - item.set_selected(m_selectedList); - if (!wasSelected && callback_onSelectionChanged) - callback_onSelectionChanged(m_selectedList); - event.handle(); - break; - } - y += itemH; - } - } - - Rectangle ListView::getContentExtents(void) const - { - - if (!m_extentsDirty) - return m_cachedExtents; - f32 maxX = 0, maxY = 0; - for (auto& item : m_list) - { - if (!item.isValid()) continue; - Vec2 size = item.getDimensions(); - maxX = std::max(maxX, size.x); - maxY += size.y; - } - m_cachedExtents = { 0, 0, maxX, maxY }; - m_extentsDirty = false; - return m_cachedExtents; - } - - ListView::Item& ListView::getLine(const String& text) - { - for (auto& item : m_list) - if (item.getText() == text) - return item; - return InvalidItem; - } - - ListView::Item& ListView::getLine(u32 index) - { - if (index < m_list.size()) - return m_list[index]; - return InvalidItem; - } - - ListView::Item& ListView::addLine(const String& text) - { - Item item { *this }; - item.setTextColor(m_defaultLinetextColor); - item.setPadding(m_defaultLinepadding); - item.setFontSize(m_defaultLinefontSize); - item.setSelectedColor(m_defaultSelectionColor); - item.setSelectedTextColor(m_defaultSelectionTextColor); - item.setText(text); - m_list.push_back(item); - if (m_list.size() > 0) - m_list[0].set_selected(m_selectedList); - m_extentsDirty = true; - return m_list.back(); - } - - bool ListView::removeLine(const String& text) - { - auto it = std::find_if(m_list.begin(), m_list.end(), [&](const Item& item) { - return item.getText() == text; - }); - if (it != m_list.end()) - { - m_list.erase(it); - return true; - } - return false; - } - - bool ListView::removeLine(u32 index) - { - if (!hasLine(index)) - return false; - m_list.erase(m_list.begin() + index); - return true; - } - - bool ListView::hasLine(const String& text) - { - for (auto& item : m_list) - if (item.getText() == text) - return true; - return false; - } - - bool ListView::hasLine(u32 index) - { - return m_list.size() > index; - } - } -} diff --git a/src/ogfx/gui/widgets/ListView.hpp b/src/ogfx/gui/widgets/ListView.hpp deleted file mode 100644 index 019bcfe..0000000 --- a/src/ogfx/gui/widgets/ListView.hpp +++ /dev/null @@ -1,116 +0,0 @@ -/* - 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 ListView : public ScrollableWidget - { - public: class Item : public ostd::__i_stringeable - { - public: - inline Item(ListView& parent) { m_parent = &parent; } - void setText(const String& text); - void setFontSize(i32 fontSize); - - inline String toString(void) const override { return m_text; } - inline bool isValid(void) const { return m_parent != nullptr && m_text.new_trim() != ""; } - - inline Vec2 getDimensions(void) const { return m_dimensions; } - inline i32 getFontSize(void) const { return m_fontSize; } - inline String getText(void) const { return m_text; } - inline bool isSelected(void) const { return m_selected; } - - OSTD_PARAM_GETSET(Color, TextColor, m_textColor); - OSTD_PARAM_GETSET(Rectangle, Padding, m_padding); - OSTD_PARAM_GETSET(Color, SelectedTextColor, m_selectedTextColor); - OSTD_PARAM_GETSET(Color, SelectedColor, m_selectedColor); - - private: - void update_dimensions(void); - void set_selected(stdvec& selectionList); - void add_selected(stdvec& selectionList); - void remove_selected(stdvec& selectionList); - - private: - Vec2 m_dimensions { 0, 0 }; - bool m_selected { false }; - ListView* m_parent { nullptr }; - String m_text { "" }; - i32 m_fontSize { 16 }; - Color m_textColor { 30, 30, 30 }; - Color m_selectedTextColor { Colors::White }; - Color m_selectedColor { Colors::Crimson }; - Rectangle m_padding { 5, 5, 20, 0 }; - - friend class ListView; - }; - public: - inline ListView(WindowCore& window) : ScrollableWidget(window) { create(); } - ListView& create(void); - void applyTheme(const ostd::Stylesheet& theme) override; - void onDraw(ogfx::BasicRenderer2D& gfx) override; - void afterDraw(ogfx::BasicRenderer2D& gfx) override; - void onMouseReleased(const Event& event) override; - Rectangle getContentExtents(void) const override; - Item& getLine(const String& text); - Item& getLine(u32 index); - Item& addLine(const String& text); - bool removeLine(const String& text); - bool removeLine(u32 index); - bool hasLine(const String& text); - bool hasLine(u32 index); - inline void addLine(const Item& line) { m_list.push_back(line); if (m_list.size() == 1) m_list.back().set_selected(m_selectedList); } - inline stdvec& getSelection(void) { return m_selectedList; } - inline void setSelectionChangedCallback(std::function& selection)> callback) { callback_onSelectionChanged = std::move(callback); } - - OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor); - OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine); - OSTD_PARAM_GETSET(i32, DefaultLineFontSize, m_defaultLinefontSize); - OSTD_PARAM_GETSET(Color, DefaultLineTextColor, m_defaultLinetextColor); - OSTD_PARAM_GETSET(Rectangle, DefaultLinePadding, m_defaultLinepadding); - OSTD_PARAM_GETSET(Color, DefaultLineSelectionColor, m_defaultSelectionColor); - OSTD_PARAM_GETSET(Color, DefaultLineSelectionTextColor, m_defaultSelectionTextColor); - - private: - Item InvalidItem { *this }; - std::deque m_list; - stdvec m_selectedList; - std::function& selection)> callback_onSelectionChanged { nullptr }; - mutable Rectangle m_cachedExtents { 0, 0, 0, 0 }; - mutable bool m_extentsDirty { true }; - - Color m_lineColor { 40, 40, 40 }; - bool m_showLine { true }; - i32 m_defaultLinefontSize { 16 }; - Color m_defaultLinetextColor { 30, 30, 30 }; - Rectangle m_defaultLinepadding { 5, 5, 20, 0 }; - Color m_defaultSelectionColor { Colors::Crimson }; - Color m_defaultSelectionTextColor { Colors::White }; - }; - } -} diff --git a/src/ogfx/gui/widgets/TreeView.cpp b/src/ogfx/gui/widgets/TreeView.cpp new file mode 100644 index 0000000..290d33a --- /dev/null +++ b/src/ogfx/gui/widgets/TreeView.cpp @@ -0,0 +1,573 @@ +/* + 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 "TreeView.hpp" +#include "../../render/BasicRenderer.hpp" +#include "../Window.hpp" +#include "../../../ostd/io/Memory.hpp" +#include + +namespace ogfx +{ + namespace gui + { + void TreeView::Item::setText(const String& text) + { + if (m_text == text) + return; + m_text = text; + update_dimensions(); + if (m_treeView) m_treeView->m_extentsDirty = true; + } + + void TreeView::Item::setIcon(const Icon& icon) + { + m_icon = icon; + enableIcon(); + } + + void TreeView::Item::update_dimensions(void) + { + if (!isValid()) + return; + auto pad = m_treeView->getLinePadding(); + m_dimensions = m_treeView->getWindow().getGFX().getStringDimensions(m_text, m_treeView->getFontSize()); + m_dimensions += Vec2 { pad.x + pad.w, pad.y + pad.h }; + } + + void TreeView::Item::set_selected(stdvec& selectionList) + { + for (auto& sel : selectionList) + sel->m_selected = false; + selectionList.clear(); + selectionList.push_back(this); + m_selected = true; + } + + void TreeView::Item::add_selected(stdvec& selectionList) + { + if (STDVEC_CONTAINS(selectionList, this)) + return; + selectionList.push_back(this); + m_selected = true; + } + + void TreeView::Item::remove_selected(stdvec& selectionList) + { + if (!STDVEC_CONTAINS(selectionList, this)) + return; + STDVEC_REMOVE(selectionList, this); + m_selected = false; + } + + void TreeView::Item::setExpanded(bool expanded) + { + if (m_expanded == expanded) + return; + m_expanded = expanded; + if (m_treeView) m_treeView->m_extentsDirty = true; + } + + bool TreeView::Item::isVisible(void) const + { + // An item is visible if every ancestor is expanded. + const Item* p = m_parentItem; + while (p != nullptr) + { + if (!p->m_expanded) return false; + p = p->m_parentItem; + } + return true; + } + + stdvec TreeView::Item::getFullPath(void) const + { + // Walk parent pointers up to the root, collecting text bottom-up, + // then reverse so the result reads root -> ... -> this item. + stdvec path; + path.push_back(m_text); + const Item* p = m_parentItem; + while (p != nullptr) + { + path.push_back(p->m_text); + p = p->m_parentItem; + } + std::reverse(path.begin(), path.end()); + return path; + } + + + + + + + TreeView& TreeView::create(void) + { + setPadding({ 0, 0, 0, 0 }); + setTypeName("ogfx::gui::TreeView"); + enableStopEvents(); + enableBackground(); + enableBorder(); + setBackgroundColor({ 160, 160, 160 }); + setBorderColor({ 50, 50, 50 }); + setStylesheetCategoryName("tree"); + reloadTheme(); + validate(); + return *this; + } + + void TreeView::applyTheme(const ostd::Stylesheet& theme) + { + setSeparatorLineColor(getThemeValue(theme, "separatorLineColor", getSeparatorLineColor())); + enableShowSeparatorLine(getThemeValue(theme, "showSeparatorLine", isShowSeparatorLineEnabled())); + setLinePadding(getThemeValue(theme, "linePadding", getLinePadding())); + setLineSelectionColor(getThemeValue(theme, "selectionColor", getLineSelectionColor())); + setLineSelectionTextColor(getThemeValue(theme, "selectionTextColor", getLineSelectionTextColor())); + setIconSpacing(getThemeValue(theme, "iconSpacing", getIconSpacing())); + setIconInset(getThemeValue(theme, "iconInset", getIconInset())); + enableIcons(getThemeValue(theme, "showIcons", isIconsEnabled())); + setIconTintColor(getThemeValue(theme, "iconTintColor", getIconTintColor())); + setIndentWidth(getThemeValue(theme, "indentWidth", getIndentWidth())); + setChevronColor(getThemeValue(theme, "chevronColor", getChevronColor())); + setChevronInset(getThemeValue(theme, "chevronInset", getChevronInset())); + setBranchLineColor(getThemeValue(theme, "branchLineColor", getBranchLineColor())); + enableBranchLines(getThemeValue(theme, "showBranchLines", isBranchLinesEnabled())); + setBranchLineThickness(getThemeValue(theme, "branchLineThickness", getBranchLineThickness())); + } + + void TreeView::onDraw(ogfx::BasicRenderer2D& gfx) + { + const auto& bounds = getGlobalBounds(); + const auto& content = getContentExtents(); + const f32 scrollY = -getScrollOffset().y; + const f32 visibleH = getContentBounds().h; + const f32 visibleStart = scrollY; + const f32 visibleEnd = scrollY + visibleH; + Color textColor; + + // Walk forward through visible items only, advancing y for each one we draw. + f32 y = 0; + i32 startIdx = 0; + for (auto& itemPtr : m_list) + { + auto& item = *itemPtr; + if (!item.isValid() || !item.isVisible()) { startIdx++; continue; } + f32 itemH = item.getDimensions().y; + if (y + itemH > visibleStart) break; + y += itemH; + startIdx++; + } + + for (i32 i = startIdx; i < (i32)m_list.size(); i++) + { + auto& item = *m_list[i]; + if (!item.isValid()) continue; + if (!item.isVisible()) continue; + f32 itemH = item.getDimensions().y; + if (y > visibleEnd) break; + f32 lineW = content.w; + if (content.w < bounds.w) + lineW = bounds.w; + auto pad = getLinePadding(); + Rectangle lineRect = { Vec2 { bounds.x, bounds.y + y + pad.h } + getScrollOffset(), { lineW, itemH } }; + if (item.isSelected()) + { + textColor = getLineSelectionTextColor(); + gfx.fillRect(lineRect, getLineSelectionColor()); + } + else + textColor = getTextColor(); + + // Branch lines (drawn under everything else for this row). + if (isBranchLinesEnabled()) + render_branch_lines(gfx, item, lineRect); + + // The chevron occupies one slot at column == depth. + // Text/icon start one slot further right (column == depth + 1). + const f32 indent = getIndentWidth(); + const f32 chevronX = bounds.x + item.getDepth() * indent + getScrollOffset().x; + const f32 contentLeftX = chevronX + indent; // where icon (if any) or text starts + + if (item.hasChildren()) + { + Rectangle chevronBounds { + chevronX, + bounds.y + y + pad.h + getScrollOffset().y, + indent, + itemH + }; + if (m_chevronDrawCallback) + { + gfx.pushClippingRect(chevronBounds); + m_chevronDrawCallback(*this, item, chevronBounds, gfx); + gfx.popClippingRect(); + } + else + { + render_chevron(gfx, item, chevronBounds); + } + } + + f32 textX = contentLeftX - bounds.x - getScrollOffset().x; + if (item.isIconEnabled() && isIconsEnabled()) + { + gfx.drawAnimation(item.getIcon(), + Vec2 { contentLeftX + getIconInset(), bounds.y + getIconInset() + y + pad.h + getScrollOffset().y }, + { itemH - (getIconInset() * 2), itemH - (getIconInset() * 2) }, + getIconTintColor()); + textX += itemH + getIconSpacing(); + } + gfx.drawLine({ Vec2 { bounds.x, bounds.y + y + itemH + pad.h } + getScrollOffset(), Vec2 { bounds.x + lineW, bounds.y + y + itemH + pad.h } + getScrollOffset() }, getSeparatorLineColor(), 1); + gfx.drawVCenteredString(item, lineRect + Rectangle { textX, 0, -(textX * 2), 0 }, textColor, getFontSize()); + y += itemH; + } + } + + void TreeView::afterDraw(ogfx::BasicRenderer2D& gfx) + { + drawScrollbars(gfx); + } + + void TreeView::onMouseReleased(const Event& event) + { + if (!isMouseInside()) + return; + if (event.mouse->button != ogfx::MouseEventData::eButton::Left) + return; + if (isMouseInsideAnyScrollbar()) + return; + + const Vec2 mousePos { event.mouse->position_x, event.mouse->position_y }; + const Vec2 origin = getGlobalBounds().getPosition(); + const Vec2 scroll = getScrollOffset(); + const f32 localY = mousePos.y - origin.y - scroll.y; + const f32 localX = mousePos.x - origin.x - scroll.x; + const f32 indent = getIndentWidth(); + + f32 y = 0; + for (auto& itemPtr : m_list) + { + auto& item = *itemPtr; + if (!item.isValid()) continue; + if (!item.isVisible()) continue; + const f32 itemH = item.getDimensions().y; + if (localY >= y && localY < y + itemH) + { + // Chevron hit-test takes priority over selection. + if (item.hasChildren()) + { + const f32 chevronXLocal = item.getDepth() * indent; + if (localX >= chevronXLocal && localX < chevronXLocal + indent) + { + item.toggleExpanded(); + event.handle(); + return; + } + } + bool wasSelected = item.isSelected(); + item.set_selected(m_selectedList); + if (!wasSelected && callback_onSelectionChanged) + callback_onSelectionChanged(m_selectedList); + event.handle(); + break; + } + y += itemH; + } + } + + Rectangle TreeView::getContentExtents(void) const + { + if (!m_extentsDirty) + return m_cachedExtents; + f32 maxX = 0, maxY = 0; + const f32 indent = m_indentWidth; + for (auto& itemPtr : m_list) + { + auto& item = *itemPtr; + if (!item.isValid()) continue; + if (!item.isVisible()) continue; + Vec2 size = item.getDimensions(); + // Width of a row is its text/icon width plus the indent + chevron-slot space to its left. + f32 rowW = size.x + (item.getDepth() + 1) * indent; + maxX = std::max(maxX, rowW); + maxY += size.y; + } + m_cachedExtents = { 0, 0, maxX, maxY }; + m_extentsDirty = false; + return m_cachedExtents; + } + + TreeView::Item& TreeView::getLine(const String& text) + { + for (auto& itemPtr : m_list) + if (itemPtr->getText() == text) + return *itemPtr; + return InvalidItem; + } + + TreeView::Item& TreeView::getLine(u32 index) + { + if (index < m_list.size()) + return *m_list[index]; + return InvalidItem; + } + + TreeView::Item& TreeView::addRoot(const String& text) + { + // Flip the previously-last root's flag, then become the new last root. + if (m_lastRoot) m_lastRoot->m_isLastChild = false; + + auto item = std::make_unique(*this); + item->setText(text); + item->m_parentItem = nullptr; + item->m_depth = 0; + item->m_isLastChild = true; + Item* itemPtr = item.get(); + m_list.push_back(std::move(item)); + m_lastRoot = itemPtr; + + if (m_selectedList.empty()) + itemPtr->set_selected(m_selectedList); + m_extentsDirty = true; + return *itemPtr; + } + + TreeView::Item& TreeView::addRoot(const String& text, const Icon& icon) + { + auto& item = addRoot(text); + item.setIcon(icon); + return item; + } + + TreeView::Item& TreeView::addChild(Item& parentItem, const String& text) + { + // Locate the parent's slot in m_list (needed for the insertion offset). + i32 parentIdx = -1; + for (i32 i = 0; i < (i32)m_list.size(); i++) + { + if (m_list[i].get() == &parentItem) { parentIdx = i; break; } + } + if (parentIdx < 0) + return InvalidItem; + + // Insert right after the parent's last descendant so display order == deque order. + u32 insertIdx = get_last_descendant_index((u32)parentIdx) + 1; + + // The previously-last direct child (if any) loses its last-child flag. + if (parentItem.m_lastChild) + parentItem.m_lastChild->m_isLastChild = false; + + auto newItem = std::make_unique(*this); + newItem->setText(text); + newItem->m_parentItem = &parentItem; + newItem->m_depth = parentItem.m_depth + 1; + newItem->m_isLastChild = true; + Item* newPtr = newItem.get(); + + // std::deque::insert moves the pointer slots around, but the Item objects + // themselves are heap-allocated and stay put. Pointers stored on other Items + // (m_parentItem, m_lastChild) and in m_selectedList remain valid. + m_list.insert(m_list.begin() + insertIdx, std::move(newItem)); + + parentItem.m_hasChildren = true; + parentItem.m_lastChild = newPtr; + m_extentsDirty = true; + return *newPtr; + } + + TreeView::Item& TreeView::addChild(Item& parentItem, const String& text, const Icon& icon) + { + auto& item = addChild(parentItem, text); + item.setIcon(icon); + return item; + } + + u32 TreeView::get_last_descendant_index(u32 itemIndex) const + { + // Walk forward while items are deeper than the anchor; the last such index is the answer. + if (itemIndex >= m_list.size()) return itemIndex; + const u32 anchorDepth = m_list[itemIndex]->m_depth; + u32 last = itemIndex; + for (u32 i = itemIndex + 1; i < m_list.size(); i++) + { + if (m_list[i]->m_depth > anchorDepth) + last = i; + else + break; + } + return last; + } + + void TreeView::recompute_has_children_and_last_sibling(Item* parentItem) + { + // Recomputes m_hasChildren on parentItem (or m_lastRoot if null) and + // m_isLastChild flags + cached last-child pointer for the children of that parent. + Item* lastSeen = nullptr; + bool any = false; + for (auto& itPtr : m_list) + { + auto& it = *itPtr; + if (!it.isValid()) continue; + if (it.m_parentItem == parentItem) + { + it.m_isLastChild = false; + lastSeen = ⁢ + any = true; + } + } + if (lastSeen) lastSeen->m_isLastChild = true; + if (parentItem) + { + parentItem->m_hasChildren = any; + parentItem->m_lastChild = lastSeen; + } + else + { + m_lastRoot = lastSeen; + } + } + + bool TreeView::removeLine(const String& text) + { + i32 idx = -1; + for (i32 i = 0; i < (i32)m_list.size(); i++) + { + if (m_list[i]->getText() == text) { idx = i; break; } + } + if (idx < 0) return false; + return removeLine((u32)idx); + } + + bool TreeView::removeLine(u32 index) + { + if (!hasLine(index)) + return false; + + Item* removedParent = m_list[index]->m_parentItem; + // Remove the item plus its entire descendant range. + u32 lastDesc = get_last_descendant_index(index); + // Drop any selection pointers that fall inside the removed range. + for (u32 i = index; i <= lastDesc; i++) + { + Item* p = m_list[i].get(); + STDVEC_REMOVE(m_selectedList, p); + } + m_list.erase(m_list.begin() + index, m_list.begin() + lastDesc + 1); + + // Fix up parent flags / sibling last-child markers. + recompute_has_children_and_last_sibling(removedParent); + m_extentsDirty = true; + return true; + } + + bool TreeView::hasLine(const String& text) + { + for (auto& itemPtr : m_list) + if (itemPtr->getText() == text) + return true; + return false; + } + + bool TreeView::hasLine(u32 index) + { + return m_list.size() > index; + } + + void TreeView::render_chevron(ogfx::BasicRenderer2D& gfx, const Item& item, const Rectangle& bounds) + { + // Default chevron: a small triangle. Right-pointing when collapsed, down-pointing when expanded. + const f32 inset = getChevronInset(); + const f32 cx = bounds.x + bounds.w * 0.5f; + const f32 cy = bounds.y + bounds.h * 0.5f; + // Half-extents inside the slot. + const f32 hx = std::max(2.0f, bounds.w * 0.5f - inset); + const f32 hy = std::max(2.0f, bounds.h * 0.25f); + Vec2 p0, p1, p2; + if (item.isExpanded()) + { + // Down-pointing: ▼ + p0 = { cx - hx, cy - hy }; + p1 = { cx + hx, cy - hy }; + p2 = { cx, cy + hy }; + } + else + { + // Right-pointing: ▶ + p0 = { cx - hy, cy - hx }; + p1 = { cx - hy, cy + hx }; + p2 = { cx + hy, cy }; + } + gfx.fillTriangle(p0, p1, p2, getChevronColor()); + } + + void TreeView::render_branch_lines(ogfx::BasicRenderer2D& gfx, const Item& item, const Rectangle& rowRect) + { + const f32 indent = getIndentWidth(); + const f32 thick = getBranchLineThickness(); + const Color col = getBranchLineColor(); + const u32 d = item.getDepth(); + + if (d == 0) return; // roots have no parent line / no elbow + + // Collect ancestor "isLastChild" flags. ancestorIsLast[0] = immediate parent (depth d-1), + // ancestorIsLast[k] = ancestor at depth (d-1-k). + std::vector ancestorIsLast; + ancestorIsLast.reserve(d); + const Item* p = item.getParentItem(); + while (p != nullptr) + { + ancestorIsLast.push_back(p->isLastChild()); + p = p->getParentItem(); + } + + // For each "passing-through" column a in [0, d-1): draw a vertical line spanning the + // full row IF the ancestor at depth (a+1) is NOT the last child of its parent. + // (Column 'a' carries the sibling line for depth a+1; if the depth-a+1 ancestor has + // no later sibling, the line ended above us and shouldn't be drawn here.) + // The column for our own row (a == d-1) is handled by the elbow logic below. + for (u32 a = 0; a + 1 < d; a++) + { + const u32 ancestorDepth = a + 1; + const u32 ancestorIdx = d - 1 - ancestorDepth; // index into ancestorIsLast + if (ancestorIsLast[ancestorIdx]) continue; // no continuing line in this column + const f32 colX = rowRect.x + a * indent + indent * 0.5f; + gfx.drawLine({ Vec2 { colX, rowRect.y }, Vec2 { colX, rowRect.y + rowRect.h } }, col, thick); + } + + // Elbow in column d-1 connecting our row to the chevron slot at column d. + const f32 colX = rowRect.x + (d - 1) * indent + indent * 0.5f; + const f32 midY = rowRect.y + rowRect.h * 0.5f; + // If this item has no chevron drawn, extend the horizontal arm all the way to where + // the icon/text starts (the right edge of the chevron slot, i.e. column d+1's left). + // Otherwise stop at the left edge of the chevron slot so the chevron sits clean. + const f32 armEndX = item.hasChildren() + ? rowRect.x + d * indent + : rowRect.x + (d + 1) * indent; + + // Vertical: top -> middle of the row (always) + gfx.drawLine({ Vec2 { colX, rowRect.y }, Vec2 { colX, midY } }, col, thick); + // Vertical: middle -> bottom (only if there is a sibling below us at this depth) + if (!item.isLastChild()) + gfx.drawLine({ Vec2 { colX, midY }, Vec2 { colX, rowRect.y + rowRect.h } }, col, thick); + // Horizontal arm + gfx.drawLine({ Vec2 { colX, midY }, Vec2 { armEndX, midY } }, col, thick); + } + } +} diff --git a/src/ogfx/gui/widgets/TreeView.hpp b/src/ogfx/gui/widgets/TreeView.hpp new file mode 100644 index 0000000..dd0a9f2 --- /dev/null +++ b/src/ogfx/gui/widgets/TreeView.hpp @@ -0,0 +1,161 @@ +/* + 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 +#include +#include + +namespace ogfx +{ + namespace gui + { + class TreeView : public ScrollableWidget + { + public: class Item : public ostd::__i_stringeable + { + public: + inline Item(TreeView& parent) { m_treeView = &parent; } + void setText(const String& text); + void setIcon(const Icon& icon); + inline Animation& getIcon(void) { return m_icon; } + + inline String toString(void) const override { return m_text; } + inline bool isValid(void) const { return m_treeView != nullptr && m_text.new_trim() != ""; } + inline Vec2 getDimensions(void) const { return m_dimensions; } + inline String getText(void) const { return m_text; } + inline bool isSelected(void) const { return m_selected; } + + inline bool isExpanded(void) const { return m_expanded; } + inline bool hasChildren(void) const { return m_hasChildren; } + inline u32 getDepth(void) const { return m_depth; } + inline Item* getParentItem(void) { return m_parentItem; } + inline const Item* getParentItem(void) const { return m_parentItem; } + inline bool isLastChild(void) const { return m_isLastChild; } + void setExpanded(bool expanded); + inline void toggleExpanded(void) { setExpanded(!m_expanded); } + bool isVisible(void) const; + stdvec getFullPath(void) const; + + OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon); + + private: + void update_dimensions(void); + void set_selected(stdvec& selectionList); + void add_selected(stdvec& selectionList); + void remove_selected(stdvec& selectionList); + + private: + Vec2 m_dimensions { 0, 0 }; + bool m_selected { false }; + TreeView* m_treeView { nullptr }; + String m_text { "" }; + Animation m_icon; + bool m_showIcon { false }; + + // Tree state + Item* m_parentItem { nullptr }; + Item* m_lastChild { nullptr }; + u32 m_depth { 0 }; + bool m_expanded { true }; + bool m_hasChildren { false }; + bool m_isLastChild { true }; + + friend class TreeView; + }; + public: + using ChevronDrawCallback = std::function; + + inline TreeView(WindowCore& window) : ScrollableWidget(window) { create(); } + TreeView& create(void); + void applyTheme(const ostd::Stylesheet& theme) override; + void onDraw(ogfx::BasicRenderer2D& gfx) override; + void afterDraw(ogfx::BasicRenderer2D& gfx) override; + void onMouseReleased(const Event& event) override; + Rectangle getContentExtents(void) const override; + Item& getLine(const String& text); + Item& getLine(u32 index); + Item& addRoot(const String& text); + Item& addRoot(const String& text, const Icon& icon); + Item& addChild(Item& parentItem, const String& text); + Item& addChild(Item& parentItem, const String& text, const Icon& icon); + bool removeLine(const String& text); + bool removeLine(u32 index); + bool hasLine(const String& text); + bool hasLine(u32 index); + inline stdvec& getSelection(void) { return m_selectedList; } + inline void setSelectionChangedCallback(std::function& selection)> callback) { callback_onSelectionChanged = std::move(callback); } + inline void setChevronDrawCallback(ChevronDrawCallback cb) { m_chevronDrawCallback = std::move(cb); } + + + OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor); + OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine); + OSTD_BOOL_PARAM_GETSET_E(Icons, m_showIcons); + OSTD_PARAM_GETSET(Rectangle, LinePadding, m_linePadding); + OSTD_PARAM_GETSET(Color, LineSelectionColor, m_selectionColor); + OSTD_PARAM_GETSET(Color, LineSelectionTextColor, m_selectionTextColor); + OSTD_PARAM_GETSET(Color, IconTintColor, m_iconTintColor); + OSTD_PARAM_GETSET(f32, IconSpacing, m_iconSpacing); + OSTD_PARAM_GETSET(f32, IconInset, m_iconInset); + OSTD_PARAM_GETSET(f32, IndentWidth, m_indentWidth); + OSTD_PARAM_GETSET(Color, ChevronColor, m_chevronColor); + OSTD_PARAM_GETSET(f32, ChevronInset, m_chevronInset); + OSTD_PARAM_GETSET(Color, BranchLineColor, m_branchLineColor); + OSTD_BOOL_PARAM_GETSET_E(BranchLines, m_showBranchLines); + OSTD_PARAM_GETSET(f32, BranchLineThickness, m_branchLineThickness); + + private: + void render_chevron(ogfx::BasicRenderer2D& gfx, const Item& item, const Rectangle& bounds); + void render_branch_lines(ogfx::BasicRenderer2D& gfx, const Item& item, const Rectangle& rowRect); + u32 get_last_descendant_index(u32 itemIndex) const; + void recompute_has_children_and_last_sibling(Item* parentItem); + + private: + Item InvalidItem { *this }; + std::deque> m_list; + stdvec m_selectedList; + std::function& selection)> callback_onSelectionChanged { nullptr }; + ChevronDrawCallback m_chevronDrawCallback { nullptr }; + mutable Rectangle m_cachedExtents { 0, 0, 0, 0 }; + mutable bool m_extentsDirty { true }; + Item* m_lastRoot { nullptr }; + + bool m_showIcons { false }; + f32 m_iconSpacing { 0 }; + f32 m_iconInset { 4 }; + Color m_lineColor { 40, 40, 40 }; + bool m_showLine { true }; + Rectangle m_linePadding { 5, 5, 20, 0 }; + Color m_selectionColor { Colors::Crimson }; + Color m_selectionTextColor{ Colors::White }; + Color m_iconTintColor { Colors::White }; + + f32 m_indentWidth { 16 }; + Color m_chevronColor { Colors::White }; + f32 m_chevronInset { 4 }; + Color m_branchLineColor { 80, 80, 80 }; + bool m_showBranchLines { true }; + f32 m_branchLineThickness { 1 }; + }; + } +} diff --git a/src/ogfx/utils/Animation.hpp b/src/ogfx/utils/Animation.hpp index 11f4f13..04994de 100644 --- a/src/ogfx/utils/Animation.hpp +++ b/src/ogfx/utils/Animation.hpp @@ -93,4 +93,14 @@ namespace ogfx bool m_back { false }; Rectangle m_frameRect { 0, 0, 0, 0 }; }; + + struct Icon + { + Image& sheet; + AnimationData info; + Animation asAnim(void) const { return { info, sheet }; } + AnimationData asAnimData(void) const { return info; } + inline operator Animation() const { return asAnim(); } + inline operator AnimationData() const { return asAnimData(); } + }; } diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index 9755b67..ca1a106 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -141,30 +141,57 @@ class TestWindow : public Window m_list.setSize(200, 300); m_list.reloadTheme(); - for (i32 i = 0; i < 10000; i++) + // for (i32 i = 0; i < 10000; i++) + // { + // m_list.addLine(ostd::Random::getString(ostd::Random::getui8(1, 40))); + // } + // m_list.addLine("Item 1"); + // m_list.addLine("Item 222"); + // m_list.addLine("Item 3333333"); + // m_list.setSelectionChangedCallback([&](stdvec& selection) -> void {\ + // std::cout << *(selection[0]) << "\n"; + // }); + + auto l_randomIcon = [&](const ogfx::AnimationData& src) -> ogfx::Icon { + static ogfx::Image img("./icons.png", getGFX()); + ogfx::AnimationData ad = src; + ad.columnOffset = ostd::Random::getui32(0, 7); + ad.rowOffset = ostd::Random::getui32(0, 4); + if (ad.rowOffset == 3) + ad.columnOffset = ostd::Random::getui32(0, 4); + return { img, ad }; + }; + + + i32 listLen = 20; + for (i32 i = 0; i < listLen; i++) { - m_list.addLine(ostd::Random::getString(ostd::Random::getui8(1, 40))); + auto& item = m_list.addRoot(String("Line ").add(i + 1), l_randomIcon(iconsAD)); } - m_list.addLine("Item 1"); - m_list.addLine("Item 222"); - m_list.addLine("Item 3333333"); - m_list.getLine(10).setFontSize(40); - m_list.getLine(160).setTextColor(Colors::Crimson); - m_list.setSelectionChangedCallback([&](stdvec& selection) -> void {\ - std::cout << *(selection[0]) << "\n"; + + for (i32 i = 0; i < 30; i++) + { + i32 index = ostd::Random::geti32(0, listLen - 1); + auto& item = m_list.getLine(index); + auto& child = m_list.addChild(item, String("Child ").add(i + 1), l_randomIcon(iconsAD)); + } + m_list.setSelectionChangedCallback([&](stdvec& selection) -> void { + auto path = (selection[0])->getFullPath(); + for (i32 i = 0; i < path.size(); i++) + { + std::cout << path[i]; + if (i < path.size() - 1) + std::cout << " -> "; + } + std::cout << "\n"; }); for (i32 i = 0; i < 20; i++) { - iconsAD.columnOffset = ostd::Random::getui32(0, 7); - iconsAD.rowOffset = ostd::Random::getui32(0, 4); - - if (iconsAD.rowOffset == 3) - iconsAD.columnOffset = ostd::Random::getui32(0, 4); - + auto icon = l_randomIcon(iconsAD); auto& btn = getToolBar().addButton("./icons.png"); btn.enableAnimated(); - btn.setAnimationData(iconsAD); + btn.setAnimationData(icon); } m_tabs.setSize(900, 700); @@ -194,7 +221,7 @@ class TestWindow : public Window auto* header = new Button(*this); header->layoutHint().preferred = { -1, 32 }; // fixed 32px height, width follows cross-stretch - auto* body = new ListView(*this); + auto* body = new TreeView(*this); body->layoutHint().stretch = 1.0f; // takes all leftover vertical space auto* footer = new Button(*this); @@ -214,7 +241,7 @@ class TestWindow : public Window t2.addWidget(*body); for (i32 i = 0; i < 100; i++) { - body->addLine(ostd::Random::getString(ostd::Random::getui8(1, 40))); + body->addRoot(ostd::Random::getString(ostd::Random::getui8(1, 40))); } t2.addWidget(*footer); @@ -333,7 +360,7 @@ class TestWindow : public Window ProgressBar m_prog { *this, 0, 100 }; Slider m_slide { *this }; Label m_slideLbl { *this }; - ListView m_list { *this }; + TreeView m_list { *this }; Label m_drawCallsLbl { *this }; ComboBox m_combo { *this };