diff --git a/Cargo.toml b/Cargo.toml index 5a3959b..bda303a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tempfile = "3.10.1" [workspace.dependencies.smithay] git = "https://github.com/Smithay/smithay" rev = "900b938" +# path = "../../git/smithay" default-features = false features = [ "desktop", diff --git a/README.md b/README.md index b706c1c..cb20f3a 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,6 @@ See [`CONTRIBUTING.md`](CONTRIBUTING.md). # Changelog See [`CHANGELOG.md`](CHANGELOG.md). + +# With Special Thanks To +- [Niri](https://github.com/YaLTeR/niri): For all that rendering and protocol stuff I, ahem, *took inspiration* from diff --git a/TODO.md b/TODO.md index 4803725..7717fc1 100644 --- a/TODO.md +++ b/TODO.md @@ -1 +1,11 @@ -- Log git commit on startup and add to --help/-V +- Re-add raising file descriptor limit + - Like an idiot I managed to remove that sometime and not add it back +- Provide scale and transform on new window/layer + +Problems: +- Pointer input to xwayland windows saturates at x=0, y=0, so windows on outputs at negative coords + get screwed up pointer events +- Xwayland popups are screwed when the output is not at (0, 0) +- Dragging an xwayland window to another output and closing a nested right click menu closes the whole + right click menu because the keyboard focus is getting updated on the original output. +- Transactions don't render floating windows diff --git a/api/lua/examples/default/default_config.lua b/api/lua/examples/default/default_config.lua index 3d30773..fe8d8f6 100644 --- a/api/lua/examples/default/default_config.lua +++ b/api/lua/examples/default/default_config.lua @@ -13,7 +13,7 @@ require("pinnacle").setup(function(Pinnacle) ---@type Modifier local mod_key = "ctrl" - local terminal = "alacritty" + local terminal = "foot" -------------------- -- Mousebinds -- diff --git a/src/api.rs b/src/api.rs index 4fe836b..732670e 100644 --- a/src/api.rs +++ b/src/api.rs @@ -56,6 +56,7 @@ use crate::{ config::ConnectorSavedState, input::ModifierMask, output::OutputName, + render::util::snapshot::capture_snapshots_on_output, state::{State, WithState}, tag::{Tag, TagId}, }; @@ -737,19 +738,33 @@ impl tag_service_server::TagService for TagService { return; }; - match set_or_toggle { - SetOrToggle::Set => tag.set_active(true, state), - SetOrToggle::Unset => tag.set_active(false, state), - SetOrToggle::Toggle => tag.set_active(!tag.active(), state), - SetOrToggle::Unspecified => unreachable!(), - } - let Some(output) = tag.output(&state.pinnacle) else { return; }; + let snapshots = state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut state.pinnacle, renderer, &output, []) + }); + + match set_or_toggle { + SetOrToggle::Set => tag.set_active(true, &mut state.pinnacle), + SetOrToggle::Unset => tag.set_active(false, &mut state.pinnacle), + SetOrToggle::Toggle => tag.set_active(!tag.active(), &mut state.pinnacle), + SetOrToggle::Unspecified => unreachable!(), + } + state.pinnacle.fixup_xwayland_window_layering(); + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + state.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + state.pinnacle.request_layout(&output); state.update_keyboard_focus(&output); state.schedule_render(&output); @@ -772,15 +787,29 @@ impl tag_service_server::TagService for TagService { return; }; + let snapshots = state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut state.pinnacle, renderer, &output, []) + }); + output.with_state(|op_state| { for op_tag in op_state.tags.iter() { - op_tag.set_active(false, state); + op_tag.set_active(false, &mut state.pinnacle); } - tag.set_active(true, state); + tag.set_active(true, &mut state.pinnacle); }); state.pinnacle.fixup_xwayland_window_layering(); + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + state.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + state.pinnacle.request_layout(&output); state.update_keyboard_focus(&output); state.schedule_render(&output); @@ -1068,6 +1097,10 @@ impl output_service_server::OutputService for OutputService { current_scale = f64::max(current_scale, 0.25); + let snapshots = state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut state.pinnacle, renderer, &output, []) + }); + state.pinnacle.change_output_state( &output, None, @@ -1075,6 +1108,17 @@ impl output_service_server::OutputService for OutputService { Some(Scale::Fractional(current_scale)), None, ); + + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + state.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + state.pinnacle.request_layout(&output); state.schedule_render(&output); }) diff --git a/src/api/window.rs b/src/api/window.rs index ff949f8..0ea4cbf 100644 --- a/src/api/window.rs +++ b/src/api/window.rs @@ -21,7 +21,10 @@ use smithay::{ use tonic::{Request, Response, Status}; use tracing::warn; -use crate::{output::OutputName, state::WithState, tag::TagId, window::window_state::WindowId}; +use crate::{ + output::OutputName, render::util::snapshot::capture_snapshots_on_output, state::WithState, + tag::TagId, window::window_state::WindowId, +}; use super::{run_unary, run_unary_no_response, StateFnSender}; @@ -130,33 +133,26 @@ impl window_service_server::WindowService for WindowService { return Err(Status::invalid_argument("unspecified set or toggle")); } + let fullscreen = match set_or_toggle { + SetOrToggle::Set => Some(true), + SetOrToggle::Unset => Some(false), + SetOrToggle::Toggle => None, + SetOrToggle::Unspecified => unreachable!(), + }; + run_unary_no_response(&self.sender, move |state| { - let pinnacle = &mut state.pinnacle; - let Some(window) = window_id.window(pinnacle) else { + let Some(window) = window_id.window(&state.pinnacle) else { return; }; - match set_or_toggle { - SetOrToggle::Set => { - if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - } + match fullscreen { + Some(fullscreen) => state.set_window_fullscreen(&window, fullscreen), + None => { + let is_fullscreen = window + .with_state(|win_state| win_state.fullscreen_or_maximized.is_fullscreen()); + state.set_window_fullscreen(&window, !is_fullscreen); } - SetOrToggle::Unset => { - if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - } - } - SetOrToggle::Toggle => window.toggle_fullscreen(), - SetOrToggle::Unspecified => unreachable!(), } - - let Some(output) = window.output(pinnacle) else { - return; - }; - - pinnacle.request_layout(&output); - state.schedule_render(&output); }) .await } @@ -179,33 +175,26 @@ impl window_service_server::WindowService for WindowService { return Err(Status::invalid_argument("unspecified set or toggle")); } + let maximized = match set_or_toggle { + SetOrToggle::Set => Some(true), + SetOrToggle::Unset => Some(false), + SetOrToggle::Toggle => None, + SetOrToggle::Unspecified => unreachable!(), + }; + run_unary_no_response(&self.sender, move |state| { - let pinnacle = &mut state.pinnacle; - let Some(window) = window_id.window(pinnacle) else { + let Some(window) = window_id.window(&state.pinnacle) else { return; }; - match set_or_toggle { - SetOrToggle::Set => { - if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } + match maximized { + Some(maximized) => state.set_window_maximized(&window, maximized), + None => { + let is_maximized = window + .with_state(|win_state| win_state.fullscreen_or_maximized.is_maximized()); + state.set_window_maximized(&window, !is_maximized); } - SetOrToggle::Unset => { - if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } - } - SetOrToggle::Toggle => window.toggle_maximized(), - SetOrToggle::Unspecified => unreachable!(), } - - let Some(output) = window.output(pinnacle) else { - return; - }; - - pinnacle.request_layout(&output); - state.schedule_render(&output); }) .await } @@ -234,6 +223,12 @@ impl window_service_server::WindowService for WindowService { return; }; + let snapshots = window.output(pinnacle).map(|output| { + state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(pinnacle, renderer, &output, [window.clone()]) + }) + }); + match set_or_toggle { SetOrToggle::Set => { if !window.with_state(|state| state.floating_or_tiled.is_floating()) { @@ -253,6 +248,16 @@ impl window_service_server::WindowService for WindowService { return; }; + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + pinnacle.request_layout(&output); state.schedule_render(&output); }) @@ -363,12 +368,28 @@ impl window_service_server::WindowService for WindowService { let Some(tag) = tag_id.tag(pinnacle) else { return }; + let snapshots = window.output(pinnacle).map(|output| { + state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(pinnacle, renderer, &output, [window.clone()]) + }) + }); + window.with_state_mut(|state| { state.tags = vec![tag.clone()]; }); let Some(output) = tag.output(pinnacle) else { return }; + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + pinnacle.request_layout(&output); state.schedule_render(&output); @@ -405,6 +426,12 @@ impl window_service_server::WindowService for WindowService { }; let Some(tag) = tag_id.tag(pinnacle) else { return }; + let snapshots = window.output(pinnacle).map(|output| { + state.backend.with_renderer(|renderer| { + capture_snapshots_on_output(pinnacle, renderer, &output, [window.clone()]) + }) + }); + // TODO: turn state.tags into a hashset match set_or_toggle { SetOrToggle::Set => window.with_state_mut(|state| { @@ -425,6 +452,17 @@ impl window_service_server::WindowService for WindowService { } let Some(output) = tag.output(pinnacle) else { return }; + + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + pinnacle.request_layout(&output); state.schedule_render(&output); @@ -635,7 +673,7 @@ impl window_service_server::WindowService for WindowService { pinnacle .focused_output() .and_then(|output| pinnacle.focused_window(output)) - .map(|foc_win| win == &foc_win) + .map(|foc_win| win == foc_win) }); let floating = window diff --git a/src/backend.rs b/src/backend.rs index 89b2082..ad191a7 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -10,6 +10,7 @@ use smithay::{ default_primary_scanout_output_compare, utils::select_dmabuf_feedback, RenderElementStates, }, + gles::GlesRenderer, ImportDma, Renderer, TextureFilter, }, }, @@ -106,6 +107,18 @@ impl Backend { } } + pub fn with_renderer( + &mut self, + with_renderer: impl FnOnce(&mut GlesRenderer) -> T, + ) -> Option { + match self { + Backend::Winit(winit) => Some(with_renderer(winit.backend.renderer())), + Backend::Udev(udev) => Some(with_renderer(udev.renderer().ok()?.as_mut())), + #[cfg(feature = "testing")] + Backend::Dummy(_) => None, + } + } + /// Returns `true` if the backend is [`Winit`]. /// /// [`Winit`]: Backend::Winit diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 29eb021..7fb5e31 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -108,7 +108,7 @@ const SUPPORTED_FORMATS: &[Fourcc] = &[ const SUPPORTED_FORMATS_8BIT_ONLY: &[Fourcc] = &[Fourcc::Abgr8888, Fourcc::Argb8888]; /// A [`MultiRenderer`] that uses the [`GbmGlesBackend`]. -type UdevRenderer<'a> = MultiRenderer< +pub type UdevRenderer<'a> = MultiRenderer< 'a, 'a, GbmGlesBackend, @@ -866,6 +866,10 @@ fn render_frame<'a>( } impl Udev { + pub fn renderer(&mut self) -> anyhow::Result> { + Ok(self.gpu_manager.single_renderer(&self.primary_gpu)?) + } + /// A GPU was plugged in. fn device_added( &mut self, @@ -1505,6 +1509,22 @@ impl Udev { )); } + // HACK: Taking the transaction before creating render elements + // leads to a possibility where the original buffer still gets displayed. + // Need to figure that out. + // In the meantime we take the transaction afterwards and schedule another render. + let mut render_after_transaction_finish = false; + output.with_state_mut(|state| { + if state + .layout_transaction + .as_ref() + .is_some_and(|ts| ts.ready()) + { + state.layout_transaction.take(); + render_after_transaction_finish = true; + } + }); + let clear_color = if pinnacle.lock_state.is_unlocked() { CLEAR_COLOR } else { @@ -1572,6 +1592,10 @@ impl Udev { Ok(true) => surface.render_state = RenderState::WaitingForVblank { dirty: false }, Ok(false) | Err(_) => surface.render_state = RenderState::Idle, } + + if render_after_transaction_finish { + self.schedule_render(&pinnacle.loop_handle, output); + } } } diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9625259..f02ec8f 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -335,6 +335,24 @@ impl Winit { )); } + let mut clear_snapshots = false; + self.output.with_state_mut(|state| { + if state + .layout_transaction + .as_ref() + .is_some_and(|ts| ts.ready()) + { + state.layout_transaction.take(); + clear_snapshots = true; + } + }); + + if clear_snapshots { + for win in pinnacle.windows.iter() { + win.with_state_mut(|state| state.snapshot.take()); + } + } + let render_res = self.backend.bind().and_then(|_| { let age = if *full_redraw > 0 { 0 diff --git a/src/focus.rs b/src/focus.rs index 5a24ad5..4edb3dc 100644 --- a/src/focus.rs +++ b/src/focus.rs @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later use smithay::{desktop::space::SpaceElement, output::Output, utils::SERIAL_COUNTER}; -use tracing::warn; use crate::{ state::{Pinnacle, State, WithState}, @@ -77,17 +76,10 @@ impl Pinnacle { } /// Raise a window to the top of the z-index stack. - /// - /// This does nothing if the window is unmapped. pub fn raise_window(&mut self, window: WindowElement, activate: bool) { - if self.space.elements().all(|win| win != &window) { - warn!("Tried to raise an unmapped window"); - return; - } - self.space.raise_element(&window, activate); - self.z_index_stack.retain(|win| win != &window); + self.z_index_stack.retain(|win| win != window); self.z_index_stack.push(window); self.fixup_xwayland_window_layering(); @@ -128,7 +120,7 @@ impl WindowKeyboardFocusStack { /// If it's already in the stack, it will be removed then pushed. /// If it isn't, it will just be pushed. pub fn set_focus(&mut self, window: WindowElement) { - self.stack.retain(|win| win != &window); + self.stack.retain(|win| win != window); self.stack.push(window); self.focused = true; } diff --git a/src/handlers.rs b/src/handlers.rs index 87cd5d5..7de199c 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,10 +1,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later pub mod session_lock; +pub mod window; mod xdg_shell; mod xwayland; -use std::{mem, os::fd::OwnedFd, sync::Arc, time::Duration}; +use std::{mem, os::fd::OwnedFd, sync::Arc}; use smithay::{ backend::renderer::utils::{self, with_renderer_surface_state}, @@ -13,8 +14,8 @@ use smithay::{ delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell, desktop::{ - self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, - utils::surface_primary_scanout_output, PopupKind, WindowSurfaceType, + self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind, + PopupManager, WindowSurfaceType, }, input::{ pointer::{CursorImageStatus, PointerHandle}, @@ -32,12 +33,12 @@ use smithay::{ Client, Resource, }, }, - utils::{Logical, Point, Rectangle, SERIAL_COUNTER}, + utils::{Logical, Point, Rectangle}, wayland::{ buffer::BufferHandler, compositor::{ - self, BufferAssignment, CompositorClientState, CompositorHandler, CompositorState, - SurfaceAttributes, + self, add_pre_commit_hook, BufferAssignment, CompositorClientState, CompositorHandler, + CompositorState, SurfaceAttributes, }, dmabuf, fractional_scale::{self, FractionalScaleHandler}, @@ -73,11 +74,13 @@ use crate::{ backend::Backend, delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy, focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget}, + handlers::xdg_shell::snapshot_pre_commit_hook, protocol::{ foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState}, gamma_control::{GammaControlHandler, GammaControlManagerState}, screencopy::{Screencopy, ScreencopyHandler}, }, + render::util::snapshot::capture_snapshots_on_output, state::{ClientState, Pinnacle, State, WithState}, }; @@ -137,108 +140,175 @@ impl CompositorHandler for State { self.backend.early_import(surface); + if compositor::is_sync_subsurface(surface) { + return; + } + let mut root = surface.clone(); while let Some(parent) = compositor::get_parent(&root) { root = parent; } - if !compositor::is_sync_subsurface(surface) { - if let Some(window) = self.pinnacle.window_for_surface(&root) { - window.on_commit(); - if let Some(loc) = window.with_state_mut(|state| state.target_loc.take()) { - self.pinnacle.space.map_element(window.clone(), loc, false); + self.pinnacle + .root_surface_cache + .insert(surface.clone(), root.clone()); + + if let Some(window) = self.pinnacle.window_for_surface(&root) { + window.mark_serial_as_committed(); + window.on_commit(); + } + + // TODO: maps here, is that good? + self.pinnacle.move_surface_if_resized(surface); + + // Root surface commit + if surface == &root { + // Unmapped window commit + if let Some(unmapped_window) = self.pinnacle.unmapped_window_for_surface(surface) { + let Some(is_mapped) = + with_renderer_surface_state(surface, |state| state.buffer().is_some()) + else { + unreachable!("on_commit_buffer_handler was called previously"); + }; + + // Unmapped window has become mapped + if is_mapped { + unmapped_window.on_commit(); + + if let Some(toplevel) = unmapped_window.toplevel() { + let hook_id = + add_pre_commit_hook(toplevel.wl_surface(), snapshot_pre_commit_hook); + + unmapped_window + .with_state_mut(|state| state.snapshot_hook_id = Some(hook_id)); + } + + let snapshots = if let Some(output) = self.pinnacle.focused_output().cloned() { + tracing::debug!("Placing toplevel"); + unmapped_window.place_on_output(&output); + + output.with_state_mut(|state| { + state.focus_stack.set_focus(unmapped_window.clone()) + }); + + Some(self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) + })) + } else { + None + }; + + self.pinnacle + .unmapped_windows + .retain(|win| win != unmapped_window); + self.pinnacle.windows.push(unmapped_window.clone()); + + self.pinnacle.raise_window(unmapped_window.clone(), true); + + self.pinnacle.apply_window_rules(&unmapped_window); + + if let Some(focused_output) = self.pinnacle.focused_output().cloned() { + if unmapped_window.is_on_active_tag() { + self.update_keyboard_focus(&focused_output); + + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = + snapshots.flatten() + { + focused_output.with_state_mut(|state| { + state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + self.pinnacle.request_layout(&focused_output); + } + } + } else { + // Still unmapped + unmapped_window.on_commit(); + self.pinnacle.ensure_initial_configure(surface); + } + + return; + } + + // Window surface commit + if let Some(window) = self.pinnacle.window_for_surface(surface) { + if window.is_wayland() { + let Some(is_mapped) = + with_renderer_surface_state(surface, |state| state.buffer().is_some()) + else { + unreachable!("on_commit_buffer_handler was called previously"); + }; + + window.on_commit(); + + // Toplevel has become unmapped, + // see https://wayland.app/protocols/xdg-shell#xdg_toplevel + if !is_mapped { + if let Some(hook_id) = + window.with_state_mut(|state| state.snapshot_hook_id.take()) + { + compositor::remove_pre_commit_hook(surface, hook_id); + } + + if let Some(output) = window.output(&self.pinnacle) { + let snapshots = self.backend.with_renderer(|renderer| { + capture_snapshots_on_output( + &mut self.pinnacle, + renderer, + &output, + [], + ) + }); + + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + } + + self.pinnacle.remove_window(&window, true); + + if let Some(output) = window.output(&self.pinnacle) { + self.update_keyboard_focus(&output); + self.pinnacle.request_layout(&output); + } + } + + // Update reactive popups + for (popup, _) in PopupManager::popups_for_surface(surface) { + if let PopupKind::Xdg(popup) = popup { + if popup.with_pending_state(|state| state.positioner.reactive) { + self.pinnacle.position_popup(&popup); + if let Err(err) = popup.send_pending_configure() { + warn!("Failed to configure reactive popup: {err}"); + } + } + } + } } } - }; + } + + // TODO: split this up and don't call every commit + self.pinnacle.ensure_initial_configure(surface); self.pinnacle.popup_manager.commit(surface); - if let Some(new_window) = self - .pinnacle - .new_windows - .iter() - .find(|win| win.wl_surface().is_some_and(|surf| &*surf == surface)) - .cloned() - { - let Some(is_mapped) = - with_renderer_surface_state(surface, |state| state.buffer().is_some()) - else { - unreachable!("on_commit_buffer_handler was called previously"); - }; - - if is_mapped { - self.pinnacle.new_windows.retain(|win| win != &new_window); - self.pinnacle.windows.push(new_window.clone()); - - if let Some(output) = self.pinnacle.focused_output() { - tracing::debug!("Placing toplevel"); - new_window.place_on_output(output); - output.with_state_mut(|state| state.focus_stack.set_focus(new_window.clone())); - } - - // FIXME: I'm mapping way offscreen here then sending a frame to prevent a window from - // | mapping with its default geometry then immediately resizing - // | because I don't set a target geometry before the initial configure. - self.pinnacle - .space - .map_element(new_window.clone(), (1000000, 0), true); - - self.pinnacle.raise_window(new_window.clone(), true); - - self.pinnacle.apply_window_rules(&new_window); - - if let Some(focused_output) = self.pinnacle.focused_output().cloned() { - self.pinnacle.request_layout(&focused_output); - new_window.send_frame( - &focused_output, - self.pinnacle.clock.now(), - Some(Duration::ZERO), - surface_primary_scanout_output, - ); - - // FIXME: an actual way to map new windows - // This is not self.update_keyboard_focus because - // the extra configure in that causes windows to map then resize - self.pinnacle.loop_handle.insert_idle(move |state| { - if let Some(keyboard) = state.pinnacle.seat.get_keyboard() { - if new_window.is_on_active_tag() { - keyboard.set_focus( - state, - Some(KeyboardFocusTarget::Window(new_window)), - SERIAL_COUNTER.next_serial(), - ); - } - } - }); - } - } else if new_window.toplevel().is_some() { - new_window.on_commit(); - self.pinnacle.ensure_initial_configure(surface); - } - - return; - } - - self.pinnacle.ensure_initial_configure(surface); - - self.pinnacle.move_surface_if_resized(surface); - let outputs = if let Some(window) = self.pinnacle.window_for_surface(surface) { - let mut outputs = self.pinnacle.space.outputs_for_element(&window); - - // When the window hasn't been mapped `outputs` is empty, - // so also trigger a render using the window's tags' output - if let Some(output) = window.output(&self.pinnacle) { - outputs.push(output); - } - outputs // surface is a window + self.pinnacle.space.outputs_for_element(&window) // surface is a window } else if let Some(window) = self.pinnacle.window_for_surface(&root) { - let mut outputs = self.pinnacle.space.outputs_for_element(&window); - if let Some(output) = window.output(&self.pinnacle) { - outputs.push(output); - } - outputs // surface is a root window + self.pinnacle.space.outputs_for_element(&window) // surface's root is a window } else if let Some(PopupKind::Xdg(surf)) = self.pinnacle.popup_manager.find_popup(surface) { + // INFO: is this relative to the global space or no let geo = surf.with_pending_state(|state| state.geometry); let outputs = self .pinnacle @@ -250,7 +320,7 @@ impl CompositorHandler for State { }) .cloned() .collect::>(); - outputs + outputs // surface is a popup } else if let Some(output) = self .pinnacle .space @@ -278,7 +348,7 @@ impl CompositorHandler for State { }) .cloned() { - vec![output] + vec![output] // surface is a lock surface } else { return; }; @@ -288,6 +358,36 @@ impl CompositorHandler for State { } } + fn destroyed(&mut self, surface: &WlSurface) { + let Some(root_surface) = self.pinnacle.root_surface_cache.get(surface) else { + return; + }; + let Some(window) = self.pinnacle.window_for_surface(root_surface) else { + return; + }; + let Some(output) = window.output(&self.pinnacle) else { + return; + }; + let Some(loc) = self.pinnacle.space.element_location(&window) else { + return; + }; + + let loc = loc - output.current_location(); + + self.backend.with_renderer(|renderer| { + window.capture_snapshot_and_store( + renderer, + loc, + output.current_scale().fractional_scale().into(), + 1.0, + ); + }); + + self.pinnacle + .root_surface_cache + .retain(|surf, root| surf != surface && root != surface); + } + fn client_compositor_state<'a>(&self, client: &'a Client) -> &'a CompositorClientState { if let Some(state) = client.get_data::() { return &state.compositor_state; @@ -302,23 +402,20 @@ delegate_compositor!(State); impl Pinnacle { fn ensure_initial_configure(&mut self, surface: &WlSurface) { - if let (Some(window), _) | (None, Some(window)) = ( - self.window_for_surface(surface), - self.new_window_for_surface(surface), - ) { + if let Some(window) = self.unmapped_window_for_surface(surface) { if let Some(toplevel) = window.toplevel() { let initial_configure_sent = compositor::with_states(surface, |states| { states .data_map .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") + .unwrap() .lock() - .expect("Failed to lock Mutex") + .unwrap() .initial_configure_sent }); if !initial_configure_sent { - tracing::debug!("Initial configure"); + tracing::debug!("Initial configure on wl_surface {:?}", surface.id()); toplevel.send_configure(); } } @@ -331,15 +428,15 @@ impl Pinnacle { states .data_map .get::() - .expect("XdgPopupSurfaceData wasn't in popup's data map") + .unwrap() .lock() - .expect("Failed to lock Mutex") + .unwrap() .initial_configure_sent }); if !initial_configure_sent { - popup - .send_configure() - .expect("popup initial configure failed"); + popup.send_configure().expect( + "sent configure for popup that doesn't allow multiple or is nonreactive", + ); } return; } @@ -355,9 +452,9 @@ impl Pinnacle { states .data_map .get::() - .expect("no LayerSurfaceData") + .unwrap() .lock() - .expect("failed to lock data") + .unwrap() .initial_configure_sent }); @@ -725,9 +822,9 @@ impl ForeignToplevelHandler for State { output.with_state(|state| { if state.tags.contains(&tag) { for op_tag in state.tags.iter() { - op_tag.set_active(false, self); + op_tag.set_active(false, &mut self.pinnacle); } - tag.set_active(true, self); + tag.set_active(true, &mut self.pinnacle); } }); } @@ -754,16 +851,7 @@ impl ForeignToplevelHandler for State { return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - - self.pinnacle.request_layout(&output); - self.schedule_render(&output); + self.set_window_fullscreen(&window, true); } fn unset_fullscreen(&mut self, wl_surface: WlSurface) { @@ -771,16 +859,7 @@ impl ForeignToplevelHandler for State { return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - - self.pinnacle.request_layout(&output); - self.schedule_render(&output); + self.set_window_fullscreen(&window, false); } fn set_maximized(&mut self, wl_surface: WlSurface) { @@ -788,16 +867,7 @@ impl ForeignToplevelHandler for State { return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - - self.pinnacle.request_layout(&output); - self.schedule_render(&output); + self.set_window_maximized(&window, true); } fn unset_maximized(&mut self, wl_surface: WlSurface) { @@ -805,16 +875,7 @@ impl ForeignToplevelHandler for State { return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - - self.pinnacle.request_layout(&output); - self.schedule_render(&output); + self.set_window_maximized(&window, false); } fn set_minimized(&mut self, wl_surface: WlSurface) { diff --git a/src/handlers/window.rs b/src/handlers/window.rs new file mode 100644 index 0000000..44687db --- /dev/null +++ b/src/handlers/window.rs @@ -0,0 +1,69 @@ +use crate::{ + render::util::snapshot::capture_snapshots_on_output, + state::{State, WithState}, + window::WindowElement, +}; + +impl State { + pub fn set_window_maximized(&mut self, window: &WindowElement, maximized: bool) { + let snapshots = window.output(&self.pinnacle).map(|output| { + self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, [window.clone()]) + }) + }); + + if maximized { + if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { + window.toggle_maximized(); + } + } else if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { + window.toggle_maximized(); + } + + if let Some(output) = window.output(&self.pinnacle) { + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + + self.pinnacle.request_layout(&output); + self.schedule_render(&output); + } + } + + pub fn set_window_fullscreen(&mut self, window: &WindowElement, fullscreen: bool) { + let snapshots = window.output(&self.pinnacle).map(|output| { + self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, [window.clone()]) + }) + }); + + if fullscreen { + if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { + window.toggle_fullscreen(); + } + } else if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { + window.toggle_fullscreen(); + } + + if let Some(output) = window.output(&self.pinnacle) { + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|op_state| { + op_state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + + self.pinnacle.request_layout(&output); + self.schedule_render(&output); + } + } +} diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index ba0c416..f59efc4 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -9,19 +9,23 @@ use smithay::{ reexports::{ wayland_protocols::xdg::shell::server::xdg_toplevel::{self, ResizeEdge}, wayland_server::{ - protocol::{wl_output::WlOutput, wl_seat::WlSeat}, - Resource, + protocol::{wl_output::WlOutput, wl_seat::WlSeat, wl_surface::WlSurface}, + DisplayHandle, Resource, }, }, utils::Serial, - wayland::shell::xdg::{ - PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, + wayland::{ + compositor::{self, BufferAssignment, SurfaceAttributes}, + shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, + }, }, }; use tracing::trace; use crate::{ focus::keyboard::KeyboardFocusTarget, + render::util::snapshot::capture_snapshots_on_output, state::{State, WithState}, window::WindowElement, }; @@ -33,6 +37,7 @@ impl XdgShellHandler for State { fn new_toplevel(&mut self, surface: ToplevelSurface) { surface.with_pending_state(|state| { + // state.size = Some((600, 400).into()); // gets wleird-slow-ack working state.states.set(xdg_toplevel::State::TiledTop); state.states.set(xdg_toplevel::State::TiledBottom); state.states.set(xdg_toplevel::State::TiledLeft); @@ -40,7 +45,7 @@ impl XdgShellHandler for State { }); let window = WindowElement::new(Window::new_wayland_window(surface.clone())); - self.pinnacle.new_windows.push(window); + self.pinnacle.unmapped_windows.push(window); } fn toplevel_destroyed(&mut self, surface: ToplevelSurface) { @@ -50,28 +55,32 @@ impl XdgShellHandler for State { return; }; - self.pinnacle.windows.retain(|win| win != &window); + let snapshots = if let Some(output) = window.output(&self.pinnacle) { + self.backend.with_renderer(|renderer| { + Some(capture_snapshots_on_output( + &mut self.pinnacle, + renderer, + &output, + [], + )) + }) + } else { + None + }; - self.pinnacle.z_index_stack.retain(|win| win != &window); - - for output in self.pinnacle.space.outputs() { - output.with_state_mut(|state| state.focus_stack.stack.retain(|win| win != &window)); - } + self.pinnacle.remove_window(&window, false); if let Some(output) = window.output(&self.pinnacle) { self.pinnacle.request_layout(&output); - let focus = self - .pinnacle - .focused_window(&output) - .map(KeyboardFocusTarget::Window); - - if let Some(KeyboardFocusTarget::Window(window)) = &focus { - tracing::debug!("Focusing on prev win"); - self.pinnacle.raise_window(window.clone(), true); - if let Some(toplevel) = window.toplevel() { - toplevel.send_configure(); - } + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|state| { + state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ); + }); } self.update_keyboard_focus(&output); @@ -232,53 +241,30 @@ impl XdgShellHandler for State { } surface.with_pending_state(|state| { - state.states.set(xdg_toplevel::State::Fullscreen); state.size = Some(geometry.size); state.fullscreen_output = wl_output; }); let Some(window) = self.pinnacle.window_for_surface(wl_surface) else { - tracing::error!("wl_surface had no window"); return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - self.pinnacle.request_layout(&output); - } + self.set_window_fullscreen(&window, true); } surface.send_configure(); } fn unfullscreen_request(&mut self, surface: ToplevelSurface) { - if !surface - .current_state() - .states - .contains(xdg_toplevel::State::Fullscreen) - { - return; - } - surface.with_pending_state(|state| { - state.states.unset(xdg_toplevel::State::Fullscreen); - state.size = None; state.fullscreen_output.take(); }); - surface.send_pending_configure(); - let Some(window) = self.pinnacle.window_for_surface(surface.wl_surface()) else { - tracing::error!("wl_surface had no window"); return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - if let Some(output) = window.output(&self.pinnacle) { - self.pinnacle.request_layout(&output); - } - } + self.set_window_fullscreen(&window, false); } fn maximize_request(&mut self, surface: ToplevelSurface) { @@ -286,14 +272,7 @@ impl XdgShellHandler for State { return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - self.pinnacle.request_layout(&output); + self.set_window_maximized(&window, true); } fn unmaximize_request(&mut self, surface: ToplevelSurface) { @@ -301,14 +280,7 @@ impl XdgShellHandler for State { return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } - - let Some(output) = window.output(&self.pinnacle) else { - return; - }; - self.pinnacle.request_layout(&output); + self.set_window_maximized(&window, false); } fn minimize_request(&mut self, _surface: ToplevelSurface) { @@ -321,3 +293,40 @@ impl XdgShellHandler for State { // TODO: impl the rest of the fns in XdgShellHandler } delegate_xdg_shell!(State); + +pub fn snapshot_pre_commit_hook( + state: &mut State, + _display_handle: &DisplayHandle, + surface: &WlSurface, +) { + let Some(window) = state.pinnacle.window_for_surface(surface) else { + return; + }; + + let got_unmapped = compositor::with_states(surface, |states| { + let buffer = &states.cached_state.pending::().buffer; + matches!(buffer, Some(BufferAssignment::Removed)) + }); + + if got_unmapped { + let Some(output) = window.output(&state.pinnacle) else { + return; + }; + let Some(loc) = state.pinnacle.space.element_location(&window) else { + return; + }; + + let loc = loc - output.current_location(); + + state.backend.with_renderer(|renderer| { + window.capture_snapshot_and_store( + renderer, + loc, + output.current_scale().fractional_scale().into(), + 1.0, + ); + }); + } else { + window.with_state_mut(|state| state.snapshot.take()); + } +} diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 970d678..a64522c 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -26,6 +26,7 @@ use tracing::{debug, error, trace, warn}; use crate::{ cursor::Cursor, focus::keyboard::KeyboardFocusTarget, + render::util::snapshot::capture_snapshots_on_output, state::{Pinnacle, State, WithState}, window::{window_state::FloatingOrTiled, WindowElement}, }; @@ -48,14 +49,7 @@ impl XwmHandler for State { } let window = WindowElement::new(Window::new_x11_window(surface)); - self.pinnacle - .space - .map_element(window.clone(), (0, 0), true); - let bbox = self - .pinnacle - .space - .element_bbox(&window) - .expect("called element_bbox on an unmapped window"); + let bbox = window.bbox(); let output_size = self .pinnacle @@ -83,16 +77,13 @@ impl XwmHandler for State { unreachable!() }; - self.pinnacle.space.map_element(window.clone(), loc, true); surface.set_mapped(true).expect("failed to map x11 window"); let bbox = Rectangle::from_loc_and_size(loc, bbox.size); - debug!("map_window_request, configuring with bbox {bbox:?}"); surface .configure(bbox) .expect("failed to configure x11 window"); - // TODO: ssd if let Some(output) = self.pinnacle.focused_output() { window.place_on_output(output); @@ -102,21 +93,41 @@ impl XwmHandler for State { window.with_state_mut(|state| { state.floating_or_tiled = FloatingOrTiled::Floating(bbox); }); + self.pinnacle.space.map_element(window.clone(), loc, true); } - // TODO: will an unmap -> map duplicate the window + // TODO: do snapshot and transaction here BUT ONLY IF TILED AND ON ACTIVE TAG + + let snapshots = if let Some(output) = window.output(&self.pinnacle) { + Some(self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) + })) + } else { + None + }; + self.pinnacle.windows.push(window.clone()); self.pinnacle.raise_window(window.clone(), true); self.pinnacle.apply_window_rules(&window); - if let Some(output) = window.output(&self.pinnacle) { - output.with_state_mut(|state| state.focus_stack.set_focus(window.clone())); - self.pinnacle.request_layout(&output); + if window.is_on_active_tag() { + if let Some(output) = window.output(&self.pinnacle) { + output.with_state_mut(|state| state.focus_stack.set_focus(window.clone())); + self.update_keyboard_focus(&output); - self.pinnacle.loop_handle.insert_idle(move |state| { - state.update_keyboard_focus(&output); - }); + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|state| { + state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); + } + + self.pinnacle.request_layout(&output); + } } } @@ -238,10 +249,6 @@ impl XwmHandler for State { } fn maximize_request(&mut self, _xwm: XwmId, window: X11Surface) { - window - .set_maximized(true) - .expect("failed to set x11 win to maximized"); - let Some(window) = window .wl_surface() .and_then(|surf| self.pinnacle.window_for_surface(&surf)) @@ -249,16 +256,10 @@ impl XwmHandler for State { return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } + self.set_window_maximized(&window, true); } fn unmaximize_request(&mut self, _xwm: XwmId, window: X11Surface) { - window - .set_maximized(false) - .expect("failed to set x11 win to maximized"); - let Some(window) = window .wl_surface() .and_then(|surf| self.pinnacle.window_for_surface(&surf)) @@ -266,16 +267,10 @@ impl XwmHandler for State { return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_maximized()) { - window.toggle_maximized(); - } + self.set_window_maximized(&window, false); } fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { - window - .set_fullscreen(true) - .expect("failed to set x11 win to fullscreen"); - let Some(window) = window .wl_surface() .and_then(|surf| self.pinnacle.window_for_surface(&surf)) @@ -283,19 +278,10 @@ impl XwmHandler for State { return; }; - if !window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - if let Some(output) = window.output(&self.pinnacle) { - self.pinnacle.request_layout(&output); - } - } + self.set_window_fullscreen(&window, true); } fn unfullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) { - window - .set_fullscreen(false) - .expect("failed to set x11 win to unfullscreen"); - let Some(window) = window .wl_surface() .and_then(|surf| self.pinnacle.window_for_surface(&surf)) @@ -303,12 +289,7 @@ impl XwmHandler for State { return; }; - if window.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { - window.toggle_fullscreen(); - if let Some(output) = window.output(&self.pinnacle) { - self.pinnacle.request_layout(&output); - } - } + self.set_window_fullscreen(&window, true); } fn resize_request( @@ -426,42 +407,37 @@ impl XwmHandler for State { impl State { fn remove_xwayland_window(&mut self, surface: X11Surface) { + tracing::debug!("remove_xwayland_window"); let win = self .pinnacle .windows .iter() .find(|elem| elem.x11_surface() == Some(&surface)) .cloned(); - if let Some(win) = win { debug!("removing x11 window from windows"); - for output in self.pinnacle.space.outputs() { - output.with_state_mut(|state| { - state.focus_stack.stack.retain(|w| w != &win); - }); - } - self.pinnacle.windows.retain(|w| w != &win); + let snapshots = win.output(&self.pinnacle).map(|output| { + self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) + }) + }); - self.pinnacle.z_index_stack.retain(|w| w != &win); + self.pinnacle.remove_window(&win, false); if let Some(output) = win.output(&self.pinnacle) { - self.pinnacle.request_layout(&output); - - let focus = self - .pinnacle - .focused_window(&output) - .map(KeyboardFocusTarget::Window); - - if let Some(KeyboardFocusTarget::Window(win)) = &focus { - self.pinnacle.raise_window(win.clone(), true); - if let Some(toplevel) = win.toplevel() { - toplevel.send_configure(); - } + if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots.flatten() { + output.with_state_mut(|state| { + state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + ) + }); } + self.pinnacle.request_layout(&output); self.update_keyboard_focus(&output); - self.schedule_render(&output); } } diff --git a/src/layout.rs b/src/layout.rs index 2815ae8..7e9f8fe 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,13 +1,14 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::{collections::HashMap, time::Duration}; +pub mod transaction; + +use std::collections::HashMap; use pinnacle_api_defs::pinnacle::layout::v0alpha1::{layout_request::Geometries, LayoutResponse}; use smithay::{ desktop::{layer_map_for_output, WindowSurface}, output::Output, - utils::{Logical, Point, Rectangle, Serial}, - wayland::{compositor, shell::xdg::XdgToplevelSurfaceData}, + utils::{Logical, Rectangle, Serial}, }; use tokio::sync::mpsc::UnboundedSender; use tonic::Status; @@ -15,6 +16,7 @@ use tracing::warn; use crate::{ output::OutputName, + render::util::snapshot::capture_snapshots_on_output, state::{Pinnacle, State, WithState}, window::{ window_state::{FloatingOrTiled, FullscreenOrMaximized}, @@ -22,12 +24,14 @@ use crate::{ }, }; +use self::transaction::LayoutTransaction; + impl Pinnacle { fn update_windows_with_geometries( &mut self, output: &Output, geometries: Vec>, - ) { + ) -> Vec<(WindowElement, Serial)> { let windows_on_foc_tags = output.with_state(|state| { let focused_tags = state.focused_tags().collect::>(); self.windows @@ -94,56 +98,24 @@ impl Pinnacle { } let mut pending_wins = Vec::<(WindowElement, Serial)>::new(); - let mut non_pending_wins = Vec::<(Point, WindowElement)>::new(); for win in windows_on_foc_tags.iter() { - if win.with_state(|state| state.target_loc.is_some()) { - match win.underlying_surface() { - WindowSurface::Wayland(toplevel) => { - let pending = compositor::with_states(toplevel.wl_surface(), |states| { - states - .data_map - .get::() - .expect("XdgToplevelSurfaceData wasn't in surface's data map") - .lock() - .expect("Failed to lock Mutex") - .has_pending_changes() - }); - - if pending { - pending_wins.push((win.clone(), toplevel.send_configure())) - } else { - let loc = win.with_state_mut(|state| state.target_loc.take()); - if let Some(loc) = loc { - non_pending_wins.push((loc, win.clone())); - } - } - } - WindowSurface::X11(_) => { - let loc = win.with_state_mut(|state| state.target_loc.take()); - if let Some(loc) = loc { - self.space.map_element(win.clone(), loc, false); - } - } + if let WindowSurface::Wayland(toplevel) = win.underlying_surface() { + if let Some(serial) = toplevel.send_pending_configure() { + pending_wins.push((win.clone(), serial)); } } + + // TODO: get rid of target_loc + let loc = win.with_state_mut(|state| state.target_loc.take()); + if let Some(loc) = loc { + self.space.map_element(win.clone(), loc, false); + } } - for (loc, window) in non_pending_wins { - self.space.map_element(window, loc, false); - } - - // FIXME: - // We are sending frames here to get offscreen windows to commit and map. - // Obviously this is a bad way to do this but its a bandaid solution - // until decent transactional layout applications are implemented. - for (win, _serial) in pending_wins { - win.send_frame(output, self.clock.now(), Some(Duration::ZERO), |_, _| { - Some(output.clone()) - }); - } - self.fixup_z_layering(); + + pending_wins } /// Swaps two windows in the main window vec and updates all windows. @@ -295,9 +267,27 @@ impl State { .fulfilled_requests .insert(output.clone(), current_pending); - self.pinnacle + let snapshots = self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) + }); + + let pending_windows = self + .pinnacle .update_windows_with_geometries(&output, geometries); + output.with_state_mut(|state| { + if let Some(ts) = state.layout_transaction.as_mut() { + ts.update_pending(pending_windows); + } else if let Some((fs_and_up_snapshots, under_fs_snapshots)) = snapshots { + state.layout_transaction = Some(LayoutTransaction::new( + self.pinnacle.loop_handle.clone(), + fs_and_up_snapshots, + under_fs_snapshots, + pending_windows, + )); + } + }); + self.schedule_render(&output); self.pinnacle.layout_state.pending_swap = false; diff --git a/src/layout/transaction.rs b/src/layout/transaction.rs new file mode 100644 index 0000000..771f8e9 --- /dev/null +++ b/src/layout/transaction.rs @@ -0,0 +1,213 @@ +//! Layout transactions. + +use std::{ + collections::HashMap, + time::{Duration, Instant}, +}; + +use smithay::{ + backend::renderer::element::{ + self, + surface::WaylandSurfaceRenderElement, + texture::{TextureBuffer, TextureRenderElement}, + utils::RescaleRenderElement, + }, + desktop::Space, + reexports::calloop::{ + timer::{TimeoutAction, Timer}, + LoopHandle, + }, + utils::{Logical, Point, Scale, Serial, Transform}, +}; + +use crate::{ + pinnacle_render_elements, + render::{ + texture::CommonTextureRenderElement, util::snapshot::RenderSnapshot, AsGlesRenderer, + PRenderer, + }, + state::State, + window::WindowElement, +}; + +/// The timeout before transactions stop applying. +const TIMEOUT: Duration = Duration::from_millis(150); + +/// Type for window snapshots. +pub type LayoutSnapshot = RenderSnapshot; + +pinnacle_render_elements! { + /// Render elements for an output snapshot + #[derive(Debug)] + pub enum SnapshotRenderElement { + /// Draw the window itself. + Window = WaylandSurfaceRenderElement, + /// Draw a snapshot of the window. + Snapshot = RescaleRenderElement, + } +} + +/// Specifier for snapshots +#[derive(Debug)] +pub enum SnapshotTarget { + /// Render a window. + Window(WindowElement), + /// Render a snapshot. + Snapshot(LayoutSnapshot), +} + +/// A layout transaction. +/// +/// While one is active on an output, its snapshots will be drawn instead of windows. +#[derive(Debug)] +pub struct LayoutTransaction { + /// The loop handle to schedule event loop wakeups. + loop_handle: LoopHandle<'static, State>, + /// The instant this transaction started. + /// + /// Used for transaction timeout. + start_time: Instant, + /// The snapshots to render while the transaction is processing. + pub fullscreen_and_up_snapshots: Vec, + /// The snapshots to render while the transaction is processing. + pub under_fullscreen_snapshots: Vec, + /// The windows that the transaction is waiting on. + pending_windows: HashMap, + /// Wait for an update to the windows this transaction is waiting on + /// in anticipation of a new layout. + wait: bool, +} + +impl LayoutTransaction { + /// Schedule an event after the timeout to check for readiness. + fn register_wakeup(loop_handle: &LoopHandle<'static, State>) { + let _ = loop_handle.insert_source( + Timer::from_duration(TIMEOUT + Duration::from_millis(10)), + |_, _, _| TimeoutAction::Drop, + ); + } + + /// Creates a new layout transaction that will become immediately active. + pub fn new( + loop_handle: LoopHandle<'static, State>, + fullscreen_and_up_snapshots: impl IntoIterator, + under_fullscreen_snapshots: impl IntoIterator, + pending_windows: impl IntoIterator, + ) -> Self { + Self::register_wakeup(&loop_handle); + Self { + loop_handle, + start_time: Instant::now(), + fullscreen_and_up_snapshots: fullscreen_and_up_snapshots.into_iter().collect(), + under_fullscreen_snapshots: under_fullscreen_snapshots.into_iter().collect(), + pending_windows: pending_windows.into_iter().collect(), + wait: false, + } + } + + /// Wait for the next pending window update. + pub fn wait(&mut self) { + self.wait = true; + self.start_time = Instant::now(); + Self::register_wakeup(&self.loop_handle); + } + + /// Creates a new layout transaction that waits for the next update to pending windows. + pub fn new_and_wait( + loop_handle: LoopHandle<'static, State>, + fullscreen_and_up_snapshots: impl IntoIterator, + under_fullscreen_snapshots: impl IntoIterator, + ) -> Self { + Self::register_wakeup(&loop_handle); + Self { + loop_handle, + start_time: Instant::now(), + fullscreen_and_up_snapshots: fullscreen_and_up_snapshots.into_iter().collect(), + under_fullscreen_snapshots: under_fullscreen_snapshots.into_iter().collect(), + pending_windows: HashMap::new(), + wait: true, + } + } + + /// Updates the pending windows for this transaction, for example + /// when a new layout comes in while a transaction is already processing. + pub fn update_pending( + &mut self, + pending_windows: impl IntoIterator, + ) { + self.pending_windows = pending_windows.into_iter().collect(); + self.wait = false; + self.start_time = Instant::now(); + Self::register_wakeup(&self.loop_handle); + } + + /// Returns whether all pending windows have committed their serials or the timeout has been + /// reached. + pub fn ready(&self) -> bool { + Instant::now().duration_since(self.start_time) >= TIMEOUT + || (!self.wait + && self + .pending_windows + .iter() + .all(|(win, serial)| win.is_serial_committed(*serial))) + } + + /// Render elements for this transaction, split into ones for windows fullscreen and up + /// and the rest. + /// + /// Window targets will be rendered normally and snapshot targets will + /// render their texture. + pub fn render_elements( + &self, + renderer: &mut R, + space: &Space, + output_loc: Point, + scale: Scale, + alpha: f32, + ) -> (Vec>, Vec>) { + let mut flat_map = |snapshot: &SnapshotTarget| match snapshot { + SnapshotTarget::Window(window) => { + let loc = space.element_location(window).unwrap_or_default() - output_loc; + window + .render_elements(renderer, loc, scale, alpha) + .into_iter() + .map(SnapshotRenderElement::Window) + .collect() + } + SnapshotTarget::Snapshot(snapshot) => { + let Some((texture, loc)) = snapshot.texture(renderer.as_gles_renderer()) else { + return Vec::new(); + }; + let buffer = + TextureBuffer::from_texture(renderer, texture, 1, Transform::Normal, None); + let elem = TextureRenderElement::from_texture_buffer( + loc.to_f64(), + &buffer, + Some(alpha), + None, + None, + element::Kind::Unspecified, + ); + + let common = CommonTextureRenderElement::new(elem); + + let scale = Scale::from((1.0 / scale.x, 1.0 / scale.y)); + + vec![SnapshotRenderElement::Snapshot( + RescaleRenderElement::from_element(common, loc, scale), + )] + } + }; + + ( + self.fullscreen_and_up_snapshots + .iter() + .flat_map(&mut flat_map) + .collect(), + self.under_fullscreen_snapshots + .iter() + .flat_map(&mut flat_map) + .collect(), + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index eb9c4b9..3b0e08e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![warn(clippy::unwrap_used)] - pub mod api; pub mod backend; pub mod cli; diff --git a/src/output.rs b/src/output.rs index a5e6568..ab869cc 100644 --- a/src/output.rs +++ b/src/output.rs @@ -6,15 +6,18 @@ use pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputRe use smithay::{ desktop::layer_map_for_output, output::{Mode, Output, Scale}, + reexports::calloop::LoopHandle, utils::{Logical, Point, Transform}, wayland::session_lock::LockSurface, }; use crate::{ focus::WindowKeyboardFocusStack, + layout::transaction::{LayoutTransaction, SnapshotTarget}, protocol::screencopy::Screencopy, - state::{Pinnacle, WithState}, + state::{Pinnacle, State, WithState}, tag::Tag, + window::window_state::FloatingOrTiled, }; /// A unique identifier for an output. @@ -57,6 +60,8 @@ pub struct OutputState { pub modes: Vec, pub lock_surface: Option, pub blanking_state: BlankingState, + /// A pending layout transaction. + pub layout_transaction: Option, } impl WithState for Output { @@ -89,6 +94,23 @@ impl OutputState { pub fn focused_tags(&self) -> impl Iterator { self.tags.iter().filter(|tag| tag.active()) } + + pub fn new_wait_layout_transaction( + &mut self, + loop_handle: LoopHandle<'static, State>, + fullscreen_and_up_snapshots: impl IntoIterator, + under_fullscreen_snapshots: impl IntoIterator, + ) { + if let Some(ts) = self.layout_transaction.as_mut() { + ts.wait(); + } else { + self.layout_transaction = Some(LayoutTransaction::new_and_wait( + loop_handle, + fullscreen_and_up_snapshots, + under_fullscreen_snapshots, + )); + } + } } impl Pinnacle { @@ -102,6 +124,8 @@ impl Pinnacle { scale: Option, location: Option>, ) { + let old_scale = output.current_scale().fractional_scale(); + output.change_current_state(mode, transform, scale, location); if let Some(location) = location { self.space.map_output(output, location); @@ -129,6 +153,41 @@ impl Pinnacle { output.with_state_mut(|state| state.modes.push(mode)); } + if let Some(scale) = scale { + let pos_multiplier = old_scale / scale.fractional_scale(); + + for win in self + .windows + .iter() + .filter(|win| win.output(self).as_ref() == Some(output)) + .filter(|win| win.with_state(|state| state.floating_or_tiled.is_floating())) + .cloned() + .collect::>() + { + let Some(output) = win.output(self) else { unreachable!() }; + + let output_loc = output.current_location(); + + // FIXME: get everything out of this with_state + win.with_state_mut(|state| { + let FloatingOrTiled::Floating(rect) = &mut state.floating_or_tiled else { + unreachable!() + }; + + let loc = rect.loc; + + let mut loc_relative_to_output = loc - output_loc; + loc_relative_to_output = loc_relative_to_output + .to_f64() + .upscale(pos_multiplier) + .to_i32_round(); + + rect.loc = loc_relative_to_output + output_loc; + self.space.map_element(win.clone(), rect.loc, false); + }); + } + } + if let Some(lock_surface) = output.with_state(|state| state.lock_surface.clone()) { lock_surface.with_pending_state(|state| { let Some(new_geo) = self.space.output_geometry(output) else { diff --git a/src/render.rs b/src/render.rs index 08a0e60..e3bea24 100644 --- a/src/render.rs +++ b/src/render.rs @@ -1,10 +1,16 @@ // SPDX-License-Identifier: GPL-3.0-or-later +pub mod pointer; +pub mod render_elements; +pub mod texture; +pub mod util; + use std::{ops::Deref, sync::Mutex}; use smithay::{ backend::renderer::{ element::{surface::WaylandSurfaceRenderElement, AsRenderElements, RenderElementStates}, + gles::GlesRenderer, ImportAll, ImportMem, Renderer, Texture, }, desktop::{ @@ -14,70 +20,180 @@ use smithay::{ surface_presentation_feedback_flags_from_states, surface_primary_scanout_output, OutputPresentationFeedback, }, - Space, + PopupManager, Space, WindowSurface, }, input::pointer::{CursorImageAttributes, CursorImageStatus}, output::Output, reexports::wayland_server::protocol::wl_surface::WlSurface, - render_elements, - utils::{Logical, Physical, Point, Scale}, + utils::{Logical, Point, Scale}, wayland::{compositor, shell::wlr_layer}, }; use crate::{ - backend::Backend, + backend::{udev::UdevRenderer, Backend}, + layout::transaction::{LayoutTransaction, SnapshotRenderElement, SnapshotTarget}, + pinnacle_render_elements, state::{State, WithState}, window::WindowElement, }; -use self::pointer::{PointerElement, PointerRenderElement}; - -pub mod pointer; +use self::{ + pointer::{PointerElement, PointerRenderElement}, + texture::CommonTextureRenderElement, + util::surface::texture_render_elements_from_surface_tree, +}; pub const CLEAR_COLOR: [f32; 4] = [0.6, 0.6, 0.6, 1.0]; pub const CLEAR_COLOR_LOCKED: [f32; 4] = [0.2, 0.0, 0.3, 1.0]; -render_elements! { - pub OutputRenderElement where R: ImportAll + ImportMem; - Surface = WaylandSurfaceRenderElement, - Pointer = PointerRenderElement, -} - -impl AsRenderElements for WindowElement -where - R: Renderer + ImportAll + ImportMem, - ::TextureId: Texture + Clone + 'static, -{ - type RenderElement = WaylandSurfaceRenderElement; - - fn render_elements>( - &self, - renderer: &mut R, - location: Point, - scale: Scale, - alpha: f32, - ) -> Vec { - self.deref() - .render_elements(renderer, location, scale, alpha) +pinnacle_render_elements! { + #[derive(Debug)] + pub enum OutputRenderElement { + Surface = WaylandSurfaceRenderElement, + Pointer = PointerRenderElement, + Snapshot = SnapshotRenderElement, } } -struct LayerRenderElements { +/// Trait to reduce bound specifications. +pub trait PRenderer +where + Self: Renderer + ImportAll + ImportMem, + ::TextureId: Texture + Clone + 'static, +{ + // Self::TextureId: Texture + Clone + 'static doesn't work in the where clause, + // which is why these associated types exist. + // + // From https://github.com/YaLTeR/niri/blob/ae7fb4c4f405aa0ff49930040d414581a812d938/src/render_helpers/renderer.rs#L10 + type PTextureId: Texture + Clone + 'static; + type PError: std::error::Error + Send + Sync + 'static; +} + +impl PRenderer for R +where + R: ImportAll + ImportMem, + R::TextureId: Texture + Clone + 'static, + R::Error: std::error::Error + Send + Sync + 'static, +{ + type PTextureId = R::TextureId; + type PError = R::Error; +} + +/// Trait for renderers that provide [`GlesRenderer`]s. +pub trait AsGlesRenderer { + /// Gets a [`GlesRenderer`] from this renderer. + fn as_gles_renderer(&mut self) -> &mut GlesRenderer; +} + +impl AsGlesRenderer for GlesRenderer { + fn as_gles_renderer(&mut self) -> &mut GlesRenderer { + self + } +} + +impl<'a> AsGlesRenderer for UdevRenderer<'a> { + fn as_gles_renderer(&mut self) -> &mut GlesRenderer { + self.as_mut() + } +} + +impl WindowElement { + /// Render elements for this window at the given *logical* location in the space, + /// output-relative. + pub fn render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec> { + let location = location - self.geometry().loc; + let phys_loc = location.to_f64().to_physical_precise_round(scale); + self.deref() + .render_elements(renderer, phys_loc, scale, alpha) + } + + /// Render elements for this window as textures. + pub fn texture_render_elements( + &self, + renderer: &mut R, + location: Point, + scale: Scale, + alpha: f32, + ) -> Vec { + let location = location - self.geometry().loc; + let location = location.to_f64().to_physical_precise_round(scale); + + match self.underlying_surface() { + WindowSurface::Wayland(s) => { + let mut render_elements = Vec::new(); + let surface = s.wl_surface(); + let popup_render_elements = + PopupManager::popups_for_surface(surface).flat_map(|(popup, popup_offset)| { + let offset = (self.geometry().loc + popup_offset - popup.geometry().loc) + .to_physical_precise_round(scale); + + texture_render_elements_from_surface_tree( + renderer.as_gles_renderer(), + popup.wl_surface(), + location + offset, + scale, + alpha, + ) + }); + + render_elements.extend( + popup_render_elements + .into_iter() + .map(CommonTextureRenderElement::new), + ); + + render_elements.extend( + texture_render_elements_from_surface_tree( + renderer.as_gles_renderer(), + surface, + location, + scale, + alpha, + ) + .into_iter() + .map(CommonTextureRenderElement::new), + ); + + render_elements + } + WindowSurface::X11(s) => { + if let Some(surface) = s.wl_surface() { + texture_render_elements_from_surface_tree( + renderer.as_gles_renderer(), + &surface, + location, + scale, + alpha, + ) + .into_iter() + .map(CommonTextureRenderElement::new) + .collect() + } else { + Vec::new() + } + } + } + } +} + +struct LayerRenderElements { background: Vec>, bottom: Vec>, top: Vec>, overlay: Vec>, } -fn layer_render_elements( +fn layer_render_elements( output: &Output, renderer: &mut R, scale: Scale, -) -> LayerRenderElements -where - R: Renderer + ImportAll, - ::TextureId: Clone + 'static, -{ +) -> LayerRenderElements { let layer_map = layer_map_for_output(output); let mut overlay = vec![]; let mut top = vec![]; @@ -119,38 +235,27 @@ where /// /// ret.1 contains render elements for the windows at and above the first fullscreen window. /// ret.2 contains the rest. -fn window_render_elements( +fn window_render_elements( output: &Output, windows: &[WindowElement], space: &Space, renderer: &mut R, scale: Scale, -) -> (Vec>, Vec>) -where - R: Renderer + ImportAll + ImportMem, - ::TextureId: Clone + 'static, -{ +) -> (Vec>, Vec>) { let mut last_fullscreen_split_at = 0; let mut fullscreen_and_up = windows .iter() .rev() // rev because I treat the focus stack backwards vs how the renderer orders it - .filter(|win| win.is_on_active_tag()) .enumerate() .map(|(i, win)| { if win.with_state(|state| state.fullscreen_or_maximized.is_fullscreen()) { last_fullscreen_split_at = i + 1; } - // subtract win.geometry().loc to align decorations correctly - let loc = ( - space.element_location(win) .unwrap_or((0, 0).into()) - - win.geometry().loc - - output.current_location() - ) - .to_physical_precise_round(scale); + let loc = space.element_location(win).unwrap_or_default() - output.current_location(); - win.render_elements::>(renderer, loc, scale, 1.0) + win.render_elements(renderer, loc, scale, 1.0) .into_iter() .map(OutputRenderElement::from) }).collect::>(); @@ -163,7 +268,7 @@ where ) } -pub fn pointer_render_elements( +pub fn pointer_render_elements( output: &Output, renderer: &mut R, space: &Space, @@ -171,11 +276,7 @@ pub fn pointer_render_elements( cursor_status: &mut CursorImageStatus, dnd_icon: Option<&WlSurface>, pointer_element: &PointerElement<::TextureId>, -) -> Vec> -where - R: Renderer + ImportAll, - ::TextureId: Clone + 'static, -{ +) -> Vec> { let mut output_render_elements = Vec::new(); let Some(output_geometry) = space.output_geometry(output) else { @@ -222,21 +323,54 @@ where output_render_elements } +/// Render elements for any pending layout transaction. +/// +/// Returns fullscreen_and_up elements then under_fullscreen elements. +fn layout_transaction_render_elements( + transaction: &LayoutTransaction, + space: &Space, + renderer: &mut R, + scale: Scale, + output_loc: Point, +) -> (Vec>, Vec>) { + let mut flat_map = |target: &SnapshotTarget| match target { + SnapshotTarget::Window(win) => { + let loc = space.element_location(win).unwrap_or_default() - output_loc; + win.render_elements(renderer, loc, scale, 1.0) + .into_iter() + .map(SnapshotRenderElement::from) + .collect::>() + } + SnapshotTarget::Snapshot(snapshot) => snapshot + .render_elements(renderer, scale, 1.0) + .into_iter() + .collect(), + }; + + ( + transaction + .fullscreen_and_up_snapshots + .iter() + .flat_map(&mut flat_map) + .collect::>(), + transaction + .under_fullscreen_snapshots + .iter() + .flat_map(&mut flat_map) + .collect::>(), + ) +} + /// Generate render elements for the given output. /// /// Render elements will be pulled from the provided windows, /// with the first window being at the top and subsequent ones beneath. -pub fn output_render_elements( +pub fn output_render_elements( output: &Output, renderer: &mut R, space: &Space, windows: &[WindowElement], -) -> Vec> -where - R: Renderer + ImportAll + ImportMem, - ::TextureId: 'static, - T: Texture + Clone, -{ +) -> Vec> { let scale = Scale::from(output.current_scale().fractional_scale()); let mut output_render_elements: Vec> = Vec::new(); @@ -246,6 +380,11 @@ where .cloned() .partition::, _>(|win| !win.is_x11_override_redirect()); + let windows = windows + .into_iter() + .filter(|win| win.is_on_active_tag()) + .collect::>(); + // // draw input method surface if any // let rectangle = input_method.coordinates(); // let position = Point::from(( @@ -262,17 +401,19 @@ where // )); // }); - let o_r_elements = override_redirect_windows.iter().flat_map(|surf| { - surf.render_elements::>( - renderer, - space - .element_location(surf) - .unwrap_or((0, 0).into()) - .to_physical_precise_round(scale), - scale, - 1.0, - ) - }); + let output_loc = output.current_location(); + + let o_r_elements = override_redirect_windows + .iter() + .filter(|win| win.is_on_active_tag_on_output(output)) + .flat_map(|surf| { + surf.render_elements( + renderer, + space.element_location(surf).unwrap_or_default() - output_loc, + scale, + 1.0, + ) + }); // TODO: don't unconditionally render OR windows above fullscreen ones, // | base it on if it's a descendant or not @@ -285,8 +426,28 @@ where overlay, } = layer_render_elements(output, renderer, scale); - let (fullscreen_and_up_elements, rest_of_window_elements) = - window_render_elements::(output, &windows, space, renderer, scale); + let fullscreen_and_up_elements; + let rest_of_window_elements; + + // If there is a snapshot, render its elements instead + if let Some((fs_and_up_elements, under_fs_elements)) = output.with_state(|state| { + state + .layout_transaction + .as_ref() + .map(|ts| layout_transaction_render_elements(ts, space, renderer, scale, output_loc)) + }) { + fullscreen_and_up_elements = fs_and_up_elements + .into_iter() + .map(OutputRenderElement::from) + .collect(); + rest_of_window_elements = under_fs_elements + .into_iter() + .map(OutputRenderElement::from) + .collect(); + } else { + (fullscreen_and_up_elements, rest_of_window_elements) = + window_render_elements::(output, &windows, space, renderer, scale); + } // Elements render from top to bottom @@ -335,7 +496,7 @@ pub fn take_presentation_feedback( } impl State { - /// Schedule a new render. This does nothing on the winit backend. + /// Schedule a new render. pub fn schedule_render(&mut self, output: &Output) { match &mut self.backend { Backend::Udev(udev) => { diff --git a/src/render/pointer.rs b/src/render/pointer.rs index 79dce35..523e367 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -15,6 +15,8 @@ use smithay::{ utils::{Physical, Point, Scale}, }; +use super::PRenderer; + pub struct PointerElement { texture: Option>, status: CursorImageStatus, @@ -50,16 +52,13 @@ impl PointerElement { } render_elements! { + #[derive(Debug)] pub PointerRenderElement where R: ImportAll; Surface=WaylandSurfaceRenderElement, Texture=TextureRenderElement<::TextureId>, } -impl AsRenderElements for PointerElement -where - T: Texture + Clone + 'static, - R: Renderer + ImportAll, -{ +impl AsRenderElements for PointerElement { type RenderElement = PointerRenderElement; fn render_elements>( diff --git a/src/render/render_elements.rs b/src/render/render_elements.rs new file mode 100644 index 0000000..4585332 --- /dev/null +++ b/src/render/render_elements.rs @@ -0,0 +1,229 @@ +/// A custom implementation of [`smithay::render_elements`] that is not generic but rather +/// implements over the three used renderers. +/// +/// This is needed to allow GlesTextures to be easily rendered on winit and udev. +/// +/// Also idea from Niri. Ya know this whole compositor is slowly inching towards +/// being a Niri clone lol +#[macro_export] +macro_rules! pinnacle_render_elements { + ( + $(#[$attr:meta])* + $vis:vis enum $name:ident { + $( $(#[$variant_attr:meta])* $variant:ident = $type:ty),+ $(,)? + } + ) => { + $(#[$attr])* + $vis enum $name { + $( $(#[$variant_attr])* $variant($type)),+ + } + + $(impl From<$type> for $name { + fn from(x: $type) -> Self { + Self::$variant(x) + } + })+ + + $crate::pinnacle_render_elements! { + @impl $name ($name) () => { $($variant = $type),+ } + } + }; + + ( + $(#[$attr:meta])* + $vis:vis enum $name:ident<$generic_name:ident> { + $( $(#[$variant_attr:meta])* $variant:ident = $type:ty),+ $(,)? + } + ) => { + $(#[$attr])* + $vis enum $name<$generic_name> + where + $generic_name: ::smithay::backend::renderer::Renderer + ::smithay::backend::renderer::ImportAll + ::smithay::backend::renderer::ImportMem, + $generic_name::TextureId: 'static, + { + $( $(#[$variant_attr])* $variant($type)),+ + } + + $(impl<$generic_name> From<$type> for $name<$generic_name> + where + $generic_name: ::smithay::backend::renderer::Renderer + ::smithay::backend::renderer::ImportAll + ::smithay::backend::renderer::ImportMem, + $generic_name::TextureId: 'static, + { + fn from(x: $type) -> Self { + Self::$variant(x) + } + })+ + + $crate::pinnacle_render_elements! { + @impl $name () ($name<$generic_name>) => { $($variant = $type),+ } + } + }; + + (@impl $name:ident ($($name_no_generic:ident)?) ($($name_generic:ident<$generic:ident>)?) => { + $($variant:ident = $type:ty),+ + }) => { + impl$(<$generic>)? ::smithay::backend::renderer::element::Element for $name$(<$generic>)? + $(where + $generic: ::smithay::backend::renderer::Renderer + ::smithay::backend::renderer::ImportAll + ::smithay::backend::renderer::ImportMem, + $generic::TextureId: 'static,)? + { + fn id(&self) -> &::smithay::backend::renderer::element::Id { + match self { + $($name::$variant(elem) => elem.id()),+ + } + } + + fn current_commit(&self) -> ::smithay::backend::renderer::utils::CommitCounter { + match self { + $($name::$variant(elem) => elem.current_commit()),+ + } + } + + fn geometry( + &self, + scale: ::smithay::utils::Scale + ) -> ::smithay::utils::Rectangle { + match self { + $($name::$variant(elem) => elem.geometry(scale)),+ + } + } + + fn transform(&self) -> ::smithay::utils::Transform { + match self { + $($name::$variant(elem) => elem.transform()),+ + } + } + + fn src(&self) -> ::smithay::utils::Rectangle { + match self { + $($name::$variant(elem) => elem.src()),+ + } + } + + fn damage_since( + &self, + scale: ::smithay::utils::Scale, + commit: ::std::option::Option<::smithay::backend::renderer::utils::CommitCounter>, + ) -> ::smithay::backend::renderer::utils::DamageSet { + match self { + $($name::$variant(elem) => elem.damage_since(scale, commit)),+ + } + } + + fn opaque_regions( + &self, + scale: ::smithay::utils::Scale, + ) -> ::smithay::backend::renderer::utils::OpaqueRegions { + match self { + $($name::$variant(elem) => elem.opaque_regions(scale)),+ + } + } + + fn alpha(&self) -> f32 { + match self { + $($name::$variant(elem) => elem.alpha()),+ + } + } + + fn kind(&self) -> ::smithay::backend::renderer::element::Kind { + match self { + $($name::$variant(elem) => elem.kind()),+ + } + } + } + + impl ::smithay::backend::renderer::element::RenderElement<::smithay::backend::renderer::gles::GlesRenderer> + for $($name_generic<::smithay::backend::renderer::gles::GlesRenderer>)? $($name_no_generic)? + { + fn draw( + &self, + frame: &mut ::smithay::backend::renderer::gles::GlesFrame<'_>, + src: ::smithay::utils::Rectangle, + dst: ::smithay::utils::Rectangle, + damage: &[::smithay::utils::Rectangle], + ) -> ::std::result::Result<(), ::smithay::backend::renderer::gles::GlesError> { + match self { + $($name::$variant(elem) => { + ::smithay::backend::renderer::element::RenderElement::< + ::smithay::backend::renderer::gles::GlesRenderer + >::draw(elem, frame, src, dst, damage) + })+ + } + } + + fn underlying_storage( + &self, + renderer: &mut ::smithay::backend::renderer::gles::GlesRenderer + ) -> ::std::option::Option<::smithay::backend::renderer::element::UnderlyingStorage> { + match self { + $($name::$variant(elem) => elem.underlying_storage(renderer)),+ + } + } + } + + impl<'a> ::smithay::backend::renderer::element::RenderElement<$crate::backend::udev::UdevRenderer<'a>> + for $($name_generic<$crate::backend::udev::UdevRenderer<'a>>)? $($name_no_generic)? + { + fn draw( + &self, + frame: &mut <$crate::backend::udev::UdevRenderer<'a> as ::smithay::backend::renderer::Renderer>::Frame<'_>, + src: ::smithay::utils::Rectangle, + dst: ::smithay::utils::Rectangle, + damage: &[::smithay::utils::Rectangle], + ) -> ::std::result::Result< + (), + <$crate::backend::udev::UdevRenderer as ::smithay::backend::renderer::Renderer>::Error, + > { + match self { + $($name::$variant(elem) => { + ::smithay::backend::renderer::element::RenderElement::< + $crate::backend::udev::UdevRenderer + >::draw(elem, frame, src, dst, damage) + })+ + } + } + + fn underlying_storage( + &self, + renderer: &mut $crate::backend::udev::UdevRenderer<'a>, + ) -> ::std::option::Option<::smithay::backend::renderer::element::UnderlyingStorage> { + match self { + $($name::$variant(elem) => elem.underlying_storage(renderer)),+ + } + } + } + + #[cfg(feature = "testing")] + impl ::smithay::backend::renderer::element::RenderElement<::smithay::backend::renderer::test::DummyRenderer> + for $($name_generic<::smithay::backend::renderer::test::DummyRenderer>)? $($name_no_generic)? + { + fn draw( + &self, + frame: &mut <::smithay::backend::renderer::test::DummyRenderer as ::smithay::backend::renderer::Renderer>::Frame<'_>, + src: ::smithay::utils::Rectangle, + dst: ::smithay::utils::Rectangle, + damage: &[::smithay::utils::Rectangle], + ) -> ::std::result::Result< + (), + <::smithay::backend::renderer::test::DummyRenderer as ::smithay::backend::renderer::Renderer>::Error, + > { + match self { + $($name::$variant(elem) => { + ::smithay::backend::renderer::element::RenderElement::< + ::smithay::backend::renderer::test::DummyRenderer + >::draw(elem, frame, src, dst, damage) + })+ + } + } + + fn underlying_storage( + &self, + renderer: &mut ::smithay::backend::renderer::test::DummyRenderer, + ) -> ::std::option::Option<::smithay::backend::renderer::element::UnderlyingStorage> { + match self { + $($name::$variant(elem) => elem.underlying_storage(renderer)),+ + } + } + } + } +} diff --git a/src/render/texture.rs b/src/render/texture.rs new file mode 100644 index 0000000..8c03152 --- /dev/null +++ b/src/render/texture.rs @@ -0,0 +1,123 @@ +#[cfg(feature = "testing")] +use smithay::backend::renderer::test::DummyRenderer; +use smithay::{ + backend::renderer::{ + element::{self, texture::TextureRenderElement, Element, RenderElement}, + gles::{GlesRenderer, GlesTexture}, + utils::{CommitCounter, DamageSet, OpaqueRegions}, + Renderer, + }, + utils::{Buffer, Physical, Rectangle, Scale}, +}; + +use crate::backend::udev::UdevRenderer; + +/// TODO: docs +#[derive(Debug)] +pub struct CommonTextureRenderElement(TextureRenderElement); + +impl CommonTextureRenderElement { + pub fn new(element: TextureRenderElement) -> Self { + Self(element) + } +} + +impl Element for CommonTextureRenderElement { + fn id(&self) -> &element::Id { + self.0.id() + } + + fn current_commit(&self) -> CommitCounter { + self.0.current_commit() + } + + fn src(&self) -> Rectangle { + self.0.src() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.0.geometry(scale) + } + + fn location(&self, scale: Scale) -> smithay::utils::Point { + self.0.location(scale) + } + + fn transform(&self) -> smithay::utils::Transform { + self.0.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> DamageSet { + self.0.damage_since(scale, commit) + } + + fn opaque_regions(&self, scale: Scale) -> OpaqueRegions { + self.0.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.0.alpha() + } + + fn kind(&self) -> element::Kind { + self.0.kind() + } +} + +impl RenderElement for CommonTextureRenderElement { + fn draw( + &self, + frame: &mut ::Frame<'_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + ) -> Result<(), ::Error> { + RenderElement::::draw(&self.0, frame, src, dst, damage) + } + + fn underlying_storage( + &self, + renderer: &mut GlesRenderer, + ) -> Option> { + let _ = renderer; + None + } +} + +impl<'a> RenderElement> for CommonTextureRenderElement { + fn draw( + &self, + frame: &mut as Renderer>::Frame<'_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + ) -> Result<(), as Renderer>::Error> { + RenderElement::::draw(&self.0, frame.as_mut(), src, dst, damage)?; + Ok(()) + } + + fn underlying_storage( + &self, + renderer: &mut UdevRenderer<'a>, + ) -> Option> { + let _ = renderer; + None + } +} + +#[cfg(feature = "testing")] +impl RenderElement for CommonTextureRenderElement { + fn draw( + &self, + _frame: &mut ::Frame<'_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + ) -> Result<(), ::Error> { + Ok(()) + } +} diff --git a/src/render/util.rs b/src/render/util.rs new file mode 100644 index 0000000..ab951af --- /dev/null +++ b/src/render/util.rs @@ -0,0 +1,152 @@ +//! Render utilities. + +pub mod snapshot; +pub mod surface; + +use anyhow::{bail, Context}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement}; +use smithay::backend::renderer::{Bind, Frame, Offscreen, Renderer}; +use smithay::utils::{Point, Rectangle}; +use smithay::{ + backend::renderer::{ + element::RenderElement, + gles::{GlesRenderer, GlesTexture}, + sync::SyncPoint, + }, + utils::{Physical, Scale, Size, Transform}, +}; + +/// A texture from [`render_to_encompassing_texture`]. +/// +/// Additionally contains the sync point and location that the elements would originally +/// be drawn at. +#[derive(Debug, Clone)] +pub struct EncompassingTexture { + /// The rendered texture. + pub texture: GlesTexture, + /// The sync point. + pub sync_point: SyncPoint, + /// The location the elements would have been originally drawn at. + pub loc: Point, +} + +/// Renders the given elements to a [`GlesTexture`] that encompasses them all. +/// +/// See [`render_to_texture`]. +/// +/// From https://github.com/YaLTeR/niri/blob/efb39e466b5248eb894745e899de33661493511d/src/render_helpers/mod.rs#L158 +pub fn render_to_encompassing_texture>( + renderer: &mut GlesRenderer, + elements: impl IntoIterator, + scale: Scale, + transform: Transform, + fourcc: Fourcc, +) -> anyhow::Result { + let elements = elements.into_iter().collect::>(); + + let encompassing_geo = elements + .iter() + .map(|elem| elem.geometry(scale)) + .reduce(|first, second| first.merge(second)) + .context("no elements to render")?; + + // Make elements relative to (0, 0) for rendering + let elements = elements.iter().rev().map(|elem| { + RelocateRenderElement::from_element( + elem, + (-encompassing_geo.loc.x, -encompassing_geo.loc.y), + Relocate::Relative, + ) + }); + + let (texture, sync_point) = render_to_texture( + renderer, + elements, + encompassing_geo.size, + scale, + transform, + fourcc, + )?; + + Ok(EncompassingTexture { + texture, + sync_point, + loc: encompassing_geo.loc, + }) +} + +/// Renders the given elements to a [`GlesTexture`]. +/// +/// `elements` should have their locations relative to (0, 0), as they will be rendered +/// to a texture with a rectangle of loc (0, 0) and size `size`. This can be achieved +/// by wrapping them in a +/// [`RelocateRenderElement`][smithay::backend::renderer::element::utils::RelocateRenderElement]. +/// +/// Elements outside of the rectangle will be clipped. +/// +/// From https://github.com/YaLTeR/niri/blob/efb39e466b5248eb894745e899de33661493511d/src/render_helpers/mod.rs#L180 +pub fn render_to_texture( + renderer: &mut GlesRenderer, + elements: impl IntoIterator>, + size: Size, + scale: Scale, + transform: Transform, + fourcc: Fourcc, +) -> anyhow::Result<(GlesTexture, SyncPoint)> { + if size.is_empty() { + // Causes GL_INVALID_VALUE when binding + bail!("size was empty"); + } + + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + let texture: GlesTexture = renderer + .create_buffer(fourcc, buffer_size) + .context("failed to create texture")?; + renderer + .bind(texture.clone()) + .context("failed to bind texture")?; + + let sync_point = + render_elements_to_bound_framebuffer(renderer, elements, size, scale, transform)?; + + Ok((texture, sync_point)) +} + +/// Renders the given elements into the currently bound framebuffer. +/// +/// `elements` should have their locations relative to (0, 0), as they will be rendered +/// to a texture with a rectangle of loc (0, 0) and size `size`. +/// +/// From https://github.com/YaLTeR/niri/blob/efb39e466b5248eb894745e899de33661493511d/src/render_helpers/mod.rs#L295 +fn render_elements_to_bound_framebuffer( + renderer: &mut GlesRenderer, + elements: impl IntoIterator>, + size: Size, + scale: Scale, + transform: Transform, +) -> anyhow::Result { + // TODO: see what transform.invert() does here + let dst_rect = Rectangle::from_loc_and_size((0, 0), transform.transform_size(size)); + + let mut frame = renderer + .render(size, transform) + .context("failed to start render")?; + + frame + .clear([0.0, 0.0, 0.0, 0.0], &[dst_rect]) + .context("failed to clear frame")?; + + for elem in elements { + let src = elem.src(); + let dst = elem.geometry(scale); + + if let Some(mut damage) = dst_rect.intersection(dst) { + damage.loc -= dst.loc; + elem.draw(&mut frame, src, dst, &[damage]) + .context("failed to draw element")?; + } + } + + frame.finish().context("failed to finish frame") +} diff --git a/src/render/util/snapshot.rs b/src/render/util/snapshot.rs new file mode 100644 index 0000000..67ced01 --- /dev/null +++ b/src/render/util/snapshot.rs @@ -0,0 +1,220 @@ +//! Utilities for capturing snapshots of windows and other elements. + +use std::cell::OnceCell; +use std::collections::HashSet; +use std::rc::Rc; + +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element; +use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderElement}; +use smithay::backend::renderer::element::utils::RescaleRenderElement; +use smithay::output::Output; +use smithay::utils::Logical; +use smithay::{ + backend::renderer::{ + element::RenderElement, + gles::{GlesRenderer, GlesTexture}, + }, + utils::{Physical, Point, Scale, Transform}, +}; +use tracing::error; + +use crate::layout::transaction::{LayoutSnapshot, SnapshotRenderElement, SnapshotTarget}; +use crate::render::texture::CommonTextureRenderElement; +use crate::render::{AsGlesRenderer, PRenderer}; +use crate::state::{Pinnacle, WithState}; +use crate::window::WindowElement; + +use super::{render_to_encompassing_texture, EncompassingTexture}; + +/// A snapshot of given elements that can be rendered at some point in the future. +#[derive(Debug)] +pub struct RenderSnapshot { + /// Rendered elements. + /// + /// These are not used directly in rendering due to floating-point rounding + /// inaccuracies that cause pixel imperfections when being displayed. + elements: Rc>, + /// The original scale used to create this snapshot. + scale: Scale, + /// The texture that elements will be rendered into. + /// + /// Happens lazily for performance. + texture: OnceCell<(GlesTexture, Point)>, +} + +impl Clone for RenderSnapshot { + fn clone(&self) -> Self { + Self { + elements: self.elements.clone(), + scale: self.scale, + texture: self.texture.clone(), + } + } +} + +impl> RenderSnapshot { + /// Creates a new snapshot from elements. + pub fn new(elements: impl IntoIterator, scale: Scale) -> Self { + Self { + elements: Rc::new(elements.into_iter().collect()), + scale, + texture: OnceCell::new(), + } + } + + /// Get the texture, rendering it to a new one if it doesn't exist. + pub fn texture( + &self, + renderer: &mut GlesRenderer, + ) -> Option<(GlesTexture, Point)> { + // Not `get_or_init` because that would require the cell be an option/result + // and I didn't want that + if self.texture.get().is_none() { + let EncompassingTexture { + texture, + sync_point: _, + loc, + } = match render_to_encompassing_texture( + renderer, + self.elements.as_ref(), + self.scale, + Transform::Normal, // TODO: transform + Fourcc::Argb8888, + ) { + Ok(tex) => tex, + Err(err) => { + error!("Failed to render to encompassing texture: {err}"); + return None; + } + }; + let Ok(()) = self.texture.set((texture, loc)) else { + unreachable!() + }; + } + self.texture.get().cloned() + } + + /// Render elements for this snapshot. + pub fn render_elements( + &self, + renderer: &mut R, + scale: Scale, + alpha: f32, + ) -> Option> { + let (texture, loc) = self.texture(renderer.as_gles_renderer())?; + let buffer = TextureBuffer::from_texture(renderer, texture, 1, Transform::Normal, None); + let elem = TextureRenderElement::from_texture_buffer( + loc.to_f64(), + &buffer, + Some(alpha), + None, + None, + element::Kind::Unspecified, + ); + + let common = CommonTextureRenderElement::new(elem); + + // Scale in the opposite direction from the original scale to have it be the same size + let scale = Scale::from((1.0 / scale.x, 1.0 / scale.y)); + + Some(SnapshotRenderElement::Snapshot( + RescaleRenderElement::from_element(common, loc, scale), + )) + } +} + +impl WindowElement { + /// Capture a snapshot for this window and store it in its user data. + pub fn capture_snapshot_and_store( + &self, + renderer: &mut GlesRenderer, + location: Point, + scale: Scale, + alpha: f32, + ) -> LayoutSnapshot { + self.with_state_mut(|state| { + if state.snapshot.is_none() || self.is_x11() { + let elements = self.texture_render_elements(renderer, location, scale, alpha); + + state.snapshot = Some(RenderSnapshot::new(elements, scale)); + } + + let Some(ret) = state.snapshot.clone() else { + unreachable!() + }; + + ret + }) + } +} + +/// Capture snapshots for all tiled windows on this output. +/// +/// Any windows in `also_include` are also included in the capture. +/// +/// ret.1 = fullscreen and up, +/// ret.2 = under fullscreen +pub fn capture_snapshots_on_output( + pinnacle: &mut Pinnacle, + renderer: &mut GlesRenderer, + output: &Output, + also_include: impl IntoIterator, +) -> (Vec, Vec) { + let split_index = pinnacle + .space + .elements() + .filter(|win| { + win.is_on_active_tag_on_output(output) + || (win.is_on_active_tag() + && win.with_state(|state| state.floating_or_tiled.is_floating())) + }) + .position(|win| win.with_state(|state| state.fullscreen_or_maximized.is_fullscreen())); + + let mut under_fullscreen = pinnacle + .space + .elements() + .filter(|win| { + win.is_on_active_tag_on_output(output) + || (win.is_on_active_tag() + && win.with_state(|state| state.floating_or_tiled.is_floating())) + }) + .cloned() + .collect::>(); + + let fullscreen_and_up = + under_fullscreen.split_off(split_index.unwrap_or(under_fullscreen.len())); + + let also_include = also_include.into_iter().collect::>(); + + let mut flat_map = |win: WindowElement| { + if win.with_state(|state| state.floating_or_tiled.is_tiled()) || also_include.contains(&win) + { + let loc = pinnacle.space.element_location(&win)? - output.current_location(); + let snapshot = win.capture_snapshot_and_store( + renderer, + loc, + output.current_scale().fractional_scale().into(), + 1.0, + ); + + Some(SnapshotTarget::Snapshot(snapshot)) + } else { + Some(SnapshotTarget::Window(win)) + } + }; + + let under_fullscreen_snapshots = under_fullscreen + .into_iter() + .rev() + .flat_map(&mut flat_map) + .collect(); + + let fullscreen_and_up_snapshots = fullscreen_and_up + .into_iter() + .rev() + .flat_map(&mut flat_map) + .collect(); + + (fullscreen_and_up_snapshots, under_fullscreen_snapshots) +} diff --git a/src/render/util/surface.rs b/src/render/util/surface.rs new file mode 100644 index 0000000..942d3d9 --- /dev/null +++ b/src/render/util/surface.rs @@ -0,0 +1,114 @@ +//! Utils for rendering surface trees to textures. + +use smithay::{ + backend::renderer::{ + element::{ + self, + surface::WaylandSurfaceRenderElement, + texture::{TextureBuffer, TextureRenderElement}, + }, + gles::{GlesRenderer, GlesTexture}, + utils::RendererSurfaceStateUserData, + }, + reexports::wayland_server::protocol::wl_surface::WlSurface, + utils::{Physical, Point, Scale}, + wayland::compositor::{self, TraversalAction}, +}; +use tracing::warn; + +/// Render a surface tree as [TextureRenderElement]s instead of wayland ones. +/// +/// Needed to allow WaylandSurfaceRenderElements to be dropped to free shm buffers. +pub fn texture_render_elements_from_surface_tree( + renderer: &mut GlesRenderer, + surface: &WlSurface, + location: impl Into>, + scale: impl Into>, + alpha: f32, +) -> Vec> { + let location = location.into().to_f64(); + let scale = scale.into(); + let mut surfaces: Vec> = Vec::new(); + + compositor::with_surface_tree_downward( + surface, + location, + |_, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + let data = &*data.borrow(); + + if let Some(view) = data.view() { + location += view.offset.to_f64().to_physical(scale); + TraversalAction::DoChildren(location) + } else { + TraversalAction::SkipChildren + } + } else { + TraversalAction::SkipChildren + } + }, + |surface, states, location| { + let mut location = *location; + let data = states.data_map.get::(); + + if let Some(data) = data { + let has_view = if let Some(view) = data.borrow().view() { + location += view.offset.to_f64().to_physical(scale); + true + } else { + false + }; + + if has_view { + match WaylandSurfaceRenderElement::from_surface( + renderer, + surface, + states, + location, + alpha, + element::Kind::Unspecified, + ) { + Ok(Some(surface)) => { + // Reconstruct the element as a TextureRenderElement + + let data = data.borrow(); + let view = data.view().unwrap(); + + // TODO: figure out what is making the WaylandSurfaceRenderElement + // not drop and release the shm buffer for wleird + + let texture_buffer = TextureBuffer::from_texture( + renderer, + surface.texture().clone(), + data.buffer_scale(), + data.buffer_transform(), + None, + ); + + let texture_elem = TextureRenderElement::from_texture_buffer( + location, + &texture_buffer, + Some(alpha), + Some(view.src), + Some(view.dst), + element::Kind::Unspecified, + ); + + surfaces.push(texture_elem); + } + Ok(None) => {} // surface is not mapped + Err(err) => { + warn!("Failed to import surface: {}", err); + } + }; + } + } + }, + |_, _, _| true, + ); + + surfaces +} diff --git a/src/state.rs b/src/state.rs index 9609be0..ebfa8a0 100644 --- a/src/state.rs +++ b/src/state.rs @@ -51,7 +51,7 @@ use smithay::{ }, xwayland::{X11Wm, XWaylandClientData}, }; -use std::{cell::RefCell, path::PathBuf, sync::Arc}; +use std::{cell::RefCell, collections::HashMap, path::PathBuf, sync::Arc}; use sysinfo::{ProcessRefreshKind, RefreshKind}; use tracing::{info, warn}; use xdg::BaseDirectories; @@ -115,7 +115,8 @@ pub struct Pinnacle { /// The main window vec pub windows: Vec, - pub new_windows: Vec, + /// Windows with no buffer. + pub unmapped_windows: Vec, pub config: Config, @@ -133,6 +134,9 @@ pub struct Pinnacle { pub signal_state: SignalState, pub layout_state: LayoutState, + + /// A cache of surfaces to their root surface. + pub root_surface_cache: HashMap, } impl State { @@ -147,18 +151,23 @@ impl State { winit.render_if_scheduled(&mut self.pinnacle); } + // FIXME: Don't poll this every cycle + for output in self.pinnacle.space.outputs().cloned().collect::>() { + output.with_state_mut(|state| { + if state + .layout_transaction + .as_ref() + .is_some_and(|ts| ts.ready()) + { + self.schedule_render(&output); + } + }); + } + self.pinnacle .display_handle .flush_clients() .expect("failed to flush client buffers"); - - // TODO: couple these or something, this is really error-prone - assert_eq!( - self.pinnacle.windows.len(), - self.pinnacle.z_index_stack.len(), - "Length of `windows` and `z_index_stack` are different. \ - If you see this, report it to the developer." - ); } } @@ -295,7 +304,7 @@ impl Pinnacle { popup_manager: PopupManager::default(), windows: Vec::new(), - new_windows: Vec::new(), + unmapped_windows: Vec::new(), xwm: None, xdisplay: None, @@ -312,6 +321,8 @@ impl Pinnacle { signal_state: SignalState::default(), layout_state: LayoutState::default(), + + root_surface_cache: HashMap::new(), }; Ok(pinnacle) diff --git a/src/tag.rs b/src/tag.rs index ccabd98..5ae2347 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -9,7 +9,7 @@ use std::{ use smithay::output::Output; -use crate::state::{Pinnacle, State, WithState}; +use crate::state::{Pinnacle, WithState}; static TAG_ID_COUNTER: AtomicU32 = AtomicU32::new(0); @@ -43,47 +43,55 @@ impl TagId { #[derive(Debug)] struct TagInner { - /// The internal id of this tag. - id: TagId, /// The name of this tag. name: String, /// Whether this tag is active or not. active: bool, } -impl PartialEq for TagInner { +/// A marker for windows. +/// +/// A window may have 0 or more tags, and you can display 0 or more tags +/// on each output at a time. +#[derive(Debug, Clone)] +pub struct Tag { + /// The internal id of this tag. + id: TagId, + inner: Rc>, +} + +impl PartialEq for Tag { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for TagInner {} +impl Eq for Tag {} -/// A marker for windows. -/// -/// A window may have 0 or more tags, and you can display 0 or more tags -/// on each output at a time. -#[derive(Debug, Clone, PartialEq)] -pub struct Tag(Rc>); +impl Hash for Tag { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} // RefCell Safety: These methods should never panic because they are all self-contained or Copy. impl Tag { pub fn id(&self) -> TagId { - self.0.borrow().id + self.id } pub fn name(&self) -> String { - self.0.borrow().name.clone() + self.inner.borrow().name.clone() } pub fn active(&self) -> bool { - self.0.borrow().active + self.inner.borrow().active } - pub fn set_active(&self, active: bool, state: &mut State) { - self.0.borrow_mut().active = active; + pub fn set_active(&self, active: bool, pinnacle: &mut Pinnacle) { + self.inner.borrow_mut().active = active; - state.pinnacle.signal_state.tag_active.signal(|buf| { + pinnacle.signal_state.tag_active.signal(|buf| { buf.push_back( pinnacle_api_defs::pinnacle::signal::v0alpha1::TagActiveResponse { tag_id: Some(self.id().0), @@ -96,11 +104,13 @@ impl Tag { impl Tag { pub fn new(name: String) -> Self { - Self(Rc::new(RefCell::new(TagInner { + Self { id: TagId::next(), - name, - active: false, - }))) + inner: Rc::new(RefCell::new(TagInner { + name, + active: false, + })), + } } /// Get the output this tag is on. diff --git a/src/window.rs b/src/window.rs index b741f12..6e5ef31 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,13 +2,13 @@ pub mod rules; -use std::{cell::RefCell, ops::Deref}; +use std::{cell::RefCell, collections::HashSet, ops::Deref}; use smithay::{ desktop::{space::SpaceElement, Window, WindowSurface}, output::Output, reexports::wayland_server::protocol::wl_surface::WlSurface, - utils::{IsAlive, Logical, Point, Rectangle}, + utils::{IsAlive, Logical, Point, Rectangle, Serial}, wayland::{compositor, seat::WaylandFocus, shell::xdg::XdgToplevelSurfaceData}, }; use tracing::{error, warn}; @@ -30,6 +30,18 @@ impl Deref for WindowElement { } } +impl PartialEq<&WindowElement> for WindowElement { + fn eq(&self, other: &&WindowElement) -> bool { + self == *other + } +} + +impl PartialEq for &WindowElement { + fn eq(&self, other: &WindowElement) -> bool { + *self == other + } +} + impl WindowElement { pub fn new(window: Window) -> Self { Self(window) @@ -131,6 +143,23 @@ impl WindowElement { self.with_state(|state| state.tags.iter().any(|tag| tag.active())) } + pub fn is_on_active_tag_on_output(&self, output: &Output) -> bool { + // PERF: dear god benchmark this + let win_tags = self + .with_state(|state| state.tags.clone()) + .into_iter() + .collect::>(); + output.with_state(|state| { + state + .focused_tags() + .cloned() + .collect::>() + .intersection(&win_tags) + .next() + .is_some() + }) + } + /// Place this window on the given output, giving it the output's focused tags. /// /// RefCell Safety: Uses `with_state_mut` on the window and `with_state` on the output @@ -158,6 +187,30 @@ impl WindowElement { pub fn is_x11_override_redirect(&self) -> bool { matches!(self.x11_surface(), Some(surface) if surface.is_override_redirect()) } + + /// Marks the currently acked configure as committed. + pub fn mark_serial_as_committed(&self) { + let Some(toplevel) = self.toplevel() else { return }; + let serial = compositor::with_states(toplevel.wl_surface(), |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .configure_serial + }); + + self.with_state_mut(|state| state.committed_serial = serial); + } + + /// Returns whether the currently committed serial is >= the given serial. + pub fn is_serial_committed(&self, serial: Serial) -> bool { + match self.with_state(|state| state.committed_serial) { + Some(committed_serial) => committed_serial >= serial, + None => false, + } + } } impl SpaceElement for WindowElement { @@ -229,24 +282,34 @@ impl WithState for WindowElement { impl Pinnacle { /// Returns the [Window] associated with a given [WlSurface]. pub fn window_for_surface(&self, surface: &WlSurface) -> Option { - self.space - .elements() - .find(|window| window.wl_surface().map(|s| &*s == surface).unwrap_or(false)) - .or_else(|| { - self.windows - .iter() - .find(|&win| win.wl_surface().is_some_and(|surf| &*surf == surface)) - }) - .cloned() - } - - /// `window_for_surface` but for windows that haven't commited a buffer yet. - /// - /// Currently only used in `ensure_initial_configure` in [`handlers`][crate::handlers]. - pub fn new_window_for_surface(&self, surface: &WlSurface) -> Option { - self.new_windows + self.windows .iter() .find(|&win| win.wl_surface().is_some_and(|surf| &*surf == surface)) .cloned() } + + /// [`Self::window_for_surface`] but for windows that don't have a buffer. + pub fn unmapped_window_for_surface(&self, surface: &WlSurface) -> Option { + self.unmapped_windows + .iter() + .find(|&win| win.wl_surface().is_some_and(|surf| &*surf == surface)) + .cloned() + } + + /// Removes a window from the main window vec, z_index stack, and focus stacks. + /// + /// If `unmap` is true the window has become unmapped and will be pushed to `unmapped_windows`. + pub fn remove_window(&mut self, window: &WindowElement, unmap: bool) { + self.windows.retain(|win| win != window); + self.unmapped_windows.retain(|win| win != window); + if unmap { + self.unmapped_windows.push(window.clone()); + } + + self.z_index_stack.retain(|win| win != window); + + for output in self.space.outputs() { + output.with_state_mut(|state| state.focus_stack.stack.retain(|win| win != window)); + } + } } diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 96bd3db..9d546f4 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -5,10 +5,12 @@ use std::sync::atomic::{AtomicU32, Ordering}; use smithay::{ desktop::{space::SpaceElement, WindowSurface}, reexports::wayland_protocols::xdg::shell::server::xdg_toplevel, - utils::{Logical, Point, Rectangle}, + utils::{Logical, Point, Rectangle, Serial}, + wayland::compositor::HookId, }; use crate::{ + layout::transaction::LayoutSnapshot, state::{Pinnacle, WithState}, tag::Tag, }; @@ -54,6 +56,10 @@ pub struct WindowElementState { pub fullscreen_or_maximized: FullscreenOrMaximized, pub target_loc: Option>, pub minimized: bool, + /// The most recent serial that has been committed. + pub committed_serial: Option, + pub snapshot: Option, + pub snapshot_hook_id: Option, } impl WindowElement { @@ -303,6 +309,9 @@ impl WindowElementState { fullscreen_or_maximized: FullscreenOrMaximized::Neither, target_loc: None, minimized: false, + committed_serial: None, + snapshot: None, + snapshot_hook_id: None, } } }