Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions rift.default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ column_width_ratio = 0.7
# min_column_width_ratio / max_column_width_ratio: clamp bounds used by resize commands
min_column_width_ratio = 0.3
max_column_width_ratio = 0.9
# Preset column widths cycled by resize_window_preset_next / resize_window_preset_prev.
# Each value is a fraction of screen width (0.0–1.0).
# Empty: disables preset cycling.
column_width_presets = [0.33, 0.50, 0.66, 0.99]
# Alignment for the focused column: "left", "center", or "right"
alignment = "center"
# Horizontal focus behavior:
Expand Down Expand Up @@ -359,6 +363,7 @@ comb1 = "Alt + Shift"
# - toggle_stack / toggle_orientation / unjoin_windows
# - toggle_focus_floating / toggle_window_floating / toggle_fullscreen / toggle_fullscreen_within_gaps
# - resize_window_grow / resize_window_shrink / resize_window_by = 0.05
# - resize_window_preset_next / resize_window_preset_prev
# - swap_windows = [123, 456]
# - exec = "command" | exec = ["cmd", "arg1", "..."]
# - move_mouse_to_display = "left"|"right"|"up"|"down"|N|"<display_uuid>"
Expand Down
11 changes: 11 additions & 0 deletions src/bin/rift-cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,11 @@ enum WindowCommands {
/// rift-cli execute window resize-by --amount 0.05 # grow by 5%
/// rift-cli execute window resize-by --amount -0.10 # shrink by 10%
ResizeBy { amount: f64 },
/// Cycle the selected window to the next column width preset.
/// The presets are defined in settings.layout.scrolling.column_width_presets.
ResizePresetNext,
/// Cycle the selected window to the previous column width preset.
ResizePresetPrev,
/// Close a window by window server identifier
Close {
/// Window Id (window server id or idx from window id)
Expand Down Expand Up @@ -576,6 +581,12 @@ fn map_window_command(cmd: WindowCommands) -> Result<RiftCommand, String> {
WindowCommands::ResizeBy { amount } => Ok(RiftCommand::Reactor(reactor::Command::Layout(
LC::ResizeWindowBy { amount },
))),
WindowCommands::ResizePresetNext => Ok(RiftCommand::Reactor(reactor::Command::Layout(
LC::ResizeWindowPresetNext,
))),
WindowCommands::ResizePresetPrev => Ok(RiftCommand::Reactor(reactor::Command::Layout(
LC::ResizeWindowPresetPrev,
))),
WindowCommands::Close { window_id } => {
let wsid = parse_window_server_id(&window_id)?;
Ok(RiftCommand::Reactor(reactor::Command::Reactor(
Expand Down
15 changes: 15 additions & 0 deletions src/common/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,11 @@ pub struct ScrollingLayoutSettings {
/// Trackpad gestures for scrolling layout
#[serde(default)]
pub gestures: ScrollingGestureSettings,
/// Preset column widths cycled by ResizeWindowPresetCycle.
/// Each value is a fraction of screen width (0.0–1.0).
/// Empty: preset cycling is disabled.
#[serde(default)]
pub column_width_presets: Vec<f64>,
}

impl Default for ScrollingLayoutSettings {
Expand All @@ -669,6 +674,7 @@ impl Default for ScrollingLayoutSettings {
alignment: ScrollingAlignment::default(),
focus_navigation_style: ScrollingFocusNavigationStyle::default(),
gestures: ScrollingGestureSettings::default(),
column_width_presets: Vec::new(),
}
}
}
Expand Down Expand Up @@ -965,6 +971,15 @@ impl ScrollingLayoutSettings {
));
}

for (i, preset) in self.column_width_presets.iter().enumerate() {
if !(0.0..=1.0).contains(preset) {
issues.push(format!(
"layout.scrolling.column_width_presets[{}] must be between 0.0 and 1.0, got {}",
i, preset
));
}
}

issues
}
}
Expand Down
26 changes: 26 additions & 0 deletions src/layout_engine/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ pub enum LayoutCommand {
ResizeWindowBy {
amount: f64,
},
/// Cycle the selected window to the next column width preset.
ResizeWindowPresetNext,
/// Cycle the selected window to the previous column width preset.
ResizeWindowPresetPrev,

/// Scroll the strip by a normalized delta (scaled by column step width)
ScrollStrip {
Expand Down Expand Up @@ -1647,6 +1651,28 @@ impl LayoutEngine {
self.workspace_tree_mut(workspace_id).resize_selection_by(layout, amount);
EventResponse::default()
}
LayoutCommand::ResizeWindowPresetNext => {
if is_floating {
return EventResponse::default();
}

self.workspace_layouts.mark_last_saved(space, workspace_id, layout);
let presets = self.layout_settings.scrolling.column_width_presets.clone();
self.workspace_tree_mut(workspace_id)
.resize_selection_preset(layout, &presets, false);
EventResponse::default()
}
LayoutCommand::ResizeWindowPresetPrev => {
if is_floating {
return EventResponse::default();
}

self.workspace_layouts.mark_last_saved(space, workspace_id, layout);
let presets = self.layout_settings.scrolling.column_width_presets.clone();
self.workspace_tree_mut(workspace_id)
.resize_selection_preset(layout, &presets, true);
EventResponse::default()
}
LayoutCommand::AdjustMasterRatio { delta } => {
self.workspace_layouts.mark_last_saved(space, workspace_id, layout);
if let LayoutSystemKind::MasterStack(s) = self.workspace_tree_mut(workspace_id) {
Expand Down
1 change: 1 addition & 0 deletions src/layout_engine/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ pub trait LayoutSystem: Serialize + for<'de> Deserialize<'de> {
fn parent_of_selection_is_stacked(&self, layout: LayoutId) -> bool;
fn unjoin_selection(&mut self, _layout: LayoutId);
fn resize_selection_by(&mut self, layout: LayoutId, amount: f64);
fn resize_selection_preset(&mut self, _layout: LayoutId, _presets: &[f64], _reverse: bool) {}
fn rebalance(&mut self, layout: LayoutId);
fn toggle_tile_orientation(&mut self, layout: LayoutId);
}
Expand Down
214 changes: 214 additions & 0 deletions src/layout_engine/systems/scrolling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,6 +1348,68 @@ impl LayoutSystem for ScrollingLayoutSystem {
}
}

fn resize_selection_preset(&mut self, layout: LayoutId, presets: &[f64], reverse: bool) {
if presets.is_empty() {
return;
}
let niri_navigation = matches!(
self.settings.focus_navigation_style,
ScrollingFocusNavigationStyle::Niri
);
let Some(state) = self.layout_state_mut(layout) else {
return;
};
let base_ratio = state.column_width_ratio;

let (effective, col_idx_opt, was_fullscreen) =
if let Some((col_idx, win_idx)) = state.selected_location() {
let wid = state.columns[col_idx].windows[win_idx];
let was_fs = state.fullscreen.contains(&wid)
|| state.fullscreen_within_gaps.contains(&wid);
state.fullscreen.remove(&wid);
state.fullscreen_within_gaps.remove(&wid);
(
base_ratio + state.columns[col_idx].width_offset,
Some(col_idx),
was_fs,
)
} else {
(base_ratio, None, false)
};

let target = if was_fullscreen {
if reverse {
*presets.last().unwrap()
} else {
presets[0]
}
} else if reverse {
presets
.iter()
.rev()
.find(|&&p| p < effective)
.copied()
.unwrap_or_else(|| *presets.last().unwrap())
} else {
presets
.iter()
.find(|&&p| p > effective)
.copied()
.unwrap_or(presets[0])
};

if let Some(col_idx) = col_idx_opt {
state.columns[col_idx].width_offset = target - base_ratio;
if niri_navigation {
state.reveal_selected_without_direction();
} else {
state.align_scroll_to_selected();
}
} else {
state.column_width_ratio = target;
}
}

fn rebalance(&mut self, _layout: LayoutId) {}

fn toggle_tile_orientation(&mut self, _layout: LayoutId) {}
Expand Down Expand Up @@ -2013,4 +2075,156 @@ mod tests {
after.origin.x
);
}

#[test]
fn preset_next_cycles_forward() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_ratio = 0.33;
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, _) = setup_two_windows(settings);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], false);
let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.50).abs() < 0.01,
"expected second preset 0.50, got {}",
effective
);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], false);
let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.66).abs() < 0.01,
"expected third preset 0.66, got {}",
effective
);
}

#[test]
fn preset_prev_cycles_backward() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_ratio = 0.33;
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, _) = setup_two_windows(settings);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], true);
let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.99).abs() < 0.01,
"expected last preset 0.99, got {}",
effective
);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], true);
let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.66).abs() < 0.01,
"expected third preset 0.66, got {}",
effective
);
}

#[test]
fn preset_next_wraps_to_first() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_ratio = 0.99;
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, _) = setup_two_windows(settings);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], false);

let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.33).abs() < 0.01,
"expected wrap to first preset 0.33, got {}",
effective
);
}

#[test]
fn preset_prev_wraps_to_last() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_ratio = 0.33;
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, _) = setup_two_windows(settings);

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], true);

let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.99).abs() < 0.01,
"expected wrap to last preset 0.99, got {}",
effective
);
}

#[test]
fn preset_next_from_fullscreen_goes_to_first() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, w2) = setup_two_windows(settings);

system.toggle_fullscreen_of_selection(layout);
system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], false);

let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.33).abs() < 0.01,
"expected first preset 0.33 after exiting fullscreen, got {}",
effective
);
assert!(!state.fullscreen.contains(&w2));
assert!(!state.fullscreen_within_gaps.contains(&w2));
}

#[test]
fn preset_prev_from_fullscreen_goes_to_last() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let (mut system, layout, _, w2) = setup_two_windows(settings);

system.toggle_fullscreen_within_gaps_of_selection(layout);
system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], true);

let state = system.layouts.get(layout).expect("layout state missing");
let effective = state.column_width_ratio + state.columns[1].width_offset;
assert!(
(effective - 0.99).abs() < 0.01,
"expected last preset 0.99 after exiting fullscreen, got {}",
effective
);
assert!(!state.fullscreen.contains(&w2));
assert!(!state.fullscreen_within_gaps.contains(&w2));
}

#[test]
fn preset_without_selection_adjusts_base_ratio() {
let mut settings = ScrollingLayoutSettings::default();
settings.column_width_presets = vec![0.33, 0.50, 0.66, 0.99];
let mut system = ScrollingLayoutSystem::new(&settings);
let layout = system.create_layout();
let w1 = wid(1, 1);
system.add_window_after_selection(layout, w1);
// Clear selection
{
let state = system.layouts.get_mut(layout).expect("layout state missing");
state.selected = None;
}

system.resize_selection_preset(layout, &[0.33, 0.50, 0.66, 0.99], false);

let state = system.layouts.get(layout).expect("layout state missing");
assert!(
(state.column_width_ratio - 0.99).abs() < 0.01,
"expected base ratio set to next preset 0.99, got {}",
state.column_width_ratio
);
}
}