Skip to content

feat(scroll): add consume/expel window commands (with gating/compat alignment)#151

Open
BarutSRB wants to merge 18 commits intotmandry:mainfrom
BarutSRB:feat/scroll-consume-expel
Open

feat(scroll): add consume/expel window commands (with gating/compat alignment)#151
BarutSRB wants to merge 18 commits intotmandry:mainfrom
BarutSRB:feat/scroll-consume-expel

Conversation

@BarutSRB
Copy link
Copy Markdown
Contributor

Summary

Add scroll-layout consume_or_expel_window commands (left/right) to move the focused window into/out of adjacent columns.

What this adds

  • New layout command: consume_or_expel_window = "left" | "right".
  • Keybinding support for the command in config parsing.
  • Scroll-aware behavior in layout operations for consume/expel workflows.

Included compatibility/alignment fixes

While integrating this feature, this PR also includes small safety alignments so behavior stays correct:

  • Scroll-only commands remain no-op when experimental.scroll.enable = false.
  • Legacy restored layouts backfill scroll_roots on load to preserve expected scroll-tree behavior.

Files

  • glide.default.toml
  • src/actor/layout.rs
  • src/config.rs
  • src/model/layout_tree.rs

Verification

Ran and passed locally:

  • cargo test consume_or_expel_window -- --nocapture
  • cargo test rebuild_scroll_roots -- --nocapture
  • cargo test -q scroll_wheel_is_ignored_when_scroll_gate_disabled -- --nocapture
  • cargo test -q scroll -- --nocapture (28 passed)
  • cargo test -q change_layout_kind -- --nocapture (1 passed)

CI will run full fmt/check/build/test on the PR.

Notes

Feature remains behind existing experimental scroll gating.

Barut and others added 18 commits February 2, 2026 11:15
Niri Layout
# Conflicts:
#	src/config.rs
- Fix animation timer: use Timer::manual() with guard to only fire while animating
- Restore removed doc comments flagged in review
- Apply if-let chain suggestion in reactor.rs
- Use ScreenInfo struct per review suggestion
- Document scroll settings individually in glide.default.toml
- Add TODOs for deferred review items (configurable modifier, animation in model)
- Run cargo fmt to fix CI style failures
Unify the viewport and non-viewport code paths for interactive window
move hit-testing into a single loop. Previously the logic was duplicated
inside an `if let Some(vp)` branch and its `else` branch.

Addresses Gemini review feedback on PR tmandry#129.
Extract the common layout-application logic from `update_layout` and
`update_layout_no_anim` into a shared `apply_layout` function.
A `LayoutMode` enum distinguishes between animated and immediate
window placement, eliminating the near-complete duplication between
the two methods.

Addresses Gemini review feedback on PR tmandry#129.
Remove the ConsumeIntoColumn and ExpelFromColumn commands in favor of
using MoveNode, as suggested in PR tmandry#129 review. Also restore comments
that were inadvertently removed across layout.rs, mouse.rs, reactor.rs,
and layout_tree.rs.

Addresses tmandry's review feedback on PR tmandry#129.
solve_sizes enforces a 50px minimum window size, which is appropriate
for scroll layouts but breaks tree layouts with small test screens
(e.g. 100x100 with 2 windows where one should be 40px after resize).
Use 1.0 as the minimum for tree layouts to preserve existing behavior.

Also removes a debug eprintln accidentally left in the resize handler.
apply_viewport_to_frames referenced crate::actor::app::WindowId,
violating the model→actor layering rule. Make the function generic
over the ID type since it only passes the value through.
SpringAnimation and ViewportState called Instant::now() directly,
coupling the model layer to wall-clock time and forcing tests to use
thread::sleep. Accept Instant as a parameter throughout the model API
so callers in the actor layer provide the timestamp.
set_config unconditionally overwrote every scroll column's weight,
discarding widths set via CycleColumnWidth or interactive resize.
New columns already get their weight from visible_columns at creation
time — existing columns shouldn't be touched.
For windows smaller than 2*RESIZE_EDGE_THRESHOLD, detect_edges could
set both LEFT+RIGHT or TOP+BOTTOM simultaneously, causing conflicting
resize deltas that cancel out. Clear both edges on an axis when the
window is too small to distinguish them.
scroll_sensitivity was not validated, allowing extreme values to
produce billions of traversal steps and hang the reactor. Clamp
sensitivity to [0, 100], filter invalid column width presets, and
cap step count to 16 as defense-in-depth.
Put scroll layout behind an experimental gate and default it to off.

This adds and enforces settings.experimental.scroll.enable as the runtime gate. When the gate is off, scroll behavior is disabled: ignore scroll wheel handling for scroll layout, no-op scroll-only commands (change_layout_kind, toggle_column_tabbed, cycle_column_width), prevent default_layout_kind=scroll from activating and fall back to tree, convert active scroll layouts back to tree when the gate is disabled, and skip scroll layouts during next/prev layout cycling when gated off.

Default config was updated so scroll commands are not bound by default, with opt-in examples for users who enable the experimental gate. Added tests for gate defaults and gate-off behavior.
@gemini-code-assist

This comment was marked as off-topic.

gemini-code-assist[bot]

This comment was marked as off-topic.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

@tmandry you have to try this new command. It might just be me, but I think you’ll like it. The only problem is that niri is still very buggy with more than one monitor.

@tmandry
Copy link
Copy Markdown
Owner

tmandry commented Feb 28, 2026

So this is close to what MoveNode does in the tree layout, except it "auto creates" a column if one doesn't exist in the consume case.

Do you imagine this command doing the same in the tree layout case?

I think this would generalize in all directions... basically if you are moving left/right it should create a column or stack; if moving up/down it would create a horizontal or tabbed node.

Continuing down that path, there are basically two kinds of moves that could each be their own commands:

  1. Swap a node with its sibling
  2. Move into/out of container (this command)

If a tree layout has an invariant that each level of nesting has an alternating direction, we can distinguish between a node and its parent based on the direction. This is usually the way I use it anyway, and it points to a possible simplification in the commands used to build layouts; only "split" and "group" are needed, along with these two kinds of move commands.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

I really love your idea of making it geenralized adopting to the layout kind and the arrow directions, this would potentially lower the commands the user needs to memorize and freeing up keys for potentially other stuff or lowering conflicts with other apps.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

BarutSRB commented Feb 28, 2026

I'll get on this tonight or tomorrow ❤️

@tmandry
Copy link
Copy Markdown
Owner

tmandry commented Mar 5, 2026

We can merge the current behavior after a rebase. I do want to generalize but a follow-up PR works.

When using this I noticed that the tabs use left/right navigation but I think stacks would be more natural, what do you think? Then you can make use of both directions and avoid cycling through all tabs in a group when using the keyboard to navigate.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

BarutSRB commented Mar 5, 2026

We can merge the current behavior after a rebase. I do want to generalize but a follow-up PR works.

When using this I noticed that the tabs use left/right navigation but I think stacks would be more natural, what do you think? Then you can make use of both directions and avoid cycling through all tabs in a group when using the keyboard to navigate.

If it's the cycle command prev/next it is by design going through all the windows in order as some like to use the least amount of hot keys but we can make it ignore if column tabbed on cycle and just move to next column instead of switching tabbed windows when cycling. If it's the left/right horizontal navigation than the tabbed windows should be up/down when trying to switch them. But if you have a better idea to make the least amount of if/else logics for those commands I'm up for it.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

BarutSRB commented Mar 5, 2026

But you are right I think having cycle on tabbed columns left/right should move to next column and have an if for tabbed columns on cycle command for up/down

@tmandry
Copy link
Copy Markdown
Owner

tmandry commented Mar 5, 2026

I'm using move focus left/right commands. I think spatial commands are an important basis for how Glide is used. If people aren't familiar with vim style directions, perhaps they should use arrow keys or WASD instead.

@BarutSRB
Copy link
Copy Markdown
Contributor Author

BarutSRB commented Apr 1, 2026

@tmandry Hey sorry for being so absent, I did rewrite and then i noticed that the problem was in architecture so I went back to swift and it fixed it, but then users started following and making issues, to the point where I couldnt drink coffee as I wanted to reduce the number off issues (OCD syndrom) I still want to merge the projects, but for me biggest blocker is rust, as I truly cant understand it jsut liek C++ or Cobalt, so I have to use AI for PR and merges, and I do treust you as you have shown multiple times your knwoldge and skill, but it jsut frustrates me that I see lines of code by AI but I dont understand it in rust, so I feel usless, also i tested small parts in rust, zig and swift, and some ended up a lot better in p95 and p99 tests especially AX APis which shocked me. Why I'm writing this is because I don't know if I will have the will to keep up with everything as the job which is not IT is drainign me and the conomy is not good so that also takes a tool on me, if you are ok, I would love to be able to send PRs or explanations hwo somethign was done (for example Ghostty tabs which even Rift copied) and for you to do the polishing, as the monthly claude eats a lot of tokens and I have to use it for Rust as I dont know rust, and end up spending most of its monthly limits asking it to explain why something was done in such a way or for it to confirm in plain words line by line changes so that I can see if that is what I wanted.

The good news is that I have achieved for Niri layout perfect parity with the original including aniamtions timings, so I can not with confidence start pushing stuff via AI and than if something is wrong jsut compare it with Omni to see what is wrong, as before this wasnt possible as Omni itself has bugs.

Let me know if you are ok with taking the lead and polishing so that I can start pushing stuff, as for the previous PR I ended up asking so many times AI to explain how something is wrong because im not sure if its jsut me but I find Rust/C++ alien to me, while c and zig i find relaxing if that makes sence and swift well i know it so...

If you are ok, Id start first with native tabs fix creating ghost windows, all it woudl take is toi change so it doesnt only watch for ghostty but applies it to all apps that trigger natiuve tabs.

Than iud push smart expel/consume

And work on polishing niri layout

After this id work on swift but not swiftui as it showed in omni to be buggy for support on multipel versions of macos but instead appkit, and make a drop down menu and analyze the current config to sketch it in figma to feel intutiver and organize it, once that is done, later its easy to add extra tabs in setting sfor new configs etc...

Only problem is that for Niri to feel like the original it requires hiding on the sides other windows as it makes it least distance traveled and no resizing, and this presents a problem for multi monitor support, but i was thinginkg which is kind of a bloat, to make a virtual fake monitor where we can hide windows, so they dont leak, I tested this and it worked perfectly for hiding but it also crashed a lot, which casn always be fixed but I just did it to see if it woudl work and it did, however for some reason even though the monitor was fake, macos was still rendering it and treating it as visible and I would love to find a way to avoid that so we can get leaner resource hog

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants