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
@@ -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