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:
Samuel Onoja authored and GitHub committed 2026-10-25 13:59:34 +01:00
1 parent e64ab52190
commit ab11269246
32 files changed
+2125 -165

No files matched your search

+14
View File
@@ -202,6 +202,20 @@ impl Ferese {
}
}
for workspace in self.image_capture.captured_workspaces() {
if let Some(output) = self
.output_workspaces
.output_for_workspace(workspace)
.and_then(|id| self.outputs_by_id.get(&id))
&& self.output_id(output).is_some_and(|id| {
self.output_workspaces.active_workspace(id) == Some(workspace)
})
&& (self.output_has_animations(output)
|| self.output_has_pending_visual_changes(output))
{
self.image_capture.workspace_geometry_changed(workspace);
}
}
let delta = delta.mul_f64(self.animation_speed);
if let Some(workspace) = self
.focus_swipe()
+121 -41
View File
@@ -17,6 +17,7 @@ struct Preview {
layout: WorkspaceLayout,
input: PreviewInput,
rectangles: HashMap<WindowId, Rect>,
tabs: Vec<ferese_layout::TabBar>,
}
#[derive(PartialEq)]
@@ -126,6 +127,10 @@ impl Ferese {
.reconcile_workspaces(&mut self.workspaces, visible)
{
self.viewports.remove(&workspace);
self.workspace_previews
.borrow_mut()
.entries
.remove(&workspace);
}
}
@@ -197,6 +202,7 @@ impl Ferese {
}
fn relayout_outputs(&mut self, mut redraw_outputs: Option<Vec<Output>>, finalize: bool) {
self.image_capture.invalidate_workspaces();
if self
.desktop_transition
.as_ref()
@@ -705,6 +711,9 @@ impl Ferese {
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
if let Some(id) = self.windows.ids().get(window) {
self.window_stack.raise(*id);
if let Some(workspace) = self.workspaces.workspace_for_window(*id) {
self.image_capture.workspace_geometry_changed(workspace);
}
}
self.space.raise_element(window, activate);
@@ -781,6 +790,14 @@ impl Ferese {
return;
}
if let Some(workspace) = self
.windows
.ids()
.get(&window)
.and_then(|id| self.workspaces.workspace_for_window(*id))
{
self.image_capture.workspace_geometry_changed(workspace);
}
self.space.map_element(window, location, false);
self.stacking_cache.remapped();
}
@@ -805,21 +822,50 @@ impl Ferese {
}
pub(crate) fn workspace_preview_rects(&self) -> HashMap<WindowId, Rect> {
let constraints = self.window_constraints();
self.workspace_preview_rects_for(None)
}
/// Capture resolves only its subscribed workspace; it never activates or
/// configures hidden clients. The preview cache also serves overview.
pub(crate) fn workspace_preview_rects_for(
&self,
selected: Option<WorkspaceId>,
) -> HashMap<WindowId, Rect> {
if let Some(id) = selected {
self.workspace_preview_views(
selected,
self.output_workspaces
.output_for_workspace(id)
.map(|output| (id, output))
.into_iter(),
)
} else {
self.workspace_preview_views(
None,
self.output_workspaces
.workspace_views_iter(&self.workspaces)
.map(|view| (view.id, view.output)),
)
}
}
fn workspace_preview_views(
&self,
selected: Option<WorkspaceId>,
views: impl Iterator<Item = (WorkspaceId, ferese_core::OutputId)>,
) -> HashMap<WindowId, Rect> {
let constraints = self.window_constraints_for(selected);
let mut cache = self.workspace_previews.borrow_mut();
let mut live = HashSet::new();
let mut previews = HashMap::new();
for view in self
.output_workspaces
.workspace_views_iter(&self.workspaces)
{
let Some(workspace) = self.workspaces.workspace(view.id) else {
for (workspace_id, owner) in views {
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
continue;
};
let Some(output) = self
.output_ids
.iter()
.find(|(_, id)| **id == view.output)
.find(|(_, id)| **id == owner)
.map(|(o, _)| o)
else {
continue;
@@ -827,7 +873,7 @@ impl Ferese {
let Some(bounds) = self.output_bounds_for(output) else {
continue;
};
live.insert(view.id);
live.insert(workspace_id);
let mut windows: Vec<_> = workspace
.layout
.window_ids()
@@ -859,21 +905,21 @@ impl Ferese {
};
if !cache
.entries
.get(&view.id)
.get(&workspace_id)
.is_some_and(|cached| cached.input == input && cached.layout == workspace.layout)
{
// Only clone and solve the changed workspace. Titles, urgency and
// other shell metadata never invalidate geometry.
let mut layout = workspace.layout.clone();
let mut rectangles = layout
let resolved = layout
.resolve_geometry_with_constraints(
input.bounds,
input.gaps,
&constraints,
input.focused,
)
.map(|resolved| resolved.geometry)
.unwrap_or_default();
let mut rectangles = resolved.geometry;
for window in &input.windows {
if let Some(rect) = window.floating {
rectangles.insert(
@@ -894,11 +940,12 @@ impl Ferese {
}
}
cache.entries.insert(
view.id,
workspace_id,
Preview {
layout: workspace.layout.clone(),
input,
rectangles,
tabs: resolved.tab_bars,
},
);
#[cfg(test)]
@@ -907,44 +954,77 @@ impl Ferese {
}
}
previews.extend(
cache.entries[&view.id]
cache.entries[&workspace_id]
.rectangles
.iter()
.map(|(&id, &rect)| (id, rect)),
);
}
cache.entries.retain(|id, _| live.contains(id));
cache
.entries
.retain(|id, _| selected.is_some_and(|selected| selected != *id) || live.contains(id));
previews
}
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
self.windows
.ids()
.iter()
.filter_map(|(window, id)| {
let toplevel = window.toplevel()?;
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current();
(state.min_size, state.max_size)
});
let (minimum, maximum) = self.effective_size_constraints(
window,
(minimum.w, minimum.h),
(maximum.w, maximum.h),
);
pub(super) fn workspace_preview_tabs(&self, id: WorkspaceId) -> Vec<ferese_layout::TabBar> {
self.workspace_previews
.borrow()
.entries
.get(&id)
.map_or_else(Vec::new, |preview| preview.tabs.clone())
}
Some((
*id,
SizeConstraints {
min_width: minimum.0.max(1) as f64,
min_height: minimum.1.max(1) as f64,
max_width: (maximum.0 > 0).then_some(maximum.0 as f64),
max_height: (maximum.1 > 0).then_some(maximum.1 as f64),
},
))
})
.collect()
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
self.window_constraints_for(None)
}
fn window_constraints_for(
&self,
workspace: Option<WorkspaceId>,
) -> HashMap<WindowId, SizeConstraints> {
if let Some(workspace) = workspace {
self.constraints_for_windows(self.workspaces.workspace(workspace).into_iter().flat_map(
|workspace| {
workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
},
))
} else {
self.constraints_for_windows(self.windows.ids().values().copied())
}
}
fn constraints_for_windows(
&self,
ids: impl Iterator<Item = WindowId>,
) -> HashMap<WindowId, SizeConstraints> {
ids.filter_map(|id| {
let window = self.windows.window(id)?;
let toplevel = window.toplevel()?;
let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| {
let mut cached = states.cached_state.get::<SurfaceCachedState>();
let state = cached.current();
(state.min_size, state.max_size)
});
let (minimum, maximum) = self.effective_size_constraints(
window,
(minimum.w, minimum.h),
(maximum.w, maximum.h),
);
Some((
id,
SizeConstraints {
min_width: minimum.0.max(1) as f64,
min_height: minimum.1.max(1) as f64,
max_width: (maximum.0 > 0).then_some(maximum.0 as f64),
max_height: (maximum.1 > 0).then_some(maximum.1 as f64),
},
))
})
.collect()
}
pub(super) fn arrange_layers(&self) {
+109
View File
@@ -7,6 +7,7 @@ use super::{
/// geometry, configure barriers, animation clocks, or another output's view.
pub(crate) struct FrameScene {
pub windows: HashMap<WindowId, WindowFrame>,
pub capture_tabs: Option<Vec<ferese_layout::TabBar>>,
pub overview: crate::overview::OverviewMotion,
// Plane selection and scheduling reuse the activity sampled with this scene.
pub animating: bool,
@@ -332,6 +333,113 @@ impl Ferese {
})
}
pub(crate) fn workspace_capture_visible(
&self,
workspace: ferese_core::WorkspaceId,
output: ferese_core::OutputId,
) -> bool {
self.output_workspaces.active_workspace(output) == Some(workspace)
&& !self.workspace_slides.contains_key(&output)
}
pub(crate) fn workspace_capture_stack(
&self,
id: WindowId,
geometry: WindowGeometry,
) -> (u8, usize) {
let floating = matches!(
self.workspaces.placement(id),
Some(ferese_core::WindowPlacement::Floating { .. })
);
let above = self
.floating_above_fullscreen
.get(&id)
.is_some_and(|parent| {
self.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(*parent))
});
(
crate::stacking::layer_priority(floating, false, geometry.is_fullscreen(), above),
self.window_stack.rank(id),
)
}
/// An independent workspace view. Global overview, output switching and
/// focus on another workspace do not enter this presentation.
pub(crate) fn sample_workspace_frame(
&self,
workspace: ferese_core::WorkspaceId,
output: &Output,
) -> FrameScene {
let rectangles = self.workspace_preview_rects_for(Some(workspace));
let visible = self.output_id(output).is_some_and(|output| {
self.output_workspaces.active_workspace(output) == Some(workspace)
});
let focused = self
.workspaces
.workspace(workspace)
.and_then(|workspace| workspace.last_focused);
let mut windows = HashMap::with_capacity(rectangles.len());
for (id, rect) in rectangles {
let Some(record) = self.windows.record(id) else {
continue;
};
let Some(committed) = self.windows.window(id).and_then(super::client_size) else {
continue;
};
let mut geometry = record
.geometry
.unwrap_or_else(|| WindowGeometry::new(rect, Some(committed)));
if !visible {
// A hidden window can have no display presentation yet. Resolve
// only this value; do not issue a client configure or map it.
geometry = WindowGeometry::new(rect, Some(committed));
let fullscreen = self
.workspaces
.workspace(workspace)
.is_some_and(|workspace| workspace.fullscreen == Some(id));
let mode = if fullscreen {
super::PresentationMode::Fullscreen
} else if record.maximized {
super::PresentationMode::Maximized
} else {
super::PresentationMode::Normal
};
let _ = geometry.set_presentation_mode(rect, mode, Duration::ZERO);
geometry.settle_presentation();
geometry.presentation_changed = false;
}
let emphasis = AnimatedValue::new(if focused == Some(id) { 1.0 } else { 0.0 });
let presentation = crate::presentation::WindowPresentation {
id,
bounds: geometry.visual,
opacity: AnimatedValue::new(1.0),
focus_alpha: 1.0,
dim: 0.0,
emphasis,
shadow: emphasis,
scale_content: false,
native_size: None,
};
windows.insert(
id,
WindowFrame {
geometry,
presentation,
},
);
}
FrameScene {
windows,
capture_tabs: Some(self.workspace_preview_tabs(workspace)),
overview: Default::default(),
animating: false,
delta: Duration::ZERO,
}
}
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
let animating = self.output_has_animations(output);
let delta = if self.animations_enabled && animating {
@@ -406,6 +514,7 @@ impl Ferese {
FrameScene {
windows,
capture_tabs: None,
overview,
animating,
delta,
+42 -6
View File
@@ -184,10 +184,33 @@ impl Ferese {
&self,
bar: &ferese_layout::TabBar,
content: Rect,
) -> Option<IndicatorGeometry> {
self.tab_indicator_geometry_sampled(bar, content, None)
}
pub(crate) fn tab_indicator_geometry_for_frame(
&self,
bar: &ferese_layout::TabBar,
content: Rect,
frame: &super::FrameScene,
) -> Option<IndicatorGeometry> {
self.tab_indicator_geometry_sampled(
bar,
content,
frame.capture_tabs.as_ref().map(|_| frame),
)
}
fn tab_indicator_geometry_sampled(
&self,
bar: &ferese_layout::TabBar,
content: Rect,
frame: Option<&super::FrameScene>,
) -> Option<IndicatorGeometry> {
let workspace = self.workspaces.workspace_for_window(bar.group.active)?;
let output_id = self.output_workspaces.output_for_workspace(workspace)?;
if self.output_workspaces.active_workspace(output_id) != Some(workspace) {
if frame.is_none() && self.output_workspaces.active_workspace(output_id) != Some(workspace)
{
return None;
}
let output = self.outputs_by_id.get(&output_id)?;
@@ -200,17 +223,30 @@ impl Ferese {
);
let mut boundary = bounds.x;
// Measure the current visible gap, including animated column positions.
for window in self.space.elements() {
let Some(id) = self.windows.ids().get(window).copied() else {
continue;
};
let captured = frame
.into_iter()
.flat_map(|frame| frame.windows.keys().copied());
let displayed = self
.space
.elements()
.filter(|_| frame.is_none())
.filter_map(|window| self.windows.ids().get(window).copied());
for id in captured.chain(displayed) {
if id == bar.group.active
|| self.workspaces.workspace_for_window(id) != Some(workspace)
|| !self.workspaces.workspace(workspace)?.layout.contains(id)
{
continue;
}
let Some(rect) = self.presented_window_rect(id) else {
let Some(rect) = frame.map_or_else(
|| self.presented_window_rect(id),
|frame| {
frame
.windows
.get(&id)
.map(|sample| sample.presentation.bounds.current)
},
) else {
continue;
};
if rect.x + rect.width <= content.x