From c98d2b80eb2737287d84a3ad0aaf99d4ef8f2cf2 Mon Sep 17 00:00:00 2001 From: InauguralPhysicist Date: Mon, 13 Jul 2026 20:12:30 -0500 Subject: [PATCH] feat(ui): per-widget button colors + hover/pressed shades (#566, slice of #594) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _render_button honors optional bg / text_color fields on the button dict with theme fallback — the toggle_button (color_on/color_off) and label (color) pattern — so semantic coloring works: the record-red / play-green transport buttons DeslanStudio dropped (PORTING.md F-DS-12) now render. Custom-colored buttons get hover/pressed feedback via new theme hover_shade / pressed_shade alpha overlays drawn over the custom bg (a theme-color swap would erase the color). Theme-colored buttons keep the exact btn_hover/btn_pressed swap — existing apps render unchanged. set_theme replaces the theme dict wholesale, so custom themes without the new keys degrade gracefully (null shade = no overlay). All three built-in themes gain tuned shades. Docs: gfx_rrect and gfx_clip were registered but absent from BUILTINS.md (the #393 undiscoverability class — downstream kept believing there is no rounded-rect primitive); rows added. Tests: test_ui renders a custom-colored button through every state (rest/hover/pressed/disabled) and asserts the overrides survive; [63] is now 118 checks. Gates: release 2866/2866; ASan+UBSan detect_leaks=1 2870/2870 (tally 0); gfx-build suite 2907/2907. Visual proof on the real X display: Stop/Play/Record/Theme row with the Record button hover-shaded. Closes #566 Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 12 ++++++++++++ docs/BUILTINS.md | 2 ++ lib/ui_theme.eigs | 6 ++++++ lib/ui_w_button.eigs | 27 +++++++++++++++++++++++++-- tests/run_all_tests.sh | 12 ++++++------ tests/test_ui.eigs | 20 ++++++++++++++++++++ 6 files changed, 71 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a88165d5..03157ce4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to EigenScript are documented here. ## [Unreleased] ### Added +- **lib/ui: per-widget button colors + hover/pressed shades (#566, + first slice of #594).** `_render_button` honors optional `bg` / + `text_color` fields on the button dict with theme fallback (the + toggle_button/label pattern), so semantic coloring — the record-red / + play-green transport buttons DeslanStudio had to drop (F-DS-12) — now + works. Custom-colored buttons get hover/pressed feedback as new theme + `hover_shade` / `pressed_shade` alpha overlays (a theme-color swap + would erase the custom color); theme-colored buttons keep the exact + `btn_hover`/`btn_pressed` swap, so existing apps render unchanged. + Custom theme dicts without the new keys degrade gracefully (no + shade). Docs: `gfx_rrect` and `gfx_clip` were registered but missing + from BUILTINS.md (the #393 undiscoverability class) — rows added. - **gfx: proportional antialiased text via SDL2_ttf (#593).** The DeslanStudio side-by-side against its Qt prototype showed feature parity reading a decade old, and the dominant gap was the 5x7 bitmap diff --git a/docs/BUILTINS.md b/docs/BUILTINS.md index 98f63256..0ae2ad8a 100644 --- a/docs/BUILTINS.md +++ b/docs/BUILTINS.md @@ -539,6 +539,8 @@ libSDL2 at runtime — no SDL2 headers needed at build time. | `gfx_line` | `gfx_line of [x1, y1, x2, y2, r, g, b]` | Line segment | | `gfx_point` | `gfx_point of [x, y, r, g, b]` | Single pixel | | `gfx_circle` | `gfx_circle of [cx, cy, radius, r, g, b]` | Filled circle (midpoint) | +| `gfx_rrect` | `gfx_rrect of [x, y, w, h, radius, r, g, b]` or `[..., a]` | Filled rounded rectangle (scanline corner fill); radius clamps to half the smaller dimension, `radius 0` = plain rect | +| `gfx_clip` | `gfx_clip of [x, y, w, h]` / `gfx_clip of null` | Set / clear the render clip rectangle | | `gfx_text` | `gfx_text of [x, y, text, r, g, b]` or `[..., scale]` | Text. Proportional antialiased TTF when libSDL2_ttf + a font are available (#593); the 5x7 bitmap font otherwise — see the font note below the table | | `gfx_text_width` | `gfx_text_width of [text, scale?]` or `of "text"` | Pixel width of `text` under the active text renderer: TTF metrics when active, `len * 6 * scale` in bitmap mode. Works before `gfx_open` | | `gfx_text_height` | `gfx_text_height of scale?` | Pixel line height under the active text renderer: TTF font height when active, `7 * scale` in bitmap mode | diff --git a/lib/ui_theme.eigs b/lib/ui_theme.eigs index 5327d7b5..2f96c5b3 100644 --- a/lib/ui_theme.eigs +++ b/lib/ui_theme.eigs @@ -50,6 +50,8 @@ _theme_dark is { "tooltip_bg": [50, 50, 65], "tooltip_text": [220, 220, 230], "disabled_overlay": [25, 25, 35], + "hover_shade": [255, 255, 255, 22], + "pressed_shade": [0, 0, 0, 70], "selection_bg": [80, 140, 220], "font_scale": 2, "char_w": 12, @@ -100,6 +102,8 @@ _theme_light is { "tooltip_bg": [50, 50, 60], "tooltip_text": [240, 240, 245], "disabled_overlay": [235, 235, 240], + "hover_shade": [0, 0, 0, 14], + "pressed_shade": [0, 0, 0, 36], "selection_bg": [40, 110, 200], "font_scale": 2, "char_w": 12, @@ -150,6 +154,8 @@ _theme_high_contrast is { "tooltip_bg": [40, 40, 0], "tooltip_text": [255, 255, 255], "disabled_overlay": [0, 0, 0], + "hover_shade": [255, 255, 255, 50], + "pressed_shade": [255, 255, 255, 80], "selection_bg": [255, 255, 0], "font_scale": 2, "char_w": 12, diff --git a/lib/ui_w_button.eigs b/lib/ui_w_button.eigs index a34d5ef6..927f2f4a 100644 --- a/lib/ui_w_button.eigs +++ b/lib/ui_w_button.eigs @@ -3,6 +3,10 @@ # UI Widgets — Buttons: button, toggle_button, checkbox, toggle # ============================================================ +# Optional per-widget color overrides (#566): set `bg` / `text_color` +# on the returned dict ([r, g, b]) for semantic coloring (record-red, +# play-green, ...). Absent fields fall back to the theme, matching the +# toggle_button (color_on/color_off) and label (color) pattern. define button(id, x, y, w, h, text, on_click) as: return { "type": "button", @@ -60,16 +64,35 @@ define toggle(id, x, y, value, on_change) as: # ---- Render functions ---- define _render_button(widget, ax, ay) as: + # Per-widget color overrides with theme fallback (#566) bg is _theme.btn_bg tc is _theme.btn_text + if widget.bg != null: + bg is widget.bg + if widget.text_color != null: + tc is widget.text_color + custom is 1 + if widget.bg == null: + custom is 0 if widget.enabled == 0: bg is _theme.btn_disabled tc is _theme.btn_text_disabled - elif widget.pressed == 1: + elif custom == 0 and widget.pressed == 1: bg is _theme.btn_pressed - elif widget.hover == 1: + elif custom == 0 and widget.hover == 1: bg is _theme.btn_hover _draw_rbox of [ax, ay, widget.w, widget.h, _theme.radius_sm, bg, _theme.panel_border] + # Hover/pressed feedback for custom-colored buttons: an alpha shade + # over the custom bg (a theme-color swap would erase the color). + # Theme dicts from set_theme may predate these keys — null skips. + if custom == 1 and widget.enabled == 1: + shade is null + if widget.pressed == 1: + shade is _theme.pressed_shade + elif widget.hover == 1: + shade is _theme.hover_shade + if shade != null: + gfx_rrect of [ax, ay, widget.w, widget.h, _theme.radius_sm, shade[0], shade[1], shade[2], shade[3]] # Center text in button tw is text_width of [widget.label, _theme.font_scale] th is text_height of _theme.font_scale diff --git a/tests/run_all_tests.sh b/tests/run_all_tests.sh index 4191a02b..6f4bb1fe 100755 --- a/tests/run_all_tests.sh +++ b/tests/run_all_tests.sh @@ -2060,15 +2060,15 @@ fi echo "" # [63] UI toolkit unit tests (headless, stubs gfx) -echo "[63] UI Toolkit (115 checks)" +echo "[63] UI Toolkit (118 checks)" UI_OUTPUT=$(./eigenscript ../tests/test_ui.eigs 2>&1); UI_OUTPUT_RC=$? if rc_ok "$UI_OUTPUT_RC" "$UI_OUTPUT" && echo "$UI_OUTPUT" | grep -q "All tests passed"; then - TOTAL=$((TOTAL + 115)) - PASS=$((PASS + 115)) - echo " PASS: all 115 UI toolkit checks" + TOTAL=$((TOTAL + 118)) + PASS=$((PASS + 118)) + echo " PASS: all 118 UI toolkit checks" else - TOTAL=$((TOTAL + 115)) - FAIL=$((FAIL + 115)) + TOTAL=$((TOTAL + 118)) + FAIL=$((FAIL + 118)) echo " FAIL: UI toolkit tests" echo "$UI_OUTPUT" | grep -iE "assert|error|FAIL" | head -5 fi diff --git a/tests/test_ui.eigs b/tests/test_ui.eigs index fc0d3f03..ca4af475 100644 --- a/tests/test_ui.eigs +++ b/tests/test_ui.eigs @@ -101,6 +101,26 @@ t is toggle of ["tog1", 0, 0, 0, null] assert_eq of [t.type, "toggle", "toggle type"] assert_eq of [t.value, 0, "toggle default off"] +# ---- Per-widget button colors (#566) ---- +# Render every state with custom bg/text_color set: the render path must +# not error (gfx stubs are no-ops) and must not clobber the overrides. +bc is button of ["btnc", 0, 0, 80, 24, "Rec", null] +bc.bg is [200, 40, 40] +bc.text_color is [255, 255, 255] +_render_button of [bc, 0, 0] +bc.hover is 1 +_render_button of [bc, 0, 0] +bc.pressed is 1 +_render_button of [bc, 0, 0] +bc.enabled is 0 +_render_button of [bc, 0, 0] +assert_eq of [bc.bg[0], 200, "custom button bg preserved across renders"] +assert_eq of [bc.text_color[0], 255, "custom button text color preserved"] +bd is button of ["btnd", 0, 0, 80, 24, "Plain", null] +bd.hover is 1 +_render_button of [bd, 0, 0] +assert_eq of [bd.bg, null, "plain button has no bg override"] + # ============================================================ # 2. Registry tests # ============================================================