diff --git a/CMakeLists.txt b/CMakeLists.txt index 89b7fe6..b40feba 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/extra/DefaultThemeDark.oss b/extra/DefaultThemeDark.oss index 51d3ec3..b4e40be 100644 --- a/extra/DefaultThemeDark.oss +++ b/extra/DefaultThemeDark.oss @@ -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 +} +% ========================= diff --git a/src/ogfx/gui/FocusManager.cpp b/src/ogfx/gui/FocusManager.cpp index 9575dc3..4def4eb 100644 --- a/src/ogfx/gui/FocusManager.cpp +++ b/src/ogfx/gui/FocusManager.cpp @@ -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; diff --git a/src/ogfx/gui/WidgetManager.cpp b/src/ogfx/gui/WidgetManager.cpp index b91761a..9f730fb 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,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); diff --git a/src/ogfx/gui/Window.cpp b/src/ogfx/gui/Window.cpp index 1a26513..369b1c8 100644 --- a/src/ogfx/gui/Window.cpp +++ b/src/ogfx/gui/Window.cpp @@ -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; diff --git a/src/ogfx/gui/Window.hpp b/src/ogfx/gui/Window.hpp index 13275b7..48d9459 100644 --- a/src/ogfx/gui/Window.hpp +++ b/src/ogfx/gui/Window.hpp @@ -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); } diff --git a/src/ogfx/gui/widgets/ColorPicker.cpp b/src/ogfx/gui/widgets/ColorPicker.cpp new file mode 100644 index 0000000..9c52bf4 --- /dev/null +++ b/src/ogfx/gui/widgets/ColorPicker.cpp @@ -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 +#include + +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(theme, "stripWidth", m_stripWidth)); + setRegionSpacing(getThemeValue(theme, "regionSpacing", m_regionSpacing)); + setCursorRadius(getThemeValue(theme, "cursorRadius", m_cursorRadius)); + setInfoColumnWidth(getThemeValue(theme, "infoColumnWidth", m_infoColumnW)); + setCheckerSize(getThemeValue(theme, "checkerSize", m_checkerSize)); + setGradientSteps(getThemeValue(theme, "gradientSteps", m_gradientSteps)); + enableAlphaChannel(getThemeValue(theme, "showAlpha", m_showAlpha)); + enableColorPreview(getThemeValue(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(std::round(r * 255.0f)), + static_cast(std::round(g * 255.0f)), + static_cast(std::round(b * 255.0f)), + static_cast(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(std::round(r * 255.0f)), + static_cast(std::round(g * 255.0f)), + static_cast(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(N); + + for (i32 i = 0; i < N; ++i) + { + const f32 s = (i + 0.5f) / static_cast(N); + f32 rr, gg, bb; + Color::HSVtoRGB(m_hue, s, 1.0f, rr, gg, bb); + const Color topColor( + static_cast(std::round(rr * 255.0f)), + static_cast(std::round(gg * 255.0f)), + static_cast(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(std::round(rr * 255.0f)); + top.g = static_cast(std::round(gg * 255.0f)); + top.b = static_cast(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(std::ceil(r.w / cs)); + const i32 rows = static_cast(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()); + } + } +} diff --git a/src/ogfx/gui/widgets/ColorPicker.hpp b/src/ogfx/gui/widgets/ColorPicker.hpp new file mode 100644 index 0000000..f8b9e60 --- /dev/null +++ b/src/ogfx/gui/widgets/ColorPicker.hpp @@ -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 . +*/ + +#pragma once + +#include +#include + +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 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 callback_onColorChanged { nullptr }; + }; + } +} diff --git a/src/ogfx/gui/widgets/ColorPickerSkins.cpp b/src/ogfx/gui/widgets/ColorPickerSkins.cpp new file mode 100644 index 0000000..9e51f15 --- /dev/null +++ b/src/ogfx/gui/widgets/ColorPickerSkins.cpp @@ -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 +#include + +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(std::round(m_rSlider->getValue())), + static_cast(std::round(m_gSlider->getValue())), + static_cast(std::round(m_bSlider->getValue())), + static_cast(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(std::ceil(swatch.w / cs)); + const i32 rows = static_cast(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(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(std::ceil(r.w / cs)); + const i32 rows = static_cast(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(getWindow().getWindowHeight()); + const f32 winW = static_cast(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. + } + } + } +} diff --git a/src/ogfx/gui/widgets/ColorPickerSkins.hpp b/src/ogfx/gui/widgets/ColorPickerSkins.hpp new file mode 100644 index 0000000..82b6ec8 --- /dev/null +++ b/src/ogfx/gui/widgets/ColorPickerSkins.hpp @@ -0,0 +1,118 @@ +/* + OmniaFramework - A collection of useful functionality + Copyright (C) 2026 OmniaX-Dev + (license header same as the project) +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include + +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 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 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 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 callback_onColorChanged { nullptr }; + }; + } +} diff --git a/src/ogfx/gui/widgets/Widget.cpp b/src/ogfx/gui/widgets/Widget.cpp index 7f469c1..190f77e 100644 --- a/src/ogfx/gui/widgets/Widget.cpp +++ b/src/ogfx/gui/widgets/Widget.cpp @@ -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); diff --git a/src/ogfx/gui/widgets/Widget.hpp b/src/ogfx/gui/widgets/Widget.hpp index 6a20e73..8b658cb 100644 --- a/src/ogfx/gui/widgets/Widget.hpp +++ b/src/ogfx/gui/widgets/Widget.hpp @@ -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 }; diff --git a/src/test/GuiTest.cpp b/src/test/GuiTest.cpp index d0e2a1b..3b7d384 100644 --- a/src/test/GuiTest.cpp +++ b/src/test/GuiTest.cpp @@ -24,6 +24,8 @@ #include #include +#include + 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::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 };