Added ColorPicker Widget
This commit is contained in:
parent
999cf30c21
commit
fb9876ad22
13 changed files with 1016 additions and 4 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
% =========================
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
378
src/ogfx/gui/widgets/ColorPicker.cpp
Normal file
378
src/ogfx/gui/widgets/ColorPicker.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
141
src/ogfx/gui/widgets/ColorPicker.hpp
Normal file
141
src/ogfx/gui/widgets/ColorPicker.hpp
Normal 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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
334
src/ogfx/gui/widgets/ColorPickerSkins.cpp
Normal file
334
src/ogfx/gui/widgets/ColorPickerSkins.cpp
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/ogfx/gui/widgets/ColorPickerSkins.hpp
Normal file
118
src/ogfx/gui/widgets/ColorPickerSkins.hpp
Normal 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 };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue