diff --git a/docs/_static/images/max_solid_edge.png b/docs/_static/images/max_solid_edge.png new file mode 100644 index 00000000..e8e7049e Binary files /dev/null and b/docs/_static/images/max_solid_edge.png differ 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)