git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit5c27b014db48a56fa53ff89559ffd168cc118caa
parent3a2bb8b100
authornovakane <[email protected]>
date2021-08-12 16:16
river: implement xdg-activation-v1

- add a new "urgent" border color
- add a new event to river-status-unstable-v1

Co-authored-by: Isaac Freund <[email protected]>

 completions/bash/riverctl             |  1 +
 completions/fish/riverctl.fish        |  3 ++-
 completions/zsh/_riverctl             |  1 +
 deps/zig-wlroots                      |  2 +-
 doc/riverctl.1.scd                    |  3 +++
 protocol/river-status-unstable-v1.xml | 12 ++++++++++--
 river/Config.zig                      |  3 +++
 river/Output.zig                      | 12 ++++++++++++
 river/OutputStatus.zig                | 15 ++++++++++++++-
 river/Root.zig                        |  4 ++++
 river/Seat.zig                        |  1 +
 river/Server.zig                      |  2 ++
 river/StatusManager.zig               |  2 +-
 river/View.zig                        | 26 ++++++++++++++++++++++++++
 river/XdgToplevel.zig                 |  3 +++
 river/command.zig                     |  1 +
 river/command/config.zig              | 15 +++++++++++++++
 river/render.zig                      |  6 +++++-
 18 files changed, 105 insertions(+), 7 deletions(-)

diff --git a/completions/bash/riverctl b/completions/bash/riverctl
index 3475386..dd18ac5 100644
--- a/completions/bash/riverctl
+++ b/completions/bash/riverctl
@@ -39,6 +39,7 @@ function __riverctl_completion ()
 			background-color \
 			border-color-focused \
 			border-color-unfocused \
+			border-color-urgent \
 			border-width \
 			focus-follows-cursor \
 			set-repeat \
diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish
index 88be779..c0112c1 100644
--- a/completions/fish/riverctl.fish
+++ b/completions/fish/riverctl.fish
@@ -4,7 +4,7 @@ end
 
 function __fish_riverctl_complete_no_subcommand
     for i in (commandline -opc)
-        if contains -- $i close csd-filter-add exit float-filter-add focus-output focus-view input list-inputs list-input-configs move resize snap send-to-output spawn swap toggle-float toggle-fullscreen zoom default-layout output-layout send-layout-cmd set-focused-tags set-view-tags toggle-focused-tags toggle-view-tags spawn-tagmask declare-mode enter-mode map map-pointer unmap unmap-pointer attach-mode background-color border-color-focused border-color-unfocused border-width focus-follows-cursor set-repeat set-cursor-warp xcursor-theme
+        if contains -- $i close csd-filter-add exit float-filter-add focus-output focus-view input list-inputs list-input-configs move resize snap send-to-output spawn swap toggle-float toggle-fullscreen zoom default-layout output-layout send-layout-cmd set-focused-tags set-view-tags toggle-focused-tags toggle-view-tags spawn-tagmask declare-mode enter-mode map map-pointer unmap unmap-pointer attach-mode background-color border-color-focused border-color-unfocused border-color-urgent border-width focus-follows-cursor set-repeat set-cursor-warp xcursor-theme
             return 1
         end
     end
@@ -76,6 +76,7 @@ complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a attach-mo
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a background-color       -d 'Set the background color'
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a border-color-focused   -d 'Set the border color of focused views'
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a border-color-unfocused -d 'Set the border color of unfocused views'
+complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a border-color-urgent    -d 'Set the border color of urgent views'
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a border-width           -d 'Set the border width to pixels'
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a focus-follows-cursor   -d 'Configure the focus behavior when moving cursor'
 complete -c riverctl -x -n '__fish_riverctl_complete_no_subcommand' -a set-repeat             -d 'Set the keyboard repeat rate and repeat delay'
diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl
index d782340..1c7034a 100644
--- a/completions/zsh/_riverctl
+++ b/completions/zsh/_riverctl
@@ -45,6 +45,7 @@ _riverctl_subcommands()
         'background-color:Set the background color'
         'border-color-focused:Set the border color of focused views'
         'border-color-unfocused:Set the border color of unfocused views'
+        'border-color-urgent:Set the border color of urgent views'
         'border-width:Set the border width to pixels'
         'focus-follows-cursor:Configure the focus behavior when moving cursor'
         'set-repeat:Set the keyboard repeat rate and repeat delay'
diff --git a/deps/zig-wlroots b/deps/zig-wlroots
index 4c4e598..9bb6b03 160000
--- a/deps/zig-wlroots
+++ b/deps/zig-wlroots
@@ -1 +1 @@
-Subproject commit 4c4e598445a7c4143c5d3650d54c9ffa415119df
+Subproject commit 9bb6b03f0ea04d4ea6a102ed3e45badba9e8e262
diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index 52e14f2..ae6ac48 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -241,6 +241,9 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_
 *border-color-unfocused* _0xRRGGBB_|_0xRRGGBBAA_
 	Set the border color of unfocused views.
 
+*border-color-urgent* _0xRRGGBB_|_0xRRGGBBAA_
+	Set the border color of urgent views.
+
 *border-width* _pixels_
 	Set the border width to _pixels_.
 
diff --git a/protocol/river-status-unstable-v1.xml b/protocol/river-status-unstable-v1.xml
index e31da23..13affaa 100644
--- a/protocol/river-status-unstable-v1.xml
+++ b/protocol/river-status-unstable-v1.xml
@@ -16,7 +16,7 @@
     OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
   </copyright>
 
-  <interface name="zriver_status_manager_v1" version="1">
+  <interface name="zriver_status_manager_v1" version="2">
     <description summary="manage river status objects">
       A global factory for objects that receive status information specific
       to river. It could be used to implement, for example, a status bar.
@@ -47,7 +47,7 @@
     </request>
   </interface>
 
-  <interface name="zriver_output_status_v1" version="1">
+  <interface name="zriver_output_status_v1" version="2">
     <description summary="track output tags and focus">
       This interface allows clients to receive information about the current
       windowing state of an output.
@@ -75,6 +75,14 @@
       </description>
       <arg name="tags" type="array" summary="array of 32-bit bitfields"/>
     </event>
+
+    <event name="urgent_tags" since="2">
+      <description summary="tags of the output with an urgent view">
+        Sent once on binding the interface and again whenever the set of
+        tags with at least one urgent view changes.
+      </description>
+      <arg name="tags" type="uint" summary="32-bit bitfield"/>
+    </event>
   </interface>
 
   <interface name="zriver_seat_status_v1" version="1">
diff --git a/river/Config.zig b/river/Config.zig
index 39150f7..af80a19 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -48,6 +48,9 @@ border_color_focused: [4]f32 = [_]f32{ 0.57647059, 0.63137255, 0.63137255, 1.0 }
 /// Color of border of unfocused window in RGBA
 border_color_unfocused: [4]f32 = [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }, // Solarized base01
 
+/// Color of border of urgent window in RGBA
+border_color_urgent: [4]f32 = [_]f32{ 0.86274510, 0.19607843, 0.18431373, 1.0 }, // Solarized red
+
 /// Map of keymap mode name to mode id
 mode_to_id: std.StringHashMap(usize),
 
diff --git a/river/Output.zig b/river/Output.zig
index c7d866f..2a6a1b3 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -155,6 +155,18 @@ pub fn sendViewTags(self: Self) void {
     while (it) |node| : (it = node.next) node.data.sendViewTags();
 }
 
+pub fn sendUrgentTags(self: Self) void {
+    var urgent_tags: u32 = 0;
+
+    var view_it = self.views.first;
+    while (view_it) |node| : (view_it = node.next) {
+        if (node.view.current.urgent) urgent_tags |= node.view.current.tags;
+    }
+
+    var it = self.status_trackers.first;
+    while (it) |node| : (it = node.next) node.data.sendUrgentTags(urgent_tags);
+}
+
 pub fn arrangeFilter(view: *View, filter_tags: u32) bool {
     return !view.destroying and !view.pending.float and !view.pending.fullscreen and
         view.pending.tags & filter_tags != 0;
diff --git a/river/OutputStatus.zig b/river/OutputStatus.zig
index 2000126..1f088f8 100644
--- a/river/OutputStatus.zig
+++ b/river/OutputStatus.zig
@@ -38,9 +38,16 @@ pub fn init(self: *Self, output: *Output, output_status: *zriver.OutputStatusV1)
 
     output_status.setHandler(*Self, handleRequest, handleDestroy, self);
 
-    // Send view/focused tags once on bind.
+    // Send view/focused/urgent tags once on bind.
     self.sendViewTags();
     self.sendFocusedTags(output.current.tags);
+
+    var urgent_tags: u32 = 0;
+    var view_it = self.output.views.first;
+    while (view_it) |node| : (view_it = node.next) {
+        if (node.view.current.urgent) urgent_tags |= node.view.current.tags;
+    }
+    self.sendUrgentTags(urgent_tags);
 }
 
 pub fn destroy(self: *Self) void {
@@ -82,3 +89,9 @@ pub fn sendViewTags(self: Self) void {
 pub fn sendFocusedTags(self: Self, tags: u32) void {
     self.output_status.sendFocusedTags(tags);
 }
+
+pub fn sendUrgentTags(self: Self, tags: u32) void {
+    if (self.output_status.getVersion() >= 2) {
+        self.output_status.sendUrgentTags(tags);
+    }
+}
diff --git a/river/Root.zig b/river/Root.zig
index 3af4ec1..bc6dbef 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -379,6 +379,7 @@ fn commitTransaction(self: *Self) void {
         output.current = output.pending;
 
         var view_tags_changed = false;
+        var urgent_tags_dirty = false;
 
         var view_it = output.views.first;
         while (view_it) |view_node| {
@@ -395,12 +396,15 @@ fn commitTransaction(self: *Self) void {
             // Apply pending state of the view
             view.pending_serial = null;
             if (view.pending.tags != view.current.tags) view_tags_changed = true;
+            if (view.pending.urgent != view.current.urgent) urgent_tags_dirty = true;
+            if (view.pending.urgent and view_tags_changed) urgent_tags_dirty = true;
             view.current = view.pending;
 
             view.dropSavedBuffers();
         }
 
         if (view_tags_changed) output.sendViewTags();
+        if (urgent_tags_dirty) output.sendUrgentTags();
 
         output.damage.addWhole();
     }
diff --git a/river/Seat.zig b/river/Seat.zig
index 0a5db7d..5cee706 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -218,6 +218,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
                 std.debug.assert(self.focused_output == target_view.output);
                 if (target_view.pending.focus == 0) target_view.setActivated(true);
                 target_view.pending.focus += 1;
+                target_view.pending.urgent = false;
             },
             .layer => |target_layer| std.debug.assert(self.focused_output == target_layer.output),
             .none => {},
diff --git a/river/Server.zig b/river/Server.zig
index 6c0a5cc..ebe9096 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -59,6 +59,7 @@ xwayland: if (build_options.xwayland) *wlr.Xwayland else void,
 new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void,
 
 foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1,
+xdg_activation: *wlr.XdgActivationV1,
 
 decoration_manager: DecorationManager,
 input_manager: InputManager,
@@ -109,6 +110,7 @@ pub fn init(self: *Self) !void {
     }
 
     self.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(self.wl_server);
+    self.xdg_activation = try wlr.XdgActivationV1.create(self.wl_server);
 
     _ = try wlr.PrimarySelectionDeviceManagerV1.create(self.wl_server);
 
diff --git a/river/StatusManager.zig b/river/StatusManager.zig
index 245cef2..f8ea03e 100644
--- a/river/StatusManager.zig
+++ b/river/StatusManager.zig
@@ -40,7 +40,7 @@ server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleSer
 
 pub fn init(self: *Self) !void {
     self.* = .{
-        .global = try wl.Global.create(server.wl_server, zriver.StatusManagerV1, 1, *Self, self, bind),
+        .global = try wl.Global.create(server.wl_server, zriver.StatusManagerV1, 2, *Self, self, bind),
     };
 
     server.wl_server.addDestroyListener(&self.server_destroy);
diff --git a/river/View.zig b/river/View.zig
index c3148b2..86863d1 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -71,6 +71,7 @@ const State = struct {
 
     float: bool = false,
     fullscreen: bool = false,
+    urgent: bool = false,
 };
 
 const SavedBuffer = struct {
@@ -130,6 +131,9 @@ foreign_fullscreen: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen) =
 foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) =
     wl.Listener(*wlr.ForeignToplevelHandleV1).init(handleForeignClose),
 
+request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) =
+    wl.Listener(*wlr.XdgActivationV1.event.RequestActivate).init(handleRequestActivate),
+
 pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void {
     self.* = .{
         .output = output,
@@ -164,6 +168,8 @@ pub fn destroy(self: *Self) void {
         .xwayland_view => |*xwayland_view| xwayland_view.deinit(),
     }
 
+    self.request_activate.link.remove();
+
     const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
     self.output.views.remove(node);
     util.gpa.destroy(node);
@@ -277,6 +283,11 @@ pub fn sendToOutput(self: *Self, destination_output: *Output) void {
     self.output.sendViewTags();
     destination_output.sendViewTags();
 
+    if (self.pending.urgent) {
+        self.output.sendUrgentTags();
+        destination_output.sendUrgentTags();
+    }
+
     if (self.surface) |surface| {
         surface.sendLeave(self.output.wlr_output);
         surface.sendEnter(destination_output.wlr_output);
@@ -446,6 +457,8 @@ pub fn map(self: *Self) !void {
         handle.outputEnter(self.output.wlr_output);
     }
 
+    server.xdg_activation.events.request_activate.add(&self.request_activate);
+
     // Add the view to the stack of its output
     const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
     self.output.views.attach(node, server.config.attach_mode);
@@ -535,3 +548,16 @@ fn handleForeignClose(
     const self = @fieldParentPtr(Self, "foreign_close", listener);
     self.close();
 }
+
+fn handleRequestActivate(
+    listener: *wl.Listener(*wlr.XdgActivationV1.event.RequestActivate),
+    event: *wlr.XdgActivationV1.event.RequestActivate,
+) void {
+    const self = @fieldParentPtr(Self, "request_activate", listener);
+    if (fromWlrSurface(event.surface)) |view| {
+        if (view.current.focus == 0) {
+            view.pending.urgent = true;
+            server.root.startTransaction();
+        }
+    }
+}
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 454ce77..dfdf188 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -275,8 +275,11 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), surface: *wlr.Surface) voi
                 server.root.notifyConfigured();
             } else {
                 const self_tags_changed = view.pending.tags != view.current.tags;
+                const urgent_tags_dirty = view.pending.urgent != view.current.urgent or
+                    (view.pending.urgent and self_tags_changed);
                 view.current = view.pending;
                 if (self_tags_changed) view.output.sendViewTags();
+                if (urgent_tags_dirty) view.output.sendUrgentTags();
 
                 // This is necessary if this view was part of a transaction that didn't get completed
                 // before some change occured that caused shouldTrackConfigure() to return false.
diff --git a/river/command.zig b/river/command.zig
index d32101e..9db3995 100644
--- a/river/command.zig
+++ b/river/command.zig
@@ -46,6 +46,7 @@ const str_to_impl_fn = [_]struct {
     .{ .name = "background-color",       .impl = @import("command/config.zig").backgroundColor },
     .{ .name = "border-color-focused",   .impl = @import("command/config.zig").borderColorFocused },
     .{ .name = "border-color-unfocused", .impl = @import("command/config.zig").borderColorUnfocused },
+    .{ .name = "border-color-urgent",    .impl = @import("command/config.zig").borderColorUrgent },
     .{ .name = "border-width",           .impl = @import("command/config.zig").borderWidth },
     .{ .name = "close",                  .impl = @import("command/close.zig").close },
     .{ .name = "csd-filter-add",         .impl = @import("command/filter.zig").csdFilterAdd },
diff --git a/river/command/config.zig b/river/command/config.zig
index 9161c7d..f2046ce 100644
--- a/river/command/config.zig
+++ b/river/command/config.zig
@@ -83,6 +83,21 @@ pub fn borderColorUnfocused(
     while (it) |node| : (it = node.next) node.data.damage.addWhole();
 }
 
+pub fn borderColorUrgent(
+    allocator: *std.mem.Allocator,
+    seat: *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;
+
+    server.config.border_color_urgent = try parseRgba(args[1]);
+
+    var it = server.root.outputs.first;
+    while (it) |node| : (it = node.next) node.data.damage.addWhole();
+}
+
 pub fn setCursorWarp(
     allocator: *std.mem.Allocator,
     seat: *Seat,
diff --git a/river/render.zig b/river/render.zig
index 9c0bd52..4192d14 100644
--- a/river/render.zig
+++ b/river/render.zig
@@ -311,7 +311,11 @@ fn renderTexture(
 
 fn renderBorders(output: *const Output, view: *View, now: *os.timespec) void {
     const config = &server.config;
-    const color = if (view.current.focus != 0) &config.border_color_focused else &config.border_color_unfocused;
+    const color = blk: {
+        if (view.current.urgent) break :blk &config.border_color_urgent;
+        if (view.current.focus != 0) break :blk &config.border_color_focused;
+        break :blk &config.border_color_unfocused;
+    };
     const border_width = config.border_width;
     const actual_box = if (view.saved_buffers.items.len != 0) view.saved_surface_box else view.surface_box;