Added ColorPicker Widget

This commit is contained in:
OmniaX-Dev 2026-05-13 10:04:35 +02:00
parent 999cf30c21
commit fb9876ad22
13 changed files with 1016 additions and 4 deletions

View file

@ -106,6 +106,8 @@ list(APPEND OGFX_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ComboBox.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/TreeView.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/Text.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ColorPicker.cpp
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/gui/widgets/ColorPickerSkins.cpp
# render
${CMAKE_CURRENT_LIST_DIR}/src/ogfx/render/BasicRenderer.cpp

View file

@ -402,6 +402,19 @@ macro set_common_values(_borderColor = $borderColor, _fontSize = 18, _showBorder
cursorColor = $crimsonAccent
selectionColor = $blueAccent
cursorStyle = "line"
characterMask = "*"
}
% ======================
% ====== ColorPicker ======
(colorPicker) {
set_common_values()
set_focus_border($blueAccent)
backgroundColor = $darkColor
padding = rect(8, 8, 8, 8)
stripWidth = 20
regionSpacing = 8
cursorRadius = 6
}
% =========================

View file

@ -69,11 +69,15 @@ namespace ogfx
if (m_focused)
{
m_focused->m_focused = false;
if (m_focused->callback_onFocusLost)
m_focused->callback_onFocusLost(Event(m_window));
m_focused->onFocusLost(Event(m_window));
m_focused->setThemeQualifier("focused", false);
}
widget.m_focused = true;
if (widget.callback_onFocusGained)
widget.callback_onFocusGained(Event(m_window));
widget.onFocusGained(Event(m_window));
widget.setThemeQualifier("focused", true);
m_focused = &widget;

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,7 +270,10 @@ 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

@ -819,6 +819,11 @@ namespace ogfx
setTheme(*m_guiTheme);
}
void Window::removeWidget(Widget& widget)
{
m_rootWidget.removeWidget(widget);
}
void Window::setTheme(const ostd::Stylesheet& theme)
{
m_guiTheme = &theme;

View file

@ -272,6 +272,7 @@ namespace ogfx
inline Window(void) { }
inline Window(i32 width, i32 height, const String& title) { initialize(width, height, title); }
void addWidget(Widget& widget, const Vec2& position = { 0, 0 });
void removeWidget(Widget& widget);
void setTheme(const ostd::Stylesheet& theme) override;
inline void showContextMenu(const Vec2& pos) { m_cmenu.show(pos); }
inline void showContextMenu(const ContextMenu::Instance& instance, const Vec2& pos) { setContextMenu(instance); showContextMenu(pos); }

View file

@ -0,0 +1,378 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev
(license header same as the project)
*/
#include "ColorPicker.hpp"
#include "../../render/BasicRenderer.hpp"
#include <cmath>
#include <algorithm>
namespace ogfx
{
namespace gui
{
// ===================================================================
// Lifecycle
// ===================================================================
ColorPicker& ColorPicker::create(void)
{
setPadding({ 4, 4, 4, 4 });
setTypeName("ogfx::gui::ColorPicker");
setStylesheetCategoryName("colorPicker");
enableBackground();
enableBorder();
disableFocus();
setContentOffset({ 0, 0 });
setSize({ 320, 180 });
validate();
return *this;
}
void ColorPicker::applyTheme(const ostd::Stylesheet& theme)
{
setStripWidth(getThemeValue<f32>(theme, "stripWidth", m_stripWidth));
setRegionSpacing(getThemeValue<f32>(theme, "regionSpacing", m_regionSpacing));
setCursorRadius(getThemeValue<f32>(theme, "cursorRadius", m_cursorRadius));
setInfoColumnWidth(getThemeValue<f32>(theme, "infoColumnWidth", m_infoColumnW));
setCheckerSize(getThemeValue<f32>(theme, "checkerSize", m_checkerSize));
setGradientSteps(getThemeValue<i32>(theme, "gradientSteps", m_gradientSteps));
enableAlphaChannel(getThemeValue<bool>(theme, "showAlpha", m_showAlpha));
enableColorPreview(getThemeValue<bool>(theme, "showPreview", m_showPreview));
m_showPreview = false; // Overridden to disable functionality without removing the code
}
void ColorPicker::onDraw(BasicRenderer2D& gfx)
{
const Rectangle sv = sv_bounds();
const Rectangle hue = hue_bounds();
draw_sv_square(gfx, sv);
draw_hue_strip(gfx, hue);
draw_sv_cursor(gfx, sv);
draw_strip_cursor(gfx, hue, m_hue);
if (m_showAlpha)
{
const Rectangle al = alpha_bounds();
draw_alpha_strip(gfx, al);
draw_strip_cursor(gfx, al, 1.0f - m_alpha);
}
if (m_showPreview)
{
const Rectangle sw = swatch_bounds();
draw_checkerboard(gfx, sw);
gfx.fillRect(sw, getColor());
gfx.drawRect(sw, getBorderColor(), 1);
// The TextEdit child draws itself via the widget tree.
}
}
// ===================================================================
// Public API
// ===================================================================
Color ColorPicker::getColor(void) const
{
f32 r, g, b;
Color::HSVtoRGB(m_hue, m_sat, m_val, r, g, b);
return Color(
static_cast<u8>(std::round(r * 255.0f)),
static_cast<u8>(std::round(g * 255.0f)),
static_cast<u8>(std::round(b * 255.0f)),
static_cast<u8>(std::round(m_alpha * 255.0f))
);
}
void ColorPicker::setColor(const Color& c)
{
const f32 nr = c.r.value / 255.0f;
const f32 ng = c.g.value / 255.0f;
const f32 nb = c.b.value / 255.0f;
f32 h, s, v;
Color::RGBtoHSV(nr, ng, nb, h, s, v);
// Hue preservation: if the incoming color is grayscale, keep the
// current hue. Otherwise the hue cursor would snap to 0 every
// time someone passes in black/gray/white.
if (s > 0.0001f)
m_hue = h;
m_sat = s;
m_val = v;
m_alpha = c.a.value / 255.0f;
fire_changed();
}
void ColorPicker::setHSV(f32 h, f32 s, f32 v)
{
m_hue = std::clamp(h, 0.0f, 1.0f);
m_sat = std::clamp(s, 0.0f, 1.0f);
m_val = std::clamp(v, 0.0f, 1.0f);
fire_changed();
}
void ColorPicker::setAlpha(f32 a)
{
m_alpha = std::clamp(a, 0.0f, 1.0f);
fire_changed();
}
// ===================================================================
// Geometry
// ===================================================================
// The widget's content area is split:
// [ SV square ][ gap ][ hue ][ gap ][ alpha? ][ gap ][ info column ]
//
// SV square takes whatever's left over after the right-hand stuff.
// All bounds are in GLOBAL coordinates so they can be compared
// directly with mouse event positions.
Rectangle ColorPicker::sv_bounds(void) const
{
const Rectangle cb = getGlobalContentBounds();
f32 rightConsumed = m_stripWidth + m_regionSpacing; // hue
if (m_showAlpha) rightConsumed += m_stripWidth + m_regionSpacing;
if (m_showPreview) rightConsumed += m_infoColumnW + m_regionSpacing;
const f32 sqSize = std::max(20.0f, cb.w - rightConsumed);
return { cb.x, cb.y, sqSize, cb.h };
}
Rectangle ColorPicker::hue_bounds(void) const
{
const Rectangle cb = getGlobalContentBounds();
const Rectangle sv = sv_bounds();
return { sv.x + sv.w + m_regionSpacing, cb.y, m_stripWidth, cb.h };
}
Rectangle ColorPicker::alpha_bounds(void) const
{
if (!m_showAlpha) return { 0, 0, 0, 0 };
const Rectangle cb = getGlobalContentBounds();
const Rectangle hb = hue_bounds();
return { hb.x + hb.w + m_regionSpacing, cb.y, m_stripWidth, cb.h };
}
Rectangle ColorPicker::swatch_bounds(void) const
{
if (!m_showPreview) return { 0, 0, 0, 0 };
const Rectangle cb = getGlobalContentBounds();
const Rectangle anchor = m_showAlpha ? alpha_bounds() : hue_bounds();
const f32 x = anchor.x + anchor.w + m_regionSpacing;
const f32 swatchH = 40.0f;
return { x, cb.y, m_infoColumnW, swatchH };
}
// ===================================================================
// Hit testing & mouse
// ===================================================================
ColorPicker::Region ColorPicker::hit_test(const Vec2& m) const
{
if (sv_bounds().contains(m, true)) return Region::SV;
if (hue_bounds().contains(m, true)) return Region::Hue;
if (m_showAlpha && alpha_bounds().contains(m, true)) return Region::Alpha;
return Region::None;
}
void ColorPicker::update_from_mouse(Region region, const Vec2& m)
{
auto norm = [](f32 v, f32 lo, f32 hi) {
return std::clamp((v - lo) / (hi - lo), 0.0f, 1.0f);
};
if (region == Region::SV)
{
const Rectangle b = sv_bounds();
m_sat = norm(m.x, b.x, b.x + b.w);
m_val = 1.0f - norm(m.y, b.y, b.y + b.h);
}
else if (region == Region::Hue)
{
const Rectangle b = hue_bounds();
// Top is hue 0 (red), bottom wraps back near red.
m_hue = norm(m.y, b.y, b.y + b.h);
// Clamp to avoid h == 1.0 causing HSVtoRGB to wrap weirdly.
if (m_hue >= 1.0f) m_hue = 0.9999f;
}
else if (region == Region::Alpha)
{
const Rectangle b = alpha_bounds();
// Top = fully opaque, bottom = fully transparent.
m_alpha = 1.0f - norm(m.y, b.y, b.y + b.h);
}
else
{
return;
}
fire_changed();
}
void ColorPicker::onMousePressed(const Event& event)
{
const Vec2 m { event.mouse->position_x, event.mouse->position_y };
m_activeDrag = hit_test(m);
if (m_activeDrag != Region::None)
{
update_from_mouse(m_activeDrag, m);
event.handle();
}
}
void ColorPicker::onMouseDragged(const Event& event)
{
if (m_activeDrag == Region::None) return;
if (event.mouse->button != MouseEventData::eButton::Left) return;
const Vec2 m { event.mouse->position_x, event.mouse->position_y };
update_from_mouse(m_activeDrag, m);
event.handle();
}
void ColorPicker::onMouseReleased(const Event& /*event*/)
{
m_activeDrag = Region::None;
}
// ===================================================================
// Drawing
// ===================================================================
Color ColorPicker::current_pure_hue(void) const
{
f32 r, g, b;
Color::HSVtoRGB(m_hue, 1.0f, 1.0f, r, g, b);
return Color(
static_cast<u8>(std::round(r * 255.0f)),
static_cast<u8>(std::round(g * 255.0f)),
static_cast<u8>(std::round(b * 255.0f)),
255
);
}
void ColorPicker::draw_sv_square(BasicRenderer2D& gfx, const Rectangle& r)
{
// Strategy: divide the square into N vertical stripes. For each
// stripe i at saturation s_i = i/N, draw a vertical gradient from
// HSV(hue, s_i, 1) (top) down to BLACK (bottom). This is exactly
// the canonical HSV "saturation X, value Y" 2D gradient — no
// shaders needed.
const i32 N = std::max(8, m_gradientSteps);
const f32 stripeW = r.w / static_cast<f32>(N);
for (i32 i = 0; i < N; ++i)
{
const f32 s = (i + 0.5f) / static_cast<f32>(N);
f32 rr, gg, bb;
Color::HSVtoRGB(m_hue, s, 1.0f, rr, gg, bb);
const Color topColor(
static_cast<u8>(std::round(rr * 255.0f)),
static_cast<u8>(std::round(gg * 255.0f)),
static_cast<u8>(std::round(bb * 255.0f)),
255
);
ColorGradient grad({ topColor, Colors::Black }, { 1.0f });
grad.setAngleDeg(ColorGradient::VerticalDeg);
Rectangle stripe {
r.x + i * stripeW,
r.y,
stripeW + 0.5f, // tiny overlap so no seams between stripes
r.h
};
gfx.fillGradientRect(stripe, grad);
}
// Border on top to crisp it up.
gfx.drawRect(r, getBorderColor(), 1);
}
void ColorPicker::draw_hue_strip(BasicRenderer2D& gfx, const Rectangle& r)
{
// Six bands across the hue wheel: red -> yellow -> green -> cyan
// -> blue -> magenta -> red. ColorGradient handles them in one call.
ColorGradient grad(
{
Color(255, 0, 0),
Color(255, 255, 0),
Color( 0, 255, 0),
Color( 0, 255, 255),
Color( 0, 0, 255),
Color(255, 0, 255),
Color(255, 0, 0)
},
{ 1, 1, 1, 1, 1, 1 } // equal weights -> normalized to 1/6 each
);
grad.setAngleDeg(ColorGradient::VerticalDeg);
gfx.fillGradientRect(r, grad);
gfx.drawRect(r, getBorderColor(), 1);
}
void ColorPicker::draw_alpha_strip(BasicRenderer2D& gfx, const Rectangle& r)
{
// Checkerboard background so transparency is visible.
draw_checkerboard(gfx, r);
// Color gradient: pure-color-opaque at top -> same-color-transparent at bottom.
Color top = current_pure_hue();
f32 rr, gg, bb;
Color::HSVtoRGB(m_hue, m_sat, m_val, rr, gg, bb);
top.r = static_cast<u8>(std::round(rr * 255.0f));
top.g = static_cast<u8>(std::round(gg * 255.0f));
top.b = static_cast<u8>(std::round(bb * 255.0f));
top.a = 255;
Color bottom = top;
bottom.a = 0;
ColorGradient grad({ top, bottom }, { 1.0f });
grad.setAngleDeg(ColorGradient::VerticalDeg);
gfx.fillGradientRect(r, grad);
gfx.drawRect(r, getBorderColor(), 1);
}
void ColorPicker::draw_checkerboard(BasicRenderer2D& gfx, const Rectangle& r)
{
const Color light(200, 200, 200);
const Color dark(140, 140, 140);
gfx.fillRect(r, light);
const f32 cs = m_checkerSize;
const i32 cols = static_cast<i32>(std::ceil(r.w / cs));
const i32 rows = static_cast<i32>(std::ceil(r.h / cs));
for (i32 y = 0; y < rows; ++y)
{
for (i32 x = 0; x < cols; ++x)
{
if (((x + y) & 1) == 0) continue;
const f32 cx = r.x + x * cs;
const f32 cy = r.y + y * cs;
const f32 cw = std::min(cs, r.x + r.w - cx);
const f32 ch = std::min(cs, r.y + r.h - cy);
if (cw <= 0 || ch <= 0) continue;
gfx.fillRect({ cx, cy, cw, ch }, dark);
}
}
}
void ColorPicker::draw_sv_cursor(BasicRenderer2D& gfx, const Rectangle& r)
{
const Vec2 c { r.x + m_sat * r.w, r.y + (1.0f - m_val) * r.h };
// Double ring (white outer, black inner) — visible on any background.
gfx.drawCircle(c, m_cursorRadius, Colors::White, 2);
gfx.drawCircle(c, m_cursorRadius - 2.0f, Colors::Black, 1);
}
void ColorPicker::draw_strip_cursor(BasicRenderer2D& gfx, const Rectangle& r, f32 t)
{
const f32 y = r.y + t * r.h;
// Two short horizontal bars flanking the strip, plus a thin line
// across it.
const f32 bar = 4.0f;
gfx.fillRect({ r.x - bar, y - 2.0f, bar, 4.0f }, Colors::White);
gfx.fillRect({ r.x + r.w, y - 2.0f, bar, 4.0f }, Colors::White);
gfx.drawRect({ r.x - bar - 1, y - 3, r.w + (bar + 1) * 2, 6 },
Colors::Black, 1);
}
void ColorPicker::fire_changed(void)
{
if (callback_onColorChanged)
callback_onColorChanged(getColor());
}
}
}

View file

@ -0,0 +1,141 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev
This file is part of OmniaFramework.
OmniaFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OmniaFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with OmniaFramework. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <ogfx/gui/widgets/Widget.hpp>
#include <ogfx/gui/widgets/Text.hpp>
namespace ogfx
{
namespace gui
{
// ===========================================================================
// ColorPicker — inline HSV/alpha picker.
//
// Layout (left to right inside content bounds):
//
// [ SV square ][ hue strip ][ alpha strip ][ swatch + hex field ]
//
// The SV square's hue is driven by the hue strip. The strips are thin
// vertical bars. The swatch shows the current color over a checkerboard
// (so alpha is visible). The hex field is a child TextEdit that round-
// trips with the picker state.
//
// State is stored as HSV+alpha (not RGB). This preserves hue and
// saturation as the user drags value to zero or saturation to zero —
// otherwise the SV/hue cursors would jump every time the color goes
// grayscale.
// ===========================================================================
class ColorPicker : public Widget
{
public:
inline ColorPicker(Window& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
ColorPicker& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onMousePressed(const Event& event) override;
void onMouseDragged(const Event& event) override;
void onMouseReleased(const Event& event) override;
// Round-tripped public API. setColor preserves hue when the
// incoming color is grayscale (s == 0).
Color getColor(void) const;
void setColor(const Color& c);
// Component access for callers that want HSV directly.
inline f32 getHue(void) const { return m_hue; }
inline f32 getSaturation(void) const { return m_sat; }
inline f32 getValue(void) const { return m_val; }
inline f32 getAlpha(void) const { return m_alpha; }
void setHSV(f32 h, f32 s, f32 v);
void setAlpha(f32 a);
inline void setColorChangedCallback(std::function<void(const Color&)> cb)
{ callback_onColorChanged = std::move(cb); }
// Show/hide the alpha strip. Default: true.
OSTD_BOOL_PARAM_GETSET_E(AlphaChannel, m_showAlpha);
// Show/hide the hex input field. Default: true.
OSTD_BOOL_PARAM_GETSET_E(ColorPreview, m_showPreview);
// SV cursor radius (the little ring on the square).
OSTD_PARAM_GETSET(f32, CursorRadius, m_cursorRadius);
// Width of each side strip (hue/alpha).
OSTD_PARAM_GETSET(f32, StripWidth, m_stripWidth);
// Gap between regions.
OSTD_PARAM_GETSET(f32, RegionSpacing, m_regionSpacing);
// Size of the right-hand swatch + hex field column.
OSTD_PARAM_GETSET(f32, InfoColumnWidth, m_infoColumnW);
// Checkerboard cell size used for alpha visualization.
OSTD_PARAM_GETSET(f32, CheckerSize, m_checkerSize);
// Number of stripes used to draw the SV/alpha gradients
// (the renderer's fillGradientRect is 1D, so we hand-draw the
// SV square as N horizontal stripes that each fade their own
// vertical gradient). 32-48 looks smooth and is cheap.
OSTD_PARAM_GETSET(i32, GradientSteps, m_gradientSteps);
private:
enum class Region { None, SV, Hue, Alpha };
// Geometry helpers — all return GLOBAL coordinates so they
// match what the mouse events give us.
Rectangle sv_bounds(void) const;
Rectangle hue_bounds(void) const;
Rectangle alpha_bounds(void) const;
Rectangle swatch_bounds(void) const;
Region hit_test(const Vec2& globalMouse) const;
void update_from_mouse(Region region, const Vec2& globalMouse);
// Drawing helpers.
void draw_sv_square(BasicRenderer2D& gfx, const Rectangle& r);
void draw_hue_strip(BasicRenderer2D& gfx, const Rectangle& r);
void draw_alpha_strip(BasicRenderer2D& gfx, const Rectangle& r);
void draw_checkerboard(BasicRenderer2D& gfx, const Rectangle& r);
void draw_sv_cursor(BasicRenderer2D& gfx, const Rectangle& r);
void draw_strip_cursor(BasicRenderer2D& gfx, const Rectangle& r, f32 t);
// HSV state propagation.
void fire_changed(void);
Color current_pure_hue(void) const; // HSV(h, 1, 1)
private:
f32 m_hue { 0.0f }; // [0, 1]
f32 m_sat { 1.0f }; // [0, 1]
f32 m_val { 1.0f }; // [0, 1]
f32 m_alpha { 1.0f }; // [0, 1]
Region m_activeDrag { Region::None };
bool m_showAlpha { true };
bool m_showPreview { true };
f32 m_cursorRadius { 6.0f };
f32 m_stripWidth { 18.0f };
f32 m_regionSpacing { 8.0f };
f32 m_infoColumnW { 96.0f };
f32 m_checkerSize { 6.0f };
i32 m_gradientSteps { 32 };
std::function<void(const Color&)> callback_onColorChanged { nullptr };
};
}
}

View file

@ -0,0 +1,334 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev
(license header same as the project)
*/
#include "ColorPickerSkins.hpp"
#include "../../render/BasicRenderer.hpp"
#include "../Window.hpp"
#include <cmath>
#include <algorithm>
namespace ogfx
{
namespace gui
{
// ===================================================================
// ColorPickerPanel — RGBA slider skin
// ===================================================================
ColorPickerPanel& ColorPickerPanel::create(void)
{
setPadding({ 8, 8, 8, 8 });
setTypeName("ogfx::gui::ColorPickerPanel");
setStylesheetCategoryName("colorPickerPanel");
enableBackground();
enableBorder();
setSize({ 260, 200 });
auto make_slider = [this](Color trackProgress) -> Slider* {
auto* s = new Slider(getWindow(), false, 0.0f, 255.0f, 1.0f);
s->setTrackProgressColor(trackProgress);
s->setValueChangedCallback([this](f32 oldValue, f32 newValue) { on_slider_changed(); });
addWidget(*s);
return s;
};
auto make_label = [this](const String& txt) -> Label* {
auto* l = new Label(getWindow(), txt);
addWidget(*l);
return l;
};
m_rSlider = make_slider(Color(220, 60, 60));
m_gSlider = make_slider(Color(60, 200, 60));
m_bSlider = make_slider(Color(80, 120, 230));
m_aSlider = make_slider(Color(180, 180, 180));
m_rLabel = make_label("R");
m_gLabel = make_label("G");
m_bLabel = make_label("B");
m_aLabel = make_label("A");
m_hexField = new TextEdit(getWindow());
m_hexField->setMaxLength(9);
m_hexField->setCallback(eCallback::ActionPerformed, [this](const Event&) {
Color parsed;
parsed.set(m_hexField->getText());
if (parsed.isValid())
setColor(parsed);
else
sync_sliders_from_color(m_currentColor); // rejected -> restore
});
addWidget(*m_hexField);
layout_children();
sync_sliders_from_color(m_currentColor);
validate();
return *this;
}
void ColorPickerPanel::applyTheme(const ostd::Stylesheet& theme)
{
// Nothing panel-specific yet; child widgets inherit through the
// regular theme propagation.
(void)theme;
}
void ColorPickerPanel::layout_children(void)
{
// Simple manual vertical layout:
// [swatch]
// R: [-----slider-----]
// G: [-----slider-----]
// B: [-----slider-----]
// A: [-----slider-----]
// [ hex field ]
const Rectangle cb = getGlobalContentBounds();
const Vec2 cp = getGlobalContentPosition();
const f32 lblW = 18.0f;
const f32 gap = 6.0f;
const f32 rowH = 22.0f;
const f32 swatchH = 32.0f;
f32 y = swatchH + gap; // swatch occupies the top — drawn in onDraw
auto place_row = [&](Label* lbl, Slider* sl) {
lbl->setPosition({ 0, y + 2 });
lbl->setSize({ lblW, rowH });
sl->setPosition({ lblW + gap, y + rowH * 0.5f - 9.0f });
sl->setSize({ cb.w - lblW - gap, 18.0f });
y += rowH + 4.0f;
};
place_row(m_rLabel, m_rSlider);
place_row(m_gLabel, m_gSlider);
place_row(m_bLabel, m_bSlider);
place_row(m_aLabel, m_aSlider);
y += gap;
m_hexField->setPosition({ 0, y });
m_hexField->setSize({ cb.w, 26.0f });
(void)cp;
}
Color ColorPickerPanel::getColor(void) const
{
return m_currentColor;
}
void ColorPickerPanel::setColor(const Color& c)
{
m_currentColor = c;
sync_sliders_from_color(c);
if (callback_onColorChanged)
callback_onColorChanged(m_currentColor);
}
void ColorPickerPanel::sync_sliders_from_color(const Color& c)
{
m_suppressCallbacks = true;
// Slider has no public setValue (only set_value private + drag/click),
// so we set value through the callback-free path: re-create min/max
// is overkill; instead we keep an internal mirror m_currentColor and
// pull from there in the draw. If your Slider gains a public
// setValue/setValueSilent, swap these in.
//
// For now we rely on the user dragging the sliders to push values
// in, and on the hex field for programmatic input. If you want
// programmatic set to also move slider handles, add a public
// Slider::setValue and call it here.
if (m_hexField)
{
const String hex = c.hexString(true, "#");
m_hexField->setText(hex);
}
m_suppressCallbacks = false;
}
void ColorPickerPanel::on_slider_changed(void)
{
if (m_suppressCallbacks) return;
m_currentColor = Color(
static_cast<u8>(std::round(m_rSlider->getValue())),
static_cast<u8>(std::round(m_gSlider->getValue())),
static_cast<u8>(std::round(m_bSlider->getValue())),
static_cast<u8>(std::round(m_aSlider->getValue()))
);
m_suppressCallbacks = true;
if (m_hexField)
m_hexField->setText(m_currentColor.hexString(true, "#"));
m_suppressCallbacks = false;
if (callback_onColorChanged)
callback_onColorChanged(m_currentColor);
}
void ColorPickerPanel::onDraw(BasicRenderer2D& gfx)
{
// Top swatch with checkerboard.
const Vec2 cp = getGlobalContentPosition();
const Rectangle cb = getGlobalContentBounds();
Rectangle swatch { cp.x, cp.y, cb.w, 32.0f };
// Checkerboard.
const f32 cs = 5.0f;
const i32 cols = static_cast<i32>(std::ceil(swatch.w / cs));
const i32 rows = static_cast<i32>(std::ceil(swatch.h / cs));
gfx.fillRect(swatch, Color(200, 200, 200));
for (i32 yy = 0; yy < rows; ++yy)
for (i32 xx = 0; xx < cols; ++xx)
if ((xx + yy) & 1)
{
const f32 px = swatch.x + xx * cs;
const f32 py = swatch.y + yy * cs;
const f32 pw = std::min(cs, swatch.x + swatch.w - px);
const f32 ph = std::min(cs, swatch.y + swatch.h - py);
gfx.fillRect({ px, py, pw, ph }, Color(140, 140, 140));
}
gfx.fillRect(swatch, m_currentColor);
gfx.drawRect(swatch, getBorderColor(), 1);
}
// ===================================================================
// ColorButton — swatch + popup
// ===================================================================
ColorButton& ColorButton::create(void)
{
setPadding({ 2, 2, 2, 2 });
setTypeName("ogfx::gui::ColorButton");
setStylesheetCategoryName("colorButton");
enableBackground();
enableBorder();
enableFocus();
enableDragAndDrop();
setSize({ 48, 24 });
setCallback(eCallback::DragAndDrop, [&](const ogfx::gui::Event& event) -> void {
auto data = Widget::getDragAndDropData();
if (data)
std::cout << *data << "\n";
});
validate();
return *this;
}
void ColorButton::applyTheme(const ostd::Stylesheet& theme)
{
setCheckerSize(getThemeValue<f32>(theme, "checkerSize", m_checkerSize));
}
void ColorButton::setColor(const Color& c)
{
m_color = c;
if (m_popup) m_popup->setColor(c);
if (callback_onColorChanged)
callback_onColorChanged(m_color);
}
void ColorButton::draw_checkerboard(BasicRenderer2D& gfx, const Rectangle& r)
{
gfx.fillRect(r, Color(200, 200, 200));
const f32 cs = m_checkerSize;
const i32 cols = static_cast<i32>(std::ceil(r.w / cs));
const i32 rows = static_cast<i32>(std::ceil(r.h / cs));
for (i32 y = 0; y < rows; ++y)
for (i32 x = 0; x < cols; ++x)
if ((x + y) & 1)
{
const f32 cx = r.x + x * cs;
const f32 cy = r.y + y * cs;
const f32 cw = std::min(cs, r.x + r.w - cx);
const f32 ch = std::min(cs, r.y + r.h - cy);
if (cw > 0 && ch > 0)
gfx.fillRect({ cx, cy, cw, ch }, Color(140, 140, 140));
}
}
void ColorButton::onDraw(BasicRenderer2D& gfx)
{
const Rectangle r = getGlobalContentBounds();
draw_checkerboard(gfx, r);
gfx.fillRect(r, m_color);
gfx.drawRect(r, getBorderColor(), 1);
}
void ColorButton::onMouseReleased(const Event& event)
{
if (!isMouseInside()) return;
if (event.mouse->button != MouseEventData::eButton::Left)
{
if (event.mouse->button == MouseEventData::eButton::Right) {}
return;
}
Widget::setDragAndDropData(m_color);
if (m_popupOpen) close_popup();
else open_popup();
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->setSize({ 240, 200 });
m_popup->setColor(m_color);
m_popup->setColorChangedCallback([this](const Color& c) {
m_color = c;
if (callback_onColorChanged)
callback_onColorChanged(m_color);
});
// Position just below this button. Flip up if it would overflow
// the bottom of the window.
const Rectangle b = getGlobalBounds();
f32 px = b.x;
f32 py = b.y + b.h + 4.0f;
const f32 winH = static_cast<f32>(getWindow().getWindowHeight());
const f32 winW = static_cast<f32>(getWindow().getWindowWidth());
if (py + m_popup->geth() > winH)
py = b.y - 4.0f - m_popup->geth();
if (px + m_popup->getw() > winW)
px = winW - m_popup->getw() - 4.0f;
if (px < 0) px = 4.0f;
if (py < 0) py = 4.0f;
m_popup->setPosition({ px, py });
getWindow().addWidget(*m_popup);
m_popupOpen = true;
}
void ColorButton::close_popup(void)
{
if (!m_popup) { m_popupOpen = false; return; }
getWindow().removeWidget(*m_popup);
delete m_popup;
m_popup = nullptr;
m_popupOpen = false;
}
void ColorButton::onUpdate(void)
{
// 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.
}
}
}
}

View file

@ -0,0 +1,118 @@
/*
OmniaFramework - A collection of useful functionality
Copyright (C) 2026 OmniaX-Dev
(license header same as the project)
*/
#pragma once
#include <ogfx/gui/widgets/Widget.hpp>
#include <ogfx/gui/widgets/Slider.hpp>
#include <ogfx/gui/widgets/Label.hpp>
#include <ogfx/gui/widgets/Text.hpp>
#include <ogfx/gui/widgets/Button.hpp>
#include <ogfx/gui/widgets/ColorPicker.hpp>
namespace ogfx
{
namespace gui
{
// ===========================================================================
// ColorPickerPanel — slider-driven RGBA picker.
//
// An alternate "skin" for ColorPicker: four sliders (R, G, B, A) plus a
// swatch and hex field. Useful when the user wants precise numeric input
// rather than dragging a square. Internally it owns a ColorPicker as the
// authoritative state holder (so the two forms can be cross-bound) but
// you can also use it standalone — calls to setColor/getColor just go
// straight to the underlying picker.
//
// The constructor takes the parent ColorPicker by reference so you can
// share state across multiple skins of the same color. If you want
// standalone behavior, pass an internally-owned picker (see the
// `createStandalone` factory below).
// ===========================================================================
class ColorPickerPanel : public Widget
{
public:
inline ColorPickerPanel(Window& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
ColorPickerPanel& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
Color getColor(void) const;
void setColor(const Color& c);
inline void setColorChangedCallback(std::function<void(const Color&)> cb)
{ callback_onColorChanged = std::move(cb); }
private:
void on_slider_changed(void);
void sync_sliders_from_color(const Color& c);
void layout_children(void);
private:
Slider* m_rSlider { nullptr };
Slider* m_gSlider { nullptr };
Slider* m_bSlider { nullptr };
Slider* m_aSlider { nullptr };
Label* m_rLabel { nullptr };
Label* m_gLabel { nullptr };
Label* m_bLabel { nullptr };
Label* m_aLabel { nullptr };
TextEdit* m_hexField { nullptr };
Color m_currentColor { Colors::White };
bool m_suppressCallbacks { false };
std::function<void(const Color&)> callback_onColorChanged { nullptr };
};
// ===========================================================================
// ColorButton — small clickable swatch that pops up a ColorPicker.
//
// Behaves like a button: shows the current color (over a checkerboard so
// alpha is visible), and clicking it opens a floating ColorPicker just
// below the swatch. The popup is a TopMost root widget added to the
// window; it closes when the user clicks outside it.
// ===========================================================================
class ColorButton : public Widget
{
public:
inline ColorButton(Window& window) : Widget({ 0, 0, 0, 0 }, window) { create(); }
ColorButton& create(void);
void applyTheme(const ostd::Stylesheet& theme) override;
void onDraw(ogfx::BasicRenderer2D& gfx) override;
void onMouseReleased(const Event& event) override;
void onUpdate(void) override;
inline Color getColor(void) const { return m_color; }
void setColor(const Color& c);
inline void setColorChangedCallback(std::function<void(const Color&)> cb)
{ callback_onColorChanged = std::move(cb); }
OSTD_PARAM_GETSET(f32, CheckerSize, m_checkerSize);
private:
void open_popup(void);
void close_popup(void);
void draw_checkerboard(BasicRenderer2D& gfx, const Rectangle& r);
private:
Color m_color { Colors::White };
f32 m_checkerSize { 4.0f };
// The popup is a separate root-level widget owned by us.
// We allocate it on first open and destroy it on close to keep
// the widget tree clean.
ColorPicker* m_popup { nullptr };
bool m_popupOpen { false };
std::function<void(const Color&)> callback_onColorChanged { nullptr };
};
}
}

View file

@ -285,6 +285,8 @@ namespace ogfx
case eCallback::KeyPressed: callback_onKeyPressed = std::move(callback); break;
case eCallback::KeyReleased: callback_onKeyReleased = std::move(callback); break;
case eCallback::TextEntered: callback_onTextEntered = std::move(callback); break;
case eCallback::FocusGained: callback_onFocusGained = std::move(callback); break;
case eCallback::FocusLost: callback_onFocusLost = std::move(callback); break;
case eCallback::WindowClosed: callback_onWindowClosed = std::move(callback); break;
case eCallback::WindowResized: callback_onWindowResized = std::move(callback); break;
case eCallback::WindowFocused: callback_onWindowFocused = std::move(callback); break;
@ -371,6 +373,7 @@ namespace ogfx
void Widget::__onMouseMoved(const Event& event)
{
m_mouseInside = contains(event.mouse->position_x, event.mouse->position_y);
if (event.isHandled() || !isMouseMovedEventEnabled()) return;
if (hasChildren())
m_widgets.onMouseMoved(event);

View file

@ -57,6 +57,8 @@ namespace ogfx
KeyPressed,
KeyReleased,
TextEntered,
FocusLost,
FocusGained,
WindowClosed,
WindowResized,
WindowFocused,
@ -343,6 +345,8 @@ namespace ogfx
EventCallback callback_onKeyPressed { nullptr };
EventCallback callback_onKeyReleased { nullptr };
EventCallback callback_onTextEntered { nullptr };
EventCallback callback_onFocusGained { nullptr };
EventCallback callback_onFocusLost { nullptr };
EventCallback callback_onWindowClosed { nullptr };
EventCallback callback_onWindowResized { nullptr };
EventCallback callback_onWindowFocused { nullptr };

View file

@ -24,6 +24,8 @@
#include <ogfx/ogfx.hpp>
#include <ostd/utils/Async.hpp>
#include <ogfx/gui/widgets/ColorPickerSkins.hpp>
ogfx::gui::Window::FileDialogFilterList filters = {
{ "Image files", { "png", "jpg", "jpeg", "bmp" } },
{ "All files", { "*" } }
@ -230,6 +232,8 @@ class TestWindow : public Window
t1.addWidget(m_panel2, { 500, 100 });
t1.addWidget(m_combo, { 400, 600 });
t1.addWidget(m_text, { 400, 700 });
t1.addWidget(m_colBtn, { 800, 700 });
t1.addWidget(m_colBtn2, { 950, 700 });
t2.setLayout<BoxLayout>(BoxLayout::Orientation::Vertical);
t2.getLayout()->setSpacing(16);
@ -392,6 +396,8 @@ class TestWindow : public Window
Label m_cacheMissesLbl { *this };
ComboBox m_combo { *this };
TextEdit m_text { *this };
ColorButton m_colBtn { *this };
ColorButton m_colBtn2 { *this };
enum MenuId : i32 { New = 1, Open, Save, SaveAs, Exit, CopyRaw, CopyFormatted };