gardesk/gar / 9239558

Browse files

Add floating window move/resize with mod+drag and edge hover

- Support both Alt and Super mod keys for drag operations
- Add resize cursors for all edges and corners
- Change cursor on hover near floating window edges
- Click and drag edges to resize without mod key
- Keep button grabs on floating windows for edge detection
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
923955868720226f505166ce5205a2245b99900b
Parents
34fe404
Tree
9558cb8

3 changed files

StatusFile+-
M gar/src/core/mod.rs 8 1
M gar/src/x11/connection.rs 115 32
M gar/src/x11/events.rs 274 45
gar/src/core/mod.rsmodified
@@ -43,6 +43,8 @@ pub struct WindowManager {
43
     pub focus_history: Vec<XWindow>,
43
     pub focus_history: Vec<XWindow>,
44
     /// Struts from dock windows (status bars) - maps window ID to strut
44
     /// Struts from dock windows (status bars) - maps window ID to strut
45
     pub dock_struts: HashMap<XWindow, Strut>,
45
     pub dock_struts: HashMap<XWindow, Strut>,
46
+    /// Current edge being displayed (for cursor changes on floating window edges)
47
+    pub current_edge_cursor: Option<(XWindow, crate::x11::events::ResizeEdge)>,
46
 }
48
 }
47
 
49
 
48
 impl WindowManager {
50
 impl WindowManager {
@@ -132,6 +134,7 @@ impl WindowManager {
132
             frames: FrameManager::new(),
134
             frames: FrameManager::new(),
133
             focus_history: Vec::new(),
135
             focus_history: Vec::new(),
134
             dock_struts: HashMap::new(),
136
             dock_struts: HashMap::new(),
137
+            current_edge_cursor: None,
135
         })
138
         })
136
     }
139
     }
137
 
140
 
@@ -495,7 +498,11 @@ impl WindowManager {
495
         self.current_workspace_mut().focused = Some(window);
498
         self.current_workspace_mut().focused = Some(window);
496
 
499
 
497
         // Ungrab buttons on new focused window (allow clicks through)
500
         // Ungrab buttons on new focused window (allow clicks through)
498
-        let _ = self.conn.ungrab_button(window);
501
+        // But keep grabs on floating windows so we can detect edge resize clicks
502
+        let is_floating = self.windows.get(&window).map_or(false, |w| w.floating);
503
+        if !is_floating {
504
+            let _ = self.conn.ungrab_button(window);
505
+        }
499
 
506
 
500
         // Update focus history - move window to front
507
         // Update focus history - move window to front
501
         self.focus_history.retain(|&w| w != window);
508
         self.focus_history.retain(|&w| w != window);
gar/src/x11/connection.rsmodified
@@ -83,6 +83,16 @@ pub struct Connection {
83
     // Struts (reserved screen areas for docks/panels)
83
     // Struts (reserved screen areas for docks/panels)
84
     pub net_wm_strut: Atom,
84
     pub net_wm_strut: Atom,
85
     pub net_wm_strut_partial: Atom,
85
     pub net_wm_strut_partial: Atom,
86
+    // Cursors for resize operations
87
+    pub cursor_normal: u32,
88
+    pub cursor_top_left: u32,
89
+    pub cursor_top_right: u32,
90
+    pub cursor_bottom_left: u32,
91
+    pub cursor_bottom_right: u32,
92
+    pub cursor_left: u32,
93
+    pub cursor_right: u32,
94
+    pub cursor_top: u32,
95
+    pub cursor_bottom: u32,
86
 }
96
 }
87
 
97
 
88
 impl Connection {
98
 impl Connection {
@@ -142,6 +152,19 @@ impl Connection {
142
         let net_wm_strut = conn.intern_atom(false, b"_NET_WM_STRUT")?.reply()?.atom;
152
         let net_wm_strut = conn.intern_atom(false, b"_NET_WM_STRUT")?.reply()?.atom;
143
         let net_wm_strut_partial = conn.intern_atom(false, b"_NET_WM_STRUT_PARTIAL")?.reply()?.atom;
153
         let net_wm_strut_partial = conn.intern_atom(false, b"_NET_WM_STRUT_PARTIAL")?.reply()?.atom;
144
 
154
 
155
+        // Create cursors for pointer and resize operations
156
+        let (
157
+            cursor_normal,
158
+            cursor_top_left,
159
+            cursor_top_right,
160
+            cursor_bottom_left,
161
+            cursor_bottom_right,
162
+            cursor_left,
163
+            cursor_right,
164
+            cursor_top,
165
+            cursor_bottom,
166
+        ) = Self::create_cursors(&conn)?;
167
+
145
         tracing::info!(
168
         tracing::info!(
146
             "Connected to X server, screen {}x{}",
169
             "Connected to X server, screen {}x{}",
147
             screen_width,
170
             screen_width,
@@ -185,6 +208,15 @@ impl Connection {
185
             net_wm_bypass_compositor,
208
             net_wm_bypass_compositor,
186
             net_wm_strut,
209
             net_wm_strut,
187
             net_wm_strut_partial,
210
             net_wm_strut_partial,
211
+            cursor_normal,
212
+            cursor_top_left,
213
+            cursor_top_right,
214
+            cursor_bottom_left,
215
+            cursor_bottom_right,
216
+            cursor_left,
217
+            cursor_right,
218
+            cursor_top,
219
+            cursor_bottom,
188
         })
220
         })
189
     }
221
     }
190
 
222
 
@@ -193,10 +225,6 @@ impl Connection {
193
     }
225
     }
194
 
226
 
195
     pub fn become_wm(&self) -> Result<(), Error> {
227
     pub fn become_wm(&self) -> Result<(), Error> {
196
-        // Create a normal pointer cursor for the root window
197
-        // This prevents the ugly X cursor when no windows are focused
198
-        let cursor = self.create_cursor()?;
199
-
200
         // Set root window background to black, cursor, and subscribe to events
228
         // Set root window background to black, cursor, and subscribe to events
201
         // The background ensures old window pixels are cleared when windows close
229
         // The background ensures old window pixels are cleared when windows close
202
         let change = ChangeWindowAttributesAux::new()
230
         let change = ChangeWindowAttributesAux::new()
@@ -207,7 +235,7 @@ impl Connection {
207
                     | EventMask::PROPERTY_CHANGE,
235
                     | EventMask::PROPERTY_CHANGE,
208
             )
236
             )
209
             .background_pixel(self.screen().black_pixel)
237
             .background_pixel(self.screen().black_pixel)
210
-            .cursor(cursor);
238
+            .cursor(self.cursor_normal);
211
 
239
 
212
         let result = self
240
         let result = self
213
             .conn
241
             .conn
@@ -254,29 +282,55 @@ impl Connection {
254
         }
282
         }
255
     }
283
     }
256
 
284
 
257
-    /// Create a left pointer cursor from the cursor font.
285
+    /// Create all cursors used by the window manager.
258
-    fn create_cursor(&self) -> Result<u32, Error> {
286
+    fn create_cursors(conn: &RustConnection) -> Result<(u32, u32, u32, u32, u32, u32, u32, u32, u32), Error> {
259
         // Open the cursor font
287
         // Open the cursor font
260
-        let font: Font = self.conn.generate_id()?;
288
+        let font: Font = conn.generate_id()?;
261
-        self.conn.open_font(font, b"cursor")?;
289
+        conn.open_font(font, b"cursor")?;
262
-
290
+
263
-        // Create cursor from font glyphs
291
+        // Cursor glyph numbers from the cursor font:
264
-        // left_ptr is glyph 68, its mask is glyph 69
292
+        // left_ptr = 68, top_left_corner = 134, top_right_corner = 136
265
-        let cursor = self.conn.generate_id()?;
293
+        // bottom_left_corner = 12, bottom_right_corner = 14
266
-        self.conn.create_glyph_cursor(
294
+        // left_side = 70, right_side = 96, top_side = 138, bottom_side = 16
267
-            cursor,
295
+
268
-            font,
296
+        let create = |glyph: u16| -> Result<u32, Error> {
269
-            font,
297
+            let cursor = conn.generate_id()?;
270
-            68,  // left_ptr glyph
298
+            conn.create_glyph_cursor(
271
-            69,  // mask glyph
299
+                cursor,
272
-            0, 0, 0,           // foreground RGB (black)
300
+                font,
273
-            0xFFFF, 0xFFFF, 0xFFFF,  // background RGB (white)
301
+                font,
274
-        )?;
302
+                glyph,
275
-
303
+                glyph + 1,
276
-        // Close font (cursor keeps its own reference)
304
+                0, 0, 0,
277
-        self.conn.close_font(font)?;
305
+                0xFFFF, 0xFFFF, 0xFFFF,
278
-
306
+            )?;
279
-        Ok(cursor)
307
+            Ok(cursor)
308
+        };
309
+
310
+        let cursor_normal = create(68)?;       // left_ptr
311
+        let cursor_top_left = create(134)?;    // top_left_corner
312
+        let cursor_top_right = create(136)?;   // top_right_corner
313
+        let cursor_bottom_left = create(12)?;  // bottom_left_corner
314
+        let cursor_bottom_right = create(14)?; // bottom_right_corner
315
+        let cursor_left = create(70)?;         // left_side
316
+        let cursor_right = create(96)?;        // right_side
317
+        let cursor_top = create(138)?;         // top_side
318
+        let cursor_bottom = create(16)?;       // bottom_side
319
+
320
+        // Close font (cursors keep their own references)
321
+        conn.close_font(font)?;
322
+
323
+        Ok((
324
+            cursor_normal,
325
+            cursor_top_left,
326
+            cursor_top_right,
327
+            cursor_bottom_left,
328
+            cursor_bottom_right,
329
+            cursor_left,
330
+            cursor_right,
331
+            cursor_top,
332
+            cursor_bottom,
333
+        ))
280
     }
334
     }
281
 
335
 
282
     /// Grab a key combination on the root window.
336
     /// Grab a key combination on the root window.
@@ -330,13 +384,15 @@ impl Connection {
330
         Ok(())
384
         Ok(())
331
     }
385
     }
332
 
386
 
333
-    /// Grab Alt+Button1 and Alt+Button3 on root for floating window move/resize.
387
+    /// Grab Mod+Button1 and Mod+Button3 on root for floating window move/resize.
388
+    /// Grabs both Alt (M1) and Super (M4) to support either mod key configuration.
334
     pub fn grab_mod_buttons(&self) -> Result<(), Error> {
389
     pub fn grab_mod_buttons(&self) -> Result<(), Error> {
335
         let numlock = ModMask::M2;
390
         let numlock = ModMask::M2;
336
         let capslock = ModMask::LOCK;
391
         let capslock = ModMask::LOCK;
337
 
392
 
338
-        // Grab Alt+Button1 (move) and Alt+Button3 (resize) with NumLock/CapsLock variants
393
+        // Grab both Alt (M1) and Super (M4) with Button1 (move) and Button3 (resize)
339
         for button in [ButtonIndex::M1, ButtonIndex::M3] {
394
         for button in [ButtonIndex::M1, ButtonIndex::M3] {
395
+            // Alt variants
340
             for mods in [
396
             for mods in [
341
                 ModMask::M1,
397
                 ModMask::M1,
342
                 ModMask::M1 | numlock,
398
                 ModMask::M1 | numlock,
@@ -355,8 +411,34 @@ impl Connection {
355
                     mods,
411
                     mods,
356
                 )?;
412
                 )?;
357
             }
413
             }
414
+            // Super variants
415
+            for mods in [
416
+                ModMask::M4,
417
+                ModMask::M4 | numlock,
418
+                ModMask::M4 | capslock,
419
+                ModMask::M4 | numlock | capslock,
420
+            ] {
421
+                self.conn.grab_button(
422
+                    false,
423
+                    self.root,
424
+                    EventMask::BUTTON_PRESS | EventMask::BUTTON_RELEASE | EventMask::BUTTON_MOTION,
425
+                    GrabMode::ASYNC,
426
+                    GrabMode::ASYNC,
427
+                    x11rb::NONE,
428
+                    x11rb::NONE,
429
+                    button,
430
+                    mods,
431
+                )?;
432
+            }
358
         }
433
         }
359
-        tracing::debug!("Grabbed Alt+Button1/Button3 on root for floating move/resize");
434
+        tracing::debug!("Grabbed Mod+Button1/Button3 on root for floating move/resize");
435
+        Ok(())
436
+    }
437
+
438
+    /// Set the cursor for a window.
439
+    pub fn set_window_cursor(&self, window: Window, cursor: u32) -> Result<(), Error> {
440
+        let change = ChangeWindowAttributesAux::new().cursor(cursor);
441
+        self.conn.change_window_attributes(window, &change)?;
360
         Ok(())
442
         Ok(())
361
     }
443
     }
362
 
444
 
@@ -448,7 +530,7 @@ impl Connection {
448
 
530
 
449
     /// Grab the pointer for drag operations.
531
     /// Grab the pointer for drag operations.
450
     pub fn grab_pointer(&self, _window: Window) -> Result<(), Error> {
532
     pub fn grab_pointer(&self, _window: Window) -> Result<(), Error> {
451
-        self.conn.grab_pointer(
533
+        let reply = self.conn.grab_pointer(
452
             false,
534
             false,
453
             self.root,
535
             self.root,
454
             EventMask::BUTTON_RELEASE | EventMask::POINTER_MOTION,
536
             EventMask::BUTTON_RELEASE | EventMask::POINTER_MOTION,
@@ -457,7 +539,8 @@ impl Connection {
457
             x11rb::NONE,
539
             x11rb::NONE,
458
             x11rb::NONE,
540
             x11rb::NONE,
459
             CURRENT_TIME,
541
             CURRENT_TIME,
460
-        )?;
542
+        )?.reply()?;
543
+        tracing::debug!("grab_pointer result: {:?}", reply.status);
461
         Ok(())
544
         Ok(())
462
     }
545
     }
463
 
546
 
gar/src/x11/events.rsmodified
@@ -31,12 +31,17 @@ pub enum DragState {
31
     },
31
     },
32
 }
32
 }
33
 
33
 
34
-#[derive(Debug, Clone, Copy)]
34
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35
 pub enum ResizeEdge {
35
 pub enum ResizeEdge {
36
     TopLeft,
36
     TopLeft,
37
+    Top,
37
     TopRight,
38
     TopRight,
39
+    Left,
40
+    Right,
38
     BottomLeft,
41
     BottomLeft,
42
+    Bottom,
39
     BottomRight,
43
     BottomRight,
44
+    None,
40
 }
45
 }
41
 
46
 
42
 impl WindowManager {
47
 impl WindowManager {
@@ -124,22 +129,26 @@ impl WindowManager {
124
                 continue;
129
                 continue;
125
             }
130
             }
126
 
131
 
132
+            // Check window rules and EWMH hints
133
+            let rule_actions = self.check_rules(window);
134
+            let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window));
135
+
127
             // Subscribe to events on the window
136
             // Subscribe to events on the window
128
-            self.conn.select_input(
137
+            // Floating windows get POINTER_MOTION for edge resize cursors
129
-                window,
138
+            let base_events = EventMask::ENTER_WINDOW
130
-                EventMask::ENTER_WINDOW
139
+                | EventMask::FOCUS_CHANGE
131
-                    | EventMask::FOCUS_CHANGE
140
+                | EventMask::PROPERTY_CHANGE
132
-                    | EventMask::PROPERTY_CHANGE
141
+                | EventMask::STRUCTURE_NOTIFY;
133
-                    | EventMask::STRUCTURE_NOTIFY,
142
+            let events = if should_float {
134
-            )?;
143
+                base_events | EventMask::POINTER_MOTION
144
+            } else {
145
+                base_events
146
+            };
147
+            self.conn.select_input(window, events)?;
135
 
148
 
136
             // Grab button for click-to-focus
149
             // Grab button for click-to-focus
137
             self.conn.grab_button(window)?;
150
             self.conn.grab_button(window)?;
138
 
151
 
139
-            // Check window rules and EWMH hints
140
-            let rule_actions = self.check_rules(window);
141
-            let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window));
142
-
143
             // Manage the window
152
             // Manage the window
144
             if should_float {
153
             if should_float {
145
                 self.manage_window_floating(window);
154
                 self.manage_window_floating(window);
@@ -154,7 +163,10 @@ impl WindowManager {
154
             // Focus the first window
163
             // Focus the first window
155
             if let Some(window) = self.focused_window {
164
             if let Some(window) = self.focused_window {
156
                 self.set_focus(window, true)?;
165
                 self.set_focus(window, true)?;
157
-                self.conn.ungrab_button(window)?;
166
+                // Ungrab button for click-through (unless floating - keep for edge resize)
167
+                if !self.is_floating(window) {
168
+                    self.conn.ungrab_button(window)?;
169
+                }
158
             }
170
             }
159
             tracing::info!("Adopted {} existing windows", adopted);
171
             tracing::info!("Adopted {} existing windows", adopted);
160
         }
172
         }
@@ -233,18 +245,6 @@ impl WindowManager {
233
             return Ok(());
245
             return Ok(());
234
         }
246
         }
235
 
247
 
236
-        // Subscribe to events on the window
237
-        self.conn.select_input(
238
-            window,
239
-            EventMask::ENTER_WINDOW
240
-                | EventMask::FOCUS_CHANGE
241
-                | EventMask::PROPERTY_CHANGE
242
-                | EventMask::STRUCTURE_NOTIFY,
243
-        )?;
244
-
245
-        // Grab button for click-to-focus
246
-        self.conn.grab_button(window)?;
247
-
248
         // Check window rules first
248
         // Check window rules first
249
         let rule_actions = self.check_rules(window);
249
         let rule_actions = self.check_rules(window);
250
 
250
 
@@ -255,6 +255,22 @@ impl WindowManager {
255
         // Determine if window should float (rule > ICCCM/EWMH hints)
255
         // Determine if window should float (rule > ICCCM/EWMH hints)
256
         let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window));
256
         let should_float = rule_actions.floating.unwrap_or_else(|| self.conn.should_float(window));
257
 
257
 
258
+        // Subscribe to events on the window
259
+        // Floating windows get POINTER_MOTION for edge resize cursors
260
+        let base_events = EventMask::ENTER_WINDOW
261
+            | EventMask::FOCUS_CHANGE
262
+            | EventMask::PROPERTY_CHANGE
263
+            | EventMask::STRUCTURE_NOTIFY;
264
+        let events = if should_float {
265
+            base_events | EventMask::POINTER_MOTION
266
+        } else {
267
+            base_events
268
+        };
269
+        self.conn.select_input(window, events)?;
270
+
271
+        // Grab button for click-to-focus
272
+        self.conn.grab_button(window)?;
273
+
258
         // Manage window on target workspace
274
         // Manage window on target workspace
259
         if target_idx != self.focused_workspace {
275
         if target_idx != self.focused_workspace {
260
             // Window goes to a different workspace
276
             // Window goes to a different workspace
@@ -403,8 +419,16 @@ impl WindowManager {
403
         let child = event.child;
419
         let child = event.child;
404
         tracing::debug!("ButtonPress on window {}, child {}, button {}", window, child, event.detail);
420
         tracing::debug!("ButtonPress on window {}, child {}, button {}", window, child, event.detail);
405
 
421
 
422
+        // If we're already in a drag, ignore additional button presses
423
+        if self.drag_state.is_some() {
424
+            tracing::debug!("Already in drag state, ignoring ButtonPress");
425
+            return Ok(());
426
+        }
427
+
406
         // Check for mod+click on floating windows (move/resize)
428
         // Check for mod+click on floating windows (move/resize)
407
-        let has_mod = event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD1);
429
+        // Support both Alt (MOD1) and Super (MOD4) as the modifier
430
+        let has_mod = event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD1)
431
+            || event.state.contains(x11rb::protocol::xproto::KeyButMask::MOD4);
408
 
432
 
409
         // For Alt+click from root grab, use child window (the window under cursor)
433
         // For Alt+click from root grab, use child window (the window under cursor)
410
         let target = if window == self.conn.root && child != 0 {
434
         let target = if window == self.conn.root && child != 0 {
@@ -429,8 +453,8 @@ impl WindowManager {
429
                 self.conn.grab_pointer(target)?;
453
                 self.conn.grab_pointer(target)?;
430
                 return Ok(());
454
                 return Ok(());
431
             } else if event.detail == 3 {
455
             } else if event.detail == 3 {
432
-                // Mod+Button3 = Resize
456
+                // Mod+Button3 = Resize (quadrant-based edge detection)
433
-                let edge = determine_resize_edge(&geometry, event.root_x, event.root_y);
457
+                let edge = determine_resize_edge_quadrant(&geometry, event.root_x, event.root_y);
434
                 tracing::debug!("Starting resize for floating window {}, edge {:?}", target, edge);
458
                 tracing::debug!("Starting resize for floating window {}, edge {:?}", target, edge);
435
                 self.drag_state = Some(DragState::Resize {
459
                 self.drag_state = Some(DragState::Resize {
436
                     window: target,
460
                     window: target,
@@ -444,6 +468,45 @@ impl WindowManager {
444
             }
468
             }
445
         }
469
         }
446
 
470
 
471
+        // Check for edge resize on floating windows (click on edge without mod key)
472
+        let is_floating_win = self.is_floating(window);
473
+        tracing::debug!(
474
+            "Edge resize check: window={}, is_floating={}, button={}, pos=({},{})",
475
+            window, is_floating_win, event.detail, event.root_x, event.root_y
476
+        );
477
+
478
+        if !has_mod && is_floating_win && event.detail == 1 {
479
+            let geometry = self.get_floating_geometry(window);
480
+            let edge = determine_resize_edge(&geometry, event.root_x, event.root_y);
481
+            tracing::debug!(
482
+                "Edge detection: geometry=({},{} {}x{}), edge={:?}",
483
+                geometry.x, geometry.y, geometry.width, geometry.height, edge
484
+            );
485
+            if edge != ResizeEdge::None {
486
+                tracing::info!("Starting edge resize for floating window {}, edge {:?}", window, edge);
487
+                self.drag_state = Some(DragState::Resize {
488
+                    window,
489
+                    start_x: event.root_x,
490
+                    start_y: event.root_y,
491
+                    start_geometry: geometry.clone(),
492
+                    edge,
493
+                });
494
+                tracing::debug!("Grabbing pointer for resize, geometry={:?}", geometry);
495
+                self.conn.grab_pointer(window)?;
496
+                self.conn.flush()?;
497
+                return Ok(());
498
+            }
499
+            // Not on edge - if this is a focused floating window, replay the click
500
+            if self.focused_window == Some(window) {
501
+                self.conn.conn.allow_events(
502
+                    x11rb::protocol::xproto::Allow::REPLAY_POINTER,
503
+                    x11rb::CURRENT_TIME,
504
+                )?;
505
+                self.conn.flush()?;
506
+                return Ok(());
507
+            }
508
+        }
509
+
447
         // Only handle if we manage this window
510
         // Only handle if we manage this window
448
         if !self.windows.contains_key(&window) {
511
         if !self.windows.contains_key(&window) {
449
             return Ok(());
512
             return Ok(());
@@ -451,14 +514,21 @@ impl WindowManager {
451
 
514
 
452
         // Focus the clicked window
515
         // Focus the clicked window
453
         if self.focused_window != Some(window) {
516
         if self.focused_window != Some(window) {
454
-            // Regrab button on old focused window
517
+            // Regrab button on old focused window (unless it's floating - keep grab for edge resize)
455
             if let Some(old) = self.focused_window {
518
             if let Some(old) = self.focused_window {
456
-                self.conn.grab_button(old)?;
519
+                if !self.is_floating(old) {
520
+                    self.conn.grab_button(old)?;
521
+                }
457
             }
522
             }
458
 
523
 
459
-            // Set focus and ungrab button on new focused window (no warp - mouse click)
524
+            // Set focus
460
             self.set_focus(window, false)?;
525
             self.set_focus(window, false)?;
461
-            self.conn.ungrab_button(window)?;
526
+
527
+            // For non-floating windows, ungrab button for click-through
528
+            // For floating windows, keep grab for edge resize detection
529
+            if !self.is_floating(window) {
530
+                self.conn.ungrab_button(window)?;
531
+            }
462
 
532
 
463
             // Raise floating windows on focus
533
             // Raise floating windows on focus
464
             if self.is_floating(window) {
534
             if self.is_floating(window) {
@@ -476,7 +546,9 @@ impl WindowManager {
476
         Ok(())
546
         Ok(())
477
     }
547
     }
478
 
548
 
479
-    fn handle_button_release(&mut self, _event: ButtonReleaseEvent) -> Result<()> {
549
+    fn handle_button_release(&mut self, event: ButtonReleaseEvent) -> Result<()> {
550
+        tracing::debug!("ButtonRelease: button={}, window={}, in_drag={}",
551
+            event.detail, event.event, self.drag_state.is_some());
480
         if self.drag_state.is_some() {
552
         if self.drag_state.is_some() {
481
             tracing::debug!("Ending drag operation");
553
             tracing::debug!("Ending drag operation");
482
             self.drag_state = None;
554
             self.drag_state = None;
@@ -487,10 +559,24 @@ impl WindowManager {
487
     }
559
     }
488
 
560
 
489
     fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<()> {
561
     fn handle_motion_notify(&mut self, event: MotionNotifyEvent) -> Result<()> {
490
-        let Some(ref drag) = self.drag_state else {
562
+        // If not in a drag, check for edge cursor changes on floating windows
563
+        if self.drag_state.is_none() {
564
+            let window = event.event;
565
+            let is_managed = self.windows.contains_key(&window);
566
+            let is_float = is_managed && self.is_floating(window);
567
+
568
+            if is_float {
569
+                tracing::trace!(
570
+                    "Motion on floating window {}: root({},{}) event({},{})",
571
+                    window, event.root_x, event.root_y, event.event_x, event.event_y
572
+                );
573
+                self.update_edge_cursor(window, event.root_x, event.root_y)?;
574
+            }
491
             return Ok(());
575
             return Ok(());
492
-        };
576
+        }
493
 
577
 
578
+        let drag = self.drag_state.as_ref().unwrap();
579
+        tracing::debug!("Motion during drag: root({},{}), drag_state={:?}", event.root_x, event.root_y, drag);
494
         match drag {
580
         match drag {
495
             DragState::Move {
581
             DragState::Move {
496
                 window,
582
                 window,
@@ -531,6 +617,53 @@ impl WindowManager {
531
         Ok(())
617
         Ok(())
532
     }
618
     }
533
 
619
 
620
+    /// Handle pointer motion for edge cursor changes on floating windows.
621
+    fn update_edge_cursor(&mut self, window: u32, root_x: i16, root_y: i16) -> Result<()> {
622
+        if !self.is_floating(window) {
623
+            // Not a floating window, ensure cursor is normal
624
+            if self.current_edge_cursor.is_some() {
625
+                self.conn.set_window_cursor(window, self.conn.cursor_normal)?;
626
+                self.current_edge_cursor = None;
627
+            }
628
+            return Ok(());
629
+        }
630
+
631
+        let geometry = self.get_floating_geometry(window);
632
+        let edge = determine_resize_edge(&geometry, root_x, root_y);
633
+
634
+        // Check if we need to update the cursor
635
+        let current = self.current_edge_cursor;
636
+        if current.map(|(w, e)| (w, e)) == Some((window, edge)) {
637
+            return Ok(()); // No change needed
638
+        }
639
+
640
+        // Get the appropriate cursor for this edge
641
+        let cursor = match edge {
642
+            ResizeEdge::TopLeft => self.conn.cursor_top_left,
643
+            ResizeEdge::Top => self.conn.cursor_top,
644
+            ResizeEdge::TopRight => self.conn.cursor_top_right,
645
+            ResizeEdge::Left => self.conn.cursor_left,
646
+            ResizeEdge::Right => self.conn.cursor_right,
647
+            ResizeEdge::BottomLeft => self.conn.cursor_bottom_left,
648
+            ResizeEdge::Bottom => self.conn.cursor_bottom,
649
+            ResizeEdge::BottomRight => self.conn.cursor_bottom_right,
650
+            ResizeEdge::None => self.conn.cursor_normal,
651
+        };
652
+
653
+        // Set cursor on the window
654
+        self.conn.set_window_cursor(window, cursor)?;
655
+        self.conn.flush()?;
656
+
657
+        if edge != ResizeEdge::None {
658
+            tracing::debug!("Set resize cursor {:?} for floating window {} edge", edge, window);
659
+            self.current_edge_cursor = Some((window, edge));
660
+        } else {
661
+            self.current_edge_cursor = None;
662
+        }
663
+
664
+        Ok(())
665
+    }
666
+
534
     fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> {
667
     fn handle_enter_notify(&mut self, event: EnterNotifyEvent) -> Result<()> {
535
         let window = event.event;
668
         let window = event.event;
536
 
669
 
@@ -563,14 +696,20 @@ impl WindowManager {
563
 
696
 
564
         tracing::debug!("Focus follows mouse: focusing window {}", window);
697
         tracing::debug!("Focus follows mouse: focusing window {}", window);
565
 
698
 
566
-        // Regrab button on old focused window
699
+        // Regrab button on old focused window (unless floating - keep for edge resize)
567
         if let Some(old) = self.focused_window {
700
         if let Some(old) = self.focused_window {
568
-            self.conn.grab_button(old)?;
701
+            if !self.is_floating(old) {
702
+                self.conn.grab_button(old)?;
703
+            }
569
         }
704
         }
570
 
705
 
571
         // Focus the new window (no warp - mouse enter)
706
         // Focus the new window (no warp - mouse enter)
572
         self.set_focus(window, false)?;
707
         self.set_focus(window, false)?;
573
-        self.conn.ungrab_button(window)?;
708
+
709
+        // Ungrab button for click-through (unless floating - keep for edge resize)
710
+        if !self.is_floating(window) {
711
+            self.conn.ungrab_button(window)?;
712
+        }
574
 
713
 
575
         // Raise floating windows on focus
714
         // Raise floating windows on focus
576
         if self.is_floating(window) {
715
         if self.is_floating(window) {
@@ -968,11 +1107,17 @@ impl WindowManager {
968
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
1107
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
969
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
1108
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
970
         {
1109
         {
1110
+            // Regrab on old window (unless floating)
971
             if let Some(old) = self.focused_window {
1111
             if let Some(old) = self.focused_window {
972
-                self.conn.grab_button(old)?;
1112
+                if !self.is_floating(old) {
1113
+                    self.conn.grab_button(old)?;
1114
+                }
973
             }
1115
             }
974
             self.set_focus(window, true)?;
1116
             self.set_focus(window, true)?;
975
-            self.conn.ungrab_button(window)?;
1117
+            // Ungrab on new window (unless floating - keep for edge resize)
1118
+            if !self.is_floating(window) {
1119
+                self.conn.ungrab_button(window)?;
1120
+            }
976
         } else {
1121
         } else {
977
             // No windows on target monitor - clear focus and warp to monitor center
1122
             // No windows on target monitor - clear focus and warp to monitor center
978
             self.focused_window = None;
1123
             self.focused_window = None;
@@ -1646,11 +1791,17 @@ impl WindowManager {
1646
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
1791
             .or_else(|| self.workspaces[workspace_idx].floating.last().copied())
1647
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
1792
             .or_else(|| self.workspaces[workspace_idx].tree.first_window())
1648
         {
1793
         {
1794
+            // Regrab on old window (unless floating)
1649
             if let Some(old) = self.focused_window {
1795
             if let Some(old) = self.focused_window {
1650
-                self.conn.grab_button(old)?;
1796
+                if !self.is_floating(old) {
1797
+                    self.conn.grab_button(old)?;
1798
+                }
1651
             }
1799
             }
1652
             self.set_focus(window, true)?;
1800
             self.set_focus(window, true)?;
1653
-            self.conn.ungrab_button(window)?;
1801
+            // Ungrab on new window (unless floating - keep for edge resize)
1802
+            if !self.is_floating(window) {
1803
+                self.conn.ungrab_button(window)?;
1804
+            }
1654
         } else {
1805
         } else {
1655
             // No windows - warp to monitor center
1806
             // No windows - warp to monitor center
1656
             self.focused_window = None;
1807
             self.focused_window = None;
@@ -1761,14 +1912,16 @@ impl WindowManager {
1761
 
1912
 
1762
         let next_window = floating[next_idx];
1913
         let next_window = floating[next_idx];
1763
 
1914
 
1764
-        // Regrab button on old focused window
1915
+        // Regrab button on old focused window (unless it's floating)
1765
         if let Some(old) = self.focused_window {
1916
         if let Some(old) = self.focused_window {
1766
-            self.conn.grab_button(old)?;
1917
+            if !self.is_floating(old) {
1918
+                self.conn.grab_button(old)?;
1919
+            }
1767
         }
1920
         }
1768
 
1921
 
1769
         // Focus and raise the next floating window (keyboard action, warp pointer)
1922
         // Focus and raise the next floating window (keyboard action, warp pointer)
1923
+        // Keep button grabbed on floating windows for edge resize
1770
         self.set_focus(next_window, true)?;
1924
         self.set_focus(next_window, true)?;
1771
-        self.conn.ungrab_button(next_window)?;
1772
         self.raise_window(next_window)?;
1925
         self.raise_window(next_window)?;
1773
 
1926
 
1774
         tracing::debug!("Cycled to floating window {} (idx {})", next_window, next_idx);
1927
         tracing::debug!("Cycled to floating window {} (idx {})", next_window, next_idx);
@@ -1800,6 +1953,21 @@ impl WindowManager {
1800
                 win.floating = false;
1953
                 win.floating = false;
1801
             }
1954
             }
1802
 
1955
 
1956
+            // Remove POINTER_MOTION event mask (no longer need edge detection)
1957
+            self.conn.select_input(
1958
+                window,
1959
+                EventMask::ENTER_WINDOW
1960
+                    | EventMask::FOCUS_CHANGE
1961
+                    | EventMask::PROPERTY_CHANGE
1962
+                    | EventMask::STRUCTURE_NOTIFY,
1963
+            )?;
1964
+
1965
+            // Clear edge cursor state if this window had one
1966
+            if self.current_edge_cursor.map(|(w, _)| w) == Some(window) {
1967
+                self.conn.set_window_cursor(window, self.conn.cursor_normal)?;
1968
+                self.current_edge_cursor = None;
1969
+            }
1970
+
1803
             // Remove from floating list
1971
             // Remove from floating list
1804
             self.current_workspace_mut().remove_floating(window);
1972
             self.current_workspace_mut().remove_floating(window);
1805
 
1973
 
@@ -1842,6 +2010,20 @@ impl WindowManager {
1842
                 win.floating_geometry = geometry;
2010
                 win.floating_geometry = geometry;
1843
             }
2011
             }
1844
 
2012
 
2013
+            // Add POINTER_MOTION event mask for edge detection
2014
+            self.conn.select_input(
2015
+                window,
2016
+                EventMask::ENTER_WINDOW
2017
+                    | EventMask::FOCUS_CHANGE
2018
+                    | EventMask::PROPERTY_CHANGE
2019
+                    | EventMask::STRUCTURE_NOTIFY
2020
+                    | EventMask::POINTER_MOTION,
2021
+            )?;
2022
+
2023
+            // Re-establish button grabs for edge resize detection
2024
+            // (buttons may have been ungrabbed when the window was focused as tiled)
2025
+            self.conn.grab_button(window)?;
2026
+
1845
             // Add to floating list (on top)
2027
             // Add to floating list (on top)
1846
             self.current_workspace_mut().add_floating(window);
2028
             self.current_workspace_mut().add_floating(window);
1847
 
2029
 
@@ -2053,7 +2235,37 @@ fn parse_direction(s: &str) -> Option<Direction> {
2053
     }
2235
     }
2054
 }
2236
 }
2055
 
2237
 
2238
+/// Threshold in pixels for detecting edge proximity
2239
+const EDGE_THRESHOLD: i16 = 12;
2240
+
2241
+/// Determine which edge/corner of a window a point is near.
2242
+/// Returns ResizeEdge::None if not near any edge.
2056
 fn determine_resize_edge(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge {
2243
 fn determine_resize_edge(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge {
2244
+    let left = geometry.x;
2245
+    let right = geometry.x + geometry.width as i16;
2246
+    let top = geometry.y;
2247
+    let bottom = geometry.y + geometry.height as i16;
2248
+
2249
+    let near_left = click_x >= left && click_x < left + EDGE_THRESHOLD;
2250
+    let near_right = click_x > right - EDGE_THRESHOLD && click_x <= right;
2251
+    let near_top = click_y >= top && click_y < top + EDGE_THRESHOLD;
2252
+    let near_bottom = click_y > bottom - EDGE_THRESHOLD && click_y <= bottom;
2253
+
2254
+    match (near_left, near_right, near_top, near_bottom) {
2255
+        (true, _, true, _) => ResizeEdge::TopLeft,
2256
+        (_, true, true, _) => ResizeEdge::TopRight,
2257
+        (true, _, _, true) => ResizeEdge::BottomLeft,
2258
+        (_, true, _, true) => ResizeEdge::BottomRight,
2259
+        (true, _, _, _) => ResizeEdge::Left,
2260
+        (_, true, _, _) => ResizeEdge::Right,
2261
+        (_, _, true, _) => ResizeEdge::Top,
2262
+        (_, _, _, true) => ResizeEdge::Bottom,
2263
+        _ => ResizeEdge::None,
2264
+    }
2265
+}
2266
+
2267
+/// Determine resize edge for mod+click (quadrant-based, always picks a corner)
2268
+fn determine_resize_edge_quadrant(geometry: &Rect, click_x: i16, click_y: i16) -> ResizeEdge {
2057
     let center_x = geometry.x + geometry.width as i16 / 2;
2269
     let center_x = geometry.x + geometry.width as i16 / 2;
2058
     let center_y = geometry.y + geometry.height as i16 / 2;
2270
     let center_y = geometry.y + geometry.height as i16 / 2;
2059
 
2271
 
@@ -2082,20 +2294,37 @@ fn calculate_resize(geometry: &Rect, edge: ResizeEdge, dx: i16, dy: i16) -> (i16
2082
             w = (w as i16 - dx).max(MIN_SIZE as i16) as u16;
2294
             w = (w as i16 - dx).max(MIN_SIZE as i16) as u16;
2083
             h = (h as i16 - dy).max(MIN_SIZE as i16) as u16;
2295
             h = (h as i16 - dy).max(MIN_SIZE as i16) as u16;
2084
         }
2296
         }
2297
+        ResizeEdge::Top => {
2298
+            y += dy;
2299
+            h = (h as i16 - dy).max(MIN_SIZE as i16) as u16;
2300
+        }
2085
         ResizeEdge::TopRight => {
2301
         ResizeEdge::TopRight => {
2086
             y += dy;
2302
             y += dy;
2087
             w = (w as i16 + dx).max(MIN_SIZE as i16) as u16;
2303
             w = (w as i16 + dx).max(MIN_SIZE as i16) as u16;
2088
             h = (h as i16 - dy).max(MIN_SIZE as i16) as u16;
2304
             h = (h as i16 - dy).max(MIN_SIZE as i16) as u16;
2089
         }
2305
         }
2306
+        ResizeEdge::Left => {
2307
+            x += dx;
2308
+            w = (w as i16 - dx).max(MIN_SIZE as i16) as u16;
2309
+        }
2310
+        ResizeEdge::Right => {
2311
+            w = (w as i16 + dx).max(MIN_SIZE as i16) as u16;
2312
+        }
2090
         ResizeEdge::BottomLeft => {
2313
         ResizeEdge::BottomLeft => {
2091
             x += dx;
2314
             x += dx;
2092
             w = (w as i16 - dx).max(MIN_SIZE as i16) as u16;
2315
             w = (w as i16 - dx).max(MIN_SIZE as i16) as u16;
2093
             h = (h as i16 + dy).max(MIN_SIZE as i16) as u16;
2316
             h = (h as i16 + dy).max(MIN_SIZE as i16) as u16;
2094
         }
2317
         }
2318
+        ResizeEdge::Bottom => {
2319
+            h = (h as i16 + dy).max(MIN_SIZE as i16) as u16;
2320
+        }
2095
         ResizeEdge::BottomRight => {
2321
         ResizeEdge::BottomRight => {
2096
             w = (w as i16 + dx).max(MIN_SIZE as i16) as u16;
2322
             w = (w as i16 + dx).max(MIN_SIZE as i16) as u16;
2097
             h = (h as i16 + dy).max(MIN_SIZE as i16) as u16;
2323
             h = (h as i16 + dy).max(MIN_SIZE as i16) as u16;
2098
         }
2324
         }
2325
+        ResizeEdge::None => {
2326
+            // No resize
2327
+        }
2099
     }
2328
     }
2100
 
2329
 
2101
     (x, y, w, h)
2330
     (x, y, w, h)