From 58d3855e1198db57672db60e07fa7d372f2ce2eb Mon Sep 17 00:00:00 2001 From: elParaguayo Date: Sat, 11 May 2024 07:39:45 +0100 Subject: [PATCH] Add more window border stuff - Add new SolidEdge border - Add tests for config errors --- docs/_static/images/max_solid_edge.png | Bin 0 -> 10125 bytes qtile_extras/layout/decorations/__init__.py | 1 + qtile_extras/layout/decorations/borders.py | 113 ++++++++++++++---- .../decorations/test_border_decorations.py | 38 +++++- 4 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 docs/_static/images/max_solid_edge.png diff --git a/docs/_static/images/max_solid_edge.png b/docs/_static/images/max_solid_edge.png new file mode 100644 index 0000000000000000000000000000000000000000..e8e7049e24ef903b3512768afe5fd6a31eb111c0 GIT binary patch literal 10125 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 Fe*o_CL;?T+ literal 0 HcmV?d00001 diff --git a/qtile_extras/layout/decorations/__init__.py b/qtile_extras/layout/decorations/__init__.py index 7bac7666..f0fad602 100644 --- a/qtile_extras/layout/decorations/__init__.py +++ b/qtile_extras/layout/decorations/__init__.py @@ -23,6 +23,7 @@ GradientBorder, GradientFrame, ScreenGradientBorder, + SolidEdge, ) diff --git a/qtile_extras/layout/decorations/borders.py b/qtile_extras/layout/decorations/borders.py index 280f7b43..6c4a60fe 100644 --- a/qtile_extras/layout/decorations/borders.py +++ b/qtile_extras/layout/decorations/borders.py @@ -18,6 +18,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import cairocffi +import xcffib.xproto from libqtile import qtile from libqtile.configurable import Configurable from libqtile.confreader import ConfigError @@ -25,9 +26,11 @@ try: from libqtile.backend.wayland._ffi import ffi, lib + from libqtile.backend.wayland.window import _rgb from wlroots import ffi as wlr_ffi from wlroots import lib as wlr_lib from wlroots.wlr_types import Buffer, SceneBuffer + from wlroots.wlr_types.scene import SceneRect HAS_WAYLAND = True except (ImportError, ModuleNotFoundError): @@ -49,6 +52,13 @@ class _BorderStyle(Configurable): needs_surface = True + def _check_colours(self): + for colour in self.colours: + try: + rgb(colour) + except ValueError: + raise ConfigError(f"Invalid colour value in border decoration: {colour}.") + def _create_xcb_surface(self): root = self.window.conn.conn.get_setup().roots[0] @@ -77,11 +87,20 @@ def _new_buffer(self): return image_buffer, surface + def _get_edges(self, bw, x, y, width, height): + return [ + (x, y, width, bw), + (self.outer_w - bw - x, bw + y, bw, height - 2 * bw), + (x, self.outer_h - y - bw, width, bw), + (x, bw + y, bw, height - bw * 2), + ] + def _x11_draw( self, window, depth, pixmap, gc, outer_w, outer_h, borderwidth, x, y, width, height ): self.visual = window.get_attributes().visual self.window = window + self.core = window.conn.conn.core self.wid = window.wid self.depth = depth self.pixmap = pixmap @@ -106,33 +125,28 @@ def _wayland_draw(self, window, outer_w, outer_h, borderwidth, x, y, width, heig self.wid = window.wid self.outer_w = outer_w self.outer_h = outer_h - bw = borderwidth - self.rects = [ - (x, y, width, bw), - (outer_w - bw - x, bw + y, bw, height - 2 * bw), - (x, outer_h - y - bw, width, bw), - (x, bw + y, bw, height - bw * 2), - ] + self.rects = self._get_edges(borderwidth, x, y, width, height) if self.needs_surface: image_buffer, surface = self._new_buffer() else: image_buffer = None surface = None - self.wayland_draw(borderwidth, x, y, width, height, surface) - - scenes = [] - for x, y, w, h in self.rects: - scene_buffer = SceneBuffer.create(self.window.container, Buffer(image_buffer)) - scene_buffer.node.set_position(x, y) - wlr_lib.wlr_scene_buffer_set_dest_size(scene_buffer._ptr, w, h) - fbox = wlr_ffi.new("struct wlr_fbox *") - fbox.x = x - fbox.y = y - fbox.width = w - fbox.height = h - wlr_lib.wlr_scene_buffer_set_source_box(scene_buffer._ptr, fbox) - scenes.append(scene_buffer) + scenes = self.wayland_draw(borderwidth, x, y, width, height, surface) + + if self.needs_surface: + scenes = [] + for x, y, w, h in self.rects: + scene_buffer = SceneBuffer.create(self.window.container, Buffer(image_buffer)) + scene_buffer.node.set_position(x, y) + wlr_lib.wlr_scene_buffer_set_dest_size(scene_buffer._ptr, w, h) + fbox = wlr_ffi.new("struct wlr_fbox *") + fbox.x = x + fbox.y = y + fbox.width = w + fbox.height = h + wlr_lib.wlr_scene_buffer_set_source_box(scene_buffer._ptr, fbox) + scenes.append(scene_buffer) return scenes, image_buffer, surface @@ -188,11 +202,16 @@ def __init__(self, **config): _BorderStyle.__init__(self, **config) self.add_defaults(GradientBorder.defaults) + if not isinstance(self.colours, (list, tuple)): + raise ConfigError("colours must be a list or tuple.") + 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'.") + self._check_colours() + def draw(self, surface, bw, x, y, width, height): def pos(point): return tuple(p * d for p, d in zip(point, (width, height))) @@ -239,6 +258,11 @@ def __init__(self, **config): self.add_defaults(GradientFrame.defaults) self.offsets = [x / (len(self.colours) - 1) for x in range(len(self.colours))] + if not isinstance(self.colours, (list, tuple)): + raise ConfigError("colours must be a list or tuple.") + + self._check_colours() + def draw(self, surface, bw, x, y, width, height): with cairocffi.Context(surface) as ctx: ctx.save() @@ -357,3 +381,50 @@ def pos(point): ctx.set_source(gradient) ctx.paint() ctx.restore() + + +class SolidEdge(_BorderStyle): + """ + A decoration that renders a solid border. Colours can be specified for + each edge. + """ + + _screenshots = [("max_solid_edge.png", 'SolidEdge(colours=["00f", "0ff", "00f", "0ff"])')] + + needs_surface = False + + defaults = [ + ( + "colours", + ["00f", "00f", "00f", "00f"], + "List of colours for each edge of the window [N, E, S, W].", + ) + ] + + def __init__(self, **config): + _BorderStyle.__init__(self, **config) + self.add_defaults(SolidEdge.defaults) + + if not (isinstance(self.colours, (list, tuple)) and len(self.colours) == 4): + raise ConfigError("colours must have 4 values.") + + self._check_colours() + + def x11_draw(self, borderwidth, x, y, width, height, surface): + edges = self._get_edges(borderwidth, x, y, width, height) + for (x, y, w, h), c in zip(edges, self.colours): + self.core.ChangeGC( + self.gc, xcffib.xproto.GC.Foreground, [self.window.conn.color_pixel(c)] + ) + rect = xcffib.xproto.RECTANGLE.synthetic(x, y, w, h) + self.core.PolyFillRectangle(self.pixmap, self.gc, 1, [rect]) + + def wayland_draw(self, borderwidth, x, y, width, height, surface): + scene_rects = [] + edges = self._get_edges(borderwidth, x, y, width, height) + for (x, y, w, h), c in zip(edges, self.colours): + rect = SceneRect(self.window.container, w, h, _rgb(c)) + rect.node.set_position(x, y) + scene_rects.append(rect) + + return scene_rects diff --git a/test/layout/decorations/test_border_decorations.py b/test/layout/decorations/test_border_decorations.py index 20c23bec..f7f312df 100644 --- a/test/layout/decorations/test_border_decorations.py +++ b/test/layout/decorations/test_border_decorations.py @@ -19,10 +19,15 @@ # SOFTWARE. import pytest from libqtile.config import Screen -from libqtile.confreader import Config +from libqtile.confreader import Config, ConfigError from libqtile.layout import Matrix -from qtile_extras.layout.decorations import GradientBorder, GradientFrame, ScreenGradientBorder +from qtile_extras.layout.decorations import ( + GradientBorder, + GradientFrame, + ScreenGradientBorder, + SolidEdge, +) @pytest.fixture @@ -54,6 +59,8 @@ class BorderDecorationConfig(Config): ScreenGradientBorder(colours=["f00", "0f0", "00f"], points=[(0, 0), (1, 0)]), ScreenGradientBorder(colours=["f00", "0f0", "00f"], offsets=[0, 0.1, 1]), ScreenGradientBorder(colours=["f00", "0f0", "00f"], radial=True), + # SolidEdge(), + # SolidEdge(colours=["f00", "00f", "f00", "00f"]) ], indirect=True, ) @@ -61,3 +68,30 @@ def test_window_decoration(manager): manager.test_window("one") manager.test_window("two") assert True + + +@pytest.mark.parametrize( + "classname,config", + [ + (GradientBorder, {"colours": "f00"}), # not a list + (GradientBorder, {"colours": [1, 2, 3, 4]}), # not a valid color + ( + GradientBorder, + {"colours": ["f00", "0ff"], "offsets": [0, 0.5, 1]}, + ), # offsets doesn't match colours length + (GradientFrame, {"colours": "f00"}), # not a list + (GradientFrame, {"colours": [1, 2, 3, 4]}), # not a valid color + (ScreenGradientBorder, {"colours": "f00"}), # not a list + (ScreenGradientBorder, {"colours": [1, 2, 3, 4]}), # not a valid color + ( + ScreenGradientBorder, + {"colours": ["f00", "0ff"], "offsets": [0, 0.5, 1]}, + ), # offsets doesn't match colours length + (SolidEdge, {"colours": ["f00", "f00"]}), # not enough values + (SolidEdge, {"colours": "f00"}), # not a list + (SolidEdge, {"colours": [1, 2, 3, 4]}), # not a valid color + ], +) +def test_decoration_config_errors(classname, config): + with pytest.raises(ConfigError): + classname(**config)