mirror of
https://github.com/YaLTeR/niri.git
synced 2025-10-06 00:23:14 +02:00
Implement dynamic screencast target
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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.
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
129
src/niri.rs
129
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) {
|
||||
|
@@ -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,
|
||||
|
@@ -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())
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user