diff --git a/picom.sample.conf b/picom.sample.conf index 777cf0d0..b7b10626 100644 --- a/picom.sample.conf +++ b/picom.sample.conf @@ -1,3 +1,42 @@ +################################# +# Transitions # +################################# + +# When windows get moved or resized it transitions window position +transition = true; + +# How many pixels move window to make the first position in transition (defaults to 20) +transition-offset = 20; + +# Direction of transition (top, right, bottom, left) e.g: "right" direction will make +# all windows come from right to left + +# (smart-x, smart-y) are smart direction that will check if there are +# multiple windows that splits the screen and will change their directions, +# in "smart-x" it changes direction of left window to "right" and direction of +# right window to "left", if screen is not splited and a window is taking +# a lot of screen it will change that window direction to "left". +# "smart-y" is also exactly like "smart-x" but instead of translating directions to +# "right" and "left", it translate to "top" and "bottom" +transition-direction = "smart-x"; + +# Function that calculates new position of window (defaults to "ease-out-cubic") +# see https://easings.net for list of all functions +# naming conventions are different to that site tho, e.g "easeInSine" is listed +# on site but here that translated to "ease-in-sine" +transition-timing-function = "ease-out-cubic"; + +# Time between frames in transition. (0.01 - 1.0, defaults to 0.028) +transition-step = 0.028; + +# Similar to opacity rules but determites transition direction e.g: +# "right: name *= 'Firefox'" will make firefox transition direction to right +# Specify a list of transition rules, in the format `DIRECTION:PATTERN` + +# for disabling transition on specific patterns use "none" keyword as a direction +# e.g: use "none: window_type = 'popup_menu'" for disabling transitions on popup menus +transition-rule = []; + ################################# # Shadows # ################################# diff --git a/src/config.c b/src/config.c index 90324778..ae400c1b 100644 --- a/src/config.c +++ b/src/config.c @@ -579,6 +579,11 @@ char *parse_config(options_t *opt, const char *config_file, bool *shadow_enable, .track_leader = false, + .transition_offset = 20, + .transition_direction = 0, + .transition_step = 0.028, + .transition_timing_function = ease_out_cubic, + .rounded_corners_blacklist = NULL }; // clang-format on diff --git a/src/config.h b/src/config.h index 03ef74b3..4cefb4c1 100644 --- a/src/config.h +++ b/src/config.h @@ -26,6 +26,8 @@ #include "types.h" #include "win_defs.h" +#include "timing_functions.h" + typedef struct session session_t; /// @brief Possible backends @@ -68,6 +70,18 @@ enum blur_method { BLUR_METHOD_INVALID, }; +enum transition_direction { + TRANSITION_DIR_NONE = 0, + TRANSITION_DIR_LEFT, + TRANSITION_DIR_BOTTOM, + TRANSITION_DIR_RIGHT, + TRANSITION_DIR_TOP, + TRANSITION_DIR_SMART_X, + TRANSITION_DIR_SMART_Y, +}; + +typedef double (*timing_function)(double); + typedef struct _c2_lptr c2_lptr_t; /// Structure representing all options. @@ -246,6 +260,22 @@ typedef struct options { // Make transparent windows clip other windows, instead of blending on top of // them bool transparent_clipping; + + // === Transition === + // How many pixels move window to make the first position in transition + int transition_offset; + + // Direction of transition + enum transition_direction transition_direction; + + // Rules to change window transition + c2_lptr_t *transition_rules; + + // Function that calculate new position + timing_function transition_timing_function; + + // Time between frames in transition + double transition_step; } options_t; extern const char *const BACKEND_STRS[NUM_BKEND + 1]; diff --git a/src/config_libconfig.c b/src/config_libconfig.c index e9818ebc..60e72a86 100644 --- a/src/config_libconfig.c +++ b/src/config_libconfig.c @@ -295,6 +295,90 @@ static inline void parse_wintype_config(const config_t *cfg, const char *member_ } } +enum transition_direction parse_transition_direction(const char *direction) { + static const char *names[] = {"none", "left", "bottom", "right", + "top", "smart-x", "smart-y"}; + + for (unsigned int i = 0; i < sizeof(names) / sizeof(char *); i++) { + if (strcmp(direction, names[i]) == 0) { + return i; + } + } + + log_error("'%s' is not a valid transition direction.", direction); + return TRANSITION_DIR_NONE; +} + +timing_function parse_timing_function(const char *timing_name) { + // clang-format off + static const char *names[] = { + "sine", "cubic", "quint", "circ", "elastic", + "quad", "quart", "etpo", "back", "bounce" + }; + + static const char *prefixes[] = {"in", "out", "in-out"}; + + static timing_function functions[] = { + ease_in_sine , ease_out_sine , ease_in_out_sine , + ease_in_cubic , ease_out_cubic , ease_in_out_cubic , + ease_in_quint , ease_out_quint , ease_in_out_quint , + ease_in_circ , ease_out_circ , ease_in_out_circ , + ease_in_elastic, ease_out_elastic, ease_in_out_elastic, + ease_in_quad , ease_out_quad , ease_in_out_quad , + ease_in_quart , ease_out_quart , ease_in_out_quart , + ease_in_etpo , ease_out_etpo , ease_in_out_etpo , + ease_in_back , ease_out_back , ease_in_out_back , + ease_in_bounce , ease_out_bounce , ease_in_out_bounce , + }; + // clang-format on + + char buffer[64]; + for (unsigned int i = 0; i < sizeof(names) / sizeof(char *); i++) { + for (unsigned int p = 0; p < 3; p++) { + snprintf(buffer, sizeof(buffer), "ease-%s-%s", prefixes[p], names[i]); + + if (strcmp(buffer, timing_name) == 0) { + unsigned int function_index = (i * 3) + p; + return functions[function_index]; + } + } + } + + log_error("'%s' is not a valid transition timing function.", timing_name); + return NULL; +} + +static inline void +parse_cfg_condlst_trns(options_t *opt, const config_t *pcfg, const char *name) { + config_setting_t *setting = config_lookup(pcfg, name); + if (setting) { + int length = config_setting_length(setting); + + for (int i = 0; i < length; i++) { + const char *elem = config_setting_get_string_elem(setting, i); + + char rule[512]; + unsigned long elem_index = 0; + + for (int rule_index = 0; elem_index < strlen(elem); elem_index++) { + char character = elem[elem_index]; + if (character == ':') { + rule[rule_index] = '\0'; + break; + } + + if (!isspace(character)) { + rule[rule_index] = character; + rule_index++; + } + } + + int *direction = (int *)parse_transition_direction(rule); + c2_parse(&opt->transition_rules, &elem[elem_index + 1], direction); + } + } +} + /** * Parse a configuration file from default location. * @@ -642,6 +726,28 @@ char *parse_config_libconfig(options_t *opt, const char *config_file, bool *shad opt->write_pid_path = strdup(sval); } + // Transition + if (lcfg_lookup_bool(&cfg, "transition", &bval)) { + if (bval) { + config_lookup_int(&cfg, "transition-offset", &opt->transition_offset); + config_lookup_float(&cfg, "transition-step", &opt->transition_step); + + if (config_lookup_string(&cfg, "transition-direction", &sval)) { + opt->transition_direction = parse_transition_direction(sval); + } + + if (config_lookup_string(&cfg, "transition-timing-function", &sval)) { + timing_function res = parse_timing_function(sval); + + if (res != NULL) { + opt->transition_timing_function = res; + } + } + + parse_cfg_condlst_trns(opt, &cfg, "transition-rule"); + } + } + // Wintype settings // XXX ! Refactor all the wintype_* arrays into a struct diff --git a/src/event.c b/src/event.c index e6052f1d..f9ca356a 100644 --- a/src/event.c +++ b/src/event.c @@ -186,6 +186,10 @@ static inline void ev_create_notify(session_t *ps, xcb_create_notify_event_t *ev } } +static inline unsigned int distance(int x1, int x2, int y1, int y2) { + return (unsigned int)(abs(x2 - x1) + abs(y2 - y1)); +} + /// Handle configure event of a regular window static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { auto w = find_win(ps, ce->window); @@ -217,6 +221,15 @@ static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { // visible/mapped ps->pending_updates = true; + static const unsigned int small_diff = 100; + bool small_move = + position_changed && + distance(mw->pending_g.x, ce->x, mw->pending_g.y, ce->y) < small_diff; + + bool small_resize = + size_changed && distance(mw->pending_g.width, ce->width, + mw->pending_g.height, ce->height) < small_diff; + // At least one of the following if's is true if (position_changed) { log_trace("Window position changed, %dx%d -> %dx%d", mw->g.x, @@ -235,6 +248,20 @@ static void configure_win(session_t *ps, xcb_configure_notify_event_t *ce) { win_set_flags(mw, WIN_FLAGS_SIZE_STALE); } + if (mw->transition_direction != TRANSITION_DIR_NONE) { + // Dont't transition windows that wanna go out of screen + if (ce->x >= 0 && ce->x <= ps->root_width) { + mw->transition_time = 0.0f; + mw->target_geometry = mw->pending_g; + } else { + mw->transition_time = -1.0f; + } + } + + if (mw->transition_time != -1.0f && (small_move || small_resize)) { + mw->transition_time = -1.0f; + } + // Recalculate which screen this window is on win_update_screen(ps->xinerama_nscrs, ps->xinerama_scr_regs, mw); } diff --git a/src/meson.build b/src/meson.build index 0a882f93..341df783 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,7 +9,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') ] + 'options.c', 'event.c', 'cache.c', 'atom.c', 'file_watch.c', 'timing_functions.c') ] picom_inc = include_directories('.') cflags = [] diff --git a/src/picom.c b/src/picom.c index 81fb334d..2c9a42ad 100644 --- a/src/picom.c +++ b/src/picom.c @@ -680,6 +680,51 @@ static struct managed_win *paint_preprocess(session_t *ps, bool *fade_running) { if (was_painted && w->mode != mode_old) { w->reg_ignore_valid = false; } + + // Transition + bool valid_trns_time = w->transition_time >= 0.0f && w->transition_time <= 1.0f; + if (w->transition_direction && valid_trns_time) { + double transition = + ps->o.transition_timing_function(w->transition_time); + + w->transition_time += ps->o.transition_step; + if (w->transition_time > 1.0f) + transition = 1.0f; + + add_damage_from_win(ps, w); + unsigned int direction = w->transition_direction; + + // Smart direction + if (direction == TRANSITION_DIR_SMART_X || + direction == TRANSITION_DIR_SMART_Y) { + + bool wide_enough = w->g.width > 80 * ps->root_width / 100; + bool bigger_than_half = + w->target_geometry.x > ps->root_width / 2; + + /* + Not changing transition_direction because + smart calculation have to be calculated each time + */ + direction = w->transition_direction - + ((bigger_than_half || wide_enough) ? 4 : 2); + } + + // Determite we are working on x or y of window + int8_t xy = !(direction % 2); + + int16_t xy_target = *(((int16_t *)&w->target_geometry) + xy); + int16_t *xy_source = ((int16_t *)&w->g) + xy; + + int8_t sign = (direction - 1) % 3 ? 1 : -1; + int start_location = xy_target + sign * ps->o.transition_offset; + + *xy_source = (int16_t)round( + transition * (xy_target - start_location) + start_location); + + w->mode = WMODE_TRANS; + *fade_running = true; + } } // Opacity will not change, from now on. @@ -1804,6 +1849,7 @@ static session_t *session_init(int argc, char **argv, Display *dpy, c2_list_postprocess(ps, ps->o.invert_color_list) && c2_list_postprocess(ps, ps->o.opacity_rules) && c2_list_postprocess(ps, ps->o.rounded_corners_blacklist) && + c2_list_postprocess(ps, ps->o.transition_rules) && c2_list_postprocess(ps, ps->o.focus_blacklist))) { log_error("Post-processing of conditionals failed, some of your rules " "might not work"); @@ -2174,6 +2220,7 @@ static void session_destroy(session_t *ps) { free_wincondlst(&ps->o.paint_blacklist); free_wincondlst(&ps->o.unredir_if_possible_blacklist); free_wincondlst(&ps->o.rounded_corners_blacklist); + free_wincondlst(&ps->o.transition_rules); // Free tracked atom list { diff --git a/src/timing_functions.c b/src/timing_functions.c new file mode 100644 index 00000000..a6424bd2 --- /dev/null +++ b/src/timing_functions.c @@ -0,0 +1,178 @@ +#include "timing_functions.h" +#include + +// clang-format off +double ease_in_sine(double t) { + return 1 - cos((t * M_PI) / 2); +} + +double ease_out_sine(double t) { + return sin((t * M_PI) / 2); +} + +double ease_in_out_sine(double t) { + return -(cos(M_PI * t) - 1) / 2; +} + +double ease_in_cubic(double t) { + return t * t * t; +} + +double ease_out_cubic(double t) { + return 1 - pow(1 - t, 3); +} + +double ease_in_out_cubic(double t) { + return t < 0.5 ? 4 * t * t * t : 1 - pow(-2 * t + 2, 3) / 2; +} + +double ease_in_quint(double t) { + return t * t * t * t * t; +} + +double ease_out_quint(double t) { + return 1 - pow(1 - t, 5); +} + +double ease_in_out_quint(double t) { + return t < 0.5 ? 16 * t * t * t * t * t : 1 - pow(-2 * t + 2, 5) / 2; +} + +double ease_in_circ(double t) { + return 1 - sqrt(1 - pow(t, 2)); +} + +double ease_out_circ(double t) { + return sqrt(1 - pow(t - 1, 2)); +} + +double ease_in_out_circ(double t) { + return t < 0.5 + ? (1 - sqrt(1 - pow(2 * t, 2))) / 2 + : (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2; +} + +double ease_in_elastic(double t) { + double c4 = (2 * M_PI) / 3; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : -pow(2, 10 * t - 10) * sin((t * 10 - 10.75) * c4); +} + +double ease_out_elastic(double t) { + double c4 = (2 * M_PI) / 3; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : pow(2, -10 * t) * sin((t * 10 - 0.75) * c4) + 1; +} + +double ease_in_out_elastic(double t) { + double c5 = (2 * M_PI) / 4.5; + + return t == 0 + ? 0 + : t == 1 + ? 1 + : t < 0.5 + ? -(pow(2, 20 * t - 10) * sin((20 * t - 11.125) * c5)) / 2 + : (pow(2, -20 * t + 10) * sin((20 * t - 11.125) * c5)) / 2 + 1; +} + +double ease_in_quad(double t) { + return t * t; +} + +double ease_out_quad(double t) { + return 1 - (1 - t) * (1 - t); +} + +double ease_in_out_quad(double t) { + return t < 0.5 ? 2 * t * t : 1 - pow(-2 * t + 2, 2) / 2; +} + +double ease_in_quart(double t) { + return t * t * t * t; +} + +double ease_out_quart(double t) { + return 1 - pow(1 - t, 4); +} + +double ease_in_out_quart(double t) { + return t < 0.5 ? 8 * t * t * t * t : 1 - pow(-2 * t + 2, 4) / 2; +} + +double ease_in_etpo(double t) { + return t == 0 ? 0 : pow(2, 10 * t - 10); +} + +double ease_out_etpo(double t) { + return t == 1 ? 1 : 1 - pow(2, -10 * t); +} + +double ease_in_out_etpo(double t) { + return t == 0 + ? 0 + : t == 1 + ? 1 + : t < 0.5 ? pow(2, 20 * t - 10) / 2 + : (2 - pow(2, -20 * t + 10)) / 2; +} + +double ease_in_back(double t) { + double c1 = 1.70158; + double c3 = c1 + 1; + + return c3 * t * t * t - c1 * t * t; +} + +double ease_out_back(double t) { + double c1 = 1.70158; + double c3 = c1 + 1; + + return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2); +} + +double ease_in_out_back(double t) { + double c1 = 1.70158; + double c2 = c1 * 1.525; + + return t < 0.5 + ? (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 + : (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2; +} + +double ease_in_bounce(double t) { + return 1 - ease_out_bounce(1 - t); +} + +double ease_out_bounce(double t) { + double n1 = 7.5625; + double d1 = 2.75; + + if (t < 1 / d1) { + return n1 * t * t; + } else if (t < 2 / d1) { + t -= 1.5 / d1; + return n1 * t * t + 0.75; + } else if (t < 2.5 / d1) { + t -= 2.25 / d1; + return n1 * t * t + 0.9375; + } else { + t -= 2.625 / d1; + return n1 * t * t + 0.984375; + } +} + +double ease_in_out_bounce(double t) { + return t < 0.5 + ? (1 - ease_out_bounce(1 - 2 * t)) / 2 + : (1 + ease_out_bounce(2 * t - 1)) / 2; +} +// clang-format on \ No newline at end of file diff --git a/src/timing_functions.h b/src/timing_functions.h new file mode 100644 index 00000000..8e6776e3 --- /dev/null +++ b/src/timing_functions.h @@ -0,0 +1,46 @@ +/* + all functions and formulas gathered from + https://easings.net/ +*/ + +#pragma once + +double ease_in_sine(double t); +double ease_out_sine(double t); +double ease_in_out_sine(double t); + +double ease_in_cubic(double t); +double ease_out_cubic(double t); +double ease_in_out_cubic(double t); + +double ease_in_quint(double t); +double ease_out_quint(double t); +double ease_in_out_quint(double t); + +double ease_in_circ(double t); +double ease_out_circ(double t); +double ease_in_out_circ(double t); + +double ease_in_elastic(double t); +double ease_out_elastic(double t); +double ease_in_out_elastic(double t); + +double ease_in_quad(double t); +double ease_out_quad(double t); +double ease_in_out_quad(double t); + +double ease_in_quart(double t); +double ease_out_quart(double t); +double ease_in_out_quart(double t); + +double ease_in_etpo(double t); +double ease_out_etpo(double t); +double ease_in_out_etpo(double t); + +double ease_in_back(double t); +double ease_out_back(double t); +double ease_in_out_back(double t); + +double ease_out_bounce(double t); +double ease_in_bounce(double t); +double ease_in_out_bounce(double t); \ No newline at end of file diff --git a/src/win.c b/src/win.c index 7bdb05f5..5171284f 100644 --- a/src/win.c +++ b/src/win.c @@ -1118,6 +1118,14 @@ void win_update_opacity_rule(session_t *ps, struct managed_win *w) { w->opacity_is_set = is_set; } +void win_update_transition_rule(session_t *ps, struct managed_win *w) { + void *val; + if (c2_match(ps, w, ps->o.transition_rules, &val)) { + // uses multiple casters to trick compiler to not give warnings + w->transition_direction = (unsigned int)(long)val; + } +} + /** * Function to be called on window data changes. * @@ -1129,6 +1137,7 @@ void win_on_factor_change(session_t *ps, struct managed_win *w) { // state of the window win_update_focused(ps, w); + win_update_transition_rule(ps, w); win_determine_shadow(ps, w); win_determine_clip_shadow_above(ps, w); win_determine_invert_color(ps, w); @@ -1497,6 +1506,8 @@ struct win *fill_win(session_t *ps, struct win *w) { .shadow_paint = PAINT_INIT, .corner_radius = 0, + + .transition_time = -1.0f, }; assert(!w->destroyed); @@ -1568,6 +1579,8 @@ struct win *fill_win(session_t *ps, struct win *w) { .border_width = g->border_width, }; + new->transition_direction = ps->o.transition_direction; + free(g); // Create Damage for window (if not Input Only) diff --git a/src/win.h b/src/win.h index 6c6ae323..2a5779c0 100644 --- a/src/win.h +++ b/src/win.h @@ -270,6 +270,15 @@ struct managed_win { /// Whether to blur window background. bool blur_background; + /// Transition time that used as input in timing function + double transition_time; + + /// Save new geometry for calculating transition + struct win_geometry target_geometry; + + /// Transition direction of window + enum transition_direction transition_direction; + #ifdef CONFIG_OPENGL /// Textures and FBO background blur use. glx_blur_cache_t glx_blur_cache;