diff --git a/include/picom/backend.h b/include/picom/backend.h index e087894f..f8a60c98 100644 --- a/include/picom/backend.h +++ b/include/picom/backend.h @@ -183,6 +183,7 @@ enum backend_command_op { /// will later be filled in by the renderer using this symbolic reference. enum backend_command_source { BACKEND_COMMAND_SOURCE_WINDOW, + BACKEND_COMMAND_SOURCE_WINDOW_SAVED, BACKEND_COMMAND_SOURCE_SHADOW, BACKEND_COMMAND_SOURCE_BACKGROUND, }; diff --git a/man/picom.1.adoc b/man/picom.1.adoc index 15dc77c3..2f5b1c96 100644 --- a/man/picom.1.adoc +++ b/man/picom.1.adoc @@ -697,6 +697,10 @@ Currently, these output variables are supported: ::: _crop-x_, _crop-y_, _crop-width_, _crop-height_:: These four values combined defines a rectangle on the screen. The window and its shadow will be cropped to this rectangle. If not defined, the window and shadow will not be cropped. + _saved-image-blend_:: When the window's geometry changes, its content will often change drastically, creating a jarring discontinuity. This output variable allows you to blend the window's content before and after the geometry change, the before and after images will be stretched appropriately to match the animation. This way you can smoothly animated geometry changes. This is a number between 0 and 1. 0 means the saved image is not used, whereas 1 means you will only see the saved image. (EXPERIMENTAL) ++ +WARNING: The _saved-image-blend_ variable is experimental. It might work incorrectly, cause visual artifacts, or slow down your system. You are welcome to open an issue on GitHub if you encounter any problems to help us improve it, though resolution is not guaranteed. + All coordinates are in pixels, and are in the coordinate system of the screen. Sizes are also in pixels. IMPORTANT: If an output variable name is not defined in your animation script, it will take the default value for whichever state the window is in. Specifically, if you don't define an _opacity_ variable in the animation script for the "close" or "hide" trigger, a closed window will, by default, have 0 opacity. So you will just see it disappear instantly. Oftentimes, you will want to set _opacity_ to 1 to make the window visible for the duration of the animation. diff --git a/src/backend/backend.c b/src/backend/backend.c index 5dcc1954..481084fe 100644 --- a/src/backend/backend.c +++ b/src/backend/backend.c @@ -11,6 +11,7 @@ #include "config.h" #include "log.h" #include "region.h" +#include "renderer/layout.h" #include "wm/win.h" #include "x.h" @@ -121,6 +122,7 @@ bool backend_execute(struct backend_base *backend, image_handle target, unsigned static inline const char *render_command_source_name(enum backend_command_source source) { switch (source) { case BACKEND_COMMAND_SOURCE_WINDOW: return "window"; + case BACKEND_COMMAND_SOURCE_WINDOW_SAVED: return "window_saved"; case BACKEND_COMMAND_SOURCE_SHADOW: return "shadow"; case BACKEND_COMMAND_SOURCE_BACKGROUND: return "background"; } diff --git a/src/renderer/command_builder.c b/src/renderer/command_builder.c index a8c791ee..ac65aba5 100644 --- a/src/renderer/command_builder.c +++ b/src/renderer/command_builder.c @@ -15,10 +15,11 @@ /// it's stored in `cmd` going backwards, i.e. cmd - 1, -2, ... /// @return number of commands generated static inline unsigned -commands_for_window_body(struct layer *layer, struct backend_command *cmd, +commands_for_window_body(struct layer *layer, struct backend_command *cmd_base, const region_t *frame_region, bool inactive_dim_fixed, double max_brightness) { auto w = layer->win; + auto cmd = cmd_base; scoped_region_t crop = region_from_box(layer->crop); auto mode = win_calc_mode_raw(layer->win); int border_width = w->g.border_width; @@ -48,18 +49,20 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, if (layer->options.corner_radius > 0) { win_region_remove_corners(w, layer->window.origin, &cmd->opaque_region); } - region_scale(&cmd->target_mask, layer->window.origin, layer->scale); - region_scale(&cmd->opaque_region, layer->window.origin, layer->scale); - pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); - pixman_region32_intersect(&cmd->opaque_region, &cmd->opaque_region, &crop); - cmd->op = BACKEND_COMMAND_BLIT; - cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; - cmd->origin = layer->window.origin; - cmd->blit = (struct backend_blit_args){ + + float opacity = layer->opacity * (1 - layer->saved_image_blend); + if (opacity > (1. - 1. / MAX_ALPHA)) { + // Avoid division by a very small number + opacity = 1; + } + float opacity_saved = 0; + if (opacity < 1) { + opacity_saved = layer->opacity * layer->saved_image_blend / (1 - opacity); + } + struct backend_blit_args args_base = { .border_width = border_width, - .target_mask = &cmd->target_mask, .corner_radius = layer->options.corner_radius, - .opacity = layer->opacity, + .opacity = opacity, .dim = dim, .scale = layer->scale, .effective_size = layer->window.size, @@ -68,11 +71,37 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, .source_mask = NULL, .max_brightness = max_brightness, }; + region_scale(&cmd->target_mask, layer->window.origin, layer->scale); + region_scale(&cmd->opaque_region, layer->window.origin, layer->scale); + pixman_region32_intersect(&cmd->target_mask, &cmd->target_mask, &crop); + pixman_region32_intersect(&cmd->opaque_region, &cmd->opaque_region, &crop); + cmd->op = BACKEND_COMMAND_BLIT; + cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; + cmd->origin = layer->window.origin; + cmd->blit = args_base; + cmd->blit.target_mask = &cmd->target_mask; + cmd -= 1; + if (layer->saved_image_blend > 0) { + pixman_region32_copy(&cmd->target_mask, &cmd[1].target_mask); + cmd->opaque_region = cmd[1].opaque_region; + pixman_region32_init(&cmd[1].opaque_region); + cmd->op = BACKEND_COMMAND_BLIT; + cmd->source = BACKEND_COMMAND_SOURCE_WINDOW_SAVED; + cmd->origin = layer->window.origin; + cmd->blit = args_base; + cmd->blit.effective_size = (ivec2){ + .width = (int)(layer->window.size.width / w->saved_win_image_scale.width), + .height = (int)(layer->window.size.height / w->saved_win_image_scale.height), + }; + cmd->blit.opacity = opacity_saved; + cmd->blit.target_mask = &cmd->target_mask; + cmd->blit.scale = vec2_scale(cmd->blit.scale, w->saved_win_image_scale); + cmd -= 1; + } if (w->frame_opacity == 1 || w->frame_opacity == 0) { - return 1; + return (unsigned)(cmd_base - cmd); } - cmd -= 1; pixman_region32_copy(&cmd->target_mask, frame_region); region_scale(&cmd->target_mask, cmd->origin, layer->scale); @@ -81,10 +110,27 @@ commands_for_window_body(struct layer *layer, struct backend_command *cmd, cmd->op = BACKEND_COMMAND_BLIT; cmd->origin = layer->window.origin; cmd->source = BACKEND_COMMAND_SOURCE_WINDOW; - cmd->blit = cmd[1].blit; + cmd->blit = args_base; cmd->blit.target_mask = &cmd->target_mask; - cmd->blit.opacity *= w->frame_opacity; - return 2; + cmd->blit.opacity = w->frame_opacity * opacity; + cmd -= 1; + if (layer->saved_image_blend > 0) { + pixman_region32_copy(&cmd->target_mask, &cmd[1].target_mask); + pixman_region32_init(&cmd->opaque_region); + cmd->op = BACKEND_COMMAND_BLIT; + cmd->source = BACKEND_COMMAND_SOURCE_WINDOW_SAVED; + cmd->origin = layer->window.origin; + cmd->blit = args_base; + cmd->blit.effective_size = (ivec2){ + .width = (int)(layer->window.size.width / w->saved_win_image_scale.width), + .height = (int)(layer->window.size.height / w->saved_win_image_scale.height), + }; + cmd->blit.opacity = w->frame_opacity * opacity_saved; + cmd->blit.target_mask = &cmd->target_mask; + cmd->blit.scale = vec2_scale(cmd->blit.scale, w->saved_win_image_scale); + cmd -= 1; + } + return (unsigned)(cmd_base - cmd); } /// Generate render command for the shadow in `layer` @@ -115,7 +161,8 @@ command_for_shadow(struct layer *layer, struct backend_command *cmd, // should be blits for the current window. for (auto j = cmd + 1; j != end; j++) { assert(j->op == BACKEND_COMMAND_BLIT); - assert(j->source == BACKEND_COMMAND_SOURCE_WINDOW); + assert(j->source == BACKEND_COMMAND_SOURCE_WINDOW || + j->source == BACKEND_COMMAND_SOURCE_WINDOW_SAVED); if (j->blit.corner_radius == 0) { pixman_region32_subtract( &cmd->target_mask, &cmd->target_mask, &j->target_mask); @@ -375,11 +422,16 @@ void command_builder_build(struct command_builder *cb, struct layout *layout, if (layer->options.shadow) { ncmds += 1; } + + unsigned n_cmds_for_window_body = 1; if (layer->win->frame_opacity < 1 && layer->win->frame_opacity > 0) { // Needs to draw the frame separately - ncmds += 1; + n_cmds_for_window_body += 1; } - ncmds += 1; // window body + if (layer->saved_image_blend > 0) { + n_cmds_for_window_body *= 2; + } + ncmds += n_cmds_for_window_body; // window body } auto list = command_builder_command_list_new(cb, ncmds); diff --git a/src/renderer/damage.c b/src/renderer/damage.c index 48c650d5..5778e717 100644 --- a/src/renderer/damage.c +++ b/src/renderer/damage.c @@ -30,6 +30,10 @@ layer_compare(const struct layer *past_layer, const struct backend_command *past // Shadow moved or size changed return false; } + if (past_layer->saved_image_blend != curr_layer->saved_image_blend) { + // The amount of blending with the saved image changed + return false; + } if (past_layer->number_of_commands != curr_layer->number_of_commands) { // Number of render commands changed. We are being conservative // here, because even though the number of commands changed, we can still diff --git a/src/renderer/layout.c b/src/renderer/layout.c index f7f84011..6c828bba 100644 --- a/src/renderer/layout.c +++ b/src/renderer/layout.c @@ -101,6 +101,12 @@ static bool layer_from_window(struct layer *out_layer, struct win *w, ivec2 size goto out; } + out_layer->saved_image_blend = + (float)win_animatable_get(w, WIN_SCRIPT_SAVED_IMAGE_BLEND); + if (w->saved_win_image == NULL) { + out_layer->saved_image_blend = 0; + } + pixman_region32_copy(&out_layer->damaged, &w->damaged); pixman_region32_translate(&out_layer->damaged, out_layer->window.origin.x, out_layer->window.origin.y); diff --git a/src/renderer/layout.h b/src/renderer/layout.h index f370de97..66b64790 100644 --- a/src/renderer/layout.h +++ b/src/renderer/layout.h @@ -38,6 +38,8 @@ struct layer { float blur_opacity; /// Opacity of this window's shadow float shadow_opacity; + /// How much the image of this window should be blended with the saved image + float saved_image_blend; /// Crop the content of this layer to this box, in screen coordinates. struct ibox crop; diff --git a/src/renderer/renderer.c b/src/renderer/renderer.c index aa86b5bc..d228bcd1 100644 --- a/src/renderer/renderer.c +++ b/src/renderer/renderer.c @@ -416,6 +416,9 @@ static bool renderer_prepare_commands(struct renderer *r, struct backend_base *b } else if (cmd->source == BACKEND_COMMAND_SOURCE_WINDOW) { assert(w->win_image); cmd->blit.source_image = w->win_image; + } else if (cmd->source == BACKEND_COMMAND_SOURCE_WINDOW_SAVED) { + assert(w->saved_win_image); + cmd->blit.source_image = w->saved_win_image; } if (cmd->blit.source_mask != NULL) { if (w->mask_image == NULL && diff --git a/src/wm/defs.h b/src/wm/defs.h index 7c2b8421..9ab4305f 100644 --- a/src/wm/defs.h +++ b/src/wm/defs.h @@ -101,5 +101,8 @@ enum win_script_output { WIN_SCRIPT_CROP_WIDTH, /// Height of the crop box WIN_SCRIPT_CROP_HEIGHT, + /// How much to blend in the saved window image + WIN_SCRIPT_SAVED_IMAGE_BLEND, + + NUM_OF_WIN_SCRIPT_OUTPUTS, }; -#define NUM_OF_WIN_SCRIPT_OUTPUTS (WIN_SCRIPT_CROP_HEIGHT + 1) diff --git a/src/wm/win.c b/src/wm/win.c index 58b5cfdb..8768d7ad 100644 --- a/src/wm/win.c +++ b/src/wm/win.c @@ -1767,6 +1767,8 @@ double win_animatable_get(const struct win *w, enum win_script_output output) { case WIN_SCRIPT_SHADOW_SCALE_Y: return 1; case WIN_SCRIPT_CROP_WIDTH: case WIN_SCRIPT_CROP_HEIGHT: return INFINITY; + case WIN_SCRIPT_SAVED_IMAGE_BLEND: return 0; + default: unreachable(); } unreachable(); } @@ -1948,13 +1950,17 @@ bool win_process_animation_and_state_change(struct session *ps, struct win *w, d // old has left off. Note we still need to advance the old animation for // the last interval. win_advance_animation(w, delta_t, &win_ctx); + auto memory = w->running_animation_instance->memory; + auto output_indices = w->running_animation.output_indices; + if (output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND] >= 0) { + memory[output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND]] = + 1 - memory[output_indices[WIN_SCRIPT_SAVED_IMAGE_BLEND]]; + } if (geometry_changed) { // If the window has moved, we need to adjust scripts // outputs so that the window will stay in the same position and // size after applying the animation. This way the window's size // and position won't change discontinuously. - auto memory = w->running_animation_instance->memory; - auto output_indices = w->running_animation.output_indices; struct { int output; double delta; diff --git a/src/wm/win.h b/src/wm/win.h index 03aef76a..f852cb5f 100644 --- a/src/wm/win.h +++ b/src/wm/win.h @@ -313,6 +313,7 @@ static const struct script_output_info win_script_outputs[] = { [WIN_SCRIPT_CROP_Y] = {"crop-y"}, [WIN_SCRIPT_CROP_WIDTH] = {"crop-width"}, [WIN_SCRIPT_CROP_HEIGHT] = {"crop-height"}, + [WIN_SCRIPT_SAVED_IMAGE_BLEND] = {"saved-image-blend"}, [NUM_OF_WIN_SCRIPT_OUTPUTS] = {NULL}, };