diff --git a/src/backend/backend.c b/src/backend/backend.c index 80b78a165c..34e83e0798 100644 --- a/src/backend/backend.c +++ b/src/backend/backend.c @@ -10,6 +10,7 @@ #include "config.h" #include "log.h" #include "region.h" +#include "transition.h" #include "types.h" #include "win.h" #include "x.h" @@ -253,45 +254,20 @@ bool paint_all_new(session_t *ps, struct managed_win *const t) { auto real_win_mode = w->mode; coord_t window_coord = {.x = w->g.x, .y = w->g.y}; + const double window_opacity = animatable_get(&w->opacity); if (w->blur_background && (ps->o.force_win_blend || real_win_mode == WMODE_TRANS || (ps->o.blur_background_frame && real_win_mode == WMODE_FRAME_TRANS))) { // Minimize the region we try to blur, if the window // itself is not opaque, only the frame is. - double blur_opacity = 1; - if (w->opacity < (1.0 / MAX_ALPHA)) { - // Hide blur for fully transparent windows. - blur_opacity = 0; - } else if (w->state == WSTATE_MAPPING) { - // Gradually increase the blur intensity during - // fading in. - assert(w->opacity <= w->opacity_target); - blur_opacity = w->opacity / w->opacity_target; - } else if (w->state == WSTATE_UNMAPPING || - w->state == WSTATE_DESTROYING) { - // Gradually decrease the blur intensity during - // fading out. - assert(w->opacity <= w->opacity_target_old); - blur_opacity = w->opacity / w->opacity_target_old; - } else if (w->state == WSTATE_FADING) { - if (w->opacity < w->opacity_target && - w->opacity_target_old < (1.0 / MAX_ALPHA)) { - // Gradually increase the blur intensity during - // fading in. - assert(w->opacity <= w->opacity_target); - blur_opacity = w->opacity / w->opacity_target; - } else if (w->opacity > w->opacity_target && - w->opacity_target < (1.0 / MAX_ALPHA)) { - // Gradually decrease the blur intensity during - // fading out. - assert(w->opacity <= w->opacity_target_old); - blur_opacity = w->opacity / w->opacity_target_old; - } - } + const double blur_opacity = animatable_get(&w->blur_opacity); assert(blur_opacity >= 0 && blur_opacity <= 1); - if (real_win_mode == WMODE_TRANS || ps->o.force_win_blend) { + if (blur_opacity * MAX_ALPHA < 1) { + // We don't need to blur if it would be completely + // transparent + } else if (real_win_mode == WMODE_TRANS || ps->o.force_win_blend) { // We need to blur the bounding shape of the window // (reg_paint_in_bound = reg_bound \cap reg_paint) ps->backend_data->ops->blur( @@ -404,7 +380,7 @@ bool paint_all_new(session_t *ps, struct managed_win *const t) { if (w->dim) { dim_opacity = ps->o.inactive_dim; if (!ps->o.inactive_dim_fixed) { - dim_opacity *= w->opacity; + dim_opacity *= window_opacity; } } @@ -418,7 +394,8 @@ bool paint_all_new(session_t *ps, struct managed_win *const t) { ps->backend_data, IMAGE_PROPERTY_DIM_LEVEL, w->win_image, &dim_opacity); ps->backend_data->ops->set_image_property( - ps->backend_data, IMAGE_PROPERTY_OPACITY, w->win_image, &w->opacity); + ps->backend_data, IMAGE_PROPERTY_OPACITY, w->win_image, + &window_opacity); ps->backend_data->ops->set_image_property( ps->backend_data, IMAGE_PROPERTY_CORNER_RADIUS, w->win_image, (double[]){w->corner_radius}); @@ -440,7 +417,7 @@ bool paint_all_new(session_t *ps, struct managed_win *const t) { w->fg_shader ? (void *)w->fg_shader->backend_shader : NULL); } - if (w->opacity * MAX_ALPHA < 1) { + if (window_opacity * MAX_ALPHA < 1) { // We don't need to paint the window body itself if it's // completely transparent. goto skip; diff --git a/src/dbus.c b/src/dbus.c index ca379fecb1..f9553723f4 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -24,10 +24,12 @@ #include "list.h" #include "log.h" #include "string_utils.h" +#include "transition.h" #include "types.h" #include "uthash_extra.h" #include "utils.h" #include "win.h" +#include "win_defs.h" #include "dbus.h" @@ -876,7 +878,7 @@ cdbus_process_window_property_get(session_t *ps, DBusMessage *msg, cdbus_window_ if (!strcmp("Mapped", target)) { cdbus_reply(ps, msg, cdbus_append_bool_variant, - (bool[]){win_is_mapped_in_x(w)}); + (bool[]){w->state == WSTATE_MAPPED}); return true; } @@ -991,14 +993,17 @@ static bool cdbus_process_win_get(session_t *ps, DBusMessage *msg) { cdbus_m_win_get_do(class_general, cdbus_reply_string); cdbus_m_win_get_do(role, cdbus_reply_string); - cdbus_m_win_get_do(opacity, cdbus_reply_double); - cdbus_m_win_get_do(opacity_target, cdbus_reply_double); + cdbus_m_win_get_do(opacity.target, cdbus_reply_double); cdbus_m_win_get_do(has_opacity_prop, cdbus_reply_bool); cdbus_m_win_get_do(opacity_prop, cdbus_reply_uint32); cdbus_m_win_get_do(opacity_is_set, cdbus_reply_bool); cdbus_m_win_get_do(opacity_set, cdbus_reply_double); cdbus_m_win_get_do(frame_opacity, cdbus_reply_double); + if (strcmp(target, "opacity") == 0) { + cdbus_reply_double(ps, msg, animatable_get(&w->opacity)); + return true; + } if (!strcmp("left_width", target)) { cdbus_reply_int32(ps, msg, w->frame_extents.left); return true; diff --git a/src/event.c b/src/event.c index 1ab1961c78..f76d693c7f 100644 --- a/src/event.c +++ b/src/event.c @@ -21,6 +21,7 @@ #include "region.h" #include "utils.h" #include "win.h" +#include "win_defs.h" #include "x.h" /// Event handling with X is complicated. Handling events with other events possibly @@ -273,18 +274,27 @@ static inline void ev_destroy_notify(session_t *ps, xcb_destroy_notify_event_t * assert(&mw->base == w); mw = NULL; } + + // A window can't be a client window and a top-level window at the same time, + // so only one of `w` and `mw` can be non-NULL assert(w == NULL || mw == NULL); if (w != NULL) { - auto _ attr_unused = destroy_win_start(ps, w); - } else if (mw != NULL) { + destroy_win_start(ps, w); + if (!w->managed || !((struct managed_win *)w)->to_paint) { + // If the window wasn't managed, or was already not rendered, + // we don't need to fade it out. + destroy_win_finish(ps, w); + } + return; + } + if (mw != NULL) { win_unmark_client(ps, mw); win_set_flags(mw, WIN_FLAGS_CLIENT_STALE); ps->pending_updates = true; - } else { - log_debug("Received a destroy notify from an unknown window, %#010x", - ev->window); + return; } + log_debug("Received a destroy notify from an unknown window, %#010x", ev->window); } static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { @@ -308,6 +318,13 @@ static inline void ev_map_notify(session_t *ps, xcb_map_notify_event_t *ev) { } win_set_flags(w, WIN_FLAGS_MAPPED); + // We set `ever_damaged` to false here, instead of in `map_win_start`, + // because we might receive damage events before that function is called + // (which is called when we handle the `WIN_FLAGS_MAPPED` flag), in + // which case `repair_win` will be called, which uses `ever_damaged` so + // it needs to be correct. This also covers the case where the window is + // unmapped before `map_win_start` is called. + w->ever_damaged = false; // FocusIn/Out may be ignored when the window is unmapped, so we must // recheck focus here @@ -349,8 +366,7 @@ static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t { auto w = find_win(ps, ev->window); if (w) { - auto ret = destroy_win_start(ps, w); - if (!ret && w->managed) { + if (w->managed) { auto mw = (struct managed_win *)w; // Usually, damage for unmapped windows // are added in `paint_preprocess`, when @@ -362,8 +378,17 @@ static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t if (mw->to_paint) { add_damage_from_win(ps, mw); } - CHECK(win_skip_fading(ps, mw)); + // Emulating what X server does: a destroyed + // window is always unmapped first. + if (mw->state == WSTATE_MAPPED) { + unmap_win_start(ps, mw); + } } + // Window reparenting is unlike normal window destruction, + // This window is going to be rendered under another + // parent, so we don't fade here. + destroy_win_start(ps, w); + destroy_win_finish(ps, w); } } @@ -378,25 +403,14 @@ static inline void ev_reparent_notify(session_t *ps, xcb_reparent_notify_event_t evmask |= XCB_EVENT_MASK_PROPERTY_CHANGE; } else { auto w_real_top = find_managed_window_or_parent(ps, ev->parent); - if (w_real_top && w_real_top->state != WSTATE_UNMAPPED && - w_real_top->state != WSTATE_UNMAPPING) { + if (w_real_top) { log_debug("Mark window %#010x (%s) as having a stale " "client", w_real_top->base.id, w_real_top->name); win_set_flags(w_real_top, WIN_FLAGS_CLIENT_STALE); ps->pending_updates = true; } else { - if (!w_real_top) { - log_debug("parent %#010x not found", ev->parent); - } else { - // Window is not currently mapped, unmark its - // client to trigger a client recheck when it is - // mapped later. - win_unmark_client(ps, w_real_top); - log_debug("parent %#010x (%s) is in state %d", - w_real_top->base.id, w_real_top->name, - w_real_top->state); - } + log_debug("parent %#010x not found", ev->parent); } } XCB_AWAIT_VOID(xcb_change_window_attributes, ps->c.c, ev->window, @@ -605,7 +619,7 @@ static inline void ev_property_notify(session_t *ps, xcb_property_notify_event_t static inline void repair_win(session_t *ps, struct managed_win *w) { // Only mapped window can receive damages - assert(win_is_mapped_in_x(w)); + assert(w->state == WSTATE_MAPPED || win_check_flags_all(w, WIN_FLAGS_MAPPED)); region_t parts; pixman_region32_init(&parts); @@ -628,6 +642,10 @@ static inline void repair_win(session_t *ps, struct managed_win *w) { free(e); } win_extents(w, &parts); + + // We only binds the window pixmap once the window is damaged. + win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); + ps->pending_updates = true; } else { auto cookie = xcb_damage_subtract(ps->c.c, w->damage, XCB_NONE, ps->damage_ring.x_region); diff --git a/src/meson.build b/src/meson.build index bf02388621..31ec0b4905 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,7 +10,7 @@ base_deps = [ srcs = [ files('picom.c', 'win.c', 'c2.c', 'x.c', 'config.c', 'vsync.c', 'utils.c', 'diagnostic.c', 'string_utils.c', 'render.c', 'kernel.c', 'log.c', 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'statistics.c', - 'vblank.c') ] + 'vblank.c', 'transition.c') ] picom_inc = include_directories('.') cflags = [] diff --git a/src/picom.c b/src/picom.c index 1a9708fb83..ec26cfe992 100644 --- a/src/picom.c +++ b/src/picom.c @@ -48,6 +48,7 @@ #include "inspect.h" #include "kernel.h" #include "picom.h" +#include "transition.h" #include "win_defs.h" #ifdef CONFIG_OPENGL #include "opengl.h" @@ -495,22 +496,11 @@ static double fade_timeout(session_t *ps) { * @param steps steps of fading * @return whether we are still in fading mode */ -static bool run_fade(session_t *ps, struct managed_win **_w, long long steps) { +static bool run_fade(struct managed_win **_w, unsigned int steps) { auto w = *_w; - log_trace("Process fading for window %s (%#010x), steps: %lld", w->name, - w->base.id, steps); - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - // We are not fading - assert(w->opacity_target == w->opacity); - log_trace("|- not fading"); - return false; - } - - if (!win_should_fade(ps, w)) { - log_trace("|- in transition but doesn't need fading"); - w->opacity = w->opacity_target; - } - if (w->opacity == w->opacity_target) { + log_trace("Process fading for window %s (%#010x), steps: %u", w->name, w->base.id, + steps); + if (w->number_of_animations == 0) { // We have reached target opacity. // We don't call win_check_fade_finished here because that could destroy // the window, but we still need the damage info from this window @@ -518,20 +508,14 @@ static bool run_fade(session_t *ps, struct managed_win **_w, long long steps) { return false; } - log_trace("|- fading, opacity: %lf", w->opacity); - if (steps) { - if (w->opacity < w->opacity_target) { - w->opacity = clamp(w->opacity + ps->o.fade_in_step * (double)steps, - 0.0, w->opacity_target); - } else { - w->opacity = clamp(w->opacity - ps->o.fade_out_step * (double)steps, - w->opacity_target, 1); - } - log_trace("|- opacity updated: %lf", w->opacity); - } + log_trace("|- fading, opacity: %lf", animatable_get(&w->opacity)); + animatable_step(&w->opacity, steps); + animatable_step(&w->blur_opacity, steps); + log_trace("|- opacity updated: %lf (%u steps)", animatable_get(&w->opacity), steps); - // Note even if opacity == opacity_target here, we still want to run preprocess - // one last time to finish state transition. So return true in that case too. + // Note even if the animatable is not animating anymore at this point, we still + // want to run preprocess one last time to finish state transition. So return true + // in that case too. return true; } @@ -638,10 +622,7 @@ static void rebuild_shadow_exclude_reg(session_t *ps) { static void destroy_backend(session_t *ps) { win_stack_foreach_managed_safe(w, &ps->window_stack) { // Wrapping up fading in progress - if (win_skip_fading(ps, w)) { - // `w` is freed by win_skip_fading - continue; - } + win_skip_fading(w); if (ps->backend_data) { // Unmapped windows could still have shadow images, but not pixmap @@ -657,6 +638,10 @@ static void destroy_backend(session_t *ps) { win_release_images(ps->backend_data, w); } free_paint(ps, &w->paint); + + if (w->state == WSTATE_DESTROYED) { + destroy_win_finish(ps, &w->base); + } } HASH_ITER2(ps->shaders, shader) { @@ -918,17 +903,19 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, *out_bottom = NULL; // Fading step calculation - long long steps = 0L; + unsigned int steps = 0L; auto now = get_time_ms(); if (ps->fade_time) { assert(now >= ps->fade_time); - steps = (now - ps->fade_time) / ps->o.fade_delta; + auto raw_steps = (now - ps->fade_time) / ps->o.fade_delta; + assert(raw_steps <= UINT_MAX); + steps = (unsigned int)raw_steps; + ps->fade_time += raw_steps * ps->o.fade_delta; } else { // Reset fade_time if unset ps->fade_time = get_time_ms(); steps = 0L; } - ps->fade_time += steps * ps->o.fade_delta; // First, let's process fading, and animated shaders // TODO(yshui) check if a window is fully obscured, and if we don't need to @@ -936,7 +923,6 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, win_stack_foreach_managed_safe(w, &ps->window_stack) { const winmode_t mode_old = w->mode; const bool was_painted = w->to_paint; - const double opacity_old = w->opacity; if (win_should_dim(ps, w) != w->dim) { w->dim = win_should_dim(ps, w); @@ -948,21 +934,23 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, *animation = true; } - // Run fading - if (run_fade(ps, &w, steps)) { - *fade_running = true; - } - // Add window to damaged area if its opacity changes // If was_painted == false, and to_paint is also false, we don't care // If was_painted == false, but to_paint is true, damage will be added in // the loop below - if (was_painted && w->opacity != opacity_old) { + if (was_painted && w->number_of_animations != 0) { add_damage_from_win(ps, w); } - if (win_check_fade_finished(ps, w)) { - // the window has been destroyed because fading finished + // Run fading + if (run_fade(&w, steps)) { + *fade_running = true; + } + + if (w->state == WSTATE_DESTROYED && w->number_of_animations == 0) { + // the window should be destroyed because it was destroyed + // by X server and now its animations are finished + destroy_win_finish(ps, &w->base); continue; } @@ -994,6 +982,8 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, bool to_paint = true; // w->to_paint remembers whether this window is painted last time const bool was_painted = w->to_paint; + const double window_opacity = animatable_get(&w->opacity); + const double blur_opacity = animatable_get(&w->blur_opacity); // Destroy reg_ignore if some window above us invalidated it if (!reg_ignore_valid) { @@ -1007,7 +997,12 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, // Give up if it's not damaged or invisible, or it's unmapped and its // pixmap is gone (for example due to a ConfigureNotify), or when it's // excluded - if (w->state == WSTATE_UNMAPPED) { + if (w->state == WSTATE_UNMAPPED && w->number_of_animations == 0) { + if (window_opacity != 0 || blur_opacity != 0) { + log_warn("Window %#010x (%s) is unmapped but still has " + "opacity", + w->base.id, w->name); + } log_trace("|- is unmapped"); to_paint = false; } else if (unlikely(ps->debug_window != XCB_NONE) && @@ -1015,22 +1010,18 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, w->client_win == ps->debug_window)) { log_trace("|- is the debug window"); to_paint = false; - } else if (!w->ever_damaged && w->state != WSTATE_UNMAPPING && - w->state != WSTATE_DESTROYING) { - // Unmapping clears w->ever_damaged, but the fact that the window - // is fading out means it must have been damaged when it was still - // mapped (because unmap_win_start will skip fading if it wasn't), - // so we still need to paint it. + } else if (!w->ever_damaged) { log_trace("|- has not received any damages"); to_paint = false; } else if (unlikely(w->g.x + w->g.width < 1 || w->g.y + w->g.height < 1 || w->g.x >= ps->root_width || w->g.y >= ps->root_height)) { log_trace("|- is positioned outside of the screen"); to_paint = false; - } else if (unlikely((double)w->opacity * MAX_ALPHA < 1 && !w->blur_background)) { - /* TODO(yshui) for consistency, even a window has 0 opacity, we - * still probably need to blur its background, so to_paint - * shouldn't be false for them. */ + } else if (unlikely(window_opacity * MAX_ALPHA < 1 && + (!w->blur_background || blur_opacity * MAX_ALPHA < 1))) { + // For consistency, even a window has 0 opacity, we would still + // blur its background. (unless it's background is not blurred, or + // the blur opacity is 0) log_trace("|- has 0 opacity"); to_paint = false; } else if (w->paint_excluded) { @@ -1060,7 +1051,7 @@ static bool paint_preprocess(session_t *ps, bool *fade_running, bool *animation, log_verbose("Window %#010x (%s) will be painted", w->base.id, w->name); // Calculate shadow opacity - w->shadow_opacity = ps->o.shadow_opacity * w->opacity * ps->o.frame_opacity; + w->shadow_opacity = ps->o.shadow_opacity * window_opacity * ps->o.frame_opacity; // Generate ignore region for painting to reduce GPU load if (!w->reg_ignore) { @@ -1823,7 +1814,10 @@ static void draw_callback_impl(EV_P_ session_t *ps, int revents attr_unused) { // Using foreach_safe here since skipping fading can cause window to be // freed if it's destroyed. win_stack_foreach_managed_safe(w, &ps->window_stack) { - auto _ attr_unused = win_skip_fading(ps, w); + win_skip_fading(w); + if (w->state == WSTATE_DESTROYED) { + destroy_win_finish(ps, &w->base); + } } } diff --git a/src/render.c b/src/render.c index 316544334a..b03a2102f3 100644 --- a/src/render.c +++ b/src/render.c @@ -12,6 +12,7 @@ #include "common.h" #include "options.h" +#include "transition.h" #ifdef CONFIG_OPENGL #include "backend/gl/glx.h" @@ -436,6 +437,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) const int y = w->g.y; const uint16_t wid = to_u16_checked(w->widthb); const uint16_t hei = to_u16_checked(w->heightb); + const double window_opacity = animatable_get(&w->opacity); xcb_render_picture_t pict = w->paint.pict; @@ -472,7 +474,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) } if (w->frame_opacity == 1) { - paint_region(ps, w, 0, 0, wid, hei, w->opacity, reg_paint, pict); + paint_region(ps, w, 0, 0, wid, hei, window_opacity, reg_paint, pict); } else { // Painting parameters const margin_t extents = win_calc_frame_extents(w); @@ -482,8 +484,8 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) auto const r = extents.right; #define COMP_BDR(cx, cy, cwid, chei) \ - paint_region(ps, w, (cx), (cy), (cwid), (chei), w->frame_opacity * w->opacity, \ - reg_paint, pict) + paint_region(ps, w, (cx), (cy), (cwid), (chei), \ + w->frame_opacity *window_opacity, reg_paint, pict) // Sanitize the margins, in case some broken WM makes // top_width + bottom_width > height in some cases. @@ -542,7 +544,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) // body paint_region(ps, w, cleft, ctop, body_width, body_height, - w->opacity, reg_paint, pict); + window_opacity, reg_paint, pict); } while (0); } @@ -557,7 +559,7 @@ void paint_one(session_t *ps, struct managed_win *w, const region_t *reg_paint) if (w->dim) { double dim_opacity = ps->o.inactive_dim; if (!ps->o.inactive_dim_fixed) { - dim_opacity *= w->opacity; + dim_opacity *= window_opacity; } switch (ps->o.backend) { @@ -899,12 +901,13 @@ win_blur_background(session_t *ps, struct managed_win *w, xcb_render_picture_t t auto const wid = to_u16_checked(w->widthb); auto const hei = to_u16_checked(w->heightb); const int cr = w ? w->corner_radius : 0; + const double window_opacity = animatable_get(&w->opacity); double factor_center = 1.0; // Adjust blur strength according to window opacity, to make it appear // better during fading if (!ps->o.blur_background_fixed) { - double pct = 1.0 - w->opacity * (1.0 - 1.0 / 9.0); + double pct = 1.0 - window_opacity * (1.0 - 1.0 / 9.0); factor_center = pct * 8.0 / (1.1 - pct); } @@ -1143,7 +1146,7 @@ void paint_all(session_t *ps, struct managed_win *t) { } // Only clip shadows above visible windows - if (w->opacity * MAX_ALPHA >= 1) { + if (animatable_get(&w->opacity) * MAX_ALPHA >= 1) { if (w->clip_shadow_above) { // Add window bounds to shadow-clip region pixman_region32_union(®_shadow_clip, ®_shadow_clip, diff --git a/src/transition.c b/src/transition.c new file mode 100644 index 0000000000..939fc525d0 --- /dev/null +++ b/src/transition.c @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#include +#include +#include + +#include "compiler.h" +#include "transition.h" +#include "utils.h" + +/// Get the current value of an `animatable`. +double animatable_get(const struct animatable *a) { + if (a->step_state) { + return a->step_state->current; + } + + if (a->duration) { + assert(a->progress < a->duration); + return a->interpolator(a); + } + return a->target; +} + +double animatable_get_progress(const struct animatable *a) { + if (a->duration) { + return (double)a->progress / a->duration; + } + return 1; +} + +/// Advance the animation by a given number of steps. +void animatable_step(struct animatable *a, unsigned int steps) { + if (!a->duration || !steps) { + return; + } + + assert(a->progress < a->duration); + if (steps > a->duration - a->progress) { + steps = a->duration - a->progress; + } + a->progress += steps; + if (a->step_state) { + a->step(a, steps); + } + + if (a->progress == a->duration) { + a->start = a->target; + a->duration = 0; + a->progress = 0; + if (a->step_state) { + a->step_state->current = a->target; + } + if (a->callback) { + a->callback(TRANSITION_COMPLETED, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + } +} + +/// Returns whether an `animatable` is currently animating. +bool animatable_is_animating(const struct animatable *a) { + assert(!a->duration || a->progress < a->duration); + return a->duration; +} + +/// Cancel the current animation of an `animatable`. This stops the animation and +/// the `animatable` will retain its current value. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_cancel(struct animatable *a) { + if (!a->duration) { + return false; + } + + a->start = animatable_get(a); + a->target = a->start; + a->duration = 0; + a->progress = 0; + if (a->step_state) { + a->step_state->current = a->start; + } + if (a->callback) { + a->callback(TRANSITION_CANCELED, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + return true; +} + +/// Cancel the current animation of an `animatable` and set its value to its target. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_early_stop(struct animatable *a) { + if (!a->duration) { + return false; + } + + a->start = a->target; + a->duration = 0; + a->progress = 0; + if (a->step_state) { + a->step_state->current = a->target; + } + if (a->callback) { + a->callback(TRANSITION_STOPPED_EARLY, a->callback_data); + a->callback = NULL; + a->callback_data = NULL; + } + return true; +} + +/// Change the target value of an `animatable`. +/// If the `animatable` is already animating, the animation will be canceled first. +void animatable_set_target(struct animatable *a, double target, unsigned int duration, + transition_callback_fn cb, void *data) { + animatable_cancel(a); + if (!duration) { + a->start = target; + a->target = target; + if (cb) { + cb(TRANSITION_COMPLETED, data); + } + return; + } + + a->target = target; + a->duration = duration; + a->progress = 0; + if (a->step_state) { + a->step(a, 0); + } + a->callback = cb; + a->callback_data = data; +} + +/// Create a new animatable. +struct animatable animatable_new(double value, interpolator_fn interpolator, step_fn step) { + assert(!interpolator || !step); + struct animatable ret = { + .start = value, + .target = value, + .duration = 0, + .progress = 0, + .step_state = NULL, + }; + if (interpolator) { + ret.interpolator = interpolator; + } else if (step) { + ret.step = step; + step(&ret, 0); + } + return ret; +} + +double linear_interpolator(const struct animatable *a) { + double t = (double)a->progress / a->duration; + return (1 - t) * a->start + t * a->target; +} diff --git a/src/transition.h b/src/transition.h new file mode 100644 index 0000000000..1d55918b5c --- /dev/null +++ b/src/transition.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) Yuxuan Shui + +#pragma once +#include + +struct animatable; +enum transition_event; +/// The interpolator function for an animatable. This function should calculate the +/// current value of the `animatable` based on its `start`, `target`, `duration` and +/// `progress`. +typedef double (*interpolator_fn)(const struct animatable *); +/// The step function for an animatable. This function should advance the animation state +/// by one step. This function is called _after_ `progress` is incremented. If `progress` +/// is 0 when the function is called, it means an animation has just started, and this +/// function should initialize the state. If `step_state` is NULL when this function is +/// called, this function should allocate and initialize `step_state`. +/// `steps` is the number of steps to advance. This is always 1 or more, unless `progress` +/// is 0 or `step_state` is NULL, in which case `steps` is 0. +typedef void (*step_fn)(struct animatable *, unsigned int steps); + +/// Callback when the transition state changes. Callback might be called by: +/// - `animatable_set_target` generates TRANSITION_COMPLETED when the specified duration +/// is 0. also generates TRANSITION_CANCELLED if the animatable was already animating. +/// - `animatable_cancel` generates TRANSITION_CANCELED +/// - `animatable_early_stop` generates TRANSITION_STOPPED_EARLY +/// - `animatable_step` generates TRANSITION_COMPLETED when the animation is completed. +/// Callback is guaranteed to be called exactly once for each `animatable_set_target` +/// call, unless an animatable is freed before the transition is completed. +typedef void (*transition_callback_fn)(enum transition_event event, void *data); + +enum transition_event { + TRANSITION_COMPLETED, + TRANSITION_CANCELED, + TRANSITION_STOPPED_EARLY, +}; + +/// The base type for step_state. +struct step_state_base { + /// The current value of the `animatable`. + /// If the `animatable` is not animated, this equals to `animatable->target`. + double current; +}; + +/// An animatable value +struct animatable { + /// The starting value. + /// When this `animatable` is not animated, this is the current value. + double start; + /// The target value. + /// If the `animatable` is not animated, this equals to `start`. + double target; + /// The animation duration in number of steps. + /// If the `animatable` is not animated, this is 0. + unsigned int duration; + /// The current progress of the animation. From 0 to `duration - 1`. + /// If the `animatable` is not animated, this is 0. + unsigned int progress; + + transition_callback_fn callback; + void *callback_data; + + /// Step function state. + struct step_state_base *step_state; + /// The function for calculating the current value. If + /// `step_state` is not NULL, the `step` function is used; + /// otherwise, the `interpolator` function is used. + union { + /// The interpolator function. + interpolator_fn interpolator; + /// The step function. + step_fn step; + }; +}; + +// =============================== API =============================== + +/// Get the current value of an `animatable`. +double animatable_get(const struct animatable *a); +/// Get the animation progress as a percentage of the total duration. +double animatable_get_progress(const struct animatable *a); +/// Advance the animation by a given number of steps. +void animatable_step(struct animatable *a, unsigned int steps); +/// Returns whether an `animatable` is currently animating. +bool animatable_is_animating(const struct animatable *a); +/// Cancel the current animation of an `animatable`. This stops the animation and +/// the `animatable` will retain its current value. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_cancel(struct animatable *a); +/// Cancel the current animation of an `animatable` and set its value to its target. +/// +/// Returns true if the `animatable` was animated before this function is called. +bool animatable_early_stop(struct animatable *a); +/// Change the target value of an `animatable`. +/// If the `animatable` is already animating, the animation will be canceled first. +void animatable_set_target(struct animatable *a, double target, unsigned int duration, + transition_callback_fn cb, void *data); +/// Create a new animatable. +struct animatable animatable_new(double value, interpolator_fn interpolator, step_fn step); + +// ========================== Interpolators ========================== + +double linear_interpolator(const struct animatable *a); diff --git a/src/win.c b/src/win.c index 78b5da84a5..09e6a7641c 100644 --- a/src/win.c +++ b/src/win.c @@ -27,9 +27,11 @@ #include "region.h" #include "render.h" #include "string_utils.h" +#include "transition.h" #include "types.h" #include "uthash_extra.h" #include "utils.h" +#include "win_defs.h" #include "x.h" #ifdef CONFIG_DBUS @@ -117,17 +119,6 @@ static inline xcb_window_t win_get_leader(session_t *ps, struct managed_win *w) return win_get_leader_raw(ps, w, 0); } -/** - * Whether the real content of the window is visible. - * - * A window is not considered "real" visible if it's fading out. Because in that case a - * cached version of the window is displayed. - */ -static inline bool attr_pure win_is_real_visible(const struct managed_win *w) { - return w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING; -} - /** * Update focused state of a window. */ @@ -436,6 +427,9 @@ static void win_clear_all_properties_stale(struct managed_win *w); /// Fetch new window properties from the X server, and run appropriate updates. /// Might set WIN_FLAGS_FACTOR_CHANGED static void win_update_properties(session_t *ps, struct managed_win *w) { + // we cannot receive property change when window has been destroyed + assert(w->state != WSTATE_DESTROYED); + if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_TYPE)) { if (win_update_wintype(&ps->c, ps->atoms, w)) { win_set_flags(w, WIN_FLAGS_FACTOR_CHANGED); @@ -444,8 +438,6 @@ static void win_update_properties(session_t *ps, struct managed_win *w) { if (win_fetch_and_unset_property_stale(w, ps->atoms->a_NET_WM_WINDOW_OPACITY)) { win_update_opacity_prop(&ps->c, ps->atoms, w, ps->o.detect_client_opacity); - // we cannot receive OPACITY change when window has been destroyed - assert(w->state != WSTATE_DESTROYING); win_update_opacity_target(ps, w); } @@ -502,20 +494,18 @@ static void win_update_properties(session_t *ps, struct managed_win *w) { /// Handle non-image flags. This phase might set IMAGES_STALE flags void win_process_update_flags(session_t *ps, struct managed_win *w) { - // Whether the window was visible before we process the mapped flag. i.e. - // is the window just mapped. - bool was_visible = win_is_real_visible(w); - log_trace("Processing flags for window %#010x (%s), was visible: %d, flags: " + log_trace("Processing flags for window %#010x (%s), was rendered: %d, flags: " "%#" PRIx64, - w->base.id, w->name, was_visible, w->flags); + w->base.id, w->name, w->to_paint, w->flags); if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { map_win_start(ps, w); win_clear_flags(w, WIN_FLAGS_MAPPED); } - if (!win_is_real_visible(w)) { - // Flags of invisible windows are processed when they are mapped + if (w->state != WSTATE_MAPPED) { + // Window is not mapped, so we ignore all its changes until it's mapped + // again. return; } @@ -532,16 +522,24 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { bool damaged = false; if (win_check_flags_any(w, WIN_FLAGS_SIZE_STALE | WIN_FLAGS_POSITION_STALE)) { - if (was_visible) { + // For damage calculation purposes, we don't care if the window + // is mapped in X server, we only care if we rendered it last + // frame. + // + // We do not process window flags for unmapped windows even when + // it was rendered, so an window fading out won't move even if the + // underlying unmapped window is moved. When the window is + // mapped again when it's still fading out, it should have the + // same effect as a mapped window being moved, meaning we have + // to add both the previous and the new window extents to + // damage. + // + // All that is basically me saying what really matters is if the + // window was rendered last frame, not if it's mapped in X server. + if (w->to_paint) { // Mark the old extents of this window as damaged. The new // extents will be marked damaged below, after the window // extents are updated. - // - // If the window is just mapped, we don't need to mark the - // old extent as damaged. (It's possible that the window - // was in fading and is interrupted by being mapped. In - // that case, the fading window will be added to damage by - // map_win_start, so we don't need to do it here) add_damage_from_win(ps, w); } @@ -598,10 +596,10 @@ void win_process_update_flags(session_t *ps, struct managed_win *w) { } void win_process_image_flags(session_t *ps, struct managed_win *w) { + // Assert that the MAPPED flag is already handled. assert(!win_check_flags_all(w, WIN_FLAGS_MAPPED)); - if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYING || - w->state == WSTATE_UNMAPPING) { + if (w->state != WSTATE_MAPPED) { // Flags of invisible windows are processed when they are mapped return; } @@ -620,7 +618,6 @@ void win_process_image_flags(session_t *ps, struct managed_win *w) { // otherwise we won't be able to rebind pixmap after // releasing it, yet we might still need the pixmap for // rendering. - assert(w->state != WSTATE_UNMAPPING && w->state != WSTATE_DESTROYING); if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { // Must release images first, otherwise breaks // NVIDIA driver @@ -799,7 +796,7 @@ bool win_client_has_alpha(const struct managed_win *w) { } winmode_t win_calc_mode(const struct managed_win *w) { - if (w->opacity < 1.0) { + if (animatable_get(&w->opacity) < 1.0) { return WMODE_TRANS; } @@ -846,16 +843,13 @@ winmode_t win_calc_mode(const struct managed_win *w) { * * @return target opacity */ -double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { +static double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { double opacity = 1; - if (w->state == WSTATE_UNMAPPED) { + if (w->state == WSTATE_UNMAPPED || w->state == WSTATE_DESTROYED) { // be consistent return 0; } - if (w->state == WSTATE_UNMAPPING || w->state == WSTATE_DESTROYING) { - return 0; - } // Try obeying opacity property and window type opacity firstly if (w->has_opacity_prop) { opacity = ((double)w->opacity_prop) / OPAQUE; @@ -882,19 +876,62 @@ double win_calc_opacity_target(session_t *ps, const struct managed_win *w) { return opacity; } -/** - * Determine whether a window is to be dimmed. - */ -bool win_should_dim(session_t *ps, const struct managed_win *w) { - // Make sure we do nothing if the window is unmapped / being destroyed - if (w->state == WSTATE_UNMAPPED) { - return false; +/// Finish the unmapping of a window (e.g. after fading has finished). +/// Doesn't free `w` +static void unmap_win_finish(session_t *ps, struct managed_win *w) { + w->reg_ignore_valid = false; + w->state = WSTATE_UNMAPPED; + + // We are in unmap_win, this window definitely was viewable + if (ps->backend_data) { + // Only the pixmap needs to be freed and reacquired when mapping. + // Shadow image can be preserved. + if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { + win_release_pixmap(ps->backend_data, w); + } + } else { + assert(!w->win_image); + assert(!w->shadow_image); } - if (ps->o.inactive_dim > 0 && !(w->focused)) { - return true; + free_paint(ps, &w->paint); + free_paint(ps, &w->shadow_paint); + + // Try again at binding images when the window is mapped next time + win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); +} + +struct window_transition_data { + struct managed_win *w; + session_t *ps; + // TODO(yshui) switch to only pass backend_data after the legacy backend removal + // struct backend_base *backend_data; + uint64_t refcount; +}; + +static void win_transition_callback(enum transition_event event attr_unused, void *data_) { + auto data = (struct window_transition_data *)data_; + auto w = data->w; + w->number_of_animations--; + if (w->number_of_animations == 0) { + if (w->state == WSTATE_DESTROYED || w->state == WSTATE_UNMAPPED) { + if (animatable_get(&w->opacity) != 0) { + log_warn("Window %#010x (%s) has finished fading out but " + "its opacity is not 0", + w->base.id, w->name); + } + } + if (w->state == WSTATE_UNMAPPED) { + unmap_win_finish(data->ps, data->w); + } + // Destroyed windows are freed in paint_preprocess, this makes managing + // the lifetime of windows easier. + w->in_openclose = false; + } + data->refcount--; + if (data->refcount == 0) { + free(data); } - return false; } /** @@ -909,7 +946,7 @@ bool win_should_fade(session_t *ps, const struct managed_win *w) { if (ps->o.no_fading_openclose && w->in_openclose) { return false; } - if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYING && + if (ps->o.no_fading_destroyed_argb && w->state == WSTATE_DESTROYED && win_has_alpha(w) && w->client_win && w->client_win != w->base.id) { // deprecated return false; @@ -920,6 +957,59 @@ bool win_should_fade(session_t *ps, const struct managed_win *w) { return ps->o.wintype_option[w->window_type].fade; } +/// Call `animatable_set_target` on the opacity of a window, with appropriate +/// target opacity and duration. +static inline void +win_start_fade(session_t *ps, struct managed_win *w, double target_blur_opacity) { + double current_opacity = animatable_get(&w->opacity), + target_opacity = win_calc_opacity_target(ps, w); + double step_size = + target_opacity > current_opacity ? ps->o.fade_in_step : ps->o.fade_out_step; + unsigned int duration = + (unsigned int)(fabs(target_opacity - current_opacity) / step_size); + if (!win_should_fade(ps, w)) { + duration = 0; + } + auto data = ccalloc(1, struct window_transition_data); + data->ps = ps; + data->w = w; + data->refcount = 1; + + animatable_cancel(&w->blur_opacity); // Cancel any ongoing blur animation + // so we can check its current value + + // We want to set the correct `number_of_animations` before calling + // `animatable_set_target`, because it might trigger our callback which will + // decrement `number_of_animations` + w->number_of_animations++; + if (target_blur_opacity != w->blur_opacity.start) { + w->number_of_animations++; + data->refcount++; + } + + animatable_set_target(&w->opacity, target_opacity, duration, + win_transition_callback, data); + if (target_blur_opacity != w->blur_opacity.start) { + animatable_set_target(&w->blur_opacity, target_blur_opacity, duration, + win_transition_callback, data); + } +} + +/** + * Determine whether a window is to be dimmed. + */ +bool win_should_dim(session_t *ps, const struct managed_win *w) { + // Make sure we do nothing if the window is unmapped / being destroyed + if (w->state == WSTATE_UNMAPPED) { + return false; + } + + if (ps->o.inactive_dim > 0 && !(w->focused)) { + return true; + } + return false; +} + /** * Reread _COMPTON_SHADOW property from a window. * @@ -949,8 +1039,7 @@ static void win_set_shadow(session_t *ps, struct managed_win *w, bool shadow_new // We don't handle property updates of non-visible windows until they are // mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); + assert(w->state == WSTATE_MAPPED); // Keep a copy of window extent before the shadow change. Will be used for // calculation of damaged region @@ -1308,8 +1397,7 @@ void win_on_win_size_change(struct managed_win *w, int shadow_offset_x, // We don't handle property updates of non-visible windows until they are // mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); + assert(w->state == WSTATE_MAPPED); } /** @@ -1605,8 +1693,6 @@ struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct wi .cache_leader = XCB_NONE, .window_type = WINTYPE_UNKNOWN, .focused = false, - .opacity = 0, - .opacity_target = 0, .has_opacity_prop = false, .opacity_prop = OPAQUE, .opacity_is_set = false, @@ -1685,6 +1771,8 @@ struct win *attr_ret_nonnull maybe_allocate_managed_win(session_t *ps, struct wi new->base = *w; new->base.managed = true; new->a = *a; + new->opacity = animatable_new(0, linear_interpolator, NULL); + new->blur_opacity = animatable_new(0, linear_interpolator, NULL); pixman_region32_init(&new->bounding_shape); free(a); @@ -1936,8 +2024,7 @@ void win_update_bounding_shape(struct x_connection *c, struct managed_win *w, bool shape_exists, bool detect_rounded_corners) { // We don't handle property updates of non-visible windows until they are // mapped. - assert(w->state != WSTATE_UNMAPPED && w->state != WSTATE_DESTROYING && - w->state != WSTATE_UNMAPPING); + assert(w->state == WSTATE_MAPPED); pixman_region32_clear(&w->bounding_shape); // Start with the window rectangular region @@ -2095,51 +2182,17 @@ void win_ev_stop(session_t *ps, const struct win *w) { } } -/// Finish the unmapping of a window (e.g. after fading has finished). -/// Doesn't free `w` -static void unmap_win_finish(session_t *ps, struct managed_win *w) { - w->reg_ignore_valid = false; - w->state = WSTATE_UNMAPPED; - - // We are in unmap_win, this window definitely was viewable - if (ps->backend_data) { - // Only the pixmap needs to be freed and reacquired when mapping. - // Shadow image can be preserved. - if (!win_check_flags_all(w, WIN_FLAGS_PIXMAP_NONE)) { - win_release_pixmap(ps->backend_data, w); - } - } else { - assert(!w->win_image); - assert(!w->shadow_image); - } - - free_paint(ps, &w->paint); - free_paint(ps, &w->shadow_paint); - - // Try again at binding images when the window is mapped next time - win_clear_flags(w, WIN_FLAGS_IMAGE_ERROR); -} - /// Finish the destruction of a window (e.g. after fading has finished). /// Frees `w` -static void destroy_win_finish(session_t *ps, struct win *w) { - log_trace("Trying to finish destroying (%#010x)", w->id); +void destroy_win_finish(session_t *ps, struct win *w) { + log_verbose("Trying to finish destroying (%#010x)", w->id); auto next_w = win_stack_find_next_managed(ps, &w->stack_neighbour); list_remove(&w->stack_neighbour); if (w->managed) { auto mw = (struct managed_win *)w; - - if (mw->state != WSTATE_UNMAPPED) { - // Only UNMAPPED state has window resources freed, - // otherwise we need to call unmap_win_finish to free - // them. - // XXX actually we unmap_win_finish only frees the - // rendering resources, we still need to call free_win_res. - // will fix later. - unmap_win_finish(ps, mw); - } + unmap_win_finish(ps, mw); // Unmapping might preserve the shadow image, so free it here win_release_shadow(ps->backend_data, mw); @@ -2183,11 +2236,6 @@ static void destroy_win_finish(session_t *ps, struct win *w) { free(w); } -static void map_win_finish(struct managed_win *w) { - w->in_openclose = false; - w->state = WSTATE_MAPPED; -} - /// Move window `w` so it's before `next` in the list static inline void restack_win(session_t *ps, struct win *w, struct list_node *next) { struct managed_win *mw = NULL; @@ -2275,9 +2323,7 @@ void restack_top(session_t *ps, struct win *w) { /// Start destroying a window. Windows cannot always be destroyed immediately /// because of fading and such. -/// -/// @return whether the window has finished destroying and is freed -bool destroy_win_start(session_t *ps, struct win *w) { +void destroy_win_start(session_t *ps, struct win *w) { auto mw = (struct managed_win *)w; assert(w); @@ -2292,14 +2338,15 @@ bool destroy_win_start(session_t *ps, struct win *w) { // finishes destroying. HASH_DEL(ps->windows, w); - if (!w->managed || mw->state == WSTATE_UNMAPPED) { - // Window is already unmapped, or is an unmanaged window, just - // destroy it - destroy_win_finish(ps, w); - return true; - } - if (w->managed) { + if (mw->state != WSTATE_UNMAPPED) { + // Only UNMAPPED state has window resources freed, + // otherwise we need to call unmap_win_finish to free + // them. + log_warn("Did X server not unmap window %#010x before destroying " + "it?", + w->id); + } // Clear IMAGES_STALE flags since the window is destroyed: Clear // PIXMAP_STALE as there is no pixmap available anymore, so STALE // doesn't make sense. @@ -2331,9 +2378,17 @@ bool destroy_win_start(session_t *ps, struct win *w) { WIN_FLAGS_FACTOR_CHANGED | WIN_FLAGS_CLIENT_STALE); // Update state flags of a managed window - mw->state = WSTATE_DESTROYING; + mw->state = WSTATE_DESTROYED; mw->a.map_state = XCB_MAP_STATE_UNMAPPED; mw->in_openclose = true; + + // We don't initiate animation here, because it should already have been + // started by unmap_win_start, because X automatically unmaps windows + // before destroying them. But we do need to stop animation if + // no_fading_destroyed_windows, or no_fading_openclose is enabled. + if (!win_should_fade(ps, mw)) { + win_skip_fading(mw); + } } // don't need win_ev_stop because the window is gone anyway @@ -2344,12 +2399,10 @@ bool destroy_win_start(session_t *ps, struct win *w) { } #endif - if (!ps->redirected) { + if (!ps->redirected && w->managed) { // Skip transition if we are not rendering - return win_skip_fading(ps, mw); + win_skip_fading(mw); } - - return false; } void unmap_win_start(session_t *ps, struct managed_win *w) { @@ -2359,25 +2412,13 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { log_debug("Unmapping %#010x \"%s\"", w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { - log_warn("Trying to undestroy a window?"); - assert(false); - } - - bool was_damaged = w->ever_damaged; - w->ever_damaged = false; + assert(w->state != WSTATE_DESTROYED); - if (unlikely(w->state == WSTATE_UNMAPPING || w->state == WSTATE_UNMAPPED)) { - if (win_check_flags_all(w, WIN_FLAGS_MAPPED)) { - // Clear the pending map as this window is now unmapped - win_clear_flags(w, WIN_FLAGS_MAPPED); - } else { - log_warn("Trying to unmapping an already unmapped window " - "%#010x " - "\"%s\"", - w->base.id, w->name); - assert(false); - } + if (unlikely(w->state == WSTATE_UNMAPPED)) { + assert(win_check_flags_all(w, WIN_FLAGS_MAPPED)); + // Window is mapped, but we hadn't had a chance to handle the MAPPED flag. + // Clear the pending map as this window is now unmapped + win_clear_flags(w, WIN_FLAGS_MAPPED); return; } @@ -2385,9 +2426,8 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { // triggered by subsequence Focus{In, Out} event, or by recheck_focus w->a.map_state = XCB_MAP_STATE_UNMAPPED; - w->state = WSTATE_UNMAPPING; - w->opacity_target_old = fmax(w->opacity_target, w->opacity_target_old); - w->opacity_target = win_calc_opacity_target(ps, w); + w->state = WSTATE_UNMAPPED; + win_start_fade(ps, w, 0); #ifdef CONFIG_DBUS // Send D-Bus signal @@ -2396,51 +2436,24 @@ void unmap_win_start(session_t *ps, struct managed_win *w) { } #endif - if (!ps->redirected || !was_damaged) { + if (!ps->redirected || !w->ever_damaged) { // If we are not redirected, we skip fading because we aren't // rendering anything anyway. If the window wasn't ever damaged, // it shouldn't be painted either. But a fading out window is // always painted, so we have to skip fading here. - CHECK(!win_skip_fading(ps, w)); + win_skip_fading(w); } } -/** - * Execute fade callback of a window if fading finished. - * - * @return whether the window is destroyed and freed - */ -bool win_check_fade_finished(session_t *ps, struct managed_win *w) { - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - // No fading in progress - assert(w->opacity_target == w->opacity); - return false; - } - if (w->opacity == w->opacity_target) { - switch (w->state) { - case WSTATE_UNMAPPING: unmap_win_finish(ps, w); return false; - case WSTATE_DESTROYING: destroy_win_finish(ps, &w->base); return true; - case WSTATE_MAPPING: map_win_finish(w); return false; - case WSTATE_FADING: w->state = WSTATE_MAPPED; break; - default: unreachable(); - } - } - - return false; -} - /// Skip the current in progress fading of window, /// transition the window straight to its end state -/// -/// @return whether the window is destroyed and freed -bool win_skip_fading(session_t *ps, struct managed_win *w) { - if (w->state == WSTATE_MAPPED || w->state == WSTATE_UNMAPPED) { - assert(w->opacity_target == w->opacity); - return false; +void win_skip_fading(struct managed_win *w) { + if (w->number_of_animations == 0) { + return; } log_debug("Skipping fading process of window %#010x (%s)", w->base.id, w->name); - w->opacity = w->opacity_target; - return win_check_fade_finished(ps, w); + animatable_early_stop(&w->opacity); + animatable_early_stop(&w->blur_opacity); } // TODO(absolutelynothelix): rename to x_update_win_(randr_?)monitor and move to @@ -2476,26 +2489,13 @@ void map_win_start(session_t *ps, struct managed_win *w) { log_debug("Mapping (%#010x \"%s\")", w->base.id, w->name); - assert(w->state != WSTATE_DESTROYING); - if (w->state != WSTATE_UNMAPPED && w->state != WSTATE_UNMAPPING) { - log_warn("Mapping an already mapped window"); + assert(w->state != WSTATE_DESTROYED); + if (w->state == WSTATE_MAPPED) { + log_error("Mapping an already mapped window"); + assert(false); return; } - if (w->state == WSTATE_UNMAPPING) { - CHECK(!win_skip_fading(ps, w)); - // We skipped the unmapping process, the window was rendered, now - // it is not anymore. So we need to mark the then unmapping window - // as damaged. - // - // Solves problem when, for example, a window is unmapped then - // mapped in a different location - add_damage_from_win(ps, w); - assert(w); - } - - assert(w->state == WSTATE_UNMAPPED); - // Rant: window size could change after we queried its geometry here and // before we get its pixmap. Later, when we get back to the event // processing loop, we will get the notification about size change from @@ -2511,24 +2511,11 @@ void map_win_start(session_t *ps, struct managed_win *w) { log_debug("Window (%#010x) has type %s", w->base.id, WINTYPES[w->window_type].name); - // XXX We need to make sure that win_data is available - // iff `state` is MAPPED - w->state = WSTATE_MAPPING; - w->opacity_target_old = 0; - w->opacity_target = win_calc_opacity_target(ps, w); + w->state = WSTATE_MAPPED; + win_start_fade(ps, w, 1); log_debug("Window %#010x has opacity %f, opacity target is %f", w->base.id, - w->opacity, w->opacity_target); - - // Cannot set w->ever_damaged = false here, since window mapping could be - // delayed, so a damage event might have already arrived before this - // function is called. But this should be unnecessary in the first place, - // since ever_damaged is set to false in unmap_win_finish anyway. - - // Sets the WIN_FLAGS_IMAGES_STALE flag so later in the critical section - // the window's image will be bound - - win_set_flags(w, WIN_FLAGS_PIXMAP_STALE); + animatable_get(&w->opacity), w->opacity.target); #ifdef CONFIG_DBUS // Send D-Bus signal @@ -2538,7 +2525,7 @@ void map_win_start(session_t *ps, struct managed_win *w) { #endif if (!ps->redirected) { - CHECK(!win_skip_fading(ps, w)); + win_skip_fading(w); } } @@ -2546,53 +2533,18 @@ void map_win_start(session_t *ps, struct managed_win *w) { * Update target window opacity depending on the current state. */ void win_update_opacity_target(session_t *ps, struct managed_win *w) { - auto opacity_target_old = w->opacity_target; - w->opacity_target = win_calc_opacity_target(ps, w); + win_start_fade(ps, w, w->blur_opacity.target); // We don't want to change + // blur_opacity target - if (opacity_target_old == w->opacity_target) { + if (w->number_of_animations == 0) { return; } - if (w->state == WSTATE_MAPPED) { - // Opacity target changed while MAPPED. Transition to FADING. - assert(w->opacity == opacity_target_old); - w->opacity_target_old = opacity_target_old; - w->state = WSTATE_FADING; - log_debug("Window %#010x (%s) opacity %f, opacity target %f, set " - "old target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } else if (w->state == WSTATE_MAPPING) { - // Opacity target changed while fading in. - if (w->opacity >= w->opacity_target) { - // Already reached new target opacity. Transition to - // FADING. - map_win_finish(w); - w->opacity_target_old = fmax(opacity_target_old, w->opacity); - w->state = WSTATE_FADING; - log_debug("Window %#010x (%s) opacity %f already reached " - "new opacity target %f while mapping, set old " - "target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } - } else if (w->state == WSTATE_FADING) { - // Opacity target changed while FADING. - if ((w->opacity < opacity_target_old && w->opacity > w->opacity_target) || - (w->opacity > opacity_target_old && w->opacity < w->opacity_target)) { - // Changed while fading in and will fade out or while - // fading out and will fade in. - w->opacity_target_old = opacity_target_old; - log_debug("Window %#010x (%s) opacity %f already reached " - "new opacity target %f while fading, set " - "old target %f", - w->base.id, w->name, w->opacity, w->opacity_target, - w->opacity_target_old); - } - } + log_debug("Window %#010x (%s) opacity %f, opacity target %f, start %f", w->base.id, + w->name, animatable_get(&w->opacity), w->opacity.target, w->opacity.start); if (!ps->redirected) { - CHECK(!win_skip_fading(ps, w)); + win_skip_fading(w); } } @@ -2620,7 +2572,7 @@ struct managed_win *find_managed_win(session_t *ps, xcb_window_t id) { } auto mw = (struct managed_win *)w; - assert(mw->state != WSTATE_DESTROYING); + assert(mw->state != WSTATE_DESTROYED); return mw; } @@ -2690,7 +2642,7 @@ struct managed_win *find_managed_window_or_parent(session_t *ps, xcb_window_t wi /// Set flags on a window. Some sanity checks are performed void win_set_flags(struct managed_win *w, uint64_t flags) { log_debug("Set flags %" PRIu64 " to window %#010x (%s)", flags, w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { + if (unlikely(w->state == WSTATE_DESTROYED)) { log_error("Flags set on a destroyed window %#010x (%s)", w->base.id, w->name); return; } @@ -2702,7 +2654,7 @@ void win_set_flags(struct managed_win *w, uint64_t flags) { void win_clear_flags(struct managed_win *w, uint64_t flags) { log_debug("Clear flags %" PRIu64 " from window %#010x (%s)", flags, w->base.id, w->name); - if (unlikely(w->state == WSTATE_DESTROYING)) { + if (unlikely(w->state == WSTATE_DESTROYED)) { log_warn("Flags cleared on a destroyed window %#010x (%s)", w->base.id, w->name); return; @@ -2821,9 +2773,3 @@ win_stack_find_next_managed(const session_t *ps, const struct list_node *w) { } return NULL; } - -/// Return whether this window is mapped on the X server side -bool win_is_mapped_in_x(const struct managed_win *w) { - return w->state == WSTATE_MAPPING || w->state == WSTATE_FADING || - w->state == WSTATE_MAPPED || (w->flags & WIN_FLAGS_MAPPED); -} diff --git a/src/win.h b/src/win.h index 7040669fa4..370bd2223d 100644 --- a/src/win.h +++ b/src/win.h @@ -16,6 +16,7 @@ #include "list.h" #include "region.h" #include "render.h" +#include "transition.h" #include "types.h" #include "utils.h" #include "win_defs.h" @@ -126,7 +127,10 @@ struct managed_win { const xcb_render_pictforminfo_t *client_pictfmt; /// Window painting mode. winmode_t mode; - /// Whether the window has been damaged at least once. + /// Whether the window has been damaged at least once since it + /// was mapped. Unmapped windows that were previously mapped + /// retain their `ever_damaged` state. Mapping a window resets + /// this. bool ever_damaged; /// Whether the window was damaged after last paint. bool pixmap_damaged; @@ -204,12 +208,12 @@ struct managed_win { bool is_ewmh_focused; // Opacity-related members - /// Current window opacity. - double opacity; - /// Target window opacity. - double opacity_target; - /// Previous window opacity. - double opacity_target_old; + /// Window opacity + struct animatable opacity; + /// Opacity of the window's background blur + /// Used to gracefully fade in/out the window, otherwise the blur + /// would be at full/zero intensity immediately which will be jarring. + struct animatable blur_opacity; /// true if window (or client window, for broken window managers /// not transferring client window's _NET_WM_WINDOW_OPACITY value) has opacity /// prop @@ -281,6 +285,10 @@ struct managed_win { struct c2_window_state c2_state; + // Animation related + /// Number of animations currently in progress + unsigned int number_of_animations; + #ifdef CONFIG_OPENGL /// Textures and FBO background blur use. glx_blur_cache_t glx_blur_cache; @@ -308,7 +316,7 @@ void map_win_start(struct session *, struct managed_win *); /// Start the destroying of a window. Windows cannot always be destroyed immediately /// because of fading and such. -bool must_use destroy_win_start(session_t *ps, struct win *w); +void destroy_win_start(session_t *ps, struct win *w); /// Release images bound with a window, set the *_NONE flags on the window. Only to be /// used when de-initializing the backend outside of win.c @@ -326,20 +334,6 @@ bool attr_pure win_should_fade(session_t *ps, const struct managed_win *w); void win_on_factor_change(session_t *ps, struct managed_win *w); void win_unmark_client(session_t *ps, struct managed_win *w); -/** - * Calculate and return the opacity target of a window. - * - * The priority of opacity settings are: - * - * inactive_opacity_override (if set, and unfocused) > _NET_WM_WINDOW_OPACITY (if set) > - * opacity-rules (if matched) > window type default opacity > active/inactive opacity - * - * @param ps current session - * @param w struct _win object representing the window - * - * @return target opacity - */ -double attr_pure win_calc_opacity_target(session_t *ps, const struct managed_win *w); bool attr_pure win_should_dim(session_t *ps, const struct managed_win *w); void win_update_monitor(struct x_monitors *monitors, struct managed_win *mw); @@ -394,18 +388,16 @@ void restack_bottom(session_t *ps, struct win *w); void restack_top(session_t *ps, struct win *w); /** - * Execute fade callback of a window if fading finished. + * Release a destroyed window that is no longer needed. */ -bool must_use win_check_fade_finished(session_t *ps, struct managed_win *w); +void destroy_win_finish(session_t *ps, struct win *w); // Stop receiving events (except ConfigureNotify, XXX why?) from a window void win_ev_stop(session_t *ps, const struct win *w); /// Skip the current in progress fading of window, /// transition the window straight to its end state -/// -/// @return whether the window is destroyed and freed -bool must_use win_skip_fading(session_t *ps, struct managed_win *w); +void win_skip_fading(struct managed_win *w); /** * Find a managed window from window id in window linked list of the session. */ diff --git a/src/win_defs.h b/src/win_defs.h index dc96a0c5bb..a9d3b5fd4d 100644 --- a/src/win_defs.h +++ b/src/win_defs.h @@ -27,44 +27,18 @@ typedef enum { WMODE_SOLID, // The window is opaque including the frame } winmode_t; -/// Transition table: -/// (DESTROYED is when the win struct is destroyed and freed) -/// ('o' means in all other cases) -/// (Window is created in the UNMAPPED state) -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | |UNMAPPING|DESTROYING|MAPPING|FADING |UNMAPPED| MAPPED |DESTROYED| -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | UNMAPPING | o | Window |Window | - | Fading | - | - | -/// | | |destroyed |mapped | |finished| | | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | DESTROYING | - | o | - | - | - | - | Fading | -/// | | | | | | | |finished | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | MAPPING | Window | Window | o |Opacity| - | Fading | - | -/// | |unmapped |destroyed | |change | |finished| | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | FADING | Window | Window | - | o | - | Fading | - | -/// | |unmapped |destroyed | | | |finished| | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | UNMAPPED | - | - |Window | - | o | - | Window | -/// | | | |mapped | | | |destroyed| -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ -/// | MAPPED | Window | Window | - |Opacity| - | o | - | -/// | |unmapped |destroyed | |change | | | | -/// +-------------+---------+----------+-------+-------+--------+--------+---------+ +/// The state of a window from Xserver's perspective typedef enum { - // The window is being faded out because it's unmapped. - WSTATE_UNMAPPING, - // The window is being faded out because it's destroyed, - WSTATE_DESTROYING, - // The window is being faded in - WSTATE_MAPPING, - // Window opacity is not at the target level - WSTATE_FADING, - // The window is mapped, no fading is in progress. - WSTATE_MAPPED, - // The window is unmapped, no fading is in progress. + /// The window is unmapped. Equivalent to map-state == XCB_MAP_STATE_UNMAPPED WSTATE_UNMAPPED, + /// The window no longer exists on the X server. + WSTATE_DESTROYED, + /// The window is mapped and viewable. Equivalent to map-state == + /// XCB_MAP_STATE_VIEWABLE + WSTATE_MAPPED, + + // XCB_MAP_STATE_UNVIEWABLE is not represented here because it should not be + // possible for top-level windows. } winstate_t; enum win_flags {