solium_layout/tree.rs
1//! Dwindle, as Hyprland actually does it: a tree, not a formula.
2//!
3//! The arrangement people mean by "Hyprland tiling" cannot be written as a
4//! function of how many windows there are. Read `addTarget` in Hyprland's
5//! `DwindleAlgorithm.cpp` and the reason is immediate: a new window splits **a
6//! specific existing window** — the one under the mouse, or the focused one —
7//! and the split runs across whichever axis *that window's own box* is longer
8//! on. Two windows opened in a different order, or with the cursor somewhere
9//! else, give different layouts. There is no count to compute from.
10//!
11//! So this is a binary tree. Leaves are windows; every branch remembers which
12//! way it was cut and where. Inserting replaces a leaf with a branch holding
13//! the old window and the new one. Removing replaces a branch with whichever
14//! child is left, which is what makes closing a window hand its space back to
15//! its neighbour rather than reshuffling the screen.
16//!
17//! Per-branch ratios are the other half of it: resizing a window moves one
18//! seam, and everything on the far side of the tree stays exactly where it is.
19//!
20//! [`Tiling::insert`] splits the tile it is pointed at however small that tile
21//! already is, so tiles go on halving for as long as windows keep opening.
22//! [`Settings::minimum`] is the floor (#134). `insert` still ignores it, because
23//! it is what a layout falls back to when nothing else will do; the two
24//! `insert_*` methods beside it refuse a split that would go under it, so a
25//! layout can try them first, and the seam movers will not move a seam to take
26//! a tile under it either.
27//!
28//! A window's own floor is the other bound (#115): what its application says
29//! it cannot go under, frame included, handed in with [`Tiling::set_floors`].
30//! It is the one thing that moves a seam nobody dragged. [`Tiling::layout`]
31//! lays a split out at its own ratio, then moves it just far enough for a side
32//! under a window's floor to reach it, taking the room from the other side down
33//! to *its* floors and [`Settings::minimum`], and no further. The ratio the
34//! branch holds is left as it was, so a window whose floor goes away hands the
35//! room back and the arrangement is the one the user made. Where the room is
36//! not there, the window is laid out short and [`Tiling::cramped`] says so.
37//! The splits and the seams are held to the same floors: `insert_fitting` and
38//! `insert_largest` refuse a split that would put a window under its own, and
39//! a seam stops at the floors on either side of it. Each of those sentences is
40//! a test in `floor_tests`.
41
42use crate::{Floors, Minimum, Rect, Settings};
43
44/// How far under the minimum a tile may come out and still count as at it.
45///
46/// A split's halves are `f64` arithmetic on a box that has already been
47/// through a division or two, so a tile laid out at exactly the minimum can
48/// land a rounding error short of it. A millionth of a pixel is far below
49/// anything drawn and far above that error.
50const SLACK: f64 = 1e-6;
51
52/// Which way a branch was cut.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub enum Axis {
55 /// Children sit side by side.
56 Vertical,
57 /// Children sit one above the other.
58 Horizontal,
59}
60
61impl Axis {
62 /// The cut a box would get from [`Tiling::insert`]: across whichever side
63 /// is longer, so the split follows the shape of the space being divided.
64 fn longer(rect: Rect) -> Self {
65 if rect.w >= rect.h {
66 Self::Vertical
67 } else {
68 Self::Horizontal
69 }
70 }
71
72 /// The other one.
73 const fn across(self) -> Self {
74 match self {
75 Self::Vertical => Self::Horizontal,
76 Self::Horizontal => Self::Vertical,
77 }
78 }
79
80 /// The side of a size this cut divides: the width for children side by
81 /// side, the height for children one above the other.
82 const fn of(self, size: Minimum) -> f64 {
83 match self {
84 Self::Vertical => size.w,
85 Self::Horizontal => size.h,
86 }
87 }
88
89 /// The same, of a rectangle.
90 const fn extent(self, rect: Rect) -> f64 {
91 match self {
92 Self::Vertical => rect.w,
93 Self::Horizontal => rect.h,
94 }
95 }
96}
97
98/// Which side of a window was grabbed.
99///
100/// Not an axis, and the difference is the whole of #120. A window that is the
101/// right-hand child of a vertical split has that split's seam on its **left**.
102/// Dragging its left edge moves that seam; dragging its right edge must find a
103/// different seam, or none at all. An axis cannot tell those two apart, so a
104/// layout handed only "horizontal" moves whichever seam the walk to the root
105/// meets first — which for a right-edge drag is the seam on the far side, and
106/// the window's left edge then jumps while the edge under the pointer sits
107/// still. Measured: the slot's x moved 209px in one frame while its width did
108/// not change at all.
109///
110/// The compositor has known the side all along — `ResizeEdge` in `state.rs`,
111/// which the floating path already uses — and reduced it to a pair of booleans
112/// on the way to the layout.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub enum Edge {
115 /// The low-x side, which a vertical split's seam bounds from the left.
116 Left,
117 /// The high-x side.
118 Right,
119 /// The low-y side, which a horizontal split's seam bounds from above.
120 Top,
121 /// The high-y side.
122 Bottom,
123}
124
125impl Edge {
126 /// The axis a seam along this side is cut on.
127 #[must_use]
128 pub const fn axis(self) -> Axis {
129 match self {
130 Self::Left | Self::Right => Axis::Vertical,
131 Self::Top | Self::Bottom => Axis::Horizontal,
132 }
133 }
134
135 /// Which child of a split has that split's seam on this side.
136 ///
137 /// A split's seam lies *between* its two children, so the child that has
138 /// the seam on its left — or above it — is the second one, and the child
139 /// that has it on its right or below is the first. This single index is
140 /// what turns "a branch cut the right way" into "the branch whose seam is
141 /// actually under the pointer".
142 const fn child(self) -> usize {
143 match self {
144 Self::Left | Self::Top => 1,
145 Self::Right | Self::Bottom => 0,
146 }
147 }
148}
149
150#[derive(Clone, Copy, Debug)]
151enum Node {
152 Window {
153 id: u64,
154 },
155 Split {
156 axis: Axis,
157 ratio: f64,
158 children: [usize; 2],
159 },
160}
161
162/// A dwindle tree.
163#[derive(Clone, Debug, Default)]
164pub struct Tiling {
165 nodes: Vec<Option<Node>>,
166 root: Option<usize>,
167 /// Each window's own floor, as the caller last handed them in. See
168 /// [`Self::set_floors`].
169 floors: Floors,
170}
171
172/// What a subtree asks of the room along one axis: the least it must have for
173/// every window in it that has a floor of its own to be at that floor, and the
174/// least it will give up.
175///
176/// `pull` is zero for a subtree with no window floor in it: [`Settings::minimum`]
177/// bounds splits and seams and moves nothing by itself, as it did not before
178/// (#134). `hold` is what the subtree can be taken down to when a sibling
179/// pulls -- each window's own floor, or the minimum where that is larger.
180#[derive(Clone, Copy, Debug, Default)]
181struct Need {
182 pull: f64,
183 hold: f64,
184}
185
186/// A size read from a caller as a size: a finite length of more than nothing,
187/// and anything else as no size at all.
188fn length(value: f64) -> f64 {
189 if value.is_finite() && value > 0.0 {
190 value
191 } else {
192 0.0
193 }
194}
195
196impl Tiling {
197 #[must_use]
198 pub fn new() -> Self {
199 Self::default()
200 }
201
202 /// Each window's own floor, by id, for every call after this one.
203 ///
204 /// Replaced whole, not merged: the caller hands in what every window says
205 /// now, and a window missing from it has no floor
206 /// (`floor_tests::a_window_whose_floor_goes_away_hands_the_room_back`). A
207 /// window that is not in the tree yet is read when it is inserted
208 /// (`floor_tests::a_split_that_would_put_a_window_under_its_floor_is_refused`).
209 pub fn set_floors(&mut self, floors: Floors) {
210 self.floors = floors;
211 }
212
213 /// Whether `rect`, a tile this tree laid out for window `id`, is smaller
214 /// than that window's own floor on either side.
215 ///
216 /// Its own floor and nothing else: a tile under [`Settings::minimum`] is
217 /// one the layout chose or allowed (#134), and a client inside it is not
218 /// refusing anything (`floor_tests::a_sibling_gives_no_more_than_it_holds`
219 /// has one of each). This is the window whose client will commit more than
220 /// it was given and be cut to its tile (#133).
221 #[must_use]
222 pub fn cramped(&self, id: u64, rect: Rect) -> bool {
223 let floor = self.floor_of(id);
224 rect.w + SLACK < floor.w || rect.h + SLACK < floor.h
225 }
226
227 /// Window `id`'s own floor, each side read as a [`length`].
228 fn floor_of(&self, id: u64) -> Minimum {
229 self.floors
230 .get(&id)
231 .map_or_else(Minimum::default, |floor| Minimum {
232 w: length(floor.w),
233 h: length(floor.h),
234 })
235 }
236
237 /// What window `id` asks of the room along `axis`. See [`need_of`].
238 fn leaf_need(&self, id: u64, axis: Axis, settings: Settings) -> Need {
239 need_of(self.floor_of(id), axis, settings)
240 }
241
242 /// What the subtree at `index` asks of the room along `axis`. See [`Need`].
243 ///
244 /// Cut along `axis`, its children share that room, so it needs both of
245 /// theirs and the gap -- and once anything in it pulls, it needs every
246 /// window in it at what that window will give down to, since those are
247 /// what the pulling one takes its room from. Cut the other way, each child
248 /// has the whole extent, so it needs only the larger. Both are
249 /// `floor_tests::a_floor_deep_in_a_subtree_takes_room_across_the_root`.
250 fn need(&self, index: usize, axis: Axis, settings: Settings) -> Need {
251 match self.nodes.get(index).copied().flatten() {
252 Some(Node::Window { id }) => self.leaf_need(id, axis, settings),
253 Some(Node::Split {
254 axis: cut_on,
255 children,
256 ..
257 }) => {
258 let first = self.need(children[0], axis, settings);
259 let second = self.need(children[1], axis, settings);
260 if cut_on == axis {
261 let hold = if first.hold > 0.0 || second.hold > 0.0 {
262 first.hold + settings.gap + second.hold
263 } else {
264 0.0
265 };
266 let pull = if first.pull > 0.0 || second.pull > 0.0 {
267 hold
268 } else {
269 0.0
270 };
271 Need { pull, hold }
272 } else {
273 Need {
274 pull: first.pull.max(second.pull),
275 hold: first.hold.max(second.hold),
276 }
277 }
278 }
279 None => Need::default(),
280 }
281 }
282
283 /// The ratio a split is laid out at, in `rect`: its own, or the one that
284 /// gives a side under its floor its floor. See [`shared`].
285 ///
286 /// No floors at all is the ratio as it is, without any arithmetic on it:
287 /// a tree nobody has handed floors lays out what it did before #115, and
288 /// every test above `floor_tests` is one that has none.
289 fn share(
290 &self,
291 axis: Axis,
292 ratio: f64,
293 children: [usize; 2],
294 rect: Rect,
295 settings: Settings,
296 ) -> f64 {
297 if self.floors.is_empty() {
298 return ratio;
299 }
300 shared(
301 axis,
302 ratio,
303 self.need(children[0], axis, settings),
304 self.need(children[1], axis, settings),
305 rect,
306 settings,
307 )
308 }
309
310 #[must_use]
311 pub fn is_empty(&self) -> bool {
312 self.root.is_none()
313 }
314
315 #[must_use]
316 pub fn contains(&self, id: u64) -> bool {
317 self.leaf(id).is_some()
318 }
319
320 /// Every window in the tree, in insertion-independent tree order.
321 #[must_use]
322 pub fn windows(&self) -> Vec<u64> {
323 let mut out = Vec::new();
324 if let Some(root) = self.root {
325 self.walk(root, &mut out);
326 }
327 out
328 }
329
330 fn walk(&self, index: usize, out: &mut Vec<u64>) {
331 match self.nodes.get(index).copied().flatten() {
332 Some(Node::Window { id }) => out.push(id),
333 Some(Node::Split { children, .. }) => {
334 self.walk(children[0], out);
335 self.walk(children[1], out);
336 }
337 None => {}
338 }
339 }
340
341 /// Add a window by splitting `target`, or the whole area if there is none.
342 ///
343 /// `at` is where the pointer was. It decides which side of the split the
344 /// new window lands on — dropping a window on the right half of another
345 /// puts it on the right — which is the part that makes this feel like
346 /// placing a window rather than appending to a list.
347 ///
348 /// Splits whatever it is pointed at, across that box's longer side,
349 /// however small the halves come out: [`Settings::minimum`] is not read
350 /// here. This is the last resort, `"allow"` in `config.tiling.overflow`;
351 /// [`Self::insert_fitting`] is the same placement with the floor on it.
352 pub fn insert(
353 &mut self,
354 id: u64,
355 target: Option<u64>,
356 at: Option<(f64, f64)>,
357 area: Rect,
358 settings: Settings,
359 ) {
360 if self.contains(id) {
361 return;
362 }
363 let Some(root) = self.root else {
364 self.root = Some(self.push(Node::Window { id }));
365 return;
366 };
367 let boxes = self.layout(area, settings);
368 let target = self.split_target(id, target, at, &boxes).unwrap_or(root);
369 let rect = self.box_of(target, &boxes, area);
370 self.split(id, target, rect, Axis::longer(rect), at, settings);
371 }
372
373 /// [`Self::insert`], unless that would make a tile smaller than
374 /// [`Settings::minimum`], or leave the window being split or the one being
375 /// added under its own floor ([`Self::set_floors`]) — in which case the
376 /// other axis is tried, and failing that nothing is done at all.
377 ///
378 /// The same target as `insert` and the same side of it: this is "open
379 /// where the pointer is" with a floor under it, not a different rule. The
380 /// other axis first because a box that cannot be divided one way often can
381 /// be divided the other — a wide, short tile that has no room side by side
382 /// may still have room one above the other — and that is still the tile
383 /// the user was pointing at.
384 ///
385 /// Returns whether the window is in the tree now. `false` leaves the tree
386 /// exactly as it was, so the caller can try somewhere else; nothing is
387 /// pushed until a split has been chosen, because a node pushed and then
388 /// abandoned would be found by [`Self::contains`] for ever after. An empty
389 /// tree always has room: its one window takes the whole area whatever
390 /// size that is, since there is nothing to divide.
391 pub fn insert_fitting(
392 &mut self,
393 id: u64,
394 target: Option<u64>,
395 at: Option<(f64, f64)>,
396 area: Rect,
397 settings: Settings,
398 ) -> bool {
399 if self.contains(id) {
400 return true;
401 }
402 let Some(root) = self.root else {
403 self.root = Some(self.push(Node::Window { id }));
404 return true;
405 };
406 let boxes = self.layout(area, settings);
407 let target = self.split_target(id, target, at, &boxes).unwrap_or(root);
408 let rect = self.box_of(target, &boxes, area);
409 let Some(axis) = self.room_in(rect, target, id, at, settings) else {
410 return false;
411 };
412 self.split(id, target, rect, axis, at, settings);
413 true
414 }
415
416 /// Split the largest tile that has room for another window without going
417 /// under [`Settings::minimum`] or either window's own floor: `"largest"` in
418 /// `config.tiling.overflow`.
419 ///
420 /// Largest by area, and the largest *with room* rather than the largest:
421 /// a big square tile can fail both ways where a smaller, longer one has
422 /// room along its length, and skipping it is the difference between
423 /// placing the window and sending it off this workspace. Ties go to the
424 /// earlier tile in tree order, so the choice does not depend on anything
425 /// the tree does not hold.
426 ///
427 /// Each tile is tried across its longer side and then the other, as
428 /// [`Self::insert_fitting`] tries the one under the pointer. The new
429 /// window takes the far side, as it does from `insert` with no pointer:
430 /// the pointer is in some other tile by now, so it says nothing about this
431 /// one.
432 ///
433 /// Only windows the tree holds have tiles here. A window a layout took out
434 /// at `closing` (#128) has none, and the neighbour that grew into its
435 /// space is measured at the size it is now.
436 ///
437 /// Returns whether the window is in the tree now; `false` leaves the tree
438 /// as it was. An empty tree always has room, as for `insert_fitting`.
439 pub fn insert_largest(&mut self, id: u64, area: Rect, settings: Settings) -> bool {
440 if self.contains(id) {
441 return true;
442 }
443 if self.root.is_none() {
444 self.root = Some(self.push(Node::Window { id }));
445 return true;
446 }
447 let mut boxes = self.layout(area, settings);
448 // Stable, so equal areas keep tree order.
449 boxes.sort_by(|(_, a), (_, b)| (b.w * b.h).total_cmp(&(a.w * a.h)));
450 for (other, rect) in boxes {
451 if let Some(target) = self.leaf(other)
452 && let Some(axis) = self.room_in(rect, target, id, None, settings)
453 {
454 self.split(id, target, rect, axis, None, settings);
455 return true;
456 }
457 }
458 false
459 }
460
461 /// The leaf a new window would split: the one named, else the one under
462 /// the pointer, else the one nearest it. `None` only for an empty tree.
463 ///
464 /// The last step matters more than it looks: falling back to the root
465 /// instead means splitting the whole screen, and every new window then
466 /// lands as another full-height column — which is not dwindle at all, and
467 /// is exactly what this did before. Hyprland calls this `getClosestNode`.
468 fn split_target(
469 &self,
470 id: u64,
471 target: Option<u64>,
472 at: Option<(f64, f64)>,
473 boxes: &[(u64, Rect)],
474 ) -> Option<usize> {
475 // A window cannot be split by itself, and the caller can easily name
476 // it: a new window is mapped and under the pointer before this runs,
477 // so hit-testing the cursor answers with the very window being
478 // inserted. Back when its node was pushed before this lookup, `leaf`
479 // found that node, the branch took it as both children and hung from
480 // nothing, and the window vanished without any error at all. The node
481 // is pushed after the lookup now, in [`Self::split`], so `leaf` would
482 // find nothing; the filter says so outright instead of leaving it to
483 // the order of two calls. `a_window_named_as_its_own_target_is_still_added`
484 // pins the outcome.
485 //
486 // Hyprland guards the same case in `addTarget`, calling it a fail-safe
487 // and picking a different node. This is that guard.
488 target
489 .filter(|named| *named != id)
490 .and_then(|named| self.leaf(named))
491 .or_else(|| self.leaf_at(at, boxes))
492 .or_else(|| self.closest_leaf(at, boxes))
493 }
494
495 /// The box a split target has now, which is what decides the axis: the
496 /// split follows the shape of the space being divided rather than the
497 /// shape of the screen.
498 fn box_of(&self, target: usize, boxes: &[(u64, Rect)], area: Rect) -> Rect {
499 self.id_of(target)
500 .and_then(|id| boxes.iter().find(|(other, _)| *other == id))
501 .map_or(area, |(_, rect)| *rect)
502 }
503
504 /// Hang window `id` beside `target`, which occupies `rect`, across `axis`.
505 ///
506 /// Everything that decides *whether* and *where* is done by the callers;
507 /// this is the one place a node is pushed for a window that is not the
508 /// root, so a caller that has decided against a split has pushed nothing.
509 fn split(
510 &mut self,
511 id: u64,
512 target: usize,
513 rect: Rect,
514 axis: Axis,
515 at: Option<(f64, f64)>,
516 settings: Settings,
517 ) {
518 let fresh = self.push(Node::Window { id });
519
520 let children = if goes_second(at, rect, axis) {
521 [target, fresh]
522 } else {
523 [fresh, target]
524 };
525
526 // Where the target hung, found *before* the branch exists. Looking it
527 // up afterwards finds the new branch instead — the branch holds the
528 // target as a child — and hanging the branch inside itself makes a
529 // cycle. That only shows up from the third window on, because until
530 // then the target is the root and the root is handled separately.
531 let hung_from = self.parent(target);
532 let branch = self.push(Node::Split {
533 axis,
534 ratio: settings.split.clamp(0.05, 0.95),
535 children,
536 });
537 self.attach(hung_from, target, branch);
538 }
539
540 /// Trade the tiles of windows `a` and `b`, and change nothing else: every
541 /// split keeps its axis and its ratio, so the arrangement is the one it
542 /// was with two windows in each other's places, and a second swap puts it
543 /// back. What a keyboard move does in tiling (#150). Returns whether both
544 /// were in the tree; `false` leaves it as it was.
545 /// `swap_trades_two_tiles_and_nothing_else`.
546 pub fn swap(&mut self, a: u64, b: u64) -> bool {
547 let (Some(first), Some(second)) = (self.leaf(a), self.leaf(b)) else {
548 return false;
549 };
550 self.nodes[first] = Some(Node::Window { id: b });
551 self.nodes[second] = Some(Node::Window { id: a });
552 true
553 }
554
555 /// Take a window out. Its sibling inherits the space, which is what makes
556 /// closing a window a local change rather than a re-tile of the screen.
557 pub fn remove(&mut self, id: u64) {
558 let Some(leaf) = self.leaf(id) else {
559 return;
560 };
561 let Some(parent) = self.parent(leaf) else {
562 self.nodes[leaf] = None;
563 self.root = None;
564 return;
565 };
566 let Some(Node::Split { children, .. }) = self.nodes[parent] else {
567 return;
568 };
569 let sibling = if children[0] == leaf {
570 children[1]
571 } else {
572 children[0]
573 };
574 self.nodes[leaf] = None;
575 self.replace(parent, sibling);
576 self.nodes[parent] = None;
577 }
578
579 /// Put the seam beside a window's `edge` where `edge_at` says that edge
580 /// belongs.
581 ///
582 /// `edge_at` is a position on each axis and only the one `edge` names is
583 /// read — `.0` for a left or right side, `.1` for a top or bottom — which
584 /// is what lets a corner drag call this twice with one pair and have each
585 /// axis take its own edge. The coordinates are the ones [`Self::layout`]
586 /// hands back, so "where this window's right edge should be" is directly
587 /// comparable with the `x + w` of the slot this same tree produced for it.
588 ///
589 /// It is **not** where the pointer is, and calling it that was #124. The
590 /// two coincide only for a drag that began exactly on the edge, and no
591 /// gesture does: a border grab is a band sixteen pixels wide, and the
592 /// modifier drag starts from wherever in the window the button went down.
593 /// The compositor derives this from the rectangle the drag has produced —
594 /// `solium::input::resize::dragged_edge` — so what arrives here has already
595 /// moved exactly as far as the pointer has, from exactly where the edge
596 /// already was. The arithmetic below did not change for #124 and did not
597 /// need to.
598 ///
599 /// This is the **tiled** resize path: a tiled window has no size of its
600 /// own, so an edge drag moves the division it shares with its neighbour
601 /// and the compositor never resizes the window directly. The floating path
602 /// is the other one — see `Solium::settle_resize`, which only reaches
603 /// `hold_resize` when no layout claimed the drag — and the two are
604 /// deliberately independent.
605 ///
606 /// Three things this does that adding a delta to the nearest parent
607 /// cannot.
608 ///
609 /// It finds the seam on the side that was grabbed. [`Self::seam_beside`]
610 /// carries the argument; the short version is that "the nearest ancestor
611 /// cut on this axis" is not the same branch as "the branch whose seam is
612 /// under the pointer", and taking the first for the second is #120.
613 ///
614 /// It reaches past the immediate parent. A window's parent may have been
615 /// cut the other way — in a two-by-two, the seam beside a side edge is two
616 /// levels up — which is why width could be dragged in some arrangements
617 /// and not others.
618 ///
619 /// And it is idempotent. The ratio comes from a *position*, not from how
620 /// far anything moved, so handling the same drag twice gives the same
621 /// layout. Accumulating deltas fed the layout's own response back in as the
622 /// next input, and the windows shook themselves apart for as long as the
623 /// button was held. A relative gesture and a delta are not the same thing:
624 /// #124 made the gesture relative by choosing a better position to send,
625 /// and left this idempotence exactly where it was.
626 pub fn drag_seam(
627 &mut self,
628 id: u64,
629 edge: Edge,
630 edge_at: (f64, f64),
631 area: Rect,
632 settings: Settings,
633 ) {
634 let Some(leaf) = self.leaf(id) else {
635 return;
636 };
637 // No seam on that side: the window is flush against its container
638 // there, and the thing beyond its edge is the screen, which does not
639 // move. Doing nothing is the answer — moving some other seam instead
640 // is the whole of the bug this replaced.
641 let Some(seam) = self.seam_beside(leaf, edge) else {
642 return;
643 };
644
645 let Some(rect) = self.node_box(seam, area, settings) else {
646 return;
647 };
648 // Inverted from [`cut`], which is the only thing that decides where an
649 // edge actually lands. Along the cut axis it lays out a first child of
650 // `(rect.w - gap) * ratio`, then a `gap`-wide band, then the second
651 // child — so a seam is not a line but a band, and the two windows
652 // beside it have their edges on its two sides:
653 //
654 // first child's trailing edge: rect.x + (rect.w - gap) * ratio
655 // second child's leading edge: rect.x + (rect.w - gap) * ratio + gap
656 //
657 // Solving each for `ratio` gives the two spellings below. The share is
658 // of `rect.w - gap` and not of `rect.w` because the band is not part
659 // of what the ratio divides, and the leading side subtracts a further
660 // `gap` because the second child begins on the far side of it.
661 //
662 // Dividing by `rect.w` and ignoring the band — what this did — leaves
663 // the grabbed edge near the asked-for place rather than on it. With the
664 // shipped `gap` of 12, asking for x=1200 on a 1920 screen put a right
665 // edge at 1192.5 and a left edge at 1204.5: one seam, grabbed from its
666 // two sides, coming to rest a full gap apart. "The edge goes where it
667 // was asked to" is the whole of #120, so this arithmetic has to be
668 // right and not merely close. It is also why `edge_at` and `rect` have
669 // to be measured in the same space, which they are: `node_box` and
670 // [`Self::layout`] both start from `area.inset(gap)` and both descend
671 // through [`cut`], so a slot's edge and a seam's box are two readings
672 // off one arrangement. `dragged_edge_tests` pins that at a real gap,
673 // where half a gap of skew would be the whole of #120 again.
674 //
675 // [`Edge::child`] already answers which of the two sides the hand is
676 // on — it is the index [`Self::seam_beside`] matched the window
677 // against — so reading it as a distance rather than as an index is
678 // what keeps the two in step.
679 let gap = settings.gap;
680 let leading = if edge.child() == 1 { gap } else { 0.0 };
681 let ratio = match edge.axis() {
682 Axis::Vertical => (edge_at.0 - rect.x - leading) / (rect.w - gap).max(1.0),
683 Axis::Horizontal => (edge_at.1 - rect.y - leading) / (rect.h - gap).max(1.0),
684 };
685 // **The only clamps on a tiled drag, which is why they have to be
686 // here.** `edge_at` arrives unbounded: the compositor sends this pane's
687 // own laid-out edge displaced by the pointer, and it deliberately does
688 // not floor it. It cannot usefully. `resize::MINIMUM` is a floor on a
689 // *window's* size, and applying it to a seam's position put a 120-pixel
690 // window's floor on a tile that was narrower than that to begin with --
691 // which reported an edge 120px from where it was on the first frame of
692 // the drag, before the pointer had moved at all. A ratio of the seam's
693 // own box is the quantity this layout actually owns, and the box is not
694 // the window.
695 //
696 // Three bounds on that ratio, and [`Self::room`] works out all of
697 // them. One is 0.05..0.95, because a seam driven to either end of its
698 // box leaves a child of no width, and a window of no width cannot be
699 // grabbed again to undo it. Another, since #134, is
700 // `settings.minimum`: no tile on either side of the seam is taken
701 // under it, so a seam cannot be dragged to make a tile smaller than a
702 // new window is allowed to open in. A tile, not a window, so it is
703 // this layout's to hold -- and it bends for a side that is already
704 // under it rather than snapping that side up to it, which is what
705 // keeps the first frame of such a drag where it was. See `room`.
706 //
707 // The third, since #115, is each window's own floor: a seam stops
708 // where a window beyond it reaches the size its application says it
709 // cannot go under, measured on the tiles as the layout rebalances
710 // them. See `room`, and
711 // `floor_tests::a_seam_stops_at_a_neighbours_own_floor`.
712 //
713 // This is also the tighter of two clamps on a *floating* drag, where
714 // `resize::MINIMUM` does apply -- to the window, in `resized`, for the
715 // floating path's own rectangle. The two never meet on one number.
716 let (low, high) = self.room(seam, rect, settings);
717 if let Some(Node::Split { ratio: current, .. }) = self.nodes[seam].as_mut() {
718 *current = within(ratio, low, high, *current);
719 }
720 }
721
722 /// The ratios a seam may take: 0.05..0.95, narrowed so that no tile on
723 /// either side of it is made smaller than [`Settings::minimum`].
724 ///
725 /// `rect` is the seam's own box, from [`Self::node_box`]. Each side needs
726 /// [`Self::floor`] of it along the seam's axis, and a side's share of the
727 /// box less the gap is what the ratio divides, so the floors turn into
728 /// ratios by the same arithmetic [`cut`] runs forwards.
729 ///
730 /// **A side already under its floor is held where it is rather than moved
731 /// up to it.** Tiles under the minimum are ordinary: `"allow"` makes them,
732 /// a monitor change or a smaller work area can, and so can raising the
733 /// minimum in a running session. Clamping such a seam into its range on
734 /// the first frame of a drag would throw it across the screen before the
735 /// pointer had moved -- the #124 class of bug, reached through a floor
736 /// instead of a skew. So the range is widened to take in the ratio the
737 /// seam has now: that side can be grown, and cannot be made smaller still.
738 /// With both sides under, the seam cannot move at all, because any move
739 /// shrinks one of them.
740 ///
741 /// No minimum on the seam's axis leaves exactly 0.05..0.95, which is what
742 /// this clamp was before #134.
743 ///
744 /// **Each window's own floor narrows it further (#115)**, by what each
745 /// side needs for every window in it with a floor to reach it -- [`Need`],
746 /// the same measure [`Self::layout`] rebalances by. So the seam itself
747 /// stops there, and not only the picture of it: a floor that goes away
748 /// afterwards leaves the seam where the drag was stopped
749 /// (`floor_tests::a_seam_stops_at_a_neighbours_own_floor`). "The ratio it
750 /// has" is the one it is drawn at, [`Self::share`], which is the one it
751 /// holds unless a floor has moved it.
752 fn room(&self, seam: usize, rect: Rect, settings: Settings) -> (f64, f64) {
753 let Some(Node::Split {
754 axis,
755 ratio,
756 children,
757 }) = self.nodes.get(seam).copied().flatten()
758 else {
759 return (0.05, 0.95);
760 };
761 let current = self.share(axis, ratio, children, rect, settings);
762 let span = (axis.extent(rect) - settings.gap).max(1.0);
763 let (mut low, mut high) = (0.05_f64, 0.95_f64);
764 let least = axis.of(settings.minimum);
765 // NaN is spelled out because it compares false both ways, and a NaN
766 // floor would make every ratio below NaN too.
767 if !(least.is_nan() || least <= 0.0) {
768 low = low.max(self.floor(children[0], axis, least, settings.gap) / span);
769 high = high.min(1.0 - self.floor(children[1], axis, least, settings.gap) / span);
770 }
771 if !self.floors.is_empty() {
772 let first = self.need(children[0], axis, settings);
773 let second = self.need(children[1], axis, settings);
774 if first.pull > 0.0 {
775 low = low.max(first.pull / span);
776 }
777 if second.pull > 0.0 {
778 high = high.min(1.0 - second.pull / span);
779 }
780 }
781 (low.min(current).max(0.05), high.max(current).min(0.95))
782 }
783
784 /// The least room along `axis` the subtree at `index` needs for every tile
785 /// in it to be at least `least` long on that axis, with its own seams
786 /// where they are now.
787 ///
788 /// At the ratios it has rather than at the best ratios it could have: a
789 /// drag moves one seam, so a subtree's inner seams stay where they are and
790 /// its tiles grow and shrink in proportion. A branch cut along `axis`
791 /// needs each child's floor divided by that child's share, plus the gap
792 /// between them. A branch cut the other way gives each child the whole of
793 /// its extent along `axis`, so it needs only the larger of the two floors
794 /// -- a column of stacked windows is as narrow as its widest floor, not
795 /// the sum of them.
796 fn floor(&self, index: usize, axis: Axis, least: f64, gap: f64) -> f64 {
797 match self.nodes.get(index).copied().flatten() {
798 Some(Node::Window { .. }) => least,
799 Some(Node::Split {
800 axis: cut_on,
801 ratio,
802 children,
803 }) => {
804 let first = self.floor(children[0], axis, least, gap);
805 let second = self.floor(children[1], axis, least, gap);
806 if cut_on == axis {
807 // Clamped as `cut` clamps it, since that is the share the
808 // children are actually laid out at.
809 let ratio = ratio.clamp(0.05, 0.95);
810 gap + (first / ratio).max(second / (1.0 - ratio))
811 } else {
812 first.max(second)
813 }
814 }
815 None => 0.0,
816 }
817 }
818
819 /// The branch whose seam runs along `edge` of the window at `leaf`.
820 ///
821 /// Two conditions, and the old walk checked only the first. The branch has
822 /// to be cut along the dragged axis, *and* this subtree has to be on the
823 /// side of it that puts the seam under the grabbed edge — which is
824 /// [`Edge::child`]. A window that is the second child of a vertical split
825 /// has that seam on its left and nowhere else; matching it for a
826 /// right-edge drag moves the window's far side while the pointer's side
827 /// stands still.
828 ///
829 /// The walk continues towards the root rather than stopping at the first
830 /// branch on the axis, because a branch on the wrong side is not a
831 /// near-miss: the seam beside a right edge may be any number of levels up,
832 /// past ancestors cut the other way and past ancestors cut the same way
833 /// that this subtree happens to sit on the far side of.
834 ///
835 /// `None` when the walk reaches the root having found no such branch,
836 /// which means exactly one thing: the window is flush against its
837 /// container on that side, and there is no seam there to move.
838 fn seam_beside(&self, leaf: usize, edge: Edge) -> Option<usize> {
839 let axis = edge.axis();
840 let side = edge.child();
841 let mut node = leaf;
842 while let Some(parent) = self.parent(node) {
843 if matches!(
844 self.nodes[parent],
845 Some(Node::Split { axis: cut, children, .. })
846 if cut == axis && children[side] == node
847 ) {
848 return Some(parent);
849 }
850 node = parent;
851 }
852 None
853 }
854
855 /// The rectangle a node occupies, branches included.
856 fn node_box(&self, wanted: usize, area: Rect, settings: Settings) -> Option<Rect> {
857 fn find(
858 tree: &Tiling,
859 index: usize,
860 rect: Rect,
861 wanted: usize,
862 settings: Settings,
863 ) -> Option<Rect> {
864 if index == wanted {
865 return Some(rect);
866 }
867 match tree.nodes.get(index).copied().flatten() {
868 Some(Node::Split {
869 axis,
870 ratio,
871 children,
872 }) => {
873 let ratio = tree.share(axis, ratio, children, rect, settings);
874 let (first, second) = cut(rect, axis, ratio, settings.gap);
875 find(tree, children[0], first, wanted, settings)
876 .or_else(|| find(tree, children[1], second, wanted, settings))
877 }
878 _ => None,
879 }
880 }
881 let root = self.root?;
882 find(self, root, area.inset(settings.gap), wanted, settings)
883 }
884
885 /// Grow a window along `axis` by a fraction, moving one seam. Everything
886 /// on the far side stays put.
887 ///
888 /// The keyboard path. It takes an axis and not an [`Edge`] because a
889 /// keypress names neither side: `super+equal` means "make this wider", and
890 /// which of the two seams beside it gives up the space is not something
891 /// the user expressed. A drag is the opposite — the hand is on one
892 /// specific edge — which is why the two take different arguments.
893 ///
894 /// So it prefers the trailing seam (right, or below) and falls back to the
895 /// leading one, with the sign flipped so that positive `by` still grows
896 /// the window either way. Only a window with no seam on either side —
897 /// which on that axis means the only window on the screen — does nothing.
898 ///
899 /// This used to adjust the immediate parent and nothing else, which is the
900 /// same shortcut [`Self::drag_seam`] made: in an ordinary four-window
901 /// dwindle every leaf's parent is a horizontal split, so the centre
902 /// vertical seam could not be reached from the keyboard at all and
903 /// `super+equal` silently changed the height instead.
904 ///
905 /// Bounded as a drag is, by [`Self::room`]: a press does not make a tile
906 /// smaller than [`Settings::minimum`], nor one already under it any
907 /// smaller. That is why this takes the area and the settings, which it did
908 /// not need before #134 -- the floor is in pixels, and the seam's box is
909 /// what turns pixels into a ratio.
910 pub fn resize(&mut self, id: u64, axis: Axis, by: f64, area: Rect, settings: Settings) {
911 let Some(leaf) = self.leaf(id) else {
912 return;
913 };
914 let (trailing, leading) = match axis {
915 Axis::Vertical => (Edge::Right, Edge::Left),
916 Axis::Horizontal => (Edge::Bottom, Edge::Top),
917 };
918 // A seam on the trailing side is the first child's, so raising its
919 // ratio grows this window; a seam on the leading side is the second
920 // child's, where the same move shrinks it. Hence the negation, which
921 // is the one thing about this that reads backwards.
922 let (seam, towards) = match self.seam_beside(leaf, trailing) {
923 Some(seam) => (seam, by),
924 None => match self.seam_beside(leaf, leading) {
925 Some(seam) => (seam, -by),
926 None => return,
927 },
928 };
929 // `seam_beside` only returns a branch it reached by walking up from a
930 // leaf of this tree, so `node_box` finds it walking down; the whole
931 // area is a fallback that keeps some bound rather than none.
932 let rect = self.node_box(seam, area, settings).unwrap_or(area);
933 let (low, high) = self.room(seam, rect, settings);
934 // From the ratio the seam is drawn at, which a window's floor may have
935 // moved off the one it holds (#115): a press moves what the user can
936 // see by `by`. From the one it holds, a press that did not reach past
937 // the floor changed nothing on screen at all.
938 // `floor_tests::a_press_beside_a_floor_moves_the_seam_from_where_it_is_drawn`.
939 let Some(Node::Split {
940 axis: cut,
941 ratio: held,
942 children,
943 }) = self.nodes[seam]
944 else {
945 return;
946 };
947 let shown = self.share(cut, held, children, rect, settings);
948 if let Some(Node::Split { ratio, .. }) = self.nodes[seam].as_mut() {
949 *ratio = within(shown + towards, low, high, *ratio);
950 }
951 }
952
953 /// Where every window goes.
954 ///
955 /// Each split at the ratio it holds, unless a window beside it is under
956 /// its own floor -- see the module note and [`shared`].
957 #[must_use]
958 pub fn layout(&self, area: Rect, settings: Settings) -> Vec<(u64, Rect)> {
959 let mut out = Vec::new();
960 if let Some(root) = self.root {
961 self.place(root, area.inset(settings.gap), settings, &mut out);
962 }
963 out
964 }
965
966 fn place(&self, index: usize, rect: Rect, settings: Settings, out: &mut Vec<(u64, Rect)>) {
967 match self.nodes.get(index).copied().flatten() {
968 Some(Node::Window { id }) => out.push((id, rect)),
969 Some(Node::Split {
970 axis,
971 ratio,
972 children,
973 }) => {
974 let ratio = self.share(axis, ratio, children, rect, settings);
975 let (first, second) = cut(rect, axis, ratio, settings.gap);
976 self.place(children[0], first, settings, out);
977 self.place(children[1], second, settings, out);
978 }
979 None => {}
980 }
981 }
982
983 fn push(&mut self, node: Node) -> usize {
984 self.nodes.push(Some(node));
985 self.nodes.len() - 1
986 }
987
988 fn leaf(&self, id: u64) -> Option<usize> {
989 self.nodes
990 .iter()
991 .position(|node| matches!(node, Some(Node::Window { id: other }) if *other == id))
992 }
993
994 fn id_of(&self, index: usize) -> Option<u64> {
995 match self.nodes.get(index).copied().flatten() {
996 Some(Node::Window { id }) => Some(id),
997 _ => None,
998 }
999 }
1000
1001 /// The window a point lands in.
1002 fn leaf_at(&self, at: Option<(f64, f64)>, boxes: &[(u64, Rect)]) -> Option<usize> {
1003 let (x, y) = at?;
1004 boxes
1005 .iter()
1006 .find(|(_, rect)| rect.contains(x, y))
1007 .and_then(|(id, _)| self.leaf(*id))
1008 }
1009
1010 /// The window nearest a point, by distance from its centre.
1011 ///
1012 /// Used when the pointer is over no window at all — off the edge, or on a
1013 /// gap. There is always a window to split; the question is only which.
1014 fn closest_leaf(&self, at: Option<(f64, f64)>, boxes: &[(u64, Rect)]) -> Option<usize> {
1015 let (x, y) = at.unwrap_or_else(|| {
1016 boxes
1017 .first()
1018 .map_or((0.0, 0.0), |(_, rect)| (rect.x, rect.y))
1019 });
1020 boxes
1021 .iter()
1022 .min_by(|(_, a), (_, b)| {
1023 let distance = |rect: &Rect| {
1024 let dx = rect.x + rect.w / 2.0 - x;
1025 let dy = rect.y + rect.h / 2.0 - y;
1026 dx.mul_add(dx, dy * dy)
1027 };
1028 distance(a).total_cmp(&distance(b))
1029 })
1030 .and_then(|(id, _)| self.leaf(*id))
1031 }
1032
1033 fn parent(&self, index: usize) -> Option<usize> {
1034 self.nodes.iter().position(
1035 |node| matches!(node, Some(Node::Split { children, .. }) if children.contains(&index)),
1036 )
1037 }
1038
1039 /// Put `with` wherever `what` was.
1040 fn replace(&mut self, what: usize, with: usize) {
1041 let parent = self.parent(what);
1042 self.attach(parent, what, with);
1043 }
1044
1045 /// Put `with` where `what` hung, given the parent it hung from.
1046 ///
1047 /// Taking the parent as an argument rather than looking it up is what
1048 /// makes this safe to call once the replacement already references the
1049 /// thing being replaced.
1050 fn attach(&mut self, parent: Option<usize>, what: usize, with: usize) {
1051 let Some(parent) = parent else {
1052 if self.root == Some(what) {
1053 self.root = Some(with);
1054 }
1055 return;
1056 };
1057 if let Some(Node::Split { children, .. }) = self.nodes[parent].as_mut() {
1058 for child in children.iter_mut() {
1059 if *child == what {
1060 *child = with;
1061 }
1062 }
1063 }
1064 }
1065}
1066
1067impl Tiling {
1068 /// Which way `rect`, window `old`'s tile, can be split to add window `new`
1069 /// with neither half under [`Settings::minimum`] or under its own window's
1070 /// floor: across its longer side if that has room, else across the other,
1071 /// else neither.
1072 ///
1073 /// Measured with [`cut`] itself, at the configured split and gap, so "has
1074 /// room" means exactly what the layout will then draw -- and since #115 at
1075 /// the ratio [`shared`] moves that split to, which is what the layout will
1076 /// draw when one of the two has a floor of its own. `at` puts the new
1077 /// window on the side [`Self::split`] will, since a split that is not
1078 /// at one half is not the same split mirrored. Both halves on both sides:
1079 /// a split across the width leaves the height alone, and a tile that is
1080 /// already too short does not become tall enough by being divided.
1081 ///
1082 /// Neither window with a floor of its own is the check #134 made, to the
1083 /// bit: the ratio is the split, and each half is held to the minimum.
1084 fn room_in(
1085 &self,
1086 rect: Rect,
1087 old: usize,
1088 new: u64,
1089 at: Option<(f64, f64)>,
1090 settings: Settings,
1091 ) -> Option<Axis> {
1092 let old = self
1093 .id_of(old)
1094 .map_or_else(Minimum::default, |id| self.floor_of(id));
1095 let new = self.floor_of(new);
1096 let longer = Axis::longer(rect);
1097 [longer, longer.across()].into_iter().find(|axis| {
1098 let (first, second) = if goes_second(at, rect, *axis) {
1099 (old, new)
1100 } else {
1101 (new, old)
1102 };
1103 let ratio = shared(
1104 *axis,
1105 settings.split,
1106 need_of(first, *axis, settings),
1107 need_of(second, *axis, settings),
1108 rect,
1109 settings,
1110 );
1111 let (one, two) = cut(rect, *axis, ratio, settings.gap);
1112 holds(one, first, settings) && holds(two, second, settings)
1113 })
1114 }
1115}
1116
1117/// What a window with `floor` asks of the room along `axis`: its own floor to
1118/// pull towards, and the larger of that and the minimum to give down to.
1119fn need_of(floor: Minimum, axis: Axis, settings: Settings) -> Need {
1120 let pull = axis.of(floor);
1121 Need {
1122 pull,
1123 hold: pull.max(length(axis.of(settings.minimum))),
1124 }
1125}
1126
1127/// Whether `half` is at least a window's `floor` and the minimum, both ways.
1128///
1129/// The larger of the two per side, spelled so that a minimum of NaN still
1130/// refuses every split as it did before #115: `f64::max` would drop it.
1131fn holds(half: Rect, floor: Minimum, settings: Settings) -> bool {
1132 let least = |own: f64, minimum: f64| if own > minimum { own } else { minimum };
1133 half.w + SLACK >= least(floor.w, settings.minimum.w)
1134 && half.h + SLACK >= least(floor.h, settings.minimum.h)
1135}
1136
1137/// Whether a window added to `rect` across `axis` takes the second child: the
1138/// half the pointer is in, and the far side by default.
1139///
1140/// One answer for [`Tiling::split`], which does it, and [`Tiling::room_in`],
1141/// which has to measure the split it will be.
1142fn goes_second(at: Option<(f64, f64)>, rect: Rect, axis: Axis) -> bool {
1143 at.is_none_or(|(x, y)| match axis {
1144 Axis::Vertical => x >= rect.x + rect.w / 2.0,
1145 Axis::Horizontal => y >= rect.y + rect.h / 2.0,
1146 })
1147}
1148
1149/// The ratio a split of `rect` across `axis` is drawn at, when it holds
1150/// `ratio` and its two children ask `first` and `second` of the room (#115).
1151///
1152/// `ratio` itself unless exactly one side is under what it pulls towards.
1153/// Then the seam moves towards the other side by the shortfall, or by as much
1154/// as the other side has above what it holds, whichever is less: a window
1155/// under its floor is given the room its siblings can spare and none they
1156/// cannot (`floor_tests::a_window_under_its_floor_takes_room_from_its_sibling`
1157/// and `floor_tests::a_sibling_gives_no_more_than_it_holds`). With both sides
1158/// short the seam stays where the user put it, since moving it would take from
1159/// one window short of room to give to another
1160/// (`floor_tests::two_windows_that_cannot_both_fit_keep_the_users_seam`).
1161/// The span is what [`cut`] divides, and `cut` clamps the result as it clamps
1162/// every ratio.
1163fn shared(
1164 axis: Axis,
1165 ratio: f64,
1166 first: Need,
1167 second: Need,
1168 rect: Rect,
1169 settings: Settings,
1170) -> f64 {
1171 if first.pull <= 0.0 && second.pull <= 0.0 {
1172 return ratio;
1173 }
1174 let span = (axis.extent(rect) - settings.gap).max(1.0);
1175 let one = span * ratio.clamp(0.05, 0.95);
1176 let two = span - one;
1177 let (short_one, short_two) = (first.pull - one, second.pull - two);
1178 let one = if short_one > SLACK && short_two <= SLACK {
1179 one + short_one.min((two - second.hold).max(0.0))
1180 } else if short_two > SLACK && short_one <= SLACK {
1181 one - short_two.min((one - first.hold).max(0.0))
1182 } else {
1183 return ratio;
1184 };
1185 one / span
1186}
1187
1188/// `value` held to `low..=high`, or `otherwise` when that range is empty.
1189///
1190/// Not `f64::clamp` alone, which panics when `low > high` or either is NaN --
1191/// and a compositor that panics takes the session with it. [`Tiling::room`]
1192/// always includes the current ratio, so its range is empty only when that
1193/// ratio is itself NaN; `otherwise` is then the seam left as it was.
1194fn within(value: f64, low: f64, high: f64, otherwise: f64) -> f64 {
1195 if low <= high {
1196 value.clamp(low, high)
1197 } else {
1198 otherwise
1199 }
1200}
1201
1202fn cut(rect: Rect, axis: Axis, ratio: f64, gap: f64) -> (Rect, Rect) {
1203 let ratio = ratio.clamp(0.05, 0.95);
1204 match axis {
1205 Axis::Vertical => {
1206 let first = ((rect.w - gap) * ratio).max(1.0);
1207 let second = (rect.w - gap - first).max(1.0);
1208 (
1209 Rect::new(rect.x, rect.y, first, rect.h),
1210 Rect::new(rect.x + first + gap, rect.y, second, rect.h),
1211 )
1212 }
1213 Axis::Horizontal => {
1214 let first = ((rect.h - gap) * ratio).max(1.0);
1215 let second = (rect.h - gap - first).max(1.0);
1216 (
1217 Rect::new(rect.x, rect.y, rect.w, first),
1218 Rect::new(rect.x, rect.y + first + gap, rect.w, second),
1219 )
1220 }
1221 }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226 use super::{Axis, Tiling};
1227 use crate::{Rect, Settings};
1228
1229 fn area() -> Rect {
1230 Rect::new(0.0, 0.0, 1000.0, 600.0)
1231 }
1232
1233 fn settings() -> Settings {
1234 Settings {
1235 gap: 0.0,
1236 split: 0.5,
1237 ..Settings::default()
1238 }
1239 }
1240
1241 fn rect_of(tiling: &Tiling, id: u64) -> Rect {
1242 tiling
1243 .layout(area(), settings())
1244 .into_iter()
1245 .find(|(other, _)| *other == id)
1246 .expect("window is in the tree")
1247 .1
1248 }
1249
1250 #[test]
1251 fn the_first_window_takes_everything() {
1252 let mut tiling = Tiling::new();
1253 tiling.insert(1, None, None, area(), settings());
1254 assert!((rect_of(&tiling, 1).w - 1000.0).abs() < 1.0);
1255 }
1256
1257 /// The split runs across the longer axis of *the window being split*, not
1258 /// of the screen. This is the property a count-based spiral cannot have,
1259 /// because it never knows which window is being divided.
1260 #[test]
1261 fn a_wide_window_splits_side_by_side_and_a_tall_one_does_not() {
1262 let mut tiling = Tiling::new();
1263 tiling.insert(1, None, None, area(), settings());
1264 tiling.insert(2, Some(1), None, area(), settings());
1265 assert!(
1266 (rect_of(&tiling, 1).w - 500.0).abs() < 1.0,
1267 "cut down the middle"
1268 );
1269
1270 // Window 2 is now 500x600 — taller than wide — so splitting *it* cuts
1271 // across, not down.
1272 tiling.insert(3, Some(2), None, area(), settings());
1273 let two = rect_of(&tiling, 2);
1274 let three = rect_of(&tiling, 3);
1275 assert!((two.w - 500.0).abs() < 1.0, "width untouched");
1276 assert!((two.h - 300.0).abs() < 1.0, "split horizontally: {two:?}");
1277 assert!(three.y > two.y, "the new window went below");
1278 }
1279
1280 /// Where the pointer is decides which side the window lands on. This is
1281 /// what "the window opens where the cursor is" means, and it is the whole
1282 /// difference between placing a window and appending to a list.
1283 #[test]
1284 fn the_pointer_decides_which_side_the_new_window_takes() {
1285 let mut left = Tiling::new();
1286 left.insert(1, None, None, area(), settings());
1287 left.insert(2, Some(1), Some((100.0, 300.0)), area(), settings());
1288 assert!(
1289 rect_of(&left, 2).x < rect_of(&left, 1).x,
1290 "dropped left, opens left"
1291 );
1292
1293 let mut right = Tiling::new();
1294 right.insert(1, None, None, area(), settings());
1295 right.insert(2, Some(1), Some((900.0, 300.0)), area(), settings());
1296 assert!(
1297 rect_of(&right, 2).x > rect_of(&right, 1).x,
1298 "dropped right, opens right"
1299 );
1300 }
1301
1302 /// With no target named, the window under the pointer is the one that
1303 /// gets split — which is how Hyprland picks `OPENINGON`.
1304 #[test]
1305 fn with_no_target_the_window_under_the_pointer_is_split() {
1306 let mut tiling = Tiling::new();
1307 tiling.insert(1, None, None, area(), settings());
1308 tiling.insert(2, Some(1), Some((900.0, 300.0)), area(), settings());
1309 // Point into window 1's half and open a third with no explicit target.
1310 tiling.insert(3, None, Some((100.0, 300.0)), area(), settings());
1311 assert!(
1312 (rect_of(&tiling, 2).w - 500.0).abs() < 1.0,
1313 "the untouched half kept its width"
1314 );
1315 // Window 1 was 500x600 — taller than wide — so dividing it cuts
1316 // across: its width is untouched and its height halves.
1317 let one = rect_of(&tiling, 1);
1318 assert!((one.w - 500.0).abs() < 1.0, "width unchanged: {one:?}");
1319 assert!(
1320 (one.h - 300.0).abs() < 1.0,
1321 "the pointed-at window was divided: {one:?}"
1322 );
1323 }
1324
1325 /// Closing a window hands its space to its neighbour instead of re-tiling
1326 /// the screen. Everything else must stay exactly where it was.
1327 #[test]
1328 fn removing_a_window_gives_its_space_to_its_sibling() {
1329 let mut tiling = Tiling::new();
1330 tiling.insert(1, None, None, area(), settings());
1331 tiling.insert(2, Some(1), None, area(), settings());
1332 tiling.insert(3, Some(2), None, area(), settings());
1333 let before = rect_of(&tiling, 1);
1334
1335 tiling.remove(3);
1336 assert_eq!(tiling.windows().len(), 2);
1337 let after = rect_of(&tiling, 1);
1338 assert!(
1339 (before.w - after.w).abs() < 1.0,
1340 "the far side did not move"
1341 );
1342 assert!(
1343 (rect_of(&tiling, 2).h - 600.0).abs() < 1.0,
1344 "the sibling took the space"
1345 );
1346 }
1347
1348 #[test]
1349 fn removing_the_last_window_empties_the_tree() {
1350 let mut tiling = Tiling::new();
1351 tiling.insert(1, None, None, area(), settings());
1352 tiling.remove(1);
1353 assert!(tiling.is_empty());
1354 assert!(tiling.layout(area(), settings()).is_empty());
1355 }
1356
1357 /// Resizing moves one seam. A tree that re-derived every box from a count
1358 /// could not do this at all.
1359 #[test]
1360 fn resizing_moves_one_seam_and_leaves_the_rest() {
1361 let mut tiling = Tiling::new();
1362 tiling.insert(1, None, None, area(), settings());
1363 tiling.insert(2, Some(1), None, area(), settings());
1364 tiling.insert(3, Some(2), None, area(), settings());
1365 let untouched = rect_of(&tiling, 1);
1366
1367 tiling.resize(2, Axis::Horizontal, 0.2, area(), settings());
1368 assert!(rect_of(&tiling, 2).h > 300.0, "the seam moved");
1369 assert!(
1370 (rect_of(&tiling, 1).w - untouched.w).abs() < 1.0,
1371 "the other branch is untouched"
1372 );
1373 }
1374
1375 #[test]
1376 fn every_window_gets_a_box_and_none_collapses() {
1377 let mut tiling = Tiling::new();
1378 for id in 1..=10 {
1379 tiling.insert(id, Some(id.saturating_sub(1)), None, area(), settings());
1380 }
1381 let boxes = tiling.layout(area(), settings());
1382 assert_eq!(boxes.len(), 10);
1383 for (id, rect) in boxes {
1384 assert!(
1385 rect.w >= 1.0 && rect.h >= 1.0,
1386 "window {id} collapsed: {rect:?}"
1387 );
1388 }
1389 }
1390
1391 #[test]
1392 fn a_window_is_never_added_twice() {
1393 let mut tiling = Tiling::new();
1394 tiling.insert(1, None, None, area(), settings());
1395 tiling.insert(1, None, None, area(), settings());
1396 assert_eq!(tiling.windows(), vec![1]);
1397 }
1398
1399 #[test]
1400 fn axes_are_named_for_how_the_children_sit() {
1401 assert_ne!(Axis::Vertical, Axis::Horizontal);
1402 }
1403
1404 /// **A swap trades two tiles and nothing else** (#150). Three windows at
1405 /// an uneven seam: after 1 and 3 swap, each has the other's rectangle,
1406 /// 2 has not moved, and swapping again is the tree it started as. A
1407 /// window the tree does not hold swaps with nothing.
1408 #[test]
1409 fn swap_trades_two_tiles_and_nothing_else() {
1410 let mut tiling = Tiling::new();
1411 tiling.insert(1, None, None, area(), settings());
1412 tiling.insert(2, Some(1), Some((900.0, 300.0)), area(), settings());
1413 tiling.insert(3, Some(2), Some((900.0, 500.0)), area(), settings());
1414 tiling.resize(1, Axis::Vertical, 0.1, area(), settings());
1415 let before: Vec<_> = [1, 2, 3].map(|id| rect_of(&tiling, id)).into();
1416
1417 assert!(tiling.swap(1, 3), "both windows are in the tree");
1418 assert_eq!(rect_of(&tiling, 1), before[2], "1 took 3's tile");
1419 assert_eq!(rect_of(&tiling, 3), before[0], "3 took 1's tile");
1420 assert_eq!(rect_of(&tiling, 2), before[1], "2 was not part of it");
1421
1422 assert!(tiling.swap(3, 1));
1423 let after: Vec<_> = [1, 2, 3].map(|id| rect_of(&tiling, id)).into();
1424 assert_eq!(after, before, "a second swap is the tree it started as");
1425
1426 assert!(!tiling.swap(1, 9), "9 is not in the tree");
1427 let untouched: Vec<_> = [1, 2, 3].map(|id| rect_of(&tiling, id)).into();
1428 assert_eq!(untouched, before, "a refused swap changed the tree");
1429 }
1430}
1431
1432#[cfg(test)]
1433mod fallback_tests {
1434 use super::Tiling;
1435 use crate::{Rect, Settings};
1436
1437 fn area() -> Rect {
1438 Rect::new(0.0, 0.0, 1000.0, 600.0)
1439 }
1440 fn settings() -> Settings {
1441 Settings {
1442 gap: 0.0,
1443 split: 0.5,
1444 ..Settings::default()
1445 }
1446 }
1447
1448 /// With the pointer nowhere near a window, the split must still land on a
1449 /// *window*. Falling back to the root splits the whole screen instead, and
1450 /// every window after the first then becomes another full-height column —
1451 /// three terminals in a row rather than a dwindle.
1452 #[test]
1453 fn a_pointer_off_the_edge_still_divides_a_window_not_the_screen() {
1454 let mut tiling = Tiling::new();
1455 tiling.insert(1, None, None, area(), settings());
1456 tiling.insert(2, None, Some((-500.0, -500.0)), area(), settings());
1457 tiling.insert(3, None, Some((-500.0, -500.0)), area(), settings());
1458
1459 let boxes = tiling.layout(area(), settings());
1460 let full_height = boxes.iter().filter(|(_, r)| r.h > 599.0).count();
1461 assert!(
1462 full_height < 3,
1463 "all three are full-height columns, so the screen was split each time: {boxes:?}"
1464 );
1465 }
1466}
1467
1468#[cfg(test)]
1469mod insert_sequence {
1470 use super::Tiling;
1471 use crate::{Rect, Settings};
1472
1473 /// The exact sequence the tiling script performs: a work area with a gap,
1474 /// a pointer that is outside every window, and no named target — so the
1475 /// closest-leaf fallback chooses. Three windows opened this way must all
1476 /// be in the tree.
1477 #[test]
1478 fn opening_three_windows_at_the_origin_keeps_all_three() {
1479 let area = Rect::new(0.0, 0.0, 1600.0, 900.0);
1480 let settings = Settings {
1481 gap: 12.0,
1482 split: 0.5,
1483 ..Settings::default()
1484 };
1485 let mut tiling = Tiling::new();
1486 for id in 1..=3 {
1487 tiling.insert(id, None, Some((0.0, 0.0)), area, settings);
1488 println!("after {id}: {:?}", tiling.windows());
1489 }
1490 assert_eq!(tiling.windows().len(), 3, "got {:?}", tiling.windows());
1491 }
1492}
1493
1494#[cfg(test)]
1495mod self_target {
1496 use super::Tiling;
1497 use crate::{Rect, Settings};
1498
1499 fn area() -> Rect {
1500 Rect::new(0.0, 0.0, 1600.0, 900.0)
1501 }
1502 fn settings() -> Settings {
1503 Settings {
1504 gap: 12.0,
1505 split: 0.5,
1506 ..Settings::default()
1507 }
1508 }
1509
1510 /// Naming the window being inserted as its own split target must not lose
1511 /// it. The tiling script does exactly this by accident: a new window is
1512 /// mapped and under the pointer before the layout runs, so hit-testing the
1513 /// cursor answers with the window being opened. Every window after the
1514 /// first was silently dropped — the tree kept one, and three terminals
1515 /// showed one.
1516 #[test]
1517 fn a_window_named_as_its_own_target_is_still_added() {
1518 let mut tiling = Tiling::new();
1519 for id in 1..=4 {
1520 tiling.insert(id, Some(id), Some((0.0, 0.0)), area(), settings());
1521 }
1522 assert_eq!(
1523 tiling.windows().len(),
1524 4,
1525 "windows were dropped: {:?}",
1526 tiling.windows()
1527 );
1528 for (id, rect) in tiling.layout(area(), settings()) {
1529 assert!(rect.w >= 1.0 && rect.h >= 1.0, "window {id} collapsed");
1530 }
1531 }
1532}
1533
1534#[cfg(test)]
1535mod seam_tests {
1536 use super::{Edge, Tiling};
1537 use crate::{Rect, Settings};
1538
1539 fn area() -> Rect {
1540 Rect::new(0.0, 0.0, 1000.0, 600.0)
1541 }
1542 fn settings() -> Settings {
1543 Settings {
1544 gap: 0.0,
1545 split: 0.5,
1546 ..Settings::default()
1547 }
1548 }
1549 fn rect_of(tiling: &Tiling, id: u64) -> Rect {
1550 tiling
1551 .layout(area(), settings())
1552 .into_iter()
1553 .find(|(other, _)| *other == id)
1554 .expect("in the tree")
1555 .1
1556 }
1557
1558 /// A two-by-two: every window's immediate parent is a horizontal split, so
1559 /// dragging a side edge has to reach a seam two levels up. Adjusting only
1560 /// the parent does nothing at all, which is why width could be dragged in
1561 /// some arrangements and not this one.
1562 fn quad() -> Tiling {
1563 let mut tiling = Tiling::new();
1564 tiling.insert(1, None, None, area(), settings());
1565 tiling.insert(2, Some(1), None, area(), settings());
1566 tiling.insert(3, Some(1), None, area(), settings());
1567 tiling.insert(4, Some(2), None, area(), settings());
1568 tiling
1569 }
1570
1571 #[test]
1572 fn a_two_by_two_can_have_its_width_dragged() {
1573 let mut tiling = quad();
1574 let before = rect_of(&tiling, 1).w;
1575 assert!((before - 500.0).abs() < 1.0, "starts halved: {before}");
1576
1577 tiling.drag_seam(1, Edge::Right, (700.0, 300.0), area(), settings());
1578 let after = rect_of(&tiling, 1).w;
1579 assert!(
1580 (after - 700.0).abs() < 2.0,
1581 "the seam went where it was asked: {after}"
1582 );
1583 }
1584
1585 /// Dragging to the same place twice gives the same layout. Deltas fed the
1586 /// layout's own response back in and the windows shook themselves apart.
1587 #[test]
1588 fn dragging_to_the_same_place_twice_is_the_same_layout() {
1589 let mut tiling = quad();
1590 tiling.drag_seam(1, Edge::Right, (650.0, 300.0), area(), settings());
1591 let once = rect_of(&tiling, 1).w;
1592 for _ in 0..20 {
1593 tiling.drag_seam(1, Edge::Right, (650.0, 300.0), area(), settings());
1594 }
1595 let many = rect_of(&tiling, 1).w;
1596 assert!((once - many).abs() < f64::EPSILON, "{once} then {many}");
1597 }
1598
1599 #[test]
1600 fn height_drags_find_the_horizontal_seam() {
1601 let mut tiling = quad();
1602 tiling.drag_seam(1, Edge::Bottom, (250.0, 400.0), area(), settings());
1603 let one = rect_of(&tiling, 1);
1604 assert!(
1605 (one.h - 400.0).abs() < 2.0,
1606 "went where it was asked: {one:?}"
1607 );
1608 }
1609
1610 #[test]
1611 fn a_lone_window_has_no_seam_to_drag() {
1612 let mut tiling = Tiling::new();
1613 tiling.insert(1, None, None, area(), settings());
1614 tiling.drag_seam(1, Edge::Right, (700.0, 300.0), area(), settings());
1615 assert!((rect_of(&tiling, 1).w - 1000.0).abs() < 1.0, "unchanged");
1616 }
1617}
1618
1619/// #120: the seam that moves is the one beside the edge that was grabbed.
1620///
1621/// The measurement this pins came off a real drag. Dragging one edge of a
1622/// tiled window slowly, the drag's own rectangle moved smoothly — 808, 809,
1623/// 817, 835 — while the slot the layout produced went 806, 1015, 1016, 1024:
1624/// a 209px jump in one frame, three frames of unchanged width, then another
1625/// leap. That is the signature of moving the seam on the *far* side, because
1626/// the far side moving is the near side standing still.
1627///
1628/// Every test below is written against [`quad`], where each window's immediate
1629/// parent is cut the opposite way to the split its side edges touch, so
1630/// "nearest ancestor on the axis" and "seam beside the edge" are different
1631/// branches and the difference is visible in the rectangles.
1632#[cfg(test)]
1633mod dragged_edge_tests {
1634 use super::{Axis, Edge, Tiling};
1635 use crate::{Rect, Settings};
1636
1637 fn area() -> Rect {
1638 Rect::new(0.0, 0.0, 1000.0, 600.0)
1639 }
1640 fn settings() -> Settings {
1641 Settings {
1642 gap: 0.0,
1643 split: 0.5,
1644 ..Settings::default()
1645 }
1646 }
1647 fn rect_of(tiling: &Tiling, id: u64) -> Rect {
1648 tiling
1649 .layout(area(), settings())
1650 .into_iter()
1651 .find(|(other, _)| *other == id)
1652 .expect("in the tree")
1653 .1
1654 }
1655 fn every_rect(tiling: &Tiling) -> Vec<(u64, Rect)> {
1656 let mut out = tiling.layout(area(), settings());
1657 out.sort_by_key(|(id, _)| *id);
1658 out
1659 }
1660
1661 /// Two columns of two. Window 1 is top-left, 2 top-right, 3 bottom-left,
1662 /// 4 bottom-right; the columns are divided by one vertical seam at the
1663 /// root, and each column by a horizontal seam of its own.
1664 ///
1665 /// The same shape as `seam_tests::quad`, restated here so this module
1666 /// reads on its own — the arrangement *is* the argument, and a fixture
1667 /// imported from elsewhere is one the reader has to go and look up.
1668 fn quad() -> Tiling {
1669 let mut tiling = Tiling::new();
1670 tiling.insert(1, None, None, area(), settings());
1671 tiling.insert(2, Some(1), None, area(), settings());
1672 tiling.insert(3, Some(1), None, area(), settings());
1673 tiling.insert(4, Some(2), None, area(), settings());
1674 tiling
1675 }
1676
1677 /// Window 1 is the first child of the root's vertical split, so that seam
1678 /// is on its right. Dragging the right edge to 700 has to leave the left
1679 /// edge at 0 — the failure being pinned is precisely a left edge that
1680 /// moves instead — and has to carry window 3 with it, because they share
1681 /// the column the root seam bounds.
1682 #[test]
1683 fn a_right_edge_drag_moves_the_seam_on_the_right() {
1684 let mut tiling = quad();
1685 tiling.drag_seam(1, Edge::Right, (700.0, 150.0), area(), settings());
1686
1687 let one = rect_of(&tiling, 1);
1688 assert!(
1689 (one.x - 0.0).abs() < 1.0,
1690 "the left edge stood still: {one:?}"
1691 );
1692 assert!(
1693 (one.w - 700.0).abs() < 2.0,
1694 "the right edge followed: {one:?}"
1695 );
1696 assert!(
1697 (rect_of(&tiling, 3).w - 700.0).abs() < 2.0,
1698 "the root seam moved, so the whole column did"
1699 );
1700 assert!((one.h - 300.0).abs() < 1.0, "nothing horizontal moved");
1701 }
1702
1703 /// The mirror. Window 2 is the *second* child of the root's vertical
1704 /// split, so that same seam is on its left, and a left-edge drag is what
1705 /// moves it. Its right edge must stay against the container at 1000.
1706 #[test]
1707 fn a_left_edge_drag_moves_the_seam_on_the_left() {
1708 let mut tiling = quad();
1709 tiling.drag_seam(2, Edge::Left, (700.0, 150.0), area(), settings());
1710
1711 let two = rect_of(&tiling, 2);
1712 assert!(
1713 (two.x - 700.0).abs() < 2.0,
1714 "the left edge followed: {two:?}"
1715 );
1716 assert!(
1717 (two.x + two.w - 1000.0).abs() < 2.0,
1718 "the right edge stood still: {two:?}"
1719 );
1720 }
1721
1722 /// Window 1 is the first child of its column's horizontal split, so that
1723 /// seam is below it. Its top edge is the container's, and its width is
1724 /// bounded by a seam that a vertical drag must not touch.
1725 #[test]
1726 fn a_bottom_edge_drag_moves_the_seam_below() {
1727 let mut tiling = quad();
1728 tiling.drag_seam(1, Edge::Bottom, (250.0, 400.0), area(), settings());
1729
1730 let one = rect_of(&tiling, 1);
1731 assert!(
1732 (one.y - 0.0).abs() < 1.0,
1733 "the top edge stood still: {one:?}"
1734 );
1735 assert!(
1736 (one.h - 400.0).abs() < 2.0,
1737 "the bottom edge followed: {one:?}"
1738 );
1739 assert!((one.w - 500.0).abs() < 1.0, "nothing vertical moved");
1740 assert!(
1741 (rect_of(&tiling, 2).h - 300.0).abs() < 1.0,
1742 "the other column's seam is a different seam and did not move"
1743 );
1744 }
1745
1746 /// And the mirror of that: window 3 is the second child of the left
1747 /// column's split, so the seam is above it and its bottom edge is the
1748 /// container's.
1749 #[test]
1750 fn a_top_edge_drag_moves_the_seam_above() {
1751 let mut tiling = quad();
1752 tiling.drag_seam(3, Edge::Top, (250.0, 200.0), area(), settings());
1753
1754 let three = rect_of(&tiling, 3);
1755 assert!(
1756 (three.y - 200.0).abs() < 2.0,
1757 "the top edge followed: {three:?}"
1758 );
1759 assert!(
1760 (three.y + three.h - 600.0).abs() < 2.0,
1761 "the bottom edge stood still: {three:?}"
1762 );
1763 assert!(
1764 (rect_of(&tiling, 1).h - 200.0).abs() < 2.0,
1765 "the neighbour above gave up the space"
1766 );
1767 }
1768
1769 /// The four cases where the grabbed edge is the container's own, which is
1770 /// where the old walk did its damage: it found the nearest ancestor cut on
1771 /// the axis regardless of which side of it the window sat, so every one of
1772 /// these dragged the seam on the *opposite* side and the window jumped.
1773 ///
1774 /// Nothing at all is the right answer. The thing beyond that edge is the
1775 /// screen, and the screen does not move.
1776 #[test]
1777 fn a_window_against_the_container_has_no_seam_on_that_side() {
1778 // Window 1 is top-left: its left edge and its top edge are the
1779 // container's. Window 2 is top-right, so its right edge is; window 3
1780 // is bottom-left, so its bottom edge is.
1781 let cases = [
1782 (1_u64, Edge::Left, (200.0, 150.0)),
1783 (1, Edge::Top, (250.0, 200.0)),
1784 (2, Edge::Right, (800.0, 150.0)),
1785 (3, Edge::Bottom, (250.0, 450.0)),
1786 ];
1787 for (id, edge, at) in cases {
1788 let mut tiling = quad();
1789 let before = every_rect(&tiling);
1790 tiling.drag_seam(id, edge, at, area(), settings());
1791 assert_eq!(
1792 every_rect(&tiling),
1793 before,
1794 "dragging window {id}'s {edge:?} edge moved something"
1795 );
1796 }
1797 }
1798
1799 /// A three-level tree, where the seam beside an edge is provably *not* the
1800 /// nearest ancestor cut on that axis — the case the two-by-two cannot show
1801 /// because there the nearest ancestor is the only one.
1802 ///
1803 /// Splitting window 4 again gives the bottom-right corner its own vertical
1804 /// seam. Window 4's right edge touches that inner seam; its *left* edge
1805 /// touches the root's, two branches further up, past a horizontal split
1806 /// and past a vertical one it is on the wrong side of. Walking to the
1807 /// nearest vertical ancestor finds the inner seam for both.
1808 fn nested() -> Tiling {
1809 let mut tiling = quad();
1810 tiling.insert(5, Some(4), None, area(), settings());
1811 tiling
1812 }
1813
1814 #[test]
1815 fn the_seam_beside_an_edge_is_not_always_the_nearest_ancestor() {
1816 let mut tiling = nested();
1817 assert!(
1818 (rect_of(&tiling, 4).w - 250.0).abs() < 1.0,
1819 "the corner was split again: {:?}",
1820 rect_of(&tiling, 4)
1821 );
1822
1823 // The left edge of window 4 is the root's seam. Moving it to 700 has
1824 // to widen the left column to 700 and leave window 5 — on the far side
1825 // of the inner seam — with its right edge still against the container.
1826 tiling.drag_seam(4, Edge::Left, (700.0, 450.0), area(), settings());
1827 assert!(
1828 (rect_of(&tiling, 1).w - 700.0).abs() < 2.0,
1829 "the root seam moved: {:?}",
1830 rect_of(&tiling, 1)
1831 );
1832 let five = rect_of(&tiling, 5);
1833 assert!(
1834 (five.x + five.w - 1000.0).abs() < 2.0,
1835 "the far side of the inner seam is still against the edge: {five:?}"
1836 );
1837 }
1838
1839 /// Window 5 sits in the bottom-right corner, so its right edge is the
1840 /// container's even though it has a vertical seam on its other side. The
1841 /// nearest-ancestor walk finds that inner seam and drags it, which moves
1842 /// the window's left edge for a drag on its right.
1843 #[test]
1844 fn a_nested_window_at_the_far_edge_still_moves_nothing() {
1845 let mut tiling = nested();
1846 let before = every_rect(&tiling);
1847 tiling.drag_seam(5, Edge::Right, (900.0, 450.0), area(), settings());
1848 assert_eq!(every_rect(&tiling), before, "the inner seam was dragged");
1849 }
1850
1851 /// The keyboard path, which took the same shortcut from the other end: it
1852 /// adjusted the immediate parent, and in this arrangement every leaf's
1853 /// parent is a horizontal split. `super+equal` meaning "wider" could not
1854 /// reach the centre vertical seam at all — it changed the height instead.
1855 #[test]
1856 fn the_keyboard_can_reach_the_centre_vertical_seam() {
1857 let mut tiling = quad();
1858 tiling.resize(1, Axis::Vertical, 0.1, area(), settings());
1859
1860 let one = rect_of(&tiling, 1);
1861 assert!((one.w - 600.0).abs() < 2.0, "it got wider: {one:?}");
1862 assert!((one.h - 300.0).abs() < 1.0, "and not taller: {one:?}");
1863 }
1864
1865 /// Growing is growing from either side. Window 2 has no seam on its right
1866 /// — it is against the container — so the seam on its left gives up the
1867 /// space, and the sign has to flip for the window to get bigger rather
1868 /// than smaller.
1869 #[test]
1870 fn a_window_against_the_container_grows_from_the_other_side() {
1871 let mut tiling = quad();
1872 tiling.resize(2, Axis::Vertical, 0.1, area(), settings());
1873
1874 let two = rect_of(&tiling, 2);
1875 assert!((two.w - 600.0).abs() < 2.0, "it got wider: {two:?}");
1876 assert!(
1877 (two.x + two.w - 1000.0).abs() < 2.0,
1878 "from the left, because the right is the container's: {two:?}"
1879 );
1880 }
1881
1882 /// The gap the compositor actually ships with.
1883 ///
1884 /// Every test above this point runs at `gap: 0.0`, and zero is the single
1885 /// value at which the seam band has no width — so "a share of the box" and
1886 /// "a share of the box less the gap" give the same answer, and the two
1887 /// sides of a seam are the same line. A suite written only at zero cannot
1888 /// tell whether a grabbed edge lands under the pointer or several pixels
1889 /// off it, which is the only thing #120 is about. Hence the cases below.
1890 fn gapped() -> Settings {
1891 Settings {
1892 gap: 12.0,
1893 split: 0.5,
1894 ..Settings::default()
1895 }
1896 }
1897
1898 /// [`quad`] built at a given gap.
1899 ///
1900 /// The same arrangement, not a different one: `insert` picks each split's
1901 /// axis from the target box's aspect ratio, and insetting a 1000x600
1902 /// screen by 12 changes neither which side of the root box is longer nor
1903 /// which side of a column's is. The tree is identical; only the pixels
1904 /// move.
1905 fn quad_at(settings: Settings) -> Tiling {
1906 let mut tiling = Tiling::new();
1907 tiling.insert(1, None, None, area(), settings);
1908 tiling.insert(2, Some(1), None, area(), settings);
1909 tiling.insert(3, Some(1), None, area(), settings);
1910 tiling.insert(4, Some(2), None, area(), settings);
1911 tiling
1912 }
1913
1914 fn rect_at(tiling: &Tiling, id: u64, settings: Settings) -> Rect {
1915 tiling
1916 .layout(area(), settings)
1917 .into_iter()
1918 .find(|(other, _)| *other == id)
1919 .expect("in the tree")
1920 .1
1921 }
1922
1923 /// The whole of #120 as a coordinate: the edge the hand is on comes to rest
1924 /// exactly where it was asked to.
1925 ///
1926 /// Named for the ask and not for the pointer, because since #124 they are
1927 /// not the same place -- the compositor sends where the *edge* belongs.
1928 /// What #120 was about is unaffected: whatever the caller asks for, the
1929 /// grabbed edge has to land on it and not a gap away from it.
1930 ///
1931 /// Both halves drag the *same* seam — the root's vertical one — to the
1932 /// same x, one of them by window 1's right edge and one by window 2's
1933 /// left. Each has to put its own grabbed edge at exactly 700, and so the
1934 /// two drags have to agree with each other about where 700 is.
1935 ///
1936 /// Against the formula this replaced — a share of `rect.w`, with no term
1937 /// for the gap — the right edge came to rest at 691.5 and the left at
1938 /// 703.5. Each wrong, wrong in opposite directions, and a full gap apart
1939 /// from each other; all three assertions below fail on it. At `gap: 0.0`
1940 /// not one of them does, which is how the defect passed a green suite.
1941 #[test]
1942 fn a_dragged_vertical_edge_lands_where_it_was_asked_across_the_gap() {
1943 let settings = gapped();
1944
1945 let mut trailing = quad_at(settings);
1946 trailing.drag_seam(1, Edge::Right, (700.0, 150.0), area(), settings);
1947 let one = rect_at(&trailing, 1, settings);
1948 assert!(
1949 (one.x + one.w - 700.0).abs() < 0.5,
1950 "the grabbed right edge is where it was asked: {one:?}"
1951 );
1952
1953 let mut leading = quad_at(settings);
1954 leading.drag_seam(2, Edge::Left, (700.0, 150.0), area(), settings);
1955 let two = rect_at(&leading, 2, settings);
1956 assert!(
1957 (two.x - 700.0).abs() < 0.5,
1958 "the grabbed left edge is where it was asked: {two:?}"
1959 );
1960
1961 // One seam, grabbed from either side, put in one place. This is the
1962 // assertion the old arithmetic missed by exactly `gap`.
1963 assert!(
1964 ((one.x + one.w) - two.x).abs() < 0.5,
1965 "the two sides of one seam disagree: {one:?} against {two:?}"
1966 );
1967 }
1968
1969 /// The same, on the other axis, because the gap term is spelled once per
1970 /// axis and a fix to one of them is not a fix to the other. Window 1's
1971 /// bottom edge and window 3's top edge are the two sides of the left
1972 /// column's horizontal seam.
1973 #[test]
1974 fn a_dragged_horizontal_edge_lands_where_it_was_asked_across_the_gap() {
1975 let settings = gapped();
1976
1977 let mut trailing = quad_at(settings);
1978 trailing.drag_seam(1, Edge::Bottom, (250.0, 400.0), area(), settings);
1979 let one = rect_at(&trailing, 1, settings);
1980 assert!(
1981 (one.y + one.h - 400.0).abs() < 0.5,
1982 "the grabbed bottom edge is where it was asked: {one:?}"
1983 );
1984
1985 let mut leading = quad_at(settings);
1986 leading.drag_seam(3, Edge::Top, (250.0, 400.0), area(), settings);
1987 let three = rect_at(&leading, 3, settings);
1988 assert!(
1989 (three.y - 400.0).abs() < 0.5,
1990 "the grabbed top edge is where it was asked: {three:?}"
1991 );
1992
1993 assert!(
1994 ((one.y + one.h) - three.y).abs() < 0.5,
1995 "the two sides of one seam disagree: {one:?} against {three:?}"
1996 );
1997 }
1998
1999 /// #124: a window handed its own edge does not move.
2000 ///
2001 /// **Internal to this tree, and the #124 review is why that is now written
2002 /// down.** This was billed as the space check — the thing that pins the
2003 /// compositor's rectangle for a pane against the one [`Tiling::node_box`]
2004 /// measures a seam in. It is not, and structurally cannot be: the edge it
2005 /// hands `drag_seam` comes from `rect_at`, which is [`Tiling::layout`], so
2006 /// both ends of the round trip are this same tree. A skew introduced
2007 /// anywhere on the compositor's side of the call leaves it green, which is
2008 /// exactly what it did.
2009 ///
2010 /// What it *does* pin is worth keeping and is a narrower claim: within one
2011 /// arrangement, `node_box` and `layout` agree. Both begin at
2012 /// `area.inset(gap)` and both descend through [`cut`], so a slot's edge and
2013 /// a seam's box are two readings off one thing, and the inversion in
2014 /// [`Tiling::drag_seam`] undoes `cut` exactly rather than approximately. A
2015 /// half-gap error in either — #120's symptom by another route — shows here.
2016 ///
2017 /// The check this one was mistaken for lives in the compositor, because
2018 /// only the compositor has both rectangles:
2019 /// `solium::state::tests::real_client::a_client_that_rounds_its_size_does_not_move_the_seam`
2020 /// lays a real tree out through `Solium::place`, lets a real client commit a
2021 /// cell less than it was asked for, and feeds the edge the compositor would
2022 /// send on the first frame back into the tree. That is the boundary, and it
2023 /// cannot be crossed from this crate at all.
2024 ///
2025 /// At [`gapped`] rather than at zero, which is the first reason this can
2026 /// say anything: at `gap: 0` the two sides of a seam are one line and every
2027 /// skew it could catch is zero pixels wide.
2028 ///
2029 /// **And off-centre, which is the second, discovered by checking that the
2030 /// test can fail.** Skewing [`Tiling::node_box`]'s own inset by half a gap
2031 /// and re-running it — the exact defect it is here to catch — left it
2032 /// green. At ratio 0.5 that skew is self-cancelling: moving the box's
2033 /// origin out by 6 and its width out by 12 leaves its midpoint exactly
2034 /// where it was, so a seam sitting at the midpoint does not move and a
2035 /// fixture built at `split: 0.5` cannot tell. Each case therefore drags its
2036 /// seam somewhere deliberately lopsided first, and asserts that it went. On
2037 /// the same skew the strengthened version fails on its first case, by
2038 /// 1.87px — which is why the tolerance is 0.5px and not "a few pixels". A
2039 /// skew does not have to be as wide as the gap to be a skew.
2040 ///
2041 /// Unlike the compositor-side tests in
2042 /// `solium::input::resize::dragged_edge_tests`, this one passes against
2043 /// `bf80265` and against `ec1da24` too: `drag_seam` did not change for #124
2044 /// and has not changed since. It is a guard on this tree's own internal
2045 /// consistency, not a regression test for the value the compositor sends.
2046 #[test]
2047 fn a_window_handed_its_own_edge_does_not_move() {
2048 let settings = gapped();
2049 // One case per side, each on a window that actually has a seam there:
2050 // 1 has the root's vertical seam on its right and its column's
2051 // horizontal seam below, 2 has the root's on its left, 3 has its
2052 // column's above. A window flush against the container on the side
2053 // tested would pass this vacuously, `drag_seam` having returned early.
2054 //
2055 // The third number is where that seam goes first. Every one of them is
2056 // well inside the 0.05..0.95 clamp -- the extremes of these two seams
2057 // are 60..928 and 40..548 -- because a clamped ratio would hold still
2058 // for the second drag whatever space it was measured in, which is the
2059 // other way this test could pass without meaning anything.
2060 for (id, edge, lopsided) in [
2061 (1_u64, Edge::Right, (340.0, 150.0)),
2062 (2, Edge::Left, (640.0, 150.0)),
2063 (1, Edge::Bottom, (250.0, 200.0)),
2064 (3, Edge::Top, (250.0, 420.0)),
2065 ] {
2066 let mut tiling = quad_at(settings);
2067 let centred = rect_at(&tiling, id, settings);
2068 tiling.drag_seam(id, edge, lopsided, area(), settings);
2069 let before = rect_at(&tiling, id, settings);
2070 assert!(
2071 (before.w - centred.w).abs() > 50.0 || (before.h - centred.h).abs() > 50.0,
2072 "the fixture has to be off-centre before it proves anything: \
2073 window {id} is still {before:?}"
2074 );
2075
2076 let own_edge = match edge {
2077 Edge::Left => (before.x, before.y + before.h / 2.0),
2078 Edge::Right => (before.x + before.w, before.y + before.h / 2.0),
2079 Edge::Top => (before.x + before.w / 2.0, before.y),
2080 Edge::Bottom => (before.x + before.w / 2.0, before.y + before.h),
2081 };
2082
2083 tiling.drag_seam(id, edge, own_edge, area(), settings);
2084 let after = rect_at(&tiling, id, settings);
2085
2086 assert!(
2087 (after.x - before.x).abs() < 0.5
2088 && (after.y - before.y).abs() < 0.5
2089 && (after.w - before.w).abs() < 0.5
2090 && (after.h - before.h).abs() < 0.5,
2091 "window {id} handed its own {edge:?} edge at {own_edge:?} moved: \
2092 {before:?} became {after:?}"
2093 );
2094 }
2095 }
2096
2097 /// Idempotence at a real gap.
2098 ///
2099 /// The doc on [`Tiling::drag_seam`] promises that dragging to the same
2100 /// place twice gives the same layout, and that promise is what stops the
2101 /// windows shaking while the button is held. It is worth pinning at a
2102 /// non-zero gap specifically.
2103 ///
2104 /// Unlike the two above, this one passed against the old formula too —
2105 /// being wrong by a constant offset is still being wrong in the same place
2106 /// every time. It is a guard and not a witness: what it catches is a
2107 /// future inversion that reads its own output back, the shape the
2108 /// delta-accumulating version had, and only a non-zero gap would make that
2109 /// term visible.
2110 #[test]
2111 fn dragging_to_the_same_place_twice_changes_nothing_at_a_real_gap() {
2112 let settings = gapped();
2113 let mut tiling = quad_at(settings);
2114
2115 tiling.drag_seam(1, Edge::Right, (700.0, 150.0), area(), settings);
2116 let once = rect_at(&tiling, 1, settings);
2117 tiling.drag_seam(1, Edge::Right, (700.0, 150.0), area(), settings);
2118 let twice = rect_at(&tiling, 1, settings);
2119
2120 assert!(
2121 (once.w - twice.w).abs() < 0.001,
2122 "the second drag moved it: {once:?} then {twice:?}"
2123 );
2124 }
2125}
2126
2127/// #134: a floor under the tiles, and the seams that honour it.
2128///
2129/// Every arrangement here is built with `insert`, which does not read the
2130/// minimum, and with drags at [`free`] -- no minimum, the tree as it was
2131/// before #134 -- and only then is a question asked with a minimum in force.
2132/// A fixture built under the floor would already have been bent by it, and
2133/// the question would be about the fixture.
2134#[cfg(test)]
2135mod minimum_tests {
2136 use super::{Axis, Edge, Tiling};
2137 use crate::{Minimum, Rect, Settings};
2138
2139 fn free(gap: f64) -> Settings {
2140 Settings {
2141 gap,
2142 split: 0.5,
2143 ..Settings::default()
2144 }
2145 }
2146
2147 fn floored(gap: f64, w: f64, h: f64) -> Settings {
2148 Settings {
2149 minimum: Minimum { w, h },
2150 ..free(gap)
2151 }
2152 }
2153
2154 /// What `config.tiling.minimum` ships as, and `config.gap`.
2155 fn shipped() -> Settings {
2156 floored(12.0, 160.0, 96.0)
2157 }
2158
2159 fn rect_in(tiling: &Tiling, id: u64, area: Rect, settings: Settings) -> Rect {
2160 tiling
2161 .layout(area, settings)
2162 .into_iter()
2163 .find(|(other, _)| *other == id)
2164 .expect("in the tree")
2165 .1
2166 }
2167
2168 fn every_rect(tiling: &Tiling, area: Rect, settings: Settings) -> Vec<(u64, Rect)> {
2169 let mut out = tiling.layout(area, settings);
2170 out.sort_by_key(|(id, _)| *id);
2171 out
2172 }
2173
2174 fn near(left: Rect, right: Rect) -> bool {
2175 (left.x - right.x).abs() < 0.5
2176 && (left.y - right.y).abs() < 0.5
2177 && (left.w - right.w).abs() < 0.5
2178 && (left.h - right.h).abs() < 0.5
2179 }
2180
2181 /// **The tile under the pointer tries the other direction first.** A wide,
2182 /// short tile with no room side by side still has room one above the
2183 /// other, and that is still the tile the user pointed at.
2184 ///
2185 /// 300x200 at no gap: side by side is two 150-wide halves, under a
2186 /// 160-wide floor; one above the other is two 300x100s, over a 96-high one.
2187 /// `insert` -- the rule before #134, and `"allow"` now -- cuts it side by
2188 /// side regardless, which is the contrast that makes this a test of the
2189 /// turn rather than of the arrangement.
2190 #[test]
2191 fn a_tile_with_no_room_side_by_side_splits_one_above_the_other() {
2192 let area = Rect::new(0.0, 0.0, 300.0, 200.0);
2193 let settings = floored(0.0, 160.0, 96.0);
2194 let mut tiling = Tiling::new();
2195 tiling.insert(1, None, None, area, settings);
2196
2197 let mut allowed = tiling.clone();
2198 allowed.insert(2, None, Some((150.0, 150.0)), area, settings);
2199 assert!(
2200 (rect_in(&allowed, 2, area, settings).w - 150.0).abs() < 0.5,
2201 "the premise: `insert` cuts this tile side by side"
2202 );
2203
2204 // The pointer in the lower half, so the new window goes below.
2205 assert!(tiling.insert_fitting(2, None, Some((150.0, 150.0)), area, settings));
2206 let (one, two) = (
2207 rect_in(&tiling, 1, area, settings),
2208 rect_in(&tiling, 2, area, settings),
2209 );
2210 assert!(
2211 (one.w - 300.0).abs() < 0.5 && (two.w - 300.0).abs() < 0.5,
2212 "split one above the other, full width each: {one:?} {two:?}"
2213 );
2214 assert!(
2215 (one.h - 100.0).abs() < 0.5 && (two.h - 100.0).abs() < 0.5,
2216 "and half the height each: {one:?} {two:?}"
2217 );
2218 assert!(two.y > one.y, "on the pointer's side, below: {two:?}");
2219 }
2220
2221 /// **A tile with room neither way is left alone**, and so is the whole
2222 /// tree: `false` is only useful to a caller if trying somewhere else
2223 /// starts from where it was. Then `insert` still takes the window, once,
2224 /// which is what a node pushed and abandoned by the refusal would break --
2225 /// `contains` would find it, and `insert` would return without adding it.
2226 #[test]
2227 fn a_tile_with_no_room_either_way_is_left_alone() {
2228 let area = Rect::new(0.0, 0.0, 300.0, 180.0);
2229 let settings = floored(0.0, 160.0, 96.0);
2230 let mut tiling = Tiling::new();
2231 tiling.insert(1, None, None, area, settings);
2232 let before = every_rect(&tiling, area, settings);
2233
2234 assert!(!tiling.insert_fitting(2, None, Some((150.0, 90.0)), area, settings));
2235 assert!(!tiling.insert_largest(2, area, settings));
2236 assert!(!tiling.contains(2), "a refused window is not in the tree");
2237 assert_eq!(every_rect(&tiling, area, settings), before);
2238
2239 tiling.insert(2, None, Some((150.0, 90.0)), area, settings);
2240 assert_eq!(tiling.windows().len(), 2, "{:?}", tiling.windows());
2241 }
2242
2243 /// **Exactly the minimum is enough; a pixel less is not.** 356 wide less
2244 /// a 12 gap on each side is a 332 tile, and 332 less the 12 between the
2245 /// halves is two of exactly 160. One pixel narrower and each half is
2246 /// 159.5, and turning it does not help: 200 high splits into two of 94,
2247 /// under 96.
2248 #[test]
2249 fn a_split_that_lands_exactly_on_the_minimum_has_room() {
2250 let settings = shipped();
2251 let exact = Rect::new(0.0, 0.0, 356.0, 224.0);
2252 let mut tiling = Tiling::new();
2253 tiling.insert(1, None, None, exact, settings);
2254 assert!(
2255 tiling.insert_fitting(2, None, Some((300.0, 100.0)), exact, settings),
2256 "two halves of exactly the minimum were refused"
2257 );
2258 for id in [1, 2] {
2259 let rect = rect_in(&tiling, id, exact, settings);
2260 assert!(
2261 (rect.w - 160.0).abs() < 1e-9,
2262 "window {id} is not at the minimum: {rect:?}"
2263 );
2264 }
2265
2266 let short = Rect::new(0.0, 0.0, 355.0, 224.0);
2267 let mut tiling = Tiling::new();
2268 tiling.insert(1, None, None, short, settings);
2269 assert!(
2270 !tiling.insert_fitting(2, None, Some((300.0, 100.0)), short, settings),
2271 "halves of 159.5 were let under a 160 minimum"
2272 );
2273 }
2274
2275 /// An empty tree has room whatever size it is: its one window is not a
2276 /// division of anything, so there is no tile to keep above the floor.
2277 #[test]
2278 fn an_empty_tree_always_has_room() {
2279 let tiny = Rect::new(0.0, 0.0, 100.0, 50.0);
2280 let mut fitting = Tiling::new();
2281 assert!(fitting.insert_fitting(1, None, None, tiny, shipped()));
2282 let mut largest = Tiling::new();
2283 assert!(largest.insert_largest(1, tiny, shipped()));
2284 assert_eq!(fitting.windows(), vec![1]);
2285 assert_eq!(largest.windows(), vec![1]);
2286 }
2287
2288 /// Window 1 on the left, 599x700; windows 2 over 3 on the right, 601x401
2289 /// and 601x299. Built without a minimum, then asked about one of 300x360.
2290 ///
2291 /// Window 1 is the largest and has no room: across it is two of 299.5,
2292 /// under 300, and down it is two of 350, under 360. Window 3 is already
2293 /// shorter than 360 and no split makes it taller. Window 2 has room across,
2294 /// two of 300.5 by 401.
2295 fn crowded() -> (Tiling, Rect) {
2296 let area = Rect::new(0.0, 0.0, 1200.0, 700.0);
2297 let settings = free(0.0);
2298 let mut tiling = Tiling::new();
2299 tiling.insert(1, None, None, area, settings);
2300 tiling.insert(2, Some(1), None, area, settings);
2301 tiling.insert(3, Some(2), None, area, settings);
2302 tiling.drag_seam(1, Edge::Right, (599.0, 0.0), area, settings);
2303 tiling.drag_seam(2, Edge::Bottom, (0.0, 401.0), area, settings);
2304 (tiling, area)
2305 }
2306
2307 /// **`"largest"` splits the largest tile that has room, which is not
2308 /// always the largest tile.** See [`crowded`].
2309 #[test]
2310 fn largest_splits_the_largest_tile_that_has_room() {
2311 let (mut tiling, area) = crowded();
2312 let settings = floored(0.0, 300.0, 360.0);
2313 let one = rect_in(&tiling, 1, area, settings);
2314 let three = rect_in(&tiling, 3, area, settings);
2315 assert!(
2316 (one.w - 599.0).abs() < 0.5 && (three.h - 299.0).abs() < 0.5,
2317 "the fixture is not the one described: {:?}",
2318 every_rect(&tiling, area, settings)
2319 );
2320
2321 // Pointing into window 3, which has no room.
2322 assert!(!tiling.insert_fitting(4, None, Some((900.0, 600.0)), area, settings));
2323 assert!(tiling.insert_largest(4, area, settings));
2324
2325 let (two, four) = (
2326 rect_in(&tiling, 2, area, settings),
2327 rect_in(&tiling, 4, area, settings),
2328 );
2329 assert!(
2330 (two.w - 300.5).abs() < 0.5 && (four.w - 300.5).abs() < 0.5,
2331 "window 2's tile was not the one split: {:?}",
2332 every_rect(&tiling, area, settings)
2333 );
2334 assert!(four.x > two.x, "the new window takes the far side");
2335 assert!(near(rect_in(&tiling, 1, area, settings), one), "1 moved");
2336 assert!(near(rect_in(&tiling, 3, area, settings), three), "3 moved");
2337 }
2338
2339 /// Two columns of two at a real gap, with no minimum, exactly as
2340 /// `dragged_edge_tests::quad_at` builds them: 1 top-left, 2 top-right, 3
2341 /// bottom-left, 4 bottom-right, each 482x282 in a 1000x600 area.
2342 fn quad(settings: Settings) -> Tiling {
2343 let area = area();
2344 let mut tiling = Tiling::new();
2345 tiling.insert(1, None, None, area, settings);
2346 tiling.insert(2, Some(1), None, area, settings);
2347 tiling.insert(3, Some(1), None, area, settings);
2348 tiling.insert(4, Some(2), None, area, settings);
2349 tiling
2350 }
2351
2352 fn area() -> Rect {
2353 Rect::new(0.0, 0.0, 1000.0, 600.0)
2354 }
2355
2356 /// **No drag takes a tile under the minimum**, on any of the four sides
2357 /// that have a seam. The box is 976 wide less a 12 gap, so a 300 floor on
2358 /// either side of the root seam leaves 364 of travel, and each is dragged
2359 /// well past it.
2360 #[test]
2361 fn a_seam_cannot_be_dragged_to_make_a_tile_smaller_than_the_minimum() {
2362 let settings = floored(12.0, 300.0, 150.0);
2363 for (id, edge, to, measured, wanted) in [
2364 (1_u64, Edge::Right, (100.0, 150.0), 1_u64, 300.0),
2365 (2, Edge::Left, (950.0, 150.0), 2, 300.0),
2366 (1, Edge::Bottom, (250.0, 50.0), 1, 150.0),
2367 (3, Edge::Top, (250.0, 580.0), 3, 150.0),
2368 ] {
2369 let mut tiling = quad(free(12.0));
2370 tiling.drag_seam(id, edge, to, area(), settings);
2371 let rect = rect_in(&tiling, measured, area(), settings);
2372 let size = match edge.axis() {
2373 Axis::Vertical => rect.w,
2374 Axis::Horizontal => rect.h,
2375 };
2376 assert!(
2377 (size - wanted).abs() < 0.5,
2378 "dragging window {id}'s {edge:?} edge to {to:?} left window {measured} at \
2379 {rect:?}, not stopped at the {wanted} minimum"
2380 );
2381 }
2382 }
2383
2384 /// **The floor counts every tile beyond the seam, not only the one beside
2385 /// it.** Window 4 is split again, so the right column holds 2 above a pair
2386 /// side by side, and the root seam -- window 4's left edge -- has to leave
2387 /// room for both halves of that pair: 160, the gap, 160.
2388 #[test]
2389 fn a_seam_leaves_room_for_every_tile_beyond_it() {
2390 let settings = shipped();
2391 let mut tiling = quad(free(12.0));
2392 tiling.insert(5, Some(4), None, area(), free(12.0));
2393 tiling.drag_seam(4, Edge::Left, (900.0, 450.0), area(), settings);
2394 let (four, five) = (
2395 rect_in(&tiling, 4, area(), settings),
2396 rect_in(&tiling, 5, area(), settings),
2397 );
2398 assert!(
2399 (four.w - 160.0).abs() < 0.5 && (five.w - 160.0).abs() < 0.5,
2400 "the pair beyond the seam was squeezed under the minimum: {four:?} {five:?}"
2401 );
2402 assert!(
2403 (five.x + five.w - 988.0).abs() < 0.5,
2404 "and the far edge stayed against the container: {five:?}"
2405 );
2406 }
2407
2408 /// **A drag on a seam that is already under the minimum does not jump.**
2409 /// The #124 class of bug, through a floor instead of a skew: clamping a
2410 /// ratio that is outside its range moves the seam on the first frame of
2411 /// the drag, with the pointer still where it pressed.
2412 ///
2413 /// Window 1 is dragged to 328 wide with no minimum, then a 600 minimum is
2414 /// put on it. Handed its own edge, nothing moves. Asked to shrink further,
2415 /// nothing moves. Asked to grow, it follows -- until window 2, at 636 and
2416 /// so over the floor, reaches 600 on the other side of the seam.
2417 #[test]
2418 fn a_drag_on_a_seam_already_under_the_minimum_does_not_jump() {
2419 let settings = floored(12.0, 600.0, 96.0);
2420 let lopsided = || {
2421 let mut tiling = quad(free(12.0));
2422 tiling.drag_seam(1, Edge::Right, (340.0, 150.0), area(), free(12.0));
2423 tiling
2424 };
2425 let before = every_rect(&lopsided(), area(), settings);
2426 let one = rect_in(&lopsided(), 1, area(), settings);
2427 assert!(
2428 (one.x + one.w - 340.0).abs() < 0.5,
2429 "the premise: window 1's right edge at 340: {one:?}"
2430 );
2431
2432 for (to, says) in [
2433 (340.0, "handed its own edge"),
2434 (300.0, "asked to shrink a tile already under the minimum"),
2435 ] {
2436 let mut tiling = lopsided();
2437 tiling.drag_seam(1, Edge::Right, (to, 150.0), area(), settings);
2438 assert_eq!(
2439 every_rect(&tiling, area(), settings),
2440 before,
2441 "{says}, the seam moved"
2442 );
2443 }
2444
2445 let mut tiling = lopsided();
2446 tiling.drag_seam(1, Edge::Right, (350.0, 150.0), area(), settings);
2447 let one = rect_in(&tiling, 1, area(), settings);
2448 assert!(
2449 (one.x + one.w - 350.0).abs() < 0.5,
2450 "growing the small side was refused: {one:?}"
2451 );
2452
2453 tiling.drag_seam(1, Edge::Right, (900.0, 150.0), area(), settings);
2454 let two = rect_in(&tiling, 2, area(), settings);
2455 assert!(
2456 (two.w - 600.0).abs() < 0.5,
2457 "the far side went under its own minimum: {two:?}"
2458 );
2459 }
2460
2461 /// With both sides of a seam under the minimum, any move shrinks one of
2462 /// them, so the seam holds still. The quad's columns are 482 each, both
2463 /// under 600.
2464 #[test]
2465 fn a_seam_with_both_sides_under_the_minimum_holds_still() {
2466 let settings = floored(12.0, 600.0, 96.0);
2467 for to in [100.0, 494.0, 900.0] {
2468 let mut tiling = quad(free(12.0));
2469 let before = every_rect(&tiling, area(), settings);
2470 tiling.drag_seam(1, Edge::Right, (to, 150.0), area(), settings);
2471 assert_eq!(
2472 every_rect(&tiling, area(), settings),
2473 before,
2474 "dragged to {to}, a seam with no room on either side moved"
2475 );
2476 }
2477 }
2478
2479 /// **The shipped minimum changes none of the #120 and #124 drags.** Each
2480 /// is the drag from `dragged_edge_tests` -- the four lopsided moves of
2481 /// `a_window_handed_its_own_edge_does_not_move`, then the window handed
2482 /// its own edge, and the two-sided 700 of the #120 case -- run once with
2483 /// no minimum and once at 160x96, and the two have to agree to the pixel.
2484 /// Every tile involved stays over 160x96, so a floor that moved any of
2485 /// them would be a floor clamping where it has no business to.
2486 #[test]
2487 fn the_shipped_minimum_leaves_every_existing_drag_where_it_was() {
2488 let cases: [(u64, Edge, (f64, f64)); 6] = [
2489 (1, Edge::Right, (340.0, 150.0)),
2490 (2, Edge::Left, (640.0, 150.0)),
2491 (1, Edge::Bottom, (250.0, 200.0)),
2492 (3, Edge::Top, (250.0, 420.0)),
2493 (1, Edge::Right, (700.0, 150.0)),
2494 (2, Edge::Left, (700.0, 150.0)),
2495 ];
2496 for (id, edge, to) in cases {
2497 let run = |settings: Settings| {
2498 let mut tiling = quad(settings);
2499 tiling.drag_seam(id, edge, to, area(), settings);
2500 let placed = rect_in(&tiling, id, area(), settings);
2501 let own = match edge {
2502 Edge::Left => (placed.x, placed.y + placed.h / 2.0),
2503 Edge::Right => (placed.x + placed.w, placed.y + placed.h / 2.0),
2504 Edge::Top => (placed.x + placed.w / 2.0, placed.y),
2505 Edge::Bottom => (placed.x + placed.w / 2.0, placed.y + placed.h),
2506 };
2507 tiling.drag_seam(id, edge, own, area(), settings);
2508 every_rect(&tiling, area(), settings)
2509 };
2510 let (without, with) = (run(free(12.0)), run(shipped()));
2511 assert!(
2512 without
2513 .iter()
2514 .zip(&with)
2515 .all(|((a, left), (b, right))| a == b && near(*left, *right)),
2516 "window {id}'s {edge:?} drag to {to:?} came out differently under the \
2517 shipped minimum: {without:?} against {with:?}"
2518 );
2519 }
2520 }
2521
2522 /// **The keyboard is held to the same floor**, and to the same rule for a
2523 /// tile already under it. At a 400 minimum window 1 shrinks to 400 and no
2524 /// further. Then, lopsided to 328 and put under a 600 minimum, a press
2525 /// that would shrink it does nothing and one that grows it stops where
2526 /// window 2 reaches 600.
2527 #[test]
2528 fn the_keyboard_does_not_shrink_a_tile_under_the_minimum() {
2529 let settings = floored(12.0, 400.0, 96.0);
2530 let mut tiling = quad(free(12.0));
2531 for _ in 0..5 {
2532 tiling.resize(1, Axis::Vertical, -0.05, area(), settings);
2533 }
2534 let one = rect_in(&tiling, 1, area(), settings);
2535 assert!((one.w - 400.0).abs() < 0.5, "shrunk past 400: {one:?}");
2536
2537 let settings = floored(12.0, 600.0, 96.0);
2538 let mut tiling = quad(free(12.0));
2539 tiling.drag_seam(1, Edge::Right, (340.0, 150.0), area(), free(12.0));
2540 let before = every_rect(&tiling, area(), settings);
2541 tiling.resize(1, Axis::Vertical, -0.05, area(), settings);
2542 assert_eq!(
2543 every_rect(&tiling, area(), settings),
2544 before,
2545 "a press shrank a tile already under the minimum"
2546 );
2547 tiling.resize(1, Axis::Vertical, 0.05, area(), settings);
2548 let two = rect_in(&tiling, 2, area(), settings);
2549 assert!(
2550 (two.w - 600.0).abs() < 0.5,
2551 "growing window 1 took window 2 under the minimum, or stopped short: {two:?}"
2552 );
2553 }
2554
2555 /// A split of NaN -- `split = 0/0` in a configuration -- is a seam whose
2556 /// ratio is NaN, and [`Tiling::room`] then cannot widen its range to take
2557 /// that ratio in. With a minimum no ratio can satisfy -- 600 on each side
2558 /// of a 964 span -- the range is empty, which is exactly where `f64::clamp`
2559 /// panics, and a panic here is the session gone. The press and the drag
2560 /// below have to come back; that is the assertion, with every rect finite
2561 /// besides.
2562 #[test]
2563 fn a_nan_split_leaves_the_clamp_standing() {
2564 let broken = Settings {
2565 split: f64::NAN,
2566 ..floored(12.0, 600.0, 96.0)
2567 };
2568 let mut tiling = Tiling::new();
2569 tiling.insert(1, None, None, area(), broken);
2570 tiling.insert(2, Some(1), None, area(), broken);
2571 tiling.resize(1, Axis::Vertical, 0.05, area(), broken);
2572 tiling.drag_seam(1, Edge::Right, (500.0, 300.0), area(), broken);
2573 for (id, rect) in tiling.layout(area(), broken) {
2574 assert!(
2575 rect.w.is_finite() && rect.h.is_finite(),
2576 "window {id} was laid out at {rect:?}"
2577 );
2578 }
2579 }
2580}
2581
2582/// **#115: a window's own floor**, as [`Tiling::set_floors`] hands it in.
2583///
2584/// A 1000x600 area at no gap unless a test says otherwise, so every number is
2585/// the one on the page. Each fixture is built with no floors -- the tree the
2586/// user made -- and the floors are handed in afterwards, which is the order a
2587/// session has them in: a client says how small it can go after its window
2588/// has a tile.
2589#[cfg(test)]
2590mod floor_tests {
2591 use super::{Axis, Edge, Tiling};
2592 use crate::{Floors, Minimum, Rect, Settings};
2593
2594 fn area() -> Rect {
2595 Rect::new(0.0, 0.0, 1000.0, 600.0)
2596 }
2597
2598 fn free() -> Settings {
2599 Settings {
2600 gap: 0.0,
2601 split: 0.5,
2602 ..Settings::default()
2603 }
2604 }
2605
2606 fn floored(w: f64, h: f64) -> Settings {
2607 Settings {
2608 minimum: Minimum { w, h },
2609 ..free()
2610 }
2611 }
2612
2613 fn floors(of: &[(u64, f64, f64)]) -> Floors {
2614 of.iter()
2615 .map(|&(id, w, h)| (id, Minimum { w, h }))
2616 .collect()
2617 }
2618
2619 fn rect_in(tiling: &Tiling, id: u64, area: Rect, settings: Settings) -> Rect {
2620 tiling
2621 .layout(area, settings)
2622 .into_iter()
2623 .find(|(other, _)| *other == id)
2624 .expect("in the tree")
2625 .1
2626 }
2627
2628 /// 1 on the left and 2 on the right, 500 each.
2629 fn pair() -> Tiling {
2630 let mut tiling = Tiling::new();
2631 tiling.insert(1, None, None, area(), free());
2632 tiling.insert(2, Some(1), None, area(), free());
2633 tiling
2634 }
2635
2636 /// **A window under its floor takes the room it needs from its sibling.**
2637 /// Window 1 says it cannot go under 700 wide in a 500 tile: the seam moves
2638 /// 200 to the right, and window 2 is what gives it.
2639 #[test]
2640 fn a_window_under_its_floor_takes_room_from_its_sibling() {
2641 let mut tiling = pair();
2642 tiling.set_floors(floors(&[(1, 700.0, 0.0)]));
2643 let (one, two) = (
2644 rect_in(&tiling, 1, area(), free()),
2645 rect_in(&tiling, 2, area(), free()),
2646 );
2647 assert!(
2648 (one.w - 700.0).abs() < 1e-6,
2649 "window 1 was not given its floor: {one:?}"
2650 );
2651 assert!(
2652 (two.x - 700.0).abs() < 1e-6 && (two.w - 300.0).abs() < 1e-6,
2653 "window 2 did not give up the difference: {two:?}"
2654 );
2655 assert!(!tiling.cramped(1, one) && !tiling.cramped(2, two));
2656 }
2657
2658 /// **And no more than the sibling holds**: a sibling gives down to its own
2659 /// floor or the minimum, whichever is larger, and not a pixel past it. At a
2660 /// 400-wide minimum window 1, asking for 800, gets the 600 that leaves
2661 /// window 2 at 400, and is laid out short -- cramped. Window 2, at the
2662 /// minimum exactly, is not; nor is a tile under the minimum with no floor
2663 /// of its own, which is `"allow"`'s business and not a refusal.
2664 #[test]
2665 fn a_sibling_gives_no_more_than_it_holds() {
2666 let settings = floored(400.0, 0.0);
2667 let mut tiling = pair();
2668 tiling.set_floors(floors(&[(1, 800.0, 0.0)]));
2669 let (one, two) = (
2670 rect_in(&tiling, 1, area(), settings),
2671 rect_in(&tiling, 2, area(), settings),
2672 );
2673 assert!((one.w - 600.0).abs() < 1e-6, "{one:?}");
2674 assert!(
2675 (two.w - 400.0).abs() < 1e-6,
2676 "window 2 went under the minimum: {two:?}"
2677 );
2678 assert!(
2679 tiling.cramped(1, one),
2680 "a window laid out under its floor is cramped"
2681 );
2682 assert!(!tiling.cramped(2, two), "a window at the minimum is not");
2683
2684 let under = Rect::new(0.0, 0.0, 100.0, 600.0);
2685 assert!(
2686 !tiling.cramped(2, under),
2687 "a tile under the minimum with no floor of its own is not cramped"
2688 );
2689 }
2690
2691 /// **The ratio the branch holds is left alone**, so a window whose floor
2692 /// goes away hands the room back. The floors are replaced whole: handing in
2693 /// a set without window 1 is window 1 having none.
2694 #[test]
2695 fn a_window_whose_floor_goes_away_hands_the_room_back() {
2696 let mut tiling = pair();
2697 tiling.set_floors(floors(&[(1, 700.0, 0.0)]));
2698 assert!((rect_in(&tiling, 1, area(), free()).w - 700.0).abs() < 1e-6);
2699
2700 tiling.set_floors(floors(&[(2, 0.0, 100.0)]));
2701 let one = rect_in(&tiling, 1, area(), free());
2702 assert!(
2703 (one.w - 500.0).abs() < 1e-6,
2704 "the seam stayed where the floor pushed it: {one:?}"
2705 );
2706 }
2707
2708 /// **Two windows that cannot both fit leave the seam where the user put
2709 /// it.** 700 and 700 do not go into 1000; moving the seam either way takes
2710 /// from one window short of room to give to the other.
2711 #[test]
2712 fn two_windows_that_cannot_both_fit_keep_the_users_seam() {
2713 let mut tiling = pair();
2714 tiling.drag_seam(1, Edge::Right, (450.0, 300.0), area(), free());
2715 tiling.set_floors(floors(&[(1, 700.0, 0.0), (2, 700.0, 0.0)]));
2716 let (one, two) = (
2717 rect_in(&tiling, 1, area(), free()),
2718 rect_in(&tiling, 2, area(), free()),
2719 );
2720 assert!((one.w - 450.0).abs() < 1e-6, "{one:?}");
2721 assert!((two.w - 550.0).abs() < 1e-6, "{two:?}");
2722 assert!(tiling.cramped(1, one) && tiling.cramped(2, two));
2723 }
2724
2725 /// **A floor deep in a subtree takes its room across the root**, and a
2726 /// subtree cut the same way gives down to what each window in it holds.
2727 ///
2728 /// On 1600x600 at a 100-wide minimum: 1 on the left, 800; 2 and 3 side by
2729 /// side on the right, 400 each. Window 3 asks for 750. The right side needs
2730 /// 750 for window 3 and 100 for window 2, 850 in all, so the root seam
2731 /// moves 50 left; then window 2 gives 350 of its 400 to window 3.
2732 ///
2733 /// Then cut the other way: 3 under 2 in the right column, and a floor of
2734 /// 900 on 3. A column is as narrow as its widest floor, so the root seam
2735 /// gives the whole column 900, and window 2 goes with it.
2736 #[test]
2737 fn a_floor_deep_in_a_subtree_takes_room_across_the_root() {
2738 let wide = Rect::new(0.0, 0.0, 1600.0, 600.0);
2739 let settings = floored(100.0, 0.0);
2740 let mut tiling = Tiling::new();
2741 tiling.insert(1, None, None, wide, free());
2742 tiling.insert(2, Some(1), None, wide, free());
2743 tiling.insert(3, Some(2), None, wide, free());
2744 assert!(
2745 (rect_in(&tiling, 3, wide, free()).w - 400.0).abs() < 1e-6,
2746 "the premise: 2 and 3 side by side on the right"
2747 );
2748 tiling.set_floors(floors(&[(3, 750.0, 0.0)]));
2749 let (one, two, three) = (
2750 rect_in(&tiling, 1, wide, settings),
2751 rect_in(&tiling, 2, wide, settings),
2752 rect_in(&tiling, 3, wide, settings),
2753 );
2754 assert!((one.w - 750.0).abs() < 1e-6, "the root seam: {one:?}");
2755 assert!(
2756 (two.w - 100.0).abs() < 1e-6,
2757 "window 2 held its minimum: {two:?}"
2758 );
2759 assert!(
2760 (three.w - 750.0).abs() < 1e-6,
2761 "window 3 has its floor: {three:?}"
2762 );
2763
2764 let mut column = Tiling::new();
2765 column.insert(1, None, None, area(), free());
2766 column.insert(2, Some(1), None, area(), free());
2767 column.insert(3, Some(2), None, area(), free());
2768 assert!(
2769 (rect_in(&column, 3, area(), free()).h - 300.0).abs() < 1e-6,
2770 "the premise: 3 under 2 on the right"
2771 );
2772 column.set_floors(floors(&[(3, 900.0, 0.0)]));
2773 let (one, two, three) = (
2774 rect_in(&column, 1, area(), settings),
2775 rect_in(&column, 2, area(), settings),
2776 rect_in(&column, 3, area(), settings),
2777 );
2778 assert!((one.w - 100.0).abs() < 1e-6, "{one:?}");
2779 assert!(
2780 (two.w - 900.0).abs() < 1e-6 && (three.w - 900.0).abs() < 1e-6,
2781 "the column is as wide as its widest floor: {two:?} {three:?}"
2782 );
2783 }
2784
2785 /// **A split that would leave a window under its own floor is refused**,
2786 /// by both of #134's inserts, and whichever of the two windows the floor
2787 /// is on. At a 400x400 minimum in 1000x600: side by side, a window that
2788 /// needs 700 gets 600 at most; one above the other is 300 high each. So
2789 /// window 1 with a floor has no room for window 2 either way, and nor does
2790 /// window 1 without one for a window 2 with it -- the new window's floor is
2791 /// read before it is in the tree.
2792 ///
2793 /// Without floors, the same split has room: 500 and 500 side by side.
2794 #[test]
2795 fn a_split_that_would_put_a_window_under_its_floor_is_refused() {
2796 let settings = floored(400.0, 400.0);
2797 for (owner, says) in [(1, "the window being split"), (2, "the window being added")] {
2798 let mut tiling = Tiling::new();
2799 tiling.insert(1, None, None, area(), settings);
2800 let mut control = tiling.clone();
2801 assert!(
2802 control.insert_fitting(2, None, Some((900.0, 300.0)), area(), settings),
2803 "the premise: without a floor there is room"
2804 );
2805
2806 tiling.set_floors(floors(&[(owner, 700.0, 0.0)]));
2807 assert!(
2808 !tiling.insert_fitting(2, None, Some((900.0, 300.0)), area(), settings),
2809 "a floor on {says} was not read by insert_fitting"
2810 );
2811 assert!(
2812 !tiling.insert_largest(2, area(), settings),
2813 "a floor on {says} was not read by insert_largest"
2814 );
2815 assert_eq!(tiling.windows(), vec![1]);
2816 }
2817 }
2818
2819 /// **A seam stops at a neighbour's own floor -- the seam itself, not only
2820 /// the picture of it.** Window 2 needs 600: dragging window 1's right edge
2821 /// to 900 stops at 400. Then window 2's floor goes away, and the seam is
2822 /// still where the drag stopped it; a seam that had followed the pointer
2823 /// out of sight would now jump to 900.
2824 ///
2825 /// And from the other side: window 1 needs 600, so dragging its right edge
2826 /// in to 100 leaves it at 600.
2827 #[test]
2828 fn a_seam_stops_at_a_neighbours_own_floor() {
2829 let mut tiling = pair();
2830 tiling.set_floors(floors(&[(2, 600.0, 0.0)]));
2831 tiling.drag_seam(1, Edge::Right, (900.0, 300.0), area(), free());
2832 let two = rect_in(&tiling, 2, area(), free());
2833 assert!(
2834 (two.w - 600.0).abs() < 1e-6,
2835 "window 2 went under its floor: {two:?}"
2836 );
2837 tiling.set_floors(Floors::new());
2838 let one = rect_in(&tiling, 1, area(), free());
2839 assert!(
2840 (one.w - 400.0).abs() < 1e-6,
2841 "the seam went on past where it was drawn: {one:?}"
2842 );
2843
2844 let mut tiling = pair();
2845 tiling.set_floors(floors(&[(1, 600.0, 0.0)]));
2846 tiling.drag_seam(1, Edge::Right, (100.0, 300.0), area(), free());
2847 let one = rect_in(&tiling, 1, area(), free());
2848 assert!((one.w - 600.0).abs() < 1e-6, "{one:?}");
2849 }
2850
2851 /// **A press stops at a window's own floor**, as a drag does: window 1
2852 /// needs 600, and five presses that would take it to 250 leave it there.
2853 #[test]
2854 fn a_press_stops_at_a_windows_own_floor() {
2855 let mut tiling = pair();
2856 tiling.set_floors(floors(&[(1, 600.0, 0.0)]));
2857 for _ in 0..5 {
2858 tiling.resize(1, Axis::Vertical, -0.05, area(), free());
2859 }
2860 tiling.set_floors(Floors::new());
2861 let one = rect_in(&tiling, 1, area(), free());
2862 assert!((one.w - 600.0).abs() < 1e-6, "{one:?}");
2863 }
2864
2865 /// **A press moves the seam from where it is drawn.** The tree holds 0.5
2866 /// and window 1's floor draws it at 0.7; `super+equal` grows window 1 by
2867 /// 0.05 of the box from there, to 750. From the ratio it holds, 0.55, the
2868 /// press would have changed nothing on screen.
2869 #[test]
2870 fn a_press_beside_a_floor_moves_the_seam_from_where_it_is_drawn() {
2871 let mut tiling = pair();
2872 tiling.set_floors(floors(&[(1, 700.0, 0.0)]));
2873 tiling.resize(1, Axis::Vertical, 0.05, area(), free());
2874 let one = rect_in(&tiling, 1, area(), free());
2875 assert!((one.w - 750.0).abs() < 1e-6, "{one:?}");
2876 }
2877}