Added Drag and Drop for Color Picker
This commit is contained in:
parent
6fb05f77ab
commit
28380f94d9
8 changed files with 173 additions and 143 deletions
|
|
@ -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)
|
||||
}
|
||||
% =========================
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue