Merge branch 'main' of github.com:OmniaX-dev/OmniaFramework

This commit is contained in:
OmniaX-Dev 2026-05-16 04:20:15 +02:00
commit 72f2a0285f
8 changed files with 173 additions and 143 deletions

View file

@ -417,4 +417,8 @@ macro set_common_values(_borderColor = $borderColor, _fontSize = 18, _showBorder
regionSpacing = 8 regionSpacing = 8
cursorRadius = 6 cursorRadius = 6
} }
(colorButton) {
set_common_values()
set_focus_border($blueAccent)
}
% ========================= % =========================

View file

@ -151,14 +151,14 @@ namespace ogfx
{ {
if (w->m_mouseInside) if (w->m_mouseInside)
{ {
// w->m_mouseInside = false; w->m_mouseInside = false;
w->__onMouseExited(event); w->__onMouseExited(event);
} }
continue; continue;
} }
if (!w->m_mouseInside) if (!w->m_mouseInside)
{ {
// w->m_mouseInside = true; w->m_mouseInside = true;
w->__onMouseEntered(event); w->__onMouseEntered(event);
} }
else else
@ -176,7 +176,7 @@ namespace ogfx
Widget* ww = m_widgetList[j]; Widget* ww = m_widgetList[j];
if (ww->m_mouseInside) if (ww->m_mouseInside)
{ {
// ww->m_mouseInside = false; ww->m_mouseInside = false;
ww->__onMouseExited(event); ww->__onMouseExited(event);
} }
} }
@ -270,10 +270,7 @@ namespace ogfx
if (widget == nullptr) return; if (widget == nullptr) return;
if (!widget->isVisible()) return; if (!widget->isVisible()) return;
if (!widget->isDragAndDropEnabled()) return; if (!widget->isDragAndDropEnabled()) return;
_DEBUG(4);
std::cout << widget->getTypeName() << "\n" << widget->getGlobalBounds() << "\n" << ostd::Vec2 { event.mouse->position_x, event.mouse->position_y } << "\n";
if (!widget->isMouseInside()) return; if (!widget->isMouseInside()) return;
_DEBUG(5);
if (Widget::s_dragAndDropData != nullptr) if (Widget::s_dragAndDropData != nullptr)
{ {
auto& const_cast_event = const_cast<Event&>(event); auto& const_cast_event = const_cast<Event&>(event);

View file

@ -205,9 +205,25 @@ namespace ogfx
setSize({ 48, 24 }); setSize({ 48, 24 });
setCallback(eCallback::DragAndDrop, [&](const ogfx::gui::Event& event) -> void { setCallback(eCallback::DragAndDrop, [&](const ogfx::gui::Event& event) -> void {
auto data = Widget::getDragAndDropData(); auto data = Widget::getDragAndDropData();
if (data) if (data && data->getTypeName() == Color().getTypeName())
std::cout << *data << "\n"; setColor(*cast<Color*>(data));
}); });
connectSignal(ostd::BuiltinSignals::MousePressed);
m_contextMenu.onActivate = [this](const ContextMenu::Entry& e) {
if (e.id == MenuId::Copy)
{
// TODO: Replace once I have a clipboard system
SDL_SetClipboardText(getColor().hexString(true, "#"));
}
else if (e.id == MenuId::Paste)
{
// TODO: Replace once I have a clipboard system
const String clipboardText = SDL_GetClipboardText();
Color tmp;
if (!clipboardText.empty() && Color::isValidColorString(clipboardText, &tmp))
setColor(tmp);
}
};
validate(); validate();
return *this; return *this;
} }
@ -254,25 +270,38 @@ namespace ogfx
void ColorButton::onMouseReleased(const Event& event) void ColorButton::onMouseReleased(const Event& event)
{ {
bool mph = m_mousePressedHere;
m_mousePressedHere = false;
if (!isMouseInside()) return; if (!isMouseInside()) return;
if (event.mouse->button != MouseEventData::eButton::Left) if (event.mouse->button != MouseEventData::eButton::Left)
{ {
if (event.mouse->button == MouseEventData::eButton::Right) {} if (event.mouse->button == MouseEventData::eButton::Right)
getWindow().showContextMenu(m_contextMenu, { event.mouse->position_x, event.mouse->position_y });
return; return;
} }
Widget::setDragAndDropData(m_color); if (!mph) return;
if (m_popupOpen) close_popup(); if (m_popupOpen) close_popup();
else open_popup(); else open_popup();
event.handle(); event.handle();
} }
void ColorButton::onMousePressed(const Event& event)
{
if (!isMouseInside()) return;
if (event.mouse->button != MouseEventData::eButton::Left)
return;
Widget::setDragAndDropData(m_color);
m_mousePressedHere = true;
event.handle();
}
void ColorButton::open_popup(void) void ColorButton::open_popup(void)
{ {
if (m_popup) close_popup(); if (m_popup) close_popup();
m_popup = new ColorPicker(getWindow()); m_popup = new ColorPicker(getWindow());
m_popup->setRootChild(); m_popup->setRootChild();
// m_popup->setTopMost(true); // m_popup->setTopMost(true); // TODO: Implement
m_popup->setSize({ 240, 200 }); m_popup->setSize({ 240, 200 });
m_popup->setColor(m_color); m_popup->setColor(m_color);
@ -310,25 +339,13 @@ namespace ogfx
m_popupOpen = false; m_popupOpen = false;
} }
void ColorButton::onUpdate(void) void ColorButton::handleSignal(ostd::Signal& signal)
{ {
// Click-outside dismissal: while the popup is open, check whether if (!m_popupOpen) return;
// the mouse has been pressed outside of both the popup AND this if (signal.ID != ostd::BuiltinSignals::MousePressed) return;
// button. We piggy-back on the mouse position via the focus if (isMouseInside()) return;
// manager — if neither the popup nor the button has focus, close. if (!m_popup->isMouseInside())
// close_popup();
// A simpler heuristic that works well enough: if the popup is open
// and the user clicks somewhere that isn't inside it, the popup's
// own focus is lost. We watch for that here.
if (m_popupOpen && m_popup && !m_popup->isFocused() && !isFocused())
{
// Avoid closing on the same frame the popup opened (it hasn't
// had a chance to be focused yet). The popup grabs focus on
// its first mouse interaction, so we'll only close once the
// user has actually interacted with it and then clicked away.
// For now we leave the close-on-outside-click to the host app
// or to a future FocusManager::onFocusLeft signal.
}
} }
} }
} }

View file

@ -12,6 +12,7 @@
#include <ogfx/gui/widgets/Text.hpp> #include <ogfx/gui/widgets/Text.hpp>
#include <ogfx/gui/widgets/Button.hpp> #include <ogfx/gui/widgets/Button.hpp>
#include <ogfx/gui/widgets/ColorPicker.hpp> #include <ogfx/gui/widgets/ColorPicker.hpp>
#include <ogfx/gui/ContextMenu.hpp>
namespace ogfx namespace ogfx
{ {
@ -87,7 +88,8 @@ namespace ogfx
void applyTheme(const ostd::Stylesheet& theme) override; void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override; void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override; void onMouseReleased(const Event& event) override;
void onUpdate(void) override; void onMousePressed(const Event& event) override;
void handleSignal(ostd::Signal& signal) override;
inline Color getColor(void) const { return m_color; } inline Color getColor(void) const { return m_color; }
void setColor(const Color& c); void setColor(const Color& c);
@ -111,8 +113,16 @@ namespace ogfx
// the widget tree clean. // the widget tree clean.
ColorPicker* m_popup { nullptr }; ColorPicker* m_popup { nullptr };
bool m_popupOpen { false }; bool m_popupOpen { false };
bool m_mousePressedHere { false };
std::function<void(const Color&)> callback_onColorChanged { nullptr }; std::function<void(const Color&)> callback_onColorChanged { nullptr };
enum MenuId : i32 { Copy = 1, Paste };
ContextMenu::Instance m_contextMenu {{
{ "Copy Color", MenuId::Copy },
{ "Paste Color", MenuId::Paste },
}, nullptr
};
}; };
} }
} }

View file

@ -360,16 +360,11 @@ namespace ogfx
} }
void Widget::__onDragAndDrop(const Event& event) void Widget::__onDragAndDrop(const Event& event)
{
if (hasChildren())
m_widgets.onMouseReleased(event);
if (!event.isHandled())
{ {
if (callback_onDragAndDrop) if (callback_onDragAndDrop)
callback_onDragAndDrop(event); callback_onDragAndDrop(event);
onDragAndDrop(event); onDragAndDrop(event);
} }
}
void Widget::__onMouseMoved(const Event& event) void Widget::__onMouseMoved(const Event& event)
{ {
@ -424,6 +419,7 @@ namespace ogfx
void Widget::__onMouseDragged(const Event& event) void Widget::__onMouseDragged(const Event& event)
{ {
m_mouseInside = contains(event.mouse->position_x, event.mouse->position_y);
if (event.isHandled() || !isMouseDraggedEventEnabled()) return; if (event.isHandled() || !isMouseDraggedEventEnabled()) return;
if (hasChildren()) if (hasChildren())
m_widgets.onMouseDragged(event); m_widgets.onMouseDragged(event);

View file

@ -97,110 +97,11 @@ namespace ostd
Color& Color::set(const String& color_string) Color& Color::set(const String& color_string)
{ {
String se(color_string); if (!isValidColorString(color_string, this))
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; OX_WARN("ostd::Color::set(const String&) -> Invalid color string: %s", color_string.c_str());
auto positions = se.regexFind("\\.(lighten|darken)\\(\\s*([0-9]*\\.?[0-9]+)\\s*\\)\\s*$", true, &matches); r = 0; g = 0; b = 0; a = 255; // match original default-on-failure
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();
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(","))
{
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("ostd::Color::set(const String&) -> Invalid rgb string format: %s.", color_string.c_str());
return *this;
}
r = tokens.next().toInt();
g = tokens.next().toInt();
b = tokens.next().toInt();
if (tokens.hasNext())
a = tokens.next().toInt();
}
else
{
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; return *this;
} }
@ -362,4 +263,105 @@ namespace ostd
} }
} }
bool Color::isValidColorString(const String& str, Color* out)
{
String se(str);
se.trim();
if (se.len() == 0) return false;
// --- Step 1: Extract optional .lighten(x) / .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;
const u32 openParen = whole.indexOf("(");
const u32 closeParen = whole.indexOf(")");
String numStr = whole.new_substr(openParen + 1, closeParen);
numStr.trim();
modAmount = numStr.toFloat();
if (modAmount < 0.0f) modAmount = 0.0f;
if (modAmount > 1.0f) modAmount = 1.0f;
se = se.new_substr(0, positions[0]);
se.trim();
}
if (se.len() == 0) return false;
}
// Local working color — only written back to *out if everything validates.
u8 rr = 0, gg = 0, bb = 0, aa = 255;
// --- Step 2: Normalize "#" prefix to "0x" ---
if (se.startsWith("#"))
se = String("0x") + se.new_substr(1).trim();
// --- Step 3a: Hex form ---
if (se.toLower().startsWith("0x"))
{
if (!se.isHex()) return false;
const u32 digits = se.len() - 2;
if (digits < 1 || digits > 8) return false;
const u32 c = static_cast<u32>(se.toInt());
if (digits <= 6)
{
rr = (c >> 16) & 0xFF;
gg = (c >> 8) & 0xFF;
bb = (c >> 0) & 0xFF;
aa = 255;
}
else
{
rr = (c >> 24) & 0xFF;
gg = (c >> 16) & 0xFF;
bb = (c >> 8) & 0xFF;
aa = (c >> 0) & 0xFF;
}
}
// --- Step 3b: Functional form ---
else if ((se.startsWith("(") || se.startsWith("rgb(") || se.startsWith("rgba("))
&& se.endsWith(")") && se.contains(","))
{
String inner = se.new_substr(se.indexOf("(") + 1, se.len() - 1).trim();
auto tokens = inner.tokenize(",", true, false);
const u32 n = tokens.count();
if (n < 3 || n > 4) return false;
i64 vals[4] = { 0, 0, 0, 255 };
for (u32 i = 0; i < n; ++i)
{
String tok = tokens.next();
if (tok.len() == 0 || !tok.isInt()) return false;
const i64 v = tok.toInt();
if (v < 0 || v > 255) return false;
vals[i] = v;
}
rr = static_cast<u8>(vals[0]);
gg = static_cast<u8>(vals[1]);
bb = static_cast<u8>(vals[2]);
aa = static_cast<u8>(vals[3]);
}
else
{
return false;
}
// --- Step 4: Build the output color and apply modifier ---
if (out != nullptr)
{
out->set(rr, gg, bb, aa);
if (mod == Modifier::Lighten) out->lighten(modAmount);
else if (mod == Modifier::Darken) out->darken(modAmount);
}
return true;
}
} }

View file

@ -94,6 +94,8 @@ namespace ostd
static void RGBtoHSV(f32 r, f32 g, f32 b, f32& h, f32& s, f32& v); static void RGBtoHSV(f32 r, f32 g, f32 b, f32& h, f32& s, f32& v);
static void HSVtoRGB(f32 h, f32 s, f32 v, f32& r, f32& g, f32& b); static void HSVtoRGB(f32 h, f32 s, f32 v, f32& r, f32& g, f32& b);
static bool isValidColorString(const String& str, Color* out = nullptr);
public: public:
Channel r; Channel r;
Channel g; Channel g;

View file

@ -234,6 +234,7 @@ class TestWindow : public Window
t1.addWidget(m_text, { 400, 700 }); t1.addWidget(m_text, { 400, 700 });
t1.addWidget(m_colBtn, { 800, 700 }); t1.addWidget(m_colBtn, { 800, 700 });
t1.addWidget(m_colBtn2, { 950, 700 }); t1.addWidget(m_colBtn2, { 950, 700 });
t1.addWidget(m_colPan, { 1200, 700 });
t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical); t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical);
t2.getLayout()->setSpacing(16); t2.getLayout()->setSpacing(16);
@ -347,9 +348,9 @@ class TestWindow : public Window
} }
else if (signal.ID == ostd::BuiltinSignals::MouseReleased) else if (signal.ID == ostd::BuiltinSignals::MouseReleased)
{ {
auto& mmd = cast<ogfx::MouseEventData&>(signal.userData); // auto& mmd = cast<ogfx::MouseEventData&>(signal.userData);
if (mmd.button == ogfx::MouseEventData::eButton::Right) // if (mmd.button == ogfx::MouseEventData::eButton::Right)
showContextMenu(m_menu, { mmd.position_x, mmd.position_y }); // showContextMenu(m_menu, { mmd.position_x, mmd.position_y });
} }
else if (signal.ID == ostd::BuiltinSignals::WindowResized) else if (signal.ID == ostd::BuiltinSignals::WindowResized)
{ {
@ -398,6 +399,7 @@ class TestWindow : public Window
TextEdit m_text { *this }; TextEdit m_text { *this };
ColorButton m_colBtn { *this }; ColorButton m_colBtn { *this };
ColorButton m_colBtn2 { *this }; ColorButton m_colBtn2 { *this };
ColorPicker m_colPan { *this };
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted }; enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };