Various bugfixes

This commit is contained in:
OmniaX-Dev 2026-05-01 21:17:23 +02:00
parent 796b03f08c
commit 16e78d07b1
12 changed files with 286 additions and 73 deletions

View file

@ -39,7 +39,7 @@ const $backgroundColor = rgba(30, 30, 30, 255)
showBorder = true
showBackground = true
titlebarType = full
titlebarHeight = 18
titlebarHeight = 35
titlebarFontSize = 18
titlebarTextAlign = text_left
scrollSpeed = 1.2
@ -362,18 +362,20 @@ const $backgroundColor = rgba(30, 30, 30, 255)
% ====== TreeView ======
(tree) {
linePadding = rect(5, 5, 20, 0)
linePadding = rect(5, 5, 0, 0)
selectionColor = $accentColor
selectionTextColor = color_white
iconSpacing = 0.0
iconInset = 4.0
iconInset = 4
showIcons = true
fontSize = 16
textColor = $textColor
backgroundColor = $darkColor
backgroundColor2 = $darkColor.lighten(0.06)
showAlternatingBackground = true
borderColor = $borderColor
separatorLineColor = $borderColor
showSeparatorLine = true
showSeparatorLine = false
iconTintColor = $accentColorLight
indentWidth = 16.0
chevronColor = $accentColorDark

View file

@ -23,12 +23,12 @@
***Add Dark Mode Default theme
***FIX: Optimize ListView
***FIX: Limit FPS even when continuous events happen
***FIX: Float values in stylesheet needing to have decimal point to be read as float
***Make parsing of vec2 and rect in stylesheet, consistent with other value functions like gradients and animations
Implement cursors in Stylesheet
FIX: Window getting exponentially bigger when Desktop scale changes
FIX: Refreshing scroll when content extent changes
See if I can query OS theme mode to saelect the right theme
FIX: Float values in stylesheet needing to have decimal point to be read as float
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 TreeView
@ -51,6 +51,7 @@ 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
Redesign theme overrides to be more robust

View file

@ -135,6 +135,8 @@ namespace ogfx
void TreeView::applyTheme(const ostd::Stylesheet& theme)
{
enableAlternatingBackground(getThemeValue<bool>(theme, "showAlternatingBackground", isAlternatingBackgroundEnabled()));
setBackgroundColor2(getThemeValue<Color>(theme, "backgroundColor2", getBackgroundColor2()));
setSeparatorLineColor(getThemeValue<Color>(theme, "separatorLineColor", getSeparatorLineColor()));
enableShowSeparatorLine(getThemeValue<bool>(theme, "showSeparatorLine", isShowSeparatorLineEnabled()));
setLinePadding(getThemeValue<Rectangle>(theme, "linePadding", getLinePadding()));
@ -165,6 +167,7 @@ namespace ogfx
// Walk forward through visible items only, advancing y for each one we draw.
f32 y = 0;
i32 startIdx = 0;
u32 visibleRow = 0; // counts only items whose row is actually drawn
for (auto& itemPtr : m_list)
{
auto& item = *itemPtr;
@ -172,6 +175,7 @@ namespace ogfx
f32 itemH = item.getDimensions().y;
if (y + itemH > visibleStart) break;
y += itemH;
visibleRow++;
startIdx++;
}
@ -186,14 +190,21 @@ namespace ogfx
if (content.w < bounds.w)
lineW = bounds.w;
auto pad = getLinePadding();
Rectangle lineRect = { Vec2 { bounds.x, bounds.y + y + pad.h } + getScrollOffset(), { lineW, itemH } };
Rectangle lineRect = { Vec2 { bounds.x, bounds.y + y + pad.right() } + getScrollOffset(), { lineW, itemH } };
if (item.isSelected())
{
textColor = getLineSelectionTextColor();
gfx.fillRect(lineRect, getLineSelectionColor());
}
else
{
textColor = getTextColor();
if (isAlternatingBackgroundEnabled())
{
bool lineColorToggle = visibleRow % 2 == 0;
gfx.fillRect(lineRect, (lineColorToggle ? getBackgroundColor() : getBackgroundColor2()));
}
}
// Branch lines (drawn under everything else for this row).
if (isBranchLinesEnabled())
@ -215,7 +226,7 @@ namespace ogfx
};
if (m_chevronDrawCallback)
{
gfx.pushClippingRect(chevronBounds);
gfx.pushClippingRect(chevronBounds, true);
m_chevronDrawCallback(*this, item, chevronBounds, gfx);
gfx.popClippingRect();
}
@ -234,9 +245,43 @@ namespace ogfx
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);
if (isShowSeparatorLineEnabled())
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;
visibleRow++;
}
}
void TreeView::onUpdate(void)
{
ScrollableWidget::onUpdate();
const f32 scrollY = -getScrollOffset().y;
const f32 visibleH = getContentBounds().h;
const f32 visibleStart = scrollY;
const f32 visibleEnd = scrollY + visibleH;
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;
if (!item.isIconEnabled()) continue;
if (y > visibleEnd) break;
if (item.getIcon().getFrameCount() != 1)
std::cout << item.getIcon().getCurrentFrame() << "\n";
item.getIcon().update();
}
}
@ -490,6 +535,26 @@ namespace ogfx
return m_list.size() > index;
}
void TreeView::expandAll(void)
{
for (auto& itemPtr : m_list)
{
if (itemPtr->m_hasChildren && !itemPtr->m_expanded)
itemPtr->m_expanded = true;
}
m_extentsDirty = true;
}
void TreeView::collapseAll(void)
{
for (auto& itemPtr : m_list)
{
if (itemPtr->m_hasChildren && itemPtr->m_expanded)
itemPtr->m_expanded = false;
}
m_extentsDirty = true;
}
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.

View file

@ -90,6 +90,7 @@ namespace ogfx
TreeView& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onUpdate(void) override;
void afterDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
Rectangle getContentExtents(void) const override;
@ -103,11 +104,15 @@ namespace ogfx
bool removeLine(u32 index);
bool hasLine(const String& text);
bool hasLine(u32 index);
void expandAll(void);
void collapseAll(void);
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, BackgroundColor2, m_backgroundColor2);
OSTD_BOOL_PARAM_GETSET_E(AlternatingBackground, m_showAlternatingBackground);
OSTD_PARAM_GETSET(Color, SeparatorLineColor, m_lineColor);
OSTD_BOOL_PARAM_GETSET_E(ShowSeparatorLine, m_showLine);
OSTD_BOOL_PARAM_GETSET_E(Icons, m_showIcons);
@ -149,6 +154,8 @@ namespace ogfx
Color m_selectionColor { Colors::Crimson };
Color m_selectionTextColor{ Colors::White };
Color m_iconTintColor { Colors::White };
Color m_backgroundColor2 { Colors::Black };
bool m_showAlternatingBackground { true };
f32 m_indentWidth { 16 };
Color m_chevronColor { Colors::White };

View file

@ -77,6 +77,7 @@ namespace ogfx
else
m_currentFrame = 0;
}
if (m_animData.columns <= 0) return;
i32 col = m_currentFrame % m_animData.columns;
i32 row = (m_animData.rows > 1) ? cast<i32>(m_currentFrame / m_animData.columns) : 0;

View file

@ -36,6 +36,7 @@ namespace ogfx
inline Animation(const AnimationData& ad, Image& spriteSheet) { create(ad, spriteSheet); };
inline Animation& create(const AnimationData& ad) { return create(ad, InvalidImage); }
inline void update(void) { m_timer.update(); }
inline i32 getCurrentFrame(void) const { return m_currentFrame; }
Animation& create(const AnimationData& ad, Image& spriteSheet);
void resetAnimation(void);

View file

@ -8,35 +8,35 @@ namespace ostd
Color::Color(void) : r(*this), g(*this), b(*this), a(*this)
{
set();
setTypeName("ox::Color");
setTypeName("ostd::Color");
BaseObject::setValid(true);
}
Color::Color(u8 rgb_single_value, u8 alpha) : r(*this), g(*this), b(*this), a(*this)
{
set(rgb_single_value, alpha);
setTypeName("ox::Color");
setTypeName("ostd::Color");
BaseObject::setValid(true);
}
Color::Color(u8 _r, u8 _g, u8 _b, u8 alpha) : r(*this), g(*this), b(*this), a(*this)
{
set(_r, _g, _b, alpha);
setTypeName("ox::Color");
setTypeName("ostd::Color");
BaseObject::setValid(true);
}
Color::Color(const String& color_string) : r(*this), g(*this), b(*this), a(*this)
{
set(color_string);
setTypeName("ox::Color");
setTypeName("ostd::Color");
BaseObject::setValid(true);
}
Color::Color(const FloatCol& normalized_color) : r(*this), g(*this), b(*this), a(*this)
{
set(normalized_color);
setTypeName("ox::Color");
setTypeName("ostd::Color");
BaseObject::setValid(true);
}
@ -99,37 +99,88 @@ namespace ostd
{
String se(color_string);
se.trim();
// --- Step 1: Extract optional .lighten(x) or .darken(x) suffix ---
enum class Modifier { None, Lighten, Darken };
Modifier mod = Modifier::None;
f32 modAmount = 0.0f;
{
stdvec<String> matches;
auto positions = se.regexFind("\\.(lighten|darken)\\(\\s*([0-9]*\\.?[0-9]+)\\s*\\)\\s*$", true, &matches);
if (!positions.empty())
{
const String& whole = matches[0];
mod = whole.contains("lighten") ? Modifier::Lighten : Modifier::Darken;
// Extract the number between '(' and ')'
u32 openParen = whole.indexOf("(");
u32 closeParen = whole.indexOf(")");
String numStr = whole.new_substr(openParen + 1, closeParen);
numStr.trim();
modAmount = numStr.toFloat();
// Clamp to [0, 1]
if (modAmount < 0.0f) modAmount = 0.0f;
if (modAmount > 1.0f) modAmount = 1.0f;
// Strip the suffix from se
se = se.substr(0, positions[0]);
se.trim();
}
}
// --- Step 2: Default values ---
r = 0;
g = 0;
b = 0;
a = 255;
// --- Step 3: Normalize "#" prefix to "0x" ---
if (se.startsWith("#"))
{
String tmp = se.new_substr(1);
tmp.trim();
se = String("0x") + tmp;
}
// --- Step 4: Parse the color body ---
if (se.startsWith("0x"))
{
String hexPart = se.new_substr(2);
hexPart.trim();
u32 hexDigits = hexPart.len();
i64 ic = se.toInt();
union uC32 {
u8 data[4];
u32 value;
} c32_u;
c32_u.value = cast<u32>(ic);
a = c32_u.data[0];
b = c32_u.data[1];
g = c32_u.data[2];
r = c32_u.data[3];
u32 c = static_cast<u32>(ic);
if (hexDigits <= 6)
{
// RGB only — alpha defaults to 255
r = (c >> 16) & 0xFF;
g = (c >> 8) & 0xFF;
b = (c >> 0) & 0xFF;
a = 255;
}
else
{
// RGBA — 7 or 8 digits
r = (c >> 24) & 0xFF;
g = (c >> 16) & 0xFF;
b = (c >> 8) & 0xFF;
a = (c >> 0) & 0xFF;
}
}
else if ((se.startsWith("(") || se.startsWith("rgba(") || se.startsWith("rgb(")) && se.endsWith(")") && se.contains(","))
else if ((se.startsWith("(") || se.startsWith("rgba(") || se.startsWith("rgb("))
&& se.endsWith(")") && se.contains(","))
{
se = se.substr(se.indexOf("(") + 1, se.len() - 1);
se.trim();
auto tokens = se.tokenize(",", true, false);
if (tokens.count() < 3 || tokens.count() > 4)
{
OX_WARN("ox::Color::set(const String&) -> Invalid rgb string format: %s.", color_string.c_str());
OX_WARN("ostd::Color::set(const String&) -> Invalid rgb string format: %s.", color_string.c_str());
return *this;
}
r = tokens.next().toInt();
@ -140,8 +191,16 @@ namespace ostd
}
else
{
OX_WARN("ox::Color::set(const String&) -> Unkown color string format: %s", color_string.c_str());
OX_WARN("ostd::Color::set(const String&) -> Unknown color string format: %s", color_string.c_str());
return *this;
}
// --- Step 5: Apply lighten/darken modifier ---
if (mod == Modifier::Lighten)
lighten(modAmount);
else if (mod == Modifier::Darken)
darken(modAmount);
return *this;
}

View file

@ -257,20 +257,38 @@ namespace ostd
if (key == "")
return false;
String themeID = "";
auto l_parseColor = [this](const String& _value) -> String {
String value = _value.new_toLower().trim();
if (value.startsWith("color(") && value.endsWith(")"))
auto l_isVec2 = [this](String& value) -> bool {
value.trim();
if (value.new_toLower().startsWith("vec2(") && value.new_toLower().endsWith(")"))
{
value.substr(5, value.len() - 1).trim();
return true;
}
return false;
};
auto l_isRect = [this](String& value) -> bool {
value.trim();
if (value.new_toLower().startsWith("rect(") && value.new_toLower().endsWith(")"))
{
value.substr(5, value.len() - 1).trim();
return true;
}
return false;
};
auto l_isColor = [this](String& value) -> bool {
value.trim();
if (value.new_toLower().startsWith("color(") && value.new_toLower().endsWith(")"))
{
value.substr(6, value.len() - 1).trim();
return value;
return true;
}
else if ((value.startsWith("#") && (value.len() == 7 || value.len() == 9)) ||
(value.startsWith("rgb(") && value.endsWith(")")) ||
(value.startsWith("rgba(") && value.endsWith(")")))
else if ((value.new_toLower().startsWith("#")) ||
(value.new_toLower().startsWith("rgb(")) ||
(value.new_toLower().startsWith("rgba(")))
{
return value;
return true;
}
return "";
return false;
};
auto l_isColorGradientValue = [this](const String& _value) -> bool {
String value = _value.new_toLower().trim();
@ -314,10 +332,10 @@ namespace ostd
themeID = key.new_substr(1, key.indexOf(" ")).trim();
key.substr(key.indexOf(" ") + 1).trim();
}
if (value.isInt())
set(key, cast<i32>(value.toInt()), themeID);
else if (value.isNumeric(true))
if (value.isNumeric(true))
set(key, value.toFloat(), themeID);
else if (value.isInt())
set(key, cast<i32>(value.toInt()), themeID);
else if (value == "true" || value == "false")
set(key, value == "true", themeID);
else if (value.startsWith("\"") && value.endsWith("\""))
@ -325,9 +343,9 @@ namespace ostd
valuePreserveCase.substr(1, value.len() - 1);
set(key, valuePreserveCase, themeID);
}
else if (String v = l_parseColor(value); v != "")
else if (l_isColor(value))
{
set(key, Color(v), themeID);
set(key, parseColor(value, variables), themeID);
}
else if (l_isColorGradientValue(value))
{
@ -353,35 +371,13 @@ namespace ostd
else
return false;
}
else if (value.startsWith("vec2(") && value.endsWith(")"))
else if (l_isVec2(value))
{
value.substr(5, value.len() - 1).trim();
auto tokens = value.tokenize(",");
if (tokens.count() != 2)
return false;
stdvec<f32> vec;
for (const auto& tok : tokens)
{
if (!tok.isNumeric(true))
return false;
vec.push_back(tok.toFloat());
}
set(key, Vec2(vec[0], vec[1]), themeID);
set(key, parseVec2(value, variables), themeID);
}
else if (value.startsWith("rect(") && value.endsWith(")"))
else if (l_isRect(value))
{
value.substr(5, value.len() - 1).trim();
auto tokens = value.tokenize(",");
if (tokens.count() != 4)
return false;
stdvec<f32> vec;
for (const auto& tok : tokens)
{
if (!tok.isNumeric(true))
return false;
vec.push_back(tok.toFloat());
}
set(key, Rectangle(vec[0], vec[1], vec[2], vec[3]), themeID);
set(key, parseRect(value, variables), themeID);
}
else if (exitCondition)
{
@ -464,6 +460,44 @@ namespace ostd
return newLines;
}
Color Stylesheet::parseColor(const String& _value, const VariableList& variables)
{
String value = replaceVariables(_value, variables);
return Color(value);
}
Vec2 Stylesheet::parseVec2(const String& _value, const VariableList& variables)
{
String value = replaceVariables(_value, variables);
auto tokens = value.tokenize(",");
if (tokens.count() != 2)
return { 0, 0 };
stdvec<f32> vec;
for (const auto& tok : tokens)
{
if (!tok.isNumeric(true))
return { 0, 0 };
vec.push_back(tok.toFloat());
}
return { vec[0], vec[1] };
}
Rectangle Stylesheet::parseRect(const String& _value, const VariableList& variables)
{
String value = replaceVariables(_value, variables);
auto tokens = value.tokenize(",");
if (tokens.count() != 4)
return { 0, 0, 0, 0 };
stdvec<f32> vec;
for (const auto& tok : tokens)
{
if (!tok.isNumeric(true))
return { 0, 0, 0, 0 };
vec.push_back(tok.toFloat());
}
return { vec[0], vec[1], vec[2], vec[3] };
}
ColorGradient Stylesheet::parseColorGradient(const String& _value, const VariableList& variables)
{
String value = _value.new_trim();
@ -671,7 +705,7 @@ namespace ostd
break;
}
}
return lineCopy;
return lineCopy.trim();
}
void Stylesheet::debugPrint(void)

View file

@ -50,8 +50,21 @@ namespace ostd
{
if (auto v = getVariant(key, themeIDList, qualifierList))
{
// Direct match: return as-is
if (auto p = std::get_if<T>(v))
return *p;
// Cross-cast for numeric types
if constexpr (std::is_same_v<T, i32>)
{
if (auto p = std::get_if<f32>(v))
return cast<i32>(*p);
}
else if constexpr (std::is_same_v<T, f32>)
{
if (auto p = std::get_if<i32>(v))
return cast<f32>(*p);
}
}
return fallback;
}
@ -63,6 +76,9 @@ namespace ostd
bool parseThemeFileLine(const String& line, const VariableList& variables, bool exitCondition = false);
String parseGroupSelector(const String& rawSelector) const;
stdvec<String> parseGroup(const String& selector, const stdvec<String>& group);
Color parseColor(const String& _value, const VariableList& variables);
Vec2 parseVec2(const String& _value, const VariableList& variables);
Rectangle parseRect(const String& _value, const VariableList& variables);
ColorGradient parseColorGradient(const String& _value, const VariableList& variables);
AnimationData parseAnim(const String& _value, bool& outError, const VariableList& variables);
String replaceVariables(const String& line, const VariableList& variables, bool stop_at_first_match = true);

View file

@ -523,18 +523,35 @@ namespace ostd
return m_data.ends_with(str);
}
bool String::regexMatches(const String& regex_pattern, bool case_insensitive) const
stdvec<u32> String::regexFind(const String& regex_pattern, bool case_insensitive, stdvec<String>* outMatchList) const
{
stdvec<u32> indices;
if (outMatchList)
outMatchList->clear();
try
{
boost::regex rgx(regex_pattern.cpp_str(), case_insensitive ? boost::regex_constants::icase : boost::regex_constants::normal);
return boost::regex_search(m_data, rgx);
boost::regex rgx(regex_pattern.cpp_str(),
case_insensitive ? boost::regex_constants::icase : boost::regex_constants::normal);
auto begin = boost::sregex_iterator(m_data.begin(), m_data.end(), rgx);
auto end = boost::sregex_iterator();
for (auto it = begin; it != end; ++it)
{
const boost::smatch& match = *it;
indices.push_back(static_cast<u32>(match.position()));
if (outMatchList)
outMatchList->emplace_back(match.str(0));
}
}
catch (const boost::regex_error& err)
{
std::cerr << err.what() << '\n'; //TODO: Better error handling
return false;
// indices is already empty, outMatchList already cleared
}
return indices;
}
u32 String::count(const String& str) const

View file

@ -167,7 +167,8 @@ namespace ostd
bool contains(const String& str) const;
bool startsWith(const String& str) const;
bool endsWith(const String& str) const;
bool regexMatches(const String& regex_pattern, bool case_insensitive = false) const;
stdvec<u32> regexFind(const String& regex_pattern, bool case_insensitive = false, stdvec<String>* outMatchList = nullptr) const;
inline bool regexMatches(const String& regex_pattern, bool case_insensitive = false) const { return regexFind(regex_pattern, case_insensitive, nullptr).size() > 0; }
u32 count(const String& str) const;
i32 indexOf(char c, u32 start = 0) const;
i32 indexOf(const String& str, u32 start = 0) const;

View file

@ -152,7 +152,7 @@ class TestWindow : public Window
// std::cout << *(selection[0]) << "\n";
// });
auto l_randomIcon = [&](const ogfx::AnimationData& src) -> ogfx::Icon {
auto l_randomIcon = [this](const ogfx::AnimationData& src) -> ogfx::Icon {
static ogfx::Image img("./icons.png", getGFX());
ogfx::AnimationData ad = src;
ad.columnOffset = ostd::Random::getui32(0, 7);
@ -175,6 +175,8 @@ class TestWindow : public Window
auto& item = m_list.getLine(index);
auto& child = m_list.addChild(item, String("Child ").add(i + 1), l_randomIcon(iconsAD));
}
static ogfx::Image animImg("./img.png", getGFX());
m_list.getLine(20).setIcon({ animImg, ad });
m_list.setSelectionChangedCallback([&](stdvec<TreeView::Item*>& selection) -> void {
auto path = (selection[0])->getFullPath();
for (i32 i = 0; i < path.size(); i++)
@ -185,6 +187,13 @@ class TestWindow : public Window
}
std::cout << "\n";
});
// m_list.setChevronDrawCallback([&](TreeView& sender, const TreeView::Item& item, const ostd::Rectangle& bounds, ogfx::BasicRenderer2D& gfx) -> void {
// if (item.isExpanded())
// gfx.fillRect(bounds, Colors::Red);
// else
// gfx.fillRect(bounds, Colors::Blue);
// });
m_list.collapseAll();
for (i32 i = 0; i < 20; i++)
{