Skip to content

Commit a87e907

Browse files
committed
feat: various improvements to the scroller layout
1 parent d87b2fb commit a87e907

5 files changed

Lines changed: 169 additions & 3 deletions

File tree

examples/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,22 @@ doorsctl config scroller_ignore_proportion_single true|false
178178

179179
When true, proportion settings are ignored for single-window scroller desktops.
180180

181+
```
182+
doorsctl config scroller_center_focused_column never|on_overflow|always
183+
```
184+
185+
Controls how the focused column is positioned in the scroller viewport.
186+
187+
- **never**: The column is left-aligned (view offset stays at 0).
188+
- **on_overflow** (default): Columns narrower than the viewport are left-aligned; columns wider than the viewport are centered so their middle is visible.
189+
- **always**: The focused column is always centered in the viewport.
190+
191+
```
192+
doorsctl config scroller_always_center_single_column true|false
193+
```
194+
195+
When true (default), the single column on a one-column desktop is centered regardless of `scroller_center_focused_column`. Only applies when the column is narrower than the viewport.
196+
181197
```
182198
doorsctl config scroller_structs <n>
183199
```
@@ -893,6 +909,8 @@ doorsctl config enable_animations [true|false]
893909
doorsctl config edge_scroller_pointer_focus [true|false]
894910
doorsctl config scroller_default_proportion [<value>]
895911
doorsctl config scroller_proportion_preset [<values>]
912+
doorsctl config scroller_center_focused_column [never|on_overflow|always]
913+
doorsctl config scroller_always_center_single_column [true|false]
896914
doorsctl config animation_bezier [<name>]
897915
doorsctl config animation_duration [<ms>]
898916
doorsctl config animation <type> [bezier|duration|spring|enabled] [<value>]

include/scroller.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "types.h"
44

55
#include <stdbool.h>
6+
#include <time.h>
67
#include <wlr/util/box.h>
78

89
struct output_t;
@@ -43,6 +44,14 @@ typedef struct scroller_state_t {
4344
scroller_column_t *columns;
4445
int column_count, capacity, active_column_idx;
4546
double view_offset;
47+
bool view_offset_dirty;
48+
struct {
49+
bool active;
50+
double from;
51+
double target;
52+
struct timespec start;
53+
uint64_t duration_ms;
54+
} view_anim;
4655
struct wlr_box working_area;
4756
} scroller_state_t;
4857

@@ -65,6 +74,7 @@ bool scroller_consume_into_column(desktop_t *d);
6574
bool scroller_expel_from_column(desktop_t *d);
6675

6776
bool scroller_is_tiled(const client_t *c);
77+
bool scroller_tick(struct output_t *output, struct timespec now);
6878
void scroller_apply_client_rules(client_t *c, float rule_proportion, float rule_proportion_single);
6979

7080
void scroller_resize_width(client_t *client, float delta);
@@ -84,3 +94,12 @@ extern bool scroller_ignore_proportion_single;
8494
extern bool edge_scroller_pointer_focus;
8595
extern float *scroller_proportion_preset;
8696
extern int scroller_proportion_preset_count;
97+
98+
enum scroller_center_mode {
99+
SCROLLER_CENTER_NEVER = 0,
100+
SCROLLER_CENTER_ON_OVERFLOW = 1,
101+
SCROLLER_CENTER_ALWAYS = 2,
102+
};
103+
104+
extern int scroller_center_focused_column;
105+
extern bool scroller_always_center_single_column;

src/ipc_cmd/config.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,32 @@ void ipc_cmd_config(char **args, int num, int client_fd) {
297297
ipc_handle_bool(args, num, client_fd, &scroller_prefer_overspread, IPC_FLAG_NONE);
298298
} else if (streq("scroller_ignore_proportion_single", *args)) {
299299
ipc_handle_bool(args, num, client_fd, &scroller_ignore_proportion_single, IPC_FLAG_NONE);
300+
} else if (streq("scroller_center_focused_column", *args)) {
301+
if (num >= 2) {
302+
if (strcmp(args[1], "never") == 0)
303+
scroller_center_focused_column = SCROLLER_CENTER_NEVER;
304+
else if (strcmp(args[1], "on_overflow") == 0)
305+
scroller_center_focused_column = SCROLLER_CENTER_ON_OVERFLOW;
306+
else if (strcmp(args[1], "always") == 0)
307+
scroller_center_focused_column = SCROLLER_CENTER_ALWAYS;
308+
else {
309+
send_failure(client_fd,
310+
"scroller_center_focused_column: must be 'never', 'on_overflow', or 'always'\n");
311+
return;
312+
}
313+
send_success(client_fd, "scroller_center_focused_column set\n");
314+
} else {
315+
const char *names[] = {
316+
"never",
317+
"on_overflow",
318+
"always"
319+
};
320+
char buf[64];
321+
snprintf(buf, sizeof(buf), "%s\n", names[scroller_center_focused_column]);
322+
send_success(client_fd, buf);
323+
}
324+
} else if (streq("scroller_always_center_single_column", *args)) {
325+
ipc_handle_bool(args, num, client_fd, &scroller_always_center_single_column, IPC_FLAG_NONE);
300326
} else if (streq("scroller_structs", *args)) {
301327
ipc_handle_int(args, num, client_fd, &scroller_structs, IPC_FLAG_NONE, 0, 1000000, NULL);
302328
} else if (streq("focus_follows_pointer", *args) || streq("focus_follows_mouse", *args)) {

src/output.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "lock.h"
88
#include "output.h"
99
#include "output_config.h"
10+
#include "scroller.h"
1011
#include "server.h"
1112
#include "toplevel.h"
1213
#include "tree.h"
@@ -228,6 +229,10 @@ static void output_repaint(output_t *output) {
228229
clock_gettime(CLOCK_MONOTONIC, &now);
229230
bool animating = animation_update_output(output, now);
230231

232+
// tick scroller view offset animation
233+
if (output->desk && output->desk->layout == LAYOUT_SCROLLER)
234+
animating = scroller_tick(output, now) || animating;
235+
231236
// ensure blur nodes track animated window positions during workspace slide
232237
if (animating)
233238
animation_update_slide_blur(output);

src/scroller.c

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include "animation.h"
12
#include "output.h"
23
#include "scroller.h"
34
#include "toplevel.h"
@@ -19,6 +20,9 @@ bool edge_scroller_pointer_focus = true;
1920
float *scroller_proportion_preset = NULL;
2021
int scroller_proportion_preset_count = 0;
2122

23+
int scroller_center_focused_column = SCROLLER_CENTER_ON_OVERFLOW;
24+
bool scroller_always_center_single_column = true;
25+
2226
static int max_i(int a, int b) {
2327
return a > b ? a : b;
2428
}
@@ -27,6 +31,9 @@ static double max_d(double a, double b) {
2731
return a > b ? a : b;
2832
}
2933

34+
static double compute_view_offset(scroller_state_t *s, struct wlr_box available);
35+
static void scroller_start_view_anim(scroller_state_t *s, double target_offset);
36+
3037
static scroller_tile_t *tile_grow(scroller_column_t *col) {
3138
if (col->tile_count >= col->capacity) {
3239
int new_cap = col->capacity ? col->capacity * 2 : 4;
@@ -371,6 +378,12 @@ void scroller_arrange(struct output_t *m, desktop_t *d, struct wlr_box available
371378
}
372379
col_xs[s->column_count] = wx; // sentinel
373380

381+
// auto-compute view offset when active column changes
382+
if (s->view_offset_dirty) {
383+
scroller_start_view_anim(s, compute_view_offset(s, available));
384+
s->view_offset_dirty = false;
385+
}
386+
374387
// view position (camera in world space)
375388
double active_col_x = col_xs[s->active_column_idx];
376389
double view_pos = active_col_x + s->view_offset;
@@ -439,6 +452,91 @@ static void apply_active_focus(desktop_t *d) {
439452
}
440453
}
441454

455+
static double compute_view_offset(scroller_state_t *s, struct wlr_box available) {
456+
scroller_column_t *col = &s->columns[s->active_column_idx];
457+
double col_width = col->resolved_width;
458+
double avail_width = (double)available.width;
459+
460+
if (scroller_always_center_single_column && s->column_count == 1 && col_width < avail_width)
461+
return (col_width - avail_width) / 2.0;
462+
463+
switch (scroller_center_focused_column) {
464+
case SCROLLER_CENTER_NEVER:
465+
return 0.0;
466+
case SCROLLER_CENTER_ALWAYS:
467+
return (col_width - avail_width) / 2.0;
468+
case SCROLLER_CENTER_ON_OVERFLOW:
469+
default:
470+
if (col_width > avail_width)
471+
return (col_width - avail_width) / 2.0;
472+
return 0.0;
473+
}
474+
}
475+
476+
static void scroller_apply_positions(output_t *m, scroller_state_t *s) {
477+
desktop_t *d = m->desk;
478+
int bw = (int)effective_border_width(d);
479+
480+
for (int i = 0; i < s->column_count; i++) {
481+
scroller_column_t *col = &s->columns[i];
482+
for (int j = 0; j < col->tile_count; j++) {
483+
client_t *c = col->tiles[j].client;
484+
if (!c || !c->toplevel || !c->toplevel->node)
485+
continue;
486+
487+
struct wlr_scene_tree *st = client_get_scene_tree(c);
488+
if (st)
489+
wlr_scene_node_set_position(&st->node, c->tiled_rectangle.x, c->tiled_rectangle.y);
490+
491+
struct wlr_scene_tree *bt = client_border_tree(c);
492+
if (bt)
493+
wlr_scene_node_set_position(&bt->node, c->tiled_rectangle.x - bw, c->tiled_rectangle.y - bw);
494+
}
495+
}
496+
}
497+
498+
static void scroller_start_view_anim(scroller_state_t *s, double target_offset) {
499+
if (enable_animations) {
500+
s->view_anim.active = true;
501+
s->view_anim.from = s->view_offset;
502+
s->view_anim.target = target_offset;
503+
clock_gettime(CLOCK_MONOTONIC, &s->view_anim.start);
504+
s->view_anim.duration_ms = animation_get_duration();
505+
} else {
506+
s->view_offset = target_offset;
507+
s->view_offset_dirty = false;
508+
}
509+
}
510+
511+
bool scroller_tick(output_t *output, struct timespec now) {
512+
if (!output || !output->desk || output->desk->layout != LAYOUT_SCROLLER)
513+
return false;
514+
515+
scroller_state_t *s = output->desk->scroller_state;
516+
if (!s || !s->view_anim.active)
517+
return false;
518+
519+
long elapsed_ms = (now.tv_sec - s->view_anim.start.tv_sec) * 1000 + (now.tv_nsec -
520+
s->view_anim.start.tv_nsec) / 1000000;
521+
522+
if (elapsed_ms >= (long)s->view_anim.duration_ms) {
523+
s->view_offset = s->view_anim.target;
524+
s->view_anim.active = false;
525+
arrange(output, output->desk, true);
526+
return false;
527+
}
528+
529+
double t = (double)elapsed_ms / (double)s->view_anim.duration_ms;
530+
// ease-out cubic (no pow to avoid -lm)
531+
double eased = 1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t);
532+
s->view_offset = s->view_anim.from + (s->view_anim.target - s->view_anim.from) * eased;
533+
534+
arrange(output, output->desk, false);
535+
scroller_apply_positions(output, s);
536+
537+
return true;
538+
}
539+
442540
bool scroller_focus_next(desktop_t *d) {
443541
scroller_state_t *s = d->scroller_state;
444542
if (!s || s->column_count == 0)
@@ -450,7 +548,7 @@ bool scroller_focus_next(desktop_t *d) {
450548
} else {
451549
s->active_column_idx++;
452550
}
453-
s->view_offset = 0.0; // reset scroll for now
551+
s->view_offset_dirty = true;
454552
apply_active_focus(d);
455553
return true;
456554
}
@@ -466,7 +564,7 @@ bool scroller_focus_prev(desktop_t *d) {
466564
} else {
467565
s->active_column_idx--;
468566
}
469-
s->view_offset = 0.0;
567+
s->view_offset_dirty = true;
470568
apply_active_focus(d);
471569
return true;
472570
}
@@ -514,7 +612,7 @@ void scroller_center_window(desktop_t *d, client_t *client) {
514612

515613
s->active_column_idx = col_idx;
516614
s->columns[col_idx].active_tile_idx = tile_idx;
517-
s->view_offset = 0.0;
615+
s->view_offset_dirty = true;
518616
apply_active_focus(d);
519617
}
520618

0 commit comments

Comments
 (0)