@@ -1,7 +1,7 @@ |
| 1 | | -//! Frame window management for title bars. |
| 1 | +//! Frame window management for title bars and gradient borders. |
| 2 | 2 | //! |
| 3 | | -//! When title bars are enabled, client windows are reparented into frame windows. |
| 4 | | -//! The frame handles the title bar drawing and the client sits below it. |
| 3 | +//! When title bars or gradient borders are enabled, client windows are reparented into frame windows. |
| 4 | +//! The frame handles the title bar drawing, gradient border rendering, and the client sits below/inside it. |
| 5 | 5 | |
| 6 | 6 | use std::collections::HashMap; |
| 7 | 7 | |
@@ -130,11 +130,15 @@ impl FrameManager { |
| 130 | 130 | if let Some(frame) = self.client_to_frame.remove(&client) { |
| 131 | 131 | self.frame_to_client.remove(&frame); |
| 132 | 132 | |
| 133 | | - // Reparent client back to root |
| 134 | | - conn.reparent_window(client, root, 0, 0)?; |
| 133 | + // Unmap frame first to prevent visual flash of empty frame background |
| 134 | + let _ = conn.unmap_window(frame); |
| 135 | + |
| 136 | + // Reparent client back to root (may fail if client was already destroyed) |
| 137 | + // Ignore errors since the client might already be gone |
| 138 | + let _ = conn.reparent_window(client, root, 0, 0); |
| 135 | 139 | |
| 136 | 140 | // Destroy the frame window |
| 137 | | - conn.destroy_window(frame)?; |
| 141 | + let _ = conn.destroy_window(frame); |
| 138 | 142 | |
| 139 | 143 | tracing::debug!("Destroyed frame {} for client {}", frame, client); |
| 140 | 144 | } |
@@ -281,6 +285,346 @@ impl FrameManager { |
| 281 | 285 | pub fn all_frames(&self) -> impl Iterator<Item = Window> + '_ { |
| 282 | 286 | self.frame_to_client.keys().copied() |
| 283 | 287 | } |
| 288 | + |
| 289 | + /// Draw a gradient border around the frame. |
| 290 | + /// This draws the border area of the frame with a color gradient. |
| 291 | + /// The `direction` can be "vertical", "horizontal", or "diagonal". |
| 292 | + pub fn draw_gradient_border<C: X11Connection>( |
| 293 | + &mut self, |
| 294 | + conn: &C, |
| 295 | + client: Window, |
| 296 | + width: u16, |
| 297 | + height: u16, |
| 298 | + border_width: u16, |
| 299 | + start_color: u32, |
| 300 | + end_color: u32, |
| 301 | + direction: &str, |
| 302 | + ) -> Result<(), Error> { |
| 303 | + let Some(&frame) = self.client_to_frame.get(&client) else { |
| 304 | + return Ok(()); |
| 305 | + }; |
| 306 | + |
| 307 | + if border_width == 0 { |
| 308 | + return Ok(()); |
| 309 | + } |
| 310 | + |
| 311 | + // Ensure we have a GC |
| 312 | + let gc = match self.gc { |
| 313 | + Some(gc) => gc, |
| 314 | + None => { |
| 315 | + let gc = conn.generate_id()?; |
| 316 | + let aux = CreateGCAux::new().foreground(start_color); |
| 317 | + conn.create_gc(gc, frame, &aux)?; |
| 318 | + self.gc = Some(gc); |
| 319 | + gc |
| 320 | + } |
| 321 | + }; |
| 322 | + |
| 323 | + // Number of gradient steps for smooth appearance |
| 324 | + let steps: u16 = 32.min(border_width * 2); |
| 325 | + |
| 326 | + match direction { |
| 327 | + "vertical" => { |
| 328 | + // Draw gradient from top to bottom across the border areas |
| 329 | + // We draw along the sides |
| 330 | + let step_height = height / steps; |
| 331 | + for i in 0..steps { |
| 332 | + let t = i as f32 / (steps - 1).max(1) as f32; |
| 333 | + let color = interpolate_color(start_color, end_color, t); |
| 334 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 335 | + |
| 336 | + let y = (i * step_height) as i16; |
| 337 | + let seg_height = step_height.min(height.saturating_sub(i * step_height)); |
| 338 | + |
| 339 | + // Left border strip |
| 340 | + let left_rect = Rectangle { |
| 341 | + x: 0, |
| 342 | + y, |
| 343 | + width: border_width, |
| 344 | + height: seg_height, |
| 345 | + }; |
| 346 | + // Right border strip |
| 347 | + let right_rect = Rectangle { |
| 348 | + x: (width - border_width) as i16, |
| 349 | + y, |
| 350 | + width: border_width, |
| 351 | + height: seg_height, |
| 352 | + }; |
| 353 | + conn.poly_fill_rectangle(frame, gc, &[left_rect, right_rect])?; |
| 354 | + } |
| 355 | + |
| 356 | + // Top and bottom borders (full width gradient) |
| 357 | + let step_width = width / steps; |
| 358 | + for i in 0..steps { |
| 359 | + let t = i as f32 / (steps - 1).max(1) as f32; |
| 360 | + let color = interpolate_color(start_color, end_color, t); |
| 361 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 362 | + |
| 363 | + let x = (i * step_width) as i16; |
| 364 | + let seg_width = step_width.min(width.saturating_sub(i * step_width)); |
| 365 | + |
| 366 | + // Top border strip |
| 367 | + let top_rect = Rectangle { |
| 368 | + x, |
| 369 | + y: 0, |
| 370 | + width: seg_width, |
| 371 | + height: border_width, |
| 372 | + }; |
| 373 | + // Bottom border strip |
| 374 | + let bottom_rect = Rectangle { |
| 375 | + x, |
| 376 | + y: (height - border_width) as i16, |
| 377 | + width: seg_width, |
| 378 | + height: border_width, |
| 379 | + }; |
| 380 | + conn.poly_fill_rectangle(frame, gc, &[top_rect, bottom_rect])?; |
| 381 | + } |
| 382 | + } |
| 383 | + "horizontal" => { |
| 384 | + // Draw gradient from left to right across all border areas |
| 385 | + let step_width = width / steps; |
| 386 | + for i in 0..steps { |
| 387 | + let t = i as f32 / (steps - 1).max(1) as f32; |
| 388 | + let color = interpolate_color(start_color, end_color, t); |
| 389 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 390 | + |
| 391 | + let x = (i * step_width) as i16; |
| 392 | + let seg_width = step_width.min(width.saturating_sub(i * step_width)); |
| 393 | + |
| 394 | + // Top border |
| 395 | + let top_rect = Rectangle { |
| 396 | + x, |
| 397 | + y: 0, |
| 398 | + width: seg_width, |
| 399 | + height: border_width, |
| 400 | + }; |
| 401 | + // Bottom border |
| 402 | + let bottom_rect = Rectangle { |
| 403 | + x, |
| 404 | + y: (height - border_width) as i16, |
| 405 | + width: seg_width, |
| 406 | + height: border_width, |
| 407 | + }; |
| 408 | + conn.poly_fill_rectangle(frame, gc, &[top_rect, bottom_rect])?; |
| 409 | + } |
| 410 | + |
| 411 | + // Side borders |
| 412 | + let step_height = height / steps; |
| 413 | + for i in 0..steps { |
| 414 | + let t = i as f32 / (steps - 1).max(1) as f32; |
| 415 | + let color = interpolate_color(start_color, end_color, t); |
| 416 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 417 | + |
| 418 | + let y = (i * step_height) as i16; |
| 419 | + let seg_height = step_height.min(height.saturating_sub(i * step_height)); |
| 420 | + |
| 421 | + // Left border |
| 422 | + let left_rect = Rectangle { |
| 423 | + x: 0, |
| 424 | + y, |
| 425 | + width: border_width, |
| 426 | + height: seg_height, |
| 427 | + }; |
| 428 | + // Right border |
| 429 | + let right_rect = Rectangle { |
| 430 | + x: (width - border_width) as i16, |
| 431 | + y, |
| 432 | + width: border_width, |
| 433 | + height: seg_height, |
| 434 | + }; |
| 435 | + conn.poly_fill_rectangle(frame, gc, &[left_rect, right_rect])?; |
| 436 | + } |
| 437 | + } |
| 438 | + "diagonal" | _ => { |
| 439 | + // Draw a diagonal gradient (top-left to bottom-right) |
| 440 | + // We approximate by using the sum of x+y position |
| 441 | + let max_dist = (width + height) as f32; |
| 442 | + |
| 443 | + // Draw borders with diagonal gradient |
| 444 | + // Top border |
| 445 | + for x in 0..width { |
| 446 | + let t = x as f32 / max_dist; |
| 447 | + let color = interpolate_color(start_color, end_color, t); |
| 448 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 449 | + let rect = Rectangle { |
| 450 | + x: x as i16, |
| 451 | + y: 0, |
| 452 | + width: 1, |
| 453 | + height: border_width, |
| 454 | + }; |
| 455 | + conn.poly_fill_rectangle(frame, gc, &[rect])?; |
| 456 | + } |
| 457 | + // Bottom border |
| 458 | + for x in 0..width { |
| 459 | + let t = (x as f32 + (height - border_width) as f32) / max_dist; |
| 460 | + let color = interpolate_color(start_color, end_color, t.min(1.0)); |
| 461 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 462 | + let rect = Rectangle { |
| 463 | + x: x as i16, |
| 464 | + y: (height - border_width) as i16, |
| 465 | + width: 1, |
| 466 | + height: border_width, |
| 467 | + }; |
| 468 | + conn.poly_fill_rectangle(frame, gc, &[rect])?; |
| 469 | + } |
| 470 | + // Left border |
| 471 | + for y in border_width..(height - border_width) { |
| 472 | + let t = y as f32 / max_dist; |
| 473 | + let color = interpolate_color(start_color, end_color, t); |
| 474 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 475 | + let rect = Rectangle { |
| 476 | + x: 0, |
| 477 | + y: y as i16, |
| 478 | + width: border_width, |
| 479 | + height: 1, |
| 480 | + }; |
| 481 | + conn.poly_fill_rectangle(frame, gc, &[rect])?; |
| 482 | + } |
| 483 | + // Right border |
| 484 | + for y in border_width..(height - border_width) { |
| 485 | + let t = ((width - border_width) as f32 + y as f32) / max_dist; |
| 486 | + let color = interpolate_color(start_color, end_color, t.min(1.0)); |
| 487 | + conn.change_gc(gc, &x11rb::protocol::xproto::ChangeGCAux::new().foreground(color))?; |
| 488 | + let rect = Rectangle { |
| 489 | + x: (width - border_width) as i16, |
| 490 | + y: y as i16, |
| 491 | + width: border_width, |
| 492 | + height: 1, |
| 493 | + }; |
| 494 | + conn.poly_fill_rectangle(frame, gc, &[rect])?; |
| 495 | + } |
| 496 | + } |
| 497 | + } |
| 498 | + |
| 499 | + Ok(()) |
| 500 | + } |
| 501 | + |
| 502 | + /// Create a border frame (frame without titlebar, just for gradient borders). |
| 503 | + /// The frame acts as a border container with the client centered inside. |
| 504 | + pub fn create_border_frame<C: X11Connection>( |
| 505 | + &mut self, |
| 506 | + conn: &C, |
| 507 | + root: Window, |
| 508 | + client: Window, |
| 509 | + x: i16, |
| 510 | + y: i16, |
| 511 | + width: u16, |
| 512 | + height: u16, |
| 513 | + border_width: u16, |
| 514 | + bg_color: u32, |
| 515 | + ) -> Result<Window, Error> { |
| 516 | + // Frame size includes the border on all sides |
| 517 | + let frame_width = width + border_width * 2; |
| 518 | + let frame_height = height + border_width * 2; |
| 519 | + |
| 520 | + // Generate a new window ID for the frame |
| 521 | + let frame = conn.generate_id()?; |
| 522 | + |
| 523 | + // Create the frame window with no X11 border (we draw our own) |
| 524 | + let aux = CreateWindowAux::new() |
| 525 | + .event_mask( |
| 526 | + EventMask::SUBSTRUCTURE_REDIRECT |
| 527 | + | EventMask::SUBSTRUCTURE_NOTIFY |
| 528 | + | EventMask::BUTTON_PRESS |
| 529 | + | EventMask::BUTTON_RELEASE |
| 530 | + | EventMask::ENTER_WINDOW |
| 531 | + | EventMask::EXPOSURE, |
| 532 | + ) |
| 533 | + .background_pixel(bg_color) |
| 534 | + .border_pixel(0); |
| 535 | + |
| 536 | + conn.create_window( |
| 537 | + COPY_DEPTH_FROM_PARENT, |
| 538 | + frame, |
| 539 | + root, |
| 540 | + x, |
| 541 | + y, |
| 542 | + frame_width, |
| 543 | + frame_height, |
| 544 | + 0, // No X11 border - we draw gradients ourselves |
| 545 | + WindowClass::INPUT_OUTPUT, |
| 546 | + 0, // CopyFromParent visual |
| 547 | + &aux, |
| 548 | + )?; |
| 549 | + |
| 550 | + // Reparent the client window into the frame, inset by border_width |
| 551 | + conn.reparent_window(client, frame, border_width as i16, border_width as i16)?; |
| 552 | + |
| 553 | + // Configure the client to fill the center of the frame |
| 554 | + let client_aux = ConfigureWindowAux::new() |
| 555 | + .x(border_width as i32) |
| 556 | + .y(border_width as i32) |
| 557 | + .width(width as u32) |
| 558 | + .height(height as u32) |
| 559 | + .border_width(0); |
| 560 | + conn.configure_window(client, &client_aux)?; |
| 561 | + |
| 562 | + // Track the mapping |
| 563 | + self.client_to_frame.insert(client, frame); |
| 564 | + self.frame_to_client.insert(frame, client); |
| 565 | + |
| 566 | + tracing::debug!( |
| 567 | + "Created border frame {} for client {} at ({}, {}) size {}x{} (border: {})", |
| 568 | + frame, client, x, y, frame_width, frame_height, border_width |
| 569 | + ); |
| 570 | + |
| 571 | + Ok(frame) |
| 572 | + } |
| 573 | + |
| 574 | + /// Configure a border frame's geometry. |
| 575 | + pub fn configure_border_frame<C: X11Connection>( |
| 576 | + &self, |
| 577 | + conn: &C, |
| 578 | + client: Window, |
| 579 | + x: i16, |
| 580 | + y: i16, |
| 581 | + width: u16, |
| 582 | + height: u16, |
| 583 | + border_width: u16, |
| 584 | + ) -> Result<(), Error> { |
| 585 | + if let Some(&frame) = self.client_to_frame.get(&client) { |
| 586 | + let frame_width = width + border_width * 2; |
| 587 | + let frame_height = height + border_width * 2; |
| 588 | + |
| 589 | + // Configure frame position and size |
| 590 | + let frame_aux = ConfigureWindowAux::new() |
| 591 | + .x(x as i32) |
| 592 | + .y(y as i32) |
| 593 | + .width(frame_width as u32) |
| 594 | + .height(frame_height as u32) |
| 595 | + .border_width(0); |
| 596 | + conn.configure_window(frame, &frame_aux)?; |
| 597 | + |
| 598 | + // Configure client within frame |
| 599 | + let client_aux = ConfigureWindowAux::new() |
| 600 | + .x(border_width as i32) |
| 601 | + .y(border_width as i32) |
| 602 | + .width(width as u32) |
| 603 | + .height(height as u32); |
| 604 | + conn.configure_window(client, &client_aux)?; |
| 605 | + } |
| 606 | + Ok(()) |
| 607 | + } |
| 608 | +} |
| 609 | + |
| 610 | +/// Interpolate between two colors. |
| 611 | +/// t should be in range 0.0 to 1.0. |
| 612 | +fn interpolate_color(c1: u32, c2: u32, t: f32) -> u32 { |
| 613 | + let t = t.clamp(0.0, 1.0); |
| 614 | + |
| 615 | + let r1 = ((c1 >> 16) & 0xFF) as f32; |
| 616 | + let g1 = ((c1 >> 8) & 0xFF) as f32; |
| 617 | + let b1 = (c1 & 0xFF) as f32; |
| 618 | + |
| 619 | + let r2 = ((c2 >> 16) & 0xFF) as f32; |
| 620 | + let g2 = ((c2 >> 8) & 0xFF) as f32; |
| 621 | + let b2 = (c2 & 0xFF) as f32; |
| 622 | + |
| 623 | + let r = (r1 + (r2 - r1) * t) as u32; |
| 624 | + let g = (g1 + (g2 - g1) * t) as u32; |
| 625 | + let b = (b1 + (b2 - b1) * t) as u32; |
| 626 | + |
| 627 | + (r << 16) | (g << 8) | b |
| 284 | 628 | } |
| 285 | 629 | |
| 286 | 630 | impl Default for FrameManager { |