Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,15 @@ features, alongside source, packaging, retained release-contract, and C# tool
checks when .NET is installed. CI runs the equivalent C# command and additionally checks dynamic
and static gtk4-layer-shell linkage and uploads its code-health report.

GTK widget coverage runs separately with `./tools/test-gtk-widgets.sh` (Weston and
`dbus-run-session` required). It creates a private headless display and requires
GTK initialization; an unavailable display fails this check. Successful widget
bodies print `EXECUTED` markers. Ordinary tests without a display report an
optional skip. Neither route proves layer-shell focus or screen capture behavior
on a user's compositor.
GTK widget coverage runs separately with `./tools/test-gtk-widgets.sh` (Weston,
`dbus-run-session`, Python 3, `pkg-config`, Mesa software OpenGL, and Wayland
protocol XML required). It creates a private headless display and requires GTK
initialization; an unavailable display fails this check. The native popup tests
use another private Weston with software OpenGL and a protocol proxy to hold one
popup frame callback while its shared clock paints, then require capture to finish
after the popup's fresh render is acknowledged. They cover a plain `GtkPopover`
and `GtkPopoverMenu`'s empty proof overlay and menu restoration. Successful bodies
print `EXECUTED` markers. Ordinary widget tests without a display report an
optional skip; when a display is available, their GTK assertions run. The native
popup tests require the dedicated GTK gate. Neither route proves layer-shell
focus or screen capture behavior on a user's compositor.
96 changes: 96 additions & 0 deletions src/toolbar_gtk/view/capture_proof.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;

use gtk4::gdk;
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;

#[derive(Clone, Copy)]
pub(super) struct ProofRender {
pub serial: u8,
pub frame_counter: i64,
}

pub(super) type ProofObservation = Rc<Cell<Option<ProofRender>>>;

mod imp {
use super::*;

#[derive(Default)]
pub struct CaptureProof {
pub(super) texture: RefCell<Option<gdk::MemoryTexture>>,
pub(super) picture: glib::WeakRef<gtk4::Picture>,
pub(super) observed: RefCell<Option<ProofObservation>>,
pub(super) serial: Cell<u8>,
}

#[glib::object_subclass]
impl ObjectSubclass for CaptureProof {
const NAME: &'static str = "WayscriberCaptureProof";
type Type = super::CaptureProof;
type Interfaces = (gdk::Paintable,);
}

impl ObjectImpl for CaptureProof {}

impl gdk::subclass::prelude::PaintableImpl for CaptureProof {
fn snapshot(&self, snapshot: &gdk::Snapshot, width: f64, height: f64) {
let texture = self.texture.borrow();
let Some(texture) = texture.as_ref() else {
return;
};

texture.snapshot(snapshot, width, height);

if let Some(picture) = self.picture.upgrade()
&& let Some(clock) = picture.frame_clock()
&& let Some(observed) = self.observed.borrow().as_ref()
{
let serial = self.serial.get();
let frame = clock.frame_counter();
observed.set(Some(ProofRender {
serial,
frame_counter: frame,
}));
}
}

fn flags(&self) -> gdk::PaintableFlags {
gdk::PaintableFlags::STATIC_CONTENTS | gdk::PaintableFlags::STATIC_SIZE
}

fn intrinsic_width(&self) -> i32 {
1
}

fn intrinsic_height(&self) -> i32 {
1
}

fn intrinsic_aspect_ratio(&self) -> f64 {
1.0
}
}
}

glib::wrapper! {
pub struct CaptureProof(ObjectSubclass<imp::CaptureProof>) @implements gdk::Paintable;
}

impl CaptureProof {
pub(super) fn new(
texture: &gdk::MemoryTexture,
picture: &gtk4::Picture,
observed: ProofObservation,
serial: u8,
) -> Self {
let proof: Self = glib::Object::new();
proof.imp().texture.replace(Some(texture.clone()));
proof.imp().picture.set(Some(picture));
proof.imp().observed.replace(Some(observed));
proof.imp().serial.set(serial);

proof
}
}
17 changes: 6 additions & 11 deletions src/toolbar_gtk/view/capture_suppression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ use std::time::{Duration, Instant};

use gtk4::prelude::*;

use super::{
CaptureProofTarget, CaptureSurfaceContent, after_next_surface_paint_counter,
widget_native_is_mapped,
};
use super::{CaptureProofTarget, CaptureSurfaceContent, widget_native_is_mapped};
use crate::toolbar_gtk::css::CAPTURE_TRANSPARENT_CLASS;
use native_popovers::NativePopoverCapture;

Expand Down Expand Up @@ -398,8 +395,6 @@ async fn wait_for_surface_presentation(
"GTK capture suppression generation {generation} found no frame clock for mapped {name}"
)
})?;
let rendered_counter = Rc::new(Cell::new(None));
let callback_counter = Rc::clone(&rendered_counter);

log::info!(
"capture.preflight id={generation} component=gtk surface={name} phase=proof-requested sequence={ordinal}/{target_count} elapsed_ms={}",
Expand All @@ -411,11 +406,10 @@ async fn wait_for_surface_presentation(
// without a new Wayland buffer or presentation event. Give this target a
// fresh but still alpha-zero texture before arming its dedicated frame.
target.refresh_transparent_proof();
after_next_surface_paint_counter(widget, move |counter| {
callback_counter.set(counter);
});
// A shared frame clock can paint the parent while the popup is frozen.
// Bind this proof to the fresh paintable's actual native snapshot.

while rendered_counter.get().is_none() {
while target.content.proof_frame().is_none() {
reject_withdrawn_target(generation, target, started)?;
if proof_started.elapsed() >= CAPTURE_PAINT_TIMEOUT || Instant::now() >= deadline {
return Err(format!(
Expand All @@ -425,7 +419,7 @@ async fn wait_for_surface_presentation(
gtk4::glib::timeout_future(CAPTURE_PAINT_POLL_INTERVAL).await;
}

let Some(frame_counter) = rendered_counter.get() else {
let Some(frame_counter) = target.content.proof_frame() else {
return Err(format!(
"GTK capture suppression generation {generation} lost the dedicated transparent render from {name}"
));
Expand Down Expand Up @@ -493,6 +487,7 @@ fn reject_withdrawn_target(

#[cfg(test)]
mod tests {
mod presentation;
mod process;

use super::*;
Expand Down
Loading
Loading