feat: add smooth focus transition animation for opacity and border

This adds animated transitions when switching focus between windows.
Both window opacity and border color now fade smoothly using cubic
bezier easing instead of changing instantly.

Implementation:
- Added animation_duration_focus config option (default 400ms)
- Added animation_curve_focus for cubic bezier easing curve
- Window opacity and border color animate together when focus changes
- Uses existing animation infrastructure (baked bezier points)

The feature is backwards compatible and can be disabled by setting
animation_duration_focus=0 in config file.

Changes affect 5 files with minimal additions to keep code clean.
This commit is contained in:
Szymon Rączka 2025-10-31 22:33:38 +01:00 committed by DreamMaoMao
parent cbfd20bff8
commit e0f114af59
5 changed files with 86 additions and 7 deletions

View file

@ -932,12 +932,35 @@ bool client_draw_frame(Client *c) {
if (!c || !client_surface(c)->mapped) if (!c || !client_surface(c)->mapped)
return false; return false;
// Animate focus transitions (opacity + border color)
if (c->isfullscreen) { if (c->isfullscreen) {
client_set_opacity(c, 1); client_set_opacity(c, 1);
} else if (c == selmon->sel && !c->animation.running) { c->current_opacity = 1;
client_set_opacity(c, c->focused_opacity); c->target_opacity = 1;
} else if (!c->animation.running) { } else if (c->opacity_animation_frames > 0 && c->opacity_animation_passed < c->opacity_animation_frames) {
client_set_opacity(c, c->unfocused_opacity); float linear_progress = (float)c->opacity_animation_passed / c->opacity_animation_frames;
float eased_progress = find_animation_curve_at(linear_progress, FOCUS);
// Animate opacity
float opacity_start = (c->target_opacity == c->focused_opacity) ? c->unfocused_opacity : c->focused_opacity;
c->current_opacity = opacity_start + (c->target_opacity - opacity_start) * eased_progress;
client_set_opacity(c, c->current_opacity);
// Animate border color
bool focusing = (c->target_border_color[0] == focuscolor[0]);
float *border_start = focusing ? bordercolor : focuscolor;
for (int i = 0; i < 4; i++) {
c->current_border_color[i] = border_start[i] + (c->target_border_color[i] - border_start[i]) * eased_progress;
}
client_set_border_color(c, c->current_border_color);
c->opacity_animation_passed++;
} else {
// Animation complete or disabled - apply target values
c->current_opacity = c->target_opacity;
client_set_opacity(c, c->current_opacity);
memcpy(c->current_border_color, c->target_border_color, sizeof(c->current_border_color));
client_set_border_color(c, c->current_border_color);
} }
if (!c->need_output_flush) if (!c->need_output_flush)

View file

@ -9,6 +9,8 @@ struct dvec2 calculate_animation_curve_at(double t, int type) {
animation_curve = animation_curve_tag; animation_curve = animation_curve_tag;
} else if (type == CLOSE) { } else if (type == CLOSE) {
animation_curve = animation_curve_close; animation_curve = animation_curve_close;
} else if (type == FOCUS) {
animation_curve = animation_curve_focus;
} else { } else {
animation_curve = animation_curve_move; animation_curve = animation_curve_move;
} }
@ -28,6 +30,8 @@ void init_baked_points(void) {
baked_points_tag = calloc(BAKED_POINTS_COUNT, sizeof(*baked_points_tag)); baked_points_tag = calloc(BAKED_POINTS_COUNT, sizeof(*baked_points_tag));
baked_points_close = baked_points_close =
calloc(BAKED_POINTS_COUNT, sizeof(*baked_points_close)); calloc(BAKED_POINTS_COUNT, sizeof(*baked_points_close));
baked_points_focus =
calloc(BAKED_POINTS_COUNT, sizeof(*baked_points_focus));
for (unsigned int i = 0; i < BAKED_POINTS_COUNT; i++) { for (unsigned int i = 0; i < BAKED_POINTS_COUNT; i++) {
baked_points_move[i] = calculate_animation_curve_at( baked_points_move[i] = calculate_animation_curve_at(
@ -45,6 +49,10 @@ void init_baked_points(void) {
baked_points_close[i] = calculate_animation_curve_at( baked_points_close[i] = calculate_animation_curve_at(
(double)i / (BAKED_POINTS_COUNT - 1), CLOSE); (double)i / (BAKED_POINTS_COUNT - 1), CLOSE);
} }
for (unsigned int i = 0; i < BAKED_POINTS_COUNT; i++) {
baked_points_focus[i] = calculate_animation_curve_at(
(double)i / (BAKED_POINTS_COUNT - 1), FOCUS);
}
} }
double find_animation_curve_at(double t, int type) { double find_animation_curve_at(double t, int type) {
@ -61,6 +69,8 @@ double find_animation_curve_at(double t, int type) {
baked_points = baked_points_tag; baked_points = baked_points_tag;
} else if (type == CLOSE) { } else if (type == CLOSE) {
baked_points = baked_points_close; baked_points = baked_points_close;
} else if (type == FOCUS) {
baked_points = baked_points_focus;
} else { } else {
baked_points = baked_points_move; baked_points = baked_points_move;
} }

View file

@ -176,10 +176,12 @@ typedef struct {
uint32_t animation_duration_open; uint32_t animation_duration_open;
uint32_t animation_duration_tag; uint32_t animation_duration_tag;
uint32_t animation_duration_close; uint32_t animation_duration_close;
uint32_t animation_duration_focus;
double animation_curve_move[4]; double animation_curve_move[4];
double animation_curve_open[4]; double animation_curve_open[4];
double animation_curve_tag[4]; double animation_curve_tag[4];
double animation_curve_close[4]; double animation_curve_close[4];
double animation_curve_focus[4];
int scroller_structs; int scroller_structs;
float scroller_default_proportion; float scroller_default_proportion;
@ -1113,6 +1115,8 @@ void parse_option(Config *config, char *key, char *value) {
config->animation_duration_tag = atoi(value); config->animation_duration_tag = atoi(value);
} else if (strcmp(key, "animation_duration_close") == 0) { } else if (strcmp(key, "animation_duration_close") == 0) {
config->animation_duration_close = atoi(value); config->animation_duration_close = atoi(value);
} else if (strcmp(key, "animation_duration_focus") == 0) {
config->animation_duration_focus = atoi(value);
} else if (strcmp(key, "animation_curve_move") == 0) { } else if (strcmp(key, "animation_curve_move") == 0) {
int num = parse_double_array(value, config->animation_curve_move, 4); int num = parse_double_array(value, config->animation_curve_move, 4);
if (num != 4) { if (num != 4) {
@ -1138,6 +1142,13 @@ void parse_option(Config *config, char *key, char *value) {
"Error: Failed to parse animation_curve_close: %s\n", "Error: Failed to parse animation_curve_close: %s\n",
value); value);
} }
} else if (strcmp(key, "animation_curve_focus") == 0) {
int num = parse_double_array(value, config->animation_curve_focus, 4);
if (num != 4) {
fprintf(stderr,
"Error: Failed to parse animation_curve_focus: %s\n",
value);
}
} else if (strcmp(key, "scroller_structs") == 0) { } else if (strcmp(key, "scroller_structs") == 0) {
config->scroller_structs = atoi(value); config->scroller_structs = atoi(value);
} else if (strcmp(key, "scroller_default_proportion") == 0) { } else if (strcmp(key, "scroller_default_proportion") == 0) {
@ -2527,6 +2538,8 @@ void override_config(void) {
animation_duration_tag = CLAMP_INT(config.animation_duration_tag, 1, 50000); animation_duration_tag = CLAMP_INT(config.animation_duration_tag, 1, 50000);
animation_duration_close = animation_duration_close =
CLAMP_INT(config.animation_duration_close, 1, 50000); CLAMP_INT(config.animation_duration_close, 1, 50000);
animation_duration_focus =
CLAMP_INT(config.animation_duration_focus, 1, 50000);
// 滚动布局设置 // 滚动布局设置
scroller_default_proportion = scroller_default_proportion =
@ -2640,6 +2653,8 @@ void override_config(void) {
sizeof(animation_curve_tag)); sizeof(animation_curve_tag));
memcpy(animation_curve_close, config.animation_curve_close, memcpy(animation_curve_close, config.animation_curve_close,
sizeof(animation_curve_close)); sizeof(animation_curve_close));
memcpy(animation_curve_focus, config.animation_curve_focus,
sizeof(animation_curve_focus));
} }
void set_value_default() { void set_value_default() {
@ -2662,6 +2677,8 @@ void set_value_default() {
animation_duration_tag; // Animation tag speed animation_duration_tag; // Animation tag speed
config.animation_duration_close = config.animation_duration_close =
animation_duration_close; // Animation tag speed animation_duration_close; // Animation tag speed
config.animation_duration_focus =
animation_duration_focus; // Animation focus opacity speed
/* appearance */ /* appearance */
config.axis_bind_apply_timeout = config.axis_bind_apply_timeout =
@ -2760,6 +2777,8 @@ void set_value_default() {
sizeof(animation_curve_tag)); sizeof(animation_curve_tag));
memcpy(config.animation_curve_close, animation_curve_close, memcpy(config.animation_curve_close, animation_curve_close,
sizeof(animation_curve_close)); sizeof(animation_curve_close));
memcpy(config.animation_curve_focus, animation_curve_focus,
sizeof(animation_curve_focus));
memcpy(config.rootcolor, rootcolor, sizeof(rootcolor)); memcpy(config.rootcolor, rootcolor, sizeof(rootcolor));
memcpy(config.bordercolor, bordercolor, sizeof(bordercolor)); memcpy(config.bordercolor, bordercolor, sizeof(bordercolor));

View file

@ -25,10 +25,12 @@ uint32_t animation_duration_move = 500; // Animation move speed
uint32_t animation_duration_open = 400; // Animation open speed uint32_t animation_duration_open = 400; // Animation open speed
uint32_t animation_duration_tag = 300; // Animation tag speed uint32_t animation_duration_tag = 300; // Animation tag speed
uint32_t animation_duration_close = 300; // Animation close speed uint32_t animation_duration_close = 300; // Animation close speed
uint32_t animation_duration_focus = 400; // Animation focus opacity speed
double animation_curve_move[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线 double animation_curve_move[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线
double animation_curve_open[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线 double animation_curve_open[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线
double animation_curve_tag[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线 double animation_curve_tag[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线
double animation_curve_close[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线 double animation_curve_close[4] = {0.46, 1.0, 0.29, 0.99}; // 动画曲线
double animation_curve_focus[4] = {0.08, 0.82, 0.17, 1}; // 动画曲线
/* appearance */ /* appearance */
unsigned int axis_bind_apply_timeout = 100; // 滚轮绑定动作的触发的时间间隔 unsigned int axis_bind_apply_timeout = 100; // 滚轮绑定动作的触发的时间间隔

View file

@ -157,7 +157,7 @@ enum {
}; /* EWMH atoms */ }; /* EWMH atoms */
#endif #endif
enum { UP, DOWN, LEFT, RIGHT, UNDIR }; /* smartmovewin */ enum { UP, DOWN, LEFT, RIGHT, UNDIR }; /* smartmovewin */
enum { NONE, OPEN, MOVE, CLOSE, TAG }; enum { NONE, OPEN, MOVE, CLOSE, TAG, FOCUS };
enum { UNFOLD, FOLD, INVALIDFOLD }; enum { UNFOLD, FOLD, INVALIDFOLD };
enum { PREV, NEXT }; enum { PREV, NEXT };
@ -339,6 +339,12 @@ struct Client {
int isunglobal; int isunglobal;
float focused_opacity; float focused_opacity;
float unfocused_opacity; float unfocused_opacity;
float current_opacity;
float target_opacity;
unsigned int opacity_animation_frames;
unsigned int opacity_animation_passed;
float current_border_color[4];
float target_border_color[4];
char oldmonname[128]; char oldmonname[128];
struct wlr_ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel; struct wlr_ext_foreign_toplevel_handle_v1 *ext_foreign_toplevel;
double master_mfact_per, master_inner_per, stack_innder_per; double master_mfact_per, master_inner_per, stack_innder_per;
@ -787,6 +793,7 @@ struct dvec2 *baked_points_move;
struct dvec2 *baked_points_open; struct dvec2 *baked_points_open;
struct dvec2 *baked_points_tag; struct dvec2 *baked_points_tag;
struct dvec2 *baked_points_close; struct dvec2 *baked_points_close;
struct dvec2 *baked_points_focus;
static struct wl_event_source *hide_source; static struct wl_event_source *hide_source;
static bool cursor_hidden = false; static bool cursor_hidden = false;
@ -3036,7 +3043,13 @@ void focusclient(Client *c, int lift) {
// change border color // change border color
c->isurgent = 0; c->isurgent = 0;
setborder_color(c); // Start border color animation to focused
memcpy(c->target_border_color, focuscolor, sizeof(c->target_border_color));
// Start opacity animation to focused
c->target_opacity = c->focused_opacity;
c->opacity_animation_frames = (animation_duration_focus * 60) / 1000; // 60fps
c->opacity_animation_passed = 0;
} }
if (c && !c->iskilling && c->foreign_toplevel) if (c && !c->iskilling && c->foreign_toplevel)
@ -3064,7 +3077,13 @@ void focusclient(Client *c, int lift) {
* probably other clients */ * probably other clients */
} else if (w && !client_is_unmanaged(w) && } else if (w && !client_is_unmanaged(w) &&
(!c || !client_wants_focus(c))) { (!c || !client_wants_focus(c))) {
setborder_color(w); // Start border color animation to unfocused
memcpy(w->target_border_color, bordercolor, sizeof(w->target_border_color));
// Start opacity animation to unfocused
w->target_opacity = w->unfocused_opacity;
w->opacity_animation_frames = (animation_duration_focus * 60) / 1000; // 60fps
w->opacity_animation_passed = 0;
client_activate_surface(old_keyboard_focus_surface, 0); client_activate_surface(old_keyboard_focus_surface, 0);
} }
@ -3420,6 +3439,12 @@ void init_client_properties(Client *c) {
c->fake_no_border = false; c->fake_no_border = false;
c->focused_opacity = focused_opacity; c->focused_opacity = focused_opacity;
c->unfocused_opacity = unfocused_opacity; c->unfocused_opacity = unfocused_opacity;
c->current_opacity = unfocused_opacity;
c->target_opacity = unfocused_opacity;
c->opacity_animation_frames = 0;
c->opacity_animation_passed = 0;
memcpy(c->current_border_color, bordercolor, sizeof(c->current_border_color));
memcpy(c->target_border_color, bordercolor, sizeof(c->target_border_color));
c->nofadein = 0; c->nofadein = 0;
c->nofadeout = 0; c->nofadeout = 0;
c->no_force_center = 0; c->no_force_center = 0;