Commit 9baaafa4b167
tarball6 files changed+1772-0
@@ -409,6 +409,11 @@ version = "0.0.0"
409409
[[package]]410410
name = "monolith-layout"411411
version = "0.0.0"412
+dependencies = [413
+ "monolith-planner",414
+ "proptest",415
+ "slotmap",416
+]412417
413418
[[package]]414419
name = "monolith-motion"@@ -743,6 +748,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
743748
checksum = "33f4fe9184a62d842c9ef383018f3306d8ba224fd9d836f56d7288308847c256"744749
745750
[[package]]751
+name = "slotmap"752
+version = "1.1.1"753
+source = "registry+https://github.com/rust-lang/crates.io-index"754
+checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038"755
+dependencies = [756
+ "version_check",757
+]758
+759
+[[package]]746760
name = "smallvec"747761
version = "1.16.3"748762
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -996,6 +1010,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
9961010
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"9971011
9981012
[[package]]1013
+name = "version_check"1014
+version = "0.9.5"1015
+source = "registry+https://github.com/rust-lang/crates.io-index"1016
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"1017
+1018
+[[package]]9991019
name = "wait-timeout"10001020
version = "0.2.1"10011021
source = "registry+https://github.com/rust-lang/crates.io-index"@@ -8,3 +8,9 @@ publish.workspace = true
88
[lints]99
workspace = true1010
11
+[dependencies]12
+monolith-planner.workspace = true13
+slotmap = "1"14
+15
+[dev-dependencies]16
+proptest.workspace = true@@ -0,0 +1,7 @@
1
+# Seeds for failure cases proptest has generated in the past. It is2
+# automatically read and these particular cases re-run before any3
+# novel cases are generated.4
+#5
+# It is recommended to check this file in to source control so that6
+# everyone who runs the test benefits from these saved cases.7
+cc feaaec708a35beac0ab9749dd260c423ed4177da099bd85a2041f38172334f65 # shrinks to ops = [Add(0, false), Add(0, false), Add(0, false), View(1), Scratch(0), Add(0, false), Add(0, false), Add(0, false), Summon, Summon, Flip, Add(1, false), Move(Left), Add(0, false), Add(0, false), Focus(114), Flip, Move(Left), Focus(0), Add(0, false), View(0), View(0), View(0), Add(0, false), Expand(0)]@@ -1 +1,908 @@
1
+//! Window organisation without smithay. Tags with per-output views, one2
+//! tiling tree per output filtered by those views, floating windows,3
+//! scratchpads, focus, and restoring windows when an output comes back.14
5
+mod tree;6
+7
+#[cfg(test)]8
+mod tests;9
+10
+pub use monolith_planner::Rect;11
+pub use tree::{Axis, Dir, Tree};12
+13
+use slotmap::{SlotMap, new_key_type};14
+use std::collections::HashMap;15
+16
+new_key_type! {17
+ pub struct WindowId;18
+ pub struct TagId;19
+ pub struct OutputId;20
+}21
+22
+pub const HOMELESS: &str = "homeless";23
+24
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]25
+pub enum ExpandScope {26
+ Window,27
+ Block,28
+ All,29
+}30
+31
+#[derive(Clone, Copy, Debug, PartialEq)]32
+pub struct Gaps {33
+ pub inner: f64,34
+ pub outer: f64,35
+}36
+37
+impl Default for Gaps {38
+ fn default() -> Self {39
+ Self {40
+ inner: 8.0,41
+ outer: 8.0,42
+ }43
+ }44
+}45
+46
+#[derive(Clone, Debug)]47
+pub struct Window {48
+ pub output: Option<OutputId>,49
+ pub tag: TagId,50
+ pub floating: bool,51
+ pub float_rect: Option<Rect>,52
+ pub fullscreen: bool,53
+ pub sticky: bool,54
+ pub scratchpad: Option<usize>,55
+ evicted_from: Option<String>,56
+}57
+58
+#[derive(Clone, Debug)]59
+pub struct Tag {60
+ pub name: String,61
+ pub urgent: bool,62
+}63
+64
+#[derive(Clone, Debug)]65
+pub struct Output {66
+ pub name: String,67
+ pub area: Rect,68
+ pub tree: Tree<WindowId>,69
+ /// Viewed tags besides the homeless tag, which is always viewed.70
+ pub viewed: Vec<TagId>,71
+ /// Floating windows, bottom to top.72
+ pub floating: Vec<WindowId>,73
+}74
+75
+#[derive(Clone, Debug)]76
+pub struct Scratchpad {77
+ pub name: String,78
+ pub windows: Vec<WindowId>,79
+ pub shown: Option<usize>,80
+ pub output: Option<OutputId>,81
+}82
+83
+#[derive(Clone, Debug)]84
+struct Parked {85
+ tree: Tree<WindowId>,86
+ viewed: Vec<TagId>,87
+ floating: Vec<WindowId>,88
+ area: Rect,89
+}90
+91
+#[derive(Clone, Debug, Default)]92
+pub struct Arrangement {93
+ /// Every tiled window on the output. Hidden ones are zero-size seeds.94
+ pub tiled: Vec<Placed>,95
+ /// Visible floating windows and shown scratchpads, bottom to top.96
+ pub floating: Vec<(WindowId, Rect)>,97
+ pub fullscreen: Option<WindowId>,98
+}99
+100
+#[derive(Clone, Copy, Debug)]101
+pub struct Placed {102
+ pub window: WindowId,103
+ pub rect: Rect,104
+ pub visible: bool,105
+}106
+107
+#[derive(Clone, Debug)]108
+pub struct Layout {109
+ windows: SlotMap<WindowId, Window>,110
+ tags: SlotMap<TagId, Tag>,111
+ tag_order: Vec<TagId>,112
+ homeless: TagId,113
+ outputs: SlotMap<OutputId, Output>,114
+ active: Option<OutputId>,115
+ /// Focus history, most recent last.116
+ history: Vec<WindowId>,117
+ scratchpads: Vec<Scratchpad>,118
+ last_scratchpad: Option<usize>,119
+ parked: HashMap<String, Parked>,120
+ pub gaps: Gaps,121
+}122
+123
+impl Default for Layout {124
+ fn default() -> Self {125
+ let mut tags = SlotMap::with_key();126
+ let homeless = tags.insert(Tag {127
+ name: HOMELESS.into(),128
+ urgent: false,129
+ });130
+ Self {131
+ windows: SlotMap::with_key(),132
+ tags,133
+ tag_order: vec![homeless],134
+ homeless,135
+ outputs: SlotMap::with_key(),136
+ active: None,137
+ history: Vec::new(),138
+ scratchpads: Vec::new(),139
+ last_scratchpad: None,140
+ parked: HashMap::new(),141
+ gaps: Gaps::default(),142
+ }143
+ }144
+}145
+146
+impl Layout {147
+ pub fn homeless(&self) -> TagId {148
+ self.homeless149
+ }150
+151
+ /// Looks a tag up by name, creating it at the end of the order.152
+ pub fn tag(&mut self, name: &str) -> TagId {153
+ if let Some(id) = self.tag_id(name) {154
+ return id;155
+ }156
+ let id = self.tags.insert(Tag {157
+ name: name.into(),158
+ urgent: false,159
+ });160
+ self.tag_order.push(id);161
+ id162
+ }163
+164
+ pub fn tag_id(&self, name: &str) -> Option<TagId> {165
+ self.tag_order166
+ .iter()167
+ .copied()168
+ .find(|&t| self.tags[t].name == name)169
+ }170
+171
+ pub fn tags(&self) -> impl Iterator<Item = (TagId, &Tag)> {172
+ self.tag_order.iter().map(|&t| (t, &self.tags[t]))173
+ }174
+175
+ pub fn reorder_tag(&mut self, tag: TagId, index: usize) {176
+ if let Some(i) = self.tag_order.iter().position(|&t| t == tag) {177
+ self.tag_order.remove(i);178
+ let index = index.min(self.tag_order.len());179
+ self.tag_order.insert(index, tag);180
+ }181
+ }182
+183
+ pub fn window(&self, w: WindowId) -> Option<&Window> {184
+ self.windows.get(w)185
+ }186
+187
+ pub fn windows(&self) -> impl Iterator<Item = (WindowId, &Window)> {188
+ self.windows.iter()189
+ }190
+191
+ pub fn output(&self, o: OutputId) -> Option<&Output> {192
+ self.outputs.get(o)193
+ }194
+195
+ pub fn outputs(&self) -> impl Iterator<Item = (OutputId, &Output)> {196
+ self.outputs.iter()197
+ }198
+199
+ pub fn scratchpads(&self) -> &[Scratchpad] {200
+ &self.scratchpads201
+ }202
+203
+ pub fn active_output(&self) -> Option<OutputId> {204
+ self.active205
+ }206
+207
+ pub fn set_active_output(&mut self, o: OutputId) {208
+ if self.outputs.contains_key(o) {209
+ self.active = Some(o);210
+ }211
+ }212
+213
+ /// Adds an output. One that was unplugged before gets back its tree,214
+ /// views and the windows that had to leave it.215
+ pub fn add_output(&mut self, name: &str, area: Rect) -> OutputId {216
+ let id = self.outputs.insert(Output {217
+ name: name.into(),218
+ area,219
+ tree: Tree::default(),220
+ viewed: Vec::new(),221
+ floating: Vec::new(),222
+ });223
+ if let Some(parked) = self.parked.remove(name) {224
+ self.restore(id, parked);225
+ }226
+ self.active.get_or_insert(id);227
+ let detached: Vec<WindowId> = self228
+ .windows229
+ .iter()230
+ .filter(|(_, w)| w.output.is_none())231
+ .map(|(id, _)| id)232
+ .collect();233
+ for w in detached {234
+ self.windows[w].output = Some(id);235
+ self.place(w);236
+ }237
+ id238
+ }239
+240
+ fn restore(&mut self, id: OutputId, parked: Parked) {241
+ let name = self.outputs[id].name.clone();242
+ let ours = |l: &Self, w: WindowId| {243
+ l.windows244
+ .get(w)245
+ .is_some_and(|x| x.evicted_from.as_deref() == Some(&name) && x.scratchpad.is_none())246
+ };247
+ let mut tree = parked.tree;248
+ tree.retain(|w| ours(self, w) && !self.windows[w].floating);249
+ let floating: Vec<WindowId> = parked250
+ .floating251
+ .into_iter()252
+ .filter(|&w| ours(self, w) && self.windows[w].floating)253
+ .collect();254
+ for w in tree255
+ .windows()256
+ .chain(floating.iter().copied())257
+ .collect::<Vec<_>>()258
+ {259
+ self.unplace(w);260
+ let win = &mut self.windows[w];261
+ if let (Some(from), Some(rect)) = (win.output, win.float_rect) {262
+ win.float_rect = Some(translate(rect, self.outputs[from].area, parked.area));263
+ }264
+ win.output = Some(id);265
+ win.evicted_from = None;266
+ }267
+ let out = &mut self.outputs[id];268
+ out.tree = tree;269
+ out.floating = floating;270
+ out.viewed = parked271
+ .viewed272
+ .into_iter()273
+ .filter(|&t| self.tags.contains_key(t))274
+ .collect();275
+ }276
+277
+ /// Removes an output. Its windows move to an output viewing their tag,278
+ /// or the active one, and remember where they came from.279
+ pub fn remove_output(&mut self, o: OutputId) {280
+ let Some(out) = self.outputs.remove(o) else {281
+ return;282
+ };283
+ self.parked.insert(284
+ out.name.clone(),285
+ Parked {286
+ tree: out.tree,287
+ viewed: out.viewed,288
+ floating: out.floating,289
+ area: out.area,290
+ },291
+ );292
+ if self.active == Some(o) {293
+ self.active = self.outputs.keys().next();294
+ }295
+ for sp in &mut self.scratchpads {296
+ if sp.output == Some(o) {297
+ sp.output = self.active;298
+ }299
+ }300
+ let leaving: Vec<WindowId> = self301
+ .windows302
+ .iter()303
+ .filter(|(_, w)| w.output == Some(o))304
+ .map(|(id, _)| id)305
+ .collect();306
+ for w in leaving {307
+ let tag = self.windows[w].tag;308
+ let target = self309
+ .outputs310
+ .keys()311
+ .find(|&t| self.is_viewed(t, tag))312
+ .or(self.active);313
+ let win = &mut self.windows[w];314
+ if let (Some(t), Some(rect)) = (target, win.float_rect) {315
+ win.float_rect = Some(translate(rect, out.area, self.outputs[t].area));316
+ }317
+ win.output = target;318
+ if win.scratchpad.is_some() {319
+ continue;320
+ }321
+ win.evicted_from = Some(out.name.clone());322
+ if target.is_some() {323
+ self.place(w);324
+ }325
+ }326
+ }327
+328
+ pub fn set_output_area(&mut self, o: OutputId, area: Rect) {329
+ if let Some(out) = self.outputs.get_mut(o) {330
+ out.area = area;331
+ }332
+ }333
+334
+ pub fn is_viewed(&self, o: OutputId, tag: TagId) -> bool {335
+ tag == self.homeless336
+ || self337
+ .outputs338
+ .get(o)339
+ .is_some_and(|out| out.viewed.contains(&tag))340
+ }341
+342
+ /// Views `tag` alone, besides the homeless tag.343
+ pub fn view(&mut self, o: OutputId, tag: TagId) {344
+ let Some(out) = self.outputs.get_mut(o) else {345
+ return;346
+ };347
+ out.viewed.clear();348
+ if tag != self.homeless {349
+ out.viewed.push(tag);350
+ }351
+ self.tags[tag].urgent = false;352
+ }353
+354
+ pub fn toggle_view(&mut self, o: OutputId, tag: TagId) {355
+ if tag == self.homeless {356
+ return;357
+ }358
+ let Some(out) = self.outputs.get_mut(o) else {359
+ return;360
+ };361
+ if let Some(i) = out.viewed.iter().position(|&t| t == tag) {362
+ out.viewed.remove(i);363
+ } else {364
+ out.viewed.push(tag);365
+ self.tags[tag].urgent = false;366
+ }367
+ }368
+369
+ pub fn view_everywhere(&mut self, tag: TagId) {370
+ for o in self.outputs.keys().collect::<Vec<_>>() {371
+ self.view(o, tag);372
+ }373
+ }374
+375
+ pub fn visible(&self, w: WindowId) -> bool {376
+ let Some(win) = self.windows.get(w) else {377
+ return false;378
+ };379
+ let Some(o) = win.output else {380
+ return false;381
+ };382
+ match win.scratchpad {383
+ Some(s) => {384
+ let sp = &self.scratchpads[s];385
+ sp.output == Some(o) && sp.shown.map(|i| sp.windows[i]) == Some(w)386
+ }387
+ None => win.sticky || self.is_viewed(o, win.tag),388
+ }389
+ }390
+391
+ /// Adds a window on `output` (default the active one) and `tag` (default392
+ /// homeless). A window landing on a tag its output is not viewing marks393
+ /// the tag urgent.394
+ pub fn add_window(395
+ &mut self,396
+ output: Option<OutputId>,397
+ tag: Option<TagId>,398
+ floating: Option<Rect>,399
+ ) -> WindowId {400
+ let output = output401
+ .filter(|&o| self.outputs.contains_key(o))402
+ .or(self.active);403
+ let tag = tag404
+ .filter(|&t| self.tags.contains_key(t))405
+ .unwrap_or(self.homeless);406
+ let w = self.windows.insert(Window {407
+ output,408
+ tag,409
+ floating: floating.is_some(),410
+ float_rect: floating,411
+ fullscreen: false,412
+ sticky: false,413
+ scratchpad: None,414
+ evicted_from: None,415
+ });416
+ if let Some(o) = output {417
+ if !self.is_viewed(o, tag) {418
+ self.tags[tag].urgent = true;419
+ }420
+ self.place(w);421
+ }422
+ w423
+ }424
+425
+ pub fn remove_window(&mut self, w: WindowId) {426
+ if !self.windows.contains_key(w) {427
+ return;428
+ }429
+ self.unplace(w);430
+ self.history.retain(|&h| h != w);431
+ if let Some(s) = self.windows[w].scratchpad {432
+ self.leave_scratchpad(s, w);433
+ }434
+ self.windows.remove(w);435
+ }436
+437
+ /// Puts a window into its output's tree or floating stack. Tiled windows438
+ /// go next to the focused window if both are visible, else next to the439
+ /// latest window of their tag, else into a new row.440
+ fn place(&mut self, w: WindowId) {441
+ let win = &self.windows[w];442
+ let Some(o) = win.output else { return };443
+ if win.scratchpad.is_some() {444
+ return;445
+ }446
+ if win.floating {447
+ if win.float_rect.is_none() {448
+ let rect = centred(self.outputs[o].area, 0.6);449
+ self.windows[w].float_rect = Some(rect);450
+ }451
+ self.outputs[o].floating.push(w);452
+ return;453
+ }454
+ let tag = win.tag;455
+ let tree = &self.outputs[o].tree;456
+ let focused = self457
+ .focused()458
+ .filter(|&f| tree.contains(f) && self.is_viewed(o, tag));459
+ let anchor = focused.or_else(|| {460
+ self.history461
+ .iter()462
+ .rev()463
+ .copied()464
+ .find(|&h| tree.contains(h) && self.windows[h].tag == tag)465
+ });466
+ let tree = &mut self.outputs[o].tree;467
+ match anchor {468
+ Some(a) => tree.insert_after(w, a),469
+ None => tree.insert_row(w),470
+ }471
+ }472
+473
+ fn unplace(&mut self, w: WindowId) {474
+ let Some(o) = self.windows[w].output else {475
+ return;476
+ };477
+ let out = &mut self.outputs[o];478
+ out.tree.remove(w);479
+ out.floating.retain(|&f| f != w);480
+ }481
+482
+ pub fn focus(&mut self, w: WindowId) {483
+ let Some(win) = self.windows.get(w) else {484
+ return;485
+ };486
+ self.active = win.output.or(self.active);487
+ if let Some(o) = win.output {488
+ let stack = &mut self.outputs[o].floating;489
+ if let Some(i) = stack.iter().position(|&f| f == w) {490
+ stack.remove(i);491
+ stack.push(w);492
+ }493
+ }494
+ self.history.retain(|&h| h != w);495
+ self.history.push(w);496
+ }497
+498
+ /// The most recently focused visible window.499
+ pub fn focused(&self) -> Option<WindowId> {500
+ self.history501
+ .iter()502
+ .rev()503
+ .copied()504
+ .find(|&w| self.visible(w))505
+ }506
+507
+ /// The most recently focused visible window on `o`.508
+ pub fn focused_on(&self, o: OutputId) -> Option<WindowId> {509
+ self.history510
+ .iter()511
+ .rev()512
+ .copied()513
+ .find(|&w| self.windows[w].output == Some(o) && self.visible(w))514
+ }515
+516
+ /// Focuses the nearest visible window in `dir`, crossing to the next517
+ /// output when there is none on this one.518
+ pub fn focus_dir(&mut self, dir: Dir) -> Option<WindowId> {519
+ let o = self.active?;520
+ let from = self.focused_on(o).and_then(|f| self.rect_of(f));521
+ let target = from.and_then(|from| {522
+ let arr = self.arrange(o);523
+ let candidates = arr524
+ .tiled525
+ .iter()526
+ .filter(|p| p.visible)527
+ .map(|p| (p.window, p.rect))528
+ .chain(arr.floating.iter().copied())529
+ .filter(|&(w, _)| Some(w) != self.focused_on(o));530
+ nearest(from, dir, candidates)531
+ });532
+ if let Some(w) = target {533
+ self.focus(w);534
+ return Some(w);535
+ }536
+ let here = self.outputs[o].area;537
+ let next = nearest(538
+ here,539
+ dir,540
+ self.outputs.iter().map(|(id, out)| (id, out.area)),541
+ )?;542
+ self.active = Some(next);543
+ let w = self.focused_on(next);544
+ if let Some(w) = w {545
+ self.focus(w);546
+ }547
+ w548
+ }549
+550
+ fn rect_of(&self, w: WindowId) -> Option<Rect> {551
+ let o = self.windows.get(w)?.output?;552
+ let arr = self.arrange(o);553
+ arr.tiled554
+ .iter()555
+ .find(|p| p.window == w)556
+ .map(|p| p.rect)557
+ .or_else(|| arr.floating.iter().find(|f| f.0 == w).map(|f| f.1))558
+ }559
+560
+ fn vis_on(&self, o: OutputId) -> impl Fn(WindowId) -> bool + '_ {561
+ move |w| self.windows[w].output == Some(o) && self.visible(w)562
+ }563
+564
+ fn focused_tiled(&self) -> Option<(WindowId, OutputId)> {565
+ let w = self.focused()?;566
+ let o = self.windows[w].output?;567
+ self.outputs[o].tree.contains(w).then_some((w, o))568
+ }569
+570
+ pub fn move_window(&mut self, dir: Dir) -> bool {571
+ let Some((w, o)) = self.focused_tiled() else {572
+ return false;573
+ };574
+ let mut tree = std::mem::take(&mut self.outputs[o].tree);575
+ let moved = tree.move_window(w, dir, self.vis_on(o));576
+ self.outputs[o].tree = tree;577
+ moved578
+ }579
+580
+ pub fn move_block(&mut self, dir: Dir) -> bool {581
+ let Some((w, o)) = self.focused_tiled() else {582
+ return false;583
+ };584
+ let mut tree = std::mem::take(&mut self.outputs[o].tree);585
+ let moved = tree.move_block(w, dir, self.vis_on(o));586
+ self.outputs[o].tree = tree;587
+ moved588
+ }589
+590
+ pub fn resize(&mut self, axis: Axis, delta: f64) -> bool {591
+ let Some((w, o)) = self.focused_tiled() else {592
+ return false;593
+ };594
+ let mut tree = std::mem::take(&mut self.outputs[o].tree);595
+ let done = tree.resize(w, axis, delta, self.vis_on(o));596
+ self.outputs[o].tree = tree;597
+ done598
+ }599
+600
+ pub fn expand(&mut self, scope: ExpandScope) -> bool {601
+ let Some((w, o)) = self.focused_tiled() else {602
+ return false;603
+ };604
+ let (skip, count) = match scope {605
+ ExpandScope::Window => (0, 1),606
+ ExpandScope::Block => (1, 1),607
+ ExpandScope::All => (0, usize::MAX),608
+ };609
+ self.outputs[o].tree.toggle_expand(w, skip, count)610
+ }611
+612
+ pub fn flip(&mut self, o: OutputId) {613
+ if let Some(out) = self.outputs.get_mut(o) {614
+ out.tree.flip();615
+ }616
+ }617
+618
+ /// Retags a window. It keeps its slot and disappears if the new tag is619
+ /// not viewed.620
+ pub fn set_tag(&mut self, w: WindowId, tag: TagId) {621
+ if self.tags.contains_key(tag)622
+ && let Some(win) = self.windows.get_mut(w)623
+ {624
+ win.tag = tag;625
+ }626
+ }627
+628
+ pub fn send_to_output(&mut self, w: WindowId, o: OutputId) {629
+ let Some(win) = self.windows.get(w) else {630
+ return;631
+ };632
+ if !self.outputs.contains_key(o) || win.output == Some(o) || win.scratchpad.is_some() {633
+ return;634
+ }635
+ let from = win.output.map(|f| self.outputs[f].area);636
+ self.unplace(w);637
+ let win = &mut self.windows[w];638
+ if let (Some(from), Some(rect)) = (from, win.float_rect) {639
+ win.float_rect = Some(translate(rect, from, self.outputs[o].area));640
+ }641
+ win.output = Some(o);642
+ win.evicted_from = None;643
+ self.place(w);644
+ }645
+646
+ pub fn toggle_floating(&mut self, w: WindowId) {647
+ let Some(win) = self.windows.get(w) else {648
+ return;649
+ };650
+ if win.scratchpad.is_some() {651
+ return;652
+ }653
+ self.unplace(w);654
+ let win = &mut self.windows[w];655
+ win.floating = !win.floating;656
+ self.place(w);657
+ }658
+659
+ pub fn set_float_rect(&mut self, w: WindowId, rect: Rect) {660
+ if let Some(win) = self.windows.get_mut(w) {661
+ win.float_rect = Some(rect);662
+ }663
+ }664
+665
+ pub fn toggle_fullscreen(&mut self, w: WindowId) {666
+ if let Some(win) = self.windows.get_mut(w) {667
+ win.fullscreen = !win.fullscreen;668
+ }669
+ }670
+671
+ pub fn toggle_sticky(&mut self, w: WindowId) {672
+ if let Some(win) = self.windows.get_mut(w) {673
+ win.sticky = !win.sticky;674
+ }675
+ }676
+677
+ /// Adds a window to a named scratchpad, creating it. Scratchpad windows678
+ /// float and are shown only by summoning.679
+ pub fn scratchpad_add(&mut self, w: WindowId, name: &str) {680
+ let Some(win) = self.windows.get(w) else {681
+ return;682
+ };683
+ if let Some(s) = win.scratchpad {684
+ self.leave_scratchpad(s, w);685
+ }686
+ self.unplace(w);687
+ let s = match self.scratchpads.iter().position(|sp| sp.name == name) {688
+ Some(s) => s,689
+ None => {690
+ self.scratchpads.push(Scratchpad {691
+ name: name.into(),692
+ windows: Vec::new(),693
+ shown: None,694
+ output: None,695
+ });696
+ self.scratchpads.len() - 1697
+ }698
+ };699
+ self.scratchpads[s].windows.push(w);700
+ let area = self.windows[w].output.map(|o| self.outputs[o].area);701
+ let win = &mut self.windows[w];702
+ win.scratchpad = Some(s);703
+ win.floating = true;704
+ if win.float_rect.is_none() {705
+ win.float_rect = area.map(|a| centred(a, 0.6));706
+ }707
+ }708
+709
+ /// Cycles a scratchpad through off, each window, and off again, on the710
+ /// active output.711
+ pub fn summon(&mut self, name: &str) -> Option<WindowId> {712
+ let s = self.scratchpads.iter().position(|sp| sp.name == name)?;713
+ let o = self.active?;714
+ let sp = &mut self.scratchpads[s];715
+ let next = match sp.shown {716
+ None => 0,717
+ Some(i) => i + 1,718
+ };719
+ if next >= sp.windows.len() {720
+ sp.shown = None;721
+ return None;722
+ }723
+ let from = sp.output.map(|f| self.outputs[f].area);724
+ sp.shown = Some(next);725
+ sp.output = Some(o);726
+ let w = sp.windows[next];727
+ self.last_scratchpad = Some(s);728
+ let area = self.outputs[o].area;729
+ let win = &mut self.windows[w];730
+ if win.output != Some(o) {731
+ win.float_rect = match (from, win.float_rect) {732
+ (Some(from), Some(r)) => Some(translate(r, from, area)),733
+ _ => Some(centred(area, 0.6)),734
+ };735
+ win.output = Some(o);736
+ }737
+ self.focus(w);738
+ Some(w)739
+ }740
+741
+ /// Hides a scratchpad, by default the last one summoned.742
+ pub fn dismiss(&mut self, name: Option<&str>) {743
+ let s = match name {744
+ Some(name) => self.scratchpads.iter().position(|sp| sp.name == name),745
+ None => self.last_scratchpad,746
+ };747
+ if let Some(sp) = s.and_then(|s| self.scratchpads.get_mut(s)) {748
+ sp.shown = None;749
+ }750
+ }751
+752
+ fn leave_scratchpad(&mut self, s: usize, w: WindowId) {753
+ let sp = &mut self.scratchpads[s];754
+ let Some(i) = sp.windows.iter().position(|&x| x == w) else {755
+ return;756
+ };757
+ sp.windows.remove(i);758
+ sp.shown = match sp.shown {759
+ Some(j) if j == i => None,760
+ Some(j) if j > i => Some(j - 1),761
+ other => other,762
+ };763
+ self.windows[w].scratchpad = None;764
+ }765
+766
+ /// Target geometry for everything on `o`, in output-local logical767
+ /// coordinates offset by the output's area.768
+ pub fn arrange(&self, o: OutputId) -> Arrangement {769
+ let Some(out) = self.outputs.get(o) else {770
+ return Arrangement::default();771
+ };772
+ let area = inset(out.area, self.gaps.outer);773
+ let vis = self.vis_on(o);774
+ let tiled = out775
+ .tree776
+ .arrange(area, self.gaps.inner, &vis)777
+ .into_iter()778
+ .map(|(window, rect)| Placed {779
+ window,780
+ rect,781
+ visible: vis(window),782
+ })783
+ .collect();784
+ let mut floating: Vec<(WindowId, Rect)> = out785
+ .floating786
+ .iter()787
+ .copied()788
+ .filter(|&w| vis(w))789
+ .filter_map(|w| Some((w, self.windows[w].float_rect?)))790
+ .collect();791
+ for sp in &self.scratchpads {792
+ if let Some(i) = sp.shown793
+ && sp.output == Some(o)794
+ && let Some(r) = self.windows[sp.windows[i]].float_rect795
+ {796
+ floating.push((sp.windows[i], r));797
+ }798
+ }799
+ let fullscreen = self800
+ .history801
+ .iter()802
+ .rev()803
+ .copied()804
+ .find(|&w| self.windows[w].fullscreen && vis(w));805
+ Arrangement {806
+ tiled,807
+ floating,808
+ fullscreen,809
+ }810
+ }811
+812
+ #[cfg(test)]813
+ pub(crate) fn check(&self) {814
+ for (o, out) in &self.outputs {815
+ out.tree.check();816
+ for w in out.tree.windows() {817
+ let win = &self.windows[w];818
+ assert_eq!(win.output, Some(o));819
+ assert!(!win.floating && win.scratchpad.is_none());820
+ }821
+ for &w in &out.floating {822
+ assert_eq!(self.windows[w].output, Some(o));823
+ assert!(self.windows[w].floating);824
+ }825
+ }826
+ for (w, win) in &self.windows {827
+ let homes = self828
+ .outputs829
+ .values()830
+ .filter(|out| out.tree.contains(w) || out.floating.contains(&w))831
+ .count();832
+ let expected = usize::from(win.output.is_some() && win.scratchpad.is_none());833
+ assert_eq!(homes, expected, "window placed {homes} times");834
+ }835
+ }836
+}837
+838
+fn nearest<K: Copy>(839
+ from: Rect,840
+ dir: Dir,841
+ candidates: impl Iterator<Item = (K, Rect)>,842
+) -> Option<K> {843
+ const TOL: f64 = 0.5;844
+ let score = |r: Rect| -> Option<(bool, f64, f64)> {845
+ let (gap, overlap, offset) = match dir {846
+ Dir::Right => (847
+ r.x - from.right(),848
+ span_overlap(from.y, from.h, r.y, r.h),849
+ r.center_y() - from.center_y(),850
+ ),851
+ Dir::Left => (852
+ from.x - r.right(),853
+ span_overlap(from.y, from.h, r.y, r.h),854
+ r.center_y() - from.center_y(),855
+ ),856
+ Dir::Down => (857
+ r.y - from.bottom(),858
+ span_overlap(from.x, from.w, r.x, r.w),859
+ r.center_x() - from.center_x(),860
+ ),861
+ Dir::Up => (862
+ from.y - r.bottom(),863
+ span_overlap(from.x, from.w, r.x, r.w),864
+ r.center_x() - from.center_x(),865
+ ),866
+ };867
+ (gap > -TOL).then_some((overlap <= 0.0, gap.max(0.0), offset.abs()))868
+ };869
+ candidates870
+ .filter_map(|(k, r)| score(r).map(|s| (k, s)))871
+ .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))872
+ .map(|(k, _)| k)873
+}874
+875
+fn span_overlap(a: f64, al: f64, b: f64, bl: f64) -> f64 {876
+ (a + al).min(b + bl) - a.max(b)877
+}878
+879
+fn inset(r: Rect, by: f64) -> Rect {880
+ Rect::new(881
+ r.x + by,882
+ r.y + by,883
+ (r.w - 2.0 * by).max(0.0),884
+ (r.h - 2.0 * by).max(0.0),885
+ )886
+}887
+888
+fn centred(area: Rect, frac: f64) -> Rect {889
+ let (w, h) = (area.w * frac, area.h * frac);890
+ Rect::new(891
+ area.x + (area.w - w) / 2.0,892
+ area.y + (area.h - h) / 2.0,893
+ w,894
+ h,895
+ )896
+}897
+898
+/// Keeps a rect at the same relative spot when it changes output.899
+fn translate(r: Rect, from: Rect, to: Rect) -> Rect {900
+ let sx = if from.w > 0.0 { to.w / from.w } else { 1.0 };901
+ let sy = if from.h > 0.0 { to.h / from.h } else { 1.0 };902
+ Rect::new(903
+ to.x + (r.x - from.x) * sx,904
+ to.y + (r.y - from.y) * sy,905
+ r.w.min(to.w),906
+ r.h.min(to.h),907
+ )908
+}@@ -0,0 +1,188 @@
1
+use super::*;2
+use proptest::prelude::*;3
+4
+const AREA: Rect = Rect::new(0.0, 0.0, 1000.0, 600.0);5
+6
+#[derive(Clone, Debug)]7
+enum Op {8
+ Add(u8, bool),9
+ Remove(u8),10
+ Focus(u8),11
+ Move(Dir),12
+ Block(Dir),13
+ View(u8),14
+ ToggleView(u8),15
+ Retag(u8, u8),16
+ Float(u8),17
+ Expand(u8),18
+ Flip,19
+ Resize(bool, i8),20
+ Scratch(u8),21
+ Summon,22
+ Unplug,23
+ Replug,24
+}25
+26
+fn dir() -> impl Strategy<Value = Dir> {27
+ prop_oneof![28
+ Just(Dir::Left),29
+ Just(Dir::Right),30
+ Just(Dir::Up),31
+ Just(Dir::Down)32
+ ]33
+}34
+35
+fn op() -> impl Strategy<Value = Op> {36
+ prop_oneof![37
+ 4 => (0u8..4, prop::bool::weighted(0.15)).prop_map(|(t, f)| Op::Add(t, f)),38
+ 1 => any::<u8>().prop_map(Op::Remove),39
+ 3 => any::<u8>().prop_map(Op::Focus),40
+ 4 => dir().prop_map(Op::Move),41
+ 2 => dir().prop_map(Op::Block),42
+ 1 => (0u8..4).prop_map(Op::View),43
+ 2 => (0u8..4).prop_map(Op::ToggleView),44
+ 1 => (any::<u8>(), 0u8..4).prop_map(|(w, t)| Op::Retag(w, t)),45
+ 1 => any::<u8>().prop_map(Op::Float),46
+ 1 => (0u8..3).prop_map(Op::Expand),47
+ 1 => Just(Op::Flip),48
+ 1 => (any::<bool>(), any::<i8>()).prop_map(|(a, d)| Op::Resize(a, d)),49
+ 1 => any::<u8>().prop_map(Op::Scratch),50
+ 1 => Just(Op::Summon),51
+ 1 => Just(Op::Unplug),52
+ 1 => Just(Op::Replug),53
+ ]54
+}55
+56
+fn nth(l: &Layout, i: u8) -> Option<WindowId> {57
+ let ids: Vec<WindowId> = l.windows().map(|(id, _)| id).collect();58
+ (!ids.is_empty()).then(|| ids[i as usize % ids.len()])59
+}60
+61
+fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {62
+ let active = l.active_output();63
+ match *op {64
+ Op::Add(t, f) => {65
+ let w = l.add_window(66
+ None,67
+ Some(tags[t as usize]),68
+ f.then(|| Rect::new(10.0, 10.0, 200.0, 100.0)),69
+ );70
+ if l.visible(w) {71
+ l.focus(w);72
+ }73
+ }74
+ Op::Remove(i) => nth(l, i).into_iter().for_each(|w| l.remove_window(w)),75
+ Op::Focus(i) => {76
+ if let Some(w) = nth(l, i).filter(|&w| l.visible(w)) {77
+ l.focus(w);78
+ }79
+ }80
+ Op::Move(d) => {81
+ l.move_window(d);82
+ }83
+ Op::Block(d) => {84
+ l.move_block(d);85
+ }86
+ Op::View(t) => active.into_iter().for_each(|o| l.view(o, tags[t as usize])),87
+ Op::ToggleView(t) => active88
+ .into_iter()89
+ .for_each(|o| l.toggle_view(o, tags[t as usize])),90
+ Op::Retag(i, t) => nth(l, i)91
+ .into_iter()92
+ .for_each(|w| l.set_tag(w, tags[t as usize])),93
+ Op::Float(i) => nth(l, i).into_iter().for_each(|w| l.toggle_floating(w)),94
+ Op::Expand(s) => {95
+ l.expand([ExpandScope::Window, ExpandScope::Block, ExpandScope::All][s as usize]);96
+ }97
+ Op::Flip => active.into_iter().for_each(|o| l.flip(o)),98
+ Op::Resize(h, d) => {99
+ let axis = if h { Axis::Horizontal } else { Axis::Vertical };100
+ l.resize(axis, f64::from(d) / 200.0);101
+ }102
+ Op::Scratch(i) => nth(l, i).into_iter().for_each(|w| l.scratchpad_add(w, "s")),103
+ Op::Summon => {104
+ l.summon("s");105
+ }106
+ Op::Unplug => {107
+ let b = l.outputs().find(|(_, out)| out.name == "b").map(|(o, _)| o);108
+ if let Some(o) = b {109
+ l.remove_output(o);110
+ }111
+ }112
+ Op::Replug => {113
+ if !l.outputs().any(|(_, out)| out.name == "b") {114
+ l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));115
+ }116
+ }117
+ }118
+}119
+120
+fn check_geometry(l: &Layout) {121
+ for (o, out) in l.outputs() {122
+ let arr = l.arrange(o);123
+ let shown: Vec<Rect> = arr124
+ .tiled125
+ .iter()126
+ .filter(|p| p.visible)127
+ .map(|p| p.rect)128
+ .collect();129
+ for r in &shown {130
+ assert!(r.w >= 0.0 && r.h >= 0.0);131
+ assert!(r.x >= out.area.x - 1e-6 && r.right() <= out.area.right() + 1e-6);132
+ assert!(r.y >= out.area.y - 1e-6 && r.bottom() <= out.area.bottom() + 1e-6);133
+ }134
+ for (i, a) in shown.iter().enumerate() {135
+ for b in &shown[i + 1..] {136
+ let ox = a.right().min(b.right()) - a.x.max(b.x);137
+ let oy = a.bottom().min(b.bottom()) - a.y.max(b.y);138
+ assert!(ox <= 1e-6 || oy <= 1e-6, "{a:?} overlaps {b:?}");139
+ }140
+ }141
+ }142
+}143
+144
+proptest! {145
+ #![proptest_config(ProptestConfig::with_cases(2000))]146
+147
+ #[test]148
+ fn random_commands_keep_the_model_sound(ops in prop::collection::vec(op(), 1..80)) {149
+ let mut l = Layout::default();150
+ l.add_output("a", AREA);151
+ l.add_output("b", Rect::new(1000.0, 0.0, 800.0, 600.0));152
+ let tags = [l.homeless(), l.tag("1"), l.tag("2"), l.tag("3")];153
+ for op in &ops {154
+ apply(&mut l, &tags, op);155
+ l.check();156
+ check_geometry(&l);157
+ }158
+ }159
+}160
+161
+/// The hidden-slot rules from planner.html, demo 11.162
+#[test]163
+fn hidden_windows_keep_their_slots() {164
+ let (a, b, c, d, e) = (1, 2, 3, 4, 5);165
+ let mut t: Tree<u32> = Tree::default();166
+ t.insert_row(a);167
+ t.insert_after(d, a);168
+ t.insert_after(b, d);169
+ t.insert_row(c);170
+ t.insert_after(e, c);171
+ t.check();172
+ let hidden = |w: u32| w != d && w != e;173
+ let all = |_| true;174
+ let rect = |t: &Tree<u32>, w| {175
+ t.arrange(AREA, 0.0, all)176
+ .into_iter()177
+ .find(|p| p.0 == w)178
+ .unwrap()179
+ .1180
+ };181
+182
+ assert!(t.move_window(a, Dir::Right, hidden));183
+ assert!(rect(&t, b).x < rect(&t, d).x && rect(&t, d).x < rect(&t, a).x);184
+185
+ assert!(t.move_window(c, Dir::Up, hidden));186
+ t.check();187
+ assert_eq!(rect(&t, e).w, AREA.w, "E is demoted to a row of its own");188
+}@@ -0,0 +1,644 @@
1
+use monolith_planner::Rect;2
+use slotmap::{SlotMap, new_key_type};3
+use std::collections::HashMap;4
+use std::hash::Hash;5
+6
+new_key_type! { pub struct NodeId; }7
+8
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]9
+pub enum Axis {10
+ Horizontal,11
+ Vertical,12
+}13
+14
+impl Axis {15
+ pub fn other(self) -> Self {16
+ match self {17
+ Self::Horizontal => Self::Vertical,18
+ Self::Vertical => Self::Horizontal,19
+ }20
+ }21
+}22
+23
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]24
+pub enum Dir {25
+ Left,26
+ Right,27
+ Up,28
+ Down,29
+}30
+31
+impl Dir {32
+ pub fn axis(self) -> Axis {33
+ match self {34
+ Self::Left | Self::Right => Axis::Horizontal,35
+ Self::Up | Self::Down => Axis::Vertical,36
+ }37
+ }38
+39
+ pub fn forward(self) -> bool {40
+ matches!(self, Self::Right | Self::Down)41
+ }42
+}43
+44
+const MIN_SHARE: f64 = 0.05;45
+46
+#[derive(Clone, Debug)]47
+struct Child {48
+ node: NodeId,49
+ weight: f64,50
+}51
+52
+#[derive(Clone, Debug)]53
+enum Kind<W> {54
+ Leaf(W),55
+ Container {56
+ axis: Axis,57
+ children: Vec<Child>,58
+ expanded: Option<NodeId>,59
+ },60
+}61
+62
+#[derive(Clone, Debug)]63
+struct Node<W> {64
+ parent: Option<NodeId>,65
+ kind: Kind<W>,66
+}67
+68
+/// One output's tiling tree. Every tiled window of every tag lives here, the69
+/// caller's visibility filter decides which ones take space.70
+///71
+/// Children run on the axis opposite their parent. Containers hold at least72
+/// two children, hidden ones included, and a container left with one child73
+/// is replaced by it.74
+#[derive(Clone, Debug)]75
+pub struct Tree<W> {76
+ nodes: SlotMap<NodeId, Node<W>>,77
+ root: Option<NodeId>,78
+ leaves: HashMap<W, NodeId>,79
+ stack: Axis,80
+}81
+82
+impl<W: Copy + Eq + Hash> Default for Tree<W> {83
+ fn default() -> Self {84
+ Self {85
+ nodes: SlotMap::with_key(),86
+ root: None,87
+ leaves: HashMap::new(),88
+ stack: Axis::Vertical,89
+ }90
+ }91
+}92
+93
+impl<W: Copy + Eq + Hash> Tree<W> {94
+ /// The axis tag rows stack along.95
+ pub fn stack_axis(&self) -> Axis {96
+ self.stack97
+ }98
+99
+ pub fn contains(&self, w: W) -> bool {100
+ self.leaves.contains_key(&w)101
+ }102
+103
+ pub fn is_empty(&self) -> bool {104
+ self.root.is_none()105
+ }106
+107
+ pub fn windows(&self) -> impl Iterator<Item = W> + '_ {108
+ self.leaves.keys().copied()109
+ }110
+111
+ /// Inserts `w` as the next sibling of `anchor`, or as a new row if112
+ /// `anchor` is not in the tree.113
+ pub fn insert_after(&mut self, w: W, anchor: W) {114
+ let Some(&a) = self.leaves.get(&anchor) else {115
+ return self.insert_row(w);116
+ };117
+ let n = self.new_leaf(w);118
+ match self.position(a) {119
+ Some((p, i)) if Some(p) == self.root && self.axis(p) == Some(self.stack) => {120
+ let weight = self.unlink(p, i).weight;121
+ let c = self.new_container(self.stack.other());122
+ self.attach(c, 0, a, 1.0);123
+ self.attach(c, 1, n, 1.0);124
+ self.attach(p, i, c, weight);125
+ }126
+ Some((p, i)) => {127
+ let weight = self.mean_weight(p);128
+ self.attach(p, i + 1, n, weight);129
+ }130
+ None => {131
+ let c = self.new_container(self.stack.other());132
+ self.attach(c, 0, a, 1.0);133
+ self.attach(c, 1, n, 1.0);134
+ self.root = Some(c);135
+ }136
+ }137
+ }138
+139
+ /// Appends `w` as a row of its own along the stacking axis.140
+ pub fn insert_row(&mut self, w: W) {141
+ let n = self.new_leaf(w);142
+ match self.root {143
+ None => self.root = Some(n),144
+ Some(r) if self.axis(r) == Some(self.stack) => {145
+ let weight = self.mean_weight(r);146
+ let end = self.children(r).len();147
+ self.attach(r, end, n, weight);148
+ }149
+ Some(r) => {150
+ let c = self.new_container(self.stack);151
+ self.attach(c, 0, r, 1.0);152
+ self.attach(c, 1, n, 1.0);153
+ self.root = Some(c);154
+ }155
+ }156
+ }157
+158
+ pub fn remove(&mut self, w: W) -> bool {159
+ let Some(n) = self.leaves.remove(&w) else {160
+ return false;161
+ };162
+ self.detach(n);163
+ self.nodes.remove(n);164
+ true165
+ }166
+167
+ pub fn retain(&mut self, mut keep: impl FnMut(W) -> bool) {168
+ let gone: Vec<W> = self.windows().filter(|&w| !keep(w)).collect();169
+ for w in gone {170
+ self.remove(w);171
+ }172
+ }173
+174
+ /// Moves a window one step in `dir`. Leaves swap slots with the next175
+ /// visible sibling, enter a sibling container, or step out beside their176
+ /// container. Hidden windows never move.177
+ pub fn move_window(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {178
+ let Some(&n) = self.leaves.get(&w) else {179
+ return false;180
+ };181
+ self.move_node(n, dir, &vis, true)182
+ }183
+184
+ /// Moves the container holding `w` as a rigid block. A window sitting185
+ /// directly in the root is its own block.186
+ pub fn move_block(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {187
+ let Some(&n) = self.leaves.get(&w) else {188
+ return false;189
+ };190
+ let block = self.nodes[n]191
+ .parent192
+ .filter(|&p| Some(p) != self.root)193
+ .unwrap_or(n);194
+ self.move_node(block, dir, &vis, false)195
+ }196
+197
+ /// Transposes the whole tree.198
+ pub fn flip(&mut self) {199
+ self.stack = self.stack.other();200
+ for node in self.nodes.values_mut() {201
+ if let Kind::Container { axis, .. } = &mut node.kind {202
+ *axis = axis.other();203
+ }204
+ }205
+ }206
+207
+ /// Toggles expansion for `count` levels of `w`'s ancestry, starting208
+ /// `skip` levels up. Level 0 expands the window inside its container.209
+ pub fn toggle_expand(&mut self, w: W, skip: usize, count: usize) -> bool {210
+ let Some(&leaf) = self.leaves.get(&w) else {211
+ return false;212
+ };213
+ let mut path = Vec::new();214
+ let mut cur = leaf;215
+ while let Some(p) = self.nodes[cur].parent {216
+ path.push((p, cur));217
+ cur = p;218
+ }219
+ let path: Vec<_> = path.into_iter().skip(skip).take(count).collect();220
+ if path.is_empty() {221
+ return false;222
+ }223
+ let all = path.iter().all(|&(p, c)| self.expanded(p) == Some(c));224
+ for (p, c) in path {225
+ if let Kind::Container { expanded, .. } = &mut self.nodes[p].kind {226
+ *expanded = (!all).then_some(c);227
+ }228
+ }229
+ true230
+ }231
+232
+ /// Grows `w` by `delta`, a fraction of the visible length of the nearest233
+ /// container along `axis` where it has a visible sibling. Hidden siblings234
+ /// keep their weights, and with them their share.235
+ pub fn resize(&mut self, w: W, axis: Axis, delta: f64, vis: impl Fn(W) -> bool) -> bool {236
+ let Some(&leaf) = self.leaves.get(&w) else {237
+ return false;238
+ };239
+ let mut cur = leaf;240
+ while let Some((p, i)) = self.position(cur) {241
+ if self.axis(p) == Some(axis) {242
+ let shown: Vec<usize> = (0..self.children(p).len())243
+ .filter(|&j| self.visible(self.children(p)[j].node, &vis))244
+ .collect();245
+ if shown.len() >= 2 {246
+ let children = self.children_mut(p);247
+ let sum: f64 = shown.iter().map(|&j| children[j].weight).sum();248
+ let old = children[i].weight;249
+ let max = 1.0 - MIN_SHARE * (shown.len() - 1) as f64;250
+ let new = (old / sum + delta).clamp(MIN_SHARE, max) * sum;251
+ let scale = (sum - new) / (sum - old);252
+ for j in shown {253
+ children[j].weight = if j == i {254
+ new255
+ } else {256
+ children[j].weight * scale257
+ };258
+ }259
+ return true;260
+ }261
+ }262
+ cur = p;263
+ }264
+ false265
+ }266
+267
+ /// Lays the tree out in `area`. Hidden windows come back as zero-size268
+ /// rects on the boundary where they would reappear.269
+ pub fn arrange(&self, area: Rect, gap: f64, vis: impl Fn(W) -> bool) -> Vec<(W, Rect)> {270
+ let mut out = Vec::with_capacity(self.leaves.len());271
+ if let Some(r) = self.root {272
+ self.place(r, area, gap, &vis, &mut out);273
+ }274
+ out275
+ }276
+277
+ fn place(278
+ &self,279
+ n: NodeId,280
+ r: Rect,281
+ gap: f64,282
+ vis: &impl Fn(W) -> bool,283
+ out: &mut Vec<(W, Rect)>,284
+ ) {285
+ let (axis, children, expanded) = match &self.nodes[n].kind {286
+ Kind::Leaf(w) => return out.push((*w, r)),287
+ Kind::Container {288
+ axis,289
+ children,290
+ expanded,291
+ } => (*axis, children, *expanded),292
+ };293
+ let shown: Vec<bool> = children.iter().map(|c| self.visible(c.node, vis)).collect();294
+ let (start, len) = span(r, axis);295
+ if let Some(e) = expanded.and_then(|e| children.iter().position(|c| c.node == e))296
+ && shown[e]297
+ {298
+ for (i, c) in children.iter().enumerate() {299
+ let sub = match i.cmp(&e) {300
+ std::cmp::Ordering::Equal => r,301
+ std::cmp::Ordering::Less => with_span(r, axis, start, 0.0),302
+ std::cmp::Ordering::Greater => with_span(r, axis, start + len, 0.0),303
+ };304
+ self.place(c.node, sub, gap, vis, out);305
+ }306
+ return;307
+ }308
+ let count = shown.iter().filter(|s| **s).count();309
+ let total: f64 = children310
+ .iter()311
+ .zip(&shown)312
+ .filter(|(_, s)| **s)313
+ .map(|(c, _)| c.weight)314
+ .sum();315
+ let gaps = count.saturating_sub(1) as f64;316
+ let gap = if gaps > 0.0 { gap.min(len / gaps) } else { gap };317
+ let avail = len - gap * gaps;318
+ let last = shown.iter().rposition(|s| *s);319
+ let mut pos = start;320
+ let mut seen = false;321
+ for (i, c) in children.iter().enumerate() {322
+ if shown[i] {323
+ if seen {324
+ pos += gap;325
+ }326
+ let l = avail * c.weight / total;327
+ self.place(c.node, with_span(r, axis, pos, l), gap, vis, out);328
+ pos += l;329
+ seen = true;330
+ } else {331
+ let between = seen && last.is_some_and(|l| l > i);332
+ let at = if between { pos + gap / 2.0 } else { pos };333
+ self.place(c.node, with_span(r, axis, at, 0.0), gap, vis, out);334
+ }335
+ }336
+ }337
+338
+ fn move_node(&mut self, n: NodeId, dir: Dir, vis: &impl Fn(W) -> bool, enter: bool) -> bool {339
+ let mut cur = n;340
+ while let Some((p, i)) = self.position(cur) {341
+ if self.axis(p) == Some(dir.axis()) {342
+ if cur != n {343
+ let at = if dir.forward() { i + 1 } else { i };344
+ self.relocate(n, p, at);345
+ return true;346
+ }347
+ if let Some(j) = self.next_shown(p, i, dir, vis) {348
+ let sib = self.children(p)[j].node;349
+ if enter && self.axis(sib).is_some() {350
+ let end = self.children(sib).len();351
+ self.relocate(n, sib, end);352
+ } else {353
+ let children = self.children_mut(p);354
+ children[i].node = sib;355
+ children[j].node = n;356
+ }357
+ return true;358
+ }359
+ }360
+ cur = p;361
+ }362
+ if cur == n || self.axis(cur) == Some(dir.axis()) {363
+ return false;364
+ }365
+ let (old, i) = self.position(n).expect("n is below the root");366
+ self.unlink(old, i);367
+ self.normalize(old);368
+ let r = self.root.expect("the rest of the tree survives");369
+ if self.axis(r) == Some(dir.axis()) {370
+ let weight = self.mean_weight(r);371
+ let at = if dir.forward() {372
+ self.children(r).len()373
+ } else {374
+ 0375
+ };376
+ self.attach(r, at, n, weight);377
+ } else {378
+ let c = self.new_container(dir.axis());379
+ self.attach(c, 0, r, 1.0);380
+ let at = if dir.forward() {381
+ self.children(c).len()382
+ } else {383
+ 0384
+ };385
+ self.attach(c, at, n, 1.0);386
+ self.root = Some(c);387
+ }388
+ true389
+ }390
+391
+ fn next_shown(&self, p: NodeId, i: usize, dir: Dir, vis: &impl Fn(W) -> bool) -> Option<usize> {392
+ let children = self.children(p);393
+ if dir.forward() {394
+ (i + 1..children.len()).find(|&j| self.visible(children[j].node, vis))395
+ } else {396
+ (0..i).rev().find(|&j| self.visible(children[j].node, vis))397
+ }398
+ }399
+400
+ fn relocate(&mut self, n: NodeId, p: NodeId, at: usize) {401
+ let (old, i) = self.position(n).expect("relocated nodes have a parent");402
+ let weight = self.mean_weight(p);403
+ let at = if old == p && i < at { at - 1 } else { at };404
+ self.unlink(old, i);405
+ self.attach(p, at, n, weight);406
+ self.normalize(old);407
+ }408
+409
+ /// Places `node` among `p`'s children. A container running along `p`'s410
+ /// axis is dissolved into `p` so axes keep alternating.411
+ fn attach(&mut self, p: NodeId, at: usize, node: NodeId, weight: f64) {412
+ if self.axis(node) == self.axis(p) {413
+ let Some(Node {414
+ kind: Kind::Container { children, .. },415
+ ..416
+ }) = self.nodes.remove(node)417
+ else {418
+ unreachable!("only containers have an axis");419
+ };420
+ let total: f64 = children.iter().map(|c| c.weight).sum();421
+ for (k, c) in children.into_iter().enumerate() {422
+ self.nodes[c.node].parent = Some(p);423
+ let weight = weight * c.weight / total;424
+ self.children_mut(p).insert(425
+ at + k,426
+ Child {427
+ node: c.node,428
+ weight,429
+ },430
+ );431
+ }432
+ } else {433
+ self.nodes[node].parent = Some(p);434
+ self.children_mut(p).insert(at, Child { node, weight });435
+ }436
+ }437
+438
+ fn unlink(&mut self, p: NodeId, i: usize) -> Child {439
+ let Kind::Container {440
+ children, expanded, ..441
+ } = &mut self.nodes[p].kind442
+ else {443
+ unreachable!("parents are containers");444
+ };445
+ let child = children.remove(i);446
+ if *expanded == Some(child.node) {447
+ *expanded = None;448
+ }449
+ self.nodes[child.node].parent = None;450
+ child451
+ }452
+453
+ fn detach(&mut self, n: NodeId) {454
+ match self.position(n) {455
+ Some((p, i)) => {456
+ self.unlink(p, i);457
+ self.normalize(p);458
+ }459
+ None => self.root = None,460
+ }461
+ }462
+463
+ /// Repairs a container that lost a child.464
+ fn normalize(&mut self, c: NodeId) {465
+ if self.children(c).len() >= 2 {466
+ return;467
+ }468
+ let pos = self.position(c);469
+ let was_expanded = pos.is_some_and(|(p, _)| self.expanded(p) == Some(c));470
+ let Some(Node {471
+ kind: Kind::Container { children, .. },472
+ ..473
+ }) = self.nodes.remove(c)474
+ else {475
+ unreachable!("only containers are normalized");476
+ };477
+ let only = children.into_iter().next();478
+ match (only, pos) {479
+ (None, Some((p, i))) => {480
+ self.unlink_dead(p, i);481
+ self.normalize(p);482
+ }483
+ (None, None) => self.root = None,484
+ (Some(only), Some((p, i))) => {485
+ let weight = self.unlink_dead(p, i).weight;486
+ let leaf_like = self.axis(only.node) != self.axis(p);487
+ self.attach(p, i, only.node, weight);488
+ if was_expanded489
+ && leaf_like490
+ && let Kind::Container { expanded, .. } = &mut self.nodes[p].kind491
+ {492
+ *expanded = Some(only.node);493
+ }494
+ }495
+ (Some(only), None) => {496
+ self.nodes[only.node].parent = None;497
+ self.root = Some(only.node);498
+ }499
+ }500
+ }501
+502
+ /// Like `unlink`, for a child whose node is already gone.503
+ fn unlink_dead(&mut self, p: NodeId, i: usize) -> Child {504
+ let Kind::Container {505
+ children, expanded, ..506
+ } = &mut self.nodes[p].kind507
+ else {508
+ unreachable!("parents are containers");509
+ };510
+ let child = children.remove(i);511
+ if *expanded == Some(child.node) {512
+ *expanded = None;513
+ }514
+ child515
+ }516
+517
+ fn new_leaf(&mut self, w: W) -> NodeId {518
+ let n = self.nodes.insert(Node {519
+ parent: None,520
+ kind: Kind::Leaf(w),521
+ });522
+ self.leaves.insert(w, n);523
+ n524
+ }525
+526
+ fn new_container(&mut self, axis: Axis) -> NodeId {527
+ self.nodes.insert(Node {528
+ parent: None,529
+ kind: Kind::Container {530
+ axis,531
+ children: Vec::new(),532
+ expanded: None,533
+ },534
+ })535
+ }536
+537
+ fn position(&self, n: NodeId) -> Option<(NodeId, usize)> {538
+ let p = self.nodes[n].parent?;539
+ let i = self.children(p).iter().position(|c| c.node == n)?;540
+ Some((p, i))541
+ }542
+543
+ fn axis(&self, n: NodeId) -> Option<Axis> {544
+ match &self.nodes[n].kind {545
+ Kind::Container { axis, .. } => Some(*axis),546
+ Kind::Leaf(_) => None,547
+ }548
+ }549
+550
+ fn expanded(&self, n: NodeId) -> Option<NodeId> {551
+ match &self.nodes[n].kind {552
+ Kind::Container { expanded, .. } => *expanded,553
+ Kind::Leaf(_) => None,554
+ }555
+ }556
+557
+ fn children(&self, n: NodeId) -> &[Child] {558
+ match &self.nodes[n].kind {559
+ Kind::Container { children, .. } => children,560
+ Kind::Leaf(_) => &[],561
+ }562
+ }563
+564
+ fn children_mut(&mut self, n: NodeId) -> &mut Vec<Child> {565
+ match &mut self.nodes[n].kind {566
+ Kind::Container { children, .. } => children,567
+ Kind::Leaf(_) => unreachable!("leaves have no children"),568
+ }569
+ }570
+571
+ fn mean_weight(&self, p: NodeId) -> f64 {572
+ let children = self.children(p);573
+ if children.is_empty() {574
+ 1.0575
+ } else {576
+ children.iter().map(|c| c.weight).sum::<f64>() / children.len() as f64577
+ }578
+ }579
+580
+ fn visible(&self, n: NodeId, vis: &impl Fn(W) -> bool) -> bool {581
+ match &self.nodes[n].kind {582
+ Kind::Leaf(w) => vis(*w),583
+ Kind::Container { children, .. } => children.iter().any(|c| self.visible(c.node, vis)),584
+ }585
+ }586
+587
+ #[cfg(test)]588
+ pub(crate) fn check(&self) {589
+ let reachable = match self.root {590
+ Some(r) => {591
+ assert!(self.nodes[r].parent.is_none());592
+ self.check_node(r)593
+ }594
+ None => 0,595
+ };596
+ assert_eq!(reachable, self.nodes.len(), "orphaned nodes");597
+ for (&w, &n) in &self.leaves {598
+ assert!(matches!(self.nodes[n].kind, Kind::Leaf(x) if x == w));599
+ }600
+ }601
+602
+ #[cfg(test)]603
+ fn check_node(&self, n: NodeId) -> usize {604
+ match &self.nodes[n].kind {605
+ Kind::Leaf(_) => 1,606
+ Kind::Container {607
+ axis,608
+ children,609
+ expanded,610
+ } => {611
+ assert!(612
+ children.len() >= 2,613
+ "container with {} children",614
+ children.len()615
+ );616
+ if let Some(e) = expanded {617
+ assert!(children.iter().any(|c| c.node == *e));618
+ }619
+ let mut count = 1;620
+ for c in children {621
+ assert!(c.weight > 0.0 && c.weight.is_finite());622
+ assert_eq!(self.nodes[c.node].parent, Some(n));623
+ assert_ne!(self.axis(c.node), Some(*axis), "axes must alternate");624
+ count += self.check_node(c.node);625
+ }626
+ count627
+ }628
+ }629
+ }630
+}631
+632
+fn span(r: Rect, axis: Axis) -> (f64, f64) {633
+ match axis {634
+ Axis::Horizontal => (r.x, r.w),635
+ Axis::Vertical => (r.y, r.h),636
+ }637
+}638
+639
+fn with_span(r: Rect, axis: Axis, start: f64, len: f64) -> Rect {640
+ match axis {641
+ Axis::Horizontal => Rect::new(start, r.y, len, r.h),642
+ Axis::Vertical => Rect::new(r.x, start, r.w, len),643
+ }644
+}