Implement configurable view opacity with fade effect

This commit is contained in:
Leon Henrik Plickat 2020-10-03 22:09:15 +02:00 committed by Isaac Freund
parent 27b666dbba
commit b67ea748a3
10 changed files with 228 additions and 5 deletions

View File

@ -100,3 +100,5 @@ riverctl float-filter-add "popup"
# Set app-ids of views which should use client side decorations # Set app-ids of views which should use client side decorations
riverctl csd-filter-add "gedit" riverctl csd-filter-add "gedit"
# Set opacity and fade effect
# riverctl opacity 1.0 0.75 0.0 0.1 20

View File

@ -162,6 +162,21 @@ that tag 1 through 9 are visible.
- move-view - move-view
- resize-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_ *outer-padding* _pixels_
Set the padding around the edge of the screen to _pixels_. Set the padding around the edge of the screen to _pixels_.

View File

@ -66,6 +66,21 @@ csd_filter: std.ArrayList([]const u8),
/// The selected focus_follows_cursor mode /// The selected focus_follows_cursor mode
focus_follows_cursor: FocusFollowsCursorMode = .disabled, 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 { pub fn init() !Self {
var self = Self{ var self = Self{
.mode_to_id = std.StringHashMap(usize).init(util.gpa), .mode_to_id = std.StringHashMap(usize).init(util.gpa),

View File

@ -250,6 +250,8 @@ fn commitTransaction(self: *Self) void {
view.current = view.pending; view.current = view.pending;
view.dropSavedBuffers(); view.dropSavedBuffers();
view.commitOpacityTransition();
} }
if (view_tags_changed) output.sendViewTags(); if (view_tags_changed) output.sendViewTags();

View File

@ -188,6 +188,9 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
// activated state. // activated state.
if (build_options.xwayland and self.focused.view.impl == .xwayland_view) 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); 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); c.wlr_seat_keyboard_clear_focus(self.wlr_seat);
@ -200,6 +203,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void {
// activated state. // activated state.
if (build_options.xwayland and target_view.impl == .xwayland_view) if (build_options.xwayland and target_view.impl == .xwayland_view)
c.wlr_xwayland_surface_activate(target_view.impl.xwayland_view.wlr_xwayland_surface, true); 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), .layer => |target_layer| std.debug.assert(self.focused_output == target_layer.output),
.none => {}, .none => {},
@ -281,7 +285,7 @@ pub fn handleMapping(self: *Self, keysym: c.xkb_keysym_t, modifiers: u32, releas
if (out) |s| { if (out) |s| {
const stdout = std.io.getStdOut().outStream(); const stdout = std.io.getStdOut().outStream();
stdout.print("{}", .{s}) catch 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; return true;
} }

View File

@ -1,6 +1,7 @@
// This file is part of river, a dynamic tiling wayland compositor. // This file is part of river, a dynamic tiling wayland compositor.
// //
// Copyright 2020 Isaac Freund // Copyright 2020 Isaac Freund
// Copyright 2020 Leon Henrik Plickat
// //
// This program is free software: you can redistribute it and/or modify // 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 // it under the terms of the GNU General Public License as published by
@ -65,6 +66,9 @@ const State = struct {
float: bool = false, float: bool = false,
fullscreen: bool = false, fullscreen: bool = false,
/// Opacity the view is transitioning to
target_opacity: f32,
}; };
const SavedBuffer = struct { const SavedBuffer = struct {
@ -109,14 +113,27 @@ saved_buffers: std.ArrayList(SavedBuffer),
/// view returns to floating mode. /// view returns to floating mode.
float_box: Box = undefined, 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, draw_borders: bool = true,
pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void { pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void {
self.* = .{ self.* = .{
.output = output, .output = output,
.current = .{ .tags = tags }, .current = .{
.pending = .{ .tags = tags }, .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), .saved_buffers = std.ArrayList(SavedBuffer).init(util.gpa),
.opacity = output.root.server.config.view_opacity_initial,
}; };
if (@TypeOf(surface) == *c.wlr_xdg_surface) { if (@TypeOf(surface) == *c.wlr_xdg_surface) {
@ -337,6 +354,8 @@ pub fn shouldTrackConfigure(self: Self) bool {
pub fn map(self: *Self) void { pub fn map(self: *Self) void {
const root = self.output.root; const root = self.output.root;
self.pending.target_opacity = self.output.root.server.config.view_opacity_unfocused;
log.debug(.server, "view '{}' mapped", .{self.getTitle()}); log.debug(.server, "view '{}' mapped", .{self.getTitle()});
// Add the view to the stack of its output // Add the view to the stack of its output
@ -365,6 +384,10 @@ pub fn unmap(self: *Self) void {
self.destroying = true; self.destroying = true;
if (self.opacity_timer != null) {
self.killOpacityTimer();
}
// Inform all seats that the view has been unmapped so they can handle focus // Inform all seats that the view has been unmapped so they can handle focus
var it = root.server.input_manager.seats.first; var it = root.server.input_manager.seats.first;
while (it) |node| : (it = node.next) { while (it) |node| : (it = node.next) {
@ -379,3 +402,72 @@ pub fn unmap(self: *Self) void {
root.startTransaction(); 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();
}
}

View File

@ -251,8 +251,10 @@ fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
view.pending_serial = null; view.pending_serial = null;
if (view.shouldTrackConfigure()) if (view.shouldTrackConfigure())
view.output.root.notifyConfigured() view.output.root.notifyConfigured()
else else {
view.current = view.pending; view.current = view.pending;
view.commitOpacityTransition();
}
} else { } else {
// If the client has not yet acked our configure, we need to send a // If the client has not yet acked our configure, we need to send a
// frame done event so that it commits another buffer. These // frame done event so that it commits another buffer. These

View File

@ -49,6 +49,7 @@ const str_to_impl_fn = [_]struct {
.{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer }, .{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer },
.{ .name = "mod-master-count", .impl = @import("command/mod_master_count.zig").modMasterCount }, .{ .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 = "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 = "outer-padding", .impl = @import("command/config.zig").outerPadding },
.{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput }, .{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput },
.{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags }, .{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags },
@ -73,6 +74,7 @@ pub const Error = error{
InvalidCharacter, InvalidCharacter,
InvalidDirection, InvalidDirection,
InvalidRgba, InvalidRgba,
InvalidValue,
UnknownOption, UnknownOption,
OutOfMemory, OutOfMemory,
Other, Other,
@ -113,6 +115,7 @@ pub fn errToMsg(err: Error) [:0]const u8 {
Error.InvalidCharacter => "invalid character in argument", Error.InvalidCharacter => "invalid character in argument",
Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'", Error.InvalidDirection => "invalid direction. Must be 'next' or 'previous'",
Error.InvalidRgba => "invalid color format, must be #RRGGBB or #RRGGBBAA", Error.InvalidRgba => "invalid color format, must be #RRGGBB or #RRGGBBAA",
Error.InvalidValue => "invalid value",
Error.OutOfMemory => "out of memory", Error.OutOfMemory => "out of memory",
Error.Other => unreachable, Error.Other => unreachable,
}; };

79
river/command/opacity.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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();
}

View File

@ -36,6 +36,8 @@ const SurfaceRenderData = struct {
output_y: i32, output_y: i32,
when: *c.timespec, when: *c.timespec,
opacity: f32,
}; };
pub fn renderOutput(output: *Output) void { 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_x = layer_surface.box.x,
.output_y = layer_surface.box.y, .output_y = layer_surface.box.y,
.when = now, .when = now,
.opacity = 1.0,
}; };
c.wlr_layer_surface_v1_for_each_surface( c.wlr_layer_surface_v1_for_each_surface(
layer_surface.wlr_layer_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), .height = @intCast(c_int, saved_buffer.box.height),
}, },
saved_buffer.transform, saved_buffer.transform,
view.opacity,
); );
} else { } else {
// Since there is no stashed buffer, we are not in the middle of // 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_x = view.current.box.x - view.surface_box.x,
.output_y = view.current.box.y - view.surface_box.y, .output_y = view.current.box.y - view.surface_box.y,
.when = now, .when = now,
.opacity = view.opacity,
}; };
view.forEachSurface(renderSurfaceIterator, &rdata); 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) + .output_y = @floatToInt(i32, drag_icon.seat.cursor.wlr_cursor.y) +
drag_icon.wlr_drag_icon.surface.*.sy - output_box.*.y, drag_icon.wlr_drag_icon.surface.*.sy - output_box.*.y,
.when = now, .when = now,
.opacity = 1.0,
}; };
c.wlr_surface_for_each_surface(drag_icon.wlr_drag_icon.surface, renderSurfaceIterator, &rdata); 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_x = wlr_xwayland_surface.x - output_box.*.x,
.output_y = wlr_xwayland_surface.y - output_box.*.y, .output_y = wlr_xwayland_surface.y - output_box.*.y,
.when = now, .when = now,
.opacity = 1.0,
}; };
c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurfaceIterator, &rdata); c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurfaceIterator, &rdata);
} }
@ -233,6 +240,7 @@ fn renderSurfaceIterator(
.height = surface.?.current.height, .height = surface.?.current.height,
}, },
surface.?.current.transform, surface.?.current.transform,
rdata.opacity,
); );
c.wlr_surface_send_frame_done(surface, rdata.when); c.wlr_surface_send_frame_done(surface, rdata.when);
@ -245,6 +253,7 @@ fn renderTexture(
wlr_texture: ?*c.wlr_texture, wlr_texture: ?*c.wlr_texture,
wlr_box: c.wlr_box, wlr_box: c.wlr_box,
transform: c.wl_output_transform, transform: c.wl_output_transform,
opacity: f32,
) void { ) void {
const texture = wlr_texture orelse return; const texture = wlr_texture orelse return;
var box = wlr_box; var box = wlr_box;
@ -262,7 +271,7 @@ fn renderTexture(
// This takes our matrix, the texture, and an alpha, and performs the actual // This takes our matrix, the texture, and an alpha, and performs the actual
// rendering on the GPU. // 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 { fn renderBorders(output: Output, view: *View, now: *c.timespec) void {