Implement dynamic screencast target

This commit is contained in:
Ivan Molodetskikh
2025-03-15 11:23:01 +03:00
parent 392fc27de1
commit 31891e6642
6 changed files with 197 additions and 8 deletions

View File

@@ -1648,6 +1648,11 @@ pub enum Action {
ToggleWindowRuleOpacity,
#[knuffel(skip)]
ToggleWindowRuleOpacityById(u64),
SetDynamicCastWindow,
#[knuffel(skip)]
SetDynamicCastWindowById(u64),
SetDynamicCastMonitor(#[knuffel(argument)] Option<String>),
ClearDynamicCastTarget,
}
impl From<niri_ipc::Action> for Action {
@@ -1892,6 +1897,14 @@ impl From<niri_ipc::Action> for Action {
niri_ipc::Action::ToggleWindowRuleOpacity { id: Some(id) } => {
Self::ToggleWindowRuleOpacityById(id)
}
niri_ipc::Action::SetDynamicCastWindow { id: None } => Self::SetDynamicCastWindow,
niri_ipc::Action::SetDynamicCastWindow { id: Some(id) } => {
Self::SetDynamicCastWindowById(id)
}
niri_ipc::Action::SetDynamicCastMonitor { output } => {
Self::SetDynamicCastMonitor(output)
}
niri_ipc::Action::ClearDynamicCastTarget {} => Self::ClearDynamicCastTarget,
}
}
}

View File

@@ -704,6 +704,32 @@ pub enum Action {
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a window.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused window")
)]
SetDynamicCastWindow {
/// Id of the window to target.
///
/// If `None`, uses the focused window.
#[cfg_attr(feature = "clap", arg(long))]
id: Option<u64>,
},
/// Set the dynamic cast target to a monitor.
#[cfg_attr(
feature = "clap",
clap(about = "Set the dynamic cast target to the focused monitor")
)]
SetDynamicCastMonitor {
/// Name of the output to target.
///
/// If `None`, uses the focused output.
#[cfg_attr(feature = "clap", arg())]
output: Option<String>,
},
/// Clear the dynamic cast target, making it show nothing.
ClearDynamicCastTarget {},
}
/// Change in window or column size.

View File

@@ -41,7 +41,7 @@ use self::resize_grab::ResizeGrab;
use self::spatial_movement_grab::SpatialMovementGrab;
use crate::layout::scrolling::ScrollDirection;
use crate::layout::LayoutElement as _;
use crate::niri::State;
use crate::niri::{CastTarget, State};
use crate::ui::screenshot_ui::ScreenshotUi;
use crate::utils::spawning::spawn;
use crate::utils::{center, get_monotonic_time, ResizeEdge};
@@ -1786,6 +1786,36 @@ impl State {
}
}
}
Action::SetDynamicCastWindow => {
let id = self
.niri
.layout
.active_workspace()
.and_then(|ws| ws.active_window())
.map(|mapped| mapped.id().get());
if let Some(id) = id {
self.set_dynamic_cast_target(CastTarget::Window { id });
}
}
Action::SetDynamicCastWindowById(id) => {
let layout = &self.niri.layout;
if layout.windows().any(|(_, mapped)| mapped.id().get() == id) {
self.set_dynamic_cast_target(CastTarget::Window { id });
}
}
Action::SetDynamicCastMonitor(output) => {
let output = match output {
None => self.niri.layout.active_output(),
Some(name) => self.niri.output_by_name_match(&name),
};
if let Some(output) = output {
let output = output.downgrade();
self.set_dynamic_cast_target(CastTarget::Output(output));
}
}
Action::ClearDynamicCastTarget => {
self.set_dynamic_cast_target(CastTarget::Nothing);
}
}
}

View File

@@ -379,6 +379,10 @@ pub struct Niri {
// Screencast output for each mapped window.
#[cfg(feature = "xdp-gnome-screencast")]
pub mapped_cast_output: HashMap<Window, Output>,
/// Window ID for the "dynamic cast" special window for the xdp-gnome picker.
#[cfg(feature = "xdp-gnome-screencast")]
pub dynamic_cast_id_for_portal: MappedId,
}
#[derive(Debug)]
@@ -510,6 +514,8 @@ pub enum CenterCoords {
#[derive(Clone, PartialEq, Eq)]
pub enum CastTarget {
// Dynamic cast before selecting anything.
Nothing,
Output(WeakOutput),
Window { id: u64 },
}
@@ -1623,6 +1629,29 @@ impl State {
};
match &cast.target {
CastTarget::Nothing => {
// Matches what we create the dynamic source with.
let size = Size::from((1, 1));
let scale = Scale::from(1.);
match cast.ensure_size(size) {
Ok(CastSizeChange::Ready) => (),
Ok(CastSizeChange::Pending) => return,
Err(err) => {
warn!("error updating stream size, stopping screencast: {err:?}");
let session_id = cast.session_id;
self.niri.stop_cast(session_id);
return;
}
}
self.backend.with_primary_renderer(|renderer| {
let elements: &[MonitorRenderElement<_>] = &[];
if cast.dequeue_buffer_and_render(renderer, elements, size, scale) {
cast.last_frame_time = get_monotonic_time();
}
});
}
CastTarget::Output(weak) => {
if let Some(output) = weak.upgrade() {
self.niri.queue_redraw(&output);
@@ -1673,6 +1702,57 @@ impl State {
}
}
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub fn set_dynamic_cast_target(&mut self, _target: CastTarget) {}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn set_dynamic_cast_target(&mut self, target: CastTarget) {
let _span = tracy_client::span!("State::set_dynamic_cast_target");
let mut refresh = None;
match &target {
// Leave refresh as is when clearing. Chances are, the next refresh will match it,
// then we'll avoid reconfiguring.
CastTarget::Nothing => (),
CastTarget::Output(output) => {
if let Some(output) = output.upgrade() {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
CastTarget::Window { id } => {
let mut windows = self.niri.layout.windows();
if let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == *id) {
if let Some(output) = self.niri.mapped_cast_output.get(&mapped.window) {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
}
}
let mut to_redraw = Vec::new();
let mut to_stop = Vec::new();
for cast in &mut self.niri.casts {
if !cast.dynamic_target {
continue;
}
if let Some(refresh) = refresh {
if let Err(err) = cast.set_refresh(refresh) {
warn!("error changing cast FPS: {err:?}");
to_stop.push(cast.session_id);
continue;
}
}
cast.target = target.clone();
to_redraw.push(cast.stream_id);
}
for id in to_redraw {
self.redraw_cast(id);
}
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) {
use smithay::reexports::gbm::Modifier;
@@ -1712,6 +1792,7 @@ impl State {
}
};
let mut dynamic_target = false;
let (target, size, refresh, alpha) = match target {
StreamTargetId::Output { name } => {
let global_space = &self.niri.global_space;
@@ -1728,6 +1809,15 @@ impl State {
let refresh = mode.refresh as u32;
(CastTarget::Output(output.downgrade()), size, refresh, false)
}
StreamTargetId::Window { id }
if id == self.niri.dynamic_cast_id_for_portal.get() =>
{
dynamic_target = true;
// All dynamic casts start as Nothing to avoid surprises and exposing
// sensitive info.
(CastTarget::Nothing, Size::from((1, 1)), 1000, true)
}
StreamTargetId::Window { id } => {
let Some(window) = self.niri.layout.windows().find_map(|(_, mapped)| {
(mapped.id().get() == id).then_some(&mapped.window)
@@ -1776,6 +1866,7 @@ impl State {
session_id,
stream_id,
target,
dynamic_target,
size,
refresh,
alpha,
@@ -1851,6 +1942,15 @@ impl State {
let mut windows = HashMap::new();
#[cfg(feature = "xdp-gnome-screencast")]
windows.insert(
self.niri.dynamic_cast_id_for_portal.get(),
gnome_shell_introspect::WindowProperties {
title: String::from("niri Dynamic Cast Target"),
app_id: String::from("rs.bxt.niri"),
},
);
self.niri.layout.with_windows(|mapped, _, _| {
let id = mapped.id().get();
let props = with_toplevel_role(mapped.toplevel(), |role| {
@@ -2260,6 +2360,9 @@ impl Niri {
#[cfg(feature = "xdp-gnome-screencast")]
mapped_cast_output: HashMap::new(),
#[cfg(feature = "xdp-gnome-screencast")]
dynamic_cast_id_for_portal: MappedId::next(),
};
niri.reset_pointer_inactivity_timer();
@@ -4598,16 +4701,30 @@ impl Niri {
let _span = tracy_client::span!("Niri::stop_casts_for_target");
// This is O(N^2) but it shouldn't be a problem I think.
let ids: Vec<_> = self
.casts
.iter()
.filter(|cast| cast.target == target)
.map(|cast| cast.session_id)
.collect();
let mut saw_dynamic = false;
let mut ids = Vec::new();
for cast in &self.casts {
if cast.target != target {
continue;
}
if cast.dynamic_target {
saw_dynamic = true;
continue;
}
ids.push(cast.session_id);
}
for id in ids {
self.stop_cast(id);
}
// We don't stop dynamic casts, instead we switch them to Nothing.
if saw_dynamic {
self.event_loop
.insert_idle(|state| state.set_dynamic_cast_target(CastTarget::Nothing));
}
}
pub fn remove_screencopy_output(&mut self, output: &Output) {

View File

@@ -71,6 +71,7 @@ pub struct Cast {
_listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>,
pub target: CastTarget,
pub dynamic_target: bool,
formats: FormatSet,
state: Rc<RefCell<CastState>>,
refresh: Rc<Cell<u32>>,
@@ -186,6 +187,7 @@ impl PipeWire {
session_id: usize,
stream_id: usize,
target: CastTarget,
dynamic_target: bool,
size: Size<i32, Physical>,
refresh: u32,
alpha: bool,
@@ -651,6 +653,7 @@ impl PipeWire {
_listener: listener,
is_active,
target,
dynamic_target,
formats,
state,
refresh,

View File

@@ -136,7 +136,7 @@ static MAPPED_ID_COUNTER: IdCounter = IdCounter::new();
pub struct MappedId(u64);
impl MappedId {
fn next() -> MappedId {
pub fn next() -> MappedId {
MappedId(MAPPED_ID_COUNTER.next())
}