Commit 86ef802692f5
tarball7 files changed+183-5
@@ -9,6 +9,9 @@ startup = []
99
# keeps super for itself.1010
mod = "super"1111
gaps = { inner = 8, outer = 8 }12
+# A new window splits the one it opens beside along that window's longer side,13
+# so windows dwindle. When off it joins the container of that window instead.14
+autotile = true1215
# What a window move does when it meets a neighbour. swap trades places,1316
# merge joins its lane. invert-move does the other one for the next move.1417
move = "swap"@@ -31,6 +31,9 @@ pub struct General {
3131
#[serde(rename = "mod")]3232
pub mod_key: Modifier,3333
pub gaps: Gaps,34
+ /// A new window splits the one it opens beside along that window's35
+ /// longer side, instead of joining its container.36
+ pub autotile: bool,3437
/// What a window move does when it meets a neighbour.3538
#[serde(rename = "move")]3639
pub move_mode: MoveMode,@@ -56,6 +59,7 @@ impl Default for General {
5659
Self {5760
mod_key: Modifier::Super,5861
gaps: Gaps::default(),62
+ autotile: true,5963
move_mode: MoveMode::Swap,6064
focus_follows_mouse: false,6165
mouse_follows_focus: false,@@ -130,6 +130,7 @@ pub struct Layout {
130130
last_scratchpad: Option<usize>,131131
parked: HashMap<String, Parked>,132132
pub gaps: Gaps,133
+ pub autotile: bool,133134
}134135
135136
impl Default for Layout {@@ -151,6 +152,7 @@ impl Default for Layout {
151152
last_scratchpad: None,152153
parked: HashMap::new(),153154
gaps: Gaps::default(),155
+ autotile: false,154156
}155157
}156158
}@@ -395,13 +397,34 @@ impl Layout {
395397
.copied()396398
.find(|&h| tree.contains(h) && self.windows[h].tag == tag)397399
});400
+ let split = anchor401
+ .filter(|_| self.autotile)402
+ .and_then(|a| self.split_axis(o, a));398403
let tree = &mut self.outputs[o].tree;399
- match anchor {400
- Some(a) => tree.insert_after(w, a),401
- None => tree.insert_row(w),404
+ match (anchor, split) {405
+ (Some(a), Some(axis)) => tree.insert_split(w, a, axis),406
+ (Some(a), None) => tree.insert_after(w, a),407
+ (None, _) => tree.insert_row(w),402408
}403409
}404410
411
+ /// The axis a window opening beside `anchor` should split it along, the412
+ /// way the anchor is laid out right now. Hidden anchors have no shape.413
+ fn split_axis(&self, o: OutputId, anchor: WindowId) -> Option<Axis> {414
+ let rect = self415
+ .arrange(o)416
+ .tiled417
+ .iter()418
+ .find(|p| p.window == anchor && p.visible)?419
+ .rect;420
+ let axis = if rect.w > rect.h {421
+ Axis::Horizontal422
+ } else {423
+ Axis::Vertical424
+ };425
+ (rect.w > 0.0 && rect.h > 0.0).then_some(axis)426
+ }427
+405428
fn unplace(&mut self, w: WindowId) {406429
let Some(o) = self.windows[w].output else {407430
return;@@ -188,8 +188,14 @@ proptest! {
188188
#![proptest_config(ProptestConfig::with_cases(2000))]189189
190190
#[test]191
- fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {192
- let mut l = Layout::default();191
+ fn random_commands_keep_the_model_sound(192
+ ops in prop::collection::vec(op(), 1..80),193
+ autotile in any::<bool>(),194
+ ) {195
+ let mut l = Layout {196
+ autotile,197
+ ..Layout::default()198
+ };193199
l.add_output("a", AREA);194200
l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));195201
let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];@@ -502,3 +508,106 @@ fn fullscreen_leaves_its_transients_reachable() {
502508
assert!(!l.covered(stranger));503509
assert_eq!(l.window(dialog).and_then(|w| w.parent), None);504510
}511
+512
+fn rect_of(t: &Tree<u32>, w: u32, vis: impl Fn(u32) -> bool) -> Rect {513
+ t.arrange(AREA, 0.0, vis)514
+ .into_iter()515
+ .find(|p| p.0 == w)516
+ .unwrap()517
+ .1518
+}519
+520
+#[test]521
+fn splitting_a_lone_window_shares_its_space() {522
+ let (a, b, c) = (1, 2, 3);523
+ let mut t: Tree<u32> = Tree::default();524
+ t.insert_row(a);525
+ t.insert_split(b, a, Axis::Horizontal);526
+ t.check();527
+ let (ra, rb) = (rect_of(&t, a, |_| true), rect_of(&t, b, |_| true));528
+ assert_eq!((ra.w, rb.w, ra.h), (AREA.w / 2.0, AREA.w / 2.0, AREA.h));529
+ assert!(ra.x < rb.x);530
+531
+ t.insert_split(c, b, Axis::Vertical);532
+ t.check();533
+ let (rb, rc) = (rect_of(&t, b, |_| true), rect_of(&t, c, |_| true));534
+ assert_eq!(535
+ (rb.h, rc.h, rb.w),536
+ (AREA.h / 2.0, AREA.h / 2.0, AREA.w / 2.0)537
+ );538
+ assert!(rb.y < rc.y);539
+ assert_eq!(rect_of(&t, a, |_| true), ra, "the sibling is undisturbed");540
+}541
+542
+#[test]543
+fn splitting_along_the_parents_axis_appends_a_sibling() {544
+ let (a, b, c, d) = (1, 2, 3, 4);545
+ let mut t: Tree<u32> = Tree::default();546
+ t.insert_row(a);547
+ t.insert_split(b, a, Axis::Horizontal);548
+ t.insert_split(c, b, Axis::Vertical);549
+ let before = rect_of(&t, a, |_| true);550
+ t.insert_split(d, a, Axis::Horizontal);551
+ t.check();552
+ let all = |_| true;553
+ let (ra, rd, rb) = (554
+ rect_of(&t, a, all),555
+ rect_of(&t, d, all),556
+ rect_of(&t, b, all),557
+ );558
+ assert_eq!((ra.w, rd.w), (before.w / 2.0, before.w / 2.0));559
+ assert!(ra.x < rd.x && rd.x < rb.x);560
+ assert_eq!(rb.w, AREA.w / 2.0, "the neighbour keeps its share");561
+}562
+563
+#[test]564
+fn splitting_beside_hidden_windows_leaves_their_slots_alone() {565
+ let (a, b, c, d) = (1, 2, 3, 4);566
+ let mut t: Tree<u32> = Tree::default();567
+ t.insert_row(a);568
+ t.insert_after(b, a);569
+ t.insert_after(c, a);570
+ let tag_one = |w: u32| w == a || w == d;571
+ assert_eq!(rect_of(&t, a, tag_one).h, AREA.h);572
+573
+ t.insert_split(d, a, Axis::Vertical);574
+ t.check();575
+ let (ra, rd) = (rect_of(&t, a, tag_one), rect_of(&t, d, tag_one));576
+ assert_eq!((ra.h, rd.h, ra.w), (AREA.h / 2.0, AREA.h / 2.0, AREA.w));577
+ assert!(ra.y < rd.y);578
+579
+ t.insert_split(d + 1, a, Axis::Horizontal);580
+ t.check();581
+ let shown = |w: u32| w == a || w == d + 1;582
+ let (ra, re) = (rect_of(&t, a, shown), rect_of(&t, d + 1, shown));583
+ assert_eq!((ra.w, re.w), (AREA.w / 2.0, AREA.w / 2.0));584
+ assert!(ra.x < re.x);585
+ assert_eq!(t.windows().count(), 5, "hidden windows stay in the tree");586
+}587
+588
+#[test]589
+fn autotile_dwindles_by_the_anchors_shape() {590
+ let mut l = Layout {591
+ autotile: true,592
+ ..Layout::default()593
+ };594
+ let o = l.add_output("a", AREA);595
+ let tag = l.tag("1");596
+ l.view(o, tag);597
+ let mut ids = Vec::new();598
+ for _ in 0..4 {599
+ let w = l.add_window(Some(o), Some(tag), None);600
+ l.focus(w);601
+ ids.push(w);602
+ l.check();603
+ }604
+ let arr = l.arrange(o);605
+ let r = |w| arr.tiled.iter().find(|p| p.window == w).unwrap().rect;606
+ let [a, b, c, d] = [ids[0], ids[1], ids[2], ids[3]];607
+ assert!(608
+ r(a).x < r(b).x && r(a).h > r(b).h * 1.5,609
+ "{a:?} left, {b:?} right"610
+ );611
+ assert!(r(b).y < r(c).y && r(b).x == r(c).x);612
+ assert!(r(c).x < r(d).x && r(c).y == r(d).y);613
+}@@ -147,6 +147,43 @@ impl<W: Copy + Eq + Hash> Tree<W> {
147147
}148148
}149149
150
+ /// Inserts `w` after `anchor` by splitting `anchor` along `axis`. They151
+ /// share the space `anchor` had, and its siblings keep theirs.152
+ pub fn insert_split(&mut self, w: W, anchor: W, axis: Axis) {153
+ let Some(&a) = self.leaves.get(&anchor) else {154
+ return self.insert_row(w);155
+ };156
+ let size = self.size(a);157
+ let n = self.new_leaf(w, size);158
+ match self.position(a) {159
+ Some((p, i)) if self.axis(p) == Some(axis) => {160
+ self.halve(a, axis);161
+ self.halve(n, axis);162
+ self.attach(p, i + 1, n);163
+ }164
+ Some((p, i)) => {165
+ self.unlink(p, i);166
+ let c = self.new_container(axis);167
+ self.attach(c, 0, a);168
+ self.attach(c, 1, n);169
+ self.attach(p, i, c);170
+ }171
+ None => {172
+ let c = self.new_container(axis);173
+ self.attach(c, 0, a);174
+ self.attach(c, 1, n);175
+ self.root = Some(c);176
+ }177
+ }178
+ self.reveal(n, true);179
+ }180
+181
+ fn halve(&mut self, leaf: NodeId, axis: Axis) {182
+ if let Kind::Leaf(_, size) = &mut self.nodes[leaf].kind {183
+ size[axis.index()] /= 2.0;184
+ }185
+ }186
+150187
/// Appends `w` as a row of its own along the stacking axis.151188
pub fn insert_row(&mut self, w: W) {152189
let size = self.root.map_or([1.0; 2], |r| self.size(r));@@ -221,6 +221,7 @@ impl State {
221221
let input_changed = config.input != self.settings.config.input;222222
self.settings.binds = Binds::new(&config);223223
self.wm.layout.gaps = crate::state::gaps(&config);224
+ self.wm.layout.autotile = config.general.autotile;224225
self.wm.anim.settings = config.animation.clone();225226
self.settings.config = config;226227
if keyboard_changed {@@ -150,6 +150,7 @@ impl State {
150150
151151
let mut layout = Layout::default();152152
layout.gaps = gaps(&config);153
+ layout.autotile = config.general.autotile;153154
let animation = config.animation.clone();154155
let imports = crate::import::Imports::new(155156
&handle,