1
0
Fork 0
forked from wry/wry

Compare commits

..

1 commit

Author SHA1 Message Date
9b2550d567 fix: screencopy and portal capture state handling 2026-06-04 14:49:12 +10:00
38 changed files with 162 additions and 208 deletions

View file

@ -86,9 +86,7 @@ impl ExtImageCopyCaptureFrameV1 {
let buffer = self.session.buffer.get().unwrap();
if size != buffer.rect.size() {
self.session.buffer_size_changed();
// https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/222
// self.fail(FrameFailureReason::BufferConstraints);
// return;
return Err(FrameFailureReason::BufferConstraints);
}
if let Err(e) = buffer.update_framebuffer() {
log::error!("Could not import buffer: {}", ErrorFmt(e));
@ -102,6 +100,13 @@ impl ExtImageCopyCaptureFrameV1 {
let mut shm_staging = self.session.shm_staging.take();
match storage {
WlBufferStorage::Shm { mem, stride, .. } => {
log::debug!(
"ext-image-copy frame {:?} using wl_shm readback path: {}x{}, stride {}",
self.id,
buffer.rect.width(),
buffer.rect.height(),
*stride,
);
if let Some(b) = &shm_bridge
&& (b.physical_size() != buffer.rect.size()
|| b.format() != buffer.format
@ -159,6 +164,12 @@ impl ExtImageCopyCaptureFrameV1 {
self.session.shm_staging.set(Some(staging));
}
WlBufferStorage::Dmabuf { fb, .. } => {
log::debug!(
"ext-image-copy frame {:?} using dmabuf GPU copy path: {}x{}",
self.id,
buffer.rect.width(),
buffer.rect.height(),
);
let Some(fb) = fb else {
return Err(FrameFailureReason::BufferConstraints);
};
@ -187,7 +198,11 @@ impl ExtImageCopyCaptureFrameV1 {
) {
match self.try_copy(on, size, f) {
Ok(()) => self.session.status.set(FrameStatus::Captured),
Err(e) => self.fail(e),
Err(e) => {
if self.session.status.get() != FrameStatus::Failed {
self.fail(e);
}
}
}
}

View file

@ -83,6 +83,11 @@ impl ExtImageCopyCaptureSessionV1 {
if self.size_debounce.replace(true) {
return;
}
if let Some(frame) = self.frame.get()
&& let FrameStatus::Capturing | FrameStatus::Captured = self.status.get()
{
frame.fail(FrameFailureReason::BufferConstraints);
}
self.force_capture.set(true);
self.send_current_buffer_size();
self.send_done();

View file

@ -1520,25 +1520,25 @@ impl WlSurface {
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
let pos = self.buffer_abs_pos.get();
let apply_damage = |pos: Rect| {
let clip_damage = |mut damage: Rect| {
damage = damage.intersect(pos);
if pending.damage_full {
let mut damage = pos;
if let Some(bounds) = bounds {
damage = damage.intersect(bounds);
}
damage
};
if pending.damage_full {
self.client.state.damage(clip_damage(pos));
self.client.state.damage(damage);
} else {
let matrix = self.damage_matrix.get();
if let Some(buffer) = self.buffer.get() {
for damage in &pending.buffer_damage {
let damage = matrix.apply(
let mut damage = matrix.apply(
pos.x1(),
pos.y1(),
damage.intersect(buffer.buffer.buf.rect),
);
self.client.state.damage(clip_damage(damage));
if let Some(bounds) = bounds {
damage = damage.intersect(bounds);
}
self.client.state.damage(damage);
}
}
for damage in &pending.surface_damage {
@ -1550,7 +1550,8 @@ impl WlSurface {
let y2 = (damage.y2() + scale - 1) / scale;
damage = Rect::new_saturating(x1, y1, x2, y2);
}
self.client.state.damage(clip_damage(damage));
damage = damage.intersect(bounds.unwrap_or(pos));
self.client.state.damage(damage);
}
}
};

View file

@ -48,16 +48,13 @@ impl ZwlrScreencopyFrameV1 {
}
pub fn send_damage(&self) {
if let Some(output) = self.output.get() {
let pos = output.pos.get();
self.client.event(Damage {
self_id: self.id,
x: 0,
y: 0,
width: pos.width() as _,
height: pos.height() as _,
});
}
self.client.event(Damage {
self_id: self.id,
x: 0,
y: 0,
width: self.rect.width() as _,
height: self.rect.height() as _,
});
}
pub fn send_buffer(&self) {
@ -111,10 +108,28 @@ impl ZwlrScreencopyFrameV1 {
return Err(ZwlrScreencopyFrameV1Error::InvalidBufferFormat);
}
buffer.update_framebuffer()?;
if let Some(WlBufferStorage::Shm { stride, .. }) = buffer.storage.borrow_mut().deref()
&& *stride != self.rect.width() * 4
{
return Err(ZwlrScreencopyFrameV1Error::InvalidBufferStride);
match buffer.storage.borrow_mut().deref() {
Some(WlBufferStorage::Shm { stride, .. }) => {
if *stride != self.rect.width() * 4 {
return Err(ZwlrScreencopyFrameV1Error::InvalidBufferStride);
}
log::debug!(
"zwlr_screencopy frame {:?} using wl_shm readback path: {}x{}, stride {}",
self.id,
self.rect.width(),
self.rect.height(),
*stride,
);
}
Some(WlBufferStorage::Dmabuf { .. }) => {
log::debug!(
"zwlr_screencopy frame {:?} using dmabuf GPU copy path: {}x{}",
self.id,
self.rect.width(),
self.rect.height(),
);
}
_ => {}
}
self.buffer.set(Some(buffer));
if !with_damage && let Some(global) = self.output.get() {
@ -134,6 +149,12 @@ impl ZwlrScreencopyFrameV1 {
}
self.pending.take();
}
pub fn cancel(&self) {
self.buffer.take();
self.pending.take();
self.send_failed();
}
}
impl ZwlrScreencopyFrameV1RequestHandler for ZwlrScreencopyFrameV1 {

View file

@ -104,8 +104,8 @@ impl ZwlrScreencopyManagerV1 {
let Some(global) = output.global.get() else {
return Ok(());
};
let mode = global.mode.get();
let mut rect = Rect::new_sized_saturating(0, 0, mode.width, mode.height);
let (width, height) = global.pixel_size();
let mut rect = Rect::new_sized_saturating(0, 0, width, height);
if let Some(region) = region {
let scale = global.persistent.scale.get().to_f64();
let x1 = (region.x1() as f64 * scale).round() as i32;

View file

@ -29,17 +29,6 @@ impl TestViewport {
Ok(())
}
pub fn unset_source(&self) -> Result<(), TestError> {
self.tran.send(SetSource {
self_id: self.id,
x: Fixed::from_int(-1),
y: Fixed::from_int(-1),
width: Fixed::from_int(-1),
height: Fixed::from_int(-1),
})?;
Ok(())
}
pub fn set_destination(&self, width: i32, height: i32) -> Result<(), TestError> {
self.tran.send(SetDestination {
self_id: self.id,
@ -48,15 +37,6 @@ impl TestViewport {
})?;
Ok(())
}
pub fn unset_destination(&self) -> Result<(), TestError> {
self.tran.send(SetDestination {
self_id: self.id,
width: -1,
height: -1,
})?;
Ok(())
}
}
impl Drop for TestViewport {

View file

@ -1,6 +1,7 @@
use {
crate::{
it::{test_error::TestError, testrun::TestRun},
rect::Rect,
tree::Node,
},
std::rc::Rc,
@ -10,19 +11,29 @@ testcase!();
/// Create and map a single surface
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
let ds = run.create_default_setup().await?;
run.backend.install_default()?;
let client = run.create_client().await?;
let window = client.create_window().await?;
window.map().await?;
let workspace_rect = ds.output.workspace_rect.get();
tassert_eq!(window.tl.core.width.get(), 800);
tassert_eq!(
window.tl.core.height.get(),
600 - 2 * run.state.theme.title_plus_underline_height()
);
tassert_eq!(window.tl.core.width.get(), workspace_rect.width());
tassert_eq!(window.tl.core.height.get(), workspace_rect.height());
tassert_eq!(window.tl.server.node_absolute_position(), workspace_rect);
tassert_eq!(
window.tl.server.node_absolute_position(),
Rect::new_sized(
0,
2 * run.state.theme.title_plus_underline_height(),
window.tl.core.width.get(),
window.tl.core.height.get(),
)
.unwrap()
);
Ok(())
}

View file

@ -11,7 +11,7 @@ testcase!();
/// Create and map two surfaces
async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
let ds = run.create_default_setup().await?;
run.backend.install_default()?;
let client = run.create_client().await?;
@ -21,30 +21,17 @@ async fn test(run: Rc<TestRun>) -> Result<(), TestError> {
let window2 = client.create_window().await?;
window2.map().await?;
let workspace_rect = ds.output.workspace_rect.get();
let otop = 2 * run.state.theme.title_plus_underline_height();
let bw = run.state.theme.sizes.border_width.get();
let child_width = (workspace_rect.width() - bw) / 2;
tassert_eq!(
window.tl.server.node_absolute_position(),
Rect::new_sized(
workspace_rect.x1(),
workspace_rect.y1(),
child_width,
workspace_rect.height(),
)
.unwrap()
Rect::new_sized(0, otop, (800 - bw) / 2, 600 - otop).unwrap()
);
tassert_eq!(
window2.tl.server.node_absolute_position(),
Rect::new_sized(
workspace_rect.x1() + child_width + bw,
workspace_rect.y1(),
child_width,
workspace_rect.height(),
)
.unwrap()
Rect::new_sized((800 - bw) / 2 + bw, otop, (800 - bw) / 2, 600 - otop).unwrap()
);
Ok(())

View file

@ -48,18 +48,13 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let mono_container = w_mono2.tl.container_parent()?;
let container_pos = mono_container.tl_data().pos.get();
let (tab_x, tab_y) = {
let tab_bar = mono_container.tab_bar.borrow();
let Some(tab_bar) = tab_bar.as_ref() else {
bail!("no tab bar");
};
let w_mono1_title = &tab_bar.entries[0];
(
container_pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
container_pos.y1() + tab_bar.height / 2,
)
};
ds.mouse.abs(&ds.connector, tab_x as _, tab_y as _);
let w_mono1_title = mono_container.render_data.borrow_mut().title_rects[0]
.move_(container_pos.x1(), container_pos.y1());
ds.mouse.abs(
&ds.connector,
w_mono1_title.x1() as _,
w_mono1_title.y1() as _,
);
client.sync().await;
tassert!(enters.next().is_err());

View file

@ -26,18 +26,12 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let container = w_mono2.tl.container_parent()?;
let pos = container.tl_data().pos.get();
let (tab_x, tab_y) = {
let tab_bar = container.tab_bar.borrow();
let Some(tab_bar) = tab_bar.as_ref() else {
bail!("no tab bar");
};
let w_mono1_title = &tab_bar.entries[0];
(
pos.x1() + w_mono1_title.x.get() + w_mono1_title.width.get() / 2,
pos.y1() + tab_bar.height / 2,
)
};
ds.mouse.abs(&ds.connector, tab_x as f64, tab_y as f64);
let w_mono1_title = container.render_data.borrow_mut().title_rects[0].move_(pos.x1(), pos.y1());
ds.mouse.abs(
&ds.connector,
w_mono1_title.x1() as f64,
w_mono1_title.y1() as f64,
);
client.sync().await;
let enters = dss.kb.enter.expect()?;

View file

@ -2,7 +2,7 @@ use {
crate::{
ifs::wl_surface::xdg_surface::xdg_toplevel::STATE_SUSPENDED,
it::{
test_error::{TestErrorExt, TestResult},
test_error::TestResult,
test_utils::{
test_ouput_node_ext::TestOutputNodeExt, test_toplevel_node_ext::TestToplevelNodeExt,
},
@ -10,7 +10,7 @@ use {
},
},
isnt::std_1::collections::IsntHashSetExt,
std::{rc::Rc, time::Duration},
std::rc::Rc,
};
testcase!();
@ -19,7 +19,6 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let ds = run.create_default_setup().await?;
let client = run.create_client().await?;
let default_seat = client.get_default_seat().await?;
let win1 = client.create_window().await?;
win1.set_color(255, 0, 0, 255);
@ -45,23 +44,5 @@ async fn test(run: Rc<TestRun>) -> TestResult {
client.sync().await;
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
let leaves = default_seat.kb.leave.expect()?;
let enters = default_seat.kb.enter.expect()?;
run.cfg.set_idle(Duration::from_micros(100))?;
run.cfg.set_idle_grace_period(Duration::from_secs(0))?;
run.state.wheel.timeout(3).await?;
client.sync().await;
tassert!(win2.tl.core.states.borrow().contains(&STATE_SUSPENDED));
let leave = leaves.next().with_context(|| "no leave on suspend")?;
tassert_eq!(leave.surface, win2.surface.id);
ds.mouse.rel(1.0, 1.0);
client.sync().await;
tassert!(win2.tl.core.states.borrow().not_contains(&STATE_SUSPENDED));
let enter = enters.next().with_context(|| "no enter on restore")?;
tassert_eq!(enter.surface, win2.surface.id);
Ok(())
}

View file

@ -308,8 +308,9 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let output_damage = connector_data.damage.borrow();
tassert!(!output_damage.is_empty());
// The test window maps its 1x1 buffer through a viewport to the full window size.
let expected_buffer_damage = surface_pos;
// Buffer damage is transformed by the damage matrix which includes the surface position
// The buffer damage (0,0,1,1) should be transformed to surface coordinates
let expected_buffer_damage = buffer_damage.move_(surface_pos.x1(), surface_pos.y1());
// Find the exact output damage that matches our expected buffer damage
let mut found_exact_buffer_damage = false;
@ -330,12 +331,10 @@ async fn test(run: Rc<TestRun>) -> TestResult {
// Test 7: Check output damage from existing window's viewport (which already has scaling)
connector_data.damage.borrow_mut().clear();
// The existing window was created with create_surface_ext() which automatically creates a viewport.
// Commit the viewport size change separately; that commit intentionally damages the old/new extents.
window.surface.viewport.set_destination(150, 100)?;
window.surface.commit()?;
client.sync().await;
connector_data.damage.borrow_mut().clear();
// The existing window was created with create_surface_ext() which automatically creates a viewport
// Let's verify that the viewport's existing scaling affects buffer damage correctly
// First, let's modify the viewport scaling that already exists on the window
window.surface.viewport.set_destination(150, 100)?; // Change scaling to 150x100
// Add buffer damage to test viewport scaling coordinate transformation
window.surface.damage_buffer(0, 0, 1, 1)?; // Damage entire 1x1 buffer
@ -347,8 +346,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
let output_damage = connector_data.damage.borrow();
tassert!(!output_damage.is_empty());
// With viewporter scaling, the 1x1 buffer damage should scale to the viewport destination.
let surface_pos = window.surface.server.buffer_abs_pos.get();
// With viewporter scaling, the 1x1 buffer damage should scale to 150x100
// and be moved by surface position (0, 36) to get output coordinates (0, 36, 150, 136)
let expected_scaled_damage = Rect::new_sized(0, 0, 150, 100).unwrap();
let expected_output_damage =
expected_scaled_damage.move_(surface_pos.x1(), surface_pos.y1());
@ -403,9 +402,8 @@ async fn test(run: Rc<TestRun>) -> TestResult {
rotation_window.map().await?;
client.sync().await;
// Disable viewporter to rely purely on buffer dimensions.
rotation_window.surface.viewport.unset_source()?;
rotation_window.surface.viewport.unset_destination()?;
// Disable viewporter by setting destination to 0x0 to rely purely on buffer dimensions
rotation_window.surface.viewport.set_destination(0, 0)?; // Disable viewporter
// Use a rectangular buffer (4x2) so rotation has a visible geometric effect
// Attach AFTER mapping to avoid being overwritten by map()'s single-pixel buffer

View file

@ -315,6 +315,14 @@ impl PwClientNodeOwner for StartedScreencast {
}
}
}
log::debug!(
"Portal screencast using PipeWire dmabuf GPU copy path: {} buffers, format {}, modifier 0x{:08x}, size {}x{}",
self.buffers.borrow().len(),
self.format.get().name,
self.modifier.get(),
self.width.get(),
self.height.get(),
);
self.node
.send_port_output_buffers(&self.port, &self.buffers.borrow());
}
@ -633,15 +641,18 @@ impl UsrJayScreencastOwner for StartedScreencast {
fn ready(&self, ev: &Ready) {
let idx = ev.idx as usize;
let buffers = &*self.buffers.borrow();
let pbuffers = self.port.buffers.borrow();
let buffer = &buffers[idx];
let discard_buffer = || {
self.jay_screencast.release_buffer(idx);
};
if !self.buffers_valid.get() {
return;
}
let buffers = self.buffers.borrow();
let Some(buffer) = buffers.get(idx) else {
log::warn!("Ignoring ready event for unknown screencast buffer {idx}");
return;
};
let pbuffers = self.port.buffers.borrow();
let Some(io) = self.port.io_buffers.get() else {
discard_buffer();
return;
@ -767,7 +778,7 @@ pub(super) fn add_screencast_dbus_members(
object.add_method::<Start, _>(move |req, pr| {
dbus_start(&state, req, pr);
});
object.set_property::<AvailableSourceTypes>(Variant::U32(MONITOR.0));
object.set_property::<AvailableSourceTypes>(Variant::U32((MONITOR | WINDOW).0));
object.set_property::<AvailableCursorModes>(Variant::U32(EMBEDDED.0));
object.set_property::<version>(Variant::U32(5));
}

View file

@ -32,7 +32,6 @@ use {
numcell::NumCell,
on_drop_event::OnDropEvent,
rc_eq::rc_eq,
scroller::Scroller,
threshold_counter::ThresholdCounter,
},
},
@ -151,7 +150,6 @@ pub struct ContainerNode {
pub child_removed: Rc<LazyEventSource>,
pub all_children_resized: Rc<LazyEventSource>,
pub tab_bar: RefCell<Option<TabBar>>,
scroll: Scroller,
pub update_tab_textures_scheduled: Cell<bool>,
pub ephemeral: Cell<Ephemeral>,
}
@ -268,7 +266,6 @@ impl ContainerNode {
child_removed: state.lazy_event_sources.create_source(),
all_children_resized: state.post_layout_event_sources.create_source(),
tab_bar: RefCell::new(None),
scroll: Default::default(),
update_tab_textures_scheduled: Cell::new(false),
ephemeral: Cell::new(Ephemeral::Off),
});
@ -796,18 +793,6 @@ impl ContainerNode {
self.activate_child2(child, false);
}
fn activate_child_from_input(
self: &Rc<Self>,
child: &NodeRef<ContainerChild>,
seat: &Rc<WlSeatGlobal>,
) {
self.activate_child(child);
child
.node
.clone()
.node_do_focus(seat, Direction::Unspecified);
}
fn activate_child2(self: &Rc<Self>, child: &NodeRef<ContainerChild>, preserve_focus: bool) {
if let Some(mc) = self.mono_child.get() {
if mc.node.node_id() == child.node.node_id() {
@ -1534,7 +1519,7 @@ impl ContainerNode {
fn button(
self: Rc<Self>,
id: CursorType,
seat: &Rc<WlSeatGlobal>,
_seat: &Rc<WlSeatGlobal>,
_time_usec: u64,
pressed: bool,
button: u32,
@ -1564,7 +1549,7 @@ impl ContainerNode {
if let Some(child) = children.get(&child_id) {
let child_ref = child.to_ref();
drop(children);
self.activate_child_from_input(&child_ref, seat);
self.activate_child(&child_ref);
}
return;
}
@ -2081,33 +2066,31 @@ impl Node for ContainerNode {
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
}
fn node_on_axis_event(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
if self.mono_child.is_none() {
return;
}
let steps = match self.scroll.handle(event) {
Some(steps) => steps,
// Use vertical scroll (index 1) to switch tabs.
let v = match event.v120[1].get() {
Some(v) if v != 0 => v,
_ => return,
};
let mut target = match self.mono_child.get() {
let mono = match self.mono_child.get() {
Some(m) => m,
None => return,
};
let current_id = target.node.node_id();
for _ in 0..steps.abs() {
let next = if steps > 0 {
target.next().or_else(|| self.children.first())
} else {
target.prev().or_else(|| self.children.last())
};
match next {
Some(next) => target = next,
None => break,
let next = if v > 0 {
// Scroll down → next tab.
mono.next().or_else(|| self.children.first())
} else {
// Scroll up → previous tab.
mono.prev().or_else(|| self.children.last())
};
if let Some(next) = next {
if next.node.node_id() != mono.node.node_id() {
self.activate_child(&next);
}
}
if target.node.node_id() != current_id {
self.activate_child_from_input(&target, seat);
}
}
fn node_on_leave(&self, seat: &WlSeatGlobal) {

View file

@ -8,25 +8,18 @@ use {
renderer::Renderer,
state::State,
tree::{
Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink,
NodeLocation, OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation,
OutputNode, StackedNode, TileDragDestination, WorkspaceDragDestination,
WorkspaceNodeId, walker::NodeVisitor,
},
utils::{copyhashmap::CopyHashMap, linkedlist::LinkedList},
},
std::{
cell::{Cell, RefCell},
mem,
ops::Deref,
rc::{Rc, Weak},
},
std::{cell::Cell, ops::Deref, rc::Rc},
};
pub struct DisplayNode {
pub id: NodeId,
pub extents: Cell<Rect>,
visible: Cell<bool>,
suspend_restore_kb_foci: RefCell<Vec<(Rc<WlSeatGlobal>, Weak<dyn Node>)>>,
pub outputs: CopyHashMap<ConnectorId, Rc<OutputNode>>,
pub stacked: Rc<LinkedList<Rc<dyn StackedNode>>>,
pub stacked_above_layers: Rc<LinkedList<Rc<dyn StackedNode>>>,
@ -38,8 +31,6 @@ impl DisplayNode {
let slf = Self {
id,
extents: Default::default(),
visible: Default::default(),
suspend_restore_kb_foci: Default::default(),
outputs: Default::default(),
stacked: Default::default(),
stacked_above_layers: Default::default(),
@ -80,17 +71,6 @@ impl DisplayNode {
pub fn update_visible(&self, state: &State) {
let visible = state.root_visible();
let was_visible = self.visible.replace(visible);
if !visible && was_visible {
let mut foci = self.suspend_restore_kb_foci.borrow_mut();
foci.clear();
for seat in state.globals.seats.lock().values() {
let node = seat.get_keyboard_node();
if node.node_id() != self.id {
foci.push((seat.clone(), Rc::downgrade(&node)));
}
}
}
for output in self.outputs.lock().values() {
output.update_visible();
}
@ -102,20 +82,6 @@ impl DisplayNode {
for seat in state.globals.seats.lock().values() {
seat.set_visible(visible);
}
if visible && !was_visible {
for (seat, node) in mem::take(&mut *self.suspend_restore_kb_foci.borrow_mut()) {
if seat.get_keyboard_node().node_id() == self.id {
if let Some(node) = node.upgrade()
&& node.node_visible()
{
seat.focus_node(node);
} else {
seat.get_fallback_output()
.take_keyboard_navigation_focus(&seat, Direction::Unspecified);
}
}
}
}
if visible {
state.damage(self.extents.get());
}

View file

@ -460,9 +460,15 @@ impl OutputNode {
}
self.lock_surface.take();
self.jay_outputs.clear();
self.screencasts.clear();
self.screencopies.clear();
self.ext_copy_sessions.clear();
for screencast in self.screencasts.lock().drain_values() {
screencast.do_destroy();
}
for screencopy in self.screencopies.lock().drain_values() {
screencopy.cancel();
}
for session in self.ext_copy_sessions.lock().drain_values() {
session.stop();
}
self.ext_workspace_groups.clear();
self.latch_event.clear();
self.vblank_event.clear();