From 9db45cbb52ab00a87cde7277b4086816dfe73990 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sat, 15 Nov 2025 23:52:02 +0100 Subject: [PATCH 1/9] add first running version --- src/layout/horizontal.h | 255 ++++++++++++++++++++++++++++++++++++++++ src/layout/layout.h | 3 + 2 files changed, 258 insertions(+) diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index 2385c848..ed951509 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -377,6 +377,261 @@ void scroller(Monitor *m) { free(tempClients); // 最后释放内存 } +// Dual-row scroller layout with independent scrolling +// Top row: 30% of screen height, Bottom row: 70% of screen height +void dual_scroller(Monitor *m) { + unsigned int i, n, j; + + Client *c = NULL, *root_client = NULL; + Client **tempClients = NULL; + struct wlr_box target_geom; + int focus_client_index = 0; + bool need_scroller = false; + unsigned int cur_gappih = enablegaps ? m->gappih : 0; + unsigned int cur_gappoh = enablegaps ? m->gappoh : 0; + unsigned int cur_gappov = enablegaps ? m->gappov : 0; + unsigned int cur_gappiv = enablegaps ? m->gappiv : 0; + + cur_gappih = + smartgaps && m->visible_scroll_tiling_clients == 1 ? 0 : cur_gappih; + cur_gappoh = + smartgaps && m->visible_scroll_tiling_clients == 1 ? 0 : cur_gappoh; + cur_gappov = + smartgaps && m->visible_scroll_tiling_clients == 1 ? 0 : cur_gappov; + cur_gappiv = + smartgaps && m->visible_scroll_tiling_clients == 1 ? 0 : cur_gappiv; + + unsigned int max_client_width = + m->w.width - 2 * scroller_structs - cur_gappih; + + n = m->visible_scroll_tiling_clients; + + if (n == 0) { + return; + } + + // Allocate memory for temp clients array + tempClients = malloc(n * sizeof(Client *)); + if (!tempClients) { + return; + } + + // Fill tempClients array + j = 0; + wl_list_for_each(c, &clients, link) { + if (VISIBLEON(c, m) && ISSCROLLTILED(c)) { + tempClients[j] = c; + j++; + } + } + + // Calculate row heights (30% top, 70% bottom) + unsigned int top_row_height = (unsigned int)((m->w.height - 2 * cur_gappov - cur_gappiv) * 0.3); + unsigned int bottom_row_height = m->w.height - 2 * cur_gappov - cur_gappiv - top_row_height; + unsigned int top_row_y = m->w.y + cur_gappov; + unsigned int bottom_row_y = top_row_y + top_row_height + cur_gappiv; + + // Determine which row the focused client is in + // Clients are distributed: even indices go to top row, odd indices to bottom row + if (m->sel && !client_is_unmanaged(m->sel) && !m->sel->isfloating) { + root_client = m->sel; + } else if (m->prevsel && ISSCROLLTILED(m->prevsel) && + VISIBLEON(m->prevsel, m) && !client_is_unmanaged(m->prevsel)) { + root_client = m->prevsel; + } else { + root_client = center_tiled_select(m); + } + + if (!root_client) { + free(tempClients); + return; + } + + // Find focus client index + for (i = 0; i < n; i++) { + if (root_client == tempClients[i]) { + focus_client_index = i; + break; + } + } + + // Determine if focused client is in top or bottom row + bool focus_in_top_row = (focus_client_index % 2 == 0); + + // Check if scroller is needed for focused client + if (!root_client->is_pending_open_animation && + root_client->geom.x >= m->w.x + scroller_structs && + root_client->geom.x + root_client->geom.width <= + m->w.x + m->w.width - scroller_structs) { + need_scroller = false; + } else { + need_scroller = true; + } + + if (n == 1 && scroller_ignore_proportion_single) { + need_scroller = true; + } + + if (start_drag_window) + need_scroller = false; + + // Layout all clients + for (i = 0; i < n; i++) { + c = tempClients[i]; + bool in_top_row = (i % 2 == 0); + unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; + unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.height = client_row_height; + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.y = client_row_y; + + // Handle fullscreen and maximize + if (c->isfullscreen) { + target_geom.height = m->m.height; + target_geom.width = m->m.width; + target_geom.y = m->m.y; + target_geom.x = m->m.x; + resize(c, target_geom, 0); + continue; + } + + if (c->ismaximizescreen) { + target_geom.height = m->w.height - 2 * cur_gappov; + target_geom.width = m->w.width - 2 * cur_gappoh; + target_geom.y = m->w.y + cur_gappov; + target_geom.x = m->w.x + cur_gappoh; + resize(c, target_geom, 0); + continue; + } + + // Position logic for focused client + if (i == focus_client_index && need_scroller) { + if (scroller_focus_center || + ((!m->prevsel || + (ISSCROLLTILED(m->prevsel) && + (m->prevsel->scroller_proportion * max_client_width) + + (root_client->scroller_proportion * max_client_width) > + m->w.width - 2 * scroller_structs - cur_gappih)) && + scroller_prefer_center)) { + target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; + } else { + target_geom.x = root_client->geom.x > m->w.x + (m->w.width) / 2 + ? m->w.x + (m->w.width - + root_client->scroller_proportion * + max_client_width - + scroller_structs) + : m->w.x + scroller_structs; + } + resize(c, target_geom, 0); + } else if (i == focus_client_index) { + target_geom.x = c->geom.x; + resize(c, target_geom, 0); + } + } + + // Position clients in the same row as focus, to the left + if (focus_client_index >= 2) { + for (i = focus_client_index - 2; ; i -= 2) { + c = tempClients[i]; + bool in_top_row = (i % 2 == 0); + unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; + unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.height = client_row_height; + target_geom.y = client_row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = tempClients[i + 2]->geom.x - cur_gappih - target_geom.width; + resize(c, target_geom, 0); + } + + if (i < 2) break; + } + } + + // Position clients in the same row as focus, to the right + if (focus_client_index + 2 < n) { + for (i = focus_client_index + 2; i < n; i += 2) { + c = tempClients[i]; + bool in_top_row = (i % 2 == 0); + unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; + unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.height = client_row_height; + target_geom.y = client_row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = tempClients[i - 2]->geom.x + cur_gappih + tempClients[i - 2]->geom.width; + resize(c, target_geom, 0); + } + } + } + + // Position clients in the opposite row (independent scrolling) + // Find the first client in the opposite row and center it + int opposite_row_start = focus_in_top_row ? 1 : 0; + if (opposite_row_start < n) { + c = tempClients[opposite_row_start]; + bool in_top_row = (opposite_row_start % 2 == 0); + unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; + unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.height = client_row_height; + target_geom.y = client_row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; + resize(c, target_geom, 0); + } + + // Position other clients in the opposite row to the left + if (opposite_row_start >= 2) { + for (i = opposite_row_start - 2; ; i -= 2) { + c = tempClients[i]; + in_top_row = (i % 2 == 0); + client_row_height = in_top_row ? top_row_height : bottom_row_height; + client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.height = client_row_height; + target_geom.y = client_row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = tempClients[i + 2]->geom.x - cur_gappih - target_geom.width; + resize(c, target_geom, 0); + } + + if (i < 2) break; + } + } + + // Position other clients in the opposite row to the right + if (opposite_row_start + 2 < n) { + for (i = opposite_row_start + 2; i < n; i += 2) { + c = tempClients[i]; + in_top_row = (i % 2 == 0); + client_row_height = in_top_row ? top_row_height : bottom_row_height; + client_row_y = in_top_row ? top_row_y : bottom_row_y; + + target_geom.width = max_client_width * c->scroller_proportion; + target_geom.height = client_row_height; + target_geom.y = client_row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = tempClients[i - 2]->geom.x + cur_gappih + tempClients[i - 2]->geom.width; + resize(c, target_geom, 0); + } + } + } + } + + free(tempClients); +} + void center_tile(Monitor *m) { unsigned int i, n = 0, h, r, ie = enablegaps, mw, mx, my, oty, ety, tw; Client *c = NULL; diff --git a/src/layout/layout.h b/src/layout/layout.h index 62a3227d..1564e6db 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -11,6 +11,7 @@ static void vertical_overview(Monitor *m); static void vertical_grid(Monitor *m); static void vertical_scroller(Monitor *m); static void vertical_deck(Monitor *mon); +static void dual_scroller(Monitor *mon); /* layout(s) */ Layout overviewlayout = {"󰃇", overview, "overview"}; @@ -27,6 +28,7 @@ enum { VERTICAL_GRID, VERTICAL_DECK, RIGHT_TILE, + DUAL_SCROLLER, }; Layout layouts[] = { @@ -44,4 +46,5 @@ Layout layouts[] = { {"VT", vertical_tile, "vertical_tile", VERTICAL_TILE}, // 垂直平铺布局 {"VG", vertical_grid, "vertical_grid", VERTICAL_GRID}, // 垂直格子布局 {"VK", vertical_deck, "vertical_deck", VERTICAL_DECK}, // 垂直卡片布局 + {"DS", dual_scroller, "dual_scroller", DUAL_SCROLLER}, // 双行滚动布局 }; \ No newline at end of file From 5089995cfa33be0b280206fcef80ec3d52d9ada7 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 00:00:08 +0100 Subject: [PATCH 2/9] fix overlapping tiles --- src/fetch/monitor.h | 3 +++ src/layout/arrange.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/fetch/monitor.h b/src/fetch/monitor.h index d55e9ab6..16f1dc84 100644 --- a/src/fetch/monitor.h +++ b/src/fetch/monitor.h @@ -23,6 +23,9 @@ bool is_scroller_layout(Monitor *m) { if (m->pertag->ltidxs[m->pertag->curtag]->id == VERTICAL_SCROLLER) return true; + if (m->pertag->ltidxs[m->pertag->curtag]->id == DUAL_SCROLLER) + return true; + return false; } diff --git a/src/layout/arrange.h b/src/layout/arrange.h index ba1391e6..0d13ffab 100644 --- a/src/layout/arrange.h +++ b/src/layout/arrange.h @@ -498,6 +498,8 @@ void resize_tile_client(Client *grabc, bool isdrag, int offsetx, int offsety, resize_tile_scroller(grabc, isdrag, offsetx, offsety, time, false); } else if (current_layout->id == VERTICAL_SCROLLER) { resize_tile_scroller(grabc, isdrag, offsetx, offsety, time, true); + } else if (current_layout->id == DUAL_SCROLLER) { + resize_tile_scroller(grabc, isdrag, offsetx, offsety, time, false); } } From 8994b4cee2cca4065de86f4ced48db07e7ba0a22 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 00:08:10 +0100 Subject: [PATCH 3/9] fix rows keep their position when left --- src/layout/horizontal.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index ed951509..42d56a6d 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -571,9 +571,10 @@ void dual_scroller(Monitor *m) { } // Position clients in the opposite row (independent scrolling) - // Find the first client in the opposite row and center it + // Keep their current positions to maintain scroll state int opposite_row_start = focus_in_top_row ? 1 : 0; if (opposite_row_start < n) { + // Find a reference client in the opposite row to anchor positioning c = tempClients[opposite_row_start]; bool in_top_row = (opposite_row_start % 2 == 0); unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; @@ -584,7 +585,8 @@ void dual_scroller(Monitor *m) { target_geom.y = client_row_y; if (!c->isfullscreen && !c->ismaximizescreen) { - target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; + // Keep current X position to maintain scroll state + target_geom.x = c->geom.x; resize(c, target_geom, 0); } From 50b24942e770b0b25116842a3d7967c77abd6c82 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 00:29:13 +0100 Subject: [PATCH 4/9] fix row consistent movement and focus --- src/fetch/client.h | 9 +++++---- src/fetch/monitor.h | 10 ++++++++++ src/mango.c | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/fetch/client.h b/src/fetch/client.h index c2b0abdd..b1bfd7eb 100644 --- a/src/fetch/client.h +++ b/src/fetch/client.h @@ -146,6 +146,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, Client *c = NULL; Client **tempClients = NULL; // 初始化为 NULL int last = -1; + bool constrain_to_row = tc->mon && is_row_layout(tc->mon); // 第一次遍历,计算客户端数量 wl_list_for_each(c, &clients, link) { @@ -249,7 +250,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } break; case LEFT: - if (!ignore_align) { + if (!ignore_align || constrain_to_row) { for (int _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x < sel_x && tempClients[_i]->geom.y == sel_y && @@ -265,7 +266,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } } } - if (!tempFocusClients) { + if (!tempFocusClients && !constrain_to_row) { for (int _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x < sel_x) { int dis_x = tempClients[_i]->geom.x - sel_x; @@ -281,7 +282,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } break; case RIGHT: - if (!ignore_align) { + if (!ignore_align || constrain_to_row) { for (int _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x > sel_x && tempClients[_i]->geom.y == sel_y && @@ -297,7 +298,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } } } - if (!tempFocusClients) { + if (!tempFocusClients && !constrain_to_row) { for (int _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x > sel_x) { int dis_x = tempClients[_i]->geom.x - sel_x; diff --git a/src/fetch/monitor.h b/src/fetch/monitor.h index 16f1dc84..8711dbc2 100644 --- a/src/fetch/monitor.h +++ b/src/fetch/monitor.h @@ -29,6 +29,16 @@ bool is_scroller_layout(Monitor *m) { return false; } +bool is_row_layout(Monitor *m) { + // Layout has independent horizontal rows where navigation should be constrained + // LEFT/RIGHT: stay within same row (same Y) + // UP/DOWN: move between rows + if (m->pertag->ltidxs[m->pertag->curtag]->id == DUAL_SCROLLER) + return true; + + return false; +} + unsigned int get_tag_status(unsigned int tag, Monitor *m) { Client *c = NULL; unsigned int status = 0; diff --git a/src/mango.c b/src/mango.c index 8a27b751..d82ded7b 100644 --- a/src/mango.c +++ b/src/mango.c @@ -719,6 +719,7 @@ static struct wlr_scene_tree * wlr_scene_tree_snapshot(struct wlr_scene_node *node, struct wlr_scene_tree *parent); static bool is_scroller_layout(Monitor *m); +static bool is_row_layout(Monitor *m); static void create_output(struct wlr_backend *backend, void *data); static void get_layout_abbr(char *abbr, const char *full_name); static void apply_named_scratchpad(Client *target_client); From 09847dd09e173b5262f4d44d076b5686da92abec Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 00:57:12 +0100 Subject: [PATCH 5/9] add only respect scroller centering on bottom row --- src/layout/horizontal.h | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index 42d56a6d..0db9c7f6 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -507,13 +507,22 @@ void dual_scroller(Monitor *m) { // Position logic for focused client if (i == focus_client_index && need_scroller) { - if (scroller_focus_center || + // For top row (even index), always position at left edge (never center) + // For bottom row (odd index), respect scroller_focus_center setting + bool should_center = (scroller_focus_center || ((!m->prevsel || (ISSCROLLTILED(m->prevsel) && (m->prevsel->scroller_proportion * max_client_width) + (root_client->scroller_proportion * max_client_width) > m->w.width - 2 * scroller_structs - cur_gappih)) && - scroller_prefer_center)) { + scroller_prefer_center)); + + // Top row (even index): never center + if (in_top_row) { + should_center = false; + } + + if (should_center) { target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; } else { target_geom.x = root_client->geom.x > m->w.x + (m->w.width) / 2 From f7d4420685fe67d411675405fc291a6106a65bd7 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 01:19:44 +0100 Subject: [PATCH 6/9] update use dedicated row identifiers --- src/config/parse_config.h | 2 + src/dispatch/bind_declare.h | 1 + src/dispatch/bind_define.h | 23 +++ src/layout/horizontal.h | 347 +++++++++++++++--------------------- src/mango.c | 8 + 5 files changed, 178 insertions(+), 203 deletions(-) diff --git a/src/config/parse_config.h b/src/config/parse_config.h index a58a106f..5cff79bb 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -814,6 +814,8 @@ FuncType parse_func_name(char *func_name, Arg *arg, char *arg_value, } else if (strcmp(func_name, "focusdir") == 0) { func = focusdir; (*arg).i = parse_direction(arg_value); + } else if (strcmp(func_name, "togglerow") == 0) { + func = togglerow; } else if (strcmp(func_name, "incnmaster") == 0) { func = incnmaster; (*arg).i = atoi(arg_value); diff --git a/src/dispatch/bind_declare.h b/src/dispatch/bind_declare.h index 5bc215a2..17f04226 100644 --- a/src/dispatch/bind_declare.h +++ b/src/dispatch/bind_declare.h @@ -2,6 +2,7 @@ int minimized(const Arg *arg); int restore_minimized(const Arg *arg); int toggle_scratchpad(const Arg *arg); int focusdir(const Arg *arg); +int togglerow(const Arg *arg); int toggleoverview(const Arg *arg); int set_proportion(const Arg *arg); int switch_proportion_preset(const Arg *arg); diff --git a/src/dispatch/bind_define.h b/src/dispatch/bind_define.h index d79a7c65..6f78978c 100644 --- a/src/dispatch/bind_define.h +++ b/src/dispatch/bind_define.h @@ -125,6 +125,29 @@ int focusdir(const Arg *arg) { return 0; } +int togglerow(const Arg *arg) { + // Toggle between top and bottom row in dual-scroller layout + if (!selmon || !selmon->sel || !is_row_layout(selmon)) + return 0; + + Client *c = selmon->sel; + + // Only toggle for tiled windows + if (c->isfloating || !ISSCROLLTILED(c) || !VISIBLEON(c, selmon)) + return 0; + + // Toggle the row (0 <-> 1) + if (c->dual_scroller_row == 0) { + c->dual_scroller_row = 1; + } else { + c->dual_scroller_row = 0; + } + + // Trigger a relayout + arrange(selmon, false); + return 0; +} + int focuslast(const Arg *arg) { Client *c = NULL; diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index 0db9c7f6..e7ca9d69 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -380,13 +380,13 @@ void scroller(Monitor *m) { // Dual-row scroller layout with independent scrolling // Top row: 30% of screen height, Bottom row: 70% of screen height void dual_scroller(Monitor *m) { - unsigned int i, n, j; + unsigned int i, n_top = 0, n_bottom = 0, n_total = 0; - Client *c = NULL, *root_client = NULL; - Client **tempClients = NULL; + Client *c = NULL; + Client **top_row_clients = NULL; + Client **bottom_row_clients = NULL; struct wlr_box target_geom; - int focus_client_index = 0; - bool need_scroller = false; + unsigned int cur_gappih = enablegaps ? m->gappih : 0; unsigned int cur_gappoh = enablegaps ? m->gappoh : 0; unsigned int cur_gappov = enablegaps ? m->gappov : 0; @@ -404,24 +404,53 @@ void dual_scroller(Monitor *m) { unsigned int max_client_width = m->w.width - 2 * scroller_structs - cur_gappih; - n = m->visible_scroll_tiling_clients; + n_total = m->visible_scroll_tiling_clients; - if (n == 0) { + if (n_total == 0) { return; } - // Allocate memory for temp clients array - tempClients = malloc(n * sizeof(Client *)); - if (!tempClients) { - return; - } - - // Fill tempClients array - j = 0; + // First pass: count clients per row and assign unassigned clients wl_list_for_each(c, &clients, link) { if (VISIBLEON(c, m) && ISSCROLLTILED(c)) { - tempClients[j] = c; - j++; + // Assign to bottom row by default if not assigned + if (c->dual_scroller_row == -1) { + c->dual_scroller_row = 1; // Default to bottom row + } + + if (c->dual_scroller_row == 0) { + n_top++; + } else { + n_bottom++; + } + } + } + + // Allocate arrays for each row + if (n_top > 0) { + top_row_clients = malloc(n_top * sizeof(Client *)); + if (!top_row_clients) { + return; + } + } + + if (n_bottom > 0) { + bottom_row_clients = malloc(n_bottom * sizeof(Client *)); + if (!bottom_row_clients) { + free(top_row_clients); + return; + } + } + + // Fill row arrays + unsigned int top_idx = 0, bottom_idx = 0; + wl_list_for_each(c, &clients, link) { + if (VISIBLEON(c, m) && ISSCROLLTILED(c)) { + if (c->dual_scroller_row == 0) { + top_row_clients[top_idx++] = c; + } else { + bottom_row_clients[bottom_idx++] = c; + } } } @@ -431,216 +460,128 @@ void dual_scroller(Monitor *m) { unsigned int top_row_y = m->w.y + cur_gappov; unsigned int bottom_row_y = top_row_y + top_row_height + cur_gappiv; - // Determine which row the focused client is in - // Clients are distributed: even indices go to top row, odd indices to bottom row - if (m->sel && !client_is_unmanaged(m->sel) && !m->sel->isfloating) { - root_client = m->sel; - } else if (m->prevsel && ISSCROLLTILED(m->prevsel) && - VISIBLEON(m->prevsel, m) && !client_is_unmanaged(m->prevsel)) { - root_client = m->prevsel; - } else { - root_client = center_tiled_select(m); - } + // Helper function to layout a single row + void layout_row(Client **row_clients, unsigned int n_row, unsigned int row_y, + unsigned int row_height, bool is_top_row) { + if (n_row == 0) return; - if (!root_client) { - free(tempClients); - return; - } + Client *root_client = NULL; + int focus_index = -1; + bool need_scroller = false; - // Find focus client index - for (i = 0; i < n; i++) { - if (root_client == tempClients[i]) { - focus_client_index = i; - break; - } - } - - // Determine if focused client is in top or bottom row - bool focus_in_top_row = (focus_client_index % 2 == 0); - - // Check if scroller is needed for focused client - if (!root_client->is_pending_open_animation && - root_client->geom.x >= m->w.x + scroller_structs && - root_client->geom.x + root_client->geom.width <= - m->w.x + m->w.width - scroller_structs) { - need_scroller = false; - } else { - need_scroller = true; - } - - if (n == 1 && scroller_ignore_proportion_single) { - need_scroller = true; - } - - if (start_drag_window) - need_scroller = false; - - // Layout all clients - for (i = 0; i < n; i++) { - c = tempClients[i]; - bool in_top_row = (i % 2 == 0); - unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; - unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; - - target_geom.height = client_row_height; - target_geom.width = max_client_width * c->scroller_proportion; - target_geom.y = client_row_y; - - // Handle fullscreen and maximize - if (c->isfullscreen) { - target_geom.height = m->m.height; - target_geom.width = m->m.width; - target_geom.y = m->m.y; - target_geom.x = m->m.x; - resize(c, target_geom, 0); - continue; - } - - if (c->ismaximizescreen) { - target_geom.height = m->w.height - 2 * cur_gappov; - target_geom.width = m->w.width - 2 * cur_gappoh; - target_geom.y = m->w.y + cur_gappov; - target_geom.x = m->w.x + cur_gappoh; - resize(c, target_geom, 0); - continue; - } - - // Position logic for focused client - if (i == focus_client_index && need_scroller) { - // For top row (even index), always position at left edge (never center) - // For bottom row (odd index), respect scroller_focus_center setting - bool should_center = (scroller_focus_center || - ((!m->prevsel || - (ISSCROLLTILED(m->prevsel) && - (m->prevsel->scroller_proportion * max_client_width) + - (root_client->scroller_proportion * max_client_width) > - m->w.width - 2 * scroller_structs - cur_gappih)) && - scroller_prefer_center)); - - // Top row (even index): never center - if (in_top_row) { - should_center = false; + // Find focused client in this row + for (i = 0; i < n_row; i++) { + if (row_clients[i] == m->sel) { + root_client = row_clients[i]; + focus_index = i; + break; } + } - if (should_center) { - target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; + // If no focused client in this row, keep current scroll position + if (!root_client && n_row > 0) { + root_client = row_clients[0]; + focus_index = 0; + } + + // Check if scrolling is needed + if (root_client && !root_client->is_pending_open_animation && + root_client->geom.x >= m->w.x + scroller_structs && + root_client->geom.x + root_client->geom.width <= + m->w.x + m->w.width - scroller_structs) { + need_scroller = false; + } else { + need_scroller = true; + } + + if (start_drag_window) + need_scroller = false; + + // Layout focused client + if (focus_index >= 0 && root_client) { + target_geom.height = row_height; + target_geom.width = max_client_width * root_client->scroller_proportion; + target_geom.y = row_y; + + // Handle fullscreen and maximize + if (root_client->isfullscreen) { + target_geom.height = m->m.height; + target_geom.width = m->m.width; + target_geom.y = m->m.y; + target_geom.x = m->m.x; + resize(root_client, target_geom, 0); + } else if (root_client->ismaximizescreen) { + target_geom.height = m->w.height - 2 * cur_gappov; + target_geom.width = m->w.width - 2 * cur_gappoh; + target_geom.y = m->w.y + cur_gappov; + target_geom.x = m->w.x + cur_gappoh; + resize(root_client, target_geom, 0); + } else if (need_scroller) { + // Determine if we should center + bool should_center = (scroller_focus_center || + ((!m->prevsel || + (ISSCROLLTILED(m->prevsel) && + (m->prevsel->scroller_proportion * max_client_width) + + (root_client->scroller_proportion * max_client_width) > + m->w.width - 2 * scroller_structs - cur_gappih)) && + scroller_prefer_center)); + + // Top row: never center + if (is_top_row) { + should_center = false; + } + + if (should_center) { + target_geom.x = m->w.x + (m->w.width - target_geom.width) / 2; + } else { + target_geom.x = root_client->geom.x > m->w.x + (m->w.width) / 2 + ? m->w.x + (m->w.width - + root_client->scroller_proportion * + max_client_width - + scroller_structs) + : m->w.x + scroller_structs; + } + resize(root_client, target_geom, 0); } else { - target_geom.x = root_client->geom.x > m->w.x + (m->w.width) / 2 - ? m->w.x + (m->w.width - - root_client->scroller_proportion * - max_client_width - - scroller_structs) - : m->w.x + scroller_structs; + target_geom.x = root_client->geom.x; + resize(root_client, target_geom, 0); } - resize(c, target_geom, 0); - } else if (i == focus_client_index) { - target_geom.x = c->geom.x; - resize(c, target_geom, 0); } - } - - // Position clients in the same row as focus, to the left - if (focus_client_index >= 2) { - for (i = focus_client_index - 2; ; i -= 2) { - c = tempClients[i]; - bool in_top_row = (i % 2 == 0); - unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; - unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + // Layout clients to the left of focused + for (i = focus_index - 1; i >= 0 && i < n_row; i--) { + c = row_clients[i]; target_geom.width = max_client_width * c->scroller_proportion; - target_geom.height = client_row_height; - target_geom.y = client_row_y; + target_geom.height = row_height; + target_geom.y = row_y; if (!c->isfullscreen && !c->ismaximizescreen) { - target_geom.x = tempClients[i + 2]->geom.x - cur_gappih - target_geom.width; + target_geom.x = row_clients[i + 1]->geom.x - cur_gappih - target_geom.width; resize(c, target_geom, 0); } - - if (i < 2) break; } - } - - // Position clients in the same row as focus, to the right - if (focus_client_index + 2 < n) { - for (i = focus_client_index + 2; i < n; i += 2) { - c = tempClients[i]; - bool in_top_row = (i % 2 == 0); - unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; - unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + // Layout clients to the right of focused + for (i = focus_index + 1; i < n_row; i++) { + c = row_clients[i]; target_geom.width = max_client_width * c->scroller_proportion; - target_geom.height = client_row_height; - target_geom.y = client_row_y; + target_geom.height = row_height; + target_geom.y = row_y; if (!c->isfullscreen && !c->ismaximizescreen) { - target_geom.x = tempClients[i - 2]->geom.x + cur_gappih + tempClients[i - 2]->geom.width; + target_geom.x = row_clients[i - 1]->geom.x + cur_gappih + row_clients[i - 1]->geom.width; resize(c, target_geom, 0); } } } - // Position clients in the opposite row (independent scrolling) - // Keep their current positions to maintain scroll state - int opposite_row_start = focus_in_top_row ? 1 : 0; - if (opposite_row_start < n) { - // Find a reference client in the opposite row to anchor positioning - c = tempClients[opposite_row_start]; - bool in_top_row = (opposite_row_start % 2 == 0); - unsigned int client_row_height = in_top_row ? top_row_height : bottom_row_height; - unsigned int client_row_y = in_top_row ? top_row_y : bottom_row_y; + // Layout both rows independently + layout_row(top_row_clients, n_top, top_row_y, top_row_height, true); + layout_row(bottom_row_clients, n_bottom, bottom_row_y, bottom_row_height, false); - target_geom.width = max_client_width * c->scroller_proportion; - target_geom.height = client_row_height; - target_geom.y = client_row_y; - - if (!c->isfullscreen && !c->ismaximizescreen) { - // Keep current X position to maintain scroll state - target_geom.x = c->geom.x; - resize(c, target_geom, 0); - } - - // Position other clients in the opposite row to the left - if (opposite_row_start >= 2) { - for (i = opposite_row_start - 2; ; i -= 2) { - c = tempClients[i]; - in_top_row = (i % 2 == 0); - client_row_height = in_top_row ? top_row_height : bottom_row_height; - client_row_y = in_top_row ? top_row_y : bottom_row_y; - - target_geom.width = max_client_width * c->scroller_proportion; - target_geom.height = client_row_height; - target_geom.y = client_row_y; - - if (!c->isfullscreen && !c->ismaximizescreen) { - target_geom.x = tempClients[i + 2]->geom.x - cur_gappih - target_geom.width; - resize(c, target_geom, 0); - } - - if (i < 2) break; - } - } - - // Position other clients in the opposite row to the right - if (opposite_row_start + 2 < n) { - for (i = opposite_row_start + 2; i < n; i += 2) { - c = tempClients[i]; - in_top_row = (i % 2 == 0); - client_row_height = in_top_row ? top_row_height : bottom_row_height; - client_row_y = in_top_row ? top_row_y : bottom_row_y; - - target_geom.width = max_client_width * c->scroller_proportion; - target_geom.height = client_row_height; - target_geom.y = client_row_y; - - if (!c->isfullscreen && !c->ismaximizescreen) { - target_geom.x = tempClients[i - 2]->geom.x + cur_gappih + tempClients[i - 2]->geom.width; - resize(c, target_geom, 0); - } - } - } - } - - free(tempClients); + // Cleanup + free(top_row_clients); + free(bottom_row_clients); } void center_tile(Monitor *m) { diff --git a/src/mango.c b/src/mango.c index d82ded7b..2bdb9ac3 100644 --- a/src/mango.c +++ b/src/mango.c @@ -374,6 +374,7 @@ struct Client { double old_master_mfact_per, old_master_inner_per, old_stack_innder_per; double old_scroller_pproportion; bool ismaster; + int dual_scroller_row; // 0 = top row, 1 = bottom row, -1 = not set bool cursor_in_upper_half, cursor_in_left_half; bool isleftstack; int tearing_hint; @@ -2785,6 +2786,7 @@ createnotify(struct wl_listener *listener, void *data) { c = toplevel->base->data = ecalloc(1, sizeof(*c)); c->surface.xdg = toplevel->base; c->bw = borderpx; + c->dual_scroller_row = -1; // Not assigned to a row yet LISTEN(&toplevel->base->surface->events.commit, &c->commit, commitnotify); LISTEN(&toplevel->base->surface->events.map, &c->map, mapnotify); @@ -3744,6 +3746,11 @@ mapnotify(struct wl_listener *listener, void *data) { } if (at_client) { + // For row-based layouts, assign new client to same row as focused client + if (is_row_layout(selmon) && at_client->dual_scroller_row >= 0) { + c->dual_scroller_row = at_client->dual_scroller_row; + } + at_client->link.next->prev = &c->link; c->link.prev = &at_client->link; c->link.next = at_client->link.next; @@ -5803,6 +5810,7 @@ void createnotifyx11(struct wl_listener *listener, void *data) { c = xsurface->data = ecalloc(1, sizeof(*c)); c->surface.xwayland = xsurface; c->type = X11; + c->dual_scroller_row = -1; // Not assigned to a row yet /* Listen to the various events it can emit */ LISTEN(&xsurface->events.associate, &c->associate, associatex11); LISTEN(&xsurface->events.destroy, &c->destroy, destroynotify); From dd6223d38332acc29c6c6147f243bfce30363fbf Mon Sep 17 00:00:00 2001 From: rasmusq Date: Sun, 16 Nov 2025 02:00:42 +0100 Subject: [PATCH 7/9] fix do not scroll to index zero when no window in row is focused --- src/layout/horizontal.h | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index e7ca9d69..6477186e 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -480,8 +480,7 @@ void dual_scroller(Monitor *m) { // If no focused client in this row, keep current scroll position if (!root_client && n_row > 0) { - root_client = row_clients[0]; - focus_index = 0; + return; } // Check if scrolling is needed @@ -1010,4 +1009,4 @@ monocle(Monitor *m) { } if ((c = focustop(m))) wlr_scene_node_raise_to_top(&c->scene->node); -} \ No newline at end of file +} From 94a051e266d1780a28e42909dbfeb69dcf0dbe85 Mon Sep 17 00:00:00 2001 From: rasmusq Date: Thu, 27 Nov 2025 20:28:06 +0100 Subject: [PATCH 8/9] add: configuration options and keybinds for the dual-scroller layout --- config.conf | 5 +++++ src/config/parse_config.h | 9 +++++++++ src/config/preset.h | 1 + src/dispatch/bind_declare.h | 1 + src/dispatch/bind_define.h | 22 ++++++++++++++++++++++ src/layout/horizontal.h | 4 ++-- 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/config.conf b/config.conf index 4c8d8d3a..e72ee09f 100644 --- a/config.conf +++ b/config.conf @@ -57,6 +57,7 @@ scroller_prefer_center=0 edge_scroller_pointer_focus=1 scroller_default_proportion_single=1.0 scroller_proportion_preset=0.5,0.8,1.0 +dual_scroller_default_split_ratio=0.3 # Master-Stack Layout Setting new_is_master=1 @@ -237,6 +238,10 @@ bind=CTRL+ALT,Down,resizewin,+0,+50 bind=CTRL+ALT,Left,resizewin,-50,+0 bind=CTRL+ALT,Right,resizewin,+50,+0 +# dual-scroller split adjustment (increase/decrease top row height) +# bind=SUPER+SHIFT,Equal,adjust_dual_scroller_split,0.05 +# bind=SUPER+SHIFT,Minus,adjust_dual_scroller_split,-0.05 + # Mouse Button Bindings # NONE mode key only work in ov mode mousebind=SUPER,btn_left,moveresize,curmove diff --git a/src/config/parse_config.h b/src/config/parse_config.h index 5cff79bb..7c772dc8 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -200,6 +200,7 @@ typedef struct { int scroller_focus_center; int scroller_prefer_center; int edge_scroller_pointer_focus; + float dual_scroller_default_split_ratio; int focus_cross_monitor; int exchange_cross_monitor; int scratchpad_cross_monitor; @@ -822,6 +823,9 @@ FuncType parse_func_name(char *func_name, Arg *arg, char *arg_value, } else if (strcmp(func_name, "setmfact") == 0) { func = setmfact; (*arg).f = atof(arg_value); + } else if (strcmp(func_name, "adjust_dual_scroller_split") == 0) { + func = adjust_dual_scroller_split; + (*arg).f = atof(arg_value); } else if (strcmp(func_name, "zoom") == 0) { func = zoom; } else if (strcmp(func_name, "exchange_client") == 0) { @@ -1167,6 +1171,8 @@ void parse_option(Config *config, char *key, char *value) { config->scroller_prefer_center = atoi(value); } else if (strcmp(key, "edge_scroller_pointer_focus") == 0) { config->edge_scroller_pointer_focus = atoi(value); + } else if (strcmp(key, "dual_scroller_default_split_ratio") == 0) { + config->dual_scroller_default_split_ratio = atof(value); } else if (strcmp(key, "focus_cross_monitor") == 0) { config->focus_cross_monitor = atoi(value); } else if (strcmp(key, "exchange_cross_monitor") == 0) { @@ -2652,6 +2658,8 @@ void override_config(void) { edge_scroller_pointer_focus = CLAMP_INT(config.edge_scroller_pointer_focus, 0, 1); scroller_structs = CLAMP_INT(config.scroller_structs, 0, 1000); + dual_scroller_default_split_ratio = + CLAMP_FLOAT(config.dual_scroller_default_split_ratio, 0.1f, 0.9f); // 主从布局设置 default_mfact = CLAMP_FLOAT(config.default_mfact, 0.1f, 0.9f); @@ -2840,6 +2848,7 @@ void set_value_default() { config.scroller_focus_center = scroller_focus_center; config.scroller_prefer_center = scroller_prefer_center; config.edge_scroller_pointer_focus = edge_scroller_pointer_focus; + config.dual_scroller_default_split_ratio = dual_scroller_default_split_ratio; config.focus_cross_monitor = focus_cross_monitor; config.exchange_cross_monitor = exchange_cross_monitor; config.scratchpad_cross_monitor = scratchpad_cross_monitor; diff --git a/src/config/preset.h b/src/config/preset.h index eaa7be22..a49ec7d0 100644 --- a/src/config/preset.h +++ b/src/config/preset.h @@ -63,6 +63,7 @@ float scroller_default_proportion_single = 1.0; int scroller_ignore_proportion_single = 0; int scroller_focus_center = 0; int scroller_prefer_center = 0; +float dual_scroller_default_split_ratio = 0.3; int focus_cross_monitor = 0; int focus_cross_tag = 0; int exchange_cross_monitor = 0; diff --git a/src/dispatch/bind_declare.h b/src/dispatch/bind_declare.h index 17f04226..03fc6ccb 100644 --- a/src/dispatch/bind_declare.h +++ b/src/dispatch/bind_declare.h @@ -29,6 +29,7 @@ int switch_keyboard_layout(const Arg *arg); int setlayout(const Arg *arg); int switch_layout(const Arg *arg); int setmfact(const Arg *arg); +int adjust_dual_scroller_split(const Arg *arg); int quit(const Arg *arg); int moveresize(const Arg *arg); int exchange_client(const Arg *arg); diff --git a/src/dispatch/bind_define.h b/src/dispatch/bind_define.h index 6f78978c..99668bcb 100644 --- a/src/dispatch/bind_define.h +++ b/src/dispatch/bind_define.h @@ -330,6 +330,28 @@ int setmfact(const Arg *arg) { return 0; } +int adjust_dual_scroller_split(const Arg *arg) { + float new_ratio; + + if (!arg || !selmon) + return 0; + + // Check if we're in a dual-scroller layout + if (!is_row_layout(selmon)) + return 0; + + // Calculate new ratio: if arg->f < 1.0, treat as relative adjustment, otherwise as absolute value + new_ratio = arg->f < 1.0 ? dual_scroller_default_split_ratio + arg->f : arg->f - 1.0; + + // Clamp the ratio between 0.1 and 0.9 + if (new_ratio < 0.1 || new_ratio > 0.9) + return 0; + + dual_scroller_default_split_ratio = new_ratio; + arrange(selmon, false); + return 0; +} + int killclient(const Arg *arg) { Client *c = NULL; c = selmon->sel; diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index 6477186e..fcd50ee5 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -454,8 +454,8 @@ void dual_scroller(Monitor *m) { } } - // Calculate row heights (30% top, 70% bottom) - unsigned int top_row_height = (unsigned int)((m->w.height - 2 * cur_gappov - cur_gappiv) * 0.3); + // Calculate row heights using configurable split ratio + unsigned int top_row_height = (unsigned int)((m->w.height - 2 * cur_gappov - cur_gappiv) * dual_scroller_default_split_ratio); unsigned int bottom_row_height = m->w.height - 2 * cur_gappov - cur_gappiv - top_row_height; unsigned int top_row_y = m->w.y + cur_gappov; unsigned int bottom_row_y = top_row_y + top_row_height + cur_gappiv; From c0eceeb3bf2c995c378472900fc18801a3ff35e0 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Sat, 29 Nov 2025 22:46:47 +0800 Subject: [PATCH 9/9] fix: avoid toggle overview when setfullscreen and setmaximziescreen --- src/mango.c | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/mango.c b/src/mango.c index 5c804a0a..70b22187 100644 --- a/src/mango.c +++ b/src/mango.c @@ -102,10 +102,10 @@ A->geom.x + A->geom.width <= A->mon->m.x + A->mon->m.width && \ A->geom.y + A->geom.height <= A->mon->m.y + A->mon->m.height) #define ISTILED(A) \ - (A && !(A)->isfloating && !(A)->isminimized && !(A)->iskilling && \ + (A && !(A)->isfloating && !(A)->isminimized && !(A)->iskilling && \ !(A)->ismaximizescreen && !(A)->isfullscreen && !(A)->isunglobal) #define ISSCROLLTILED(A) \ - (A && !(A)->isfloating && !(A)->isminimized && !(A)->iskilling && \ + (A && !(A)->isfloating && !(A)->isminimized && !(A)->iskilling && \ !(A)->isunglobal) #define VISIBLEON(C, M) \ ((C) && (M) && (C)->mon == (M) && ((C)->tags & (M)->tagset[(M)->seltags])) @@ -4559,6 +4559,9 @@ void setmaximizescreen(Client *c, int maximizescreen) { if (!c || !c->mon || !client_surface(c)->mapped || c->iskilling) return; + if (c->mon->isoverview) + return; + c->ismaximizescreen = maximizescreen; if (maximizescreen) { @@ -4568,10 +4571,6 @@ void setmaximizescreen(Client *c, int maximizescreen) { if (c->isfloating) c->float_geom = c->geom; - if (selmon->isoverview) { - Arg arg = {0}; - toggleoverview(&arg); - } maximizescreen_box.x = c->mon->w.x + gappoh; maximizescreen_box.y = c->mon->w.y + gappov; @@ -4615,11 +4614,15 @@ void setfakefullscreen(Client *c, int fakefullscreen) { void setfullscreen(Client *c, int fullscreen) // 用自定义全屏代理自带全屏 { - c->isfullscreen = fullscreen; if (!c || !c->mon || !client_surface(c)->mapped || c->iskilling) return; + if (c->mon->isoverview) + return; + + c->isfullscreen = fullscreen; + client_set_fullscreen(c, fullscreen); if (fullscreen) { @@ -4628,10 +4631,6 @@ void setfullscreen(Client *c, int fullscreen) // 用自定义全屏代理自带 if (c->isfloating) c->float_geom = c->geom; - if (selmon->isoverview) { - Arg arg = {0}; - toggleoverview(&arg); - } c->bw = 0; wlr_scene_node_raise_to_top(&c->scene->node); // 将视图提升到顶层