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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user