Added Drag and Drop for Color Picker

This commit is contained in:
OmniaX-Dev 2026-05-14 14:05:43 +02:00
parent 6fb05f77ab
commit 28380f94d9
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
cursorRadius = 6
}
(colorButton) {
set_common_values()
set_focus_border($blueAccent)
}
% =========================

View file

@ -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&>(event);

View file

@ -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<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();
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();
}
}
}

View file

@ -12,6 +12,7 @@
#include <ogfx/gui/widgets/Text.hpp>
#include <ogfx/gui/widgets/Button.hpp>
#include <ogfx/gui/widgets/ColorPicker.hpp>
#include <ogfx/gui/ContextMenu.hpp>
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<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)
{
if (hasChildren())
m_widgets.onMouseReleased(event);
if (!event.isHandled())
{
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);

View file

@ -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<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();
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<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;
}
@ -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 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;

View file

@ -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>(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<ogfx::MouseEventData&>(signal.userData);
if (mmd.button == ogfx::MouseEventData::eButton::Right)
showContextMenu(m_menu, { mmd.position_x, mmd.position_y });
// auto& mmd = cast<ogfx::MouseEventData&>(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 };