Added TreeView widget
This commit is contained in:
parent
2de45908a9
commit
796b03f08c
11 changed files with 832 additions and 453 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
% ======================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -28,5 +28,13 @@
|
|||
#include <ogfx/gui/widgets/RadioButton.hpp>
|
||||
#include <ogfx/gui/widgets/ProgressBar.hpp>
|
||||
#include <ogfx/gui/widgets/Slider.hpp>
|
||||
#include <ogfx/gui/widgets/ListView.hpp>
|
||||
#include <ogfx/gui/widgets/ComboBox.hpp>
|
||||
#include <ogfx/gui/widgets/TreeView.hpp>
|
||||
|
||||
namespace ogfx
|
||||
{
|
||||
namespace gui
|
||||
{
|
||||
using ListView = TreeView;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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<Item*>& selectionList)
|
||||
{
|
||||
for (auto& sel : selectionList)
|
||||
sel->m_selected = false;
|
||||
selectionList.clear();
|
||||
selectionList.push_back(this);
|
||||
m_selected = true;
|
||||
}
|
||||
|
||||
void ListView::Item::add_selected(stdvec<Item*>& selectionList)
|
||||
{
|
||||
if (STDVEC_CONTAINS(selectionList, this))
|
||||
return;
|
||||
selectionList.push_back(this);
|
||||
m_selected = true;
|
||||
}
|
||||
|
||||
void ListView::Item::remove_selected(stdvec<Item*>& 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<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
|
||||
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
|
||||
setDefaultLineTextColor(getThemeValue<Color>(theme, "item.defaultTextColor", getDefaultLineTextColor()));
|
||||
setDefaultLineFontSize(getThemeValue<i32>(theme, "item.defaultFontSize", getDefaultLineFontSize()));
|
||||
setDefaultLinePadding(getThemeValue<Rectangle>(theme, "item.defaultPadding", getDefaultLinePadding()));
|
||||
setDefaultLineSelectionColor(getThemeValue<Color>(theme, "item.defaultSelectionColor", getDefaultLineSelectionColor()));
|
||||
setDefaultLineSelectionTextColor(getThemeValue<Color>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ogfx/gui/widgets/Widget.hpp>
|
||||
#include <ogfx/gui/widgets/Scrollbar.hpp>
|
||||
#include <deque>
|
||||
|
||||
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<Item*>& selectionList);
|
||||
void add_selected(stdvec<Item*>& selectionList);
|
||||
void remove_selected(stdvec<Item*>& 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<Item*>& getSelection(void) { return m_selectedList; }
|
||||
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& 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<Item> m_list;
|
||||
stdvec<Item*> m_selectedList;
|
||||
std::function<void(stdvec<Item*>& 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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
573
src/ogfx/gui/widgets/TreeView.cpp
Normal file
573
src/ogfx/gui/widgets/TreeView.cpp
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TreeView.hpp"
|
||||
#include "../../render/BasicRenderer.hpp"
|
||||
#include "../Window.hpp"
|
||||
#include "../../../ostd/io/Memory.hpp"
|
||||
#include <algorithm>
|
||||
|
||||
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<Item*>& selectionList)
|
||||
{
|
||||
for (auto& sel : selectionList)
|
||||
sel->m_selected = false;
|
||||
selectionList.clear();
|
||||
selectionList.push_back(this);
|
||||
m_selected = true;
|
||||
}
|
||||
|
||||
void TreeView::Item::add_selected(stdvec<Item*>& selectionList)
|
||||
{
|
||||
if (STDVEC_CONTAINS(selectionList, this))
|
||||
return;
|
||||
selectionList.push_back(this);
|
||||
m_selected = true;
|
||||
}
|
||||
|
||||
void TreeView::Item::remove_selected(stdvec<Item*>& 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<String> 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<String> 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<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
|
||||
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
|
||||
setLinePadding(getThemeValue<Rectangle>(theme, "linePadding", getLinePadding()));
|
||||
setLineSelectionColor(getThemeValue<Color>(theme, "selectionColor", getLineSelectionColor()));
|
||||
setLineSelectionTextColor(getThemeValue<Color>(theme, "selectionTextColor", getLineSelectionTextColor()));
|
||||
setIconSpacing(getThemeValue<f32>(theme, "iconSpacing", getIconSpacing()));
|
||||
setIconInset(getThemeValue<f32>(theme, "iconInset", getIconInset()));
|
||||
enableIcons(getThemeValue<bool>(theme, "showIcons", isIconsEnabled()));
|
||||
setIconTintColor(getThemeValue<Color>(theme, "iconTintColor", getIconTintColor()));
|
||||
setIndentWidth(getThemeValue<f32>(theme, "indentWidth", getIndentWidth()));
|
||||
setChevronColor(getThemeValue<Color>(theme, "chevronColor", getChevronColor()));
|
||||
setChevronInset(getThemeValue<f32>(theme, "chevronInset", getChevronInset()));
|
||||
setBranchLineColor(getThemeValue<Color>(theme, "branchLineColor", getBranchLineColor()));
|
||||
enableBranchLines(getThemeValue<bool>(theme, "showBranchLines", isBranchLinesEnabled()));
|
||||
setBranchLineThickness(getThemeValue<f32>(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<Item>(*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<Item>(*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<bool> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/ogfx/gui/widgets/TreeView.hpp
Normal file
161
src/ogfx/gui/widgets/TreeView.hpp
Normal file
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ogfx/gui/widgets/Widget.hpp>
|
||||
#include <ogfx/gui/widgets/Scrollbar.hpp>
|
||||
#include <ogfx/utils/Animation.hpp>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
|
||||
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<String> getFullPath(void) const;
|
||||
|
||||
OSTD_BOOL_PARAM_GETSET_E(Icon, m_showIcon);
|
||||
|
||||
private:
|
||||
void update_dimensions(void);
|
||||
void set_selected(stdvec<Item*>& selectionList);
|
||||
void add_selected(stdvec<Item*>& selectionList);
|
||||
void remove_selected(stdvec<Item*>& 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<void(TreeView& sender, const Item& item, const Rectangle& bounds, BasicRenderer2D& gfx)>;
|
||||
|
||||
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<Item*>& getSelection(void) { return m_selectedList; }
|
||||
inline void setSelectionChangedCallback(std::function<void(stdvec<Item*>& 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<std::unique_ptr<Item>> m_list;
|
||||
stdvec<Item*> m_selectedList;
|
||||
std::function<void(stdvec<Item*>& 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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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(); }
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TreeView::Item*>& 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<ListView::Item*>& 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<TreeView::Item*>& 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 };
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue