diff --git a/.builds/alpine.yml b/.builds/alpine.yml index ca47a9d..b72ef99 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -28,15 +28,17 @@ sources: tasks: - install_deps: | cd wayland - git checkout 1.22.0 + git checkout 1.23.0 meson setup build -Ddocumentation=false -Dtests=false --prefix /usr sudo ninja -C build install cd .. cd wlroots - git checkout 0.17.2 - meson setup build --auto-features=enabled -Drenderers=gles2 -Dexamples=false \ - -Dwerror=false -Db_ndebug=false -Dxcb-errors=disabled --prefix /usr + git checkout 0.18.0 + meson setup build --auto-features=enabled -Drenderers=gles2 \ + -Dcolor-management=disabled -Dlibliftoff=disabled \ + -Dexamples=false -Dwerror=false -Db_ndebug=false \ + -Dxcb-errors=disabled --prefix /usr sudo ninja -C build/ install cd .. diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index 7e94459..d23f799 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -26,15 +26,17 @@ sources: tasks: - install_deps: | cd wayland - git checkout 1.22.0 + git checkout 1.23.0 meson setup build -Ddocumentation=false -Dtests=false --prefix /usr sudo ninja -C build install cd .. cd wlroots - git checkout 0.17.2 - meson setup build --auto-features=enabled -Drenderers=gles2 -Dexamples=false \ - -Dwerror=false -Db_ndebug=false --prefix /usr + git checkout 0.18.0 + meson setup build --auto-features=enabled -Drenderers=gles2 \ + -Dcolor-management=disabled -Dlibliftoff=disabled \ + -Dexamples=false -Dwerror=false -Db_ndebug=false \ + -Dxcb-errors=disabled --prefix /usr sudo ninja -C build/ install cd .. diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index eeb1d1b..2928661 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -31,15 +31,17 @@ sources: tasks: - install_deps: | cd wayland - git checkout 1.22.0 + git checkout 1.23.0 meson setup build -Ddocumentation=false -Dtests=false --prefix /usr sudo ninja -C build install cd .. cd wlroots - git checkout 0.17.2 - meson setup build --auto-features=enabled -Drenderers=gles2 -Dexamples=false \ - -Dwerror=false -Db_ndebug=false --prefix /usr + git checkout 0.18.0 + meson setup build --auto-features=enabled -Drenderers=gles2 \ + -Dcolor-management=disabled -Dlibliftoff=disabled \ + -Dexamples=false -Dwerror=false -Db_ndebug=false \ + -Dxcb-errors=disabled --prefix /usr sudo ninja -C build/ install cd .. diff --git a/README.md b/README.md index a3f7577..f156566 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ River is a dynamic tiling Wayland compositor with flexible runtime configuration. -Check [packaging status](https://repology.org/project/river/versions) — +Check [packaging status](https://repology.org/project/river-compositor/versions) — Join us at [#river](https://web.libera.chat/?channels=#river) on irc.libera.chat — Read our man pages, [wiki](https://codeberg.org/river/wiki), and [Code of Conduct](CODE_OF_CONDUCT.md) @@ -60,7 +60,7 @@ distribution. - [zig](https://ziglang.org/download/) 0.13 - wayland - wayland-protocols -- [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.17.2 +- [wlroots](https://gitlab.freedesktop.org/wlroots/wlroots) 0.18 - xkbcommon - libevdev - pixman @@ -72,6 +72,7 @@ Then run, for example: zig build -Doptimize=ReleaseSafe --prefix ~/.local install ``` To enable Xwayland support pass the `-Dxwayland` option as well. +Run `zig build -h` to see a list of all options. ## Usage diff --git a/build.zig b/build.zig index 1addf36..7dac615 100644 --- a/build.zig +++ b/build.zig @@ -92,11 +92,12 @@ pub fn build(b: *Build) !void { const scanner = Scanner.create(b, .{}); scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); + scanner.addSystemProtocol("stable/tablet/tablet-v2.xml"); scanner.addSystemProtocol("staging/cursor-shape/cursor-shape-v1.xml"); scanner.addSystemProtocol("staging/ext-session-lock/ext-session-lock-v1.xml"); + scanner.addSystemProtocol("staging/tearing-control/tearing-control-v1.xml"); scanner.addSystemProtocol("unstable/pointer-constraints/pointer-constraints-unstable-v1.xml"); scanner.addSystemProtocol("unstable/pointer-gestures/pointer-gestures-unstable-v1.xml"); - scanner.addSystemProtocol("unstable/tablet/tablet-unstable-v2.xml"); scanner.addSystemProtocol("unstable/xdg-decoration/xdg-decoration-unstable-v1.xml"); scanner.addCustomProtocol("protocol/river-control-unstable-v1.xml"); @@ -124,6 +125,7 @@ pub fn build(b: *Build) !void { scanner.generate("zxdg_decoration_manager_v1", 1); scanner.generate("ext_session_lock_manager_v1", 1); scanner.generate("wp_cursor_shape_manager_v1", 1); + scanner.generate("wp_tearing_control_manager_v1", 1); scanner.generate("zriver_control_v1", 1); scanner.generate("zriver_status_manager_v1", 4); @@ -146,7 +148,7 @@ pub fn build(b: *Build) !void { // exposed to the wlroots module for @cImport() to work. This seems to be // the best way to do so with the current std.Build API. wlroots.resolved_target = target; - wlroots.linkSystemLibrary("wlroots", .{}); + wlroots.linkSystemLibrary("wlroots-0.18", .{}); const flags = b.createModule(.{ .root_source_file = b.path("common/flags.zig") }); const globber = b.createModule(.{ .root_source_file = b.path("common/globber.zig") }); @@ -167,7 +169,7 @@ pub fn build(b: *Build) !void { river.linkSystemLibrary("libevdev"); river.linkSystemLibrary("libinput"); river.linkSystemLibrary("wayland-server"); - river.linkSystemLibrary("wlroots"); + river.linkSystemLibrary("wlroots-0.18"); river.linkSystemLibrary("xkbcommon"); river.linkSystemLibrary("pixman-1"); diff --git a/build.zig.zon b/build.zig.zon index 28e5165..b35b27e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -12,8 +12,8 @@ .hash = "1220687c8c47a48ba285d26a05600f8700d37fc637e223ced3aa8324f3650bf52242", }, .@"zig-wlroots" = .{ - .url = "https://codeberg.org/ifreund/zig-wlroots/archive/084736cd92364b5fa7d8161611d085ce272fa707.tar.gz", - .hash = "12208383c1cf42e9b932b90f68cd4f378582cf966355a6377fd8f913852e7bc2d7c6", + .url = "https://codeberg.org/ifreund/zig-wlroots/archive/e486223799648d27e8b91c5fe0ea4c088b74b707.tar.gz", + .hash = "1220aeb3317e16c38583839961c9d695fa60d23a3d506c8275fb0e8fa9849844f2f7", }, .@"zig-xkbcommon" = .{ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.2.0.tar.gz", diff --git a/completions/bash/riverctl b/completions/bash/riverctl index 68684d5..a889d4c 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -99,6 +99,7 @@ function __riverctl_completion () tap-button-map \ scroll-method \ scroll-button \ + scroll-button-lock \ map-to-output" COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[3]}")) elif [ "${COMP_WORDS[1]}" == "hide-cursor" ] @@ -117,7 +118,7 @@ function __riverctl_completion () "events") OPTS="enabled disabled disabled-on-external-mouse" ;; "accel-profile") OPTS="none flat adaptive" ;; "click-method") OPTS="none button-areas clickfinger" ;; - "drag"|"drag-lock"|"disable-while-typing"|"middle-emulation"|"left-handed"|"tap") OPTS="enabled disabled" ;; + "drag"|"drag-lock"|"disable-while-typing"|"middle-emulation"|"left-handed"|"tap"|"scroll-button-lock") OPTS="enabled disabled" ;; "tap-button-map") OPTS="left-right-middle left-middle-right" ;; "scroll-method") OPTS="none two-finger edge button" ;; *) return ;; diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index 65fad0a..50458b4 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -119,10 +119,11 @@ complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_ complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'tap-button-map' -d 'Configure the button mapping for tapping' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-method' -d 'Set the scroll method' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-button' -d 'Set the scroll button' +complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-button-lock' -d 'Enable or disable the scroll button lock functionality' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'map-to-output' -d 'Map to a given output' # Subcommands for the subcommands of 'input' -complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from drag drag-lock disable-while-typing disable-while-trackpointing middle-emulation natural-scroll left-handed tap' -a 'enabled disabled' +complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from drag drag-lock disable-while-typing disable-while-trackpointing middle-emulation natural-scroll left-handed tap scroll-button-lock' -a 'enabled disabled' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from events' -a 'enabled disabled disabled-on-external-mouse' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from accel-profile' -a 'none flat adaptive' complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from click-method' -a 'none button-areas clickfinger' diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl index ade30e5..ad372d3 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -126,6 +126,7 @@ _riverctl() 'tap-button-map:Configure the button mapping for tapping' 'scroll-method:Set the scroll method' 'scroll-button:Set the scroll button' + 'scroll-button-lock:Enable or disable the scroll button lock functionality' 'map-to-output:Map to a given output' ) @@ -135,7 +136,7 @@ _riverctl() case "$line[2]" in events) _alternative 'input-cmds:args:(enabled disabled disabled-on-external-mouse)' ;; accel-profile) _alternative 'input-cmds:args:(none flat adaptive)' ;; - click-method) _alternative 'input-cmds:args:(none button-area clickfinger)' ;; + click-method) _alternative 'input-cmds:args:(none button-areas clickfinger)' ;; drag) _alternative 'input-cmds:args:(enabled disabled)' ;; drag-lock) _alternative 'input-cmds:args:(enabled disabled)' ;; disable-while-typing) _alternative 'input-cmds:args:(enabled disabled)' ;; @@ -144,6 +145,7 @@ _riverctl() natural-scroll) _alternative 'input-cmds:args:(enabled disabled)' ;; left-handed) _alternative 'input-cmds:args:(enabled disabled)' ;; tap) _alternative 'input-cmds:args:(enabled disabled)' ;; + scroll-button-lock) _alternative 'input-cmds:args:(enabled disabled)' ;; tap-button-map) _alternative 'input-cmds:args:(left-right-middle left-middle-right)' ;; scroll-method) _alternative 'input-cmds:args:(none two-finger edge button)' ;; *) return 0 ;; diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index 01b05a1..3a3ac1b 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -307,11 +307,16 @@ matches everything while _\*\*_ and the empty string are invalid. - *fullscreen*: Make the view fullscreen. Applies only to new views. - *no-fullscreen*: Don't make the view fullscreen. Applies only to new views. + - *tearing*: Allow the view to tear when fullscreen regardless of the + view's preference. Applies to new and existing views. + - *no-tearing*: Disable tearing for the view regardless of the view's + preference. Applies to new and existing views. Both *float* and *no-float* rules are added to the same list, which means that adding a *no-float* rule with the same arguments as a *float* rule will overwrite it. The same holds for *ssd* and - *csd*, *fullscreen* and *no-fullscreen* rules. + *csd*, *fullscreen* and *no-fullscreen*, *tearing* and + *no-tearing* rules. If multiple rules in a list match a given view the most specific rule will be applied. For example with the following rules @@ -364,6 +369,10 @@ matches everything while _\*\*_ and the empty string are invalid. Set the attach mode of the currently focused output, overriding the value of default-attach-mode if any. +*allow-tearing* *enabled*|*disabled* + Allow fullscreen views to tear if requested by the view. See also the + *tearing* rule to force enable tearing for specific views. + *background-color* _0xRRGGBB_|_0xRRGGBBAA_ Set the background color. @@ -465,8 +474,8 @@ matches everything while _\*\*_ and the empty string are invalid. The _input_ command can be used to create a configuration rule for an input device identified by its _name_. -The _name_ of an input device consists of its type, its numerical vendor id, -its numerical product id and finally its self-advertised name, separated by -. +The _name_ of an input device consists of its type, its decimal vendor id, +its decimal product id and finally its self-advertised name, separated by -. Simple globbing patterns are supported, see the rules section for further information on globs. @@ -536,6 +545,11 @@ However note that not every input device supports every property. Set the scroll button of an input device. _button_ is the name of a Linux input event code. +*input* _name_ *scroll-button-lock* *enabled*|*disabled* + Enable or disable the scroll button lock functionality of the input device. If + active, the button does not need to be held down. One press makes the button + considered to be held down, and a second press releases the button. + *input* _name_ *map-to-output* _output_|*disabled* Maps the input to a given output. This is valid even if the output isn't currently active and will lead to the device being mapped once it is diff --git a/river/Config.zig b/river/Config.zig index 0b54f77..f61c82c 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -68,6 +68,9 @@ pub const Dimensions = struct { height: u31, }; +/// Whether to allow tearing page flips when fullscreen if a view requests it. +allow_tearing: bool = false, + /// Color of background in RGBA with premultiplied alpha (alpha should only affect nested sessions) background_color: [4]f32 = [_]f32{ 0.0, 0.16862745, 0.21176471, 1.0 }, // Solarized base03 @@ -98,6 +101,7 @@ rules: struct { position: RuleList(Position) = .{}, dimensions: RuleList(Dimensions) = .{}, fullscreen: RuleList(bool) = .{}, + tearing: RuleList(bool) = .{}, } = .{}, /// The selected focus_follows_cursor mode diff --git a/river/Cursor.zig b/river/Cursor.zig index aa031fc..235f29c 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -324,6 +324,7 @@ fn handleAxis(listener: *wl.Listener(*wlr.Pointer.event.Axis), event: *wlr.Point math.maxInt(i32) / 2, )), event.source, + event.relative_direction, ); } @@ -568,7 +569,7 @@ fn handleTouchUp( cursor.seat.handleActivity(); if (cursor.touch_points.remove(event.touch_id)) { - cursor.seat.wlr_seat.touchNotifyUp(event.time_msec, event.touch_id); + _ = cursor.seat.wlr_seat.touchNotifyUp(event.time_msec, event.touch_id); } } @@ -582,32 +583,9 @@ fn handleTouchCancel( cursor.touch_points.clearRetainingCapacity(); - // We can't call touchNotifyCancel() from inside the loop over touch points as it also loops - // over touch points and may destroy multiple touch points in a single call. - // - // What we should do here is `while (touch_points.first()) |point| cancel()` but since the - // surface may be null we can't rely on the fact tha all touch points will be destroyed - // and risk an infinite loop if the surface of any wlr_touch_point is null. - // - // This is all really silly and totally unnecessary since all touchNotifyCancel() does with - // the surface argument is obtain a seat client and touch_point.seat_client is never null. - // TODO(wlroots) clean this up after the wlroots MR fixing this is merged: - // https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4613 - - // The upper bound of 32 comes from an implementation detail of libinput which uses - // a 32-bit integer as a map to keep track of touch points. - var surfaces: std.BoundedArray(*wlr.Surface, 32) = .{}; - { - var it = cursor.seat.wlr_seat.touch_state.touch_points.iterator(.forward); - while (it.next()) |touch_point| { - if (touch_point.surface) |surface| { - surfaces.append(surface) catch break; - } - } - } - - for (surfaces.slice()) |surface| { - cursor.seat.wlr_seat.touchNotifyCancel(surface); + const wlr_seat = cursor.seat.wlr_seat; + while (wlr_seat.touch_state.touch_points.first()) |touch_point| { + wlr_seat.touchNotifyCancel(touch_point.client); } } diff --git a/river/InputConfig.zig b/river/InputConfig.zig index 6922081..40abf3c 100644 --- a/river/InputConfig.zig +++ b/river/InputConfig.zig @@ -215,6 +215,18 @@ pub const ScrollButton = struct { } }; +pub const ScrollButtonLock = enum { + enabled, + disabled, + + fn apply(scroll_button_lock: ScrollButtonLock, device: *c.libinput_device) void { + _ = c.libinput_device_config_scroll_set_button_lock(device, switch (scroll_button_lock) { + .enabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_ENABLED, + .disabled => c.LIBINPUT_CONFIG_SCROLL_BUTTON_LOCK_DISABLED, + }); + } +}; + pub const MapToOutput = struct { output_name: ?[]const u8, @@ -232,7 +244,7 @@ pub const MapToOutput = struct { }; switch (device.wlr_device.type) { - .pointer, .touch, .tablet_tool => { + .pointer, .touch, .tablet => { log.debug("mapping input '{s}' -> '{s}'", .{ device.identifier, if (wlr_output) |o| o.name else "", @@ -240,14 +252,14 @@ pub const MapToOutput = struct { device.seat.cursor.wlr_cursor.mapInputToOutput(device.wlr_device, wlr_output); - if (device.wlr_device.type == .tablet_tool) { + if (device.wlr_device.type == .tablet) { const tablet: *Tablet = @fieldParentPtr("device", device); tablet.output_mapping = wlr_output; } }, // These devices do not support being mapped to outputs. - .keyboard, .tablet_pad, .switch_device => {}, + .keyboard, .tablet_pad, .@"switch" => {}, } } }; @@ -279,6 +291,7 @@ tap: ?TapState = null, @"pointer-accel": ?PointerAccel = null, @"scroll-method": ?ScrollMethod = null, @"scroll-button": ?ScrollButton = null, +@"scroll-button-lock": ?ScrollButtonLock = null, @"map-to-output": ?MapToOutput = null, pub fn deinit(config: *InputConfig) void { diff --git a/river/InputDevice.zig b/river/InputDevice.zig index f3d9924..a35eefd 100644 --- a/river/InputDevice.zig +++ b/river/InputDevice.zig @@ -24,6 +24,7 @@ const wl = @import("wayland").server.wl; const globber = @import("globber"); +const c = @import("c.zig"); const server = &@import("main.zig").server; const util = @import("util.zig"); @@ -52,19 +53,21 @@ config: struct { link: wl.list.Link, pub fn init(device: *InputDevice, seat: *Seat, wlr_device: *wlr.InputDevice) !void { - const device_type: []const u8 = switch (wlr_device.type) { - .switch_device => "switch", - .tablet_tool => "tablet", - else => @tagName(wlr_device.type), - }; + var vendor: c_uint = 0; + var product: c_uint = 0; + + if (wlr_device.getLibinputDevice()) |d| { + vendor = c.libinput_device_get_id_vendor(@ptrCast(d)); + product = c.libinput_device_get_id_product(@ptrCast(d)); + } const identifier = try std.fmt.allocPrint( util.gpa, "{s}-{}-{}-{s}", .{ - device_type, - wlr_device.vendor, - wlr_device.product, + @tagName(wlr_device.type), + vendor, + product, mem.trim(u8, mem.sliceTo(wlr_device.name orelse "unknown", 0), &ascii.whitespace), }, ); @@ -139,11 +142,11 @@ fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) device.deinit(); util.gpa.destroy(device); }, - .tablet_tool => { + .tablet => { const tablet: *Tablet = @fieldParentPtr("device", device); tablet.destroy(); }, - .switch_device => { + .@"switch" => { const switch_device: *Switch = @fieldParentPtr("device", device); switch_device.deinit(); util.gpa.destroy(switch_device); diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig index 81dfe57..d20d159 100644 --- a/river/LayerSurface.zig +++ b/river/LayerSurface.zig @@ -66,11 +66,6 @@ pub fn create(wlr_layer_surface: *wlr.LayerSurfaceV1) error{OutOfMemory}!void { wlr_layer_surface.surface.events.unmap.add(&layer_surface.unmap); wlr_layer_surface.surface.events.commit.add(&layer_surface.commit); wlr_layer_surface.events.new_popup.add(&layer_surface.new_popup); - - // wlroots only informs us of the new surface after the first commit, - // so our listener does not get called for this first commit. However, - // we do want our listener called in order to send the initial configure. - handleCommit(&layer_surface.commit, wlr_layer_surface.surface); } pub fn destroyPopups(layer_surface: *LayerSurface) void { @@ -100,11 +95,19 @@ fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), _: *wlr.LayerSurfa fn handleMap(listener: *wl.Listener(void)) void { const layer_surface: *LayerSurface = @fieldParentPtr("map", listener); + const wlr_surface = layer_surface.wlr_layer_surface; - log.debug("layer surface '{s}' mapped", .{layer_surface.wlr_layer_surface.namespace}); + log.debug("layer surface '{s}' mapped", .{wlr_surface.namespace}); layer_surface.output.arrangeLayers(); - handleKeyboardInteractiveExclusive(layer_surface.output); + + const consider = wlr_surface.current.keyboard_interactive == .on_demand and + (wlr_surface.current.layer == .top or wlr_surface.current.layer == .overlay); + handleKeyboardInteractiveExclusive( + layer_surface.output, + if (consider) layer_surface else null, + ); + server.root.applyPending(); } @@ -114,7 +117,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void { log.debug("layer surface '{s}' unmapped", .{layer_surface.wlr_layer_surface.namespace}); layer_surface.output.arrangeLayers(); - handleKeyboardInteractiveExclusive(layer_surface.output); + handleKeyboardInteractiveExclusive(layer_surface.output, null); server.root.applyPending(); } @@ -134,18 +137,20 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { @as(u32, @bitCast(wlr_layer_surface.current.committed)) != 0) { layer_surface.output.arrangeLayers(); - handleKeyboardInteractiveExclusive(layer_surface.output); + handleKeyboardInteractiveExclusive(layer_surface.output, null); server.root.applyPending(); } } +/// Focus topmost keyboard-interactivity-exclusive layer surface above normal +/// content, or if none found, focus the surface given as `consider`. /// Requires a call to Root.applyPending() -fn handleKeyboardInteractiveExclusive(output: *Output) void { +fn handleKeyboardInteractiveExclusive(output: *Output, consider: ?*LayerSurface) void { if (server.lock_manager.state != .unlocked) return; - // Find the topmost layer surface in the top or overlay layers which - // requests keyboard interactivity if any. - const topmost_surface = outer: for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| { + // Find the topmost layer surface (if any) in the top or overlay layers which + // requests exclusive keyboard interactivity. + const to_focus = outer: for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| { const tree = output.layerSurfaceTree(layer); // Iterate in reverse to match rendering order. var it = tree.children.iterator(.reverse); @@ -161,17 +166,21 @@ fn handleKeyboardInteractiveExclusive(output: *Output) void { } } } - } else null; + } else consider; + + if (to_focus) |s| { + assert(s.wlr_layer_surface.current.keyboard_interactive != .none); + } var it = server.input_manager.seats.first; while (it) |node| : (it = node.next) { const seat = &node.data; if (seat.focused_output == output) { - if (topmost_surface) |to_focus| { + if (to_focus) |s| { // If we found a surface on the output that requires focus, grab the focus of all // seats that are focusing that output. - seat.setFocusRaw(.{ .layer = to_focus }); + seat.setFocusRaw(.{ .layer = s }); continue; } } diff --git a/river/Output.zig b/river/Output.zig index c9d8add..be6a5bb 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -367,6 +367,8 @@ fn sendLayerConfigures( if (@as(?*SceneNodeData, @ptrFromInt(node.data))) |node_data| { const layer_surface = node_data.data.layer_surface; + if (!layer_surface.wlr_layer_surface.initialized) continue; + const exclusive = layer_surface.wlr_layer_surface.current.exclusive_zone > 0; if (exclusive != (mode == .exclusive)) { continue; @@ -536,10 +538,12 @@ fn handleFrame(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { } fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void { - if (output.gamma_dirty) { - var state = wlr.Output.State.init(); - defer state.finish(); + var state = wlr.Output.State.init(); + defer state.finish(); + if (!scene_output.buildState(&state, null)) return error.CommitFailed; + + if (output.gamma_dirty) { const control = server.root.gamma_control_manager.getControl(output.wlr_output); if (!wlr.GammaControlV1.apply(control, &state)) return error.OutOfMemory; @@ -551,18 +555,24 @@ fn renderAndCommit(output: *Output, scene_output: *wlr.SceneOutput) !void { // has a null LUT. The wayland backend for example has this behavior. state.committed.gamma_lut = false; } - - if (!scene_output.buildState(&state, null)) return error.CommitFailed; - - if (!output.wlr_output.commitState(&state)) return error.CommitFailed; - - // TODO(wlroots) remove this rotate() call when updating to wlroots 0.18 - scene_output.damage_ring.rotate(); - output.gamma_dirty = false; - } else { - if (!scene_output.commit(null)) return error.CommitFailed; } + if (output.current.fullscreen) |fullscreen| { + if (fullscreen.allowTearing()) { + state.tearing_page_flip = true; + if (!output.wlr_output.testState(&state)) { + log.debug("tearing page flip test failed for {s}, retrying without tearing", .{ + output.wlr_output.name, + }); + state.tearing_page_flip = false; + } + } + } + + if (!output.wlr_output.commitState(&state)) return error.CommitFailed; + + output.gamma_dirty = false; + if (server.lock_manager.state == .locked or (server.lock_manager.state == .waiting_for_lock_surfaces and output.locked_content.node.enabled) or server.lock_manager.state == .waiting_for_blank) diff --git a/river/PointerConstraint.zig b/river/PointerConstraint.zig index b35b29f..a2c4d74 100644 --- a/river/PointerConstraint.zig +++ b/river/PointerConstraint.zig @@ -42,7 +42,7 @@ state: union(enum) { } = .inactive, destroy: wl.Listener(*wlr.PointerConstraintV1) = wl.Listener(*wlr.PointerConstraintV1).init(handleDestroy), -set_region: wl.Listener(void) = wl.Listener(void).init(handleSetRegion), +commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), node_destroy: wl.Listener(void) = wl.Listener(void).init(handleNodeDestroy), @@ -58,7 +58,7 @@ pub fn create(wlr_constraint: *wlr.PointerConstraintV1) error{OutOfMemory}!void wlr_constraint.data = @intFromPtr(constraint); wlr_constraint.events.destroy.add(&constraint.destroy); - wlr_constraint.events.set_region.add(&constraint.set_region); + wlr_constraint.surface.events.commit.add(&constraint.commit); if (seat.wlr_seat.keyboard_state.focused_surface) |surface| { if (surface == wlr_constraint.surface) { @@ -169,7 +169,7 @@ pub fn deactivate(constraint: *PointerConstraint) void { fn warpToHintIfSet(constraint: *PointerConstraint) void { const seat: *Seat = @ptrFromInt(constraint.wlr_constraint.seat.data); - if (constraint.wlr_constraint.current.committed.cursor_hint) { + if (constraint.wlr_constraint.current.cursor_hint.enabled) { var lx: i32 = undefined; var ly: i32 = undefined; _ = constraint.state.active.node.coords(&lx, &ly); @@ -201,7 +201,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.PointerConstraintV1), _: *wlr.Point } constraint.destroy.link.remove(); - constraint.set_region.link.remove(); + constraint.commit.link.remove(); if (seat.cursor.constraint == constraint) { seat.cursor.constraint = null; @@ -210,8 +210,11 @@ fn handleDestroy(listener: *wl.Listener(*wlr.PointerConstraintV1), _: *wlr.Point util.gpa.destroy(constraint); } -fn handleSetRegion(listener: *wl.Listener(void)) void { - const constraint: *PointerConstraint = @fieldParentPtr("set_region", listener); +// It is necessary to listen for the commit event rather than the set_region +// event as the latter is not triggered by wlroots when the input region of +// the surface changes. +fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { + const constraint: *PointerConstraint = @fieldParentPtr("commit", listener); const seat: *Seat = @ptrFromInt(constraint.wlr_constraint.seat.data); switch (constraint.state) { @@ -219,7 +222,7 @@ fn handleSetRegion(listener: *wl.Listener(void)) void { const sx: i32 = @intFromFloat(state.sx); const sy: i32 = @intFromFloat(state.sy); if (!constraint.wlr_constraint.region.containsPoint(sx, sy, null)) { - log.info("deactivating pointer constraint, region change left pointer outside constraint", .{}); + log.info("deactivating pointer constraint, (input) region change left pointer outside constraint", .{}); constraint.deactivate(); } }, diff --git a/river/Root.zig b/river/Root.zig index 149e67f..15290ec 100644 --- a/river/Root.zig +++ b/river/Root.zig @@ -117,7 +117,7 @@ transaction_timeout: *wl.EventSource, pending_state_dirty: bool = false, pub fn init(root: *Root) !void { - const output_layout = try wlr.OutputLayout.create(); + const output_layout = try wlr.OutputLayout.create(server.wl_server); errdefer output_layout.destroy(); const scene = try wlr.Scene.create(); @@ -131,9 +131,6 @@ pub fn init(root: *Root) !void { const outputs = try interactive_content.createSceneTree(); const override_redirect = if (build_options.xwayland) try interactive_content.createSceneTree(); - const presentation = try wlr.Presentation.create(server.wl_server, server.backend); - scene.setPresentation(presentation); - const event_loop = server.wl_server.getEventLoop(); const transaction_timeout = try event_loop.addTimer(*Root, handleTransactionTimeout, root); errdefer transaction_timeout.remove(); @@ -166,7 +163,7 @@ pub fn init(root: *Root) !void { .all_outputs = undefined, .active_outputs = undefined, - .presentation = presentation, + .presentation = try wlr.Presentation.create(server.wl_server, server.backend), .xdg_output_manager = try wlr.XdgOutputManagerV1.create(server.wl_server, output_layout), .output_manager = try wlr.OutputManagerV1.create(server.wl_server), .power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server), @@ -676,24 +673,12 @@ fn commitTransaction(root: *Root) void { while (focus_stack_it.next()) |view| { assert(view.inflight.output == output); - if (view.current.output != view.inflight.output or - (output.current.fullscreen == view and output.inflight.fullscreen != view)) - { - if (view.inflight.float) { - view.tree.node.reparent(output.layers.float); - } else { - view.tree.node.reparent(output.layers.layout); - } - view.popup_tree.node.reparent(output.layers.popups); - } - - if (view.current.float != view.inflight.float) { - if (view.inflight.float) { - view.tree.node.reparent(output.layers.float); - } else { - view.tree.node.reparent(output.layers.layout); - } + if (view.inflight.float) { + view.tree.node.reparent(output.layers.float); + } else { + view.tree.node.reparent(output.layers.layout); } + view.popup_tree.node.reparent(output.layers.popups); view.commitTransaction(); @@ -706,15 +691,13 @@ fn commitTransaction(root: *Root) void { } } - if (output.inflight.fullscreen != output.current.fullscreen) { - if (output.inflight.fullscreen) |view| { - assert(view.inflight.output == output); - assert(view.current.output == output); - view.tree.node.reparent(output.layers.fullscreen); - } - output.current.fullscreen = output.inflight.fullscreen; - output.layers.fullscreen.node.setEnabled(output.current.fullscreen != null); + if (output.inflight.fullscreen) |view| { + assert(view.inflight.output == output); + assert(view.current.output == output); + view.tree.node.reparent(output.layers.fullscreen); } + output.current.fullscreen = output.inflight.fullscreen; + output.layers.fullscreen.node.setEnabled(output.current.fullscreen != null); output.status.handleTransactionCommit(output); } diff --git a/river/Seat.zig b/river/Seat.zig index 7bb3aaf..7c77c58 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -511,11 +511,11 @@ fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice) !void { seat.cursor.wlr_cursor.attachInputDevice(wlr_device); }, - .tablet_tool => { + .tablet => { try Tablet.create(seat, wlr_device); seat.cursor.wlr_cursor.attachInputDevice(wlr_device); }, - .switch_device => { + .@"switch" => { const switch_device = try util.gpa.create(Switch); errdefer util.gpa.destroy(switch_device); @@ -538,7 +538,7 @@ pub fn updateCapabilities(seat: *Seat) void { switch (device.wlr_device.type) { .keyboard => capabilities.keyboard = true, .touch => capabilities.touch = true, - .pointer, .switch_device, .tablet_tool => {}, + .pointer, .@"switch", .tablet => {}, .tablet_pad => unreachable, } } diff --git a/river/Server.zig b/river/Server.zig index 7b46f3f..8fc7422 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -38,6 +38,7 @@ const Root = @import("Root.zig"); const Seat = @import("Seat.zig"); const SceneNodeData = @import("SceneNodeData.zig"); const StatusManager = @import("StatusManager.zig"); +const TabletTool = @import("TabletTool.zig"); const XdgDecoration = @import("XdgDecoration.zig"); const XdgToplevel = @import("XdgToplevel.zig"); const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); @@ -83,6 +84,8 @@ screencopy_manager: *wlr.ScreencopyManagerV1, foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1, +tearing_control_manager: *wlr.TearingControlManagerV1, + input_manager: InputManager, root: Root, config: Config, @@ -96,8 +99,10 @@ xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_optio new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void = if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface).init(handleNewXwaylandSurface), -new_xdg_surface: wl.Listener(*wlr.XdgSurface) = - wl.Listener(*wlr.XdgSurface).init(handleNewXdgSurface), +renderer_lost: wl.Listener(void) = wl.Listener(void).init(handleRendererLost), + +new_xdg_toplevel: wl.Listener(*wlr.XdgToplevel) = + wl.Listener(*wlr.XdgToplevel).init(handleNewXdgToplevel), new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) = wl.Listener(*wlr.XdgToplevelDecorationV1).init(handleNewToplevelDecoration), new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1) = @@ -113,14 +118,14 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void { // This keeps the code simpler and more readable. const wl_server = try wl.Server.create(); + const loop = wl_server.getEventLoop(); var session: ?*wlr.Session = undefined; - const backend = try wlr.Backend.autocreate(wl_server, &session); + const backend = try wlr.Backend.autocreate(loop, &session); const renderer = try wlr.Renderer.autocreate(backend); const compositor = try wlr.Compositor.create(wl_server, 6, renderer); - const loop = wl_server.getEventLoop(); server.* = .{ .wl_server = wl_server, .sigint_source = try loop.addSignal(*wl.Server, posix.SIG.INT, terminate, wl_server), @@ -156,6 +161,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void { .foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(wl_server), + .tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1), + .config = try Config.init(), .root = undefined, @@ -167,7 +174,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void { .lock_manager = undefined, }; - if (renderer.getDmabufFormats() != null and renderer.getDrmFd() >= 0) { + if (renderer.getTextureFormats(@intFromEnum(wlr.BufferCap.dmabuf)) != null) { // wl_drm is a legacy interface and all clients should switch to linux_dmabuf. // However, enough widely used clients still rely on wl_drm that the pragmatic option // is to keep it around for the near future. @@ -190,7 +197,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void { try server.idle_inhibit_manager.init(); try server.lock_manager.init(); - server.xdg_shell.events.new_surface.add(&server.new_xdg_surface); + server.renderer.events.lost.add(&server.renderer_lost); + server.xdg_shell.events.new_toplevel.add(&server.new_xdg_toplevel); server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration); server.layer_shell.events.new_surface.add(&server.new_layer_surface); server.xdg_activation.events.request_activate.add(&server.request_activate); @@ -204,7 +212,8 @@ pub fn deinit(server: *Server) void { server.sigint_source.remove(); server.sigterm_source.remove(); - server.new_xdg_surface.link.remove(); + server.renderer_lost.link.remove(); + server.new_xdg_toplevel.link.remove(); server.new_toplevel_decoration.link.remove(); server.new_layer_surface.link.remove(); server.request_activate.link.remove(); @@ -277,14 +286,6 @@ fn globalFilter(client: *const wl.Client, global: *const wl.Global, server: *Ser } } -fn hackGlobal(ptr: *anyopaque) *wl.Global { - // TODO(wlroots) MR that eliminates the need for this hack: - // https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4612 - if (wlr.version.major != 0 or wlr.version.minor != 17) @compileError("FIXME"); - - return @as(*extern struct { global: *wl.Global }, @alignCast(@ptrCast(ptr))).global; -} - /// Returns true if the global is allowlisted for security contexts fn allowlist(server: *Server, global: *const wl.Global) bool { if (server.drm) |drm| if (global == drm.global) return true; @@ -300,8 +301,8 @@ fn allowlist(server: *Server, global: *const wl.Global) bool { // with an assertion failure. return global.getInterface() == wl.Output.getInterface() or global.getInterface() == wl.Seat.getInterface() or - global == hackGlobal(server.shm) or - global == hackGlobal(server.single_pixel_buffer_manager) or + global == server.shm.global or + global == server.single_pixel_buffer_manager.global or global == server.viewporter.global or global == server.fractional_scale_manager.global or global == server.compositor.global or @@ -319,7 +320,8 @@ fn allowlist(server: *Server, global: *const wl.Global) bool { global == server.input_manager.text_input_manager.global or global == server.input_manager.tablet_manager.global or global == server.input_manager.pointer_gestures.global or - global == server.idle_inhibit_manager.wlr_manager.global; + global == server.idle_inhibit_manager.wlr_manager.global or + global == server.tearing_control_manager.global; } /// Returns true if the global is blocked for security contexts @@ -336,7 +338,7 @@ fn blocklist(server: *Server, global: *const wl.Global) bool { global == server.root.output_manager.global or global == server.root.power_manager.global or global == server.root.gamma_control_manager.global or - global == hackGlobal(server.input_manager.idle_notifier) or + global == server.input_manager.idle_notifier.global or global == server.input_manager.virtual_pointer_manager.global or global == server.input_manager.virtual_keyboard_manager.global or global == server.input_manager.input_method_manager.global or @@ -349,17 +351,55 @@ fn terminate(_: c_int, wl_server: *wl.Server) c_int { return 0; } -fn handleNewXdgSurface(_: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { - if (xdg_surface.role == .popup) { - log.debug("new xdg_popup", .{}); - return; +fn handleRendererLost(listener: *wl.Listener(void)) void { + const server: *Server = @fieldParentPtr("renderer_lost", listener); + + log.info("recovering from GPU reset", .{}); + + // There's not much that can be done if creating a new renderer or allocator fails. + // With luck there might be another GPU reset after which we try again and succeed. + + server.recoverFromGpuReset() catch |err| switch (err) { + error.RendererCreateFailed => log.err("failed to create new renderer after GPU reset", .{}), + error.AllocatorCreateFailed => log.err("failed to create new allocator after GPU reset", .{}), + }; +} + +fn recoverFromGpuReset(server: *Server) !void { + const new_renderer = try wlr.Renderer.autocreate(server.backend); + errdefer new_renderer.destroy(); + + const new_allocator = try wlr.Allocator.autocreate(server.backend, new_renderer); + errdefer comptime unreachable; // no failure allowed after this point + + server.renderer_lost.link.remove(); + new_renderer.events.lost.add(&server.renderer_lost); + + server.compositor.setRenderer(new_renderer); + + { + var it = server.root.all_outputs.iterator(.forward); + while (it.next()) |output| { + // This should never fail here as failure with this combination of + // renderer, allocator, and backend should have prevented creating + // the output in the first place. + _ = output.wlr_output.initRender(new_allocator, new_renderer); + } } + server.renderer.destroy(); + server.renderer = new_renderer; + + server.allocator.destroy(); + server.allocator = new_allocator; +} + +fn handleNewXdgToplevel(_: *wl.Listener(*wlr.XdgToplevel), xdg_toplevel: *wlr.XdgToplevel) void { log.debug("new xdg_toplevel", .{}); - XdgToplevel.create(xdg_surface.role_data.toplevel.?) catch { + XdgToplevel.create(xdg_toplevel) catch { log.err("out of memory", .{}); - xdg_surface.resource.postNoMemory(); + xdg_toplevel.resource.postNoMemory(); return; }; } @@ -450,17 +490,27 @@ fn handleRequestSetCursorShape( _: *wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape), event: *wlr.CursorShapeManagerV1.event.RequestSetShape, ) void { - // Ignore requests to set a tablet tool's cursor shape for now - // TODO(wlroots): https://gitlab.freedesktop.org/wlroots/wlroots/-/issues/3821 - if (event.device_type == .tablet_tool) return; + const seat: *Seat = @ptrFromInt(event.seat_client.seat.data); - const focused_client = event.seat_client.seat.pointer_state.focused_client; + if (event.tablet_tool) |wp_tool| { + assert(event.device_type == .tablet_tool); - // This can be sent by any client, so we check to make sure this one is - // actually has pointer focus first. - if (focused_client == event.seat_client) { - const seat: *Seat = @ptrFromInt(event.seat_client.seat.data); - const name = wlr.CursorShapeManagerV1.shapeName(event.shape); - seat.cursor.setXcursor(name); + const tool = TabletTool.get(event.seat_client.seat, wp_tool.wlr_tool) catch return; + + if (tool.allowSetCursor(event.seat_client, event.serial)) { + const name = wlr.CursorShapeManagerV1.shapeName(event.shape); + tool.wlr_cursor.setXcursor(seat.cursor.xcursor_manager, name); + } + } else { + assert(event.device_type == .pointer); + + const focused_client = event.seat_client.seat.pointer_state.focused_client; + + // This can be sent by any client, so we check to make sure this one is + // actually has pointer focus first. + if (focused_client == event.seat_client) { + const name = wlr.CursorShapeManagerV1.shapeName(event.shape); + seat.cursor.setXcursor(name); + } } } diff --git a/river/Tablet.zig b/river/Tablet.zig index d5773de..8d3f0da 100644 --- a/river/Tablet.zig +++ b/river/Tablet.zig @@ -33,7 +33,7 @@ wp_tablet: *wlr.TabletV2Tablet, output_mapping: ?*wlr.Output = null, pub fn create(seat: *Seat, wlr_device: *wlr.InputDevice) !void { - assert(wlr_device.type == .tablet_tool); + assert(wlr_device.type == .tablet); const tablet = try util.gpa.create(Tablet); errdefer util.gpa.destroy(tablet); diff --git a/river/TabletTool.zig b/river/TabletTool.zig index 1642469..af558a3 100644 --- a/river/TabletTool.zig +++ b/river/TabletTool.zig @@ -102,24 +102,29 @@ fn handleDestroy(listener: *wl.Listener(*wlr.TabletTool), _: *wlr.TabletTool) vo util.gpa.destroy(tool); } +pub fn allowSetCursor(tool: *TabletTool, seat_client: *wlr.Seat.Client, serial: u32) bool { + if (tool.wp_tool.focused_surface == null or + tool.wp_tool.focused_surface.?.resource.getClient() != seat_client.client) + { + log.debug("client tried to set cursor without focus", .{}); + return false; + } + if (serial != tool.wp_tool.proximity_serial) { + log.debug("focused client tried to set cursor with incorrect serial", .{}); + return false; + } + return true; +} + fn handleSetCursor( listener: *wl.Listener(*wlr.TabletV2TabletTool.event.SetCursor), event: *wlr.TabletV2TabletTool.event.SetCursor, ) void { const tool: *TabletTool = @fieldParentPtr("set_cursor", listener); - if (tool.wp_tool.focused_surface == null or - tool.wp_tool.focused_surface.?.resource.getClient() != event.seat_client.client) - { - log.debug("client tried to set cursor without focus", .{}); - return; + if (tool.allowSetCursor(event.seat_client, event.serial)) { + tool.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y); } - if (event.serial != tool.wp_tool.proximity_serial) { - log.debug("focused client tried to set cursor with incorrect serial", .{}); - return; - } - - tool.wlr_cursor.setSurface(event.surface, event.hotspot_x, event.hotspot_y); } pub fn axis(tool: *TabletTool, tablet: *Tablet, event: *wlr.Tablet.event.Axis) void { diff --git a/river/View.zig b/river/View.zig index 9ee5d8e..57a8332 100644 --- a/river/View.zig +++ b/river/View.zig @@ -23,6 +23,7 @@ const math = std.math; const posix = std.posix; const wlr = @import("wlroots"); const wl = @import("wayland").server.wl; +const wp = @import("wayland").server.wp; const server = &@import("main.zig").server; const util = @import("util.zig"); @@ -59,6 +60,12 @@ const AttachRelativeMode = enum { below, }; +const TearingMode = enum { + no_tearing, + tearing, + window_hint, +}; + pub const State = struct { /// The output the view is currently assigned to. /// May be null if there are no outputs or for newly created views. @@ -177,6 +184,8 @@ foreign_toplevel_handle: ForeignToplevelHandle = .{}, /// Connector name of the output this view occupied before an evacuation. output_before_evac: ?[]const u8 = null, +tearing_mode: TearingMode = .window_hint, + pub fn create(impl: Impl) error{OutOfMemory}!*View { assert(impl != .none); @@ -572,6 +581,22 @@ pub fn getAppId(view: View) ?[*:0]const u8 { }; } +/// Return true if tearing should be allowed for the view. +pub fn allowTearing(view: *View) bool { + switch (view.tearing_mode) { + .no_tearing => return false, + .tearing => return true, + .window_hint => { + if (server.config.allow_tearing) { + if (view.rootSurface()) |root_surface| { + return server.tearing_control_manager.hintFromSurface(root_surface) == .@"async"; + } + } + return false; + }, + } +} + /// Clamp the width/height of the box to the constraints of the view pub fn applyConstraints(view: *View, box: *wlr.Box) void { box.width = math.clamp(box.width, view.constraints.min_width, view.constraints.max_width); @@ -640,6 +665,10 @@ pub fn map(view: *View) !void { view.pending.ssd = ssd; } + if (server.config.rules.tearing.match(view)) |tearing| { + view.tearing_mode = if (tearing) .tearing else .no_tearing; + } + if (server.config.rules.dimensions.match(view)) |dimensions| { view.pending.box.width = dimensions.width; view.pending.box.height = dimensions.height; diff --git a/river/XdgDecoration.zig b/river/XdgDecoration.zig index 8ea55de..eee0c1d 100644 --- a/river/XdgDecoration.zig +++ b/river/XdgDecoration.zig @@ -42,14 +42,9 @@ pub fn init(wlr_decoration: *wlr.XdgToplevelDecorationV1) void { wlr_decoration.events.destroy.add(&decoration.destroy); wlr_decoration.events.request_mode.add(&decoration.request_mode); - const ssd = server.config.rules.ssd.match(toplevel.view) orelse - (decoration.wlr_decoration.requested_mode != .client_side); - - // TODO(wlroots): make sure this is properly batched in a single configure - // with all other initial state when wlroots makes this possible. - _ = wlr_decoration.setMode(if (ssd) .server_side else .client_side); - - toplevel.view.pending.ssd = ssd; + if (toplevel.wlr_toplevel.base.initialized) { + handleRequestMode(&decoration.request_mode, wlr_decoration); + } } pub fn deinit(decoration: *XdgDecoration) void { diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig index fd82fbe..3467210 100644 --- a/river/XdgPopup.zig +++ b/river/XdgPopup.zig @@ -54,7 +54,7 @@ pub fn create( .tree = try parent.createSceneXdgSurface(wlr_xdg_popup.base), }; - wlr_xdg_popup.base.events.destroy.add(&xdg_popup.destroy); + wlr_xdg_popup.events.destroy.add(&xdg_popup.destroy); wlr_xdg_popup.base.surface.events.commit.add(&xdg_popup.commit); wlr_xdg_popup.base.events.new_popup.add(&xdg_popup.new_popup); wlr_xdg_popup.events.reposition.add(&xdg_popup.reposition); diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index ead95ee..67a30d9 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -62,12 +62,12 @@ configure_state: union(enum) { destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy), map: wl.Listener(void) = wl.Listener(void).init(handleMap), unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap), +commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup), // Listeners that are only active while the view is mapped ack_configure: wl.Listener(*wlr.XdgSurface.Configure) = wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure), -commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen), request_move: wl.Listener(*wlr.XdgToplevel.event.Move) = wl.Listener(*wlr.XdgToplevel.event.Move).init(handleRequestMove), @@ -104,11 +104,10 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void { wlr_toplevel.base.surface.data = @intFromPtr(&view.tree.node); // Add listeners that are active over the toplevel's entire lifetime - wlr_toplevel.base.events.destroy.add(&toplevel.destroy); + wlr_toplevel.events.destroy.add(&toplevel.destroy); wlr_toplevel.base.surface.events.map.add(&toplevel.map); + wlr_toplevel.base.surface.events.commit.add(&toplevel.commit); wlr_toplevel.base.events.new_popup.add(&toplevel.new_popup); - - _ = wlr_toplevel.setWmCapabilities(.{ .fullscreen = true }); } /// Send a configure event, applying the inflight state of the view. @@ -213,8 +212,10 @@ fn handleDestroy(listener: *wl.Listener(void)) void { toplevel.destroy.link.remove(); toplevel.map.link.remove(); toplevel.unmap.link.remove(); + toplevel.commit.link.remove(); + toplevel.new_popup.link.remove(); - // The wlr_surface may outlive the wlr_xdg_surface so we must clean up the user data. + // The wlr_surface may outlive the wlr_xdg_toplevel so we must clean up the user data. toplevel.wlr_toplevel.base.surface.data = 0; const view = toplevel.view; @@ -228,7 +229,6 @@ fn handleMap(listener: *wl.Listener(void)) void { // Add listeners that are only active while mapped toplevel.wlr_toplevel.base.events.ack_configure.add(&toplevel.ack_configure); - toplevel.wlr_toplevel.base.surface.events.commit.add(&toplevel.commit); toplevel.wlr_toplevel.events.request_fullscreen.add(&toplevel.request_fullscreen); toplevel.wlr_toplevel.events.request_move.add(&toplevel.request_move); toplevel.wlr_toplevel.events.request_resize.add(&toplevel.request_resize); @@ -270,7 +270,6 @@ fn handleUnmap(listener: *wl.Listener(void)) void { // Remove listeners that are only active while mapped toplevel.ack_configure.link.remove(); - toplevel.commit.link.remove(); toplevel.request_fullscreen.link.remove(); toplevel.request_move.link.remove(); toplevel.request_resize.link.remove(); @@ -309,6 +308,23 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { const toplevel: *XdgToplevel = @fieldParentPtr("commit", listener); const view = toplevel.view; + if (toplevel.wlr_toplevel.base.initial_commit) { + _ = toplevel.wlr_toplevel.setWmCapabilities(.{ .fullscreen = true }); + + if (toplevel.decoration) |decoration| { + const ssd = server.config.rules.ssd.match(toplevel.view) orelse + (decoration.wlr_decoration.requested_mode != .client_side); + _ = decoration.wlr_decoration.setMode(if (ssd) .server_side else .client_side); + toplevel.view.pending.ssd = ssd; + } + + return; + } + + if (!view.mapped) { + return; + } + { const state = &toplevel.wlr_toplevel.current; view.constraints = .{ diff --git a/river/command.zig b/river/command.zig index 6ea5b96..85a5247 100644 --- a/river/command.zig +++ b/river/command.zig @@ -41,6 +41,7 @@ const command_impls = std.StaticStringMap( ).initComptime( .{ // zig fmt: off + .{ "allow-tearing", @import("command/config.zig").allowTearing }, .{ "attach-mode", @import("command/attach_mode.zig").defaultAttachMode }, .{ "background-color", @import("command/config.zig").backgroundColor }, .{ "border-color-focused", @import("command/config.zig").borderColorFocused }, diff --git a/river/command/config.zig b/river/command/config.zig index a97e233..531ae92 100644 --- a/river/command/config.zig +++ b/river/command/config.zig @@ -24,6 +24,20 @@ const Error = @import("../command.zig").Error; const Seat = @import("../Seat.zig"); const Config = @import("../Config.zig"); +pub fn allowTearing( + _: *Seat, + args: []const [:0]const u8, + _: *?[]const u8, +) Error!void { + if (args.len < 2) return Error.NotEnoughArguments; + if (args.len > 2) return Error.TooManyArguments; + + const arg = std.meta.stringToEnum(enum { enabled, disabled }, args[1]) orelse + return Error.UnknownOption; + + server.config.allow_tearing = arg == .enabled; +} + pub fn borderWidth( _: *Seat, args: []const [:0]const u8, diff --git a/river/command/rule.zig b/river/command/rule.zig index 7703955..5b64400 100644 --- a/river/command/rule.zig +++ b/river/command/rule.zig @@ -38,6 +38,8 @@ const Action = enum { dimensions, fullscreen, @"no-fullscreen", + tearing, + @"no-tearing", }; pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void { @@ -53,7 +55,7 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void const action = std.meta.stringToEnum(Action, result.args[0]) orelse return Error.UnknownOption; const positional_arguments_count: u8 = switch (action) { - .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen" => 1, + .float, .@"no-float", .ssd, .csd, .fullscreen, .@"no-fullscreen", .tearing, .@"no-tearing" => 1, .tags, .output => 2, .position, .dimensions => 3, }; @@ -83,6 +85,14 @@ pub fn ruleAdd(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void apply_ssd_rules(); server.root.applyPending(); }, + .tearing, .@"no-tearing" => { + try server.config.rules.tearing.add(.{ + .app_id_glob = app_id_glob, + .title_glob = title_glob, + .value = (action == .tearing), + }); + apply_tearing_rules(); + }, .tags => { const tags = try fmt.parseInt(u32, result.args[1], 10); try server.config.rules.tags.add(.{ @@ -177,6 +187,10 @@ pub fn ruleDel(_: *Seat, args: []const [:0]const u8, _: *?[]const u8) Error!void .fullscreen, .@"no-fullscreen" => { _ = server.config.rules.fullscreen.del(rule); }, + .tearing, .@"no-tearing" => { + _ = server.config.rules.tearing.del(rule); + apply_tearing_rules(); + }, } } @@ -191,6 +205,17 @@ fn apply_ssd_rules() void { } } +fn apply_tearing_rules() void { + var it = server.root.views.iterator(.forward); + while (it.next()) |view| { + if (view.destroying) continue; + + if (server.config.rules.tearing.match(view)) |tearing| { + view.tearing_mode = if (tearing) .tearing else .no_tearing; + } + } +} + pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void { if (args.len < 2) return error.NotEnoughArguments; if (args.len > 2) return error.TooManyArguments; @@ -203,6 +228,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! position, dimensions, fullscreen, + tearing, }, args[1]) orelse return Error.UnknownOption; const max_glob_len = switch (rule_list) { inline else => |list| @field(server.config.rules, @tagName(list)).getMaxGlobLen(), @@ -218,12 +244,13 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! try writer.writeAll("action\n"); switch (rule_list) { - inline .float, .ssd, .output, .fullscreen => |list| { + inline .float, .ssd, .output, .fullscreen, .tearing => |list| { const rules = switch (list) { .float => server.config.rules.float.rules.items, .ssd => server.config.rules.ssd.rules.items, .output => server.config.rules.output.rules.items, .fullscreen => server.config.rules.fullscreen.rules.items, + .tearing => server.config.rules.tearing.rules.items, else => unreachable, }; for (rules) |rule| { @@ -234,6 +261,7 @@ pub fn listRules(_: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error! .ssd => if (rule.value) "ssd" else "csd", .output => rule.value, .fullscreen => if (rule.value) "fullscreen" else "no-fullscreen", + .tearing => if (rule.value) "tearing" else "no-tearing", else => unreachable, }}); } diff --git a/river/main.zig b/river/main.zig index 19d23b5..4226e6a 100644 --- a/river/main.zig +++ b/river/main.zig @@ -31,12 +31,6 @@ const process = @import("process.zig"); const Server = @import("Server.zig"); -comptime { - if (wlr.version.major != 0 or wlr.version.minor != 17 or wlr.version.micro < 2) { - @compileError("river requires at least wlroots version 0.17.2 due to bugs in wlroots 0.17.0/0.17.1"); - } -} - const usage: []const u8 = \\usage: river [options] \\