From 1fc42b76e54ef074da68a4ed5eda606c65564df5 Mon Sep 17 00:00:00 2001 From: ernestoCruz05 Date: Sat, 9 May 2026 12:14:57 +0100 Subject: [PATCH 1/6] feat: opt-in config field to have typical dwindle tiling --- src/animation/client.h | 39 +++++++++++++++++++++++++++++++++++++++ src/config/parse_config.h | 6 ++++++ src/mango.c | 2 +- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/animation/client.h b/src/animation/client.h index 9108c98c..ba48959d 100644 --- a/src/animation/client.h +++ b/src/animation/client.h @@ -538,6 +538,11 @@ void client_set_drop_area(Client *c) { struct wlr_box drop_box; + const Layout *cur_layout = + c->mon ? c->mon->pertag->ltidxs[c->mon->pertag->curtag] : NULL; + bool dwindle_familiar = cur_layout && cur_layout->id == DWINDLE && + config.dwindle_drop_simple_split; + // 中心区域:x和y都在30%~70%之间 → 无方向 if (rel_x > client_width * 0.3 && rel_x < client_width * 0.7 && rel_y > client_height * 0.3 && rel_y < client_height * 0.7) { @@ -546,6 +551,40 @@ void client_set_drop_area(Client *c) { drop_box.width = client_width * 0.4; drop_box.height = client_height * 0.4; drop_direction = UNDIR; + } else if (dwindle_familiar) { + // Mirror dwindle_assign's split_h = (aw >= ah) rule so the preview + // matches the auto-rotated split that dwindle will produce. + bool split_h = c->geom.width >= c->geom.height; + float ratio = config.dwindle_split_ratio; + if (split_h) { + if (rel_x < client_width * 0.5) { + drop_direction = LEFT; + drop_box.x = bw; + drop_box.y = bw; + drop_box.width = (int32_t)(client_width * ratio); + drop_box.height = client_height; + } else { + drop_direction = RIGHT; + drop_box.x = bw + (int32_t)(client_width * ratio); + drop_box.y = bw; + drop_box.width = client_width - (int32_t)(client_width * ratio); + drop_box.height = client_height; + } + } else { + if (rel_y < client_height * 0.5) { + drop_direction = UP; + drop_box.x = bw; + drop_box.y = bw; + drop_box.width = client_width; + drop_box.height = (int32_t)(client_height * ratio); + } else { + drop_direction = DOWN; + drop_box.x = bw; + drop_box.y = bw + (int32_t)(client_height * ratio); + drop_box.width = client_width; + drop_box.height = client_height - (int32_t)(client_height * ratio); + } + } } else { // 否则根据到各边的距离决定方向 double dist_left = rel_x; diff --git a/src/config/parse_config.h b/src/config/parse_config.h index 207d06f7..ceaec81c 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -248,6 +248,7 @@ typedef struct { int32_t dwindle_preserve_split; int32_t dwindle_smart_split; int32_t dwindle_smart_resize; + int32_t dwindle_drop_simple_split; float dwindle_split_ratio; uint32_t hotarea_size; @@ -1633,6 +1634,8 @@ bool parse_option(Config *config, char *key, char *value) { config->dwindle_smart_split = atoi(value); } else if (strcmp(key, "dwindle_smart_resize") == 0) { config->dwindle_smart_resize = atoi(value); + } else if (strcmp(key, "dwindle_drop_simple_split") == 0) { + config->dwindle_drop_simple_split = atoi(value); } else if (strcmp(key, "dwindle_split_ratio") == 0) { config->dwindle_split_ratio = atof(value); } else if (strcmp(key, "hotarea_size") == 0) { @@ -3205,6 +3208,8 @@ void override_config(void) { CLAMP_INT(config.dwindle_preserve_split, 0, 1); config.dwindle_smart_split = CLAMP_INT(config.dwindle_smart_split, 0, 1); config.dwindle_smart_resize = CLAMP_INT(config.dwindle_smart_resize, 0, 1); + config.dwindle_drop_simple_split = + CLAMP_INT(config.dwindle_drop_simple_split, 0, 1); config.dwindle_split_ratio = CLAMP_FLOAT(config.dwindle_split_ratio, 0.05f, 0.95f); config.hotarea_size = CLAMP_INT(config.hotarea_size, 1, 1000); @@ -3351,6 +3356,7 @@ void set_value_default() { config.dwindle_preserve_split = 0; config.dwindle_smart_split = 0; config.dwindle_smart_resize = 0; + config.dwindle_drop_simple_split = 0; config.dwindle_split_ratio = 0.5f; config.log_level = WLR_ERROR; diff --git a/src/mango.c b/src/mango.c index 17b99dca..7463262c 100644 --- a/src/mango.c +++ b/src/mango.c @@ -2229,7 +2229,7 @@ void place_drag_tile_client(Client *c) { closest->drop_direction == RIGHT); dwindle_insert(&c->mon->pertag->dwindle_root[tag], c, closest, config.dwindle_split_ratio, insert_before, split_h, - true); + !config.dwindle_drop_simple_split); setfloating(c, 0); return; } From 73ed3ce35babb1e4e7345f10daa79766bcd58a3a Mon Sep 17 00:00:00 2001 From: ernestoCruz05 Date: Sat, 9 May 2026 11:55:59 +0800 Subject: [PATCH 2/6] feat: dwindle layout support --- src/config/parse_config.h | 41 +++- src/dispatch/bind_define.h | 4 + src/layout/arrange.h | 17 ++ src/layout/dwindle.h | 433 +++++++++++++++++++++++++++++++++++++ src/layout/layout.h | 3 + src/mango.c | 55 ++++- 6 files changed, 550 insertions(+), 3 deletions(-) create mode 100644 src/layout/dwindle.h diff --git a/src/config/parse_config.h b/src/config/parse_config.h index f70a17d6..207d06f7 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -242,6 +242,14 @@ typedef struct { int32_t center_master_overspread; int32_t center_when_single_stack; + /* dwindle layout */ + int32_t dwindle_vsplit; + int32_t dwindle_hsplit; + int32_t dwindle_preserve_split; + int32_t dwindle_smart_split; + int32_t dwindle_smart_resize; + float dwindle_split_ratio; + uint32_t hotarea_size; uint32_t hotarea_corner; uint32_t enable_hotarea; @@ -1615,6 +1623,18 @@ bool parse_option(Config *config, char *key, char *value) { config->center_master_overspread = atoi(value); } else if (strcmp(key, "center_when_single_stack") == 0) { config->center_when_single_stack = atoi(value); + } else if (strcmp(key, "dwindle_vsplit") == 0) { + config->dwindle_vsplit = atoi(value); + } else if (strcmp(key, "dwindle_hsplit") == 0) { + config->dwindle_hsplit = atoi(value); + } else if (strcmp(key, "dwindle_preserve_split") == 0) { + config->dwindle_preserve_split = atoi(value); + } else if (strcmp(key, "dwindle_smart_split") == 0) { + config->dwindle_smart_split = atoi(value); + } else if (strcmp(key, "dwindle_smart_resize") == 0) { + config->dwindle_smart_resize = atoi(value); + } else if (strcmp(key, "dwindle_split_ratio") == 0) { + config->dwindle_split_ratio = atof(value); } else if (strcmp(key, "hotarea_size") == 0) { config->hotarea_size = atoi(value); } else if (strcmp(key, "hotarea_corner") == 0) { @@ -3179,6 +3199,14 @@ void override_config(void) { config.center_when_single_stack = CLAMP_INT(config.center_when_single_stack, 0, 1); config.new_is_master = CLAMP_INT(config.new_is_master, 0, 1); + config.dwindle_vsplit = CLAMP_INT(config.dwindle_vsplit, 0, 2); + config.dwindle_hsplit = CLAMP_INT(config.dwindle_hsplit, 0, 2); + config.dwindle_preserve_split = + CLAMP_INT(config.dwindle_preserve_split, 0, 1); + config.dwindle_smart_split = CLAMP_INT(config.dwindle_smart_split, 0, 1); + config.dwindle_smart_resize = CLAMP_INT(config.dwindle_smart_resize, 0, 1); + config.dwindle_split_ratio = + CLAMP_FLOAT(config.dwindle_split_ratio, 0.05f, 0.95f); config.hotarea_size = CLAMP_INT(config.hotarea_size, 1, 1000); config.hotarea_corner = CLAMP_INT(config.hotarea_corner, 0, 3); config.enable_hotarea = CLAMP_INT(config.enable_hotarea, 0, 1); @@ -3318,6 +3346,13 @@ void set_value_default() { config.center_master_overspread = 0; config.center_when_single_stack = 1; + config.dwindle_vsplit = 0; + config.dwindle_hsplit = 0; + config.dwindle_preserve_split = 0; + config.dwindle_smart_split = 0; + config.dwindle_smart_resize = 0; + config.dwindle_split_ratio = 0.5f; + config.log_level = WLR_ERROR; config.numlockon = 0; config.capslock = 0; @@ -3716,7 +3751,7 @@ void reapply_rootbg(void) { wlr_scene_rect_set_color(root_bg, config.rootcolor); } -void reapply_border(void) { +void reapply_property(void) { Client *c = NULL; // reset border width when config change @@ -3725,6 +3760,8 @@ void reapply_border(void) { if (!c->isnoborder && !c->isfullscreen) { c->bw = config.borderpx; } + + wlr_scene_rect_set_color(c->droparea, config.dropcolor); } } } @@ -3870,7 +3907,7 @@ void reset_option(void) { run_exec(); reapply_cursor_style(); - reapply_border(); + reapply_property(); reapply_rootbg(); reapply_keyboard(); reapply_pointer(); diff --git a/src/dispatch/bind_define.h b/src/dispatch/bind_define.h index 1ab84c9d..3c1d2154 100644 --- a/src/dispatch/bind_define.h +++ b/src/dispatch/bind_define.h @@ -112,6 +112,10 @@ int32_t exchange_client(const Arg *arg) { Client *tc = direction_select(arg); tc = get_focused_stack_client(tc); + + if (!tc) + return 0; + exchange_two_client(c, tc); return 0; } diff --git a/src/layout/arrange.h b/src/layout/arrange.h index 04f4554b..9e0c6fa7 100644 --- a/src/layout/arrange.h +++ b/src/layout/arrange.h @@ -488,6 +488,21 @@ void resize_tile_master_vertical(Client *grabc, bool isdrag, int32_t offsetx, } } +void resize_tile_dwindle(Client *grabc, bool isdrag, int32_t offsetx, + int32_t offsety, uint32_t time, bool isvertical) { + + if (!isdrag) { + dwindle_resize_client_step(grabc->mon, grabc, offsetx, offsety); + return; + } + + if (last_apply_drap_time == 0 || + time - last_apply_drap_time > config.drag_tile_refresh_interval) { + dwindle_resize_client(grabc->mon, grabc); + last_apply_drap_time = time; + } +} + void resize_tile_scroller(Client *grabc, bool isdrag, int32_t offsetx, int32_t offsety, uint32_t time, bool isvertical) { Client *tc = NULL; @@ -706,6 +721,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 == DWINDLE) { + resize_tile_dwindle(grabc, isdrag, offsetx, offsety, time, true); } } diff --git a/src/layout/dwindle.h b/src/layout/dwindle.h new file mode 100644 index 00000000..94d65be6 --- /dev/null +++ b/src/layout/dwindle.h @@ -0,0 +1,433 @@ +typedef struct DwindleNode DwindleNode; +struct DwindleNode { + bool is_split; + bool split_h; + bool split_locked; + float ratio; + float drag_init_ratio; + int32_t container_x; + int32_t container_y; + int32_t container_w; + int32_t container_h; + DwindleNode *parent; + DwindleNode *first; + DwindleNode *second; + Client *client; +}; + +static DwindleNode *dwindle_locked_h_node = NULL; +static DwindleNode *dwindle_locked_v_node = NULL; + +static DwindleNode *dwindle_new_leaf(Client *c) { + DwindleNode *n = calloc(1, sizeof(DwindleNode)); + n->client = c; + return n; +} + +static DwindleNode *dwindle_find_leaf(DwindleNode *node, Client *c) { + if (!node) + return NULL; + if (!node->is_split) + return node->client == c ? node : NULL; + DwindleNode *r = dwindle_find_leaf(node->first, c); + return r ? r : dwindle_find_leaf(node->second, c); +} + +static DwindleNode *dwindle_first_leaf(DwindleNode *node) { + if (!node) + return NULL; + while (node->is_split) + node = node->first; + return node; +} + +static void dwindle_free_tree(DwindleNode *node) { + if (!node) + return; + dwindle_free_tree(node->first); + dwindle_free_tree(node->second); + free(node); +} + +static void dwindle_insert(DwindleNode **root, Client *new_c, Client *focused, + float ratio, bool as_first, bool split_h, + bool lock) { + DwindleNode *new_leaf = dwindle_new_leaf(new_c); + + if (!*root) { + *root = new_leaf; + return; + } + + DwindleNode *target = focused ? dwindle_find_leaf(*root, focused) : NULL; + if (!target) + target = dwindle_first_leaf(*root); + + DwindleNode *split = calloc(1, sizeof(DwindleNode)); + split->is_split = true; + split->ratio = ratio; + split->split_h = split_h; + split->split_locked = lock; + + if (as_first) { + split->first = new_leaf; + split->second = target; + } else { + split->first = target; + split->second = new_leaf; + } + split->parent = target->parent; + target->parent = split; + new_leaf->parent = split; + + if (!split->parent) { + *root = split; + } else { + if (split->parent->first == target) + split->parent->first = split; + else + split->parent->second = split; + } +} + +static void dwindle_remove(DwindleNode **root, Client *c) { + DwindleNode *leaf = dwindle_find_leaf(*root, c); + if (!leaf) + return; + + DwindleNode *parent = leaf->parent; + if (!parent) { + free(leaf); + *root = NULL; + return; + } + + DwindleNode *sibling = + (parent->first == leaf) ? parent->second : parent->first; + DwindleNode *grandparent = parent->parent; + sibling->parent = grandparent; + + /* Preserve split direction on sibling split-nodes when requested. */ + if (!sibling->is_split || + (!config.dwindle_preserve_split && !config.dwindle_smart_split)) { + sibling->container_w = 0; + sibling->container_h = 0; + } + + if (!grandparent) { + *root = sibling; + } else { + if (grandparent->first == parent) + grandparent->first = sibling; + else + grandparent->second = sibling; + } + + free(leaf); + free(parent); +} + +static void dwindle_assign(DwindleNode *node, int32_t ax, int32_t ay, + int32_t aw, int32_t ah, int32_t gap_h, + int32_t gap_v) { + if (!node) + return; + + if (!node->is_split) { + if (node->client) { + struct wlr_box box = {ax, ay, MAX(1, aw), MAX(1, ah)}; + resize(node->client, box, 0); + } + return; + } + + if (!node->split_locked && node->container_w == 0 && node->container_h == 0) + node->split_h = (aw >= ah); + node->container_x = ax; + node->container_y = ay; + node->container_w = aw; + node->container_h = ah; + if (node->split_h) { + int32_t w1 = MAX(1, (int32_t)(aw * node->ratio) - gap_h / 2); + dwindle_assign(node->first, ax, ay, w1, ah, gap_h, gap_v); + dwindle_assign(node->second, ax + w1 + gap_h, ay, aw - w1 - gap_h, ah, + gap_h, gap_v); + } else { + int32_t h1 = MAX(1, (int32_t)(ah * node->ratio) - gap_v / 2); + dwindle_assign(node->first, ax, ay, aw, h1, gap_h, gap_v); + dwindle_assign(node->second, ax, ay + h1 + gap_v, aw, ah - h1 - gap_v, + gap_h, gap_v); + } +} + +static void dwindle_move_client(DwindleNode **root, Client *c, Client *target, + float ratio, int32_t dir) { + if (!c || !target || c == target) + return; + if (!dwindle_find_leaf(*root, c) || !dwindle_find_leaf(*root, target)) + return; + dwindle_remove(root, c); + bool as_first = (dir == UP || dir == LEFT); + bool split_h = (dir == LEFT || dir == RIGHT); + dwindle_insert(root, c, target, ratio, as_first, split_h, true); +} + +static void dwindle_swap_clients(DwindleNode **root, Client *a, Client *b) { + DwindleNode *la = dwindle_find_leaf(*root, a); + DwindleNode *lb = dwindle_find_leaf(*root, b); + if (!la || !lb || la == lb) + return; + la->client = b; + lb->client = a; +} + +static void dwindle_resize_client(Monitor *m, Client *c) { + uint32_t tag = m->pertag->curtag; + DwindleNode *leaf = dwindle_find_leaf(m->pertag->dwindle_root[tag], c); + if (!leaf) + return; + + if (!start_drag_window) { + start_drag_window = true; + dwindle_locked_h_node = NULL; + dwindle_locked_v_node = NULL; + drag_begin_cursorx = cursor->x; + drag_begin_cursory = cursor->y; + DwindleNode *node = leaf->parent; + while (node) { + if (node->split_h && !dwindle_locked_h_node) { + dwindle_locked_h_node = node; + node->drag_init_ratio = node->ratio; + } + if (!node->split_h && !dwindle_locked_v_node) { + dwindle_locked_v_node = node; + node->drag_init_ratio = node->ratio; + } + if (dwindle_locked_h_node && dwindle_locked_v_node) + break; + node = node->parent; + } + } + + if (!dwindle_locked_h_node && !dwindle_locked_v_node) + return; + + if (dwindle_locked_h_node) { + float cw = (float)MAX(1, dwindle_locked_h_node->container_w); + float ox = (float)(cursor->x - drag_begin_cursorx); + if (config.dwindle_smart_resize) { + /* Move the boundary toward the cursor: invert direction when + * the drag started on the right side of the split line. */ + float split_x = dwindle_locked_h_node->container_x + + cw * dwindle_locked_h_node->drag_init_ratio; + if (drag_begin_cursorx >= split_x) + ox = -ox; + } + dwindle_locked_h_node->ratio = + dwindle_locked_h_node->drag_init_ratio + ox / cw; + dwindle_locked_h_node->ratio = + CLAMP_FLOAT(dwindle_locked_h_node->ratio, 0.05f, 0.95f); + } + + if (dwindle_locked_v_node) { + float ch = (float)MAX(1, dwindle_locked_v_node->container_h); + float oy = (float)(cursor->y - drag_begin_cursory); + if (config.dwindle_smart_resize) { + /* Same logic for the vertical split line. */ + float split_y = dwindle_locked_v_node->container_y + + ch * dwindle_locked_v_node->drag_init_ratio; + if (drag_begin_cursory >= split_y) + oy = -oy; + } + dwindle_locked_v_node->ratio = + dwindle_locked_v_node->drag_init_ratio + oy / ch; + dwindle_locked_v_node->ratio = + CLAMP_FLOAT(dwindle_locked_v_node->ratio, 0.05f, 0.95f); + } + + int32_t n = m->visible_tiling_clients; + int32_t gap_ih = enablegaps ? m->gappih : 0; + int32_t gap_iv = enablegaps ? m->gappiv : 0; + int32_t gap_oh = enablegaps ? m->gappoh : 0; + int32_t gap_ov = enablegaps ? m->gappov : 0; + if (config.smartgaps && n == 1) + gap_ih = gap_iv = gap_oh = gap_ov = 0; + + dwindle_assign(m->pertag->dwindle_root[tag], m->w.x + gap_oh, + m->w.y + gap_ov, m->w.width - 2 * gap_oh, + m->w.height - 2 * gap_ov, gap_ih, gap_iv); +} + +static void dwindle_resize_client_step(Monitor *m, Client *c, int32_t dx, + int32_t dy) { + uint32_t tag = m->pertag->curtag; + DwindleNode *leaf = dwindle_find_leaf(m->pertag->dwindle_root[tag], c); + if (!leaf) + return; + + DwindleNode *h_node = NULL; + DwindleNode *v_node = NULL; + DwindleNode *node = leaf->parent; + + while (node) { + if (node->split_h && !h_node) + h_node = node; + if (!node->split_h && !v_node) + v_node = node; + if (h_node && v_node) + break; + node = node->parent; + } + + if (!h_node && !v_node) + return; + + if (h_node && dx) { + float cw = (float)MAX(1, h_node->container_w); + float delta = (float)dx / cw; + h_node->ratio = CLAMP_FLOAT(h_node->ratio + delta, 0.05f, 0.95f); + } + + if (v_node && dy) { + float ch = (float)MAX(1, v_node->container_h); + float delta = (float)dy / ch; + v_node->ratio = CLAMP_FLOAT(v_node->ratio + delta, 0.05f, 0.95f); + } + + int32_t n_clients = m->visible_tiling_clients; + int32_t gap_ih = enablegaps ? m->gappih : 0; + int32_t gap_iv = enablegaps ? m->gappiv : 0; + int32_t gap_oh = enablegaps ? m->gappoh : 0; + int32_t gap_ov = enablegaps ? m->gappov : 0; + if (config.smartgaps && n_clients == 1) + gap_ih = gap_iv = gap_oh = gap_ov = 0; + + dwindle_assign(m->pertag->dwindle_root[tag], m->w.x + gap_oh, + m->w.y + gap_ov, m->w.width - 2 * gap_oh, + m->w.height - 2 * gap_ov, gap_ih, gap_iv); +} + +static void dwindle_remove_client(Client *c) { + Monitor *m; + wl_list_for_each(m, &mons, link) { + for (uint32_t t = 0; t < LENGTH(tags) + 1; t++) + dwindle_remove(&m->pertag->dwindle_root[t], c); + } +} + +/* Insert a new client respecting dwindle_vsplit, dwindle_hsplit, and + * dwindle_smart_split config options. */ +static void dwindle_insert_with_config(DwindleNode **root, Client *new_c, + Client *focused, float ratio) { + bool as_first = false; + bool split_h = false; + bool lock = false; + + if (focused) { + struct wlr_box *fg = &focused->geom; + double fcx = fg->x + fg->width * 0.5; + double fcy = fg->y + fg->height * 0.5; + + if (config.dwindle_smart_split) { + double nx = (cursor->x - fcx) / (fg->width * 0.5); + double ny = (cursor->y - fcy) / (fg->height * 0.5); + + if (fabs(ny) > fabs(nx)) { + split_h = false; // vertical split + as_first = (ny < 0); // top → new window on top + } else { + split_h = true; // horizontal split + as_first = (nx < 0); // left → new window on left + } + lock = true; // lock split direction + } else { + // normal mode, auto split + bool likely_h = (fg->width >= fg->height); + if (likely_h) { + if (config.dwindle_hsplit == 0) + as_first = (cursor->x < fcx); + else + as_first = (config.dwindle_hsplit == 2); + } else { + if (config.dwindle_vsplit == 0) + as_first = (cursor->y < fcy); + else + as_first = (config.dwindle_vsplit == 2); + } + // split_h and lock are false, decided by width/height ratio + } + } + + dwindle_insert(root, new_c, focused, ratio, as_first, split_h, lock); +} + +void dwindle(Monitor *m) { + int32_t n = m->visible_tiling_clients; + if (n == 0) + return; + + uint32_t tag = m->pertag->curtag; + DwindleNode **root = &m->pertag->dwindle_root[tag]; + float ratio = config.dwindle_split_ratio; + + Client *vis[512]; + int32_t count = 0; + Client *c; + wl_list_for_each(c, &clients, link) { + if (VISIBLEON(c, m) && ISTILED(c)) + vis[count++] = c; + if (count >= 512) + break; + } + + { + DwindleNode *leaves[512]; + int32_t lc = 0; + + DwindleNode *stack[1024]; + int32_t sp = 0; + if (*root) + stack[sp++] = *root; + while (sp > 0) { + DwindleNode *nd = stack[--sp]; + if (!nd->is_split) { + leaves[lc++] = nd; + } else { + if (nd->second) + stack[sp++] = nd->second; + if (nd->first) + stack[sp++] = nd->first; + } + } + + for (int32_t i = 0; i < lc; i++) { + bool found = false; + for (int32_t j = 0; j < count; j++) + if (vis[j] == leaves[i]->client) { + found = true; + break; + } + if (!found) + dwindle_remove(root, leaves[i]->client); + } + } + + Client *focused = focustop(m); + if (focused && !dwindle_find_leaf(*root, focused)) + focused = m->sel; + for (int32_t i = 0; i < count; i++) { + if (!dwindle_find_leaf(*root, vis[i])) + dwindle_insert_with_config(root, vis[i], focused, ratio); + } + + int32_t gap_ih = enablegaps ? m->gappih : 0; + int32_t gap_iv = enablegaps ? m->gappiv : 0; + int32_t gap_oh = enablegaps ? m->gappoh : 0; + int32_t gap_ov = enablegaps ? m->gappov : 0; + if (config.smartgaps && n == 1) + gap_ih = gap_iv = gap_oh = gap_ov = 0; + + dwindle_assign(*root, m->w.x + gap_oh, m->w.y + gap_ov, + m->w.width - 2 * gap_oh, m->w.height - 2 * gap_ov, gap_ih, + gap_iv); +} diff --git a/src/layout/layout.h b/src/layout/layout.h index f896ac27..557ce571 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -12,6 +12,7 @@ static void vertical_grid(Monitor *m); static void vertical_scroller(Monitor *m); static void vertical_deck(Monitor *mon); static void tgmix(Monitor *m); +static void dwindle(Monitor *m); /* layout(s) */ Layout overviewlayout = {"󰃇", overview, "overview"}; @@ -29,6 +30,7 @@ enum { VERTICAL_DECK, RIGHT_TILE, TGMIX, + DWINDLE, }; Layout layouts[] = { @@ -47,4 +49,5 @@ Layout layouts[] = { {"VG", vertical_grid, "vertical_grid", VERTICAL_GRID}, // 垂直格子布局 {"VK", vertical_deck, "vertical_deck", VERTICAL_DECK}, // 垂直卡片布局 {"TG", tgmix, "tgmix", TGMIX}, // 混合布局 + {"DW", dwindle, "dwindle", DWINDLE}, }; \ No newline at end of file diff --git a/src/mango.c b/src/mango.c index 36deda3c..17b99dca 100644 --- a/src/mango.c +++ b/src/mango.c @@ -560,6 +560,8 @@ typedef struct { struct wl_listener destroy; } SessionLock; +typedef struct DwindleNode DwindleNode; + /* function declarations */ static void applybounds( Client *c, @@ -815,6 +817,11 @@ static void client_pending_maximized_state(Client *c, int32_t ismaximized); static void client_pending_minimized_state(Client *c, int32_t isminimized); static void scroller_insert_stack(Client *c, Client *target_client, bool insert_before); +static void dwindle_move_client(DwindleNode **root, Client *c, Client *target, + float ratio, int32_t dir); +static void dwindle_resize_client_step(Monitor *m, Client *c, int32_t dx, + int32_t dy); +static void dwindle_resize_client(Monitor *m, Client *c); #include "data/static_keymap.h" #include "dispatch/bind_declare.h" @@ -946,6 +953,7 @@ struct Pertag { int32_t no_hide[LENGTH(tags) + 1]; /* no_hide per tag */ int32_t no_render_border[LENGTH(tags) + 1]; /* no_render_border per tag */ int32_t open_as_floating[LENGTH(tags) + 1]; /* open_as_floating per tag */ + struct DwindleNode *dwindle_root[LENGTH(tags) + 1]; const Layout *ltidxs[LENGTH(tags) + 1]; /* matrix of tags and layouts indexes */ }; @@ -1018,6 +1026,7 @@ static struct wl_event_source *sync_keymap; #include "ext-protocol/all.h" #include "fetch/fetch.h" #include "layout/arrange.h" +#include "layout/dwindle.h" #include "layout/horizontal.h" #include "layout/vertical.h" @@ -1179,6 +1188,17 @@ void swallow(Client *c, Client *w) { client_pending_fullscreen_state(c, w->isfullscreen); client_pending_maximized_state(c, w->ismaximizescreen); client_pending_minimized_state(c, w->isminimized); + + Monitor *m; + wl_list_for_each(m, &mons, link) { + for (uint32_t t = 0; t < LENGTH(tags) + 1; t++) { + DwindleNode **root = &m->pertag->dwindle_root[t]; + dwindle_remove(root, c); + DwindleNode *wn = dwindle_find_leaf(*root, w); + if (wn) + wn->client = c; + } + } } bool switch_scratchpad_client_state(Client *c) { @@ -2066,6 +2086,13 @@ Client *find_closest_tiled_client(Client *c) { if (tc == c || !ISTILED(tc) || !VISIBLEON(tc, c->mon)) continue; + if (cursor->x >= tc->geom.x && + cursor->x < tc->geom.x + tc->geom.width && + cursor->y >= tc->geom.y && + cursor->y < tc->geom.y + tc->geom.height) { + return tc; + } + int32_t dx = tc->geom.x + (int32_t)(tc->geom.width / 2) - cursor->x; int32_t dy = tc->geom.y + (int32_t)(tc->geom.height / 2) - cursor->y; long dist = (long)dx * dx + (long)dy * dy; @@ -2181,8 +2208,8 @@ void place_drag_tile_client(Client *c) { closest->mon->pertag->ltidxs[closest->mon->pertag->curtag]; if (closest->drop_direction == UNDIR) { - exchange_two_client(c, closest); setfloating(c, 0); + exchange_two_client(c, closest); return; } @@ -2194,6 +2221,18 @@ void place_drag_tile_client(Client *c) { try_scroller_drop(c, closest, 1); return; } + if (layout->id == DWINDLE) { + uint32_t tag = c->mon->pertag->curtag; + bool insert_before = (closest->drop_direction == LEFT || + closest->drop_direction == UP); + bool split_h = (closest->drop_direction == LEFT || + closest->drop_direction == RIGHT); + dwindle_insert(&c->mon->pertag->dwindle_root[tag], c, closest, + config.dwindle_split_ratio, insert_before, split_h, + true); + setfloating(c, 0); + return; + } if (closest->drop_direction == LEFT || closest->drop_direction == UP) { wl_list_remove(&c->link); @@ -2507,6 +2546,9 @@ void cleanupmon(struct wl_listener *listener, void *data) { m->skip_frame_timeout = NULL; } m->wlr_output->data = NULL; + + for (uint32_t t = 0; t < LENGTH(tags) + 1; t++) + dwindle_free_tree(m->pertag->dwindle_root[t]); free(m->pertag); free(m); } @@ -5101,10 +5143,20 @@ void exchange_two_client(Client *c1, Client *c2) { tmp_tags = c2->tags; setmon(c2, c1->mon, c1->tags, false); setmon(c1, tmp_mon, tmp_tags, false); + if (c1->mon && + c1->mon->pertag->ltidxs[c1->mon->pertag->curtag]->id == DWINDLE) + dwindle_swap_clients( + &c1->mon->pertag->dwindle_root[c1->mon->pertag->curtag], c1, + c2); arrange(c1->mon, false, false); arrange(c2->mon, false, false); focusclient(c1, 0); } else { + if (c1->mon && + c1->mon->pertag->ltidxs[c1->mon->pertag->curtag]->id == DWINDLE) + dwindle_swap_clients( + &c1->mon->pertag->dwindle_root[c1->mon->pertag->curtag], c1, + c2); arrange(c1->mon, false, false); } @@ -6343,6 +6395,7 @@ void unmapnotify(struct wl_listener *listener, void *data) { c->next_in_stack = NULL; c->prev_in_stack = NULL; + dwindle_remove_client(c); wlr_scene_node_destroy(&c->scene->node); printstatus(); motionnotify(0, NULL, 0, 0, 0, 0); From 4981b07a58fd17194d82cb95904793d623ad585e Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Sat, 9 May 2026 22:07:21 +0800 Subject: [PATCH 3/6] break change: remove tgmix layout --- README.md | 2 +- docs/ipc.md | 2 +- docs/window-management/layouts.md | 2 +- src/fetch/client.h | 11 +---------- src/layout/arrange.h | 3 +-- src/layout/horizontal.h | 13 +------------ src/layout/layout.h | 3 --- 7 files changed, 6 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index bb48cd97..d3f6e7cf 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ https://github.com/user-attachments/assets/bb83004a-0563-4b48-ad89-6461a9b78b1f - vertical_tile - vertical_grid - vertical_scroller -- tgmix +- dwindle # Installation diff --git a/docs/ipc.md b/docs/ipc.md index 72beefb5..8bb0f5c1 100644 --- a/docs/ipc.md +++ b/docs/ipc.md @@ -64,7 +64,7 @@ mmsg -s -t 2^ ### Layouts -Switch layouts programmatically. Layout codes: `S` (Scroller), `T` (Tile), `G` (Grid), `M` (Monocle), `K` (Deck), `CT` (Center Tile), `RT` (Right Tile), `VS` (Vertical Scroller), `VT` (Vertical Tile), `VG` (Vertical Grid), `VK` (Vertical Deck), `TG` (TGMix). +Switch layouts programmatically. Layout codes: `S` (Scroller), `T` (Tile), `G` (Grid), `M` (Monocle), `K` (Deck), `CT` (Center Tile), `RT` (Right Tile), `VS` (Vertical Scroller), `VT` (Vertical Tile), `VG` (Vertical Grid), `VK` (Vertical Deck), `DW` (Dwindle). ```bash # Switch to Scroller diff --git a/docs/window-management/layouts.md b/docs/window-management/layouts.md index 26c05fe4..9b957b29 100644 --- a/docs/window-management/layouts.md +++ b/docs/window-management/layouts.md @@ -18,7 +18,7 @@ mangowm supports a variety of layouts that can be assigned per tag. - `vertical_scroller` - `vertical_grid` - `vertical_deck` -- `tgmix` +- `dwindle` --- diff --git a/src/fetch/client.h b/src/fetch/client.h index 8fe831be..be9be420 100644 --- a/src/fetch/client.h +++ b/src/fetch/client.h @@ -558,7 +558,7 @@ bool client_is_in_same_stack(Client *sc, Client *tc, Client *fc) { if (id != SCROLLER && id != VERTICAL_SCROLLER && id != TILE && id != VERTICAL_TILE && id != DECK && id != VERTICAL_DECK && - id != CENTER_TILE && id != RIGHT_TILE && id != TGMIX) + id != CENTER_TILE && id != RIGHT_TILE) return false; if (id == SCROLLER || id == VERTICAL_SCROLLER) { @@ -583,15 +583,6 @@ bool client_is_in_same_stack(Client *sc, Client *tc, Client *fc) { return true; } - if (id == TGMIX) { - if (tc->ismaster ^ sc->ismaster) - return false; - if (fc && !(fc->ismaster ^ sc->ismaster)) - return false; - if (!sc->ismaster && sc->mon->visible_tiling_clients <= 3) - return true; - } - if (id == CENTER_TILE) { if (tc->ismaster ^ sc->ismaster) return false; diff --git a/src/layout/arrange.h b/src/layout/arrange.h index 9e0c6fa7..b822a729 100644 --- a/src/layout/arrange.h +++ b/src/layout/arrange.h @@ -707,8 +707,7 @@ void resize_tile_client(Client *grabc, bool isdrag, int32_t offsetx, const Layout *current_layout = grabc->mon->pertag->ltidxs[grabc->mon->pertag->curtag]; if (current_layout->id == TILE || current_layout->id == DECK || - current_layout->id == CENTER_TILE || current_layout->id == RIGHT_TILE || - (current_layout->id == TGMIX && grabc->mon->visible_tiling_clients <= 3) + current_layout->id == CENTER_TILE || current_layout->id == RIGHT_TILE ) { resize_tile_master_horizontal(grabc, isdrag, offsetx, offsety, time, diff --git a/src/layout/horizontal.h b/src/layout/horizontal.h index eaa7b5c2..e69b4cdd 100644 --- a/src/layout/horizontal.h +++ b/src/layout/horizontal.h @@ -993,15 +993,4 @@ monocle(Monitor *m) { } if ((c = focustop(m))) wlr_scene_node_raise_to_top(&c->scene->node); -} - -void tgmix(Monitor *m) { - int32_t n = m->visible_tiling_clients; - if (n <= 3) { - tile(m); - return; - } else { - grid(m); - return; - } -} +} \ No newline at end of file diff --git a/src/layout/layout.h b/src/layout/layout.h index 557ce571..16773dc7 100644 --- a/src/layout/layout.h +++ b/src/layout/layout.h @@ -11,7 +11,6 @@ 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 tgmix(Monitor *m); static void dwindle(Monitor *m); /* layout(s) */ @@ -29,7 +28,6 @@ enum { VERTICAL_GRID, VERTICAL_DECK, RIGHT_TILE, - TGMIX, DWINDLE, }; @@ -48,6 +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}, // 垂直卡片布局 - {"TG", tgmix, "tgmix", TGMIX}, // 混合布局 {"DW", dwindle, "dwindle", DWINDLE}, }; \ No newline at end of file From 637dc0a09a5e46fbf2e8375e7e5b9a03e13d4288 Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Sat, 9 May 2026 22:11:18 +0800 Subject: [PATCH 4/6] opt: set dwindle_drop_simple_split default to 1 --- src/config/parse_config.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/parse_config.h b/src/config/parse_config.h index ceaec81c..006968f0 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -3356,7 +3356,7 @@ void set_value_default() { config.dwindle_preserve_split = 0; config.dwindle_smart_split = 0; config.dwindle_smart_resize = 0; - config.dwindle_drop_simple_split = 0; + config.dwindle_drop_simple_split = 1; config.dwindle_split_ratio = 0.5f; config.log_level = WLR_ERROR; From b0f8d3f8a3f9655a63a6b4061889b85843d9cb4d Mon Sep 17 00:00:00 2001 From: DreamMaoMao <2523610504@qq.com> Date: Sat, 9 May 2026 22:16:00 +0800 Subject: [PATCH 5/6] opt: more simple drop area --- src/animation/client.h | 86 ++++++++++++++++++++++++++++++------------ 1 file changed, 61 insertions(+), 25 deletions(-) diff --git a/src/animation/client.h b/src/animation/client.h index ba48959d..8ce81cd2 100644 --- a/src/animation/client.h +++ b/src/animation/client.h @@ -514,6 +514,9 @@ void client_set_drop_area(Client *c) { bool first_draw = false; int32_t drop_direction = UNDIR; + if (!c || !c->mon) + return; + if (!c->enable_drop_area_draw && !c->droparea->node.enabled) { return; } @@ -538,22 +541,21 @@ void client_set_drop_area(Client *c) { struct wlr_box drop_box; - const Layout *cur_layout = - c->mon ? c->mon->pertag->ltidxs[c->mon->pertag->curtag] : NULL; - bool dwindle_familiar = cur_layout && cur_layout->id == DWINDLE && - config.dwindle_drop_simple_split; + const Layout *cur_layout = c->mon->pertag->ltidxs[c->mon->pertag->curtag]; + bool dwindle_familiar = + cur_layout->id == DWINDLE && config.dwindle_drop_simple_split; - // 中心区域:x和y都在30%~70%之间 → 无方向 - if (rel_x > client_width * 0.3 && rel_x < client_width * 0.7 && - rel_y > client_height * 0.3 && rel_y < client_height * 0.7) { - drop_box.x = bw + client_width * 0.3; - drop_box.y = bw + client_height * 0.3; - drop_box.width = client_width * 0.4; - drop_box.height = client_height * 0.4; - drop_direction = UNDIR; - } else if (dwindle_familiar) { - // Mirror dwindle_assign's split_h = (aw >= ah) rule so the preview - // matches the auto-rotated split that dwindle will produce. + uint32_t nmaster = c->mon->pertag->nmasters[c->mon->pertag->curtag]; + + bool should_swap = + (cur_layout->id == DECK || cur_layout->id == VERTICAL_DECK || + cur_layout->id == MONOCLE || cur_layout->id == GRID || + cur_layout->id == VERTICAL_GRID) || + ((cur_layout->id == TILE || cur_layout->id == VERTICAL_TILE || + cur_layout->id == CENTER_TILE || cur_layout->id == RIGHT_TILE) && + nmaster == 1 && c->ismaster); + + if (dwindle_familiar) { bool split_h = c->geom.width >= c->geom.height; float ratio = config.dwindle_split_ratio; if (split_h) { @@ -582,46 +584,80 @@ void client_set_drop_area(Client *c) { drop_box.x = bw; drop_box.y = bw + (int32_t)(client_height * ratio); drop_box.width = client_width; - drop_box.height = client_height - (int32_t)(client_height * ratio); + drop_box.height = + client_height - (int32_t)(client_height * ratio); } } + } else if (should_swap) { + drop_box.x = bw; + drop_box.y = bw; + drop_box.width = client_width; + drop_box.height = client_height; + drop_direction = UNDIR; + } else if (cur_layout->id == TILE || cur_layout->id == DECK || + cur_layout->id == CENTER_TILE || cur_layout->id == RIGHT_TILE) { + if (rel_y < client_height * 0.5) { + drop_direction = UP; + drop_box.x = bw; + drop_box.y = bw; + drop_box.width = client_width; + drop_box.height = client_height / 2; + } else { + drop_direction = DOWN; + drop_box.x = bw; + drop_box.y = bw + client_height / 2; + drop_box.width = client_width; + drop_box.height = client_height / 2; + } + } else if (cur_layout->id == VERTICAL_TILE || + cur_layout->id == VERTICAL_DECK) { + if (rel_x < client_width * 0.5) { + drop_direction = LEFT; + drop_box.x = bw; + drop_box.y = bw; + drop_box.width = client_width / 2; + drop_box.height = client_height; + } else { + drop_direction = RIGHT; + drop_box.x = bw + client_width / 2; + drop_box.y = bw; + drop_box.width = client_width / 2; + drop_box.height = client_height; + } } else { - // 否则根据到各边的距离决定方向 double dist_left = rel_x; double dist_right = client_width - rel_x; double dist_top = rel_y; double dist_bottom = client_height - rel_y; - // 找出最小距离的方向(相等时按左、右、上、下的优先级顺序) if (dist_left <= dist_right && dist_left <= dist_top && dist_left <= dist_bottom) { drop_direction = LEFT; drop_box.x = bw; drop_box.y = bw; - drop_box.width = client_width * 0.3; + drop_box.width = client_width / 2; drop_box.height = client_height; } else if (dist_right <= dist_top && dist_right <= dist_bottom) { drop_direction = RIGHT; - drop_box.x = bw + client_width * 0.7; + drop_box.x = bw + client_width / 2; drop_box.y = bw; - drop_box.width = client_width * 0.3; + drop_box.width = client_width / 2; drop_box.height = client_height; } else if (dist_top <= dist_bottom) { drop_direction = UP; drop_box.x = bw; drop_box.y = bw; drop_box.width = client_width; - drop_box.height = client_height * 0.3; + drop_box.height = client_height / 2; } else { drop_direction = DOWN; drop_box.x = bw; - drop_box.y = bw + client_height * 0.7; + drop_box.y = bw + client_height / 2; drop_box.width = client_width; - drop_box.height = client_height * 0.3; + drop_box.height = client_height / 2; } } - // 如果方向和上次相同且不是第一次绘制,则跳过更新 if (!first_draw && c->drop_direction == drop_direction) { return; } From be575c4d24eda25571a0a46aac80f52717abf372 Mon Sep 17 00:00:00 2001 From: Ernesto Cruz Date: Sat, 9 May 2026 15:34:00 +0000 Subject: [PATCH 6/6] docs: dwindle docs --- docs/window-management/layouts.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/window-management/layouts.md b/docs/window-management/layouts.md index 9b957b29..9b0b8537 100644 --- a/docs/window-management/layouts.md +++ b/docs/window-management/layouts.md @@ -83,6 +83,35 @@ default_nmaster=1 --- +## Dwindle Layout + +The Dwindle layout arranges windows as a binary tree of recursive splits. Each new window splits the focused window's container, producing a spiral-like tiling. + +### Configuration + +| Setting | Default | Description | +| :--- | :--- | :--- | +| `dwindle_split_ratio` | `0.5` | Ratio used for new splits (`0.05`–`0.95`). | +| `dwindle_smart_split` | `0` | Pick the split axis from the cursor's position inside the focused window. The new window appears on the cursor's side. | +| `dwindle_hsplit` | `0` | Side-by-side splits: where the new window goes. `0` = follow cursor, `1` = right, `2` = left. | +| `dwindle_vsplit` | `0` | Top/bottom splits: where the new window goes. `0` = follow cursor, `1` = below, `2` = above. | +| `dwindle_preserve_split` | `0` | Keep the sibling's split orientation when a window is closed. | +| `dwindle_smart_resize` | `0` | When dragging to resize, move the split toward the cursor regardless of which side was grabbed. | +| `dwindle_drop_simple_split` | `1` | Drag-to-tile drop preview. `1` = 2-zone preview matching `dwindle_split_ratio`, `0` = 4-quadrant preview. | + +```ini +# Example dwindle configuration +dwindle_split_ratio=0.5 +dwindle_smart_split=0 +dwindle_hsplit=0 +dwindle_vsplit=0 +dwindle_preserve_split=0 +dwindle_smart_resize=0 +dwindle_drop_simple_split=1 +``` + +--- + ## Switching Layouts You can switch layouts dynamically or set a default for specific tags using [Tag Rules](/docs/window-management/rules#tag-rules).