From c1b2c7f17c8f2f9382533da8a7877503e08ce7d0 Mon Sep 17 00:00:00 2001 From: atagen Date: Mon, 8 Jun 2026 19:20:46 +1000 Subject: [PATCH] container: autotile uses major axis as first split --- book/src/tiling.md | 6 +-- crates/jay-config/src/lib.rs | 6 +-- crates/toml-spec/spec/spec.generated.json | 4 +- crates/toml-spec/spec/spec.yaml | 5 ++- src/it/test_config.rs | 6 +++ src/it/tests/t0055_autotiling.rs | 1 + src/state/tree_ops.rs | 55 ++++++++++++++++++++++- src/tree/container.rs | 11 ++++- 8 files changed, 83 insertions(+), 11 deletions(-) diff --git a/book/src/tiling.md b/book/src/tiling.md index 2ff61d5e..54ccb2bf 100644 --- a/book/src/tiling.md +++ b/book/src/tiling.md @@ -80,9 +80,9 @@ container. ## Autotiling Autotiling makes newly tiled windows alternate split direction from the focused -tiled window. The first split uses the containing group direction, then later -windows wrap the focused tile in the opposite direction, producing a horizontal, -vertical, horizontal pattern as the layout grows. +tiled window by splitting the focused tile along its largest axis. On an empty +workspace, the first split uses the largest axis of the output. This starts with +horizontal splits on landscape outputs and vertical splits on portrait outputs. ```toml [shortcuts] diff --git a/crates/jay-config/src/lib.rs b/crates/jay-config/src/lib.rs index 91dbbcae..bc8d65d0 100644 --- a/crates/jay-config/src/lib.rs +++ b/crates/jay-config/src/lib.rs @@ -438,9 +438,9 @@ pub fn get_corner_radius() -> f32 { /// Enables or disables autotiling. /// /// When enabled, newly tiled windows alternate split orientation from the -/// focused tiled window: the first split uses the containing group's direction, -/// then subsequent splits wrap the focused window in the perpendicular -/// direction. +/// focused tiled window by splitting the focused tile along its largest axis. +/// On an empty workspace, the first split uses the workspace output's largest +/// axis. /// /// The default is `false`. pub fn set_autotile(enabled: bool) { diff --git a/crates/toml-spec/spec/spec.generated.json b/crates/toml-spec/spec/spec.generated.json index 61312cf6..88e7fa2f 100644 --- a/crates/toml-spec/spec/spec.generated.json +++ b/crates/toml-spec/spec/spec.generated.json @@ -1215,7 +1215,7 @@ }, "autotile": { "type": "boolean", - "description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n" + "description": "Configures whether autotiling is enabled by default.\n\nWhen enabled, newly mapped tiled windows alternate their split\norientation automatically by splitting the focused tile along its\nlargest axis. On an empty workspace, the first split uses the largest\naxis of the output, so landscape outputs start horizontally and portrait\noutputs start vertically. This can also be toggled at runtime via the\n`enable-autotile`, `disable-autotile`, and `toggle-autotile` actions.\n\nThe default is `false`.\n" }, "modes": { "description": "Configures the input modes.\n\nModes can be used to define shortcuts that are only active when the mode is\nactive.\n\n- Example\n\n ```toml\n [modes.\"navigation\".shortcuts]\n w = \"focus-up\"\n a = \"focus-left\"\n s = \"focus-down\"\n d = \"focus-right\"\n r = \"focus-above\"\n f = \"focus-below\"\n q = \"focus-prev\"\n e = \"focus-next\"\n ```\n\nModes can be activated with the `push-mode` and `latch-mode` actions.\n", @@ -2628,4 +2628,4 @@ "required": [] } } -} \ No newline at end of file +} diff --git a/crates/toml-spec/spec/spec.yaml b/crates/toml-spec/spec/spec.yaml index 6c3b96dc..1d7061fa 100644 --- a/crates/toml-spec/spec/spec.yaml +++ b/crates/toml-spec/spec/spec.yaml @@ -3188,7 +3188,10 @@ Config: Configures whether autotiling is enabled by default. When enabled, newly mapped tiled windows alternate their split - orientation automatically. This can also be toggled at runtime via the + orientation automatically by splitting the focused tile along its + largest axis. On an empty workspace, the first split uses the largest + axis of the output, so landscape outputs start horizontally and portrait + outputs start vertically. This can also be toggled at runtime via the `enable-autotile`, `disable-autotile`, and `toggle-autotile` actions. The default is `false`. diff --git a/src/it/test_config.rs b/src/it/test_config.rs index a81ba905..199ce8a4 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -344,6 +344,12 @@ impl TestConfig { pub fn set_autotile(&self, enabled: bool) -> TestResult { self.send(ClientMessage::SetAutotile { enabled }) } + + pub fn get_autotile(&self) -> Result { + let reply = self.send_with_reply(ClientMessage::GetAutotile)?; + get_response!(reply, GetAutotile { enabled }); + Ok(enabled) + } } impl Drop for TestConfig { diff --git a/src/it/tests/t0055_autotiling.rs b/src/it/tests/t0055_autotiling.rs index 4b3611c4..95c51dc5 100644 --- a/src/it/tests/t0055_autotiling.rs +++ b/src/it/tests/t0055_autotiling.rs @@ -11,6 +11,7 @@ testcase!(); async fn test(run: Rc) -> TestResult { run.backend.install_default()?; run.cfg.set_autotile(true)?; + tassert_eq!(run.cfg.get_autotile()?, true); let client = run.create_client().await?; diff --git a/src/state/tree_ops.rs b/src/state/tree_ops.rs index 2c3872d5..2550aad2 100644 --- a/src/state/tree_ops.rs +++ b/src/state/tree_ops.rs @@ -89,11 +89,27 @@ impl State { } else { lap.add_child_after(&*la, node); } + } else if autotile { + if let Some(last) = c.children.last() { + let la = last_child_descendant(last.node.clone()); + let lap = la + .tl_data() + .parent + .get() + .and_then(|n| n.node_into_container()); + if let Some(lap) = lap { + lap.add_tiled_child_after(&*la, node); + } else { + c.append_child(node); + } + } else { + c.append_child(node); + } } else { c.append_child(node); } } else { - let container = ContainerNode::new(self, ws, node, ContainerSplit::Horizontal); + let container = ContainerNode::new(self, ws, node, initial_split_for_workspace(ws)); ws.set_container(&container); } } @@ -537,3 +553,40 @@ impl State { } } + +fn initial_split_for_workspace(ws: &WorkspaceNode) -> ContainerSplit { + ContainerSplit::for_largest_axis(ws.output.get().global.pos.get()) +} + +fn last_child_descendant(mut node: Rc) -> Rc { + loop { + let Some(container) = node.clone().node_into_container() else { + return node; + }; + let Some(last) = container.children.last() else { + return container; + }; + node = last.node.clone(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn initial_split_uses_largest_output_axis() { + assert_eq!( + ContainerSplit::for_largest_axis(Rect::new_sized_saturating(0, 0, 800, 600)), + ContainerSplit::Horizontal + ); + assert_eq!( + ContainerSplit::for_largest_axis(Rect::new_sized_saturating(0, 0, 600, 800)), + ContainerSplit::Vertical + ); + assert_eq!( + ContainerSplit::for_largest_axis(Rect::new_sized_saturating(0, 0, 600, 600)), + ContainerSplit::Horizontal + ); + } +} diff --git a/src/tree/container.rs b/src/tree/container.rs index 6712ec01..48e339cb 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -60,6 +60,14 @@ pub enum ContainerSplit { } impl ContainerSplit { + pub fn for_largest_axis(rect: Rect) -> Self { + if rect.height() > rect.width() { + ContainerSplit::Vertical + } else { + ContainerSplit::Horizontal + } + } + pub fn other(self) -> Self { match self { ContainerSplit::Horizontal => ContainerSplit::Vertical, @@ -302,11 +310,12 @@ impl ContainerNode { }; let focused_node = focused.node.clone(); let focused_active = focused_node.tl_data().active(); + let split = ContainerSplit::for_largest_axis(focused.body.get()); let sub = ContainerNode::new( &self.state, &self.workspace.get(), focused_node.clone(), - self.split.get().other(), + split, ); // Autotile-created groups are structural and collapse once only one // child remains. Explicit make-group commands control their own