oxwm

https://git.tonybtw.com/oxwm.git git://git.tonybtw.com/oxwm.git

added scrolling layout with animations.

Commit
5db567510170fd1fb0013d240cccea7747c48595
Parent
fa421d2
Author
tonybtw <tonybtw@tonybtw.com>
Date
2025-12-20 07:52:57

Diff

diff --git a/resources/test-config.lua b/resources/test-config.lua
index d9765f1..b8862db 100644
--- a/resources/test-config.lua
+++ b/resources/test-config.lua
@@ -75,8 +75,12 @@ oxwm.key.bind({ modkey, "Shift" }, "Space", oxwm.client.toggle_floating())
 
 oxwm.key.bind({ modkey }, "F", oxwm.layout.set("normie"))
 oxwm.key.bind({ modkey }, "C", oxwm.layout.set("tiling"))
+oxwm.key.bind({ modkey }, "G", oxwm.layout.set("scrolling"))
 oxwm.key.bind({ modkey }, "N", oxwm.layout.cycle())
 
+oxwm.key.bind({ modkey }, "Left", oxwm.layout.scroll_left())
+oxwm.key.bind({ modkey }, "Right", oxwm.layout.scroll_right())
+
 oxwm.key.bind({ modkey }, "A", oxwm.toggle_gaps())
 
 -- Master area controls
diff --git a/src/animations/mod.rs b/src/animations/mod.rs
new file mode 100644
index 0000000..4b9ae69
--- /dev/null
+++ b/src/animations/mod.rs
@@ -0,0 +1,42 @@
+mod scroll;
+
+pub use scroll::ScrollAnimation;
+
+use std::time::Duration;
+
+#[derive(Debug, Clone, Copy)]
+pub enum Easing {
+    Linear,
+    EaseOut,
+    EaseInOut,
+}
+
+impl Easing {
+    pub fn apply(&self, t: f64) -> f64 {
+        match self {
+            Easing::Linear => t,
+            Easing::EaseOut => 1.0 - (1.0 - t).powi(3),
+            Easing::EaseInOut => {
+                if t < 0.5 {
+                    4.0 * t * t * t
+                } else {
+                    1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
+                }
+            }
+        }
+    }
+}
+
+pub struct AnimationConfig {
+    pub duration: Duration,
+    pub easing: Easing,
+}
+
+impl Default for AnimationConfig {
+    fn default() -> Self {
+        Self {
+            duration: Duration::from_millis(150),
+            easing: Easing::EaseOut,
+        }
+    }
+}
diff --git a/src/animations/scroll.rs b/src/animations/scroll.rs
new file mode 100644
index 0000000..808b0eb
--- /dev/null
+++ b/src/animations/scroll.rs
@@ -0,0 +1,76 @@
+use std::time::Instant;
+use super::{AnimationConfig, Easing};
+
+pub struct ScrollAnimation {
+    start_value: i32,
+    end_value: i32,
+    start_time: Instant,
+    duration_ms: u64,
+    easing: Easing,
+    active: bool,
+}
+
+impl ScrollAnimation {
+    pub fn new() -> Self {
+        Self {
+            start_value: 0,
+            end_value: 0,
+            start_time: Instant::now(),
+            duration_ms: 150,
+            easing: Easing::EaseOut,
+            active: false,
+        }
+    }
+
+    pub fn start(&mut self, from: i32, to: i32, config: &AnimationConfig) {
+        if from == to {
+            self.active = false;
+            return;
+        }
+        self.start_value = from;
+        self.end_value = to;
+        self.start_time = Instant::now();
+        self.duration_ms = config.duration.as_millis() as u64;
+        self.easing = config.easing;
+        self.active = true;
+    }
+
+    pub fn update(&mut self) -> Option<i32> {
+        if !self.active {
+            return None;
+        }
+
+        let elapsed = self.start_time.elapsed().as_millis() as u64;
+
+        if elapsed >= self.duration_ms {
+            self.active = false;
+            return Some(self.end_value);
+        }
+
+        let t = elapsed as f64 / self.duration_ms as f64;
+        let eased_t = self.easing.apply(t);
+
+        let delta = (self.end_value - self.start_value) as f64;
+        let current = self.start_value as f64 + delta * eased_t;
+
+        Some(current.round() as i32)
+    }
+
+    pub fn is_active(&self) -> bool {
+        self.active
+    }
+
+    pub fn target(&self) -> i32 {
+        self.end_value
+    }
+
+    pub fn cancel(&mut self) {
+        self.active = false;
+    }
+}
+
+impl Default for ScrollAnimation {
+    fn default() -> Self {
+        Self::new()
+    }
+}
diff --git a/src/config/lua_api.rs b/src/config/lua_api.rs
index 52d815f..50a5e3e 100644
--- a/src/config/lua_api.rs
+++ b/src/config/lua_api.rs
@@ -298,8 +298,16 @@ fn register_layout_module(lua: &Lua, parent: &Table) -> Result<(), ConfigError>
         )
     })?;
 
+    let scroll_left =
+        lua.create_function(|lua, ()| create_action_table(lua, "ScrollLeft", Value::Nil))?;
+
+    let scroll_right =
+        lua.create_function(|lua, ()| create_action_table(lua, "ScrollRight", Value::Nil))?;
+
     layout_table.set("cycle", cycle)?;
     layout_table.set("set", set)?;
+    layout_table.set("scroll_left", scroll_left)?;
+    layout_table.set("scroll_right", scroll_right)?;
     parent.set("layout", layout_table)?;
     Ok(())
 }
@@ -918,6 +926,8 @@ fn string_to_action(s: &str) -> mlua::Result<KeyAction> {
         "FocusMonitor" => Ok(KeyAction::FocusMonitor),
         "TagMonitor" => Ok(KeyAction::TagMonitor),
         "ShowKeybindOverlay" => Ok(KeyAction::ShowKeybindOverlay),
+        "ScrollLeft" => Ok(KeyAction::ScrollLeft),
+        "ScrollRight" => Ok(KeyAction::ScrollRight),
         _ => Err(mlua::Error::RuntimeError(format!(
             "unknown action '{}'. this is an internal error, please report it",
             s
diff --git a/src/keyboard/handlers.rs b/src/keyboard/handlers.rs
index fc52c77..8c8e88e 100644
--- a/src/keyboard/handlers.rs
+++ b/src/keyboard/handlers.rs
@@ -41,6 +41,8 @@ pub enum KeyAction {
     ShowKeybindOverlay,
     SetMasterFactor,
     IncNumMaster,
+    ScrollLeft,
+    ScrollRight,
     None,
 }
 
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index a05ecb1..e332587 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,6 +1,7 @@
 pub mod grid;
 pub mod monocle;
 pub mod normie;
+pub mod scrolling;
 pub mod tabbed;
 pub mod tiling;
 
@@ -23,6 +24,7 @@ pub enum LayoutType {
     Grid,
     Monocle,
     Tabbed,
+    Scrolling,
 }
 
 impl LayoutType {
@@ -33,6 +35,7 @@ impl LayoutType {
             Self::Grid => Box::new(grid::GridLayout),
             Self::Monocle => Box::new(monocle::MonocleLayout),
             Self::Tabbed => Box::new(tabbed::TabbedLayout),
+            Self::Scrolling => Box::new(scrolling::ScrollingLayout),
         }
     }
 
@@ -42,7 +45,8 @@ impl LayoutType {
             Self::Normie => Self::Grid,
             Self::Grid => Self::Monocle,
             Self::Monocle => Self::Tabbed,
-            Self::Tabbed => Self::Tiling,
+            Self::Tabbed => Self::Scrolling,
+            Self::Scrolling => Self::Tiling,
         }
     }
 
@@ -53,6 +57,7 @@ impl LayoutType {
             Self::Grid => "grid",
             Self::Monocle => "monocle",
             Self::Tabbed => "tabbed",
+            Self::Scrolling => "scrolling",
         }
     }
 }
@@ -67,6 +72,7 @@ impl FromStr for LayoutType {
             "grid" => Ok(Self::Grid),
             "monocle" => Ok(Self::Monocle),
             "tabbed" => Ok(Self::Tabbed),
+            "scrolling" => Ok(Self::Scrolling),
             _ => Err(format!("Invalid Layout Type: {}", s)),
         }
     }
diff --git a/src/layout/scrolling.rs b/src/layout/scrolling.rs
new file mode 100644
index 0000000..fed8bd6
--- /dev/null
+++ b/src/layout/scrolling.rs
@@ -0,0 +1,100 @@
+use super::{GapConfig, Layout, WindowGeometry};
+use x11rb::protocol::xproto::Window;
+
+pub struct ScrollingLayout;
+
+struct GapValues {
+    outer_horizontal: u32,
+    outer_vertical: u32,
+    inner_vertical: u32,
+}
+
+impl ScrollingLayout {
+    fn getgaps(gaps: &GapConfig, window_count: usize, smartgaps_enabled: bool) -> GapValues {
+        let outer_enabled = if smartgaps_enabled && window_count == 1 {
+            0
+        } else {
+            1
+        };
+
+        GapValues {
+            outer_horizontal: gaps.outer_horizontal * outer_enabled,
+            outer_vertical: gaps.outer_vertical * outer_enabled,
+            inner_vertical: gaps.inner_vertical,
+        }
+    }
+}
+
+impl Layout for ScrollingLayout {
+    fn name(&self) -> &'static str {
+        "scrolling"
+    }
+
+    fn symbol(&self) -> &'static str {
+        "[>>]"
+    }
+
+    fn arrange(
+        &self,
+        windows: &[Window],
+        screen_width: u32,
+        screen_height: u32,
+        gaps: &GapConfig,
+        _master_factor: f32,
+        num_master: i32,
+        smartgaps_enabled: bool,
+    ) -> Vec<WindowGeometry> {
+        let window_count = windows.len();
+        if window_count == 0 {
+            return Vec::new();
+        }
+
+        let gap_values = Self::getgaps(gaps, window_count, smartgaps_enabled);
+
+        let outer_horizontal = gap_values.outer_horizontal;
+        let outer_vertical = gap_values.outer_vertical;
+        let inner_vertical = gap_values.inner_vertical;
+
+        let visible_count = if num_master > 0 {
+            num_master as usize
+        } else {
+            2
+        };
+
+        let available_width = screen_width.saturating_sub(2 * outer_vertical);
+        let available_height = screen_height.saturating_sub(2 * outer_horizontal);
+
+        let total_inner_gaps = if visible_count > 1 {
+            inner_vertical * (visible_count.min(window_count) - 1) as u32
+        } else {
+            0
+        };
+        let window_width = if window_count <= visible_count {
+            let num_windows = window_count as u32;
+            let total_gaps = if num_windows > 1 {
+                inner_vertical * (num_windows - 1)
+            } else {
+                0
+            };
+            (available_width.saturating_sub(total_gaps)) / num_windows
+        } else {
+            (available_width.saturating_sub(total_inner_gaps)) / visible_count as u32
+        };
+
+        let mut geometries = Vec::with_capacity(window_count);
+        let mut x = outer_vertical as i32;
+
+        for _window in windows.iter() {
+            geometries.push(WindowGeometry {
+                x_coordinate: x,
+                y_coordinate: outer_horizontal as i32,
+                width: window_width,
+                height: available_height,
+            });
+
+            x += window_width as i32 + inner_vertical as i32;
+        }
+
+        geometries
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index b52b23c..22a9c4c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,5 +1,6 @@
 use std::path::PathBuf;
 
+pub mod animations;
 pub mod bar;
 pub mod client;
 pub mod config;
diff --git a/src/monitor.rs b/src/monitor.rs
index 8f55a9c..cae6f33 100644
--- a/src/monitor.rs
+++ b/src/monitor.rs
@@ -35,6 +35,7 @@ pub struct Monitor {
     pub stack_head: Option<Window>,
     pub bar_window: Option<Window>,
     pub layout_indices: [usize; 2],
+    pub scroll_offset: i32,
 }
 
 impl Monitor {
@@ -67,6 +68,7 @@ impl Monitor {
             stack_head: None,
             bar_window: None,
             layout_indices: [0, 1],
+            scroll_offset: 0,
         }
     }
 
diff --git a/src/overlay/keybind.rs b/src/overlay/keybind.rs
index 6786a53..278bf28 100644
--- a/src/overlay/keybind.rs
+++ b/src/overlay/keybind.rs
@@ -239,6 +239,8 @@ impl KeybindOverlay {
             KeyAction::TagMonitor => "Send Window to Monitor".to_string(),
             KeyAction::SetMasterFactor => "Adjust Master Area Size".to_string(),
             KeyAction::IncNumMaster => "Adjust Number of Master Windows".to_string(),
+            KeyAction::ScrollLeft => "Scroll Layout Left".to_string(),
+            KeyAction::ScrollRight => "Scroll Layout Right".to_string(),
             KeyAction::None => "No Action".to_string(),
         }
     }
diff --git a/src/window_manager.rs b/src/window_manager.rs
index 6e29e90..6c8a546 100644
--- a/src/window_manager.rs
+++ b/src/window_manager.rs
@@ -1,4 +1,5 @@
 use crate::Config;
+use crate::animations::{AnimationConfig, ScrollAnimation};
 use crate::bar::Bar;
 use crate::client::{Client, TagMask};
 use crate::errors::WmError;
@@ -144,6 +145,8 @@ pub struct WindowManager {
     error_message: Option<String>,
     overlay: ErrorOverlay,
     keybind_overlay: KeybindOverlay,
+    scroll_animation: ScrollAnimation,
+    animation_config: AnimationConfig,
 }
 
 type WmResult<T> = Result<T, WmError>;
@@ -303,6 +306,8 @@ impl WindowManager {
             error_message: None,
             overlay,
             keybind_overlay,
+            scroll_animation: ScrollAnimation::new(),
+            animation_config: AnimationConfig::default(),
         };
 
         for tab_bar in &window_manager.tab_bars {
@@ -528,6 +533,8 @@ impl WindowManager {
                         last_bar_update = std::time::Instant::now();
                     }
 
+                    self.tick_animations()?;
+
                     self.connection.flush()?;
                     std::thread::sleep(std::time::Duration::from_millis(16));
                 }
@@ -610,8 +617,219 @@ impl WindowManager {
         Ok(())
     }
 
+    fn tick_animations(&mut self) -> WmResult<()> {
+        if self.scroll_animation.is_active() {
+            if let Some(new_offset) = self.scroll_animation.update() {
+                if let Some(m) = self.monitors.get_mut(self.selected_monitor) {
+                    m.scroll_offset = new_offset;
+                }
+                self.apply_layout()?;
+                self.update_bar()?;
+            }
+        }
+        Ok(())
+    }
+
+    fn scroll_layout(&mut self, direction: i32) -> WmResult<()> {
+        if self.layout.name() != "scrolling" {
+            return Ok(());
+        }
+
+        let monitor_index = self.selected_monitor;
+        let monitor = match self.monitors.get(monitor_index) {
+            Some(m) => m.clone(),
+            None => return Ok(()),
+        };
+
+        let visible_count = if monitor.num_master > 0 {
+            monitor.num_master as usize
+        } else {
+            2
+        };
+
+        let mut tiled_count = 0;
+        let mut current = self.next_tiled(monitor.clients_head, &monitor);
+        while let Some(window) = current {
+            tiled_count += 1;
+            if let Some(client) = self.clients.get(&window) {
+                current = self.next_tiled(client.next, &monitor);
+            } else {
+                break;
+            }
+        }
+
+        if tiled_count <= visible_count {
+            if let Some(m) = self.monitors.get_mut(monitor_index) {
+                m.scroll_offset = 0;
+            }
+            return Ok(());
+        }
+
+        let outer_gap = if self.gaps_enabled {
+            self.config.gap_outer_vertical
+        } else {
+            0
+        };
+        let inner_gap = if self.gaps_enabled {
+            self.config.gap_inner_vertical
+        } else {
+            0
+        };
+
+        let available_width = monitor.screen_width - 2 * outer_gap as i32;
+        let total_inner_gaps = inner_gap as i32 * (visible_count - 1) as i32;
+        let window_width = (available_width - total_inner_gaps) / visible_count as i32;
+        let scroll_amount = window_width + inner_gap as i32;
+
+        let total_width = tiled_count as i32 * window_width + (tiled_count - 1) as i32 * inner_gap as i32;
+        let max_scroll = (total_width - available_width).max(0);
+
+        let current_offset = monitor.scroll_offset;
+        let target_offset = if self.scroll_animation.is_active() {
+            self.scroll_animation.target() + direction * scroll_amount
+        } else {
+            current_offset + direction * scroll_amount
+        };
+        let target_offset = target_offset.clamp(0, max_scroll);
+
+        self.scroll_animation.start(current_offset, target_offset, &self.animation_config);
+
+        Ok(())
+    }
+
+    fn scroll_to_window(&mut self, target_window: Window, animate: bool) -> WmResult<()> {
+        if self.layout.name() != "scrolling" {
+            return Ok(());
+        }
+
+        let monitor_index = self.selected_monitor;
+        let monitor = match self.monitors.get(monitor_index) {
+            Some(m) => m.clone(),
+            None => return Ok(()),
+        };
+
+        let visible_count = if monitor.num_master > 0 {
+            monitor.num_master as usize
+        } else {
+            2
+        };
+
+        let outer_gap = if self.gaps_enabled {
+            self.config.gap_outer_vertical
+        } else {
+            0
+        };
+        let inner_gap = if self.gaps_enabled {
+            self.config.gap_inner_vertical
+        } else {
+            0
+        };
+
+        let mut tiled_windows = Vec::new();
+        let mut current = self.next_tiled(monitor.clients_head, &monitor);
+        while let Some(window) = current {
+            tiled_windows.push(window);
+            if let Some(client) = self.clients.get(&window) {
+                current = self.next_tiled(client.next, &monitor);
+            } else {
+                break;
+            }
+        }
+
+        let target_idx = tiled_windows.iter().position(|&w| w == target_window);
+        let target_idx = match target_idx {
+            Some(idx) => idx,
+            None => return Ok(()),
+        };
+
+        let tiled_count = tiled_windows.len();
+        if tiled_count <= visible_count {
+            if let Some(m) = self.monitors.get_mut(monitor_index) {
+                m.scroll_offset = 0;
+            }
+            return Ok(());
+        }
+
+        let available_width = monitor.screen_width - 2 * outer_gap as i32;
+        let total_inner_gaps = inner_gap as i32 * (visible_count - 1) as i32;
+        let window_width = (available_width - total_inner_gaps) / visible_count as i32;
+        let scroll_step = window_width + inner_gap as i32;
+
+        let total_width = tiled_count as i32 * window_width + (tiled_count - 1) as i32 * inner_gap as i32;
+        let max_scroll = (total_width - available_width).max(0);
+
+        let target_scroll = (target_idx as i32) * scroll_step;
+        let new_offset = target_scroll.clamp(0, max_scroll);
+
+        let current_offset = monitor.scroll_offset;
+        if current_offset != new_offset {
+            if animate {
+                self.scroll_animation.start(current_offset, new_offset, &self.animation_config);
+            } else {
+                if let Some(m) = self.monitors.get_mut(monitor_index) {
+                    m.scroll_offset = new_offset;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
     fn get_layout_symbol(&self) -> String {
         let layout_name = self.layout.name();
+
+        if layout_name == "scrolling" {
+            if let Some(monitor) = self.monitors.get(self.selected_monitor) {
+                let visible_count = if monitor.num_master > 0 {
+                    monitor.num_master as usize
+                } else {
+                    2
+                };
+
+                let mut tiled_count = 0;
+                let mut current = self.next_tiled(monitor.clients_head, monitor);
+                while let Some(window) = current {
+                    tiled_count += 1;
+                    if let Some(client) = self.clients.get(&window) {
+                        current = self.next_tiled(client.next, monitor);
+                    } else {
+                        break;
+                    }
+                }
+
+                if tiled_count > 0 {
+                    let outer_gap = if self.gaps_enabled {
+                        self.config.gap_outer_vertical
+                    } else {
+                        0
+                    };
+                    let inner_gap = if self.gaps_enabled {
+                        self.config.gap_inner_vertical
+                    } else {
+                        0
+                    };
+
+                    let available_width = monitor.screen_width - 2 * outer_gap as i32;
+                    let total_inner_gaps = inner_gap as i32 * (visible_count.min(tiled_count) - 1) as i32;
+                    let window_width = if tiled_count <= visible_count {
+                        (available_width - total_inner_gaps) / tiled_count as i32
+                    } else {
+                        (available_width - inner_gap as i32 * (visible_count - 1) as i32) / visible_count as i32
+                    };
+
+                    let scroll_step = window_width + inner_gap as i32;
+                    let first_visible = if scroll_step > 0 {
+                        (monitor.scroll_offset / scroll_step) + 1
+                    } else {
+                        1
+                    };
+                    let last_visible = (first_visible + visible_count as i32 - 1).min(tiled_count as i32);
+
+                    return format!("[{}-{}/{}]", first_visible, last_visible, tiled_count);
+                }
+            }
+        }
+
         self.config
             .layout_symbols
             .iter()
@@ -908,6 +1126,12 @@ impl WindowManager {
                     self.inc_num_master(*delta)?;
                 }
             }
+            KeyAction::ScrollLeft => {
+                self.scroll_layout(-1)?;
+            }
+            KeyAction::ScrollRight => {
+                self.scroll_layout(1)?;
+            }
             KeyAction::None => {}
         }
         Ok(())
@@ -1953,7 +2177,15 @@ impl WindowManager {
             )?;
         }
 
-        self.attach_aside(window, client_monitor);
+        if self.layout.name() == "scrolling" {
+            if let Some(selected) = self.monitors.get(client_monitor).and_then(|m| m.selected_client) {
+                self.attach_after(window, selected, client_monitor);
+            } else {
+                self.attach_aside(window, client_monitor);
+            }
+        } else {
+            self.attach_aside(window, client_monitor);
+        }
         self.attach_stack(window, client_monitor);
         self.windows.push(window);
 
@@ -1985,6 +2217,10 @@ impl WindowManager {
             m.selected_client = Some(window);
         }
 
+        if self.layout.name() == "scrolling" {
+            self.scroll_to_window(window, false)?;
+        }
+
         self.apply_layout()?;
         self.connection.map_window(window)?;
         self.focus(Some(window))?;
@@ -2224,6 +2460,11 @@ impl WindowManager {
         };
 
         self.focus(Some(next_window))?;
+
+        if self.layout.name() == "scrolling" {
+            self.scroll_to_window(next_window, true)?;
+        }
+
         self.update_tab_bars()?;
 
         Ok(())
@@ -3434,6 +3675,7 @@ impl WindowManager {
                 let monitor_y = monitor.screen_y;
                 let monitor_width = monitor.screen_width;
                 let monitor_height = monitor.screen_height;
+                let scroll_offset = monitor.scroll_offset;
 
                 let mut visible: Vec<Window> = Vec::new();
                 let mut current = self.next_tiled(monitor.clients_head, monitor);
@@ -3487,7 +3729,12 @@ impl WindowManager {
                         adjusted_height = hint_height as u32;
                     }
 
-                    let adjusted_x = geometry.x_coordinate + monitor_x;
+                    let is_scrolling = self.layout.name() == "scrolling";
+                    let adjusted_x = if is_scrolling {
+                        geometry.x_coordinate + monitor_x - scroll_offset
+                    } else {
+                        geometry.x_coordinate + monitor_x
+                    };
                     let adjusted_y = geometry.y_coordinate + monitor_y + bar_height as i32;
 
                     if let Some(client) = self.clients.get_mut(window) {
@@ -4046,6 +4293,20 @@ impl WindowManager {
         }
     }
 
+    fn attach_after(&mut self, window: Window, after_window: Window, monitor_index: usize) {
+        if let Some(after_client) = self.clients.get(&after_window) {
+            let old_next = after_client.next;
+            if let Some(new_client) = self.clients.get_mut(&window) {
+                new_client.next = old_next;
+            }
+            if let Some(after_client_mut) = self.clients.get_mut(&after_window) {
+                after_client_mut.next = Some(window);
+            }
+        } else {
+            self.attach(window, monitor_index);
+        }
+    }
+
     fn attach_aside(&mut self, window: Window, monitor_index: usize) {
         let monitor = match self.monitors.get(monitor_index) {
             Some(m) => m,
diff --git a/templates/oxwm.lua b/templates/oxwm.lua
index 3dc0d81..547708b 100644
--- a/templates/oxwm.lua
+++ b/templates/oxwm.lua
@@ -192,10 +192,18 @@ oxwm.layout = {}
 function oxwm.layout.cycle() end
 
 ---Set specific layout
----@param name string Layout name (e.g., "tiling", "normie", "tabbed", "grid", "monocle")
+---@param name string Layout name (e.g., "tiling", "normie", "tabbed", "grid", "monocle", "scrolling")
 ---@return table Action table for keybinding
 function oxwm.layout.set(name) end
 
+---Scroll layout left (for scrolling layout)
+---@return table Action table for keybinding
+function oxwm.layout.scroll_left() end
+
+---Scroll layout right (for scrolling layout)
+---@return table Action table for keybinding
+function oxwm.layout.scroll_right() end
+
 ---Tag/workspace management module
 ---@class oxwm.tag
 oxwm.tag = {}