Add workspace screen sharing (#64)
Add workspace selection to the screen-sharing picker with pinned capture, privacy filtering and bounded capture resources. Refresh captured tab labels when title or app-ID metadata changes.
This commit is contained in:
1 parent
e64ab52190
commit
ab11269246
32 files changed
+2125
-165
No files matched your search
+42
-4
@@ -704,6 +704,16 @@ async fn start_streams(
|
||||
sources.extend(crate::desktop::sharing_window_sources(&windows)?);
|
||||
}
|
||||
let outputs = capture_ipc(&cancel, &work, "get-outputs", serde_json::json!({})).await?;
|
||||
let workspaces = if source_types & 1 != 0 && !bar_controlled && discovery.supports_workspaces()
|
||||
{
|
||||
let workspaces =
|
||||
capture_ipc(&cancel, &work, "get-workspaces", serde_json::json!({})).await?;
|
||||
sources.extend(crate::workspace::sources(&workspaces, &outputs)?);
|
||||
Some(workspaces)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
sources.sort_by_key(|source| source.kind().category());
|
||||
let rememberable = sources
|
||||
.iter()
|
||||
.filter(|source| {
|
||||
@@ -769,7 +779,7 @@ async fn start_streams(
|
||||
.await?;
|
||||
|
||||
let region = if selection.region {
|
||||
if selected.len() != 1 || selected[0].window_id().is_some() {
|
||||
if selected.len() != 1 || !selected[0].is_monitor() {
|
||||
return Err("Region recording requires one display".into());
|
||||
}
|
||||
let mut command = child_command_for("slurp")?;
|
||||
@@ -808,7 +818,7 @@ async fn start_streams(
|
||||
|
||||
let monitors = selected
|
||||
.iter()
|
||||
.filter(|source| source.window_id().is_none())
|
||||
.filter(|source| source.is_monitor())
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if !crate::restore::selection_unchanged(&monitors, &outputs, &fresh_outputs) {
|
||||
@@ -838,7 +848,13 @@ async fn start_streams(
|
||||
|
||||
for source in &selected {
|
||||
let mut command = child_command()?;
|
||||
if let Some(id) = source.window_id() {
|
||||
if let Some(id) = source.workspace_id() {
|
||||
command.args([
|
||||
"--stream-workspace",
|
||||
&id.to_string(),
|
||||
if cursor { "embedded" } else { "hidden" },
|
||||
]);
|
||||
} else if let Some(id) = source.window_id() {
|
||||
command.args([
|
||||
"--stream-window",
|
||||
&id.to_string(),
|
||||
@@ -904,6 +920,18 @@ async fn start_streams(
|
||||
return Err("A selected stream ended during startup".into());
|
||||
}
|
||||
|
||||
if let Some(before) = &workspaces {
|
||||
let after = capture_ipc(&cancel, &work, "get-workspaces", serde_json::json!({})).await?;
|
||||
if !crate::workspace::selection_unchanged(
|
||||
&selected,
|
||||
before,
|
||||
&outputs,
|
||||
&after,
|
||||
&final_outputs,
|
||||
) {
|
||||
return Err("A selected workspace changed during approval or startup".into());
|
||||
}
|
||||
}
|
||||
let final_windows = capture_ipc(&cancel, &work, "get-windows", serde_json::json!({})).await?;
|
||||
let recording = if bar_controlled {
|
||||
let crop = region
|
||||
@@ -941,7 +969,17 @@ async fn start_streams(
|
||||
let streams = streams
|
||||
.into_iter()
|
||||
.map(|(name, ready)| {
|
||||
let metadata = if name.starts_with("window:") {
|
||||
let metadata = if let crate::capture::SourceKind::Workspace(_) =
|
||||
crate::capture::SourceKind::parse(&name)
|
||||
{
|
||||
let source = selected
|
||||
.iter()
|
||||
.find(|source| source.name == name)
|
||||
.ok_or("Unknown workspace stream")?;
|
||||
crate::workspace::metadata(source, &ready)
|
||||
} else if let crate::capture::SourceKind::Window(_) =
|
||||
crate::capture::SourceKind::parse(&name)
|
||||
{
|
||||
let source = selected
|
||||
.iter()
|
||||
.find(|source| source.name == name)
|
||||
|
||||
+63
-3
@@ -6,7 +6,7 @@ use std::time::{Duration, Instant};
|
||||
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wayland_client::protocol::{wl_output, wl_registry, wl_shm};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum};
|
||||
use wayland_protocols_wlr::screencopy::v1::client::zwlr_screencopy_manager_v1 as manager;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -20,9 +20,62 @@ pub struct Source {
|
||||
pub scale: i32,
|
||||
}
|
||||
|
||||
/// Decode opaque worker names in one place. Unknown/malformed private names
|
||||
/// must never fall through to monitor capture or remembered display consent.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SourceKind {
|
||||
Monitor,
|
||||
Window(u64),
|
||||
Workspace(u64),
|
||||
Invalid,
|
||||
}
|
||||
impl SourceKind {
|
||||
pub fn parse(name: &str) -> Self {
|
||||
if let Some(id) = name.strip_prefix("window:") {
|
||||
id.parse().map(Self::Window).unwrap_or(Self::Invalid)
|
||||
} else if let Some(id) = name.strip_prefix("workspace:") {
|
||||
id.parse().map(Self::Workspace).unwrap_or(Self::Invalid)
|
||||
} else {
|
||||
Self::Monitor
|
||||
}
|
||||
}
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Monitor => "Displays",
|
||||
Self::Window(_) => "Windows",
|
||||
Self::Workspace(_) => "Workspaces",
|
||||
Self::Invalid => "Unavailable",
|
||||
}
|
||||
}
|
||||
pub fn category(self) -> u8 {
|
||||
match self {
|
||||
Self::Monitor => 0,
|
||||
Self::Workspace(_) => 1,
|
||||
Self::Window(_) => 2,
|
||||
Self::Invalid => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Source {
|
||||
pub fn kind(&self) -> SourceKind {
|
||||
SourceKind::parse(&self.name)
|
||||
}
|
||||
pub fn is_monitor(&self) -> bool {
|
||||
self.kind() == SourceKind::Monitor
|
||||
}
|
||||
pub fn window_id(&self) -> Option<u64> {
|
||||
self.name.strip_prefix("window:")?.parse().ok()
|
||||
if let SourceKind::Window(id) = self.kind() {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
pub fn workspace_id(&self) -> Option<u64> {
|
||||
if let SourceKind::Workspace(id) = self.kind() {
|
||||
Some(id)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +138,13 @@ impl Capture {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn supports_workspaces(&self) -> bool {
|
||||
self.state
|
||||
.window_manager
|
||||
.as_ref()
|
||||
.is_some_and(|manager| manager.version() >= 3)
|
||||
}
|
||||
|
||||
pub fn supports_windows(&self) -> bool {
|
||||
self.state.window_manager.is_some()
|
||||
}
|
||||
@@ -195,7 +255,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
},
|
||||
}),
|
||||
"ferese_window_capture_manager_v1" => {
|
||||
state.window_manager = Some(registry.bind(name, 1, qh, ()))
|
||||
state.window_manager = Some(registry.bind(name, version.min(3), qh, ()))
|
||||
}
|
||||
"wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
|
||||
"zwlr_screencopy_manager_v1" => {
|
||||
|
||||
@@ -16,6 +16,7 @@ mod restore;
|
||||
mod settings;
|
||||
mod shortcuts;
|
||||
mod stream;
|
||||
mod workspace;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
@@ -24,6 +25,10 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
[flag, name, cursor] if flag == "--stream" => {
|
||||
stream::run(name.clone(), cursor == "embedded", None)
|
||||
}
|
||||
[flag, id, cursor] if flag == "--stream-workspace" => {
|
||||
let id: u64 = id.parse()?;
|
||||
stream::run(format!("workspace:{id}"), cursor == "embedded", None)
|
||||
}
|
||||
[flag, id, cursor] if flag == "--stream-window" => {
|
||||
let id: u64 = id.parse()?;
|
||||
stream::run(format!("window:{id}"), cursor == "embedded", None)
|
||||
|
||||
+15
-18
@@ -34,10 +34,10 @@ impl Prompt {
|
||||
let mut previous_kind = None;
|
||||
if !self.window_capture {
|
||||
for source in visible {
|
||||
let is_window = source.window_id().is_some();
|
||||
if previous_kind != Some(is_window) {
|
||||
let kind = source.kind().category();
|
||||
if previous_kind != Some(kind) {
|
||||
sections += 1;
|
||||
previous_kind = Some(is_window);
|
||||
previous_kind = Some(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
let prompt: Prompt = serde_json::from_str(&input)?;
|
||||
|
||||
if prompt.sources.is_empty() || prompt.sources.len() > 144 {
|
||||
if prompt.sources.is_empty() || prompt.sources.len() > 272 {
|
||||
return Err("No shareable displays".into());
|
||||
}
|
||||
|
||||
@@ -328,25 +328,20 @@ impl cosmic::Application for Picker {
|
||||
let mut sources = column([]).spacing(6);
|
||||
let mut previous_kind = None;
|
||||
for (index, source) in self.prompt.sources.iter().enumerate() {
|
||||
let is_window = self.prompt.window_capture || source.window_id().is_some();
|
||||
if !self.prompt.window_capture && previous_kind != Some(is_window) {
|
||||
sources = sources.push(
|
||||
self.text(if is_window { "Windows" } else { "Displays" })
|
||||
.size(12),
|
||||
);
|
||||
previous_kind = Some(is_window);
|
||||
let kind = source.kind().category();
|
||||
if !self.prompt.window_capture && previous_kind != Some(kind) {
|
||||
sources = sources.push(self.text(source.kind().label()).size(12));
|
||||
previous_kind = Some(kind);
|
||||
}
|
||||
let selected = self.selected.contains(&index);
|
||||
let mut entry = row![
|
||||
glyph(ferese_theme::icons::DISPLAY, 24),
|
||||
column![
|
||||
self.text(
|
||||
if self.prompt.window_capture || source.window_id().is_some() {
|
||||
&source.label
|
||||
} else {
|
||||
&source.name
|
||||
}
|
||||
)
|
||||
self.text(if self.prompt.window_capture || !source.is_monitor() {
|
||||
&source.label
|
||||
} else {
|
||||
&source.name
|
||||
})
|
||||
.size(14),
|
||||
self.text(format!("{} × {}", source.width, source.height))
|
||||
.size(12)
|
||||
@@ -377,6 +372,8 @@ impl cosmic::Application for Picker {
|
||||
"Choose a display, then record it in full or select a region."
|
||||
} else if self.prompt.window_capture {
|
||||
"Only the selected window will be captured."
|
||||
} else if self.prompt.sources.iter().any(|source| source.workspace_id().is_some()) {
|
||||
"A workspace stays shared when you switch away. Desktop panels and notifications are excluded."
|
||||
} else {
|
||||
"Displays share everything visible. Windows share only their own content."
|
||||
})
|
||||
|
||||
@@ -148,6 +148,7 @@ impl Restore {
|
||||
|| app.len() > 512
|
||||
|| sources.is_empty()
|
||||
|| sources.len() > 16
|
||||
|| sources.iter().any(|source| !source.is_monitor())
|
||||
|| !(1..=2).contains(&mode)
|
||||
{
|
||||
return None;
|
||||
|
||||
+16
-1
@@ -88,6 +88,11 @@ impl Data {
|
||||
eprintln!("DMA-BUF capture unavailable ({error}); using shared memory");
|
||||
self.force_shm = true;
|
||||
self.renegotiate = true;
|
||||
// Retry unchanged content after a failed GPU transfer.
|
||||
if let Err(error) = self.capture.borrow_mut().refresh() {
|
||||
eprintln!("Could not refresh fallback capture: {error}");
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
}
|
||||
} else {
|
||||
eprintln!("{error}");
|
||||
self.stop.store(true, Ordering::Relaxed);
|
||||
@@ -325,6 +330,10 @@ pub fn run(
|
||||
.state_changed(move |stream, data, _, state| {
|
||||
let mut data = data.borrow_mut();
|
||||
let active = matches!(state, pw::stream::StreamState::Streaming);
|
||||
if let Err(error) = data.capture.borrow_mut().set_active(active) {
|
||||
eprintln!("Could not update capture activity: {error}");
|
||||
data.stop.store(true, Ordering::Relaxed);
|
||||
}
|
||||
if active
|
||||
&& !data.active
|
||||
&& let Err(error) = data.capture.borrow_mut().refresh()
|
||||
@@ -559,7 +568,13 @@ pub fn run(
|
||||
data.borrow_mut().complete(&stream, completion);
|
||||
}
|
||||
let changed = capture.borrow_mut().take_changed();
|
||||
if changed && !name.starts_with("window:") && capture.borrow().constraints().size != size {
|
||||
if changed
|
||||
&& !matches!(
|
||||
crate::capture::SourceKind::parse(&name),
|
||||
crate::capture::SourceKind::Window(_)
|
||||
)
|
||||
&& capture.borrow().constraints().size != size
|
||||
{
|
||||
eprintln!("Shared monitor changed size; start sharing again");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
|
||||
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum};
|
||||
use wayland_protocols::ext::image_capture_source::v1::client::{
|
||||
ext_image_capture_source_v1 as source,
|
||||
ext_output_image_capture_source_manager_v1 as output_manager,
|
||||
@@ -94,6 +94,7 @@ pub(super) struct Capture {
|
||||
frame: Option<frame::ExtImageCopyCaptureFrameV1>,
|
||||
pub device: Option<gbm::Device<File>>,
|
||||
device_id: Option<u64>,
|
||||
workspace: bool,
|
||||
}
|
||||
|
||||
impl Capture {
|
||||
@@ -121,30 +122,37 @@ impl Capture {
|
||||
dispatch(&connection, &mut queue, &mut state, 20)?;
|
||||
}
|
||||
}
|
||||
let source = if let Some(id) = name
|
||||
.strip_prefix("window:")
|
||||
.and_then(|value| value.parse::<u64>().ok())
|
||||
{
|
||||
state
|
||||
let source = match crate::capture::SourceKind::parse(name) {
|
||||
crate::capture::SourceKind::Window(id) => state
|
||||
.windows
|
||||
.as_ref()
|
||||
.ok_or("Persistent window capture is unavailable")?
|
||||
.create_source((id >> 32) as u32, id as u32, &qh, ())
|
||||
} else {
|
||||
let (&id, (output, _)) = state
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|(_, (_, output_name))| output_name == name)
|
||||
.ok_or("Shared display disconnected")?;
|
||||
if generation.is_some_and(|generation| generation != id) {
|
||||
return Err("Shared display was replaced".into());
|
||||
.create_source((id >> 32) as u32, id as u32, &qh, ()),
|
||||
crate::capture::SourceKind::Workspace(id) => {
|
||||
let manager = state
|
||||
.windows
|
||||
.as_ref()
|
||||
.filter(|manager| manager.version() >= 3)
|
||||
.ok_or("Workspace capture is unavailable")?;
|
||||
manager.create_workspace_source((id >> 32) as u32, id as u32, &qh, ())
|
||||
}
|
||||
crate::capture::SourceKind::Invalid => return Err("Invalid capture source".into()),
|
||||
crate::capture::SourceKind::Monitor => {
|
||||
let (&id, (output, _)) = state
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|(_, (_, output_name))| output_name == name)
|
||||
.ok_or("Shared display disconnected")?;
|
||||
if generation.is_some_and(|generation| generation != id) {
|
||||
return Err("Shared display was replaced".into());
|
||||
}
|
||||
state.selected = Some(id);
|
||||
state
|
||||
.output_manager
|
||||
.as_ref()
|
||||
.ok_or("Persistent display capture is unavailable")?
|
||||
.create_source(output, &qh, ())
|
||||
}
|
||||
state.selected = Some(id);
|
||||
state
|
||||
.output_manager
|
||||
.as_ref()
|
||||
.ok_or("Persistent display capture is unavailable")?
|
||||
.create_source(output, &qh, ())
|
||||
};
|
||||
let options = if cursor {
|
||||
manager::Options::PaintCursors
|
||||
@@ -184,6 +192,10 @@ impl Capture {
|
||||
frame: None,
|
||||
device,
|
||||
device_id,
|
||||
workspace: matches!(
|
||||
crate::capture::SourceKind::parse(name),
|
||||
crate::capture::SourceKind::Workspace(_)
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,6 +243,18 @@ impl Capture {
|
||||
}
|
||||
Some(completion)
|
||||
}
|
||||
pub(super) fn set_active(&mut self, active: bool) -> Result<(), String> {
|
||||
if self.workspace {
|
||||
self.state
|
||||
.windows
|
||||
.as_ref()
|
||||
.ok_or("Workspace capture manager disappeared")?
|
||||
.set_source_active(&self.source, u32::from(active));
|
||||
self.connection.flush().map_err(|error| error.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn refresh(&mut self) -> Result<(), String> {
|
||||
self.state
|
||||
.windows
|
||||
@@ -271,6 +295,14 @@ impl Capture {
|
||||
}
|
||||
let fd = bo.fd().map_err(|error| error.to_string())?;
|
||||
let stride = bo.stride_for_plane(0);
|
||||
let max_stride = width
|
||||
.checked_mul(4)
|
||||
.and_then(|row| row.checked_add(4095))
|
||||
.map(|row| row & !4095)
|
||||
.ok_or("DMA-BUF row is too large")?;
|
||||
if stride > max_stride {
|
||||
return Err("DMA-BUF stride exceeds the capture memory reservation".into());
|
||||
}
|
||||
let offset = bo.offset(0);
|
||||
if offset != 0 {
|
||||
return Err("Unsupported DMA-BUF plane offset".into());
|
||||
@@ -453,7 +485,7 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
state.output_manager = Some(registry.bind(name, 1, qh, ()))
|
||||
}
|
||||
"ferese_window_capture_manager_v1" if version >= 2 => {
|
||||
state.windows = Some(registry.bind(name, 2, qh, ()))
|
||||
state.windows = Some(registry.bind(name, version.min(3), qh, ()))
|
||||
}
|
||||
"wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
|
||||
"zwp_linux_dmabuf_v1" if version >= 3 => {
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
//! Portal compatibility: a workspace is an explicitly chosen, restricted view
|
||||
//! of an existing monitor. Offer it only when MONITOR was requested; never
|
||||
//! advertise an invented WORKSPACE bit or VIRTUAL (which creates a new monitor).
|
||||
use crate::backend::Options;
|
||||
use crate::capture::Source;
|
||||
use crate::stream::Ready;
|
||||
use serde_json::Value as Json;
|
||||
use zbus::zvariant::Value;
|
||||
|
||||
pub(crate) fn sources(workspaces: &Json, outputs: &Json) -> Result<Vec<Source>, String> {
|
||||
let workspaces = workspaces.as_array().ok_or("Invalid workspace list")?;
|
||||
let outputs = outputs.as_array().ok_or("Invalid output list")?;
|
||||
Ok(workspaces
|
||||
.iter()
|
||||
.take(128)
|
||||
.filter_map(|workspace| {
|
||||
let id = workspace["id"].as_u64()?;
|
||||
let owner = workspace["output"].as_u64()?;
|
||||
let output = outputs
|
||||
.iter()
|
||||
.find(|output| output["id"].as_u64() == Some(owner) && output["enabled"] == true)?;
|
||||
let integer = |key: &str| {
|
||||
output[key]
|
||||
.as_i64()
|
||||
.and_then(|value| i32::try_from(value).ok())
|
||||
};
|
||||
let (width, height) = (integer("width")?, integer("height")?);
|
||||
if width <= 0 || height <= 0 {
|
||||
return None;
|
||||
}
|
||||
Some(Source {
|
||||
name: format!("workspace:{id}"),
|
||||
label: format!(
|
||||
"Workspace {} · {}",
|
||||
workspace["index"].as_u64()?,
|
||||
output["name"].as_str()?
|
||||
),
|
||||
width,
|
||||
height,
|
||||
x: integer("x")?,
|
||||
y: integer("y")?,
|
||||
scale: 1,
|
||||
})
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub(crate) fn selection_unchanged(
|
||||
selected: &[Source],
|
||||
before: &Json,
|
||||
old_outputs: &Json,
|
||||
after: &Json,
|
||||
new_outputs: &Json,
|
||||
) -> bool {
|
||||
selected.iter().filter_map(Source::workspace_id).all(|id| {
|
||||
let owner = |workspaces: &Json| {
|
||||
workspaces
|
||||
.as_array()?
|
||||
.iter()
|
||||
.find(|workspace| workspace["id"].as_u64() == Some(id))?["output"]
|
||||
.as_u64()
|
||||
};
|
||||
let Some(old_owner) = owner(before) else {
|
||||
return false;
|
||||
};
|
||||
if owner(after) != Some(old_owner) {
|
||||
return false;
|
||||
}
|
||||
let output = |outputs: &Json| {
|
||||
outputs
|
||||
.as_array()?
|
||||
.iter()
|
||||
.find(|output| output["id"].as_u64() == Some(old_owner))
|
||||
.cloned()
|
||||
};
|
||||
let (Some(old), Some(new)) = (output(old_outputs), output(new_outputs)) else {
|
||||
return false;
|
||||
};
|
||||
new["enabled"] == true
|
||||
&& [
|
||||
"identity",
|
||||
"name",
|
||||
"width",
|
||||
"height",
|
||||
"x",
|
||||
"y",
|
||||
"scale",
|
||||
"transform",
|
||||
"current_mode",
|
||||
]
|
||||
.into_iter()
|
||||
.all(|key| old[key] == new[key])
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
|
||||
if source.workspace_id().is_none()
|
||||
|| ready.width == 0
|
||||
|| ready.height == 0
|
||||
|| ready.logical_size != Some((source.width as u32, source.height as u32))
|
||||
{
|
||||
return Err("Invalid workspace stream geometry".into());
|
||||
}
|
||||
Ok(Options::from([
|
||||
("source_type".into(), 1u32.into()),
|
||||
(
|
||||
"size".into(),
|
||||
Value::from((source.width, source.height))
|
||||
.try_to_owned()
|
||||
.map_err(|error| error.to_string())?,
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::capture::SourceKind;
|
||||
use serde_json::json;
|
||||
|
||||
fn outputs() -> Json {
|
||||
json!([{"id": 5, "name": "DP-1", "enabled": true, "width": 1920, "height": 1080,
|
||||
"x": 0, "y": 0, "scale": 2.0, "identity": "drm-edid:test", "transform": "normal",
|
||||
"current_mode": {"width": 3840, "height": 2160}}])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_selection_uses_identity_not_index_and_never_remembers_consent() {
|
||||
let outputs = outputs();
|
||||
let before = json!([{"id": 42, "output": 5, "index": 1}]);
|
||||
let selected = sources(&before, &outputs).unwrap();
|
||||
assert_eq!(selected.len(), 1);
|
||||
assert_eq!(selected[0].kind(), SourceKind::Workspace(42));
|
||||
assert_eq!(selected[0].label, "Workspace 1 · DP-1");
|
||||
let renamed = json!([{"id": 42, "output": 5, "index": 3}]);
|
||||
assert!(selection_unchanged(
|
||||
&selected, &before, &outputs, &renamed, &outputs
|
||||
));
|
||||
let replaced = json!([{"id": 43, "output": 5, "index": 1}]);
|
||||
assert!(!selection_unchanged(
|
||||
&selected, &before, &outputs, &replaced, &outputs
|
||||
));
|
||||
let moved = json!([{"id": 42, "output": 6, "index": 1}]);
|
||||
assert!(!selection_unchanged(
|
||||
&selected, &before, &outputs, &moved, &outputs
|
||||
));
|
||||
let mut resized = outputs.clone();
|
||||
resized[0]["current_mode"]["width"] = 2560.into();
|
||||
assert!(!selection_unchanged(
|
||||
&selected, &before, &outputs, &before, &resized
|
||||
));
|
||||
assert!(
|
||||
crate::restore::Restore::create("org.test.App", false, &selected, &outputs, 2)
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_metadata_keeps_logical_geometry_for_a_smaller_pixel_canvas() {
|
||||
let selected = sources(&json!([{"id": 42, "output": 5, "index": 1}]), &outputs()).unwrap();
|
||||
let mut ready = Ready {
|
||||
node: 7,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
logical_size: Some((1920, 1080)),
|
||||
};
|
||||
let metadata = metadata(&selected[0], &ready).unwrap();
|
||||
assert_eq!(u32::try_from(&metadata["source_type"]).unwrap(), 1);
|
||||
assert!(!metadata.contains_key("position"));
|
||||
ready.logical_size = None;
|
||||
assert!(super::metadata(&selected[0], &ready).is_err());
|
||||
for name in ["window:", "workspace:no", "workspace:18446744073709551616"] {
|
||||
assert_eq!(SourceKind::parse(name), SourceKind::Invalid);
|
||||
}
|
||||
assert_eq!(SourceKind::parse("DP-1"), SourceKind::Monitor);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user