Add workspace screen sharing (#64)
Add workspace selection to the screen-sharing picker with pinned capture, privacy filtering and bounded capture resources. Refresh captured tab labels when title or app-ID metadata changes.
This commit is contained in:
1 parent
e64ab52190
commit
ab11269246
32 files changed
+2125
-165
No files matched your search
@@ -1244,6 +1244,7 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
||||
|
||||
#[track_caller]
|
||||
pub fn render_all(state: &mut Ferese) {
|
||||
state.image_capture.invalidate_workspaces();
|
||||
for output in state.space.outputs() {
|
||||
capture::changes::content_changed(output);
|
||||
}
|
||||
@@ -1320,6 +1321,18 @@ pub(crate) fn render_surface(
|
||||
return;
|
||||
}
|
||||
|
||||
let capture_root = state
|
||||
.popups
|
||||
.find_popup(&root)
|
||||
.and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok())
|
||||
.unwrap_or_else(|| root.clone());
|
||||
if let Some(workspace) = state
|
||||
.windows
|
||||
.id_for_surface(&capture_root)
|
||||
.and_then(|id| state.workspaces.workspace_for_window(id))
|
||||
{
|
||||
state.image_capture.workspace_changed(workspace);
|
||||
}
|
||||
let outputs = state.surface_outputs(&root);
|
||||
render_on(state, &outputs);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ pub(crate) mod changes;
|
||||
|
||||
pub(crate) struct CaptureTarget {
|
||||
texture: GlesTexture,
|
||||
visible: Option<smithay::backend::renderer::element::RenderElementStates>,
|
||||
damage: OutputDamageTracker,
|
||||
output: Output,
|
||||
revision: u64,
|
||||
@@ -32,9 +33,24 @@ pub(crate) struct CaptureTarget {
|
||||
}
|
||||
|
||||
impl CaptureTarget {
|
||||
pub(crate) fn surface_visible(
|
||||
&self,
|
||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
) -> bool {
|
||||
self.visible
|
||||
.as_ref()
|
||||
.and_then(|states| states.element_render_state(surface))
|
||||
.is_some_and(crate::display_presentation::is_visible)
|
||||
}
|
||||
|
||||
pub(crate) fn image(&self) -> (GlesTexture, u64) {
|
||||
(self.texture.clone(), self.revision)
|
||||
}
|
||||
|
||||
pub(crate) fn new(texture: GlesTexture, output: &Output) -> Self {
|
||||
Self {
|
||||
texture,
|
||||
visible: None,
|
||||
damage: OutputDamageTracker::from_output(output),
|
||||
output: output.clone(),
|
||||
revision: 0,
|
||||
@@ -300,7 +316,7 @@ pub(super) fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||
render_capture_with_revision(renderer, target, output, elements).map(|(target, _)| target)
|
||||
}
|
||||
|
||||
pub(super) fn render_capture_with_revision<'a, E: RenderElement<GlesRenderer>>(
|
||||
pub(crate) fn render_capture_with_revision<'a, E: RenderElement<GlesRenderer>>(
|
||||
renderer: &mut GlesRenderer,
|
||||
target: &'a mut Option<CaptureTarget>,
|
||||
output: &Output,
|
||||
@@ -349,6 +365,13 @@ pub(super) fn render_capture_with_revision<'a, E: RenderElement<GlesRenderer>>(
|
||||
target.compositions += 1;
|
||||
target.revision = NEXT_REVISION.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
if output
|
||||
.user_data()
|
||||
.get::<crate::handlers::workspace_capture::CaptureOutputId>()
|
||||
.is_some()
|
||||
{
|
||||
target.visible = Some(result.states);
|
||||
}
|
||||
Ok((framebuffer, target.revision))
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,32 @@ use smithay::wayland::image_copy_capture::{BufferConstraints, DmabufConstraints,
|
||||
use crate::Ferese;
|
||||
|
||||
const MAX_SESSIONS: usize = 16;
|
||||
// Reserve for the maximum eight portal pool buffers, a retained target and a
|
||||
// fallback transfer texture. Imports of the same allocation are not extra pixels.
|
||||
const MAX_STREAM_BYTES: u64 = 512 * 1024 * 1024;
|
||||
fn stream_bytes(size: Size<i32, Buffer>) -> u64 {
|
||||
// The portal rejects DMA strides above this allowance. Driver bookkeeping
|
||||
// and encoder memory are separate from this pixel-storage reservation.
|
||||
let stride = (size.w.max(0) as u64 * 4 + 4095) & !4095;
|
||||
stride
|
||||
.saturating_mul(size.h.max(0) as u64)
|
||||
.saturating_mul(10)
|
||||
}
|
||||
|
||||
pub(super) enum Source {
|
||||
Output(WeakOutput),
|
||||
Window(WindowId),
|
||||
Workspace(super::workspace_capture::WorkspaceSource),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct SourcePaused(pub std::sync::atomic::AtomicBool);
|
||||
|
||||
fn source_active(source: &ImageCaptureSource) -> bool {
|
||||
source
|
||||
.user_data()
|
||||
.get::<SourcePaused>()
|
||||
.is_none_or(|paused| !paused.0.load(std::sync::atomic::Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@@ -50,7 +72,7 @@ struct WindowStamp {
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct CursorStamp {
|
||||
pub(super) struct CursorStamp {
|
||||
location: Point<f64, Logical>,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
image: CursorImageStamp,
|
||||
@@ -141,10 +163,10 @@ fn pending_frame_callbacks(
|
||||
pending
|
||||
}
|
||||
|
||||
struct Active {
|
||||
session: Session,
|
||||
pending: Option<Frame>,
|
||||
revision: Option<u64>,
|
||||
pub(super) struct Active {
|
||||
pub(super) session: Session,
|
||||
pub(super) pending: Option<Frame>,
|
||||
pub(super) revision: Option<u64>,
|
||||
window_stamp: Option<WindowStamp>,
|
||||
size: Size<i32, Buffer>,
|
||||
dma_node: Option<smithay::backend::drm::DrmNode>,
|
||||
@@ -165,6 +187,10 @@ pub(crate) struct CaptureSessions {
|
||||
in_flight: HashMap<u64, InFlight>,
|
||||
next_fence: u64,
|
||||
callback_timer: Option<RegistrationToken>,
|
||||
callback_deadline: Option<Instant>,
|
||||
workspace_callbacks: HashMap<ferese_core::WorkspaceId, Instant>,
|
||||
workspaces:
|
||||
HashMap<(ferese_core::WorkspaceId, bool), super::workspace_capture::WorkspaceCapture>,
|
||||
}
|
||||
|
||||
fn authorized(client: &Client) -> bool {
|
||||
@@ -179,6 +205,43 @@ fn authorized(client: &Client) -> bool {
|
||||
}
|
||||
|
||||
impl CaptureSessions {
|
||||
pub(super) fn workspace_callback_deadline(
|
||||
&self,
|
||||
id: ferese_core::WorkspaceId,
|
||||
now: Instant,
|
||||
) -> Instant {
|
||||
self.workspace_callbacks.get(&id).copied().unwrap_or(now)
|
||||
}
|
||||
pub(super) fn set_workspace_callback_deadline(
|
||||
&mut self,
|
||||
id: ferese_core::WorkspaceId,
|
||||
deadline: Instant,
|
||||
) {
|
||||
self.workspace_callbacks.insert(id, deadline);
|
||||
}
|
||||
pub(crate) fn invalidate_workspaces(&mut self) {
|
||||
for view in self.workspaces.values_mut() {
|
||||
view.invalidate();
|
||||
}
|
||||
}
|
||||
pub(crate) fn workspace_changed(&mut self, id: ferese_core::WorkspaceId) {
|
||||
for ((workspace, _), view) in &mut self.workspaces {
|
||||
if *workspace == id {
|
||||
view.content_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn workspace_geometry_changed(&mut self, id: ferese_core::WorkspaceId) {
|
||||
for ((workspace, _), view) in &mut self.workspaces {
|
||||
if *workspace == id {
|
||||
view.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn captured_workspaces(&self) -> Vec<ferese_core::WorkspaceId> {
|
||||
self.workspaces.keys().map(|(id, _)| *id).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn new(display: &DisplayHandle) -> Self {
|
||||
Self {
|
||||
copy: ImageCopyCaptureState::new_with_filter::<Ferese, _>(display, authorized),
|
||||
@@ -187,6 +250,9 @@ impl CaptureSessions {
|
||||
in_flight: HashMap::new(),
|
||||
next_fence: 0,
|
||||
callback_timer: None,
|
||||
callback_deadline: None,
|
||||
workspace_callbacks: HashMap::new(),
|
||||
workspaces: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +346,15 @@ impl ImageCopyCaptureHandler for Ferese {
|
||||
let Some(constraints) = session.current_constraints() else {
|
||||
return;
|
||||
};
|
||||
let reserved: u64 = self
|
||||
.image_capture
|
||||
.active
|
||||
.iter()
|
||||
.map(|active| stream_bytes(active.size))
|
||||
.sum();
|
||||
if stream_bytes(constraints.size) > MAX_STREAM_BYTES.saturating_sub(reserved) {
|
||||
return;
|
||||
}
|
||||
self.image_capture.active.push(Active {
|
||||
session,
|
||||
pending: None,
|
||||
@@ -347,6 +422,36 @@ impl ImageCopyCaptureHandler for Ferese {
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(super) fn capture_cursor_stamp(
|
||||
&self,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
) -> Option<CursorStamp> {
|
||||
use smithay::input::pointer::CursorImageStatus;
|
||||
let pointer = self.seat.get_pointer()?;
|
||||
let image = match &self.cursor_status {
|
||||
CursorImageStatus::Named(_) => {
|
||||
let (icon, epoch, frame) = self.cursor_animation.capture_revision();
|
||||
CursorImageStamp::Named(icon, epoch, frame)
|
||||
}
|
||||
CursorImageStatus::Surface(surface) => {
|
||||
let hotspot = smithay::wayland::compositor::with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<smithay::input::pointer::CursorImageSurfaceData>()
|
||||
.map(|data| data.lock().expect("cursor metadata mutex poisoned").hotspot)
|
||||
.unwrap_or_default()
|
||||
});
|
||||
CursorImageStamp::Surface(surface_stamp(surface, None), hotspot)
|
||||
}
|
||||
CursorImageStatus::Hidden => CursorImageStamp::Hidden,
|
||||
};
|
||||
Some(CursorStamp {
|
||||
location: pointer.current_location(),
|
||||
geometry,
|
||||
image,
|
||||
})
|
||||
}
|
||||
|
||||
fn capture_source_geometry(
|
||||
&self,
|
||||
source: &ImageCaptureSource,
|
||||
@@ -355,6 +460,12 @@ impl Ferese {
|
||||
return None;
|
||||
}
|
||||
let (size, output) = match source.user_data().get::<Source>()? {
|
||||
Source::Workspace(view) => {
|
||||
if !view.valid(self) {
|
||||
return None;
|
||||
}
|
||||
(view.size, view.output.clone())
|
||||
}
|
||||
Source::Window(id) => {
|
||||
let info = self.window_capture_info(*id)?;
|
||||
let size = info.geometry.size.to_physical_precise_round(info.scale);
|
||||
@@ -411,31 +522,48 @@ impl Ferese {
|
||||
outputs.push(output);
|
||||
}
|
||||
}
|
||||
crate::backends::direct::schedule_capture(self, &outputs);
|
||||
if !outputs.is_empty() {
|
||||
crate::backends::direct::schedule_capture(self, &outputs);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_capture_sessions(&mut self) {
|
||||
self.image_capture.copy.cleanup();
|
||||
let mut callback_delay: Option<Duration> = None;
|
||||
// Yield between distinct workspace compositions so input and display
|
||||
// deadlines can run even when several consumers request changed views.
|
||||
let mut workspace_compositions = 1usize;
|
||||
let mut sessions = std::mem::take(&mut self.image_capture.active);
|
||||
let mut workspaces = std::mem::take(&mut self.image_capture.workspaces);
|
||||
let mut reserved: u64 = sessions
|
||||
.iter()
|
||||
.map(|active| stream_bytes(active.size))
|
||||
.sum();
|
||||
sessions.retain_mut(|active| {
|
||||
let Some((size, output)) = self.capture_source_geometry(&active.session.source())
|
||||
else {
|
||||
let session = active.session.as_ref();
|
||||
let fences: Vec<_> = self
|
||||
.image_capture
|
||||
.in_flight
|
||||
.iter()
|
||||
.filter_map(|(id, pending)| (pending.session == session).then_some(*id))
|
||||
.collect();
|
||||
for id in fences {
|
||||
self.cancel_capture_fence(id);
|
||||
}
|
||||
self.cancel_session_fences(active.session.as_ref());
|
||||
reserved = reserved.saturating_sub(stream_bytes(active.size));
|
||||
return false;
|
||||
};
|
||||
let dma = self.capture_dma_constraints(&output);
|
||||
let dma_node = dma.as_ref().map(|dma| dma.node);
|
||||
if size != active.size || dma_node != active.dma_node {
|
||||
let next_reserved = reserved
|
||||
.saturating_sub(stream_bytes(active.size))
|
||||
.saturating_add(stream_bytes(size));
|
||||
if next_reserved > MAX_STREAM_BYTES {
|
||||
self.cancel_session_fences(active.session.as_ref());
|
||||
reserved = reserved.saturating_sub(stream_bytes(active.size));
|
||||
return false;
|
||||
}
|
||||
reserved = next_reserved;
|
||||
if let Some(Source::Workspace(source)) =
|
||||
active.session.source().user_data().get::<Source>()
|
||||
{
|
||||
// GPU resources belong to the renderer that created them.
|
||||
workspaces.remove(&(source.id, active.session.draw_cursor()));
|
||||
}
|
||||
active.size = size;
|
||||
active.dma_node = dma_node;
|
||||
active.revision = None;
|
||||
@@ -449,6 +577,37 @@ impl Ferese {
|
||||
frame.fail(FailureReason::BufferConstraints);
|
||||
}
|
||||
}
|
||||
if let Some(Source::Workspace(source)) =
|
||||
active.session.source().user_data().get::<Source>()
|
||||
&& source_active(&active.session.source())
|
||||
{
|
||||
let source = source.clone();
|
||||
let key = (source.id, active.session.draw_cursor());
|
||||
if let Some(frame) = active.pending.take() {
|
||||
let view = workspaces.entry(key).or_insert_with(|| {
|
||||
super::workspace_capture::WorkspaceCapture::new(
|
||||
source.clone(),
|
||||
self.capture_epoch,
|
||||
)
|
||||
});
|
||||
if !view.source.valid(self) {
|
||||
*view = super::workspace_capture::WorkspaceCapture::new(
|
||||
source.clone(),
|
||||
self.capture_epoch,
|
||||
);
|
||||
}
|
||||
if let Some(delay) = super::workspace_capture::process(
|
||||
view,
|
||||
self,
|
||||
active,
|
||||
frame,
|
||||
&mut workspace_compositions,
|
||||
) {
|
||||
callback_delay =
|
||||
Some(callback_delay.map_or(delay, |existing| existing.min(delay)));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(Source::Window(id)) = active.session.source().user_data().get::<Source>() {
|
||||
let id = *id;
|
||||
if let Some(frame) = active.pending.take() {
|
||||
@@ -574,24 +733,59 @@ impl Ferese {
|
||||
}
|
||||
true
|
||||
});
|
||||
workspaces.retain(|(id, cursor), view| {
|
||||
let matches = |active: &Active| {
|
||||
active.session.draw_cursor() == *cursor
|
||||
&& matches!(active.session.source().user_data().get::<Source>(), Some(Source::Workspace(source)) if source.id == *id)
|
||||
};
|
||||
if !sessions.iter().any(matches) { return false; }
|
||||
let pending = sessions.iter().any(|active| matches(active)
|
||||
&& active.pending.is_some() && source_active(&active.session.source()));
|
||||
if !pending && view.target.is_some() {
|
||||
let idle = view.last_used.elapsed();
|
||||
if idle >= Duration::from_secs(2) {
|
||||
view.release_idle_target();
|
||||
} else {
|
||||
let delay = Duration::from_secs(2) - idle;
|
||||
callback_delay = Some(callback_delay.map_or(delay, |existing| existing.min(delay)));
|
||||
}
|
||||
}
|
||||
true
|
||||
});
|
||||
self.image_capture
|
||||
.workspace_callbacks
|
||||
.retain(|id, _| workspaces.keys().any(|(workspace, _)| id == workspace));
|
||||
self.image_capture.workspaces = workspaces;
|
||||
self.image_capture.active = sessions;
|
||||
if let Some(delay) = callback_delay {
|
||||
if self.image_capture.callback_timer.is_none() {
|
||||
let deadline = Instant::now() + delay;
|
||||
if self
|
||||
.image_capture
|
||||
.callback_deadline
|
||||
.is_none_or(|previous| deadline < previous)
|
||||
{
|
||||
if let Some(timer) = self.image_capture.callback_timer.take() {
|
||||
self.loop_handle.remove(timer);
|
||||
}
|
||||
self.image_capture.callback_deadline = Some(deadline);
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(delay), |_, _, state| {
|
||||
state.image_capture.callback_timer = None;
|
||||
state.image_capture.callback_deadline = None;
|
||||
TimeoutAction::Drop
|
||||
}) {
|
||||
Ok(token) => self.image_capture.callback_timer = Some(token),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "could not schedule captured window callbacks")
|
||||
self.image_capture.callback_deadline = None;
|
||||
tracing::warn!(%error, "could not schedule capture callbacks")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(timer) = self.image_capture.callback_timer.take() {
|
||||
self.loop_handle.remove(timer);
|
||||
self.image_capture.callback_deadline = None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,6 +829,18 @@ impl Ferese {
|
||||
self.image_capture.active = sessions;
|
||||
}
|
||||
|
||||
fn cancel_session_fences(&mut self, session: SessionRef) {
|
||||
let fences: Vec<_> = self
|
||||
.image_capture
|
||||
.in_flight
|
||||
.iter()
|
||||
.filter_map(|(id, pending)| (pending.session == session).then_some(*id))
|
||||
.collect();
|
||||
for id in fences {
|
||||
self.cancel_capture_fence(id);
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_capture_fence(&mut self, id: u64) {
|
||||
if let Some(pending) = self.image_capture.in_flight.remove(&id) {
|
||||
self.loop_handle.remove(pending.fence);
|
||||
@@ -642,7 +848,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_stream_frame(
|
||||
pub(super) fn finish_stream_frame(
|
||||
&mut self,
|
||||
frame: Frame,
|
||||
session: SessionRef,
|
||||
@@ -740,3 +946,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "workspace_capture_tests.rs"]
|
||||
mod workspace_tests;
|
||||
@@ -386,3 +386,5 @@ impl XdgForeignHandler for Ferese {
|
||||
}
|
||||
|
||||
impl XdgToplevelIconHandler for Ferese {}
|
||||
|
||||
pub(crate) mod workspace_capture;
|
||||
@@ -191,7 +191,7 @@ pub(super) fn init_global(
|
||||
}
|
||||
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(
|
||||
2,
|
||||
3,
|
||||
Global {
|
||||
budget: Arc::default(),
|
||||
worker,
|
||||
@@ -273,6 +273,52 @@ impl Dispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
_: &DisplayHandle,
|
||||
data_init: &mut DataInit<'_, Self>,
|
||||
) {
|
||||
if let manager::Request::CreateWorkspaceSource {
|
||||
workspace_hi,
|
||||
workspace_lo,
|
||||
source,
|
||||
} = request
|
||||
{
|
||||
use smithay::wayland::image_capture_source::{
|
||||
ImageCaptureSource, ImageCaptureSourceData,
|
||||
};
|
||||
let id =
|
||||
ferese_core::WorkspaceId((u64::from(workspace_hi) << 32) | u64::from(workspace_lo));
|
||||
let capture = ImageCaptureSource::new();
|
||||
let view = super::workspace_capture::WorkspaceSource::new(state, id);
|
||||
if let Some(view) = &view {
|
||||
capture
|
||||
.user_data()
|
||||
.insert_if_missing(|| super::image_capture::Source::Workspace(view.clone()));
|
||||
}
|
||||
let resource = data_init.init(
|
||||
source,
|
||||
ImageCaptureSourceData {
|
||||
source: capture.clone(),
|
||||
},
|
||||
);
|
||||
capture.add_instance(&resource);
|
||||
if let Some(view) = view {
|
||||
manager.source_geometry(
|
||||
&resource,
|
||||
view.geometry.size.w as u32,
|
||||
view.geometry.size.h as u32,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let manager::Request::SetSourceActive { source, active } = request {
|
||||
if let Some(source) =
|
||||
smithay::wayland::image_capture_source::ImageCaptureSource::from_resource(&source)
|
||||
{
|
||||
source
|
||||
.user_data()
|
||||
.get_or_insert_threadsafe(super::image_capture::SourcePaused::default)
|
||||
.0
|
||||
.store(active != 1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let manager::Request::RefreshSource { source } = request {
|
||||
if let Some(source) =
|
||||
smithay::wayland::image_capture_source::ImageCaptureSource::from_resource(&source)
|
||||
|
||||
@@ -0,0 +1,450 @@
|
||||
//! Workspace capture owns a view, not a second desktop. Targets exist only for
|
||||
//! subscribed sources; surface commits invalidate only their owning workspace.
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_core::WorkspaceId;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Buffer, Logical, Rectangle, Size, Transform};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::backends::direct::capture::CaptureTarget;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct WorkspaceSource {
|
||||
pub id: WorkspaceId,
|
||||
pub output: Output,
|
||||
pub render_output: Output,
|
||||
native_size: Size<i32, smithay::utils::Physical>,
|
||||
pub geometry: Rectangle<i32, Logical>,
|
||||
pub size: Size<i32, Buffer>,
|
||||
scale: f64,
|
||||
transform: Transform,
|
||||
}
|
||||
|
||||
impl WorkspaceSource {
|
||||
pub fn new(state: &Ferese, id: WorkspaceId) -> Option<Self> {
|
||||
state.workspaces.workspace(id)?;
|
||||
let owner = state.output_workspaces.output_for_workspace(id)?;
|
||||
let output = state
|
||||
.output_ids
|
||||
.iter()
|
||||
.find(|(_, id)| **id == owner)?
|
||||
.0
|
||||
.clone();
|
||||
let native_size = output.current_mode()?.size;
|
||||
let geometry = state.space.output_geometry(&output)?;
|
||||
if native_size.w <= 0 || native_size.h <= 0 || geometry.size.w <= 0 || geometry.size.h <= 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let (size, capture_scale) = capture_size(geometry.size, scale);
|
||||
// A render descriptor only: never registered, mapped, advertised, or
|
||||
// attached to input/layout. It gives shaders a bounded upright canvas.
|
||||
let render_output = Output::new(
|
||||
format!("workspace-capture-{}", id.0),
|
||||
smithay::output::PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: smithay::output::Subpixel::Unknown,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
serial_number: String::new(),
|
||||
},
|
||||
);
|
||||
render_output.change_current_state(
|
||||
Some(smithay::output::Mode {
|
||||
size: (size.w, size.h).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
Some(Transform::Normal),
|
||||
Some(smithay::output::Scale::Fractional(capture_scale)),
|
||||
None,
|
||||
);
|
||||
render_output
|
||||
.user_data()
|
||||
.insert_if_missing(|| CaptureOutputId(owner));
|
||||
Some(Self {
|
||||
id,
|
||||
output: output.clone(),
|
||||
render_output,
|
||||
geometry,
|
||||
size,
|
||||
native_size,
|
||||
scale,
|
||||
transform: output.current_transform(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn valid(&self, state: &Ferese) -> bool {
|
||||
state.workspaces.workspace(self.id).is_some()
|
||||
&& state.output_id(&self.output).is_some_and(|owner| {
|
||||
state.output_workspaces.output_for_workspace(self.id) == Some(owner)
|
||||
})
|
||||
&& state.space.output_geometry(&self.output) == Some(self.geometry)
|
||||
&& self
|
||||
.output
|
||||
.current_mode()
|
||||
.is_some_and(|mode| mode.size == self.native_size)
|
||||
&& self.output.current_scale().fractional_scale() == self.scale
|
||||
&& self.output.current_transform() == self.transform
|
||||
}
|
||||
|
||||
pub fn cursor_visible(&self, state: &Ferese) -> bool {
|
||||
!state.overview.is_presenting()
|
||||
&& !state.input_capture.active()
|
||||
&& state
|
||||
.output_id(&self.output)
|
||||
.is_some_and(|output| state.workspace_capture_visible(self.id, output))
|
||||
&& state.seat.get_pointer().is_some_and(|pointer| {
|
||||
self.geometry
|
||||
.contains(pointer.current_location().to_i32_floor())
|
||||
})
|
||||
&& !state.capture_cursor_protected()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct WorkspaceCapture {
|
||||
pub source: WorkspaceSource,
|
||||
pub content: u64,
|
||||
pub prepared: Option<u64>,
|
||||
pub cursor: Option<super::image_capture::CursorStamp>,
|
||||
pub epoch: u64,
|
||||
pub next_frame: Instant,
|
||||
pub callbacks_checked: Option<u64>,
|
||||
pub callbacks_pending: bool,
|
||||
pub target: Option<CaptureTarget>,
|
||||
pub resources: crate::render::RenderResources,
|
||||
pub scene: Option<crate::state::FrameScene>,
|
||||
pub scene_dirty: bool,
|
||||
pub last_used: Instant,
|
||||
trace: bool,
|
||||
preparations: u64,
|
||||
compositions: u64,
|
||||
last_revision: Option<u64>,
|
||||
}
|
||||
|
||||
impl WorkspaceCapture {
|
||||
pub fn new(source: WorkspaceSource, epoch: u64) -> Self {
|
||||
Self {
|
||||
source,
|
||||
content: 0,
|
||||
prepared: None,
|
||||
cursor: None,
|
||||
epoch,
|
||||
next_frame: Instant::now(),
|
||||
callbacks_checked: None,
|
||||
callbacks_pending: false,
|
||||
target: None,
|
||||
resources: Default::default(),
|
||||
scene: None,
|
||||
scene_dirty: true,
|
||||
last_used: Instant::now(),
|
||||
trace: std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(),
|
||||
preparations: 0,
|
||||
compositions: 0,
|
||||
last_revision: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate(&mut self) {
|
||||
self.content_changed();
|
||||
self.scene_dirty = true;
|
||||
}
|
||||
|
||||
pub fn release_idle_target(&mut self) {
|
||||
self.target = None;
|
||||
self.resources = Default::default();
|
||||
self.scene = None;
|
||||
self.prepared = None;
|
||||
if self.trace {
|
||||
tracing::info!(target: "ferese::render", workspace = self.source.id.0, "released idle workspace capture target");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content_changed(&mut self) {
|
||||
self.content = self.content.wrapping_add(1);
|
||||
}
|
||||
|
||||
fn callbacks(&self, state: &Ferese, send: bool) -> bool {
|
||||
let (Some(scene), Some(target)) = (&self.scene, &self.target) else {
|
||||
return false;
|
||||
};
|
||||
let mut pending = false;
|
||||
for id in scene.windows.keys() {
|
||||
let Some(window) = state
|
||||
.windows
|
||||
.window(*id)
|
||||
.filter(|window| !state.capture_protected(window))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let mut visible = false;
|
||||
window.with_surfaces(|surface, _| visible |= target.surface_visible(surface));
|
||||
if !visible {
|
||||
continue;
|
||||
}
|
||||
// Normal display callbacks may already have drained these. Capture
|
||||
// never generates presentation feedback or changes scanout ownership.
|
||||
if send {
|
||||
window.send_frame(
|
||||
&self.source.output,
|
||||
state.start_time.elapsed(),
|
||||
Some(interval()),
|
||||
|_, _| None,
|
||||
);
|
||||
}
|
||||
window.with_surfaces(|_, states| {
|
||||
pending |= !states
|
||||
.cached_state
|
||||
.get::<smithay::wayland::compositor::SurfaceAttributes>()
|
||||
.current()
|
||||
.frame_callbacks
|
||||
.is_empty();
|
||||
});
|
||||
}
|
||||
pending
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
cursor: bool,
|
||||
) -> Result<(GlesTexture, u64), String> {
|
||||
if self.epoch != state.capture_epoch {
|
||||
self.resources = Default::default();
|
||||
self.target = None;
|
||||
self.epoch = state.capture_epoch;
|
||||
self.callbacks_checked = None;
|
||||
}
|
||||
if self.scene_dirty || self.scene.is_none() {
|
||||
let scene = state.sample_workspace_frame(self.source.id, &self.source.output);
|
||||
let members = state
|
||||
.workspaces
|
||||
.workspace(self.source.id)
|
||||
.into_iter()
|
||||
.flat_map(|workspace| {
|
||||
workspace
|
||||
.layout
|
||||
.window_ids()
|
||||
.chain(workspace.floating.iter().copied())
|
||||
})
|
||||
.collect();
|
||||
self.resources.retain_workspace_windows(&members);
|
||||
self.scene = Some(scene);
|
||||
self.scene_dirty = false;
|
||||
}
|
||||
let scene = self.scene.as_ref().expect("capture scene sampled");
|
||||
let elements = crate::render::capture_workspace_elements(
|
||||
state,
|
||||
renderer,
|
||||
&self.source,
|
||||
cursor,
|
||||
scene,
|
||||
&mut self.resources,
|
||||
);
|
||||
let result = crate::backends::direct::capture::render_capture_with_revision(
|
||||
renderer,
|
||||
&mut self.target,
|
||||
&self.source.render_output,
|
||||
&elements,
|
||||
)
|
||||
.map(|_| ())
|
||||
.map_err(|error| error.to_string());
|
||||
if let Err(error) = result {
|
||||
self.target = None;
|
||||
self.prepared = None;
|
||||
return Err(error);
|
||||
}
|
||||
self.preparations += 1;
|
||||
self.prepared = Some(self.content);
|
||||
let target = self
|
||||
.target
|
||||
.as_ref()
|
||||
.expect("successful capture has a target");
|
||||
let image = target.image();
|
||||
if self.last_revision != Some(image.1) {
|
||||
self.compositions += 1;
|
||||
}
|
||||
self.last_revision = Some(image.1);
|
||||
Ok(image)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for WorkspaceCapture {
|
||||
fn drop(&mut self) {
|
||||
if self.trace {
|
||||
tracing::info!(target: "ferese::render", workspace = self.source.id.0, preparations = self.preparations, compositions = self.compositions, "workspace capture outcomes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The compositor enforces a ceiling even for a client that bypasses PipeWire's
|
||||
/// pacing. A shared deadline is used for both hidden-client callbacks and frames.
|
||||
pub(super) fn interval() -> Duration {
|
||||
static INTERVAL: std::sync::OnceLock<Duration> = std::sync::OnceLock::new();
|
||||
*INTERVAL.get_or_init(|| {
|
||||
let fps = std::env::var("FERESE_CAPTURE_MAX_FPS")
|
||||
.ok()
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
.unwrap_or(60)
|
||||
.clamp(1, 240);
|
||||
Duration::from_nanos(1_000_000_000 / fps)
|
||||
})
|
||||
}
|
||||
|
||||
/// Shader cache identity; this does not grant the descriptor a desktop output.
|
||||
pub(crate) struct CaptureOutputId(pub ferese_core::OutputId);
|
||||
|
||||
fn capture_size(logical: Size<i32, Logical>, native_scale: f64) -> (Size<i32, Buffer>, f64) {
|
||||
let (width, height) = if logical.w >= logical.h {
|
||||
(1920.0, 1080.0)
|
||||
} else {
|
||||
(1080.0, 1920.0)
|
||||
};
|
||||
let scale = native_scale
|
||||
.min(width / f64::from(logical.w.max(1)))
|
||||
.min(height / f64::from(logical.h.max(1)));
|
||||
(
|
||||
(
|
||||
(f64::from(logical.w) * scale).round().max(1.0) as i32,
|
||||
(f64::from(logical.h) * scale).round().max(1.0) as i32,
|
||||
)
|
||||
.into(),
|
||||
scale,
|
||||
)
|
||||
}
|
||||
|
||||
fn with_renderer<T>(
|
||||
state: &mut Ferese,
|
||||
output: &Output,
|
||||
render: impl FnOnce(&mut Ferese, &mut GlesRenderer) -> Result<T, String>,
|
||||
) -> Result<T, String> {
|
||||
if let Some(backend) = state.nested_backend.clone() {
|
||||
let mut backend = backend
|
||||
.try_borrow_mut()
|
||||
.map_err(|error| error.to_string())?;
|
||||
render(state, backend.renderer())
|
||||
} else if let Some(mut backend) = state.direct_backend.take() {
|
||||
let result = backend
|
||||
.capture_renderer(output)
|
||||
.ok_or_else(|| "Capture output is unavailable".to_string())
|
||||
.and_then(|renderer| render(state, renderer));
|
||||
state.direct_backend = Some(backend);
|
||||
result
|
||||
} else {
|
||||
Err("Capture renderer is unavailable".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn process(
|
||||
view: &mut WorkspaceCapture,
|
||||
state: &mut Ferese,
|
||||
active: &mut super::image_capture::Active,
|
||||
frame: smithay::wayland::image_copy_capture::Frame,
|
||||
compositions_left: &mut usize,
|
||||
) -> Option<Duration> {
|
||||
use smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::FailureReason;
|
||||
let now = Instant::now();
|
||||
view.last_used = now;
|
||||
let cursor = active.session.draw_cursor() && view.source.cursor_visible(state);
|
||||
let cursor_stamp = cursor
|
||||
.then(|| state.capture_cursor_stamp(view.source.geometry))
|
||||
.flatten();
|
||||
let changed = view.prepared != Some(view.content)
|
||||
|| view.epoch != state.capture_epoch
|
||||
|| view.cursor != cursor_stamp;
|
||||
let interval = super::workspace_capture::interval();
|
||||
let mut retry = None;
|
||||
let image = if changed && (now < view.next_frame || *compositions_left == 0) {
|
||||
retry = Some(
|
||||
view.next_frame
|
||||
.saturating_duration_since(now)
|
||||
.max(Duration::from_millis(1)),
|
||||
);
|
||||
None
|
||||
} else if changed {
|
||||
*compositions_left -= 1;
|
||||
let output = view.source.output.clone();
|
||||
let result = with_renderer(state, &output, |state, renderer| {
|
||||
view.render(state, renderer, cursor)
|
||||
});
|
||||
match result {
|
||||
Ok(image) => {
|
||||
view.cursor = cursor_stamp;
|
||||
view.next_frame = now + interval;
|
||||
Some(image)
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "workspace capture failed");
|
||||
frame.fail(FailureReason::Unknown);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view.target.as_ref().map(|target| target.image())
|
||||
};
|
||||
if let Some((texture, revision)) =
|
||||
image.filter(|(_, revision)| active.revision != Some(*revision))
|
||||
{
|
||||
crate::backends::direct::capture::touch_cache(state);
|
||||
let result = with_renderer(state, &view.source.output, |_, renderer| {
|
||||
crate::render::capture_stream_texture(
|
||||
renderer,
|
||||
&texture,
|
||||
&frame.buffer(),
|
||||
Transform::Normal,
|
||||
false,
|
||||
)
|
||||
});
|
||||
active.revision = result.as_ref().ok().map(|_| revision);
|
||||
state.finish_stream_frame(frame, active.session.as_ref(), result);
|
||||
} else {
|
||||
active.pending = Some(frame);
|
||||
}
|
||||
|
||||
let deadline = state
|
||||
.image_capture
|
||||
.workspace_callback_deadline(view.source.id, now);
|
||||
if view.prepared == Some(view.content)
|
||||
&& (view.callbacks_checked != Some(view.content)
|
||||
|| (view.callbacks_pending && now >= deadline))
|
||||
{
|
||||
view.callbacks_pending = view.callbacks(state, now >= deadline);
|
||||
view.callbacks_checked = Some(view.content);
|
||||
if now >= deadline {
|
||||
state
|
||||
.image_capture
|
||||
.set_workspace_callback_deadline(view.source.id, now + interval);
|
||||
}
|
||||
}
|
||||
if view.callbacks_pending {
|
||||
let delay = state
|
||||
.image_capture
|
||||
.workspace_callback_deadline(view.source.id, now)
|
||||
.saturating_duration_since(now);
|
||||
retry = Some(retry.map_or(delay, |existing| existing.min(delay)));
|
||||
}
|
||||
retry
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn workspace_canvas_is_bounded_and_preserves_fractional_and_portrait_geometry() {
|
||||
for (logical, native, expected) in [
|
||||
((1920, 1080), 2.0, (1920, 1080)),
|
||||
((1080, 1920), 2.0, (1080, 1920)),
|
||||
((800, 600), 1.25, (1000, 750)),
|
||||
((3440, 1440), 1.0, (1920, 804)),
|
||||
] {
|
||||
let (size, scale) = capture_size(logical.into(), native);
|
||||
assert_eq!(size, expected.into());
|
||||
assert!(scale <= native);
|
||||
assert!(size.w as u64 * size.h as u64 <= 1920 * 1080);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
use super::super::workspace_capture::{WorkspaceCapture, WorkspaceSource};
|
||||
use super::*;
|
||||
use smithay::output::{Mode, PhysicalProperties, Scale, Subpixel};
|
||||
|
||||
fn fixture() -> (
|
||||
smithay::reexports::calloop::EventLoop<'static, Ferese>,
|
||||
Ferese,
|
||||
Output,
|
||||
) {
|
||||
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
|
||||
let mut config = crate::config::Config::default().runtime_config().unwrap();
|
||||
config.animations_enabled = false;
|
||||
config.wallpaper.path = None;
|
||||
let mut state = Ferese::new(
|
||||
&mut events,
|
||||
smithay::reexports::wayland_server::Display::new().unwrap(),
|
||||
config,
|
||||
)
|
||||
.unwrap();
|
||||
let output = Output::new(
|
||||
"workspace-test".into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: String::new(),
|
||||
model: String::new(),
|
||||
serial_number: String::new(),
|
||||
},
|
||||
);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size: (1600, 1200).into(),
|
||||
refresh: 60_000,
|
||||
}),
|
||||
None,
|
||||
Some(Scale::Fractional(2.0)),
|
||||
None,
|
||||
);
|
||||
state.space.map_output(&output, (0, 0));
|
||||
state.register_output(&output, "workspace-test".into());
|
||||
(events, state, output)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_capture_samples_hidden_layout_without_switching_or_configuring_clients() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::image_capture::workspace_tests::workspace_capture_samples_hidden_layout_without_switching_or_configuring_clients",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, output) = fixture();
|
||||
let (window, _client) = crate::state::test_window(&mut state, &mut events, 0xffff0000);
|
||||
let id = state.windows.ids()[&window];
|
||||
let workspace = state.workspaces.workspace_for_window(id).unwrap();
|
||||
let source = WorkspaceSource::new(&state, workspace).unwrap();
|
||||
state.switch_workspace(2);
|
||||
let (private, _private_client) = crate::state::test_window(&mut state, &mut events, 0xff00ff00);
|
||||
let private_id = state.windows.ids()[&private];
|
||||
let focused = state.focused_window;
|
||||
let active = state.workspaces.active_id();
|
||||
let geometry = *state.windows.geometry(&id).unwrap();
|
||||
let sample = state.sample_workspace_frame(workspace, &output);
|
||||
assert!(sample.windows.contains_key(&id));
|
||||
assert!(!sample.windows.contains_key(&private_id));
|
||||
assert!(!sample.overview.is_presenting());
|
||||
assert!(sample.capture_tabs.is_some());
|
||||
assert_eq!(state.workspaces.active_id(), active);
|
||||
assert_eq!(state.focused_window, focused);
|
||||
assert_eq!(*state.windows.geometry(&id).unwrap(), geometry);
|
||||
assert!(state.space.element_location(&window).is_none());
|
||||
assert!(source.valid(&state));
|
||||
// The descriptor is never part of the desktop or advertised to clients.
|
||||
assert!(state.output_id(&source.render_output).is_none());
|
||||
assert!(state.space.output_geometry(&source.render_output).is_none());
|
||||
// A newly mapped hidden client need not have acquired a display sample.
|
||||
state.windows.record_mut(id).unwrap().geometry = None;
|
||||
assert!(
|
||||
state
|
||||
.sample_workspace_frame(workspace, &output)
|
||||
.windows
|
||||
.contains_key(&id)
|
||||
);
|
||||
assert!(state.windows.record(id).unwrap().geometry.is_none());
|
||||
output.change_current_state(None, None, Some(Scale::Fractional(1.5)), None);
|
||||
assert!(!source.valid(&state));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_commits_invalidate_only_subscribed_source_and_allocate_no_targets() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::image_capture::workspace_tests::workspace_commits_invalidate_only_subscribed_source_and_allocate_no_targets",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let (mut events, mut state, _) = fixture();
|
||||
let (first, _first_client) = crate::state::test_window(&mut state, &mut events, 0xffff0000);
|
||||
let first_id = state.windows.ids()[&first];
|
||||
let workspace = state.workspaces.workspace_for_window(first_id).unwrap();
|
||||
let source = WorkspaceSource::new(&state, workspace).unwrap();
|
||||
state.switch_workspace(2);
|
||||
let (second, _second_client) = crate::state::test_window(&mut state, &mut events, 0xff00ff00);
|
||||
for cursor in [false, true] {
|
||||
state.image_capture.workspaces.insert(
|
||||
(workspace, cursor),
|
||||
WorkspaceCapture::new(source.clone(), state.capture_epoch),
|
||||
);
|
||||
}
|
||||
crate::backends::direct::render_surface(&mut state, second.toplevel().unwrap().wl_surface());
|
||||
for view in state.image_capture.workspaces.values() {
|
||||
assert_eq!(view.content, 0);
|
||||
assert!(view.target.is_none());
|
||||
}
|
||||
crate::backends::direct::render_surface(&mut state, first.toplevel().unwrap().wl_surface());
|
||||
for view in state.image_capture.workspaces.values() {
|
||||
assert_eq!(view.content, 1);
|
||||
}
|
||||
state
|
||||
.image_capture
|
||||
.workspace_callbacks
|
||||
.insert(workspace, Instant::now());
|
||||
state.refresh_capture_sessions();
|
||||
assert!(state.image_capture.workspaces.is_empty());
|
||||
assert!(state.image_capture.workspace_callbacks.is_empty());
|
||||
assert!(state.image_capture.callback_timer.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capture_admission_accounts_for_the_pool_and_transfer_targets() {
|
||||
assert_eq!(stream_bytes((1920, 1080).into()), 8192 * 1080 * 10);
|
||||
assert!(stream_bytes((3840, 2160).into()) < MAX_STREAM_BYTES);
|
||||
assert!(stream_bytes((i32::MAX, i32::MAX).into()) > MAX_STREAM_BYTES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_tab_metadata_invalidates_only_owning_capture() {
|
||||
if !crate::startup_tests::private_runtime(
|
||||
"handlers::image_capture::workspace_tests::workspace_tab_metadata_invalidates_only_owning_capture",
|
||||
) {
|
||||
return;
|
||||
}
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::{XdgShellHandler, XdgToplevelSurfaceData};
|
||||
|
||||
for hidden in [false, true] {
|
||||
for title in [false, true] {
|
||||
let (mut events, mut state, output) = fixture();
|
||||
let (first, _first_client) =
|
||||
crate::state::test_window(&mut state, &mut events, 0xffff0000);
|
||||
state.toggle_column_tabbed();
|
||||
let (_second, _second_client) =
|
||||
crate::state::test_window(&mut state, &mut events, 0xff00ff00);
|
||||
state.consume_focused_window();
|
||||
let workspace = state.workspaces.active_id();
|
||||
let scene = state.sample_workspace_frame(workspace, &output);
|
||||
assert!(!scene.capture_tabs.as_ref().unwrap().is_empty());
|
||||
assert!(state.space.element_location(&first).is_none());
|
||||
|
||||
state.switch_workspace(2);
|
||||
let (_other, _other_client) =
|
||||
crate::state::test_window(&mut state, &mut events, 0xff0000ff);
|
||||
let other_workspace = state.workspaces.active_id();
|
||||
if !hidden {
|
||||
state.switch_workspace(1);
|
||||
}
|
||||
for id in [workspace, other_workspace] {
|
||||
for cursor in [false, true] {
|
||||
let mut view = WorkspaceCapture::new(
|
||||
WorkspaceSource::new(&state, id).unwrap(),
|
||||
state.capture_epoch,
|
||||
);
|
||||
view.prepared = Some(view.content);
|
||||
view.scene = Some(state.sample_workspace_frame(id, &output));
|
||||
view.scene_dirty = false;
|
||||
state.image_capture.workspaces.insert((id, cursor), view);
|
||||
}
|
||||
}
|
||||
let epoch = state.capture_epoch;
|
||||
let top = first.toplevel().unwrap();
|
||||
with_states(top.wl_surface(), |states| {
|
||||
let mut metadata = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.unwrap()
|
||||
.lock()
|
||||
.unwrap();
|
||||
if title {
|
||||
metadata.title = Some("Updated tab title".into());
|
||||
} else {
|
||||
metadata.title = None;
|
||||
metadata.app_id = Some("org.test.UpdatedApp".into());
|
||||
}
|
||||
});
|
||||
if title {
|
||||
state.title_changed(top.clone());
|
||||
} else {
|
||||
state.app_id_changed(top.clone());
|
||||
}
|
||||
assert_eq!(state.capture_epoch, epoch);
|
||||
for cursor in [false, true] {
|
||||
let view = &state.image_capture.workspaces[&(workspace, cursor)];
|
||||
assert_ne!(
|
||||
view.prepared,
|
||||
Some(view.content),
|
||||
"tab metadata must invalidate its capture (hidden={hidden}, title={title})"
|
||||
);
|
||||
assert!(
|
||||
!view.scene_dirty,
|
||||
"metadata must not force a layout resample"
|
||||
);
|
||||
let other = &state.image_capture.workspaces[&(other_workspace, cursor)];
|
||||
assert_eq!(other.prepared, Some(other.content));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,13 @@ impl XdgShellHandler for Ferese {
|
||||
fn title_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
if let Some(workspace) = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
.and_then(|id| self.workspaces.workspace_for_window(id))
|
||||
{
|
||||
self.image_capture.workspace_changed(workspace);
|
||||
}
|
||||
self.refresh_idle_inhibition();
|
||||
self.send_shell_snapshots();
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
@@ -49,6 +56,13 @@ impl XdgShellHandler for Ferese {
|
||||
fn app_id_changed(&mut self, surface: ToplevelSurface) {
|
||||
self.refresh_capture_privacy();
|
||||
if self.windows.refresh_metadata(surface.wl_surface()) {
|
||||
if let Some(workspace) = self
|
||||
.windows
|
||||
.id_for_surface(surface.wl_surface())
|
||||
.and_then(|id| self.workspaces.workspace_for_window(id))
|
||||
{
|
||||
self.image_capture.workspace_changed(workspace);
|
||||
}
|
||||
self.refresh_idle_inhibition();
|
||||
self.send_shell_snapshots();
|
||||
for output in self.surface_outputs(surface.wl_surface()) {
|
||||
|
||||
@@ -38,7 +38,8 @@ use overview::{overview_chrome_element, overview_strip_elements};
|
||||
pub(crate) use resources::RenderResources;
|
||||
use scene::cursor_elements;
|
||||
pub(crate) use scene::{
|
||||
capture_output_elements, frame_effect_metrics, layer_surfaces, sampled_output_elements,
|
||||
capture_output_elements, capture_workspace_elements, frame_effect_metrics, layer_surfaces,
|
||||
sampled_output_elements,
|
||||
};
|
||||
pub(crate) use shader::SharedPixelShaderElement;
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
|
||||
@@ -21,7 +21,12 @@ pub(super) fn overview_chrome_element(
|
||||
width: f64,
|
||||
radius: f64,
|
||||
) -> Option<PhysicalShaderElement> {
|
||||
let output_id = state.output_id(output)?;
|
||||
let output_id = state.output_id(output).or_else(|| {
|
||||
output
|
||||
.user_data()
|
||||
.get::<crate::handlers::workspace_capture::CaptureOutputId>()
|
||||
.map(|id| id.0)
|
||||
})?;
|
||||
let geometry = rounded_visual_rect(rect, output_geometry.loc);
|
||||
let corners = RoundedRect::new(rect, output_geometry.loc, scale, radius);
|
||||
let mode = output.current_mode()?;
|
||||
|
||||
@@ -56,6 +56,18 @@ pub(super) struct ContextPrograms {
|
||||
}
|
||||
|
||||
impl RenderResources {
|
||||
pub(crate) fn retain_workspace_windows(
|
||||
&mut self,
|
||||
windows: &std::collections::HashSet<WindowId>,
|
||||
) {
|
||||
self.windows.retain(|id, _| windows.contains(id));
|
||||
for output in self.outputs.values_mut() {
|
||||
output
|
||||
.chrome
|
||||
.retain(|(id, _), _| windows.contains(&WindowId(*id)));
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn prepare_window_corners(&mut self, id: WindowId, shape: CornerShape) -> bool {
|
||||
let buffers = self.windows.entry(id).or_default();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ pub(crate) fn sampled_output_elements(
|
||||
include_cursor: bool,
|
||||
frame: &crate::state::FrameScene,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
scene_elements(state, renderer, output, include_cursor, frame, false)
|
||||
scene_elements(state, renderer, output, include_cursor, frame, false, None)
|
||||
}
|
||||
|
||||
pub(crate) fn capture_output_elements(
|
||||
@@ -46,11 +46,35 @@ pub(crate) fn capture_output_elements(
|
||||
// display rendering. Only the capture scene can populate these caches.
|
||||
crate::backends::direct::capture::touch_cache(state);
|
||||
std::mem::swap(&mut state.render, &mut state.capture_render);
|
||||
let elements = scene_elements(state, renderer, output, include_cursor, frame, true);
|
||||
let elements = scene_elements(state, renderer, output, include_cursor, frame, true, None);
|
||||
std::mem::swap(&mut state.render, &mut state.capture_render);
|
||||
elements
|
||||
}
|
||||
|
||||
/// The caller supplies resources owned by this capture view. No display or
|
||||
/// other workspace's blur/snapshot cache may populate this scene.
|
||||
pub(crate) fn capture_workspace_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
view: &crate::handlers::workspace_capture::WorkspaceSource,
|
||||
include_cursor: bool,
|
||||
frame: &crate::state::FrameScene,
|
||||
resources: &mut super::RenderResources,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
std::mem::swap(&mut state.render, resources);
|
||||
let elements = scene_elements(
|
||||
state,
|
||||
renderer,
|
||||
&view.render_output,
|
||||
include_cursor,
|
||||
frame,
|
||||
true,
|
||||
Some(view),
|
||||
);
|
||||
std::mem::swap(&mut state.render, resources);
|
||||
elements
|
||||
}
|
||||
|
||||
fn scene_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
@@ -58,6 +82,7 @@ fn scene_elements(
|
||||
include_cursor: bool,
|
||||
frame: &crate::state::FrameScene,
|
||||
capture: bool,
|
||||
workspace: Option<&crate::handlers::workspace_capture::WorkspaceSource>,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
if state.session_lock.active() {
|
||||
let Some(geometry) = state.space.output_geometry(output) else {
|
||||
@@ -138,7 +163,10 @@ fn scene_elements(
|
||||
);
|
||||
return elements;
|
||||
}
|
||||
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||
let Some(output_geometry) = workspace
|
||||
.map(|view| view.geometry)
|
||||
.or_else(|| state.space.output_geometry(output))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
@@ -155,7 +183,9 @@ fn scene_elements(
|
||||
} else {
|
||||
&[Layer::Overlay, Layer::Top]
|
||||
};
|
||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||
if workspace.is_none() {
|
||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||
}
|
||||
if frame.overview.is_presenting() {
|
||||
elements.extend(overview_strip_elements(
|
||||
state,
|
||||
@@ -170,14 +200,22 @@ fn scene_elements(
|
||||
let prepare_window = |window: &Window| {
|
||||
// Configure immediately, but present the frame/shadow only after
|
||||
// the first buffer commit has settled the actual client geometry.
|
||||
if (capture && state.capture_protected(window)) || !state.window_content_ready(window) {
|
||||
let ready = if workspace.is_some() {
|
||||
crate::state::window_has_buffer(window)
|
||||
} else {
|
||||
state.window_content_ready(window)
|
||||
};
|
||||
if (capture && state.capture_protected(window)) || !ready {
|
||||
return None;
|
||||
}
|
||||
let id = *state.windows.ids().get(window)?;
|
||||
// Scrolling columns may sit outside their monitor's rectangle.
|
||||
// They must not reappear on a neighboring output just because
|
||||
// their global animated coordinates overlap it.
|
||||
if !state.window_belongs_to_output(id, output) {
|
||||
if workspace.map_or_else(
|
||||
|| !state.window_belongs_to_output(id, output),
|
||||
|workspace| state.workspaces.workspace_for_window(id) != Some(workspace.id),
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let sample = frame.windows.get(&id)?;
|
||||
@@ -194,12 +232,23 @@ fn scene_elements(
|
||||
presentation_alpha,
|
||||
))
|
||||
};
|
||||
let windows = if state.overview.is_active() {
|
||||
let windows = if workspace.is_none() && state.overview.is_active() {
|
||||
state
|
||||
.windows
|
||||
.overview_windows()
|
||||
.filter_map(prepare_window)
|
||||
.collect::<Vec<_>>()
|
||||
} else if workspace.is_some() {
|
||||
let mut windows = frame
|
||||
.windows
|
||||
.keys()
|
||||
.filter_map(|id| state.windows.window(*id))
|
||||
.filter_map(prepare_window)
|
||||
.collect::<Vec<_>>();
|
||||
windows.sort_by_key(|(_, id, sample, ..)| {
|
||||
std::cmp::Reverse(state.workspace_capture_stack(*id, sample.geometry))
|
||||
});
|
||||
windows
|
||||
} else {
|
||||
state
|
||||
.space
|
||||
@@ -209,27 +258,38 @@ fn scene_elements(
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut closing = super::closing::grouped_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
windows.iter().map(|(_, id, ..)| *id),
|
||||
frame.delta,
|
||||
);
|
||||
let mut closing = if workspace.is_some() {
|
||||
Default::default()
|
||||
} else {
|
||||
super::closing::grouped_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
windows.iter().map(|(_, id, ..)| *id),
|
||||
frame.delta,
|
||||
)
|
||||
};
|
||||
for (window, id, sample, visual, decoration_progress, presentation_alpha) in windows {
|
||||
if let Some(group) = closing.remove(&Some(id)) {
|
||||
elements.extend(group);
|
||||
}
|
||||
elements.extend(super::tabs::tab_elements(
|
||||
state, renderer, output, frame, id, capture,
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
frame,
|
||||
id,
|
||||
capture,
|
||||
));
|
||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||
let material_surface = window
|
||||
.toplevel()
|
||||
.map(|toplevel| toplevel.wl_surface())
|
||||
.filter(|surface| {
|
||||
crate::effects::surface_role(surface)
|
||||
.is_some_and(|(role, _)| role == crate::effects::SemanticRole::Modal)
|
||||
workspace.is_none()
|
||||
&& crate::effects::surface_role(surface)
|
||||
.is_some_and(|(role, _)| role == crate::effects::SemanticRole::Modal)
|
||||
});
|
||||
let window_radius = if material_surface.is_some() {
|
||||
state.theme_settings.material_radius
|
||||
@@ -418,13 +478,15 @@ fn scene_elements(
|
||||
if let Some(group) = closing.remove(&None) {
|
||||
elements.extend(group);
|
||||
}
|
||||
elements.extend(layer_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
&[Layer::Bottom, Layer::Background],
|
||||
));
|
||||
state.prepare_theme_wallpaper(renderer, output);
|
||||
if workspace.is_none() {
|
||||
elements.extend(layer_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
&[Layer::Bottom, Layer::Background],
|
||||
));
|
||||
}
|
||||
state.prepare_theme_wallpaper(renderer, workspace.map_or(output, |view| &view.output));
|
||||
let progress = state.theme_progress();
|
||||
let wallpaper = state.wallpaper.element(renderer, output);
|
||||
let has_wallpaper = wallpaper.is_some();
|
||||
|
||||
@@ -113,17 +113,17 @@ pub(super) fn elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
output_geometry: Rectangle<i32, Logical>,
|
||||
frame: &crate::state::FrameScene,
|
||||
bar: &TabBar,
|
||||
content: Rect,
|
||||
alpha: f32,
|
||||
capture: bool,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
let Some(g) = state.tab_indicator_geometry(bar, content) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||
let Some(g) = state.tab_indicator_geometry_for_frame(bar, content, frame) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let settings = state.tab_indicator_settings;
|
||||
let anchor = bar.group.windows[0];
|
||||
|
||||
@@ -9,6 +9,7 @@ pub(super) fn tab_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
output_geometry: Rectangle<i32, super::Logical>,
|
||||
frame: &crate::state::FrameScene,
|
||||
active: ferese_layout::WindowId,
|
||||
capture: bool,
|
||||
@@ -19,13 +20,27 @@ pub(super) fn tab_elements(
|
||||
let Some(workspace) = state.workspaces.workspace_for_window(active) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(bar) = state
|
||||
.visible_tab_bars(workspace)
|
||||
.find(|bar| bar.group.active == active)
|
||||
.cloned()
|
||||
else {
|
||||
return Vec::new();
|
||||
let bar = if let Some(tabs) = &frame.capture_tabs {
|
||||
tabs.iter()
|
||||
.find(|bar| bar.group.active == active)
|
||||
.filter(|_| {
|
||||
state
|
||||
.workspaces
|
||||
.workspace(workspace)
|
||||
.is_some_and(|ws| ws.fullscreen.is_none())
|
||||
&& state
|
||||
.windows
|
||||
.record(active)
|
||||
.is_some_and(|window| !window.maximized)
|
||||
})
|
||||
.cloned()
|
||||
} else {
|
||||
state
|
||||
.visible_tab_bars(workspace)
|
||||
.find(|bar| bar.group.active == active)
|
||||
.cloned()
|
||||
};
|
||||
let Some(bar) = bar else { return Vec::new() };
|
||||
let Some(sample) = frame.windows.get(&active) else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -37,6 +52,8 @@ pub(super) fn tab_elements(
|
||||
state,
|
||||
renderer,
|
||||
output,
|
||||
output_geometry,
|
||||
frame,
|
||||
&bar,
|
||||
sample.presentation.bounds.current,
|
||||
sample.presentation.alpha(),
|
||||
@@ -48,9 +65,7 @@ pub(super) fn tab_elements(
|
||||
}
|
||||
let content = sample.presentation.bounds.current;
|
||||
let alpha = sample.presentation.alpha();
|
||||
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let width = content.width / bar.group.windows.len() as f64;
|
||||
let shell = ferese_layout::Rect::new(
|
||||
|
||||
@@ -202,6 +202,20 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
for workspace in self.image_capture.captured_workspaces() {
|
||||
if let Some(output) = self
|
||||
.output_workspaces
|
||||
.output_for_workspace(workspace)
|
||||
.and_then(|id| self.outputs_by_id.get(&id))
|
||||
&& self.output_id(output).is_some_and(|id| {
|
||||
self.output_workspaces.active_workspace(id) == Some(workspace)
|
||||
})
|
||||
&& (self.output_has_animations(output)
|
||||
|| self.output_has_pending_visual_changes(output))
|
||||
{
|
||||
self.image_capture.workspace_geometry_changed(workspace);
|
||||
}
|
||||
}
|
||||
let delta = delta.mul_f64(self.animation_speed);
|
||||
if let Some(workspace) = self
|
||||
.focus_swipe()
|
||||
|
||||
+121
-41
@@ -17,6 +17,7 @@ struct Preview {
|
||||
layout: WorkspaceLayout,
|
||||
input: PreviewInput,
|
||||
rectangles: HashMap<WindowId, Rect>,
|
||||
tabs: Vec<ferese_layout::TabBar>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@@ -126,6 +127,10 @@ impl Ferese {
|
||||
.reconcile_workspaces(&mut self.workspaces, visible)
|
||||
{
|
||||
self.viewports.remove(&workspace);
|
||||
self.workspace_previews
|
||||
.borrow_mut()
|
||||
.entries
|
||||
.remove(&workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,6 +202,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn relayout_outputs(&mut self, mut redraw_outputs: Option<Vec<Output>>, finalize: bool) {
|
||||
self.image_capture.invalidate_workspaces();
|
||||
if self
|
||||
.desktop_transition
|
||||
.as_ref()
|
||||
@@ -705,6 +711,9 @@ impl Ferese {
|
||||
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||
if let Some(id) = self.windows.ids().get(window) {
|
||||
self.window_stack.raise(*id);
|
||||
if let Some(workspace) = self.workspaces.workspace_for_window(*id) {
|
||||
self.image_capture.workspace_geometry_changed(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
self.space.raise_element(window, activate);
|
||||
@@ -781,6 +790,14 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(workspace) = self
|
||||
.windows
|
||||
.ids()
|
||||
.get(&window)
|
||||
.and_then(|id| self.workspaces.workspace_for_window(*id))
|
||||
{
|
||||
self.image_capture.workspace_geometry_changed(workspace);
|
||||
}
|
||||
self.space.map_element(window, location, false);
|
||||
self.stacking_cache.remapped();
|
||||
}
|
||||
@@ -805,21 +822,50 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_preview_rects(&self) -> HashMap<WindowId, Rect> {
|
||||
let constraints = self.window_constraints();
|
||||
self.workspace_preview_rects_for(None)
|
||||
}
|
||||
|
||||
/// Capture resolves only its subscribed workspace; it never activates or
|
||||
/// configures hidden clients. The preview cache also serves overview.
|
||||
pub(crate) fn workspace_preview_rects_for(
|
||||
&self,
|
||||
selected: Option<WorkspaceId>,
|
||||
) -> HashMap<WindowId, Rect> {
|
||||
if let Some(id) = selected {
|
||||
self.workspace_preview_views(
|
||||
selected,
|
||||
self.output_workspaces
|
||||
.output_for_workspace(id)
|
||||
.map(|output| (id, output))
|
||||
.into_iter(),
|
||||
)
|
||||
} else {
|
||||
self.workspace_preview_views(
|
||||
None,
|
||||
self.output_workspaces
|
||||
.workspace_views_iter(&self.workspaces)
|
||||
.map(|view| (view.id, view.output)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_preview_views(
|
||||
&self,
|
||||
selected: Option<WorkspaceId>,
|
||||
views: impl Iterator<Item = (WorkspaceId, ferese_core::OutputId)>,
|
||||
) -> HashMap<WindowId, Rect> {
|
||||
let constraints = self.window_constraints_for(selected);
|
||||
let mut cache = self.workspace_previews.borrow_mut();
|
||||
let mut live = HashSet::new();
|
||||
let mut previews = HashMap::new();
|
||||
for view in self
|
||||
.output_workspaces
|
||||
.workspace_views_iter(&self.workspaces)
|
||||
{
|
||||
let Some(workspace) = self.workspaces.workspace(view.id) else {
|
||||
for (workspace_id, owner) in views {
|
||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(output) = self
|
||||
.output_ids
|
||||
.iter()
|
||||
.find(|(_, id)| **id == view.output)
|
||||
.find(|(_, id)| **id == owner)
|
||||
.map(|(o, _)| o)
|
||||
else {
|
||||
continue;
|
||||
@@ -827,7 +873,7 @@ impl Ferese {
|
||||
let Some(bounds) = self.output_bounds_for(output) else {
|
||||
continue;
|
||||
};
|
||||
live.insert(view.id);
|
||||
live.insert(workspace_id);
|
||||
let mut windows: Vec<_> = workspace
|
||||
.layout
|
||||
.window_ids()
|
||||
@@ -859,21 +905,21 @@ impl Ferese {
|
||||
};
|
||||
if !cache
|
||||
.entries
|
||||
.get(&view.id)
|
||||
.get(&workspace_id)
|
||||
.is_some_and(|cached| cached.input == input && cached.layout == workspace.layout)
|
||||
{
|
||||
// Only clone and solve the changed workspace. Titles, urgency and
|
||||
// other shell metadata never invalidate geometry.
|
||||
let mut layout = workspace.layout.clone();
|
||||
let mut rectangles = layout
|
||||
let resolved = layout
|
||||
.resolve_geometry_with_constraints(
|
||||
input.bounds,
|
||||
input.gaps,
|
||||
&constraints,
|
||||
input.focused,
|
||||
)
|
||||
.map(|resolved| resolved.geometry)
|
||||
.unwrap_or_default();
|
||||
let mut rectangles = resolved.geometry;
|
||||
for window in &input.windows {
|
||||
if let Some(rect) = window.floating {
|
||||
rectangles.insert(
|
||||
@@ -894,11 +940,12 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
cache.entries.insert(
|
||||
view.id,
|
||||
workspace_id,
|
||||
Preview {
|
||||
layout: workspace.layout.clone(),
|
||||
input,
|
||||
rectangles,
|
||||
tabs: resolved.tab_bars,
|
||||
},
|
||||
);
|
||||
#[cfg(test)]
|
||||
@@ -907,44 +954,77 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
previews.extend(
|
||||
cache.entries[&view.id]
|
||||
cache.entries[&workspace_id]
|
||||
.rectangles
|
||||
.iter()
|
||||
.map(|(&id, &rect)| (id, rect)),
|
||||
);
|
||||
}
|
||||
cache.entries.retain(|id, _| live.contains(id));
|
||||
cache
|
||||
.entries
|
||||
.retain(|id, _| selected.is_some_and(|selected| selected != *id) || live.contains(id));
|
||||
previews
|
||||
}
|
||||
|
||||
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||
self.windows
|
||||
.ids()
|
||||
.iter()
|
||||
.filter_map(|(window, id)| {
|
||||
let toplevel = window.toplevel()?;
|
||||
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
(state.min_size, state.max_size)
|
||||
});
|
||||
let (minimum, maximum) = self.effective_size_constraints(
|
||||
window,
|
||||
(minimum.w, minimum.h),
|
||||
(maximum.w, maximum.h),
|
||||
);
|
||||
pub(super) fn workspace_preview_tabs(&self, id: WorkspaceId) -> Vec<ferese_layout::TabBar> {
|
||||
self.workspace_previews
|
||||
.borrow()
|
||||
.entries
|
||||
.get(&id)
|
||||
.map_or_else(Vec::new, |preview| preview.tabs.clone())
|
||||
}
|
||||
|
||||
Some((
|
||||
*id,
|
||||
SizeConstraints {
|
||||
min_width: minimum.0.max(1) as f64,
|
||||
min_height: minimum.1.max(1) as f64,
|
||||
max_width: (maximum.0 > 0).then_some(maximum.0 as f64),
|
||||
max_height: (maximum.1 > 0).then_some(maximum.1 as f64),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||
self.window_constraints_for(None)
|
||||
}
|
||||
|
||||
fn window_constraints_for(
|
||||
&self,
|
||||
workspace: Option<WorkspaceId>,
|
||||
) -> HashMap<WindowId, SizeConstraints> {
|
||||
if let Some(workspace) = workspace {
|
||||
self.constraints_for_windows(self.workspaces.workspace(workspace).into_iter().flat_map(
|
||||
|workspace| {
|
||||
workspace
|
||||
.layout
|
||||
.window_ids()
|
||||
.chain(workspace.floating.iter().copied())
|
||||
},
|
||||
))
|
||||
} else {
|
||||
self.constraints_for_windows(self.windows.ids().values().copied())
|
||||
}
|
||||
}
|
||||
|
||||
fn constraints_for_windows(
|
||||
&self,
|
||||
ids: impl Iterator<Item = WindowId>,
|
||||
) -> HashMap<WindowId, SizeConstraints> {
|
||||
ids.filter_map(|id| {
|
||||
let window = self.windows.window(id)?;
|
||||
let toplevel = window.toplevel()?;
|
||||
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
(state.min_size, state.max_size)
|
||||
});
|
||||
let (minimum, maximum) = self.effective_size_constraints(
|
||||
window,
|
||||
(minimum.w, minimum.h),
|
||||
(maximum.w, maximum.h),
|
||||
);
|
||||
|
||||
Some((
|
||||
id,
|
||||
SizeConstraints {
|
||||
min_width: minimum.0.max(1) as f64,
|
||||
min_height: minimum.1.max(1) as f64,
|
||||
max_width: (maximum.0 > 0).then_some(maximum.0 as f64),
|
||||
max_height: (maximum.1 > 0).then_some(maximum.1 as f64),
|
||||
},
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn arrange_layers(&self) {
|
||||
|
||||
@@ -7,6 +7,7 @@ use super::{
|
||||
/// geometry, configure barriers, animation clocks, or another output's view.
|
||||
pub(crate) struct FrameScene {
|
||||
pub windows: HashMap<WindowId, WindowFrame>,
|
||||
pub capture_tabs: Option<Vec<ferese_layout::TabBar>>,
|
||||
pub overview: crate::overview::OverviewMotion,
|
||||
// Plane selection and scheduling reuse the activity sampled with this scene.
|
||||
pub animating: bool,
|
||||
@@ -332,6 +333,113 @@ impl Ferese {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_capture_visible(
|
||||
&self,
|
||||
workspace: ferese_core::WorkspaceId,
|
||||
output: ferese_core::OutputId,
|
||||
) -> bool {
|
||||
self.output_workspaces.active_workspace(output) == Some(workspace)
|
||||
&& !self.workspace_slides.contains_key(&output)
|
||||
}
|
||||
|
||||
pub(crate) fn workspace_capture_stack(
|
||||
&self,
|
||||
id: WindowId,
|
||||
geometry: WindowGeometry,
|
||||
) -> (u8, usize) {
|
||||
let floating = matches!(
|
||||
self.workspaces.placement(id),
|
||||
Some(ferese_core::WindowPlacement::Floating { .. })
|
||||
);
|
||||
let above = self
|
||||
.floating_above_fullscreen
|
||||
.get(&id)
|
||||
.is_some_and(|parent| {
|
||||
self.workspaces
|
||||
.workspace_for_window(id)
|
||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||
.is_some_and(|workspace| workspace.fullscreen == Some(*parent))
|
||||
});
|
||||
(
|
||||
crate::stacking::layer_priority(floating, false, geometry.is_fullscreen(), above),
|
||||
self.window_stack.rank(id),
|
||||
)
|
||||
}
|
||||
|
||||
/// An independent workspace view. Global overview, output switching and
|
||||
/// focus on another workspace do not enter this presentation.
|
||||
pub(crate) fn sample_workspace_frame(
|
||||
&self,
|
||||
workspace: ferese_core::WorkspaceId,
|
||||
output: &Output,
|
||||
) -> FrameScene {
|
||||
let rectangles = self.workspace_preview_rects_for(Some(workspace));
|
||||
let visible = self.output_id(output).is_some_and(|output| {
|
||||
self.output_workspaces.active_workspace(output) == Some(workspace)
|
||||
});
|
||||
let focused = self
|
||||
.workspaces
|
||||
.workspace(workspace)
|
||||
.and_then(|workspace| workspace.last_focused);
|
||||
let mut windows = HashMap::with_capacity(rectangles.len());
|
||||
for (id, rect) in rectangles {
|
||||
let Some(record) = self.windows.record(id) else {
|
||||
continue;
|
||||
};
|
||||
let Some(committed) = self.windows.window(id).and_then(super::client_size) else {
|
||||
continue;
|
||||
};
|
||||
let mut geometry = record
|
||||
.geometry
|
||||
.unwrap_or_else(|| WindowGeometry::new(rect, Some(committed)));
|
||||
if !visible {
|
||||
// A hidden window can have no display presentation yet. Resolve
|
||||
// only this value; do not issue a client configure or map it.
|
||||
geometry = WindowGeometry::new(rect, Some(committed));
|
||||
let fullscreen = self
|
||||
.workspaces
|
||||
.workspace(workspace)
|
||||
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||
let mode = if fullscreen {
|
||||
super::PresentationMode::Fullscreen
|
||||
} else if record.maximized {
|
||||
super::PresentationMode::Maximized
|
||||
} else {
|
||||
super::PresentationMode::Normal
|
||||
};
|
||||
let _ = geometry.set_presentation_mode(rect, mode, Duration::ZERO);
|
||||
geometry.settle_presentation();
|
||||
geometry.presentation_changed = false;
|
||||
}
|
||||
let emphasis = AnimatedValue::new(if focused == Some(id) { 1.0 } else { 0.0 });
|
||||
let presentation = crate::presentation::WindowPresentation {
|
||||
id,
|
||||
bounds: geometry.visual,
|
||||
opacity: AnimatedValue::new(1.0),
|
||||
focus_alpha: 1.0,
|
||||
dim: 0.0,
|
||||
emphasis,
|
||||
shadow: emphasis,
|
||||
scale_content: false,
|
||||
native_size: None,
|
||||
};
|
||||
windows.insert(
|
||||
id,
|
||||
WindowFrame {
|
||||
geometry,
|
||||
presentation,
|
||||
},
|
||||
);
|
||||
}
|
||||
FrameScene {
|
||||
windows,
|
||||
capture_tabs: Some(self.workspace_preview_tabs(workspace)),
|
||||
overview: Default::default(),
|
||||
animating: false,
|
||||
delta: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
|
||||
let animating = self.output_has_animations(output);
|
||||
let delta = if self.animations_enabled && animating {
|
||||
@@ -406,6 +514,7 @@ impl Ferese {
|
||||
|
||||
FrameScene {
|
||||
windows,
|
||||
capture_tabs: None,
|
||||
overview,
|
||||
animating,
|
||||
delta,
|
||||
|
||||
@@ -184,10 +184,33 @@ impl Ferese {
|
||||
&self,
|
||||
bar: &ferese_layout::TabBar,
|
||||
content: Rect,
|
||||
) -> Option<IndicatorGeometry> {
|
||||
self.tab_indicator_geometry_sampled(bar, content, None)
|
||||
}
|
||||
|
||||
pub(crate) fn tab_indicator_geometry_for_frame(
|
||||
&self,
|
||||
bar: &ferese_layout::TabBar,
|
||||
content: Rect,
|
||||
frame: &super::FrameScene,
|
||||
) -> Option<IndicatorGeometry> {
|
||||
self.tab_indicator_geometry_sampled(
|
||||
bar,
|
||||
content,
|
||||
frame.capture_tabs.as_ref().map(|_| frame),
|
||||
)
|
||||
}
|
||||
|
||||
fn tab_indicator_geometry_sampled(
|
||||
&self,
|
||||
bar: &ferese_layout::TabBar,
|
||||
content: Rect,
|
||||
frame: Option<&super::FrameScene>,
|
||||
) -> Option<IndicatorGeometry> {
|
||||
let workspace = self.workspaces.workspace_for_window(bar.group.active)?;
|
||||
let output_id = self.output_workspaces.output_for_workspace(workspace)?;
|
||||
if self.output_workspaces.active_workspace(output_id) != Some(workspace) {
|
||||
if frame.is_none() && self.output_workspaces.active_workspace(output_id) != Some(workspace)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let output = self.outputs_by_id.get(&output_id)?;
|
||||
@@ -200,17 +223,30 @@ impl Ferese {
|
||||
);
|
||||
let mut boundary = bounds.x;
|
||||
// Measure the current visible gap, including animated column positions.
|
||||
for window in self.space.elements() {
|
||||
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||
continue;
|
||||
};
|
||||
let captured = frame
|
||||
.into_iter()
|
||||
.flat_map(|frame| frame.windows.keys().copied());
|
||||
let displayed = self
|
||||
.space
|
||||
.elements()
|
||||
.filter(|_| frame.is_none())
|
||||
.filter_map(|window| self.windows.ids().get(window).copied());
|
||||
for id in captured.chain(displayed) {
|
||||
if id == bar.group.active
|
||||
|| self.workspaces.workspace_for_window(id) != Some(workspace)
|
||||
|| !self.workspaces.workspace(workspace)?.layout.contains(id)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(rect) = self.presented_window_rect(id) else {
|
||||
let Some(rect) = frame.map_or_else(
|
||||
|| self.presented_window_rect(id),
|
||||
|frame| {
|
||||
frame
|
||||
.windows
|
||||
.get(&id)
|
||||
.map(|sample| sample.presentation.bounds.current)
|
||||
},
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if rect.x + rect.width <= content.x
|
||||
|
||||
Reference in new issue
Block a user