Merge pull request #907 from mangowm/dwindle

feat: dwindle layout support
This commit is contained in:
DreamMaoMao 2026-05-10 09:31:33 +08:00 committed by GitHub
commit ed03dacd22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 682 additions and 49 deletions

View file

@ -48,7 +48,7 @@ https://github.com/user-attachments/assets/bb83004a-0563-4b48-ad89-6461a9b78b1f
- vertical_tile
- vertical_grid
- vertical_scroller
- tgmix
- dwindle
# Installation

View file

@ -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

View file

@ -18,7 +18,7 @@ mangowm supports a variety of layouts that can be assigned per tag.
- `vertical_scroller`
- `vertical_grid`
- `vertical_deck`
- `tgmix`
- `dwindle`
---
@ -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).

View file

@ -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,51 +541,123 @@ void client_set_drop_area(Client *c) {
struct wlr_box drop_box;
// 中心区域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;
const Layout *cur_layout = c->mon->pertag->ltidxs[c->mon->pertag->curtag];
bool dwindle_familiar =
cur_layout->id == DWINDLE && config.dwindle_drop_simple_split;
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) {
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 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;
}

View file

@ -242,6 +242,15 @@ 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;
int32_t dwindle_drop_simple_split;
float dwindle_split_ratio;
uint32_t hotarea_size;
uint32_t hotarea_corner;
uint32_t enable_hotarea;
@ -1615,6 +1624,20 @@ 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_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) {
config->hotarea_size = atoi(value);
} else if (strcmp(key, "hotarea_corner") == 0) {
@ -3179,6 +3202,16 @@ 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_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);
config.hotarea_corner = CLAMP_INT(config.hotarea_corner, 0, 3);
config.enable_hotarea = CLAMP_INT(config.enable_hotarea, 0, 1);
@ -3318,6 +3351,14 @@ 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_drop_simple_split = 1;
config.dwindle_split_ratio = 0.5f;
config.log_level = WLR_ERROR;
config.numlockon = 0;
config.capslock = 0;
@ -3716,7 +3757,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 +3766,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 +3913,7 @@ void reset_option(void) {
run_exec();
reapply_cursor_style();
reapply_border();
reapply_property();
reapply_rootbg();
reapply_keyboard();
reapply_pointer();

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
@ -692,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,
@ -706,6 +720,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);
}
}

433
src/layout/dwindle.h Normal file
View file

@ -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);
}

View file

@ -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;
}
}
}

View file

@ -11,7 +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 tgmix(Monitor *m);
static void dwindle(Monitor *m);
/* layout(s) */
Layout overviewlayout = {"󰃇", overview, "overview"};
@ -28,7 +28,7 @@ enum {
VERTICAL_GRID,
VERTICAL_DECK,
RIGHT_TILE,
TGMIX,
DWINDLE,
};
Layout layouts[] = {
@ -46,5 +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},
};

View file

@ -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,
!config.dwindle_drop_simple_split);
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);