From b67ea748a386d1050e2019d20dfe69e99bf7bad6 Mon Sep 17 00:00:00 2001 From: Leon Henrik Plickat Date: Sat, 3 Oct 2020 22:09:15 +0200 Subject: [PATCH] Implement configurable view opacity with fade effect --- contrib/config.sh | 2 + doc/riverctl.1.scd | 15 ++++++ river/Config.zig | 15 ++++++ river/Root.zig | 2 + river/Seat.zig | 6 ++- river/View.zig | 96 ++++++++++++++++++++++++++++++++++++++- river/XdgToplevel.zig | 4 +- river/command.zig | 3 ++ river/command/opacity.zig | 79 ++++++++++++++++++++++++++++++++ river/render.zig | 11 ++++- 10 files changed, 228 insertions(+), 5 deletions(-) create mode 100644 river/command/opacity.zig diff --git a/contrib/config.sh b/contrib/config.sh index 620b29f..3b51bbe 100755 --- a/contrib/config.sh +++ b/contrib/config.sh @@ -100,3 +100,5 @@ riverctl float-filter-add "popup" # Set app-ids of views which should use client side decorations riverctl csd-filter-add "gedit" +# Set opacity and fade effect +# riverctl opacity 1.0 0.75 0.0 0.1 20 diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index bbbbafa..143d57e 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -162,6 +162,21 @@ that tag 1 through 9 are visible. - move-view - resize-view +*opacity* _focused-opacity_ _unfocused-opacity_ _starting-opacity_ _opacity-step_ _opacity-delta-t_ + Set the server side opacity of views. + + _focused-opacity_ sets the opacity of the focused window, _unfocused-opacity_ + the opacity of every unfocused window while _starting-opacity_ sets the + opacity a window will have at startup before immediately transitioning to + either the focused or unfocused opacity. These settings require a floating + point number from 0.0 (fully transparent) to 1.0 (fully opaque). + + Opacity transitions can be animated. _opacity-step_ sets the amount the + opacity should be increased or decreased per step of the transition. It + requires a floating point number from 0.05 to 1.0. If set to 1.0, animations + are disabled. _opacity-delta-t_ sets the time between the transition steps + in milliseconds. + *outer-padding* _pixels_ Set the padding around the edge of the screen to _pixels_. diff --git a/river/Config.zig b/river/Config.zig index bb9b7a6..04b1893 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -66,6 +66,21 @@ csd_filter: std.ArrayList([]const u8), /// The selected focus_follows_cursor mode focus_follows_cursor: FocusFollowsCursorMode = .disabled, +/// The opacity of the focused view +view_opacity_focused: f32 = 1.0, + +/// The opacity of unfocused views +view_opacity_unfocused: f32 = 1.0, + +/// The starting opacity of new views +view_opacity_initial: f32 = 1.0, + +/// View opacity transition step +view_opacity_delta: f32 = 1.0, + +/// Time between view opacity transition steps in msec +view_opacity_delta_t: u31 = 20, + pub fn init() !Self { var self = Self{ .mode_to_id = std.StringHashMap(usize).init(util.gpa), diff --git a/river/Root.zig b/river/Root.zig index 5705ea3..d07884e 100644 --- a/river/Root.zig +++ b/river/Root.zig @@ -250,6 +250,8 @@ fn commitTransaction(self: *Self) void { view.current = view.pending; view.dropSavedBuffers(); + + view.commitOpacityTransition(); } if (view_tags_changed) output.sendViewTags(); diff --git a/river/Seat.zig b/river/Seat.zig index 77bfed8..bb6e557 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -188,6 +188,9 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { // activated state. if (build_options.xwayland and self.focused.view.impl == .xwayland_view) c.wlr_xwayland_surface_activate(self.focused.view.impl.xwayland_view.wlr_xwayland_surface, false); + if (self.focused.view.pending.focus == 0) { + self.focused.view.pending.target_opacity = self.input_manager.server.config.view_opacity_unfocused; + } } c.wlr_seat_keyboard_clear_focus(self.wlr_seat); @@ -200,6 +203,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { // activated state. if (build_options.xwayland and target_view.impl == .xwayland_view) c.wlr_xwayland_surface_activate(target_view.impl.xwayland_view.wlr_xwayland_surface, true); + target_view.pending.target_opacity = self.input_manager.server.config.view_opacity_focused; }, .layer => |target_layer| std.debug.assert(self.focused_output == target_layer.output), .none => {}, @@ -281,7 +285,7 @@ pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32, releas if (out) |s| { const stdout = std.io.getStdOut().outStream(); stdout.print("{}", .{s}) catch - |err| log.err(.command, "{}: write to stdout failed {}", .{ args[0], err }); + |err| log.err(.command, "{}: write to stdout failed {}", .{ args[0], err }); } return true; } diff --git a/river/View.zig b/river/View.zig index 3bcc684..df23d12 100644 --- a/river/View.zig +++ b/river/View.zig @@ -1,6 +1,7 @@ // This file is part of river, a dynamic tiling wayland compositor. // // Copyright 2020 Isaac Freund +// Copyright 2020 Leon Henrik Plickat // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by @@ -65,6 +66,9 @@ const State = struct { float: bool = false, fullscreen: bool = false, + + /// Opacity the view is transitioning to + target_opacity: f32, }; const SavedBuffer = struct { @@ -109,14 +113,27 @@ saved_buffers: std.ArrayList(SavedBuffer), /// view returns to floating mode. float_box: Box = undefined, +/// The current opacity of this view +opacity: f32, + +/// Opacity change timer event source +opacity_timer: ?*c.wl_event_source = null, + draw_borders: bool = true, pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void { self.* = .{ .output = output, - .current = .{ .tags = tags }, - .pending = .{ .tags = tags }, + .current = .{ + .tags = tags, + .target_opacity = output.root.server.config.view_opacity_initial, + }, + .pending = .{ + .tags = tags, + .target_opacity = output.root.server.config.view_opacity_initial, + }, .saved_buffers = std.ArrayList(SavedBuffer).init(util.gpa), + .opacity = output.root.server.config.view_opacity_initial, }; if (@TypeOf(surface) == *c.wlr_xdg_surface) { @@ -337,6 +354,8 @@ pub fn shouldTrackConfigure(self: Self) bool { pub fn map(self: *Self) void { const root = self.output.root; + self.pending.target_opacity = self.output.root.server.config.view_opacity_unfocused; + log.debug(.server, "view '{}' mapped", .{self.getTitle()}); // Add the view to the stack of its output @@ -365,6 +384,10 @@ pub fn unmap(self: *Self) void { self.destroying = true; + if (self.opacity_timer != null) { + self.killOpacityTimer(); + } + // Inform all seats that the view has been unmapped so they can handle focus var it = root.server.input_manager.seats.first; while (it) |node| : (it = node.next) { @@ -379,3 +402,72 @@ pub fn unmap(self: *Self) void { root.startTransaction(); } + +/// Change the opacity of a view by config.view_opacity_delta. +/// If the target opacity was reached, return true. +fn incrementOpacity(self: *Self) bool { + // TODO damage view when implementing damage based rendering + const config = &self.output.root.server.config; + if (self.opacity < self.current.target_opacity) { + self.opacity += config.view_opacity_delta; + if (self.opacity < self.current.target_opacity) return false; + } else { + self.opacity -= config.view_opacity_delta; + if (self.opacity > self.current.target_opacity) return false; + } + self.opacity = self.current.target_opacity; + return true; +} + +/// Destroy a views opacity timer +fn killOpacityTimer(self: *Self) void { + if (c.wl_event_source_remove(self.opacity_timer) < 0) unreachable; + self.opacity_timer = null; +} + +/// Set the timeout on a views opacity timer +fn armOpacityTimer(self: *Self) void { + const delta_t = self.output.root.server.config.view_opacity_delta_t; + if (c.wl_event_source_timer_update(self.opacity_timer, delta_t) < 0) { + log.err(.view, "failed to update opacity timer", .{}); + self.killOpacityTimer(); + } +} + +/// Called by the opacity timer +fn handleOpacityTimer(data: ?*c_void) callconv(.C) c_int { + const self = util.voidCast(Self, data.?); + if (self.incrementOpacity()) { + self.killOpacityTimer(); + } else { + self.armOpacityTimer(); + } + return 0; +} + +/// Create an opacity timer for a view and arm it +fn attachOpacityTimer(self: *Self) void { + const server = self.output.root.server; + self.opacity_timer = c.wl_event_loop_add_timer( + c.wl_display_get_event_loop(server.wl_display), + handleOpacityTimer, + self, + ) orelse { + log.err(.view, "failed to create opacity timer for view '{}'", .{self.getTitle()}); + return; + }; + self.armOpacityTimer(); +} + +/// Commit an opacity transition +pub fn commitOpacityTransition(self: *Self) void { + if (self.opacity == self.current.target_opacity) return; + + // A running timer can handle a target_opacity change + if (self.opacity_timer != null) return; + + // Do the first step now, if that step was not enough, attach timer + if (!self.incrementOpacity()) { + self.attachOpacityTimer(); + } +} diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index fcd74f4..1693271 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -251,8 +251,10 @@ fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { view.pending_serial = null; if (view.shouldTrackConfigure()) view.output.root.notifyConfigured() - else + else { view.current = view.pending; + view.commitOpacityTransition(); + } } else { // If the client has not yet acked our configure, we need to send a // frame done event so that it commits another buffer. These diff --git a/river/command.zig b/river/command.zig index bfe0727..c9935d5 100644 --- a/river/command.zig +++ b/river/command.zig @@ -49,6 +49,7 @@ const str_to_impl_fn = [_]struct { .{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer }, .{ .name = "mod-master-count", .impl = @import("command/mod_master_count.zig").modMasterCount }, .{ .name = "mod-master-factor", .impl = @import("command/mod_master_factor.zig").modMasterFactor }, + .{ .name = "opacity", .impl = @import("command/opacity.zig").opacity }, .{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding }, .{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput }, .{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags }, @@ -73,6 +74,7 @@ pub const Error = error{ InvalidCharacter, InvalidDirection, InvalidRgba, + InvalidValue, UnknownOption, OutOfMemory, Other, @@ -113,6 +115,7 @@ pub fn errToMsg(err: Error) [:0]const u8 { Error.InvalidCharacter => "invalid character in argument", Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", Error.InvalidRgba => "invalid color format, must be #RRGGBB or #RRGGBBAA", + Error.InvalidValue => "invalid value", Error.OutOfMemory => "out of memory", Error.Other => unreachable, }; diff --git a/river/command/opacity.zig b/river/command/opacity.zig new file mode 100644 index 0000000..7cf8e86 --- /dev/null +++ b/river/command/opacity.zig @@ -0,0 +1,79 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Leon Henrik Plickat +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const std = @import("std"); + +const Error = @import("../command.zig").Error; +const Seat = @import("../Seat.zig"); +const View = @import("../View.zig"); +const ViewStack = @import("../view_stack.zig").ViewStack; + +fn opacityUpdateFilter(view: *View, context: void) bool { + // We want to update all views + return true; +} + +pub fn opacity( + allocator: *std.mem.Allocator, + seat: *Seat, + args: []const []const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 6) return Error.NotEnoughArguments; + if (args.len > 6) return Error.TooManyArguments; + + const server = seat.input_manager.server; + + // Focused opacity + server.config.view_opacity_focused = try std.fmt.parseFloat(f32, args[1]); + if (server.config.view_opacity_focused < 0.0 or server.config.view_opacity_focused > 1.0) + return Error.InvalidValue; + + // Unfocused opacity + server.config.view_opacity_unfocused = try std.fmt.parseFloat(f32, args[2]); + if (server.config.view_opacity_unfocused < 0.0 or server.config.view_opacity_unfocused > 1.0) + return Error.InvalidValue; + + // Starting opacity for new views + server.config.view_opacity_initial = try std.fmt.parseFloat(f32, args[3]); + if (server.config.view_opacity_initial < 0.0 or server.config.view_opacity_initial > 1.0) + return Error.InvalidValue; + + // Opacity transition step + server.config.view_opacity_delta = try std.fmt.parseFloat(f32, args[4]); + if (server.config.view_opacity_delta < 0.0 or server.config.view_opacity_delta > 1.0) + return Error.InvalidValue; + + // Time between step + server.config.view_opacity_delta_t = try std.fmt.parseInt(u31, args[5], 10); + if (server.config.view_opacity_delta_t < 1) return Error.InvalidValue; + + // Update opacity of all views + // Unmapped views will be skipped, however their opacity gets updated on map anyway + var oit = server.root.outputs.first; + while (oit) |onode| : (oit = onode.next) { + var vit = ViewStack(View).iter(onode.data.views.first, .forward, {}, opacityUpdateFilter); + while (vit.next()) |vnode| { + if (vnode.current.focus > 0) { + vnode.pending.target_opacity = server.config.view_opacity_focused; + } else { + vnode.pending.target_opacity = server.config.view_opacity_unfocused; + } + } + } + server.root.startTransaction(); +} diff --git a/river/render.zig b/river/render.zig index 65a4ab6..857478b 100644 --- a/river/render.zig +++ b/river/render.zig @@ -36,6 +36,8 @@ const SurfaceRenderData = struct { output_y: i32, when: *c.timespec, + + opacity: f32, }; pub fn renderOutput(output: *Output) void { @@ -138,6 +140,7 @@ fn renderLayer(output: Output, layer: std.TailQueue(LayerSurface), now: *c.times .output_x = layer_surface.box.x, .output_y = layer_surface.box.y, .when = now, + .opacity = 1.0, }; c.wlr_layer_surface_v1_for_each_surface( layer_surface.wlr_layer_surface, @@ -162,6 +165,7 @@ fn renderView(output: Output, view: *View, now: *c.timespec) void { .height = @intCast(c_int, saved_buffer.box.height), }, saved_buffer.transform, + view.opacity, ); } else { // Since there is no stashed buffer, we are not in the middle of @@ -171,6 +175,7 @@ fn renderView(output: Output, view: *View, now: *c.timespec) void { .output_x = view.current.box.x - view.surface_box.x, .output_y = view.current.box.y - view.surface_box.y, .when = now, + .opacity = view.opacity, }; view.forEachSurface(renderSurfaceIterator, &rdata); @@ -191,6 +196,7 @@ fn renderDragIcons(output: Output, now: *c.timespec) void { .output_y = @floatToInt(i32, drag_icon.seat.cursor.wlr_cursor.y) + drag_icon.wlr_drag_icon.surface.*.sy - output_box.*.y, .when = now, + .opacity = 1.0, }; c.wlr_surface_for_each_surface(drag_icon.wlr_drag_icon.surface, renderSurfaceIterator, &rdata); } @@ -209,6 +215,7 @@ fn renderXwaylandUnmanaged(output: Output, now: *c.timespec) void { .output_x = wlr_xwayland_surface.x - output_box.*.x, .output_y = wlr_xwayland_surface.y - output_box.*.y, .when = now, + .opacity = 1.0, }; c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurfaceIterator, &rdata); } @@ -233,6 +240,7 @@ fn renderSurfaceIterator( .height = surface.?.current.height, }, surface.?.current.transform, + rdata.opacity, ); c.wlr_surface_send_frame_done(surface, rdata.when); @@ -245,6 +253,7 @@ fn renderTexture( wlr_texture: ?*c.wlr_texture, wlr_box: c.wlr_box, transform: c.wl_output_transform, + opacity: f32, ) void { const texture = wlr_texture orelse return; var box = wlr_box; @@ -262,7 +271,7 @@ fn renderTexture( // This takes our matrix, the texture, and an alpha, and performs the actual // rendering on the GPU. - _ = c.wlr_render_texture_with_matrix(output.getRenderer(), texture, &matrix, 1.0); + _ = c.wlr_render_texture_with_matrix(output.getRenderer(), texture, &matrix, opacity); } fn renderBorders(output: Output, view: *View, now: *c.timespec) void {