/* * Copyright (c) 2012-2026 Daniele Bartolini et al. * SPDX-License-Identifier: GPL-3.0-or-later */ extern GLib.Object gtk_color_picker_new(); extern void gtk_color_picker_pick(GLib.Object picker, GLib.AsyncReadyCallback callback); extern Gdk.RGBA? gtk_color_picker_pick_finish(GLib.Object picker, GLib.AsyncResult result, ref GLib.Error error); namespace Crown { public class ColorButton : Gtk.MenuButton { public Gdk.RGBA color = { 1.0, 1.0, 1.0, 1.0 }; public const int PADDING = 4; public ColorButton() { Object(); } public void set_color(Gdk.RGBA c) { this.color = c; this.queue_draw(); } public override bool draw(Cairo.Context cr) { base.draw(cr); Gtk.Allocation alloc; this.get_allocation(out alloc); cr.set_source_rgba(this.color.red, this.color.green, this.color.blue, this.color.alpha); cr.rectangle(PADDING, PADDING, alloc.width - 2 * PADDING, alloc.height - 2 * PADDING); cr.fill(); return false; } } static void hsv_to_rgb(ref double r, ref double g, ref double b, double h, double s, double v) { assert(h >= 0.0 && s >= 0.0 && v >= 0.0); if (s == 0.0) { r = g = b = v; return; } if (h >= 1.0) h = 0.0; double hf = h * 6.0; int i = (int)MathUtils.floor(hf); double f = MathUtils.fract(hf); double p = v * (1.0 - s); double q = v * (1.0 - s * f); double t = v * (1.0 - s * (1.0 - f)); switch (i) { case 0: r = v; g = t; b = p; break; case 1: r = q; g = v; b = p; break; case 2: r = p; g = v; b = t; break; case 3: r = p; g = q; b = v; break; case 4: r = t; g = p; b = v; break; case 5: r = v; g = p; b = q; break; default: assert(false); break; } } static void rgb_to_hsv(ref double h, ref double s, ref double v, double r, double g, double b) { assert(r >= 0.0 && g >= 0.0 && b >= 0.0); double max = double.max(double.max(r, g), b); double min = double.min(double.min(r, g), b); double delta = max - min; v = max; if (max == 0.0) { s = 0.0; h = 0.0; return; } s = (delta <= 0.0) ? 0.0 : (delta / max); if (delta <= 1e-6) { h = 0.0; } else { double hh; if (max == r) { hh = (g - b) / delta; if (hh < 0.0) hh += 6.0; } else if (max == g) { hh = (b - r) / delta + 2.0; } else { // max == b hh = (r - g) / delta + 4.0; } hh = (hh / 6.0).clamp(0.0, 1.0); if (hh >= 1.0) hh = 0.0; h = hh; } } const int HS_PALETTE_SEGMENTS = 24; const Vector3 primary_colors[] = { { 1.0, 0.0, 0.0 }, { 1.0, 1.0, 0.0 }, { 0.0, 1.0, 0.0 }, { 0.0, 1.0, 1.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 1.0 } }; static double norm_angle(double a) { double x = Math.fmod(a, PI_TWO); if (x < 0) x += PI_TWO; return x; } static double angle_midpoint(double a, double b) { double delta = b - a; if (delta <= -Math.PI) delta += PI_TWO; else if (delta > Math.PI) delta -= PI_TWO; return a + delta * 0.5; } static void color_from_anchors(out Vector3 rgb, double theta) { double theta_n = norm_angle(theta); const double seg = Math.PI / 3.0; int idx = ((int)Math.floor(theta_n / seg)).clamp(0, 5); double anchor_a = (double)idx * seg; int next = (idx + 1) % 6; double t = ((theta_n - anchor_a) / seg).clamp(0.0, 1.0); rgb.x = MathUtils.lerp(primary_colors[idx].x, primary_colors[next].x, t); rgb.y = MathUtils.lerp(primary_colors[idx].y, primary_colors[next].y, t); rgb.z = MathUtils.lerp(primary_colors[idx].z, primary_colors[next].z, t); } static Cairo.MeshPattern create_circle_mesh(double cx, double cy, double r, double hsv_value) { Cairo.MeshPattern mesh = new Cairo.MeshPattern(); double angles[HS_PALETTE_SEGMENTS]; double ox[HS_PALETTE_SEGMENTS]; double oy[HS_PALETTE_SEGMENTS]; Vector3 cols[HS_PALETTE_SEGMENTS]; for (int i = 0; i < HS_PALETTE_SEGMENTS; ++i) { double theta = PI_TWO * (double)i / (double)HS_PALETTE_SEGMENTS; angles[i] = theta; ox[i] = cx + r * Math.cos(theta); oy[i] = cy - r * Math.sin(theta); Vector3 base_col; color_from_anchors(out base_col, theta); cols[i].x = base_col.x * hsv_value; cols[i].y = base_col.y * hsv_value; cols[i].z = base_col.z * hsv_value; } for (int i = 0; i < HS_PALETTE_SEGMENTS; ++i) { int inext = (i + 1) % HS_PALETTE_SEGMENTS; double angle_mid = angle_midpoint(angles[i], angles[inext]); double mx = cx + r * Math.cos(angle_mid); double my = cy - r * Math.sin(angle_mid); mesh.begin_patch(); mesh.move_to(cx, cy); mesh.line_to(ox[i], oy[i]); mesh.line_to(mx, my); mesh.line_to(ox[inext], oy[inext]); mesh.set_corner_color_rgb(0 , hsv_value , hsv_value , hsv_value ); mesh.set_corner_color_rgb(1, cols[i].x, cols[i].y, cols[i].z ); mesh.set_corner_color_rgb(2, 0.5 * (cols[i].x + cols[inext].x), 0.5 * (cols[i].y + cols[inext].y), 0.5 * (cols[i].z + cols[inext].z) ); mesh.set_corner_color_rgb(3, cols[inext].x, cols[inext].y, cols[inext].z ); mesh.end_patch(); } mesh.set_extend(Cairo.Extend.NONE); return mesh; } public class InputColor3 : InputField { public bool _dragging; public Vector3 _drag_start_rgb; public int _hs_palette_radius; public double _hs_lens_radius_scale; public double _hs_lens_small_radius_scale; public Gtk.DrawingArea _hs_palette; public Gtk.Scale _hsv_v_scale; public Gtk.GestureMultiPress _hsv_v_scale_gesture_click; public InputDouble _rgb_r; public InputDouble _rgb_g; public InputDouble _rgb_b; public InputDouble _rgb_a; public InputDouble _hsv_h; public InputDouble _hsv_s; public InputDouble _hsv_v; public InputDouble _hsv_a; public PropertyGrid _rgb_grid; public PropertyGrid _hsv_grid; public InputString _color_string; public Gtk.Button _picker_button; public GLib.Object _picker; public Gtk.GestureMultiPress _gesture_click; public Gtk.EventControllerMotion _controller_motion; public Gtk.Box _visual_box; public Gtk.Box _numeric_box; public Gtk.Box _input_box; public Gtk.Box _utils_box; public Gtk.Box _rgb_box; public Gtk.Box _hsv_box; public Gtk.Stack _rgb_hsv_stack; public Gtk.StackSwitcher _rgb_hsv_switcher; public Gtk.Popover _popover; public ColorButton _color_button; public override void set_inconsistent(bool inconsistent) { } public override bool is_inconsistent() { return false; } public override GLib.Value union_value() { return this.value; } public override void set_union_value(GLib.Value v) { this.value = (Vector3)v; } public Vector3 value { get { return Vector3(_rgb_r.value, _rgb_g.value, _rgb_b.value); } set { Vector3 rgb = (Vector3)value; _rgb_r.value = rgb.x; _rgb_g.value = rgb.y; _rgb_b.value = rgb.z; } } public int palette_size_request() { double palette_size = 2.0 * _hs_palette_radius; double lens_size = 2.0 * (_hs_palette_radius * _hs_lens_radius_scale); return (int)(palette_size + lens_size); } public InputColor3() { _dragging = false; _drag_start_rgb = Vector3(1.0, 1.0, 1.0); _hs_palette_radius = 100; _hs_lens_radius_scale = 0.1; _hs_lens_small_radius_scale = _hs_lens_radius_scale * 0.65; _hs_palette = new Gtk.DrawingArea(); _hs_palette.halign = Gtk.Align.START; _hs_palette.draw.connect(on_draw_circle); _hs_palette.size_allocate.connect(on_circle_size_allocate); int sr = palette_size_request(); _hs_palette.set_size_request(sr, sr); _gesture_click = new Gtk.GestureMultiPress(_hs_palette); _gesture_click.set_button(0); _gesture_click.pressed.connect(on_hs_circle_button_pressed); _gesture_click.released.connect(on_hs_circle_button_released); _controller_motion = new Gtk.EventControllerMotion(_hs_palette); _controller_motion.motion.connect(on_hs_circle_motion); Gtk.Adjustment adj = new Gtk.Adjustment(0.0, 0.0, 1.0, 0.01, 0.1, 0.0); _hsv_v_scale = new Gtk.Scale(Gtk.Orientation.VERTICAL, adj); _hsv_v_scale.get_style_context().add_class("hsv-v-scale"); _hsv_v_scale.halign = Gtk.Align.END; _hsv_v_scale.draw_value = false; _hsv_v_scale.set_digits(3); _hsv_v_scale.halign = Gtk.Align.START; _hsv_v_scale.value_changed.connect(on_hsv_v_scale_value_changed); _hsv_v_scale_gesture_click = new Gtk.GestureMultiPress(_hsv_v_scale); _hsv_v_scale_gesture_click.pressed.connect(on_hsv_v_scale_pressed); _hsv_v_scale_gesture_click.released.connect(on_hsv_v_scale_released); _rgb_r = new InputDouble(1.0, 0.0, 1.0); _rgb_g = new InputDouble(1.0, 0.0, 1.0); _rgb_b = new InputDouble(1.0, 0.0, 1.0); _rgb_a = new InputDouble(1.0, 0.0, 1.0); _rgb_r.value_changed.connect(on_rgb_value_changed); _rgb_g.value_changed.connect(on_rgb_value_changed); _rgb_b.value_changed.connect(on_rgb_value_changed); _hsv_h = new InputDouble(0.0, 0.0, 1.0); _hsv_s = new InputDouble(0.0, 0.0, 1.0); _hsv_v = new InputDouble(1.0, 0.0, 1.0); _hsv_a = new InputDouble(_rgb_a.value, 0.0, 1.0); _hsv_h.value_changed.connect(on_hsv_value_changed); _hsv_s.value_changed.connect(on_hsv_value_changed); _hsv_v.value_changed.connect(on_hsv_value_changed); _rgb_grid = new PropertyGrid(); _rgb_grid.row_homogeneous = false; _rgb_grid.label_width_chars = 8; _rgb_grid.add_row("Red", _rgb_r); _rgb_grid.add_row("Green", _rgb_g); _rgb_grid.add_row("Blue", _rgb_b); _rgb_grid.add_row("Alpha", _rgb_a); _hsv_grid = new PropertyGrid(); _hsv_grid.row_homogeneous = false; _hsv_grid.label_width_chars = 8; _hsv_grid.add_row("Hue", _hsv_h); _hsv_grid.add_row("Saturation", _hsv_s); _hsv_grid.add_row("Value", _hsv_v); _hsv_grid.add_row("Alpha", _hsv_a); _color_string = new InputString(); _color_string.set_tooltip_text("Lua color code."); _color_string.value_changed.connect(on_color_string_value_changed); _picker_button = new Gtk.Button.from_icon_name("color-select-symbolic"); _picker_button.set_tooltip_text("Pick a color from the screen."); _picker_button.clicked.connect(on_picker_button_clicked); _picker = gtk_color_picker_new(); _rgb_hsv_stack = new Gtk.Stack(); _rgb_hsv_stack.add_titled(_rgb_grid, "rgb", "RGB"); _rgb_hsv_stack.add_titled(_hsv_grid, "hsv", "HSV"); _rgb_hsv_stack.map.connect(() => { _rgb_hsv_stack.set_visible_child_name("hsv"); }); _rgb_hsv_switcher = new Gtk.StackSwitcher(); _rgb_hsv_switcher.set_stack(_rgb_hsv_stack); _visual_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 0); _visual_box.margin = 0; _visual_box.pack_start(_hs_palette, true, true, 0); _visual_box.pack_start(_hsv_v_scale, false, false, 0); _numeric_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 8); _numeric_box.pack_start(_rgb_hsv_switcher); _numeric_box.pack_start(_rgb_hsv_stack); _utils_box = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 4); _utils_box.pack_start(_color_string); _utils_box.pack_start(_picker_button, false); _input_box = new Gtk.Box(Gtk.Orientation.VERTICAL, 16 /* Avoid slider overlapping buttons. */); _input_box.margin = 12; _input_box.pack_start(_visual_box); _input_box.pack_start(_numeric_box); _input_box.pack_start(_utils_box); _popover = new Gtk.Popover(null); _popover.add(_input_box); // _popover.has_arrow = false; _input_box.show_all(); _color_button = new ColorButton(); _color_button.set_tooltip_text("Choose a color."); _color_button.can_focus = false; _color_button.set_popover(_popover); this.value_changed.connect(on_value_changed); this.add(_color_button); on_value_changed(); } public void palette_radius(out double full_radius, out double radius) { double w = (double)this._hs_palette.get_allocated_width(); double h = (double)this._hs_palette.get_allocated_height(); full_radius = Math.fmin(w, h) * 0.5; radius = full_radius - full_radius * _hs_lens_radius_scale; } public bool on_draw_circle(Cairo.Context cr) { double w = (double)this._hs_palette.get_allocated_width(); double h = (double)this._hs_palette.get_allocated_height(); double cx = w / 2.0; double cy = h / 2.0; double full_radius; double radius; palette_radius(out full_radius, out radius); Cairo.MeshPattern mesh = create_circle_mesh(cx, cy, radius, _hsv_v.value); cr.save(); cr.arc(cx, cy, radius, 0, PI_TWO); cr.clip(); cr.set_source(mesh); cr.paint(); cr.restore(); // Draw outline. cr.set_line_width(1.0); cr.arc(cx, cy, radius - 0.5, 0, PI_TWO); cr.set_source_rgb(0.5, 0.5, 0.5); cr.stroke(); double scale = _dragging ? _hs_lens_radius_scale : _hs_lens_small_radius_scale; draw_lens(cr, full_radius * scale); return false; } public bool draw_lens(Cairo.Context cr, double radius) { Vector2 p = xy_from_hs({ _hsv_h.value, _hsv_s.value }); cr.save(); cr.arc(p.x, p.y, radius, 0, PI_TWO); cr.clip(); cr.set_source_rgb(_rgb_r.value, _rgb_g.value, _rgb_b.value); cr.paint(); cr.restore(); // Draw outline. cr.set_line_width(1.0); cr.arc(p.x, p.y, radius - 0.5, 0, PI_TWO); cr.set_source_rgb(0.5, 0.5, 0.5); cr.stroke(); return false; } public void on_circle_size_allocate(Gtk.Allocation allocation) { int dia = (int)Math.fmax(1.0, Math.fmin ((double)allocation.width, (double)allocation.height)); _hsv_v_scale.set_size_request(-1, dia); } public void on_hsv_v_scale_value_changed() { double v = this._hsv_v_scale.get_value(); if (v < 0.0) v = 0.0; if (v > 1.0) v = 1.0; _hsv_v.value = 1.0 - v; _hs_palette.queue_draw(); } public void on_rgb_value_changed() { double h = 0.0; double s = 0.0; double v = 1.0; disconnect_hsv(); rgb_to_hsv(ref h , ref s , ref v , _rgb_r.value , _rgb_g.value , _rgb_b.value ); _hsv_h.value = h; _hsv_s.value = s; _hsv_v.value = v; _hsv_v_scale.set_value(1.0 - _hsv_v.value); connect_hsv(); _hs_palette.queue_draw(); value_changed(this, (int)!_dragging); } public void on_hsv_value_changed() { double r = 1.0; double g = 1.0; double b = 1.0; disconnect_rgb(); hsv_to_rgb(ref r , ref g , ref b , _hsv_h.value , _hsv_s.value , _hsv_v.value ); _rgb_r.value = r; _rgb_g.value = g; _rgb_b.value = b; disconnect_hsv(); _hsv_v_scale.set_value(1.0 - _hsv_v.value); connect_hsv(); connect_rgb(); _hs_palette.queue_draw(); value_changed(this, (int)!_dragging); } public void disconnect_rgb() { _rgb_r.value_changed.disconnect(on_rgb_value_changed); _rgb_g.value_changed.disconnect(on_rgb_value_changed); _rgb_b.value_changed.disconnect(on_rgb_value_changed); } public void connect_rgb() { _rgb_r.value_changed.connect(on_rgb_value_changed); _rgb_g.value_changed.connect(on_rgb_value_changed); _rgb_b.value_changed.connect(on_rgb_value_changed); } public void disconnect_hsv() { _hsv_h.value_changed.disconnect(on_hsv_value_changed); _hsv_s.value_changed.disconnect(on_hsv_value_changed); _hsv_v.value_changed.disconnect(on_hsv_value_changed); } public void connect_hsv() { _hsv_h.value_changed.connect(on_hsv_value_changed); _hsv_s.value_changed.connect(on_hsv_value_changed); _hsv_v.value_changed.connect(on_hsv_value_changed); } public void on_value_changed() { Vector3 c = this.value; _color_button.set_color({ c.x, c.y, c.z, 1.0 }); _color_string.value_changed.disconnect(on_color_string_value_changed); _color_string.value = "Color4(%d, %d, %d, %d)".printf((int)(_rgb_r.value * 255.0) , (int)(_rgb_g.value * 255.0) , (int)(_rgb_b.value * 255.0) , (int)(_rgb_a.value * 255.0) ); _color_string.value_changed.connect(on_color_string_value_changed); } // Returns the hue (x) and saturation (y) of the specified point @a p on a HS circle. public Vector2 hs_from_xy(Vector2 p) { double w = (double)this._hs_palette.get_allocated_width(); double h = (double)this._hs_palette.get_allocated_height(); double radius = w * 0.5; double cx = (p.x - radius) / w; double cy = (h - p.y - radius) / h; Vector2 hs = { 0.0, 0.0 }; hs.x = Math.atan2(cy, cx); if (hs.x < 0) hs.x += PI_TWO; hs.x /= PI_TWO; hs.y = Vector2(cx, cy).length() * 2.0; return hs; } public Vector2 xy_from_hs(Vector2 hs) { double full_radius; double radius; palette_radius(out full_radius, out radius); double cx = Math.cos(PI_TWO * hs.x) * hs.y * radius; double cy = Math.sin(PI_TWO * hs.x) * hs.y * radius; cx = cx + full_radius; cy = full_radius - cy; return { cx, cy }; } public void on_hs_circle_button_pressed(int n_press, double x, double y) { _dragging = true; _drag_start_rgb = this.value; Vector2 hs = hs_from_xy({ x, y }); _hsv_h.value = hs.x; _hsv_s.value = hs.y; _hs_palette.get_window().set_cursor(new Gdk.Cursor.from_name(Gdk.Display.get_default(), "none")); _hs_palette.queue_draw(); } public void on_hs_circle_button_released(int n_press, double x, double y) { double h = 0.0; double s = 0.0; double v = 1.0; Vector3 current_value = this.value; this.value = _drag_start_rgb; _dragging = false; disconnect_rgb(); disconnect_hsv(); this.value = current_value; rgb_to_hsv(ref h , ref s , ref v , _rgb_r.value , _rgb_g.value , _rgb_b.value ); _hsv_h.value = h; _hsv_s.value = s; _hsv_v.value = v; _hsv_v_scale.set_value(1.0 - _hsv_v.value); connect_hsv(); connect_rgb(); value_changed(this, (int)!_dragging); _hs_palette.get_window().set_cursor(new Gdk.Cursor.from_name(Gdk.Display.get_default(), "default")); _hs_palette.queue_draw(); } public void on_hs_circle_motion(double x, double y) { Vector2 hs = hs_from_xy({ x, y }); _hsv_h.value = hs.x; _hsv_s.value = hs.y; } public void on_color_picked(GLib.Object? object, GLib.AsyncResult result) { GLib.Error error = null; Gdk.RGBA? rgba = gtk_color_picker_pick_finish(_picker, result, ref error); if (rgba != null) this.value = Vector3(rgba.red, rgba.green, rgba.blue); } public void on_picker_button_clicked() { gtk_color_picker_pick(_picker, on_color_picked); } public void on_color_string_value_changed() { int rgba[4]; if (_color_string.value.scanf("Color4(%d, %d, %d, %d)" , out rgba[0] , out rgba[1] , out rgba[2] , out rgba[3] ) == 4) { _rgb_r.value = rgba[0] / 255.0; _rgb_g.value = rgba[1] / 255.0; _rgb_b.value = rgba[2] / 255.0; _rgb_a.value = rgba[3] / 255.0; } } public void on_hsv_v_scale_pressed(int n_press, double x, double y) { _dragging = true; _drag_start_rgb = this.value; } public void on_hsv_v_scale_released(int n_press, double x, double y) { double h = 0.0; double s = 0.0; double v = 1.0; Vector3 current_value = this.value; this.value = _drag_start_rgb; _dragging = false; disconnect_rgb(); disconnect_hsv(); this.value = current_value; rgb_to_hsv(ref h , ref s , ref v , _rgb_r.value , _rgb_g.value , _rgb_b.value ); _hsv_h.value = h; _hsv_s.value = s; _hsv_v.value = v; _hsv_v_scale.set_value(1.0 - _hsv_v.value); connect_hsv(); connect_rgb(); value_changed(this, (int)!_dragging); } } } /* namespace Crown */