From 0a4c4b5941fe982ccb3d3175e26d9d83f0025ffd Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Sat, 15 Feb 2025 14:56:00 +0200 Subject: Checkpoint: preserve all uncommitted rust-rewrite worktree work Safety commit before reconciling this worktree with main, which has diverged with its own separate fixes today. Nothing here is reviewed or curated yet - this exists purely so none of this work can be lost to a git operation, disk issue, or worktree cleanup while that reconciliation happens. --- crates/wayland/src/winit/capture.rs | 17 ++- crates/wayland/src/winit/connect.rs | 5 + crates/wayland/src/winit/mod.rs | 3 +- crates/wayland/src/winit/nested_platform.rs | 192 ++++++++++++++++++++++++++++ crates/wayland/src/winit/platform.rs | 186 --------------------------- crates/wayland/src/winit/render.rs | 112 +++++++++++++--- 6 files changed, 304 insertions(+), 211 deletions(-) create mode 100644 crates/wayland/src/winit/nested_platform.rs delete mode 100644 crates/wayland/src/winit/platform.rs (limited to 'crates/wayland/src/winit') diff --git a/crates/wayland/src/winit/capture.rs b/crates/wayland/src/winit/capture.rs index a96cff9..3b1d38e 100644 --- a/crates/wayland/src/winit/capture.rs +++ b/crates/wayland/src/winit/capture.rs @@ -29,7 +29,7 @@ impl WaylandPlatform { let hide_top_layers = self.wm.borrow().visible_windows_front_to_back().any(|w| w.fullscreen); let mut custom_elements: Vec> = Vec::new(); if !hide_top_layers { - custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay))); + custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Top | Layer::Overlay))); } for id in self.wm.borrow().visible_windows_front_to_back().map(|w| w.id).collect::>() { let Some(w) = self.wm.borrow().window(id).cloned() else { continue }; @@ -41,11 +41,22 @@ impl WaylandPlatform { if let Some(dwindow) = self.state.id_to_window.get(&id) { if let Some(surface) = crate::elements::window_wl_surface(dwindow) { let band = if w.decorated { srdwm_core::TITLEBAR_HEIGHT as i32 } else { 0 }; - custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, (w.geometry.x, w.geometry.y + band), w.opacity)); + // `content_offset`: same `xdg_surface.set_window_geometry` + // subtraction every other render/capture path in this + // codebase already does (`udev/render.rs`, `winit/ + // render.rs`, `udev/capture.rs`) - missed here + // specifically. A CSD client's invisible shadow margin + // landed at `w.geometry.x, w.geometry.y + band` instead + // of its real visible content, so a screenshot taken on + // this backend showed the same content_offset-sized gap + // the on-screen render loops already had fixed. + let content_offset = dwindow.geometry().loc; + let pos = (w.geometry.x - content_offset.x, w.geometry.y + band - content_offset.y); + custom_elements.extend(crate::elements::surface_content_elements(renderer, &surface, pos, w.opacity)); } } } - custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom))); + custom_elements.extend(crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Background | Layer::Bottom))); // A throwaway damage tracker, so this pass always draws the whole // scene (age 0) and never perturbs the on-screen tracker's history. diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs index d18a489..c748ef4 100644 --- a/crates/wayland/src/winit/connect.rs +++ b/crates/wayland/src/winit/connect.rs @@ -113,6 +113,7 @@ impl WaylandPlatform { _screencopy_state: screencopy::ScreencopyState::new::(&dh), screencopy_pending: Vec::new(), _appmenu_state: crate::appmenu::AppmenuManagerState::new::(&dh), + _virtual_keyboard_state: smithay::wayland::virtual_keyboard::VirtualKeyboardManagerState::new::(&dh, |_client| true), _foreign_toplevel_state: crate::foreign_toplevel::ForeignToplevelState::new::(&dh), foreign_toplevel_managers: Vec::new(), foreign_toplevel_handles: HashMap::new(), @@ -142,6 +143,7 @@ impl WaylandPlatform { last_broadcast_workspace: None, lock: SessionLock::default(), cursor_status: smithay::input::pointer::CursorImageStatus::default_named(), + decoration_cursor_active: false, cursor_buffers: crate::cursor::make_buffers(), last_titlebar_click: None, gesture_swipe: None, @@ -159,12 +161,15 @@ impl WaylandPlatform { border_top_decorations: HashMap::new(), border_bottom_decorations: HashMap::new(), decoration_signatures: HashMap::new(), + hovered_titlebar_button: None, shadow_buffers: HashMap::new(), rounded_corners_program, content_epoch: HashMap::new(), rounded_content_buffers: HashMap::new(), border_side_buffers: HashMap::new(), + color_filter_buffers: HashMap::new(), last_synced_size: HashMap::new(), + pending_size_configure: HashMap::new(), pending: pending.clone(), bound_keys: Rc::new(bound_keys.iter().cloned().collect()), repeat_keys: Rc::new(repeat_keys.iter().cloned().collect()), diff --git a/crates/wayland/src/winit/mod.rs b/crates/wayland/src/winit/mod.rs index 707055d..6a7bac0 100644 --- a/crates/wayland/src/winit/mod.rs +++ b/crates/wayland/src/winit/mod.rs @@ -19,6 +19,7 @@ use smithay::backend::input::{ }; use smithay::backend::renderer::damage::OutputDamageTracker; use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; +use smithay::backend::renderer::element::solid::SolidColorBuffer; use smithay::backend::renderer::element::Kind; use smithay::backend::renderer::gles::GlesRenderer; use smithay::backend::renderer::ImportDma; @@ -94,6 +95,6 @@ const TARGET_FRAME_TIME: Duration = Duration::from_micros(1_000_000 / 60); mod capture; mod connect; mod events; -mod platform; +mod nested_platform; mod render; mod run; diff --git a/crates/wayland/src/winit/nested_platform.rs b/crates/wayland/src/winit/nested_platform.rs new file mode 100644 index 0000000..330ef91 --- /dev/null +++ b/crates/wayland/src/winit/nested_platform.rs @@ -0,0 +1,192 @@ +use super::*; + +impl Platform for WaylandPlatform { + fn kind(&self) -> PlatformKind { + PlatformKind::Wayland + } + + /// **Self-paced, deliberately**: nothing else in this backend ever + /// blocks. `pump_winit`'s underlying `dispatch_new_events` polls + /// (returns immediately either way), and smithay's winit backend + /// hardcodes `vsync: false` on the EGL surface it creates + /// (`init_from_attributes_with_gl_attr` in smithay 0.7.0's own + /// `backend/winit/mod.rs` - true of *every* entry point into that + /// module, including the one this backend used before it needed custom + /// `WindowAttributes`, so this was never introduced by that switch). + /// `swap_buffers` therefore returns as soon as the GPU accepts the + /// frame, with no wait for the next display refresh at all. Before this + /// fix, that meant `poll_events` -> `render_frame` -> full render + + /// `swap_buffers` ran back-to-back with nothing pacing the `while + /// running.get()` loop in `main.rs` between iterations - confirmed + /// live: an idle nested instance, zero windows, sat at a sustained + /// ~52% of one core (`ps -o %cpu`), because it was rendering and + /// presenting a full frame as fast as the CPU/GPU could physically + /// cycle, forever, whether or not anything on screen had changed. + /// Fixed by giving `idle_event_loop.dispatch` (already called every + /// tick to service `ext_idle_notify_v1`'s timers, see its field doc + /// comment) a real timeout instead of always `Duration::ZERO`: the + /// remaining budget until `TARGET_FRAME_TIME` has elapsed since the + /// last frame, clamped to zero once that budget is already spent. This + /// reuses the one blocking wait this backend already has rather than + /// adding a second, separate `thread::sleep`, and still services any + /// idle-notify timer that comes due sooner than a full frame away. + fn poll_events(&mut self) -> PlatformResult> { + self.accept_clients()?; + let closed = self.pump_winit()?; + if closed { + return Err(PlatformError::Other("compositor window closed".into())); + } + // Held bindings that repeat - see `CompState::tick_repeat`. + self.state.tick_repeat(); + self.display.dispatch_clients(&mut self.state).map_err(err)?; + self.display.flush_clients().map_err(err)?; + if let Some(ipc) = self.ipc.as_mut() { + if ipc.poll(&self.wm) { + self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged); + // Same re-sync as `udev/platform.rs`'s matching block - see + // its own comment. `handle_request` only ever touches core's + // `WindowManager`, never `state.space`, so an IPC focus + // change left rendering/hit-testing on the stale topmost + // window until something else happened to raise it. + // + // `raise_in_space`, not `focus_window` - see that + // function's doc comment: the full version re-runs the + // workspace-follow side effect on the already-focused + // window and silently reverts an `activate_workspace` IPC + // dispatch from the same cycle. + let focused = self.wm.borrow().focused_id(); + if let Some(id) = focused { + crate::input::raise_in_space(&mut self.state, id); + } + } + } + // Same lock-request draining as `udev/platform.rs`'s matching + // block - see its own comment. Exercised here too (not just on + // the real udev backend) specifically so a native lock can be + // tested against this nested dev session without ever touching + // the live tty1 one. + if self.wm.borrow_mut().drain_lock_request() { + self.state.begin_native_lock(); + } + self.state.poll_native_lock_auth(); + let wait = TARGET_FRAME_TIME.saturating_sub(self.last_frame.elapsed()); + let _ = self.idle_event_loop.dispatch(Some(wait), &mut self.state); + self.last_frame = Instant::now(); + self.render_frame()?; + Ok(self.pending.borrow_mut().drain(..).collect()) + } + + fn monitors(&mut self) -> PlatformResult> { + // Shrunk by any layer-shell exclusive zone - see the matching + // comment in `udev/platform.rs`'s `monitors()`. This backend is always a + // single output at the global origin, so the output-local zone + // rectangle already is the usable global-space rect. + let zone = layer_map_for_output(&self.output).non_exclusive_zone(); + Ok(vec![{ + let rect = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32); + let mut m = srdwm_core::Monitor::new(0, "winit", rect); + // Same fix as `udev/platform.rs`'s matching function: `Monitor::new` + // defaults `full_geometry` to `geometry`, which is already + // zone-shrunk here - without this, `toggle_fullscreen` had no + // way to actually cover a bar/dock's reserved strip, since the + // "true full rect" it targets was silently identical to the + // "usable, shrunk rect" `toggle_maximize` targets. + let full = self.backend.window_size(); + m.full_geometry = srdwm_core::Rect::new(0, 0, full.w as u32, full.h as u32); + m.maximize_geometry = crate::input::maximize_geometry_for(&self.output, m.full_geometry); + m.primary = true; + m + }]) + } + + fn apply_geometry(&mut self, window: WindowId, geometry: srdwm_core::Rect) -> PlatformResult<()> { + let _ = geometry; + self.state.sync_geometry(window); + Ok(()) + } + + fn set_title(&mut self, _window: WindowId, _title: &str) -> PlatformResult<()> { + Ok(()) + } + + /// See `udev/platform.rs`'s matching impl for why this has to go through + /// `crate::input::focus_window` (the same path a real mouse click + /// already uses) rather than only touching core state. + fn focus(&mut self, window: WindowId) -> PlatformResult<()> { + crate::input::focus_window(&mut self.state, window); + Ok(()) + } + + fn minimize(&mut self, window: WindowId) -> PlatformResult<()> { + if let Some(w) = self.state.id_to_window.get(&window) { + self.state.space.unmap_elem(w); + } + Ok(()) + } + + fn restore(&mut self, window: WindowId) -> PlatformResult<()> { + self.state.sync_geometry(window); + Ok(()) + } + + fn close(&mut self, window: WindowId) -> PlatformResult<()> { + let Some(w) = self.state.id_to_window.get(&window) else { return Ok(()) }; + if let Some(toplevel) = w.toplevel() { + toplevel.send_close(); + } else if let Some(x11) = w.x11_surface() { + // Same fix as `udev/platform.rs`'s matching function: `w.toplevel()` + // is `None` for an XWayland window, so closing one silently did + // nothing at all without this arm. + let _ = x11.close(); + } + Ok(()) + } + + fn set_decorated(&mut self, _window: WindowId, _decorated: bool) -> PlatformResult<()> { + Ok(()) + } + + fn set_border_color(&mut self, _window: WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> { + Ok(()) + } + + fn set_border_width(&mut self, _window: WindowId, _width: u32) -> PlatformResult<()> { + Ok(()) + } + + fn redraw_decoration(&mut self, window: WindowId, _win: &CoreWindow, _focused: bool) -> PlatformResult<()> { + // Re-renders the title/focus-color band and re-syncs geometry; + // `sync_geometry` re-renders the decoration too, but only if one + // already exists, so this also covers first paint. + self.state.redraw_decoration_buffer(window); + self.state.sync_geometry(window); + Ok(()) + } + + fn grab_keyboard(&mut self) -> PlatformResult<()> { + Ok(()) + } + + fn ungrab_keyboard(&mut self) -> PlatformResult<()> { + Ok(()) + } + + fn keyboard_layout(&mut self) -> PlatformResult { + let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) }; + Ok(keyboard.with_xkb_state(&mut self.state, |ctx| { + let xkb = ctx.xkb().lock().unwrap(); + let layout = xkb.active_layout(); + xkb.layout_name(layout).to_string() + })) + } + + fn cycle_keyboard_layout(&mut self) -> PlatformResult { + let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) }; + Ok(keyboard.with_xkb_state(&mut self.state, |mut ctx| { + ctx.cycle_next_layout(); + let xkb = ctx.xkb().lock().unwrap(); + let layout = xkb.active_layout(); + xkb.layout_name(layout).to_string() + })) + } +} diff --git a/crates/wayland/src/winit/platform.rs b/crates/wayland/src/winit/platform.rs deleted file mode 100644 index 96adde8..0000000 --- a/crates/wayland/src/winit/platform.rs +++ /dev/null @@ -1,186 +0,0 @@ -use super::*; - -impl Platform for WaylandPlatform { - fn kind(&self) -> PlatformKind { - PlatformKind::Wayland - } - - /// **Self-paced, deliberately**: nothing else in this backend ever - /// blocks. `pump_winit`'s underlying `dispatch_new_events` polls - /// (returns immediately either way), and smithay's winit backend - /// hardcodes `vsync: false` on the EGL surface it creates - /// (`init_from_attributes_with_gl_attr` in smithay 0.7.0's own - /// `backend/winit/mod.rs` - true of *every* entry point into that - /// module, including the one this backend used before it needed custom - /// `WindowAttributes`, so this was never introduced by that switch). - /// `swap_buffers` therefore returns as soon as the GPU accepts the - /// frame, with no wait for the next display refresh at all. Before this - /// fix, that meant `poll_events` -> `render_frame` -> full render + - /// `swap_buffers` ran back-to-back with nothing pacing the `while - /// running.get()` loop in `main.rs` between iterations - confirmed - /// live: an idle nested instance, zero windows, sat at a sustained - /// ~52% of one core (`ps -o %cpu`), because it was rendering and - /// presenting a full frame as fast as the CPU/GPU could physically - /// cycle, forever, whether or not anything on screen had changed. - /// Fixed by giving `idle_event_loop.dispatch` (already called every - /// tick to service `ext_idle_notify_v1`'s timers, see its field doc - /// comment) a real timeout instead of always `Duration::ZERO`: the - /// remaining budget until `TARGET_FRAME_TIME` has elapsed since the - /// last frame, clamped to zero once that budget is already spent. This - /// reuses the one blocking wait this backend already has rather than - /// adding a second, separate `thread::sleep`, and still services any - /// idle-notify timer that comes due sooner than a full frame away. - fn poll_events(&mut self) -> PlatformResult> { - self.accept_clients()?; - let closed = self.pump_winit()?; - if closed { - return Err(PlatformError::Other("compositor window closed".into())); - } - // Held bindings that repeat - see `CompState::tick_repeat`. - self.state.tick_repeat(); - self.display.dispatch_clients(&mut self.state).map_err(err)?; - self.display.flush_clients().map_err(err)?; - if let Some(ipc) = self.ipc.as_mut() { - if ipc.poll(&self.wm) { - self.pending.borrow_mut().push(CoreEvent::WorkspaceChanged); - // Same re-sync as `udev/platform.rs`'s matching block - see - // its own comment. `handle_request` only ever touches core's - // `WindowManager`, never `state.space`, so an IPC focus - // change left rendering/hit-testing on the stale topmost - // window until something else happened to raise it. - let focused = self.wm.borrow().focused_id(); - if let Some(id) = focused { - crate::input::focus_window(&mut self.state, id); - } - } - } - // Same lock-request draining as `udev/platform.rs`'s matching - // block - see its own comment. Exercised here too (not just on - // the real udev backend) specifically so a native lock can be - // tested against this nested dev session without ever touching - // the live tty1 one. - if self.wm.borrow_mut().drain_lock_request() { - self.state.begin_native_lock(); - } - self.state.poll_native_lock_auth(); - let wait = TARGET_FRAME_TIME.saturating_sub(self.last_frame.elapsed()); - let _ = self.idle_event_loop.dispatch(Some(wait), &mut self.state); - self.last_frame = Instant::now(); - self.render_frame()?; - Ok(self.pending.borrow_mut().drain(..).collect()) - } - - fn monitors(&mut self) -> PlatformResult> { - // Shrunk by any layer-shell exclusive zone - see the matching - // comment in `udev/platform.rs`'s `monitors()`. This backend is always a - // single output at the global origin, so the output-local zone - // rectangle already is the usable global-space rect. - let zone = layer_map_for_output(&self.output).non_exclusive_zone(); - Ok(vec![{ - let rect = srdwm_core::Rect::new(zone.loc.x, zone.loc.y, zone.size.w as u32, zone.size.h as u32); - let mut m = srdwm_core::Monitor::new(0, "winit", rect); - // Same fix as `udev/platform.rs`'s matching function: `Monitor::new` - // defaults `full_geometry` to `geometry`, which is already - // zone-shrunk here - without this, `toggle_fullscreen` had no - // way to actually cover a bar/dock's reserved strip, since the - // "true full rect" it targets was silently identical to the - // "usable, shrunk rect" `toggle_maximize` targets. - let full = self.backend.window_size(); - m.full_geometry = srdwm_core::Rect::new(0, 0, full.w as u32, full.h as u32); - m.maximize_geometry = crate::input::maximize_geometry_for(&self.output, m.full_geometry); - m.primary = true; - m - }]) - } - - fn apply_geometry(&mut self, window: WindowId, geometry: srdwm_core::Rect) -> PlatformResult<()> { - let _ = geometry; - self.state.sync_geometry(window); - Ok(()) - } - - fn set_title(&mut self, _window: WindowId, _title: &str) -> PlatformResult<()> { - Ok(()) - } - - /// See `udev/platform.rs`'s matching impl for why this has to go through - /// `crate::input::focus_window` (the same path a real mouse click - /// already uses) rather than only touching core state. - fn focus(&mut self, window: WindowId) -> PlatformResult<()> { - crate::input::focus_window(&mut self.state, window); - Ok(()) - } - - fn minimize(&mut self, window: WindowId) -> PlatformResult<()> { - if let Some(w) = self.state.id_to_window.get(&window) { - self.state.space.unmap_elem(w); - } - Ok(()) - } - - fn restore(&mut self, window: WindowId) -> PlatformResult<()> { - self.state.sync_geometry(window); - Ok(()) - } - - fn close(&mut self, window: WindowId) -> PlatformResult<()> { - let Some(w) = self.state.id_to_window.get(&window) else { return Ok(()) }; - if let Some(toplevel) = w.toplevel() { - toplevel.send_close(); - } else if let Some(x11) = w.x11_surface() { - // Same fix as `udev/platform.rs`'s matching function: `w.toplevel()` - // is `None` for an XWayland window, so closing one silently did - // nothing at all without this arm. - let _ = x11.close(); - } - Ok(()) - } - - fn set_decorated(&mut self, _window: WindowId, _decorated: bool) -> PlatformResult<()> { - Ok(()) - } - - fn set_border_color(&mut self, _window: WindowId, _rgb: (u8, u8, u8)) -> PlatformResult<()> { - Ok(()) - } - - fn set_border_width(&mut self, _window: WindowId, _width: u32) -> PlatformResult<()> { - Ok(()) - } - - fn redraw_decoration(&mut self, window: WindowId, _win: &CoreWindow, _focused: bool) -> PlatformResult<()> { - // Re-renders the title/focus-color band and re-syncs geometry; - // `sync_geometry` re-renders the decoration too, but only if one - // already exists, so this also covers first paint. - self.state.redraw_decoration_buffer(window); - self.state.sync_geometry(window); - Ok(()) - } - - fn grab_keyboard(&mut self) -> PlatformResult<()> { - Ok(()) - } - - fn ungrab_keyboard(&mut self) -> PlatformResult<()> { - Ok(()) - } - - fn keyboard_layout(&mut self) -> PlatformResult { - let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) }; - Ok(keyboard.with_xkb_state(&mut self.state, |ctx| { - let xkb = ctx.xkb().lock().unwrap(); - let layout = xkb.active_layout(); - xkb.layout_name(layout).to_string() - })) - } - - fn cycle_keyboard_layout(&mut self) -> PlatformResult { - let Some(keyboard) = self.state.seat.get_keyboard() else { return Ok(String::new()) }; - Ok(keyboard.with_xkb_state(&mut self.state, |mut ctx| { - ctx.cycle_next_layout(); - let xkb = ctx.xkb().lock().unwrap(); - let layout = xkb.active_layout(); - xkb.layout_name(layout).to_string() - })) - } -} diff --git a/crates/wayland/src/winit/render.rs b/crates/wayland/src/winit/render.rs index ec4fd69..60cccb4 100644 --- a/crates/wayland/src/winit/render.rs +++ b/crates/wayland/src/winit/render.rs @@ -4,6 +4,7 @@ impl WaylandPlatform { pub(super) fn render_frame(&mut self) -> PlatformResult<()> { self.state.tick_animations(); + self.state.tick_hover_glyph_animation(); self.state.tick_dirty_broadcasts(); let size = self.backend.window_size(); let resized = self.output.current_mode().map(|m| m.size) != Some(size); @@ -61,6 +62,20 @@ impl WaylandPlatform { // rounded-content push (further down) uses `WinitElement::Rounded` // directly. let mut custom_elements: Vec = Vec::new(); + // Night light/reading mode - pushed first (topmost) so it colours + // everything else, including the context menu below: this backend + // draws no cursor of its own to exempt (unlike udev/render.rs's + // matching push), so there's nothing that needs to stay above it. + // See `color_filter::render_element` for why this is a translucent + // overlay rather than a true per-pixel shader. + { + let color_filter = self.wm.borrow().color_filter; + let output_name = self.output.name(); + let buf = self.state.color_filter_buffers.entry(output_name).or_insert_with(SolidColorBuffer::default); + if let Some(elem) = crate::color_filter::render_element(buf, color_filter, (size.w, size.h)) { + custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Solid(elem))); + } + } // The right-click titlebar menu, if open - pushed first so it's // topmost over every window (this backend draws no cursor of its // own, see this module's doc comment, so there's no "stay under @@ -124,7 +139,7 @@ impl WaylandPlatform { let rounded_corners_enabled = self.wm.borrow().rounded_corners_enabled.unwrap_or(true); if !hide_top_layers { custom_elements.extend( - crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Top | Layer::Overlay)) + crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Top | Layer::Overlay)) .into_iter() .map(crate::rounded_corners::WinitElement::Base), ); @@ -147,24 +162,18 @@ impl WaylandPlatform { // (reported live as the border "not flush" with the window // during an animated maximize/fullscreen/open-slide transition). let geom = self.state.window_anims.get(&id).map(crate::state::WindowAnim::current_rect).unwrap_or(w.geometry); - // Same reasoning as udev/render.rs's matching push: positioned from - // `geom`, not `w.geometry`, and not fragment-clipped against - // `occluders` - see that comment. - if let Some(shadow) = self.state.shadow_buffers.get(&id) { - let rect = decoration::shadow_rect(geom); - let pos = (rect.x as f64, rect.y as f64); - match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) { - Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), - Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"), - } - } + // `geom` is this compositor's own request/target; `frame` + // corrects its far edge to match what the client's surface + // really committed - see `effective_frame`'s own doc comment + // and the matching comment in `udev/render.rs`'s render loop. + let frame = self.state.effective_frame(id, geom); if let Some(deco) = self.state.decorations.get(&id) { // Fragment-clipped, same as udev/render.rs's matching titlebar // push - see that comment for why all-or-nothing (skip // only once *fully* covered) wasn't enough: a titlebar // only partially covered, the common case for cascaded // windows, still bled through the covered part. - let titlebar_rect = srdwm_core::Rect::new(geom.x, geom.y, geom.width, srdwm_core::TITLEBAR_HEIGHT); + let titlebar_rect = srdwm_core::Rect::new(frame.x, frame.y, frame.width, srdwm_core::TITLEBAR_HEIGHT); for fragment in crate::elements::visible_border_fragments(titlebar_rect, &occluders) { let pos = (fragment.x as f64, fragment.y as f64); let src = Rectangle::new( @@ -181,9 +190,14 @@ impl WaylandPlatform { // `decoration::border_strips`), so they never overlap this same // window's own decoration/content pixels - draw order relative // to those doesn't matter, only relative to other windows'. + // (The left/right strips *do* still need cropping against the + // top/bottom strip's own extended curve - see that crop's own + // doc comment further down; that's an overlap between two + // pieces of this window's own decoration, not with its content, + // so it doesn't contradict this paragraph.) if w.border_width > 0 { - let color = crate::state::effective_border_color(w.border_color, focused == Some(id)); - let strips = decoration::border_strips(geom, w.border_width); + let color = crate::state::effective_border_color(w.border_color, focused == Some(id), self.wm.borrow().theme.border_inactive_dim); + let strips = decoration::border_strips(frame, w.border_width); // Strips 0/1 (top/bottom) are rounded on their own two // corners - see `render_border_top`/`render_border_bottom`'s // doc comments - so both are cached bitmaps (rebuilt only in @@ -199,7 +213,15 @@ impl WaylandPlatform { // all-or-nothing occlusion check. if strips[0].width > 0 && strips[0].height > 0 && !strips[0].subtract_all(&occluders).is_empty() { if let Some(buffer) = self.state.border_top_decorations.get(&id) { - match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[0].x as f64, strips[0].y as f64), buffer, None, None, None, Kind::Unspecified) { + // See `decoration::border_top_visible_rows`'s own + // doc comment: an undecorated window has no + // titlebar band to safely absorb this buffer's own + // corner-curve-only extra rows, so they're cropped + // away instead of landing on real content. + let (row0, rows, shift) = decoration::border_top_visible_rows(w.decorated, w.border_width, w.corner_radius); + let pos = (strips[0].x as f64, (strips[0].y + shift as i32) as f64); + let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[0].width as f64, rows as f64)))); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, src, None, Kind::Unspecified) { Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), Err(e) => log::warn!("failed to import top border buffer for window {id}: {e}"), } @@ -210,15 +232,39 @@ impl WaylandPlatform { // render_border_bottom`'s doc comment. if strips[1].width > 0 && strips[1].height > 0 && !strips[1].subtract_all(&occluders).is_empty() { if let Some(buffer) = self.state.border_bottom_decorations.get(&id) { - match MemoryRenderBufferRenderElement::from_buffer(renderer, (strips[1].x as f64, strips[1].y as f64), buffer, None, None, None, Kind::Unspecified) { + // See `decoration::border_bottom_visible_rows`'s + // own doc comment. + let (row0, rows, shift) = decoration::border_bottom_visible_rows(w.decorated, w.border_width, w.corner_radius); + let pos = (strips[1].x as f64, (strips[1].y - shift as i32) as f64); + let src = Some(Rectangle::new(Point::from((0.0, row0 as f64)), Size::from((strips[1].width as f64, rows as f64)))); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, buffer, None, src, None, Kind::Unspecified) { Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), Err(e) => log::warn!("failed to import bottom border buffer for window {id}: {e}"), } } } + // Cropped top and bottom by `extra` - see the matching fix + // (and its own doc comment) in `udev/render.rs`'s identical + // side-strip loop: the top/bottom strip's own curve extends + // `corner_radius - border_width` rows into what would + // otherwise be these flat, curve-unaware side strips' own + // nominal top/bottom rows, and without this crop their + // solid fill bled through the curve's own transparent + // cutout as a straight vertical line poking out of an + // otherwise correctly-rounded corner - reported live, + // confirmed via raw pixel sampling on the udev backend; + // this backend shares the identical strip geometry and was + // never actually confirmed clean, just never specifically + // screenshotted the same way. + let extra = if w.decorated { w.border_width.max(w.corner_radius).saturating_sub(w.border_width) } else { 0 }; + let mut side_strips = [strips[2], strips[3]]; + for s in &mut side_strips { + s.y += extra as i32; + s.height = s.height.saturating_sub(2 * extra); + } let pool = self.state.border_side_buffers.entry(id).or_default(); let mut buf_index = 0; - for strip in &strips[2..] { + for strip in &side_strips { if strip.width == 0 || strip.height == 0 { continue; } @@ -229,6 +275,30 @@ impl WaylandPlatform { } } } + // Shadow - pushed *after* the titlebar/border above, not + // before. See the matching fix (and its full explanation) in + // `udev/render.rs`'s render loop: `custom_elements` treats + // earlier-pushed as topmost, so a shadow pushed before this + // window's own border rendered on top of it, alpha-blending + // black over the configured border colour and muting it into a + // hazy smear instead of a crisp line - reported live as + // "spacing before the border". Positioned from `geom`, not + // `w.geometry`, same reasoning as the border above (a stale- + // position shadow during an animated tween looks as detached + // as the border did before that fix). Not fragment-clipped + // against `occluders` like the titlebar/border above: at + // `SHADOW_MAX_ALPHA`'s low opacity, a shadow bleeding slightly + // onto a window stacked in front of this one reads as a soft + // edge, not the hard-line bleed-through that made the + // titlebar/border need it. + if let Some(shadow) = self.state.shadow_buffers.get(&id) { + let rect = decoration::shadow_rect(frame); + let pos = (rect.x as f64, rect.y as f64); + match MemoryRenderBufferRenderElement::from_buffer(renderer, pos, shadow, None, None, None, Kind::Unspecified) { + Ok(elem) => custom_elements.push(crate::rounded_corners::WinitElement::Base(crate::elements::OverlayElement::Memory(elem))), + Err(e) => log::warn!("failed to import shadow buffer for window {id}: {e}"), + } + } // The window's own content, at its own `opacity` - see the // matching push in `udev/render.rs`'s render loop for why. Single // output at the global origin, so no offset to subtract (see @@ -266,13 +336,13 @@ impl WaylandPlatform { } } } - occluders.push(geom); + occluders.push(frame); } // Background/bottom layer-shell (wallpaper engines) last -- // bottommost, matching smithay's own `space_render_elements` // ordering, which this whole custom loop now replaces. custom_elements.extend( - crate::elements::output_layer_elements(renderer, &self.output, (0, 0), |layer| matches!(layer, Layer::Background | Layer::Bottom)) + crate::elements::output_layer_elements(renderer, &self.output, |layer| matches!(layer, Layer::Background | Layer::Bottom)) .into_iter() .map(crate::rounded_corners::WinitElement::Base), ); @@ -325,7 +395,7 @@ impl WaylandPlatform { self.backend.submit(None).map_err(err)?; let scale = Scale::from(self.output.current_scale().fractional_scale()); let now = self.state.start_time.elapsed(); - for w in crate::elements::windows_touched_by_damage(&self.state.space, &damage_rects, scale) { + for w in crate::elements::windows_touched_by_damage(&self.state.space, &damage_rects, (0, 0).into(), scale) { w.send_frame(&self.output, now, None, |_, _| Some(self.output.clone())); } } -- cgit v1.2.3