Skip to content

Commit

Permalink
Wayland: implement support for wlr-layer-shell-unstable-v1-protocol
Browse files Browse the repository at this point in the history
This adds support for the layer shell protocol. This allows for "layer"
surfaces to be created by clients, used for applications such as status
bars, overlays, third-party wallpapers (such as swaybg), and other tools
like dmenu, rofi etc (their wayland clones that is).

These clients are managed by Qtile as Static windows and are bound to an
output rather than a group, and are not tiled.

Part of layer shell protocol allows clients to request "exclusive
zones", however this is not implemented in this commit.

This depends on pywlroots>=0.2.7, which is reflected in tox.ini and
setup.cfg.
  • Loading branch information
m-col authored and ramnes committed May 14, 2021
1 parent a293899 commit 94d9183
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 40 deletions.
70 changes: 50 additions & 20 deletions libqtile/backend/wayland/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,22 @@
XCursorManager,
XdgOutputManagerV1,
input_device,
layer_shell_v1,
pointer,
seat,
xdg_shell,
)
from wlroots.wlr_types.cursor import WarpMode
from wlroots.wlr_types.virtual_keyboard_v1 import (
VirtualKeyboardManagerV1,
VirtualKeyboardV1,
)
from wlroots.wlr_types.xdg_shell import XdgShell, XdgSurface, XdgSurfaceRole
from xkbcommon import xkb

from libqtile import hook
from libqtile.backend import base
from libqtile.backend.wayland import keyboard, output, window, wlrq
from libqtile.backend.wayland import keyboard, window, wlrq
from libqtile.backend.wayland.output import Output
from libqtile.log_utils import logger

if typing.TYPE_CHECKING:
Expand Down Expand Up @@ -91,7 +93,7 @@ def __init__(self):

# set up outputs
self.output_layout = OutputLayout()
self.outputs: List[output.Output] = []
self.outputs: List[Output] = []
self._on_new_output_listener = Listener(self._on_new_output)
self.backend.new_output_event.add(self._on_new_output_listener)

Expand All @@ -112,9 +114,12 @@ def __init__(self):
self.cursor.motion_absolute_event.add(self._on_cursor_motion_absolute_listener)

# set up shell
self.xdg_shell = xdg_shell.XdgShell(self.display)
self.xdg_shell = XdgShell(self.display)
self._on_new_xdg_surface_listener = Listener(self._on_new_xdg_surface)
self.xdg_shell.new_surface_event.add(self._on_new_xdg_surface_listener)
self.layer_shell = layer_shell_v1.LayerShellV1(self.display)
self._on_new_layer_surface_listener = Listener(self._on_new_layer_surface)
self.layer_shell.new_surface_event.add(self._on_new_layer_surface_listener)

# Add support for additional protocols
XdgOutputManagerV1(self.display, self.output_layout)
Expand All @@ -138,6 +143,7 @@ def finalize(self):
self._on_new_input_listener.remove()
self._on_request_set_selection_listener.remove()
self._on_new_virtual_keyboard_listener.remove()
self._on_new_layer_surface_listener.remove()

for kb in self.keyboards:
kb.finalize()
Expand Down Expand Up @@ -187,27 +193,22 @@ def _on_new_output(self, _listener, wlr_output: wlrOutput):
wlr_output.enable()
wlr_output.commit()

self.outputs.append(output.Output(self, wlr_output))
self.outputs.append(Output(self, wlr_output))
self.output_layout.add_auto(wlr_output)

def _on_request_cursor(self, _listener, event: seat.PointerRequestSetCursorEvent):
logger.debug("Signal: seat request_set_cursor_event")
# if self._seat.pointer_state.focused_surface == event.seat_client: # needs updating pywlroots first
self.cursor.set_surface(event.surface, event.hotspot)

def _on_new_xdg_surface(self, _listener, surface: xdg_shell.XdgSurface):
def _on_new_xdg_surface(self, _listener, surface: XdgSurface):
logger.debug("Signal: xdg_shell new_surface_event")
assert self.qtile is not None

if surface.role != xdg_shell.XdgSurfaceRole.TOPLEVEL:
if surface.role != XdgSurfaceRole.TOPLEVEL:
return

wid = 0
wids = self.qtile.windows_map.keys()
while True:
if wid not in wids:
break
wid += 1
wid = max(self.qtile.windows_map.keys(), default=0) + 1
win = window.Window(self, self.qtile, surface, wid)
logger.info(f"Managing new top-level window with window ID: {wid}")
self.qtile.manage(win)
Expand Down Expand Up @@ -253,6 +254,15 @@ def _on_cursor_motion_absolute(self, _listener, event: pointer.PointerEventMotio
def _on_new_virtual_keyboard(self, _listener, virtual_keyboard: VirtualKeyboardV1):
self._add_new_keyboard(virtual_keyboard.input_device)

def _on_new_layer_surface(self, _listener, layer_surface: layer_shell_v1.LayerSurfaceV1):
logger.debug("Signal: layer_shell new_surface_event")
assert self.qtile is not None

wid = max(self.qtile.windows_map.keys(), default=0) + 1
win = window.Static(self, self.qtile, layer_surface, wid)
logger.info(f"Managing new layer_shell window with window ID: {wid}")
self.qtile.manage(win)

def _process_cursor_motion(self, time):
self.qtile.process_button_motion(self.cursor.x, self.cursor.y)
found = self._under_pointer()
Expand All @@ -263,7 +273,7 @@ def _process_cursor_motion(self, time):
if focus_changed:
hook.fire("client_mouse_enter", win)
if self.qtile.config.follow_mouse_focus:
if win.group.current_window != self:
if win.group.current_window != win:
win.group.focus(win, False)
if win.group.screen and self.qtile.current_screen != win.group.screen:
self.qtile.focus_screen(win.group.screen.index, False)
Expand Down Expand Up @@ -306,23 +316,34 @@ def _poll(self) -> None:
self.display.flush_clients()
self.event_loop.dispatch(-1)

def focus_window(self, win: window.Window, surface: Surface = None):
if surface is None:
def focus_window(self, win: window.WindowType, surface: Surface = None):
if self.seat.destroyed:
return

if surface is None and win is not None:
surface = win.surface.surface

previous_surface = self.seat.keyboard_state.focused_surface
if previous_surface == surface:
return
self.seat.keyboard_clear_focus()

if previous_surface is not None and previous_surface.is_xdg_surface:
# Deactivate the previously focused surface
previous_xdg_surface = xdg_shell.XdgSurface.from_surface(previous_surface)
previous_xdg_surface = XdgSurface.from_surface(previous_surface)
previous_xdg_surface.set_activated(False)

# activate the new surface
win.surface.set_activated(True)
if not win:
return

if isinstance(win.surface, layer_shell_v1.LayerSurfaceV1):
if not win.mapped or not win.surface.current.keyboard_interactive:
return

logger.debug("Focussing new window")
if surface.is_xdg_surface and isinstance(win.surface, XdgSurface):
win.surface.set_activated(True)
self.seat.keyboard_notify_enter(surface, self.seat.keyboard)
logger.debug("Focussed new window")

def focus_by_click(self, event) -> None:
found = self._under_pointer()
Expand Down Expand Up @@ -434,3 +455,12 @@ def change_vt(self, vt: int) -> bool:
@property
def painter(self):
return wlrq.Painter(self)

def output_from_wlr_output(self, wlr_output: wlrOutput) -> Output:
matched = []
for output in self.outputs:
if output.wlr_output == wlr_output:
matched.append(output)

assert len(matched) == 1
return matched[0]
68 changes: 63 additions & 5 deletions libqtile/backend/wayland/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,18 @@
from wlroots.util.clock import Timespec
from wlroots.wlr_types import Box, Matrix
from wlroots.wlr_types import Output as wlrOutput
from wlroots.wlr_types.layer_shell_v1 import (
LayerShellV1Layer,
LayerSurfaceV1,
LayerSurfaceV1Anchor,
)

from libqtile import hook
from libqtile.backend.wayland.window import Static, Window
from libqtile.log_utils import logger

if typing.TYPE_CHECKING:
from typing import Set, Tuple
from typing import List, Set, Tuple

from wlroots.wlr_types import Surface

Expand Down Expand Up @@ -60,6 +66,9 @@ def __init__(self, core: Core, wlr_output: wlrOutput):
hook.subscribe.client_killed(self._get_windows)
hook.subscribe.client_managed(self._get_windows)

# The layers enum indexes into this list to get a list of surfaces
self.layers: List[List[Static]] = [[]] * len(LayerShellV1Layer)

def finalize(self):
self._on_destroy_listener.remove()
self._on_frame_listener.remove()
Expand Down Expand Up @@ -146,9 +155,58 @@ def get_geometry(self) -> Tuple[int, int, int, int]:
return int(x), int(y), width, height

def _get_windows(self, *args):
"""Get the set of mapped windows for rendering."""
mapped = set()
"""Get the set of mapped windows for rendering and order them."""
mapped = []
mapped.extend([i for i in self.layers[LayerShellV1Layer.BACKGROUND] if i.mapped])
mapped.extend([i for i in self.layers[LayerShellV1Layer.BOTTOM] if i.mapped])

for win in self.core.qtile.windows_map.values():
if win.mapped:
mapped.add(win)
if win.mapped and isinstance(win, Window):
mapped.append(win)

mapped.extend([i for i in self.layers[LayerShellV1Layer.TOP] if i.mapped])
mapped.extend([i for i in self.layers[LayerShellV1Layer.OVERLAY] if i.mapped])
self._mapped_windows = mapped

def organise_layers(self) -> None:
"""Organise the positioning of layer shell surfaces."""
logger.info("Output: organising layers")
ow, oh = self.wlr_output.effective_resolution()

for layer in self.layers:
for win in layer:
assert isinstance(win.surface, LayerSurfaceV1)
state = win.surface.current
margin = state.margin
ww = state.desired_width
wh = state.desired_height

# Horizontal axis
if (state.anchor & LayerSurfaceV1Anchor.HORIZONTAL) and ww == 0:
x = margin.left
ww = ow - margin.left - margin.right
elif (state.anchor & LayerSurfaceV1Anchor.LEFT):
x = margin.left
elif (state.anchor & LayerSurfaceV1Anchor.RIGHT):
x = ow - ww - margin.right
else:
x = int(ow / 2 - ww / 2)

# Vertical axis
if (state.anchor & LayerSurfaceV1Anchor.VERTICAL) and wh == 0:
y = margin.top
wh = oh - margin.top - margin.bottom
elif (state.anchor & LayerSurfaceV1Anchor.TOP):
y = margin.top
elif (state.anchor & LayerSurfaceV1Anchor.BOTTOM):
y = oh - wh - margin.bottom
else:
y = int(oh / 2 - wh / 2)

if ww <= 0 or wh <= 0:
win.kill()
continue

win.place(x, y, ww, wh, 0, None)

self._get_windows()
Loading

0 comments on commit 94d9183

Please sign in to comment.