Implement configurable view opacity with fade effect
This commit is contained in:
parent
27b666dbba
commit
b67ea748a3
10 changed files with 228 additions and 5 deletions
|
@ -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
|
||||
|
|
|
@ -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_.
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -250,6 +250,8 @@ fn commitTransaction(self: *Self) void {
|
|||
view.current = view.pending;
|
||||
|
||||
view.dropSavedBuffers();
|
||||
|
||||
view.commitOpacityTransition();
|
||||
}
|
||||
|
||||
if (view_tags_changed) output.sendViewTags();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
79
river/command/opacity.zig
Normal file
79
river/command/opacity.zig
Normal 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();
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue