Skip to content

Commit

Permalink
Add more window border stuff
Browse files Browse the repository at this point in the history
- Add new SolidEdge border
- Add tests for config errors
  • Loading branch information
elParaguayo committed May 11, 2024
1 parent 70613d5 commit 58d3855
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 23 deletions.
Binary file added docs/_static/images/max_solid_edge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions qtile_extras/layout/decorations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
GradientBorder,
GradientFrame,
ScreenGradientBorder,
SolidEdge,
)


Expand Down
113 changes: 92 additions & 21 deletions qtile_extras/layout/decorations/borders.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@
# 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
from libqtile.utils import rgb

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):
Expand All @@ -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]

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
38 changes: 36 additions & 2 deletions test/layout/decorations/test_border_decorations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,10 +59,39 @@ 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,
)
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)

0 comments on commit 58d3855

Please sign in to comment.