diff --git a/config.conf b/config.conf index 6cbb3a20..6fee4a48 100644 --- a/config.conf +++ b/config.conf @@ -59,6 +59,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 @@ -239,6 +240,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 9c278724..10f381c9 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -206,6 +206,7 @@ typedef struct { int32_t scroller_focus_center; int32_t scroller_prefer_center; int32_t edge_scroller_pointer_focus; + float dual_scroller_default_split_ratio; int32_t focus_cross_monitor; int32_t exchange_cross_monitor; int32_t scratchpad_cross_monitor; @@ -828,12 +829,17 @@ 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); } 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) { @@ -1223,6 +1229,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) { @@ -2725,6 +2733,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); @@ -2919,6 +2929,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 6f3cd891..e824b8d7 100644 --- a/src/config/preset.h +++ b/src/config/preset.h @@ -65,6 +65,7 @@ float scroller_default_proportion_single = 1.0; int32_t scroller_ignore_proportion_single = 0; int32_t scroller_focus_center = 0; int32_t scroller_prefer_center = 0; +float dual_scroller_default_split_ratio = 0.3; int32_t focus_cross_monitor = 0; int32_t focus_cross_tag = 0; int32_t exchange_cross_monitor = 0; diff --git a/src/dispatch/bind_declare.h b/src/dispatch/bind_declare.h index b197778b..07630d59 100644 --- a/src/dispatch/bind_declare.h +++ b/src/dispatch/bind_declare.h @@ -19,6 +19,7 @@ int32_t togglefloating(const Arg *arg); int32_t togglefullscreen(const Arg *arg); int32_t togglemaximizescreen(const Arg *arg); int32_t togglegaps(const Arg *arg); +int32_t togglerow(const Arg *arg); int32_t tagmon(const Arg *arg); int32_t spawn(const Arg *arg); int32_t spawn_shell(const Arg *arg); @@ -28,6 +29,7 @@ int32_t switch_keyboard_layout(const Arg *arg); int32_t setlayout(const Arg *arg); int32_t switch_layout(const Arg *arg); int32_t setmfact(const Arg *arg); +int32_t adjust_dual_scroller_split(const Arg *arg); int32_t quit(const Arg *arg); int32_t moveresize(const Arg *arg); int32_t exchange_client(const Arg *arg); @@ -68,4 +70,4 @@ int32_t toggle_trackpad_enable(const Arg *arg); int32_t setoption(const Arg *arg); int32_t disable_monitor(const Arg *arg); int32_t enable_monitor(const Arg *arg); -int32_t toggle_monitor(const Arg *arg); \ No newline at end of file +int32_t toggle_monitor(const Arg *arg); diff --git a/src/dispatch/bind_define.h b/src/dispatch/bind_define.h index 19d743e7..624119a0 100644 --- a/src/dispatch/bind_define.h +++ b/src/dispatch/bind_define.h @@ -146,6 +146,29 @@ int32_t focusdir(const Arg *arg) { return 0; } +int32_t 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, false); + return 0; +} + int32_t focuslast(const Arg *arg) { Client *c = NULL; @@ -327,6 +350,28 @@ int32_t setmfact(const Arg *arg) { return 0; } +int32_t 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, false); + return 0; +} + int32_t killclient(const Arg *arg) { Client *c = NULL; c = selmon->sel; diff --git a/src/fetch/client.h b/src/fetch/client.h index 0af17238..cea00834 100644 --- a/src/fetch/client.h +++ b/src/fetch/client.h @@ -158,6 +158,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, Client *c = NULL; Client **tempClients = NULL; // 初始化为 NULL int32_t last = -1; + bool constrain_to_row = tc->mon && is_row_layout(tc->mon); // 第一次遍历,计算客户端数量 wl_list_for_each(c, &clients, link) { @@ -273,7 +274,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 (int32_t _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x < sel_x && tempClients[_i]->geom.y == sel_y && @@ -289,7 +290,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } } } - if (!tempFocusClients) { + if (!tempFocusClients && !constrain_to_row) { for (int32_t _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x < sel_x) { int32_t dis_x = tempClients[_i]->geom.x - sel_x; @@ -310,7 +311,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 (int32_t _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x > sel_x && tempClients[_i]->geom.y == sel_y && @@ -326,7 +327,7 @@ Client *find_client_by_direction(Client *tc, const Arg *arg, bool findfloating, } } } - if (!tempFocusClients) { + if (!tempFocusClients && !constrain_to_row) { for (int32_t _i = 0; _i <= last; _i++) { if (tempClients[_i]->geom.x > sel_x) { int32_t dis_x = tempClients[_i]->geom.x - sel_x; @@ -447,4 +448,4 @@ bool client_only_in_one_tag(Client *c) { } else { return false; } -} \ No newline at end of file +} diff --git a/src/fetch/monitor.h b/src/fetch/monitor.h index 47a5b824..9823d369 100644 --- a/src/fetch/monitor.h +++ b/src/fetch/monitor.h @@ -23,6 +23,19 @@ 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; +} + +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; } diff --git a/src/layout/arrange.h b/src/layout/arrange.h index d668f309..0bd11949 100644 --- a/src/layout/arrange.h +++ b/src/layout/arrange.h @@ -504,6 +504,8 @@ void resize_tile_client(Client *grabc, bool isdrag, int32_t offsetx, 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); } } diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index a2f5e5c1..7f71128e 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -367,6 +367,212 @@ 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_top = 0, n_bottom = 0, n_total = 0; + + Client *c = NULL; + Client **top_row_clients = NULL; + Client **bottom_row_clients = NULL; + struct wlr_box target_geom; + + 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_total = m->visible_scroll_tiling_clients; + + if (n_total == 0) { + return; + } + + // First pass: count clients per row and assign unassigned clients + wl_list_for_each(c, &clients, link) { + if (VISIBLEON(c, m) && ISSCROLLTILED(c)) { + // 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; + } + } + } + + // 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; + + // 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; + + Client *root_client = NULL; + int focus_index = -1; + bool need_scroller = 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 no focused client in this row, keep current scroll position + if (!root_client && n_row > 0) { + return; + } + + // 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; + resize(root_client, target_geom, 0); + } + } + + // 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 = row_height; + target_geom.y = row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = row_clients[i + 1]->geom.x - cur_gappih - target_geom.width; + resize(c, target_geom, 0); + } + } + + // 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 = row_height; + target_geom.y = row_y; + + if (!c->isfullscreen && !c->ismaximizescreen) { + target_geom.x = row_clients[i - 1]->geom.x + cur_gappih + row_clients[i - 1]->geom.width; + resize(c, target_geom, 0); + } + } + } + + // 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); + + // Cleanup + free(top_row_clients); + free(bottom_row_clients); +} + void center_tile(Monitor *m) { int32_t i, n = 0, h, r, ie = enablegaps, mw, mx, my, oty, ety, tw; Client *c = NULL; @@ -853,4 +1059,4 @@ void tgmix(Monitor *m) { grid(m); return; } -} \ No newline at end of file +} diff --git a/src/layout/layout.h b/src/layout/layout.h index 169ab119..9615cbcd 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); static void tgmix(Monitor *m); /* layout(s) */ @@ -28,6 +29,7 @@ enum { VERTICAL_GRID, VERTICAL_DECK, RIGHT_TILE, + DUAL_SCROLLER, TGMIX, }; @@ -46,5 +48,6 @@ 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}, // 双行滚动布局 {"TG", tgmix, "tgmix", TGMIX}, // 混合布局 -}; \ No newline at end of file +}; diff --git a/src/mango.c b/src/mango.c index 6760b7b5..4eec71b3 100644 --- a/src/mango.c +++ b/src/mango.c @@ -400,6 +400,7 @@ struct Client { double old_master_mfact_per, old_master_inner_per, old_stack_inner_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; int32_t tearing_hint; @@ -743,6 +744,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); @@ -2820,6 +2822,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); @@ -3829,6 +3832,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; @@ -5894,6 +5902,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);