markdown · 7474 bytes Raw Blame History

Sprint 5: Floating Windows

Goal: Full floating window support with mouse interaction.

Objectives

  • Toggle windows between tiled and floating
  • Mouse-based move and resize for floating windows
  • Automatic floating for dialogs and certain window types
  • Proper stacking order (floating above tiled)

Prerequisites

  • Sprint 4 complete (Lua configuration)

Tasks

5.1 Floating Window State

  • Add floating window list to Workspace
  • Track floating window geometry separately
  • Floating windows don't participate in BSP tree
  • Track stacking order for floating windows
pub struct Workspace {
    pub tree: Node,                    // Tiled windows
    pub floating: Vec<FloatingWindow>, // Floating windows
    pub focused: Option<WindowId>,
    // ...
}

pub struct FloatingWindow {
    pub id: WindowId,
    pub geometry: Rect,
    pub z_order: u32,
}

5.2 Toggle Floating

  • Implement Mod+Shift+Space to toggle
  • When floating: remove from tree, add to floating list
  • When tiling: remove from floating list, insert to tree
  • Preserve position when floating
  • Use smart split when returning to tiled
fn toggle_floating(&mut self, window: WindowId) {
    if self.is_floating(window) {
        // Move to tiled
        let float_win = self.floating.remove(/* ... */);
        self.tree.insert(window);
    } else {
        // Move to floating
        let geometry = self.get_window_geometry(window);
        self.tree.remove(window);
        self.floating.push(FloatingWindow {
            id: window,
            geometry,
            z_order: self.next_z_order(),
        });
    }
}

5.3 Mouse Move (Mod+LeftClick)

  • Grab Mod+Button1 on all windows
  • On press: record start position
  • On motion: update window position
  • On release: finalize position
  • Only works for floating windows
fn handle_button_press(&mut self, event: ButtonPressEvent) {
    if event.detail == 1 && event.state.contains(ModMask::M4) {
        // Start move operation
        self.drag_state = Some(DragState::Move {
            window: event.child,
            start_x: event.root_x,
            start_y: event.root_y,
            start_geometry: self.get_geometry(event.child),
        });
        self.conn.grab_pointer(/* ... */)?;
    }
}

fn handle_motion(&mut self, event: MotionNotifyEvent) {
    if let Some(DragState::Move { window, start_x, start_y, start_geometry }) = &self.drag_state {
        let dx = event.root_x - start_x;
        let dy = event.root_y - start_y;
        let new_x = start_geometry.x + dx;
        let new_y = start_geometry.y + dy;
        self.configure_window(window, new_x, new_y, None, None)?;
    }
}

5.4 Mouse Resize (Mod+RightClick)

  • Grab Mod+Button3 on all windows
  • Determine resize corner based on click position
  • On motion: adjust size (and position for top/left edges)
  • Enforce minimum window size
  • Only works for floating windows
fn determine_resize_edge(window_geometry: Rect, click_x: i16, click_y: i16) -> ResizeEdge {
    let center_x = window_geometry.x + window_geometry.width as i16 / 2;
    let center_y = window_geometry.y + window_geometry.height as i16 / 2;

    match (click_x < center_x, click_y < center_y) {
        (true, true) => ResizeEdge::TopLeft,
        (false, true) => ResizeEdge::TopRight,
        (true, false) => ResizeEdge::BottomLeft,
        (false, false) => ResizeEdge::BottomRight,
    }
}

5.5 Auto-Float Rules

  • Float _NET_WM_WINDOW_TYPE_DIALOG
  • Float _NET_WM_WINDOW_TYPE_SPLASH
  • Float _NET_WM_WINDOW_TYPE_TOOLTIP
  • Float _NET_WM_WINDOW_TYPE_UTILITY
  • Float windows with WM_TRANSIENT_FOR set
  • Respect size hints for floating position
fn should_float(conn: &impl Connection, window: Window) -> Result<bool> {
    let window_type = get_window_type(conn, window)?;

    let float_types = [
        atoms._NET_WM_WINDOW_TYPE_DIALOG,
        atoms._NET_WM_WINDOW_TYPE_SPLASH,
        atoms._NET_WM_WINDOW_TYPE_TOOLTIP,
        atoms._NET_WM_WINDOW_TYPE_UTILITY,
        atoms._NET_WM_WINDOW_TYPE_POPUP_MENU,
    ];

    if float_types.contains(&window_type) {
        return Ok(true);
    }

    // Check transient
    if get_transient_for(conn, window)?.is_some() {
        return Ok(true);
    }

    Ok(false)
}

5.6 Stacking Order

  • Floating windows above tiled
  • Most recently focused floating on top
  • Use ConfigureWindow with stack_mode and sibling
  • Maintain stacking order on focus changes
fn raise_window(&self, conn: &impl Connection, window: Window) -> Result<()> {
    conn.configure_window(
        window,
        &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
    )?;
    Ok(())
}

fn restack_floating(&self, conn: &impl Connection) -> Result<()> {
    // Stack in z_order, lowest first
    let mut sorted: Vec<_> = self.floating.iter().collect();
    sorted.sort_by_key(|w| w.z_order);

    for (i, window) in sorted.iter().enumerate() {
        if i > 0 {
            conn.configure_window(
                window.id,
                &ConfigureWindowAux::new()
                    .stack_mode(StackMode::ABOVE)
                    .sibling(sorted[i - 1].id),
            )?;
        }
    }
    Ok(())
}

5.7 Floating Focus

  • Tab cycles through floating windows
  • Focus follows click for floating
  • Raise on focus
  • Handle focus between tiled and floating
fn focus_next_floating(&mut self) {
    if self.floating.is_empty() {
        return;
    }

    let current_idx = self.floating
        .iter()
        .position(|w| Some(w.id) == self.focused)
        .unwrap_or(0);

    let next_idx = (current_idx + 1) % self.floating.len();
    let next_window = self.floating[next_idx].id;

    self.set_focus(next_window);
    self.raise_window(next_window);
}

5.8 Lua Integration

  • Add floating property to window rules
  • Add gar.toggle_floating() function
  • Add gar.float_geometry(x, y, w, h) for rules
-- Float Firefox PiP
gar.rule(
    { class = "firefox", title = "Picture-in-Picture" },
    { floating = true, geometry = { x = 100, y = 100, width = 400, height = 300 } }
)

-- Keybind
gar.bind("mod+shift+space", gar.toggle_floating)

Keybind Summary

Keybind Action
Mod+Shift+Space Toggle floating
Mod+LeftClick drag Move floating window
Mod+RightClick drag Resize floating window

Acceptance Criteria

  1. Mod+Shift+Space toggles between tiled/floating
  2. Can drag floating windows with Mod+LeftClick
  3. Can resize floating windows with Mod+RightClick
  4. Dialogs automatically float
  5. Floating windows stay above tiled
  6. Focus works correctly between tiled/floating

Testing Strategy

# Test toggle
# Open xterm, verify tiled
# Mod+Shift+Space, verify floating (doesn't affect layout)
# Mod+Shift+Space again, verify back in tile

# Test mouse
# Float a window
# Mod+LeftClick drag - should move
# Mod+RightClick drag - should resize

# Test auto-float
# Open a dialog (e.g., Firefox preferences)
# Should automatically float

# Test stacking
# Have tiled windows
# Float one
# Floating should be on top

Notes

  • Consider minimum size constraints
  • Handle fullscreen windows (Sprint 8)
  • Floating geometry should be remembered when toggling
  • Resize should work from any edge (not just corner)
View source
1 # Sprint 5: Floating Windows
2
3 **Goal:** Full floating window support with mouse interaction.
4
5 ## Objectives
6
7 - Toggle windows between tiled and floating
8 - Mouse-based move and resize for floating windows
9 - Automatic floating for dialogs and certain window types
10 - Proper stacking order (floating above tiled)
11
12 ## Prerequisites
13
14 - Sprint 4 complete (Lua configuration)
15
16 ## Tasks
17
18 ### 5.1 Floating Window State
19 - [ ] Add floating window list to Workspace
20 - [ ] Track floating window geometry separately
21 - [ ] Floating windows don't participate in BSP tree
22 - [ ] Track stacking order for floating windows
23
24 ```rust
25 pub struct Workspace {
26 pub tree: Node, // Tiled windows
27 pub floating: Vec<FloatingWindow>, // Floating windows
28 pub focused: Option<WindowId>,
29 // ...
30 }
31
32 pub struct FloatingWindow {
33 pub id: WindowId,
34 pub geometry: Rect,
35 pub z_order: u32,
36 }
37 ```
38
39 ### 5.2 Toggle Floating
40 - [ ] Implement Mod+Shift+Space to toggle
41 - [ ] When floating: remove from tree, add to floating list
42 - [ ] When tiling: remove from floating list, insert to tree
43 - [ ] Preserve position when floating
44 - [ ] Use smart split when returning to tiled
45
46 ```rust
47 fn toggle_floating(&mut self, window: WindowId) {
48 if self.is_floating(window) {
49 // Move to tiled
50 let float_win = self.floating.remove(/* ... */);
51 self.tree.insert(window);
52 } else {
53 // Move to floating
54 let geometry = self.get_window_geometry(window);
55 self.tree.remove(window);
56 self.floating.push(FloatingWindow {
57 id: window,
58 geometry,
59 z_order: self.next_z_order(),
60 });
61 }
62 }
63 ```
64
65 ### 5.3 Mouse Move (Mod+LeftClick)
66 - [ ] Grab Mod+Button1 on all windows
67 - [ ] On press: record start position
68 - [ ] On motion: update window position
69 - [ ] On release: finalize position
70 - [ ] Only works for floating windows
71
72 ```rust
73 fn handle_button_press(&mut self, event: ButtonPressEvent) {
74 if event.detail == 1 && event.state.contains(ModMask::M4) {
75 // Start move operation
76 self.drag_state = Some(DragState::Move {
77 window: event.child,
78 start_x: event.root_x,
79 start_y: event.root_y,
80 start_geometry: self.get_geometry(event.child),
81 });
82 self.conn.grab_pointer(/* ... */)?;
83 }
84 }
85
86 fn handle_motion(&mut self, event: MotionNotifyEvent) {
87 if let Some(DragState::Move { window, start_x, start_y, start_geometry }) = &self.drag_state {
88 let dx = event.root_x - start_x;
89 let dy = event.root_y - start_y;
90 let new_x = start_geometry.x + dx;
91 let new_y = start_geometry.y + dy;
92 self.configure_window(window, new_x, new_y, None, None)?;
93 }
94 }
95 ```
96
97 ### 5.4 Mouse Resize (Mod+RightClick)
98 - [ ] Grab Mod+Button3 on all windows
99 - [ ] Determine resize corner based on click position
100 - [ ] On motion: adjust size (and position for top/left edges)
101 - [ ] Enforce minimum window size
102 - [ ] Only works for floating windows
103
104 ```rust
105 fn determine_resize_edge(window_geometry: Rect, click_x: i16, click_y: i16) -> ResizeEdge {
106 let center_x = window_geometry.x + window_geometry.width as i16 / 2;
107 let center_y = window_geometry.y + window_geometry.height as i16 / 2;
108
109 match (click_x < center_x, click_y < center_y) {
110 (true, true) => ResizeEdge::TopLeft,
111 (false, true) => ResizeEdge::TopRight,
112 (true, false) => ResizeEdge::BottomLeft,
113 (false, false) => ResizeEdge::BottomRight,
114 }
115 }
116 ```
117
118 ### 5.5 Auto-Float Rules
119 - [ ] Float `_NET_WM_WINDOW_TYPE_DIALOG`
120 - [ ] Float `_NET_WM_WINDOW_TYPE_SPLASH`
121 - [ ] Float `_NET_WM_WINDOW_TYPE_TOOLTIP`
122 - [ ] Float `_NET_WM_WINDOW_TYPE_UTILITY`
123 - [ ] Float windows with WM_TRANSIENT_FOR set
124 - [ ] Respect size hints for floating position
125
126 ```rust
127 fn should_float(conn: &impl Connection, window: Window) -> Result<bool> {
128 let window_type = get_window_type(conn, window)?;
129
130 let float_types = [
131 atoms._NET_WM_WINDOW_TYPE_DIALOG,
132 atoms._NET_WM_WINDOW_TYPE_SPLASH,
133 atoms._NET_WM_WINDOW_TYPE_TOOLTIP,
134 atoms._NET_WM_WINDOW_TYPE_UTILITY,
135 atoms._NET_WM_WINDOW_TYPE_POPUP_MENU,
136 ];
137
138 if float_types.contains(&window_type) {
139 return Ok(true);
140 }
141
142 // Check transient
143 if get_transient_for(conn, window)?.is_some() {
144 return Ok(true);
145 }
146
147 Ok(false)
148 }
149 ```
150
151 ### 5.6 Stacking Order
152 - [ ] Floating windows above tiled
153 - [ ] Most recently focused floating on top
154 - [ ] Use `ConfigureWindow` with `stack_mode` and `sibling`
155 - [ ] Maintain stacking order on focus changes
156
157 ```rust
158 fn raise_window(&self, conn: &impl Connection, window: Window) -> Result<()> {
159 conn.configure_window(
160 window,
161 &ConfigureWindowAux::new().stack_mode(StackMode::ABOVE),
162 )?;
163 Ok(())
164 }
165
166 fn restack_floating(&self, conn: &impl Connection) -> Result<()> {
167 // Stack in z_order, lowest first
168 let mut sorted: Vec<_> = self.floating.iter().collect();
169 sorted.sort_by_key(|w| w.z_order);
170
171 for (i, window) in sorted.iter().enumerate() {
172 if i > 0 {
173 conn.configure_window(
174 window.id,
175 &ConfigureWindowAux::new()
176 .stack_mode(StackMode::ABOVE)
177 .sibling(sorted[i - 1].id),
178 )?;
179 }
180 }
181 Ok(())
182 }
183 ```
184
185 ### 5.7 Floating Focus
186 - [ ] Tab cycles through floating windows
187 - [ ] Focus follows click for floating
188 - [ ] Raise on focus
189 - [ ] Handle focus between tiled and floating
190
191 ```rust
192 fn focus_next_floating(&mut self) {
193 if self.floating.is_empty() {
194 return;
195 }
196
197 let current_idx = self.floating
198 .iter()
199 .position(|w| Some(w.id) == self.focused)
200 .unwrap_or(0);
201
202 let next_idx = (current_idx + 1) % self.floating.len();
203 let next_window = self.floating[next_idx].id;
204
205 self.set_focus(next_window);
206 self.raise_window(next_window);
207 }
208 ```
209
210 ### 5.8 Lua Integration
211 - [ ] Add `floating` property to window rules
212 - [ ] Add `gar.toggle_floating()` function
213 - [ ] Add `gar.float_geometry(x, y, w, h)` for rules
214
215 ```lua
216 -- Float Firefox PiP
217 gar.rule(
218 { class = "firefox", title = "Picture-in-Picture" },
219 { floating = true, geometry = { x = 100, y = 100, width = 400, height = 300 } }
220 )
221
222 -- Keybind
223 gar.bind("mod+shift+space", gar.toggle_floating)
224 ```
225
226 ## Keybind Summary
227
228 | Keybind | Action |
229 |---------|--------|
230 | Mod+Shift+Space | Toggle floating |
231 | Mod+LeftClick drag | Move floating window |
232 | Mod+RightClick drag | Resize floating window |
233
234 ## Acceptance Criteria
235
236 1. Mod+Shift+Space toggles between tiled/floating
237 2. Can drag floating windows with Mod+LeftClick
238 3. Can resize floating windows with Mod+RightClick
239 4. Dialogs automatically float
240 5. Floating windows stay above tiled
241 6. Focus works correctly between tiled/floating
242
243 ## Testing Strategy
244
245 ```bash
246 # Test toggle
247 # Open xterm, verify tiled
248 # Mod+Shift+Space, verify floating (doesn't affect layout)
249 # Mod+Shift+Space again, verify back in tile
250
251 # Test mouse
252 # Float a window
253 # Mod+LeftClick drag - should move
254 # Mod+RightClick drag - should resize
255
256 # Test auto-float
257 # Open a dialog (e.g., Firefox preferences)
258 # Should automatically float
259
260 # Test stacking
261 # Have tiled windows
262 # Float one
263 # Floating should be on top
264 ```
265
266 ## Notes
267
268 - Consider minimum size constraints
269 - Handle fullscreen windows (Sprint 8)
270 - Floating geometry should be remembered when toggling
271 - Resize should work from any edge (not just corner)