@@ -16,6 +16,7 @@ use crate::config::{Config, LuaConfig, LuaState, RuleActions, WindowMatch}; |
| 16 | 16 | use crate::ipc::IpcServer; |
| 17 | 17 | use crate::x11::Connection; |
| 18 | 18 | use crate::x11::events::DragState; |
| 19 | +use crate::x11::FrameManager; |
| 19 | 20 | use crate::Result; |
| 20 | 21 | |
| 21 | 22 | pub struct WindowManager { |
@@ -34,6 +35,8 @@ pub struct WindowManager { |
| 34 | 35 | pub ipc_server: Option<IpcServer>, |
| 35 | 36 | /// Timestamp of last pointer warp - used to suppress EnterNotify feedback loop |
| 36 | 37 | pub last_warp: std::time::Instant, |
| 38 | + /// Frame manager for title bars |
| 39 | + pub frames: FrameManager, |
| 37 | 40 | } |
| 38 | 41 | |
| 39 | 42 | impl WindowManager { |
@@ -87,16 +90,12 @@ impl WindowManager { |
| 87 | 90 | )); |
| 88 | 91 | } |
| 89 | 92 | |
| 90 | | - // Assign workspaces to monitors |
| 91 | | - let ws_count = workspaces.len(); |
| 92 | | - let mon_count = monitors.len(); |
| 93 | + // i3-style: each monitor starts with one workspace (1, 2, 3...) |
| 94 | + // Any workspace can be moved to any monitor dynamically |
| 93 | 95 | for (i, monitor) in monitors.iter_mut().enumerate() { |
| 94 | | - // Distribute workspaces: first monitor gets ws 1-N/M, etc. |
| 95 | | - let start = i * ws_count / mon_count; |
| 96 | | - let end = (i + 1) * ws_count / mon_count; |
| 97 | | - monitor.workspaces = (start..end).collect(); |
| 98 | | - monitor.active_workspace = start; |
| 99 | | - tracing::debug!("Monitor '{}' assigned workspaces {:?}", monitor.name, monitor.workspaces); |
| 96 | + monitor.workspaces = vec![i]; // Just track initial workspace |
| 97 | + monitor.active_workspace = i; // Monitor 0 shows ws 0, monitor 1 shows ws 1, etc. |
| 98 | + tracing::debug!("Monitor '{}' starts with workspace {}", monitor.name, i + 1); |
| 100 | 99 | } |
| 101 | 100 | |
| 102 | 101 | Ok(Self { |
@@ -114,6 +113,7 @@ impl WindowManager { |
| 114 | 113 | drag_state: None, |
| 115 | 114 | ipc_server, |
| 116 | 115 | last_warp: std::time::Instant::now(), |
| 116 | + frames: FrameManager::new(), |
| 117 | 117 | }) |
| 118 | 118 | } |
| 119 | 119 | |
@@ -141,14 +141,14 @@ impl WindowManager { |
| 141 | 141 | .unwrap_or_else(|| Rect::new(0, 0, self.conn.screen_width, self.conn.screen_height)) |
| 142 | 142 | } |
| 143 | 143 | |
| 144 | | - /// Find which monitor a workspace belongs to. |
| 144 | + /// Find which monitor is currently displaying a workspace (i3-style). |
| 145 | 145 | pub fn monitor_for_workspace(&self, workspace_idx: usize) -> Option<&Monitor> { |
| 146 | | - self.monitors.iter().find(|m| m.workspaces.contains(&workspace_idx)) |
| 146 | + self.monitors.iter().find(|m| m.active_workspace == workspace_idx) |
| 147 | 147 | } |
| 148 | 148 | |
| 149 | | - /// Find the monitor index for a workspace. |
| 149 | + /// Find the monitor index currently displaying a workspace (i3-style). |
| 150 | 150 | pub fn monitor_idx_for_workspace(&self, workspace_idx: usize) -> Option<usize> { |
| 151 | | - self.monitors.iter().position(|m| m.workspaces.contains(&workspace_idx)) |
| 151 | + self.monitors.iter().position(|m| m.active_workspace == workspace_idx) |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | /// Refresh monitors (called on RandR screen change). |
@@ -168,15 +168,27 @@ impl WindowManager { |
| 168 | 168 | )); |
| 169 | 169 | } |
| 170 | 170 | |
| 171 | | - // Reassign workspaces to monitors |
| 172 | | - let ws_count = self.workspaces.len(); |
| 173 | | - let mon_count = new_monitors.len(); |
| 171 | + // Try to preserve workspace assignments from old monitors |
| 172 | + // If we have more monitors now, new ones get next available workspaces |
| 173 | + let old_mon_count = self.monitors.len(); |
| 174 | + let mut used_workspaces: std::collections::HashSet<usize> = std::collections::HashSet::new(); |
| 175 | + |
| 174 | 176 | for (i, monitor) in new_monitors.iter_mut().enumerate() { |
| 175 | | - let start = i * ws_count / mon_count; |
| 176 | | - let end = (i + 1) * ws_count / mon_count; |
| 177 | | - monitor.workspaces = (start..end).collect(); |
| 178 | | - monitor.active_workspace = start; |
| 179 | | - tracing::info!("Monitor '{}' assigned workspaces {:?}", monitor.name, monitor.workspaces); |
| 177 | + if i < old_mon_count { |
| 178 | + // Preserve old monitor's workspace |
| 179 | + monitor.active_workspace = self.monitors[i].active_workspace; |
| 180 | + monitor.workspaces = vec![monitor.active_workspace]; |
| 181 | + used_workspaces.insert(monitor.active_workspace); |
| 182 | + } else { |
| 183 | + // New monitor - assign first unused workspace |
| 184 | + let first_free = (0..self.workspaces.len()) |
| 185 | + .find(|ws| !used_workspaces.contains(ws)) |
| 186 | + .unwrap_or(0); |
| 187 | + monitor.active_workspace = first_free; |
| 188 | + monitor.workspaces = vec![first_free]; |
| 189 | + used_workspaces.insert(first_free); |
| 190 | + } |
| 191 | + tracing::info!("Monitor '{}' showing workspace {}", monitor.name, monitor.active_workspace + 1); |
| 180 | 192 | } |
| 181 | 193 | |
| 182 | 194 | self.monitors = new_monitors; |
@@ -186,10 +198,8 @@ impl WindowManager { |
| 186 | 198 | self.focused_monitor = 0; |
| 187 | 199 | } |
| 188 | 200 | |
| 189 | | - // Ensure focused_workspace is on the focused monitor |
| 190 | | - if !self.monitors[self.focused_monitor].workspaces.contains(&self.focused_workspace) { |
| 191 | | - self.focused_workspace = self.monitors[self.focused_monitor].active_workspace; |
| 192 | | - } |
| 201 | + // Update focused_workspace to match the focused monitor |
| 202 | + self.focused_workspace = self.monitors[self.focused_monitor].active_workspace; |
| 193 | 203 | |
| 194 | 204 | // Re-apply layout for visible workspaces |
| 195 | 205 | self.apply_layout()?; |
@@ -233,6 +243,9 @@ impl WindowManager { |
| 233 | 243 | .insert_with_rect(window, focused, screen); |
| 234 | 244 | self.current_workspace_mut().focused = Some(window); |
| 235 | 245 | self.focused_window = Some(window); |
| 246 | + |
| 247 | + // Update EWMH client lists |
| 248 | + self.update_client_lists(); |
| 236 | 249 | } |
| 237 | 250 | |
| 238 | 251 | /// Add a window to management on a specific workspace. |
@@ -254,6 +267,9 @@ impl WindowManager { |
| 254 | 267 | let focused = self.workspaces[workspace_idx].focused; |
| 255 | 268 | let screen = self.screen_rect(); |
| 256 | 269 | self.workspaces[workspace_idx].tree.insert_with_rect(window, focused, screen); |
| 270 | + |
| 271 | + // Update EWMH client lists |
| 272 | + self.update_client_lists(); |
| 257 | 273 | } |
| 258 | 274 | |
| 259 | 275 | /// Add a window to management as a floating window on a specific workspace. |
@@ -282,6 +298,9 @@ impl WindowManager { |
| 282 | 298 | |
| 283 | 299 | // Add to target workspace's floating list |
| 284 | 300 | self.workspaces[workspace_idx].add_floating(window); |
| 301 | + |
| 302 | + // Update EWMH client lists |
| 303 | + self.update_client_lists(); |
| 285 | 304 | } |
| 286 | 305 | |
| 287 | 306 | /// Add a window to management as a floating window. |
@@ -312,6 +331,50 @@ impl WindowManager { |
| 312 | 331 | self.current_workspace_mut().add_floating(window); |
| 313 | 332 | self.current_workspace_mut().focused = Some(window); |
| 314 | 333 | self.focused_window = Some(window); |
| 334 | + |
| 335 | + // Update EWMH client lists |
| 336 | + self.update_client_lists(); |
| 337 | + } |
| 338 | + |
| 339 | + /// Create a frame for a window if title bars are enabled. |
| 340 | + /// Returns the frame window ID if created. |
| 341 | + pub fn create_frame_for_window(&mut self, window: XWindow) -> Option<XWindow> { |
| 342 | + if !self.config.titlebar_enabled { |
| 343 | + return None; |
| 344 | + } |
| 345 | + |
| 346 | + // Get window title for display |
| 347 | + let title = self.conn.get_window_title(window).unwrap_or_default(); |
| 348 | + |
| 349 | + // Create frame with initial geometry (will be updated by apply_layout) |
| 350 | + let screen = self.screen_rect(); |
| 351 | + let frame = match self.frames.create_frame( |
| 352 | + &self.conn.conn, |
| 353 | + self.conn.root, |
| 354 | + window, |
| 355 | + screen.x, |
| 356 | + screen.y, |
| 357 | + 400, // Initial width, will be adjusted |
| 358 | + 300, // Initial height, will be adjusted |
| 359 | + self.config.titlebar_height as u16, |
| 360 | + self.config.border_width as u16, |
| 361 | + self.config.border_color_unfocused, |
| 362 | + self.config.titlebar_color_unfocused, |
| 363 | + ) { |
| 364 | + Ok(f) => f, |
| 365 | + Err(e) => { |
| 366 | + tracing::error!("Failed to create frame for window {}: {}", window, e); |
| 367 | + return None; |
| 368 | + } |
| 369 | + }; |
| 370 | + |
| 371 | + // Update window state with frame and title |
| 372 | + if let Some(win) = self.windows.get_mut(&window) { |
| 373 | + win.frame = Some(frame); |
| 374 | + win.title = title; |
| 375 | + } |
| 376 | + |
| 377 | + Some(frame) |
| 315 | 378 | } |
| 316 | 379 | |
| 317 | 380 | /// Remove a window from management. |
@@ -320,6 +383,13 @@ impl WindowManager { |
| 320 | 383 | let ws_idx = win.workspace; |
| 321 | 384 | tracing::info!("Unmanaging window {} from workspace {}", window, ws_idx + 1); |
| 322 | 385 | |
| 386 | + // Destroy frame if it exists |
| 387 | + if win.frame.is_some() { |
| 388 | + if let Err(e) = self.frames.destroy_frame(&self.conn.conn, self.conn.root, window) { |
| 389 | + tracing::warn!("Failed to destroy frame for window {}: {}", window, e); |
| 390 | + } |
| 391 | + } |
| 392 | + |
| 323 | 393 | // Remove from the window's actual workspace (not current_workspace!) |
| 324 | 394 | if win.floating { |
| 325 | 395 | self.workspaces[ws_idx].remove_floating(window); |
@@ -334,6 +404,22 @@ impl WindowManager { |
| 334 | 404 | .or_else(|| self.workspaces[ws_idx].floating.last().copied()); |
| 335 | 405 | self.workspaces[ws_idx].focused = self.focused_window; |
| 336 | 406 | } |
| 407 | + |
| 408 | + // Update EWMH client lists |
| 409 | + self.update_client_lists(); |
| 410 | + } |
| 411 | + } |
| 412 | + |
| 413 | + /// Update _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING on root window. |
| 414 | + pub fn update_client_lists(&self) { |
| 415 | + let windows: Vec<u32> = self.windows.keys().copied().collect(); |
| 416 | + if let Err(e) = self.conn.update_client_list(&windows) { |
| 417 | + tracing::warn!("Failed to update client list: {}", e); |
| 418 | + } |
| 419 | + // For stacking order, we use the same list for now (tiling WM doesn't have true stacking) |
| 420 | + // A more sophisticated implementation would order by focus history |
| 421 | + if let Err(e) = self.conn.update_client_list_stacking(&windows) { |
| 422 | + tracing::warn!("Failed to update client list stacking: {}", e); |
| 337 | 423 | } |
| 338 | 424 | } |
| 339 | 425 | |
@@ -371,6 +457,15 @@ impl WindowManager { |
| 371 | 457 | pub fn set_focus(&mut self, window: XWindow) -> Result<()> { |
| 372 | 458 | self.focused_window = Some(window); |
| 373 | 459 | self.current_workspace_mut().focused = Some(window); |
| 460 | + |
| 461 | + // Clear urgency when window receives focus |
| 462 | + if let Some(win) = self.windows.get_mut(&window) { |
| 463 | + if win.urgent { |
| 464 | + tracing::debug!("Clearing urgency for window {} on focus", window); |
| 465 | + win.urgent = false; |
| 466 | + } |
| 467 | + } |
| 468 | + |
| 374 | 469 | self.conn.set_focus(window)?; |
| 375 | 470 | self.conn.set_active_window(Some(window))?; |
| 376 | 471 | self.update_borders()?; |
@@ -385,6 +480,69 @@ impl WindowManager { |
| 385 | 480 | Ok(()) |
| 386 | 481 | } |
| 387 | 482 | |
| 483 | + /// Toggle fullscreen state for a window. |
| 484 | + pub fn toggle_fullscreen(&mut self, window: XWindow) -> Result<()> { |
| 485 | + let win = match self.windows.get_mut(&window) { |
| 486 | + Some(w) => w, |
| 487 | + None => return Ok(()), |
| 488 | + }; |
| 489 | + |
| 490 | + let ws_idx = win.workspace; |
| 491 | + |
| 492 | + if win.fullscreen { |
| 493 | + // Exit fullscreen - restore previous state |
| 494 | + tracing::info!("Window {} exiting fullscreen", window); |
| 495 | + |
| 496 | + win.fullscreen = false; |
| 497 | + |
| 498 | + // Restore previous floating state |
| 499 | + let was_floating = win.pre_fullscreen_floating; |
| 500 | + if was_floating != win.floating { |
| 501 | + if was_floating { |
| 502 | + // Was floating before - remove from tree, add to floating |
| 503 | + self.workspaces[ws_idx].tree.remove(window); |
| 504 | + self.workspaces[ws_idx].add_floating(window); |
| 505 | + } else { |
| 506 | + // Was tiled before - remove from floating, add to tree |
| 507 | + self.workspaces[ws_idx].remove_floating(window); |
| 508 | + let focused = self.workspaces[ws_idx].focused; |
| 509 | + let screen = self.screen_rect(); |
| 510 | + self.workspaces[ws_idx].tree.insert_with_rect(window, focused, screen); |
| 511 | + } |
| 512 | + if let Some(w) = self.windows.get_mut(&window) { |
| 513 | + w.floating = was_floating; |
| 514 | + } |
| 515 | + } |
| 516 | + |
| 517 | + // Clear EWMH fullscreen state |
| 518 | + let _ = self.conn.set_window_state(window, &[]); |
| 519 | + } else { |
| 520 | + // Enter fullscreen |
| 521 | + tracing::info!("Window {} entering fullscreen", window); |
| 522 | + |
| 523 | + // Save current state |
| 524 | + win.pre_fullscreen_floating = win.floating; |
| 525 | + win.fullscreen = true; |
| 526 | + |
| 527 | + // Set EWMH fullscreen state |
| 528 | + let _ = self.conn.set_window_state(window, &[self.conn.net_wm_state_fullscreen]); |
| 529 | + } |
| 530 | + |
| 531 | + // Re-apply layout (fullscreen windows get special treatment in apply_layout) |
| 532 | + self.apply_layout()?; |
| 533 | + self.conn.flush()?; |
| 534 | + Ok(()) |
| 535 | + } |
| 536 | + |
| 537 | + /// Set fullscreen state for a window explicitly (for EWMH client messages). |
| 538 | + pub fn set_fullscreen(&mut self, window: XWindow, fullscreen: bool) -> Result<()> { |
| 539 | + let is_fullscreen = self.windows.get(&window).map(|w| w.fullscreen).unwrap_or(false); |
| 540 | + if is_fullscreen != fullscreen { |
| 541 | + self.toggle_fullscreen(window)?; |
| 542 | + } |
| 543 | + Ok(()) |
| 544 | + } |
| 545 | + |
| 388 | 546 | /// Warp pointer to center of a monitor (for focus without windows) |
| 389 | 547 | pub fn warp_to_monitor(&mut self, monitor_idx: usize) -> Result<()> { |
| 390 | 548 | let geom = self.monitors[monitor_idx].geometry; |
@@ -403,11 +561,12 @@ impl WindowManager { |
| 403 | 561 | Ok(()) |
| 404 | 562 | } |
| 405 | 563 | |
| 406 | | - /// Update border colors for all visible windows based on focus state. |
| 564 | + /// Update border colors for all visible windows based on focus and urgency state. |
| 407 | 565 | pub fn update_borders(&mut self) -> Result<()> { |
| 408 | 566 | let focused = self.focused_window; |
| 409 | 567 | let focused_color = self.config.border_color_focused; |
| 410 | 568 | let unfocused_color = self.config.border_color_unfocused; |
| 569 | + let urgent_color = self.config.border_color_urgent; |
| 411 | 570 | let border_width = self.config.border_width; |
| 412 | 571 | |
| 413 | 572 | // Get all visible workspace indices |
@@ -416,12 +575,25 @@ impl WindowManager { |
| 416 | 575 | // Update borders for all windows on visible workspaces |
| 417 | 576 | for ws_idx in visible_ws { |
| 418 | 577 | for window in self.workspaces[ws_idx].all_windows() { |
| 419 | | - let color = if Some(window) == focused { |
| 578 | + // Check if window is urgent (and not focused - focused clears urgency) |
| 579 | + let is_urgent = self.windows.get(&window) |
| 580 | + .map(|w| w.urgent && Some(window) != focused) |
| 581 | + .unwrap_or(false); |
| 582 | + |
| 583 | + let color = if is_urgent { |
| 584 | + urgent_color |
| 585 | + } else if Some(window) == focused { |
| 420 | 586 | focused_color |
| 421 | 587 | } else { |
| 422 | 588 | unfocused_color |
| 423 | 589 | }; |
| 424 | | - self.conn.set_border(window, border_width, color)?; |
| 590 | + |
| 591 | + // If window has a frame, set border on the frame instead |
| 592 | + if self.windows.get(&window).and_then(|w| w.frame).is_some() { |
| 593 | + self.frames.set_frame_border(&self.conn.conn, window, color)?; |
| 594 | + } else { |
| 595 | + self.conn.set_border(window, border_width, color)?; |
| 596 | + } |
| 425 | 597 | } |
| 426 | 598 | } |
| 427 | 599 | Ok(()) |
@@ -446,6 +618,37 @@ impl WindowManager { |
| 446 | 618 | |
| 447 | 619 | // Layout each monitor's active workspace |
| 448 | 620 | for (ws_idx, screen) in &visible_workspaces { |
| 621 | + // Check for fullscreen windows on this workspace |
| 622 | + let fullscreen_windows: Vec<XWindow> = self.windows.iter() |
| 623 | + .filter(|(_, w)| w.workspace == *ws_idx && w.fullscreen) |
| 624 | + .map(|(id, _)| *id) |
| 625 | + .collect(); |
| 626 | + |
| 627 | + // If there's a fullscreen window, it takes the whole monitor |
| 628 | + if let Some(&fs_window) = fullscreen_windows.first() { |
| 629 | + tracing::debug!( |
| 630 | + "apply_layout: FULLSCREEN window={} on monitor {:?}", |
| 631 | + fs_window, screen |
| 632 | + ); |
| 633 | + |
| 634 | + // Configure fullscreen window to cover entire monitor (no gaps, no borders) |
| 635 | + self.conn.configure_window( |
| 636 | + fs_window, |
| 637 | + screen.x, |
| 638 | + screen.y, |
| 639 | + screen.width, |
| 640 | + screen.height, |
| 641 | + 0, // No border for fullscreen |
| 642 | + )?; |
| 643 | + |
| 644 | + // Raise fullscreen window above everything |
| 645 | + let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE); |
| 646 | + self.conn.conn.configure_window(fs_window, &aux)?; |
| 647 | + |
| 648 | + // Skip normal layout for this workspace - fullscreen window covers everything |
| 649 | + continue; |
| 650 | + } |
| 651 | + |
| 449 | 652 | let work_area = Rect::new( |
| 450 | 653 | screen.x + gap_outer, |
| 451 | 654 | screen.y + gap_outer, |
@@ -460,6 +663,10 @@ impl WindowManager { |
| 460 | 663 | ws.tree.window_count(), ws.floating.len() |
| 461 | 664 | ); |
| 462 | 665 | |
| 666 | + // Get titlebar settings |
| 667 | + let titlebar_enabled = self.config.titlebar_enabled; |
| 668 | + let titlebar_height = self.config.titlebar_height as u16; |
| 669 | + |
| 463 | 670 | // 1. Configure tiled windows from the BSP tree |
| 464 | 671 | let geometries = ws.tree.calculate_geometries(work_area); |
| 465 | 672 | for (window, rect) in &geometries { |
@@ -473,19 +680,42 @@ impl WindowManager { |
| 473 | 680 | let final_width = gapped_width.saturating_sub(2 * border_width as u16); |
| 474 | 681 | let final_height = gapped_height.saturating_sub(2 * border_width as u16); |
| 475 | 682 | |
| 476 | | - tracing::debug!( |
| 477 | | - "apply_layout: TILED window={} at ({}, {}) size {}x{}", |
| 478 | | - window, gapped_x, gapped_y, final_width.max(1), final_height.max(1) |
| 479 | | - ); |
| 683 | + // Check if window has a frame |
| 684 | + let has_frame = self.windows.get(window).and_then(|w| w.frame).is_some(); |
| 685 | + |
| 686 | + if has_frame && titlebar_enabled { |
| 687 | + // Configure frame (includes titlebar height) |
| 688 | + let client_height = final_height.saturating_sub(titlebar_height); |
| 689 | + self.frames.configure_frame( |
| 690 | + &self.conn.conn, |
| 691 | + *window, |
| 692 | + gapped_x, |
| 693 | + gapped_y, |
| 694 | + final_width.max(1), |
| 695 | + client_height.max(1), |
| 696 | + titlebar_height, |
| 697 | + border_width as u16, |
| 698 | + )?; |
| 480 | 699 | |
| 481 | | - self.conn.configure_window( |
| 482 | | - *window, |
| 483 | | - gapped_x, |
| 484 | | - gapped_y, |
| 485 | | - final_width.max(1), |
| 486 | | - final_height.max(1), |
| 487 | | - border_width, |
| 488 | | - )?; |
| 700 | + tracing::debug!( |
| 701 | + "apply_layout: TILED+FRAME window={} at ({}, {}) size {}x{} (titlebar: {})", |
| 702 | + window, gapped_x, gapped_y, final_width.max(1), final_height.max(1), titlebar_height |
| 703 | + ); |
| 704 | + } else { |
| 705 | + tracing::debug!( |
| 706 | + "apply_layout: TILED window={} at ({}, {}) size {}x{}", |
| 707 | + window, gapped_x, gapped_y, final_width.max(1), final_height.max(1) |
| 708 | + ); |
| 709 | + |
| 710 | + self.conn.configure_window( |
| 711 | + *window, |
| 712 | + gapped_x, |
| 713 | + gapped_y, |
| 714 | + final_width.max(1), |
| 715 | + final_height.max(1), |
| 716 | + border_width, |
| 717 | + )?; |
| 718 | + } |
| 489 | 719 | } |
| 490 | 720 | |
| 491 | 721 | // 2. Configure floating windows and stack them above tiled |
@@ -498,24 +728,52 @@ impl WindowManager { |
| 498 | 728 | let adjusted_width = geom.width.saturating_sub(2 * border_width as u16); |
| 499 | 729 | let adjusted_height = geom.height.saturating_sub(2 * border_width as u16); |
| 500 | 730 | |
| 501 | | - tracing::debug!( |
| 502 | | - "apply_layout: FLOATING window={} at ({}, {}) size {}x{} (raising)", |
| 503 | | - window_id, geom.x, geom.y, adjusted_width.max(1), adjusted_height.max(1) |
| 504 | | - ); |
| 505 | | - |
| 506 | | - // Configure geometry |
| 507 | | - self.conn.configure_window( |
| 508 | | - window_id, |
| 509 | | - geom.x, |
| 510 | | - geom.y, |
| 511 | | - adjusted_width.max(1), |
| 512 | | - adjusted_height.max(1), |
| 513 | | - border_width, |
| 514 | | - )?; |
| 515 | | - |
| 516 | | - // Raise to top of stack (each subsequent window goes above the previous) |
| 517 | | - let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE); |
| 518 | | - self.conn.conn.configure_window(window_id, &aux)?; |
| 731 | + let has_frame = win.frame.is_some(); |
| 732 | + |
| 733 | + if has_frame && titlebar_enabled { |
| 734 | + // Configure frame for floating window |
| 735 | + let client_height = adjusted_height.saturating_sub(titlebar_height); |
| 736 | + self.frames.configure_frame( |
| 737 | + &self.conn.conn, |
| 738 | + window_id, |
| 739 | + geom.x, |
| 740 | + geom.y, |
| 741 | + adjusted_width.max(1), |
| 742 | + client_height.max(1), |
| 743 | + titlebar_height, |
| 744 | + border_width as u16, |
| 745 | + )?; |
| 746 | + |
| 747 | + // Raise frame to top of stack |
| 748 | + if let Some(frame) = self.frames.frame_for_client(window_id) { |
| 749 | + let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE); |
| 750 | + self.conn.conn.configure_window(frame, &aux)?; |
| 751 | + } |
| 752 | + |
| 753 | + tracing::debug!( |
| 754 | + "apply_layout: FLOATING+FRAME window={} at ({}, {}) size {}x{} (raising)", |
| 755 | + window_id, geom.x, geom.y, adjusted_width.max(1), adjusted_height.max(1) |
| 756 | + ); |
| 757 | + } else { |
| 758 | + tracing::debug!( |
| 759 | + "apply_layout: FLOATING window={} at ({}, {}) size {}x{} (raising)", |
| 760 | + window_id, geom.x, geom.y, adjusted_width.max(1), adjusted_height.max(1) |
| 761 | + ); |
| 762 | + |
| 763 | + // Configure geometry |
| 764 | + self.conn.configure_window( |
| 765 | + window_id, |
| 766 | + geom.x, |
| 767 | + geom.y, |
| 768 | + adjusted_width.max(1), |
| 769 | + adjusted_height.max(1), |
| 770 | + border_width, |
| 771 | + )?; |
| 772 | + |
| 773 | + // Raise to top of stack (each subsequent window goes above the previous) |
| 774 | + let aux = ConfigureWindowAux::new().stack_mode(StackMode::ABOVE); |
| 775 | + self.conn.conn.configure_window(window_id, &aux)?; |
| 776 | + } |
| 519 | 777 | } else { |
| 520 | 778 | tracing::warn!("apply_layout: floating window {} not in windows map!", window_id); |
| 521 | 779 | } |