From a8e006c9485895b2bccb463ee50ee0d4786ed44b Mon Sep 17 00:00:00 2001 From: Ken Sanislo Date: Mon, 11 May 2026 01:10:25 -0700 Subject: [PATCH] PerKey gradient swatch: Tabler "square" outline around the gradient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gradient swatch on the gradient-tool button drew a flat rectangular fill with a 1px translucent-black border. Visually it sat as an odd-one-out next to the rounded-square Tabler outline icons on the rest of the toolbar — a sharp-cornered patch flanked by rounded-corner icons. Render the gradient inside the same path Tabler "square" uses (rounded rect from (3,3) to (21,21), corner radius 2, stroke 2 in a 24x24 viewBox; cairo scale into the swatch's pixel size) and stroke the outline in the GTK theme's foreground color. The swatch now reads as one of the icon family — same outline style, same line weight, same theme-following color — with the gradient filling the inside. Connect style-updated to queue_draw so the outline color tracks runtime theme switches alongside the icon buttons. --- lib/solaar/ui/perkey/palette.py | 44 +++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/solaar/ui/perkey/palette.py b/lib/solaar/ui/perkey/palette.py index b9f6ee1e..95953c12 100644 --- a/lib/solaar/ui/perkey/palette.py +++ b/lib/solaar/ui/perkey/palette.py @@ -159,6 +159,10 @@ class GradientSwatch(Gtk.DrawingArea): self._active: int = 0xFF0000 self._previous: int = 0xFF0000 self.connect(GtkSignal.DRAW.value, self._on_draw) + # Re-render when the GTK theme changes, so the rounded-square + # outline (drawn in the theme foreground color) stays in sync + # with the tool icons next to it. + self.connect("style-updated", lambda w: w.queue_draw()) def update(self, active: int, previous: int) -> None: self._active = int(active) @@ -175,27 +179,51 @@ class GradientSwatch(Gtk.DrawingArea): """Return (active, previous) — the colors the gradient tool will paint with.""" return (self._active, self._previous) + @staticmethod + def _rounded_rect_path(cr, x: float, y: float, w: float, h: float, r: float) -> None: + cr.new_sub_path() + cr.arc(x + w - r, y + r, r, -1.5708, 0) + cr.arc(x + w - r, y + h - r, r, 0, 1.5708) + cr.arc(x + r, y + h - r, r, 1.5708, 3.1416) + cr.arc(x + r, y + r, r, 3.1416, 4.7124) + cr.close_path() + def _on_draw(self, _w, cr) -> None: import cairo # local: keeps the module light when GradientSwatch isn't built - s = self.SIZE - def rgb(c: int) -> tuple[float, float, float]: if c is None or c < 0: return (0.5, 0.5, 0.5) return (((c >> 16) & 0xFF) / 255.0, ((c >> 8) & 0xFF) / 255.0, (c & 0xFF) / 255.0) + # Render in Tabler "square" coordinates (24x24 viewBox, rounded + # rect from (3,3) to (21,21), corner radius 2, stroke 2) and let + # cairo scale to the swatch's pixel size. Matches the outline + # style of the tool icons exactly. + cr.save() + cr.scale(self.SIZE / 24.0, self.SIZE / 24.0) + + # Build the rounded-square path once, clip+fill the gradient + # inside it, then re-build and stroke the outline in the theme + # foreground color. + self._rounded_rect_path(cr, 3, 3, 18, 18, 2) + cr.save() + cr.clip() # Top-left (previous, gradient start) → bottom-right (active, end). # Matches the directional behavior of dragging the line tool TL → BR. - pat = cairo.LinearGradient(0, 0, s, s) + pat = cairo.LinearGradient(3, 3, 21, 21) pat.add_color_stop_rgb(0.0, *rgb(self._previous)) pat.add_color_stop_rgb(1.0, *rgb(self._active)) cr.set_source(pat) - cr.rectangle(0, 0, s, s) + cr.rectangle(3, 3, 18, 18) cr.fill() + cr.restore() # drop clip - # Subtle border so the swatch reads as a control even on similar bg. - cr.set_source_rgba(0, 0, 0, 0.45) - cr.set_line_width(1.0) - cr.rectangle(0.5, 0.5, s - 1, s - 1) + self._rounded_rect_path(cr, 3, 3, 18, 18, 2) + fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL) + cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha) + cr.set_line_width(2) + cr.set_line_join(cairo.LINE_JOIN_ROUND) + cr.set_line_cap(cairo.LINE_CAP_ROUND) cr.stroke() + cr.restore()