diff --git a/CHANGELOG b/CHANGELOG index 0cbd1109..c70fc58a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +2024-08-23: [FEATURE] Add `GradientDecoration` for widgets 2024-08-13: [RELEASE] v0.28.1 release - compatible with qtile 0.28.1 2024-08-11: [RELEASE] v0.28.0 release - compatible with qtile 0.28.0 2024-07-22: [FEATURE] Add ability to toggle visibility of groups in `GroupBox2` widget diff --git a/docs/_static/images/gradient_decoration.png b/docs/_static/images/gradient_decoration.png new file mode 100644 index 00000000..129b3530 Binary files /dev/null and b/docs/_static/images/gradient_decoration.png differ diff --git a/docs/_static/images/gradient_decoration_whole_bar.png b/docs/_static/images/gradient_decoration_whole_bar.png new file mode 100644 index 00000000..c4533982 Binary files /dev/null and b/docs/_static/images/gradient_decoration_whole_bar.png differ diff --git a/qtile_extras/widget/decorations.py b/qtile_extras/widget/decorations.py index 5934294c..0d2be65e 100644 --- a/qtile_extras/widget/decorations.py +++ b/qtile_extras/widget/decorations.py @@ -30,12 +30,16 @@ from libqtile.backend.base import Drawer from libqtile.confreader import ConfigError from libqtile.log_utils import logger +from libqtile.utils import rgb from libqtile.widget import Systray, base if TYPE_CHECKING: from typing import Any # noqa: F401 +HALF_ROOT_2 = 0.70711 + + class _Decoration(base.PaddingMixin): """ Base decoration class. Should not be called by @@ -818,6 +822,98 @@ def draw(self) -> None: self.ctx.restore() +class GradientDecoration(_Decoration): + """ + Renders a gradient background to the widget. + + ``colours`` defines the list of colours in the gradient. + + The angle/direction of the gradient is set by the ``points`` + parameter. This is a list of a two (x, y) tuples. The x and y + values are relative to the drawing area (see below). A value of (0, 0) is the top + left corner while (1, 1) represents the bottom right corner. + + ``offsets`` is used to adjust the position of the colours within + the gradient. Leaving this as ``None`` will space the colours evenly. The + values need to be in ascending order and in the range of 0.0 (the very start of + the gradient) and 1.0 (the end of the gradient). These represent positions on the + imagninary line between the two ``points`` defined above. + + When ``radial=True`` the ``points`` parameter has no impact. The gradient will be drawn from the center + of the drawing area to the corner. ``offsets`` can still be used to adjust the + spacing of the colours. + + Setting ``whole_bar=True`` will calculate the gradient by reference to the whole bar. Widgets will then + render the part of the gradient in the area covered by the widget. This allows a single gradient to be + applied consistently across all widgets. + """ + + _screenshots = [ + ("gradient_decoration.png", "whole_bar=False"), + ("gradient_decoration_whole_bar.png", "whole_bar=True"), + ] + + defaults = [ + ( + "whole_bar", + False, + "When set to ``True`` gradient is calculated by reference to the bar so " + "you can get a single gradient applied across multiple widgets.", + ), + ("colours", ["999", "000"], "List of colours in the gradient"), + ("points", [(0, 0), (1, 0)], "Points to size/angle the gradient. See docs for more."), + ( + "offsets", + None, + "Offset locations (in range of 0.0-1.0) for gradient stops. ``None`` to use regular spacing.", + ), + ("radial", False, "Use radial gradient"), + ] + + def __init__(self, **config): + _Decoration.__init__(self, **config) + self.add_defaults(GradientDecoration.defaults) + + if self.offsets is None: + self.offsets = [x / (len(self.colours) - 1) for x in range(len(self.colours))] + elif len(self.offsets) != len(self.colours): + raise ConfigError("'offsets' must be same length as 'colours'.") + + def draw(self): + width = self.parent.bar.width if self.whole_bar else self.width + height = self.parent.bar.height if self.whole_bar else self.height + + # Nothing to do if widget is hidden + if not (width and height): + return + + # Calculates absolute coordinates for gradient + def pos(point): + return tuple(p * d for p, d in zip(point, (width, height))) + + self.ctx.save() + self.ctx.rectangle(0, 0, self.width, self.height) + self.ctx.clip() + + # If we're using whole_bar then we shift 0, 0 to be top left corner of the bar + if self.whole_bar: + self.ctx.translate(-self.parent.offsetx, -self.parent.offsety) + + if self.radial: + self.ctx.translate(width // 2, height // 2) + self.ctx.scale(width, height) + gradient = cairocffi.RadialGradient(0, 0, 0, 0, 0, HALF_ROOT_2) + else: + gradient = cairocffi.LinearGradient(*pos(self.points[0]), *pos(self.points[1])) + + for offset, c in zip(self.offsets, self.colours): + gradient.add_color_stop_rgba(offset, *rgb(c)) + + self.ctx.set_source(gradient) + self.ctx.paint() + self.ctx.restore() + + def inject_decorations(classdef): """ Method to inject ability for widgets to display decorations. diff --git a/test/resources/test_images/gradient-decoration-default.png b/test/resources/test_images/gradient-decoration-default.png new file mode 100644 index 00000000..d8c940f6 Binary files /dev/null and b/test/resources/test_images/gradient-decoration-default.png differ diff --git a/test/resources/test_images/gradient-decoration-radial-whole-bar.png b/test/resources/test_images/gradient-decoration-radial-whole-bar.png new file mode 100644 index 00000000..d34cf6a5 Binary files /dev/null and b/test/resources/test_images/gradient-decoration-radial-whole-bar.png differ diff --git a/test/resources/test_images/gradient-decoration-radial.png b/test/resources/test_images/gradient-decoration-radial.png new file mode 100644 index 00000000..f186da5a Binary files /dev/null and b/test/resources/test_images/gradient-decoration-radial.png differ diff --git a/test/resources/test_images/gradient-decoration-top-bottom-whole-bar.png b/test/resources/test_images/gradient-decoration-top-bottom-whole-bar.png new file mode 100644 index 00000000..64f75b27 Binary files /dev/null and b/test/resources/test_images/gradient-decoration-top-bottom-whole-bar.png differ diff --git a/test/resources/test_images/gradient-decoration-top-bottom.png b/test/resources/test_images/gradient-decoration-top-bottom.png new file mode 100644 index 00000000..64f75b27 Binary files /dev/null and b/test/resources/test_images/gradient-decoration-top-bottom.png differ diff --git a/test/resources/test_images/gradient-decoration-whole-bar.png b/test/resources/test_images/gradient-decoration-whole-bar.png new file mode 100644 index 00000000..9b51b6bf Binary files /dev/null and b/test/resources/test_images/gradient-decoration-whole-bar.png differ diff --git a/test/widget/decorations/test_decoration_output.py b/test/widget/decorations/test_decoration_output.py index 8f539d19..823b90e4 100644 --- a/test/widget/decorations/test_decoration_output.py +++ b/test/widget/decorations/test_decoration_output.py @@ -20,7 +20,12 @@ import pytest from qtile_extras import widget -from qtile_extras.widget.decorations import BorderDecoration, PowerLineDecoration, RectDecoration +from qtile_extras.widget.decorations import ( + BorderDecoration, + GradientDecoration, + PowerLineDecoration, + RectDecoration, +) def widgets(decorations=list()): @@ -208,6 +213,44 @@ def widgets(decorations=list()): } ) +# GRADIENTDECORATION +params.append( + { + "name": "gradient-decoration-default", + "widgets": widgets([GradientDecoration()]), + } +) +params.append( + { + "name": "gradient-decoration-top-bottom", + "widgets": widgets([GradientDecoration(points=[(0, 0), (0, 1)])]), + } +) +params.append( + { + "name": "gradient-decoration-whole-bar", + "widgets": widgets([GradientDecoration(whole_bar=True)]), + } +) +params.append( + { + "name": "gradient-decoration-top-bottom-whole-bar", + "widgets": widgets([GradientDecoration(points=[(0, 0), (0, 1)], whole_bar=True)]), + } +) +params.append( + { + "name": "gradient-decoration-radial", + "widgets": widgets([GradientDecoration(radial=True)]), + } +) +params.append( + { + "name": "gradient-decoration-radial-whole-bar", + "widgets": widgets([GradientDecoration(radial=True, whole_bar=True)]), + } +) + # COMBOS decorations = [ RectDecoration(