aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-03-15 11:23:01 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-03-15 09:55:46 -0700
commit31891e6642481c018357354583db804657c09c53 (patch)
treef52ffe3fe28ebee0a7b93112285ed3a09e4c90b6
parent392fc27de110d3548095e465d5cb38bd8d5730ea (diff)
downloadniri-31891e6642481c018357354583db804657c09c53.tar.gz
niri-31891e6642481c018357354583db804657c09c53.tar.bz2
niri-31891e6642481c018357354583db804657c09c53.zip
Implement dynamic screencast target
-rw-r--r--niri-config/src/lib.rs13
-rw-r--r--niri-ipc/src/lib.rs26
-rw-r--r--src/input/mod.rs32
-rw-r--r--src/niri.rs129
-rw-r--r--src/pw_utils.rs3
-rw-r--r--src/window/mapped.rs2
6 files changed, 197 insertions, 8 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs
index 5b557b5f..ddf1ef00 100644
--- a/niri-config/src/lib.rs
+++ b/niri-config/src/lib.rs
@@ -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,
}
}
}
diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs
index 870fce45..ea7c70ee 100644
--- a/niri-ipc/src/lib.rs
+++ b/niri-ipc/src/lib.rs
@@ -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.
diff --git a/src/input/mod.rs b/src/input/mod.rs
index 95d30457..08bebe40 100644
--- a/src/input/mod.rs
+++ b/src/input/mod.rs
@@ -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);
+ }
}
}
diff --git a/src/niri.rs b/src/niri.rs
index 3ff71d09..c30ea255 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -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) {
diff --git a/src/pw_utils.rs b/src/pw_utils.rs
index 555da333..f6ad4014 100644
--- a/src/pw_utils.rs
+++ b/src/pw_utils.rs
@@ -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,
diff --git a/src/window/mapped.rs b/src/window/mapped.rs
index ac508990..16dfc4d4 100644
--- a/src/window/mapped.rs
+++ b/src/window/mapped.rs
@@ -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())
}