Commit 4444c779eb48
tarball10 files changed+139-32
@@ -9,6 +9,9 @@ startup = []
99
# keeps super for itself.1010
mod = "super"1111
gaps = { inner = 8, outer = 8 }12
+# What a window move does when it meets a neighbour. swap trades places,13
+# merge joins its lane. invert-move does the other one for the next move.14
+move = "swap"1215
1316
[animation]1417
enabled = true@@ -37,18 +40,19 @@ repeat_rate = 25
3740
"Mod+Shift+j" = "move down"3841
"Mod+Shift+k" = "move up"3942
"Mod+Shift+l" = "move right"40
-"Mod+Ctrl+h" = "move-block left"41
-"Mod+Ctrl+j" = "move-block down"42
-"Mod+Ctrl+k" = "move-block up"43
-"Mod+Ctrl+l" = "move-block right"43
+"Mod+Ctrl+h" = "move-container left"44
+"Mod+Ctrl+j" = "move-container down"45
+"Mod+Ctrl+k" = "move-container up"46
+"Mod+Ctrl+l" = "move-container right"47
+"Mod+m" = "invert-move"4448
"Mod+Alt+h" = "resize horizontal -0.05"4549
"Mod+Alt+l" = "resize horizontal 0.05"4650
"Mod+Alt+k" = "resize vertical -0.05"4751
"Mod+Alt+j" = "resize vertical 0.05"4852
4953
"Mod+e" = "expand window"50
-"Mod+Shift+w" = "expand block"51
-"Mod+w" = "expand all"54
+"Mod+Shift+w" = "expand container"55
+"Mod+w" = "maximize"5256
"Mod+o" = "flip"5357
"Mod+f" = "fullscreen"5458
"Mod+Shift+space" = "float"@@ -26,6 +26,19 @@ pub struct General {
2626
#[serde(rename = "mod")]2727
pub mod_key: Modifier,2828
pub gaps: Gaps,29
+ /// What a window move does when it meets a neighbour.30
+ #[serde(rename = "move")]31
+ pub move_mode: MoveMode,32
+}33
+34
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]35
+#[serde(rename_all = "lowercase")]36
+pub enum MoveMode {37
+ /// Trade places with it.38
+ #[default]39
+ Swap,40
+ /// Join its lane, stacking with it if it is alone.41
+ Merge,2942
}3043
3144
impl Default for General {@@ -33,6 +46,7 @@ impl Default for General {
3346
Self {3447
mod_key: Modifier::Super,3548
gaps: Gaps::default(),49
+ move_mode: MoveMode::Swap,3650
}3751
}3852
}@@ -46,8 +46,7 @@ pub enum Axis {
4646
pub enum ExpandScope {4747
#[default]4848
Window,49
- Block,50
- All,49
+ Container,5150
}5251
5352
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]@@ -65,7 +64,7 @@ pub enum Command {
6564
Move {6665
dir: Dir,6766
},68
- MoveBlock {67
+ MoveContainer {6968
dir: Dir,7069
},7170
Resize {@@ -75,6 +74,11 @@ pub enum Command {
7574
Expand {7675
scope: ExpandScope,7776
},77
+ /// Expands every container above the focused window towards it.78
+ Maximize,79
+ /// Makes the next window move do the opposite of the configured default,80
+ /// swap or merge. Again before moving cancels it.81
+ InvertMove,7882
Flip,7983
Float,8084
Fullscreen,@@ -126,7 +130,7 @@ impl fmt::Display for ParseError {
126130
impl std::error::Error for ParseError {}127131
128132
impl Command {129
- /// Parses the word form, as in `["move-block", "left"]`.133
+ /// Parses the word form, as in `["move-container", "left"]`.130134
pub fn parse<S: AsRef<str>>(words: &[S]) -> Result<Self, ParseError> {131135
let words: Vec<&str> = words.iter().map(AsRef::as_ref).collect();132136
let Some((&name, args)) = words.split_first() else {@@ -162,12 +166,14 @@ impl Command {
162166
"quit" => none(Command::Quit)?,163167
"reload" => none(Command::Reload)?,164168
"flip" => none(Command::Flip)?,169
+ "maximize" => none(Command::Maximize)?,170
+ "invert-move" => none(Command::InvertMove)?,165171
"float" => none(Command::Float)?,166172
"fullscreen" => none(Command::Fullscreen)?,167173
"sticky" => none(Command::Sticky)?,168174
"focus" => Command::Focus { dir: dir()? },169175
"move" => Command::Move { dir: dir()? },170
- "move-block" => Command::MoveBlock { dir: dir()? },176
+ "move-container" => Command::MoveContainer { dir: dir()? },171177
"focus-output" => Command::FocusOutput { dir: dir()? },172178
"send-to-output" => Command::SendToOutput { dir: dir()? },173179
"resize" => {@@ -187,9 +193,8 @@ impl Command {
187193
"expand" => Command::Expand {188194
scope: match args {189195
[] | ["window"] => ExpandScope::Window,190
- ["block"] => ExpandScope::Block,191
- ["all"] => ExpandScope::All,192
- _ => return Err(err("takes window, block or all")),196
+ ["container"] => ExpandScope::Container,197
+ _ => return Err(err("takes window or container")),193198
},194199
},195200
"view" => Command::View { tag: one()? },@@ -274,6 +279,8 @@ pub enum Answer {
274279
275280
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]276281
pub struct State {282
+ /// The next window move does the opposite of the configured default.283
+ pub move_inverted: bool,277284
pub tags: Vec<Tag>,278285
pub outputs: Vec<Output>,279286
pub windows: Vec<Window>,@@ -24,7 +24,7 @@ pub const HOMELESS: &str = "homeless";
2424
#[derive(Clone, Copy, Debug, PartialEq, Eq)]2525
pub enum ExpandScope {2626
Window,27
- Block,27
+ Container,2828
All,2929
}3030
@@ -594,16 +594,34 @@ impl Layout {
594594
moved595595
}596596
597
- pub fn move_block(&mut self, dir: Dir) -> bool {597
+ pub fn move_container(&mut self, dir: Dir) -> bool {598598
let Some((w, o)) = self.focused_tiled() else {599599
return false;600600
};601601
let mut tree = std::mem::take(&mut self.outputs[o].tree);602
- let moved = tree.move_block(w, dir, self.vis_on(o));602
+ let moved = tree.move_container(w, dir, self.vis_on(o));603603
self.outputs[o].tree = tree;604604
moved605605
}606606
607
+ pub fn merge_window(&mut self, dir: Dir) -> bool {608
+ let Some((w, o)) = self.focused_tiled() else {609
+ return false;610
+ };611
+ let mut tree = std::mem::take(&mut self.outputs[o].tree);612
+ let merged = tree.merge_window(w, dir, self.vis_on(o));613
+ self.outputs[o].tree = tree;614
+ merged615
+ }616
+617
+ /// Whether a tiled window fills its output through expansion.618
+ pub fn maximized(&self, w: WindowId) -> bool {619
+ let Some(o) = self.windows.get(w).and_then(|w| w.output) else {620
+ return false;621
+ };622
+ self.outputs[o].tree.maximized(w)623
+ }624
+607625
pub fn resize(&mut self, axis: Axis, delta: f64) -> bool {608626
let Some((w, o)) = self.focused_tiled() else {609627
return false;@@ -620,7 +638,7 @@ impl Layout {
620638
};621639
let (skip, count) = match scope {622640
ExpandScope::Window => (0, 1),623
- ExpandScope::Block => (1, 1),641
+ ExpandScope::Container => (1, 1),624642
ExpandScope::All => (0, usize::MAX),625643
};626644
self.outputs[o].tree.toggle_expand(w, skip, count)@@ -10,6 +10,7 @@ enum Op {
1010
Focus(u8),1111
Move(Dir),1212
Block(Dir),13
+ Merge(Dir),1314
View(u8),1415
ToggleView(u8),1516
Retag(u8, u8),@@ -39,6 +40,7 @@ fn op() -> impl Strategy<Value = Op> {
3940
3 => any::<u8>().prop_map(Op::Focus),4041
4 => dir().prop_map(Op::Move),4142
2 => dir().prop_map(Op::Block),43
+ 3 => dir().prop_map(Op::Merge),4244
1 => (0u8..4).prop_map(Op::View),4345
2 => (0u8..4).prop_map(Op::ToggleView),4446
1 => (any::<u8>(), 0u8..4).prop_map(|(w, t)| Op::Retag(w, t)),@@ -81,7 +83,10 @@ fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
8183
l.move_window(d);8284
}8385
Op::Block(d) => {84
- l.move_block(d);86
+ l.move_container(d);87
+ }88
+ Op::Merge(d) => {89
+ l.merge_window(d);8590
}8691
Op::View(t) => active.into_iter().for_each(|o| l.view(o, tags[t as usize])),8792
Op::ToggleView(t) => active@@ -92,7 +97,13 @@ fn apply(l: &mut Layout, tags: &[TagId], op: &Op) {
9297
.for_each(|w| l.set_tag(w, tags[t as usize])),9398
Op::Float(i) => nth(l, i).into_iter().for_each(|w| l.toggle_floating(w)),9499
Op::Expand(s) => {95
- l.expand([ExpandScope::Window, ExpandScope::Block, ExpandScope::All][s as usize]);100
+ l.expand(101
+ [102
+ ExpandScope::Window,103
+ ExpandScope::Container,104
+ ExpandScope::All,105
+ ][s as usize],106
+ );96107
}97108
Op::Flip => active.into_iter().for_each(|o| l.flip(o)),98109
Op::Resize(h, d) => {@@ -183,7 +183,7 @@ impl<W: Copy + Eq + Hash> Tree<W> {
183183
184184
/// Moves the container holding `w` as a rigid block. A window sitting185185
/// 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 {186
+ pub fn move_container(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {187187
let Some(&n) = self.leaves.get(&w) else {188188
return false;189189
};@@ -194,6 +194,43 @@ impl<W: Copy + Eq + Hash> Tree<W> {
194194
self.move_node(block, dir, &vis, false)195195
}196196
197
+ /// Moves a window to the end of the next visible lane in `dir`, stepping198
+ /// up a level when its container has none that way. A lone window there199
+ /// becomes a stack with it.200
+ pub fn merge_window(&mut self, w: W, dir: Dir, vis: impl Fn(W) -> bool) -> bool {201
+ let Some(&n) = self.leaves.get(&w) else {202
+ return false;203
+ };204
+ let mut cur = n;205
+ while let Some((p, i)) = self.position(cur) {206
+ if self.axis(p) == Some(dir.axis())207
+ && let Some(j) = self.next_shown(p, i, dir, &vis)208
+ {209
+ self.join_lane(n, p, j);210
+ return true;211
+ }212
+ cur = p;213
+ }214
+ false215
+ }216
+217
+ /// Whether every container above `w` is expanded towards it.218
+ pub fn maximized(&self, w: W) -> bool {219
+ let Some(&leaf) = self.leaves.get(&w) else {220
+ return false;221
+ };222
+ let mut cur = leaf;223
+ let mut any = false;224
+ while let Some(p) = self.nodes[cur].parent {225
+ if self.expanded(p) != Some(cur) {226
+ return false;227
+ }228
+ any = true;229
+ cur = p;230
+ }231
+ any232
+ }233
+197234
/// Transposes the whole tree.198235
pub fn flip(&mut self) {199236
self.stack = self.stack.other();@@ -417,9 +454,8 @@ impl<W: Copy + Eq + Hash> Tree<W> {
417454
(j < self.children(p).len()).then_some(j)418455
}419456
420
- /// Moves `n` to the end of the lane at `p`'s child `j`, which holds only421
- /// hidden windows, so it shares that lane when they come back instead of422
- /// opening a new one beside it. A lone window becomes a lane of its own.457
+ /// Moves `n` to the end of the lane at `p`'s child `j`. A lone window458
+ /// there becomes a lane of its own, perpendicular to `p`.423459
fn join_lane(&mut self, n: NodeId, p: NodeId, j: usize) {424460
let lane = self.children(p)[j].node;425461
let lane = if self.axis(lane).is_some() {@@ -1,5 +1,5 @@
11
use crate::state::State;2
-use monolith_config::{Config, Mods};2
+use monolith_config::{Config, Mods, MoveMode};33
use monolith_ipc::Command;44
use monolith_layout::{Axis, Dir, ExpandScope, WindowId};55
use smithay::input::keyboard::{Keysym, ModifiersState, xkb};@@ -52,6 +52,8 @@ impl State {
5252
pub fn run(&mut self, cmd: Command) -> Result<(), String> {5353
let output = self.layout.active_output();5454
let focused = self.layout.focused();55
+ let inverted = std::mem::take(&mut self.move_inverted);56
+ let merge = (self.config.general.move_mode == MoveMode::Merge) != inverted;5557
let on_focused = |state: &mut Self, f: fn(&mut monolith_layout::Layout, WindowId)| {5658
if let Some(w) = focused {5759
f(&mut state.layout, w);@@ -72,12 +74,19 @@ impl State {
7274
Command::Focus { dir: d } => {7375
self.layout.focus_dir(dir(d));7476
}77
+ Command::Move { dir: d } if merge => {78
+ self.layout.merge_window(dir(d));79
+ }7580
Command::Move { dir: d } => {7681
self.layout.move_window(dir(d));7782
}78
- Command::MoveBlock { dir: d } => {79
- self.layout.move_block(dir(d));83
+ Command::MoveContainer { dir: d } => {84
+ self.layout.move_container(dir(d));85
+ }86
+ Command::Maximize => {87
+ self.layout.expand(ExpandScope::All);8088
}89
+ Command::InvertMove => self.move_inverted = !inverted,8190
Command::Resize { axis, delta } => {8291
let axis = match axis {8392
monolith_ipc::Axis::Horizontal => Axis::Horizontal,@@ -88,8 +97,7 @@ impl State {
8897
Command::Expand { scope } => {8998
self.layout.expand(match scope {9099
monolith_ipc::ExpandScope::Window => ExpandScope::Window,91
- monolith_ipc::ExpandScope::Block => ExpandScope::Block,92
- monolith_ipc::ExpandScope::All => ExpandScope::All,100
+ monolith_ipc::ExpandScope::Container => ExpandScope::Container,93101
});94102
}95103
Command::Flip => output.into_iter().for_each(|o| self.layout.flip(o)),@@ -202,6 +202,7 @@ fn snapshot(state: &State) -> monolith_ipc::State {
202202
.collect();203203
windows.sort_by_key(|w| w.id);204204
monolith_ipc::State {205
+ move_inverted: state.move_inverted,205206
tags,206207
outputs,207208
windows,@@ -54,6 +54,7 @@ pub struct State {
5454
pub config_path: Option<PathBuf>,5555
pub binds: Binds,5656
pub suppressed: Vec<Keycode>,57
+ pub move_inverted: bool,5758
pub drag: Option<crate::input::Drag>,5859
pub ipc: crate::ipc::Server,5960
pub launches: crate::launch::Launches,@@ -137,6 +138,7 @@ impl State {
137138
config_path,138139
binds,139140
suppressed: Vec::new(),141
+ move_inverted: false,140142
drag: None,141143
ipc: crate::ipc::Server::default(),142144
launches: Default::default(),@@ -359,7 +361,7 @@ impl State {
359361
let mut mapped = Vec::new();360362
for (id, rect, kind, _) in shown {361363
if let Some(window) = self.windows.get(&id)362
- && configure(window, rect, kind)364
+ && configure(window, rect, kind, self.layout.maximized(id))363365
{364366
let loc = (rect.x.round() as i32, rect.y.round() as i32);365367
let loc = Point::from(loc) - window.geometry().loc;@@ -470,7 +472,7 @@ pub enum Kind {
470472
471473
/// Sets a window's pending size and states. Returns false for windows472474
/// squeezed to a line by an expanded sibling, which are not shown.473
-fn configure(window: &Window, rect: Rect, kind: Kind) -> bool {475
+fn configure(window: &Window, rect: Rect, kind: Kind, maximized: bool) -> bool {474476
let (x, y) = (rect.x.round(), rect.y.round());475477
let w = (rect.right().round() - x) as i32;476478
let h = (rect.bottom().round() - y) as i32;@@ -498,6 +500,11 @@ fn configure(window: &Window, rect: Rect, kind: Kind) -> bool {
498500
} else {499501
s.states.unset(XdgState::Fullscreen);500502
}503
+ if maximized && kind == Kind::Tiled {504
+ s.states.set(XdgState::Maximized);505
+ } else {506
+ s.states.unset(XdgState::Maximized);507
+ }501508
});502509
if t.is_initial_configure_sent() {503510
t.send_pending_configure();@@ -85,7 +85,8 @@ Running log of settled decisions and open questions. Research backing lives in `
8585
### Planner (see `planner.html`)8686
- Orthogonal style (one axis per window per phase) is the default and flagship look. Direct is a toggle.8787
- Shared-separator rule, plans of 1 to 4 phases, the 4-phase grid guarantees a plan for any input. Pinch and lerp are safety nets only.88
-- Window moves and block moves both exist as commands. Tag reordering plans with rigid blocks.88
+- Window moves and container moves both exist as commands. A window move either swaps with its neighbour or merges into its lane, chosen by `general.move`, and `invert-move` does the other one for the next move only. The default lives in config rather than in a runtime toggle. Moving a tag directionally was rejected because interspersed tags have no single shape to move. Tag reordering only changes tag order.89
+- `expand all` became `maximize`, which also sets the xdg maximized state.8990
- One curve per motion group.9091
- Floating windows travel above the tiled layer, exempt from the overlap rule.9192
- Seeds start as a line on the old boundary between neighbours.