diff --git a/extra/DefaultThemeDark.oss b/extra/DefaultThemeDark.oss index b4e40be..c4337b8 100644 --- a/extra/DefaultThemeDark.oss +++ b/extra/DefaultThemeDark.oss @@ -417,4 +417,8 @@ macro set_common_values(_borderColor = $borderColor, _fontSize = 18, _showBorder regionSpacing = 8 cursorRadius = 6 } +(colorButton) { + set_common_values() + set_focus_border($blueAccent) +} % ========================= diff --git a/src/ogfx/gui/WidgetManager.cpp b/src/ogfx/gui/WidgetManager.cpp index 9f730fb..b91761a 100644 --- a/src/ogfx/gui/WidgetManager.cpp +++ b/src/ogfx/gui/WidgetManager.cpp @@ -151,14 +151,14 @@ namespace ogfx { if (w->m_mouseInside) { - // w->m_mouseInside = false; + w->m_mouseInside = false; w->__onMouseExited(event); } continue; } if (!w->m_mouseInside) { - // w->m_mouseInside = true; + w->m_mouseInside = true; w->__onMouseEntered(event); } else @@ -176,7 +176,7 @@ namespace ogfx Widget* ww = m_widgetList[j]; if (ww->m_mouseInside) { - // ww->m_mouseInside = false; + ww->m_mouseInside = false; ww->__onMouseExited(event); } } @@ -270,10 +270,7 @@ namespace ogfx if (widget == nullptr) return; if (!widget->isVisible()) 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; - _DEBUG(5); if (Widget::s_dragAndDropData != nullptr) { auto& const_cast_event = const_cast(event); diff --git a/src/ogfx/gui/widgets/ColorPickerSkins.cpp b/src/ogfx/gui/widgets/ColorPickerSkins.cpp index 9e51f15..d74e0f8 100644 --- a/src/ogfx/gui/widgets/ColorPickerSkins.cpp +++ b/src/ogfx/gui/widgets/ColorPickerSkins.cpp @@ -205,9 +205,25 @@ namespace ogfx setSize({ 48, 24 }); setCallback(eCallback::DragAndDrop, [&](const ogfx::gui::Event& event) -> void { auto data = Widget::getDragAndDropData(); - if (data) - std::cout << *data << "\n"; + if (data && data->getTypeName() == Color().getTypeName()) + setColor(*cast(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(); return *this; } @@ -254,25 +270,38 @@ namespace ogfx void ColorButton::onMouseReleased(const Event& event) { + bool mph = m_mousePressedHere; + m_mousePressedHere = false; if (!isMouseInside()) return; 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; } - Widget::setDragAndDropData(m_color); + if (!mph) return; if (m_popupOpen) close_popup(); else open_popup(); 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) { if (m_popup) close_popup(); m_popup = new ColorPicker(getWindow()); m_popup->setRootChild(); - // m_popup->setTopMost(true); + // m_popup->setTopMost(true); // TODO: Implement m_popup->setSize({ 240, 200 }); m_popup->setColor(m_color); @@ -310,25 +339,13 @@ namespace ogfx m_popupOpen = false; } - void ColorButton::onUpdate(void) + void ColorButton::handleSignal(ostd::Signal& signal) { - // Click-outside dismissal: while the popup is open, check whether - // the mouse has been pressed outside of both the popup AND this - // button. We piggy-back on the mouse position via the focus - // manager — if neither the popup nor the button has focus, close. - // - // 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. - } + if (!m_popupOpen) return; + if (signal.ID != ostd::BuiltinSignals::MousePressed) return; + if (isMouseInside()) return; + if (!m_popup->isMouseInside()) + close_popup(); } } } diff --git a/src/ogfx/gui/widgets/ColorPickerSkins.hpp b/src/ogfx/gui/widgets/ColorPickerSkins.hpp index 82b6ec8..76ce9be 100644 --- a/src/ogfx/gui/widgets/ColorPickerSkins.hpp +++ b/src/ogfx/gui/widgets/ColorPickerSkins.hpp @@ -12,6 +12,7 @@ #include #include #include +#include namespace ogfx { @@ -87,7 +88,8 @@ namespace ogfx void applyTheme(const ostd::Stylesheet& theme) override; void onDraw(ogfx::BasicRenderer2D& gfx) 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; } void setColor(const Color& c); @@ -111,8 +113,16 @@ namespace ogfx // the widget tree clean. ColorPicker* m_popup { nullptr }; bool m_popupOpen { false }; + bool m_mousePressedHere { false }; std::function callback_onColorChanged { nullptr }; + + enum MenuId : i32 { Copy = 1, Paste }; + ContextMenu::Instance m_contextMenu {{ + { "Copy Color", MenuId::Copy }, + { "Paste Color", MenuId::Paste }, + }, nullptr + }; }; } } diff --git a/src/ogfx/gui/widgets/Widget.cpp b/src/ogfx/gui/widgets/Widget.cpp index 190f77e..a965299 100644 --- a/src/ogfx/gui/widgets/Widget.cpp +++ b/src/ogfx/gui/widgets/Widget.cpp @@ -361,14 +361,9 @@ namespace ogfx void Widget::__onDragAndDrop(const Event& event) { - if (hasChildren()) - m_widgets.onMouseReleased(event); - if (!event.isHandled()) - { - if (callback_onDragAndDrop) - callback_onDragAndDrop(event); - onDragAndDrop(event); - } + if (callback_onDragAndDrop) + callback_onDragAndDrop(event); + onDragAndDrop(event); } void Widget::__onMouseMoved(const Event& event) @@ -424,6 +419,7 @@ namespace ogfx void Widget::__onMouseDragged(const Event& event) { + m_mouseInside = contains(event.mouse->position_x, event.mouse->position_y); if (event.isHandled() || !isMouseDraggedEventEnabled()) return; if (hasChildren()) m_widgets.onMouseDragged(event); diff --git a/src/ostd/data/Color.cpp b/src/ostd/data/Color.cpp index 26eac37..340ffe5 100755 --- a/src/ostd/data/Color.cpp +++ b/src/ostd/data/Color.cpp @@ -97,110 +97,11 @@ namespace ostd Color& Color::set(const String& color_string) { - 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; - + if (!isValidColorString(color_string, this)) { - stdvec 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(); - } + OX_WARN("ostd::Color::set(const String&) -> Invalid color string: %s", color_string.c_str()); + r = 0; g = 0; b = 0; a = 255; // match original default-on-failure } - - // --- 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(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; } @@ -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 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(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(vals[0]); + gg = static_cast(vals[1]); + bb = static_cast(vals[2]); + aa = static_cast(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; + } } diff --git a/src/ostd/data/Color.hpp b/src/ostd/data/Color.hpp index fdf03e0..66b9ea6 100755 --- a/src/ostd/data/Color.hpp +++ b/src/ostd/data/Color.hpp @@ -94,6 +94,8 @@ namespace ostd 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 bool isValidColorString(const String& str, Color* out = nullptr); + public: Channel r; Channel g; diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index dab3ed9..9de89fb 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -234,6 +234,7 @@ class TestWindow : public Window t1.addWidget(m_text, { 400, 700 }); t1.addWidget(m_colBtn, { 800, 700 }); t1.addWidget(m_colBtn2, { 950, 700 }); + t1.addWidget(m_colPan, { 1200, 700 }); t2.setLayout(BoxLayout::Orientation::Vertical); t2.getLayout()->setSpacing(16); @@ -347,9 +348,9 @@ class TestWindow : public Window } else if (signal.ID == ostd::BuiltinSignals::MouseReleased) { - auto& mmd = cast(signal.userData); - if (mmd.button == ogfx::MouseEventData::eButton::Right) - showContextMenu(m_menu, { mmd.position_x, mmd.position_y }); + // auto& mmd = cast(signal.userData); + // if (mmd.button == ogfx::MouseEventData::eButton::Right) + // showContextMenu(m_menu, { mmd.position_x, mmd.position_y }); } else if (signal.ID == ostd::BuiltinSignals::WindowResized) { @@ -398,6 +399,7 @@ class TestWindow : public Window TextEdit m_text { *this }; ColorButton m_colBtn { *this }; ColorButton m_colBtn2 { *this }; + ColorPicker m_colPan { *this }; enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };