From a1a8281a6f72ee7f1bcedf442f6ade48da8ffef4 Mon Sep 17 00:00:00 2001 From: salmonik Date: Tue, 3 Nov 2026 23:26:50 +0100 Subject: GUI: add inline colour picker control Replace the six RGB sliders on the World tint card with a CK_COLOR control: each row shows a live swatch and expands an inline picker with a saturation/value square, a hue bar, and hex/RGB preview. The picker grows the card in place so it scrolls/clips like any other row, keeps HSV state while open (grey does not lose its hue), writes the shared 0-255 RGB triple used by the config, and saves on mouse release. Costs ~1.2k small rects per frame only while a picker is open; no texture interface needed. --- CONTEXT.md | 7 ++ src/gui.cpp | 326 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 319 insertions(+), 14 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index ce4a02e..b48100b 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -144,6 +144,13 @@ vocabulary / decisions. `x`, `y`, `category` and an `expanded` bitmask (card index order). Every feature now persists an `active` and `bind` pair, including movement, melee and spread. +- **Colour control (`CK_COLOR`)**: row shows a live swatch; clicking the row + expands an inline picker (saturation/value square + hue bar + hex/RGB + preview) instead of three RGB sliders, growing the card in place so it + scrolls and clips like any other row. The picker keeps HSV state so grey + colours do not lose their hue; dragging writes the 0-255 RGB triple shared + with the config and saves on mouse release. Only one picker is open at a + time; closing the GUI or collapsing the card hides it. - Config writes are atomic: `config.tmp` is written and flushed, then `rename()`d over the real file. A failed write raises an error toast. diff --git a/src/gui.cpp b/src/gui.cpp index 9d27a09..9981f25 100644 --- a/src/gui.cpp +++ b/src/gui.cpp @@ -182,7 +182,8 @@ enum CtrlKind { CK_BIND, CK_SECTION, CK_INFO, - CK_BUTTON + CK_BUTTON, + CK_COLOR }; enum CtrlAction { ACT_NONE = 0, ACT_RELOAD, ACT_RESET }; @@ -193,9 +194,13 @@ struct Ctrl { const char *desc; bool *bval; // CK_TOGGLE int *ival; // CK_SLIDER + int *rgb; // CK_COLOR: three ints, 0..255 int min, max; int action; // CK_BUTTON bool zero_unlimited; // slider: value 0 displays as "All" + // CK_COLOR picker state, kept while the picker is open so grey colours do + // not lose their hue when the user drags value/saturation. + float hue, sat, val; }; struct Card { @@ -247,6 +252,10 @@ static void add_ctrl(int card, int kind, const char *label, const char *desc, c.max = max; c.action = action; c.zero_unlimited = false; + c.rgb = nullptr; + c.hue = 0.0f; + c.sat = 0.0f; + c.val = 1.0f; g_cards[card].n++; } static void add_toggle(int card, const char *label, const char *desc, @@ -267,6 +276,12 @@ static void add_button(int card, const char *label, const char *desc, int action) { add_ctrl(card, CK_BUTTON, label, desc, 0, 0, nullptr, nullptr, action); } +static void add_color(int card, const char *label, const char *desc, + int *rgb) { + int i = g_nctrl; + add_ctrl(card, CK_COLOR, label, desc, 0, 255, nullptr, nullptr, ACT_NONE); + g_ctrls[i].rgb = rgb; +} static int ctrl_val(const Ctrl &c) { if (c.kind == CK_TOGGLE && c.bval) @@ -381,19 +396,11 @@ static void build_cards() { add_toggle(tint, "World brushes", "Tint world textures (sky and water excluded from this part).", &g_tint_world_on); - add_slider(tint, "World red", "Red channel of the world tint (0-255).", 0, - 255, &g_tint_world_rgb[0]); - add_slider(tint, "World green", "Green channel of the world tint (0-255).", 0, - 255, &g_tint_world_rgb[1]); - add_slider(tint, "World blue", "Blue channel of the world tint (0-255).", 0, - 255, &g_tint_world_rgb[2]); + add_color(tint, "World colour", "Click the swatch to open the picker.", + g_tint_world_rgb); add_toggle(tint, "Skybox", "Tint the skybox textures.", &g_tint_sky_on); - add_slider(tint, "Sky red", "Red channel of the sky tint (0-255).", 0, 255, - &g_tint_sky_rgb[0]); - add_slider(tint, "Sky green", "Green channel of the sky tint (0-255).", 0, - 255, &g_tint_sky_rgb[1]); - add_slider(tint, "Sky blue", "Blue channel of the sky tint (0-255).", 0, 255, - &g_tint_sky_rgb[2]); + add_color(tint, "Sky colour", "Click the swatch to open the picker.", + g_tint_sky_rgb); int spread = add_card(3, &g_spread, "Spread control", "Choose a server shot seed that tightens the cone.", @@ -436,6 +443,8 @@ static int g_scroll_drag_dy = 0; static float g_scroll_drag_start = 0.0f; static int g_hover_slider = -1; static int g_slider_drag = -1; +static int g_picker_ctrl = -1; // CK_COLOR control with its picker expanded +static int g_picker_drag = 0; // 1 = SV square, 2 = hue bar static int g_capture_card = -1; static QAngle g_frozen{0, 0, 0}; static int g_mouse_x = 0, g_mouse_y = 0; @@ -821,7 +830,40 @@ static int button_w(const char *label) { return bw; } +static int label_row_h(const Ctrl &c); +static bool picker_open_for(const Ctrl &c); +static int picker_panel_h(); +static bool card_in_category(int i); +static Rect card_rect(int index, int y); +static int card_h(const Card &c); + static int row_h(const Ctrl &c) { + int h = label_row_h(c); + if (picker_open_for(c)) + h += picker_panel_h(); + return h; +} + +// --------------------------------------------------------------------------- +// Colour picker (CK_COLOR) +// --------------------------------------------------------------------------- + +static constexpr int PICKER_SV_W = 180; +static constexpr int PICKER_SV_H = 110; +static constexpr int PICKER_HUE_H = 12; +static constexpr int PICKER_GAP = 8; + +static bool picker_open_for(const Ctrl &c) { + return c.kind == CK_COLOR && g_picker_ctrl >= 0 && + &c == &g_ctrls[g_picker_ctrl]; +} + +static int picker_panel_h() { + return S((float)(PICKER_GAP + PICKER_SV_H + PICKER_GAP + PICKER_HUE_H + + PICKER_GAP)); +} + +static int label_row_h(const Ctrl &c) { if (c.kind == CK_BUTTON) return S(32.0f); int maxw = (c.kind == CK_INFO || c.kind == CK_SECTION) @@ -837,6 +879,207 @@ static int row_h(const Ctrl &c) { return h < minh ? minh : h; } +static void hsv_to_rgb(float h, float s, float v, int &r, int &g, int &b) { + if (h < 0.0f) h = 0.0f; + h = fmodf(h, 360.0f); + if (h < 0.0f) h += 360.0f; + if (s < 0.0f) s = 0.0f; + if (s > 1.0f) s = 1.0f; + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + float c = v * s; + float x = c * (1.0f - fabsf(fmodf(h / 60.0f, 2.0f) - 1.0f)); + float m = v - c; + float rr = 0.0f, gg = 0.0f, bb = 0.0f; + if (h < 60.0f) { + rr = c; gg = x; + } else if (h < 120.0f) { + rr = x; gg = c; + } else if (h < 180.0f) { + gg = c; bb = x; + } else if (h < 240.0f) { + gg = x; bb = c; + } else if (h < 300.0f) { + rr = x; bb = c; + } else { + rr = c; bb = x; + } + r = (int)((rr + m) * 255.0f + 0.5f); + g = (int)((gg + m) * 255.0f + 0.5f); + b = (int)((bb + m) * 255.0f + 0.5f); +} + +static void rgb_to_hsv(int r, int g, int b, float &h, float &s, float &v) { + float rf = r / 255.0f, gf = g / 255.0f, bf = b / 255.0f; + float mx = fmaxf(rf, fmaxf(gf, bf)); + float mn = fminf(rf, fminf(gf, bf)); + float d = mx - mn; + v = mx; + s = mx > 0.0f ? d / mx : 0.0f; + if (d <= 0.0f) { + h = 0.0f; + return; + } + if (mx == rf) + h = 60.0f * fmodf((gf - bf) / d, 6.0f); + else if (mx == gf) + h = 60.0f * ((bf - rf) / d + 2.0f); + else + h = 60.0f * ((rf - gf) / d + 4.0f); + if (h < 0.0f) + h += 360.0f; +} + +struct PickerRects { + Rect panel, sv, hue, preview; +}; + +static PickerRects picker_rects(const Rect &cr, int row_y, int base_h) { + PickerRects p{}; + int px = cr.x + S(16.0f); + int py = row_y + base_h; + int pw = card_content_w() - S(32.0f); + if (pw > S(340.0f)) pw = S(340.0f); + p.panel = Rect{px, py, pw, picker_panel_h()}; + int preview_w = S(84.0f); + int svw = S((float)PICKER_SV_W); + if (svw + preview_w + S(16.0f) > pw) + svw = pw - preview_w - S(16.0f); + if (svw < S(60.0f)) + svw = S(60.0f); + p.sv = Rect{px + S(PICKER_GAP), py + S(PICKER_GAP), svw, + S((float)PICKER_SV_H)}; + p.hue = Rect{p.sv.x, p.sv.y + p.sv.h + S(PICKER_GAP), svw, + S((float)PICKER_HUE_H)}; + p.preview = Rect{p.sv.x + p.sv.w + S(12.0f), p.sv.y, preview_w, S(52.0f)}; + return p; +} + +// Widget row/card used to map a control index back to screen space. Needed by +// the drag handler, which runs outside click/layout context. +static bool control_rect(int ctrl_index, Rect &out_card, Rect &out_row) { + Rect content{g_win_x + S(190.0f), g_win_y + S(38.0f), + g_win_w - S(190.0f), g_win_h - S(38.0f) - S(30.0f)}; + int y = content.y - (int)g_scroll + S(8.0f); + for (int i = 0; i < g_ncards; i++) { + if (!card_in_category(i)) + continue; + Card &c = g_cards[i]; + Rect cr = card_rect(i, y); + if (c.expanded) { + int by = cr.y + header_h(c) + S(6.0f); + for (int j = 0; j < c.n; j++) { + Ctrl &ct = g_ctrls[c.c0 + j]; + int rh = row_h(ct); + if (c.c0 + j == ctrl_index) { + out_card = cr; + out_row = Rect{cr.x, by, cr.w, rh}; + return true; + } + by += rh; + } + } + y += card_h(c); + } + return false; +} + +static bool picker_rects_for(int ctrl_index, PickerRects &out) { + if (ctrl_index < 0 || ctrl_index >= g_nctrl) + return false; + Rect card, row; + if (!control_rect(ctrl_index, card, row)) + return false; + Ctrl &ct = g_ctrls[ctrl_index]; + out = picker_rects(card, row.y, label_row_h(ct)); + return true; +} + +static void picker_apply(Ctrl &ct, const PickerRects &p) { + if (!ct.rgb || p.sv.w <= 0 || p.hue.w <= 0) + return; + if (g_picker_drag == 1) { + float s = (float)(g_mouse_x - p.sv.x) / (float)p.sv.w; + float v = 1.0f - (float)(g_mouse_y - p.sv.y) / (float)p.sv.h; + if (s < 0.0f) s = 0.0f; + if (s > 1.0f) s = 1.0f; + if (v < 0.0f) v = 0.0f; + if (v > 1.0f) v = 1.0f; + ct.sat = s; + ct.val = v; + } else if (g_picker_drag == 2) { + float h = 360.0f * (float)(g_mouse_x - p.hue.x) / (float)p.hue.w; + if (h < 0.0f) h = 0.0f; + if (h >= 360.0f) h = 359.9f; + ct.hue = h; + } + hsv_to_rgb(ct.hue, ct.sat, ct.val, ct.rgb[0], ct.rgb[1], ct.rgb[2]); +} + +static void draw_picker(const Rect &clip, const Rect &cr, const Ctrl &ct, + int row_y) { + PickerRects p = picker_rects(cr, row_y, label_row_h(ct)); + fill(p.panel, Col{20, 23, 29, 250}, &clip); + outline(p.panel, C_BORDER, &clip); + + // Saturation/value field: solid rows of a white->hue gradient scaled by V. + int sx = S(4.0f); + int sy = S(4.0f); + if (sx < 2) sx = 2; + if (sy < 2) sy = 2; + for (int yy = 0; yy < p.sv.h; yy += sy) { + float v = 1.0f - ((float)yy + (float)sy * 0.5f) / (float)p.sv.h; + int rh = sy < p.sv.h - yy ? sy : p.sv.h - yy; + for (int xx = 0; xx < p.sv.w; xx += sx) { + float s = ((float)xx + (float)sx * 0.5f) / (float)p.sv.w; + int r, g, b; + hsv_to_rgb(ct.hue, s, v, r, g, b); + int rw = sx < p.sv.w - xx ? sx : p.sv.w - xx; + fill(Rect{p.sv.x + xx, p.sv.y + yy, rw, rh}, Col{r, g, b, 255}, &clip); + } + } + outline(p.sv, Col{0, 0, 0, 255}, &clip); + + // Hue bar. + int hx = S(4.0f); + if (hx < 2) hx = 2; + for (int xx = 0; xx < p.hue.w; xx += hx) { + float h = 360.0f * ((float)xx + (float)hx * 0.5f) / (float)p.hue.w; + int r, g, b; + hsv_to_rgb(h, 1.0f, 1.0f, r, g, b); + int rw = hx < p.hue.w - xx ? hx : p.hue.w - xx; + fill(Rect{p.hue.x + xx, p.hue.y, rw, p.hue.h}, Col{r, g, b, 255}, &clip); + } + outline(p.hue, Col{0, 0, 0, 255}, &clip); + + // Selection markers. + int mx = p.sv.x + (int)(ct.sat * (float)p.sv.w); + int my = p.sv.y + (int)((1.0f - ct.val) * (float)p.sv.h); + Rect svm{mx - S(4.0f), my - S(4.0f), S(8.0f), S(8.0f)}; + outline(svm, Col{0, 0, 0, 255}, &clip); + Rect svm2{mx - S(3.0f), my - S(3.0f), S(6.0f), S(6.0f)}; + outline(svm2, Col{255, 255, 255, 255}, &clip); + + int hxm = p.hue.x + (int)((ct.hue / 360.0f) * (float)p.hue.w); + line(hxm, p.hue.y - S(2.0f), hxm, p.hue.y + p.hue.h + S(2.0f), + Col{255, 255, 255, 255}); + + // Preview swatch and hex value. + int pr = ct.rgb ? ct.rgb[0] : 255; + int pg = ct.rgb ? ct.rgb[1] : 255; + int pb = ct.rgb ? ct.rgb[2] : 255; + fill(p.preview, Col{pr, pg, pb, 255}, &clip); + outline(p.preview, C_BORDER, &clip); + char hex[16]; + snprintf(hex, sizeof(hex), "#%02X%02X%02X", pr, pg, pb); + text_in(clip, p.preview.x, p.preview.y + p.preview.h + S(8.0f), hex, + g_font_body, g_body_h, p.preview.w, C_TEXT); + char rgb[48]; + snprintf(rgb, sizeof(rgb), "%d %d %d", pr, pg, pb); + text_in(clip, p.preview.x, p.preview.y + p.preview.h + S(8.0f) + g_body_h, + rgb, g_font_body, g_body_h, p.preview.w, C_DIM); +} + static int body_h(const Card &c) { if (!c.expanded) return 0; @@ -877,6 +1120,8 @@ static void close_gui() { g_dragging = false; g_scroll_drag = false; g_slider_drag = -1; + g_picker_ctrl = -1; + g_picker_drag = 0; g_reset_armed = false; input_clear_mouse_states(); if (g_sdl_ready) { @@ -1115,6 +1360,37 @@ static void handle_mouse_click() { } return; } + if (ct.kind == CK_COLOR) { + int idx = c.c0 + j; + int base = label_row_h(ct); + bool is_open = (g_picker_ctrl == idx); + if (is_open) { + PickerRects p = picker_rects(cr, by, base); + if (p.sv.has(mx, my)) { + g_picker_drag = 1; + picker_apply(g_ctrls[idx], p); + return; + } + if (p.hue.has(mx, my)) { + g_picker_drag = 2; + picker_apply(g_ctrls[idx], p); + return; + } + } + Rect sw{cr.x + cr.w - S(96.0f), by + (base - S(20.0f)) / 2, + S(72.0f), S(20.0f)}; + if (my < by + base || sw.has(mx, my)) { + if (is_open) { + g_picker_ctrl = -1; + } else { + g_picker_ctrl = idx; + if (ct.rgb) + rgb_to_hsv(ct.rgb[0], ct.rgb[1], ct.rgb[2], ct.hue, ct.sat, + ct.val); + } + } + return; + } if (ct.kind == CK_BUTTON) { int bw = button_w(ct.label); Rect b{cr.x + cr.w - S(16.0f) - bw, @@ -1291,6 +1567,17 @@ void gui_handle_input() { } } + if (g_picker_drag) { + if (!g_mouse_down) { + g_picker_drag = 0; + config_save(); + } else if (g_picker_ctrl >= 0) { + PickerRects p; + if (picker_rects_for(g_picker_ctrl, p)) + picker_apply(g_ctrls[g_picker_ctrl], p); + } + } + if (g_scroll_drag) { if (!g_mouse_down) { g_scroll_drag = false; @@ -1472,7 +1759,8 @@ static void draw_card(const Rect &clip, int index, int y) { wrap_text(ct.label, maxw, g_body_h, lines); int lh = g_body_h + S(2.0f); int text_h = (int)lines.size() * lh - S(2.0f); - int ly = by + (rh - text_h) / 2; + int base_h = label_row_h(ct); + int ly = by + (base_h - text_h) / 2; draw_wrapped(clip, cr.x + S(16.0f), ly, ct.label, g_font_body, g_body_h, maxw, C_TEXT, S(2.0f)); if (ct.kind == CK_TOGGLE) { @@ -1483,6 +1771,16 @@ static void draw_card(const Rect &clip, int index, int y) { on ? C_GOOD : C_DIM); } else if (ct.kind == CK_SLIDER) { draw_slider(clip, cr, by, rh, ct); + } else if (ct.kind == CK_COLOR) { + int pr = ct.rgb ? ct.rgb[0] : 255; + int pg = ct.rgb ? ct.rgb[1] : 255; + int pb = ct.rgb ? ct.rgb[2] : 255; + Rect sw{cr.x + cr.w - S(96.0f), + by + (base_h - S(20.0f)) / 2, S(72.0f), S(20.0f)}; + fill(sw, Col{pr, pg, pb, 255}, &clip); + outline(sw, picker_open_for(ct) ? C_ACCENT : C_BORDER, &clip); + if (picker_open_for(ct)) + draw_picker(clip, cr, ct, by); } } by += rh; -- cgit v1.2.3