Refactor window ownership and immutable frame sampling
This commit is contained in:
1 parent
c0010ccbb6
commit
07bda5a21c
31 files changed
+1222
-623
No files matched your search
@@ -37,7 +37,7 @@ use smithay::wayland::presentation::Refresh;
|
|||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||||
use crate::metrics::RenderMetrics;
|
use crate::metrics::RenderMetrics;
|
||||||
use crate::render::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
use crate::render::{frame_effect_metrics, redraw_output, sampled_output_elements};
|
||||||
|
|
||||||
pub struct DirectBackendState {
|
pub struct DirectBackendState {
|
||||||
pub session: LibSeatSession,
|
pub session: LibSeatSession,
|
||||||
@@ -793,13 +793,12 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
|
|||||||
.scheduler
|
.scheduler
|
||||||
.forecast_time(now, plan)
|
.forecast_time(now, plan)
|
||||||
.map_or(Duration::ZERO, |target| target.saturating_sub(now));
|
.map_or(Duration::ZERO, |target| target.saturating_sub(now));
|
||||||
let mut forecast = state.forecast_render(&output.output, horizon);
|
let frame = state.sample_frame(&output.output, horizon);
|
||||||
let rendered = {
|
let rendered = {
|
||||||
let state = &mut *forecast;
|
|
||||||
(|| -> Result<bool, Box<dyn Error>> {
|
(|| -> Result<bool, Box<dyn Error>> {
|
||||||
state.process_dmabuf_imports(&mut device.renderer);
|
state.process_dmabuf_imports(&mut device.renderer);
|
||||||
let (mut buffer, age) = output.surface.next_buffer()?;
|
let (mut buffer, age) = output.surface.next_buffer()?;
|
||||||
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
|
let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame);
|
||||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||||
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
||||||
let result = output.damage_tracker.render_output(
|
let result = output.damage_tracker.render_output(
|
||||||
@@ -814,7 +813,8 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
|
|||||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
||||||
|
|
||||||
if cursorless_capture {
|
if cursorless_capture {
|
||||||
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
|
let cursorless_elements =
|
||||||
|
sampled_output_elements(state, &mut device.renderer, &output.output, false, &frame);
|
||||||
redraw_output(
|
redraw_output(
|
||||||
&mut device.renderer,
|
&mut device.renderer,
|
||||||
&mut framebuffer,
|
&mut framebuffer,
|
||||||
@@ -863,7 +863,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
|
|||||||
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
|
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
|
||||||
|
|
||||||
if submitted && let Some(token) = output.callback_timer.take() {
|
if submitted && let Some(token) = output.callback_timer.take() {
|
||||||
forecast.loop_handle.remove(token);
|
state.loop_handle.remove(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
match rendered {
|
match rendered {
|
||||||
@@ -873,8 +873,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, plan: cr
|
|||||||
}
|
}
|
||||||
|
|
||||||
device.outputs.insert(crtc, output);
|
device.outputs.insert(crtc, output);
|
||||||
restore_device(&mut forecast, node, device);
|
restore_device(state, node, device);
|
||||||
drop(forecast);
|
|
||||||
if !submitted {
|
if !submitted {
|
||||||
schedule_frame_callbacks(state, node, crtc, plan.present_at);
|
schedule_frame_callbacks(state, node, crtc, plan.present_at);
|
||||||
}
|
}
|
||||||
@@ -1135,7 +1134,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
|
|
||||||
let context = device.renderer.context_id().erased();
|
let context = device.renderer.context_id().erased();
|
||||||
state.wallpaper.forget_context(&context);
|
state.wallpaper.forget_context(&context);
|
||||||
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
|
state.render.forget_context(&context);
|
||||||
for (crtc, mut output) in device.outputs {
|
for (crtc, mut output) in device.outputs {
|
||||||
cancel_output_timers(&state.loop_handle, &mut output);
|
cancel_output_timers(&state.loop_handle, &mut output);
|
||||||
state.display_handle.disable_global::<Ferese>(output.global);
|
state.display_handle.disable_global::<Ferese>(output.global);
|
||||||
@@ -1348,7 +1347,8 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
|||||||
.elements()
|
.elements()
|
||||||
.filter(|window| {
|
.filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -159,10 +159,9 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
||||||
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
|
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
|
||||||
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
||||||
if let Some(id) = data.window_ids.get(&self.window).copied() {
|
if let Some(id) = data.windows.ids().get(&self.window).copied() {
|
||||||
data.floating_resize_anchors.insert(
|
data.windows.update(id, |w| {
|
||||||
id,
|
w.resize_anchor = Some((
|
||||||
(
|
|
||||||
self.edges.left(),
|
self.edges.left(),
|
||||||
self.edges.top(),
|
self.edges.top(),
|
||||||
ferese_layout::Rect::new(
|
ferese_layout::Rect::new(
|
||||||
@@ -171,8 +170,8 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
initial.size.w as f64,
|
initial.size.w as f64,
|
||||||
initial.size.h as f64,
|
initial.size.h as f64,
|
||||||
),
|
),
|
||||||
),
|
))
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
surface.with_pending_state(|state| {
|
surface.with_pending_state(|state| {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ impl XdgActivationHandler for Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.focused_window = self.window_ids.get(&window).copied();
|
self.focused_window = self.windows.ids().get(&window).copied();
|
||||||
if let Some(focused) = self.focused_window
|
if let Some(focused) = self.focused_window
|
||||||
&& let Err(error) = self.workspaces.focus_window(focused)
|
&& let Err(error) = self.workspaces.focus_window(focused)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ impl CompositorHandler for Ferese {
|
|||||||
if surface == &root {
|
if surface == &root {
|
||||||
if crate::state::window_has_buffer(&window) {
|
if crate::state::window_has_buffer(&window) {
|
||||||
xdg_shell::apply_initial_window_rules(self, &window);
|
xdg_shell::apply_initial_window_rules(self, &window);
|
||||||
} else if self.window_ids.contains_key(&window) {
|
} else if self.windows.ids().contains_key(&window) {
|
||||||
self.remove_tiled_window(&window);
|
self.remove_tiled_window(&window);
|
||||||
self.space.map_element(window.clone(), (0, 0), false);
|
self.space.map_element(window.clone(), (0, 0), false);
|
||||||
self.restore_keyboard_focus();
|
self.restore_keyboard_focus();
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl SeatHandler for Ferese {
|
|||||||
|
|
||||||
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||||
if let Some(id) = focused.and_then(|surface| {
|
if let Some(id) = focused.and_then(|surface| {
|
||||||
self.window_ids.iter().find_map(|(window, id)| {
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
||||||
@@ -152,7 +152,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
root = parent;
|
root = parent;
|
||||||
}
|
}
|
||||||
let window = self.window_ids.iter().find_map(|(window, id)| {
|
let window = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||||
|
|||||||
@@ -186,7 +186,8 @@ impl Ferese {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let window = self
|
let window = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, candidate)| **candidate == id)?
|
.find(|(_, candidate)| **candidate == id)?
|
||||||
.0
|
.0
|
||||||
@@ -283,7 +284,7 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da
|
|||||||
};
|
};
|
||||||
if state.session_lock.active
|
if state.session_lock.active
|
||||||
|| !super::screencopy::capture_allowed()
|
|| !super::screencopy::capture_allowed()
|
||||||
|| !state.window_ids.values().any(|id| *id == snapshot.id)
|
|| !state.windows.ids().values().any(|id| *id == snapshot.id)
|
||||||
{
|
{
|
||||||
resource.failed();
|
resource.failed();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
let Some(parent) = surface.parent() else {
|
let Some(parent) = surface.parent() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let parent = self.window_ids.iter().find_map(|(window, id)| {
|
let parent = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
|
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
|
||||||
@@ -60,7 +60,8 @@ impl XdgShellHandler for Ferese {
|
|||||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||||
// Hidden workspace windows are managed but unmapped from Space.
|
// Hidden workspace windows are managed but unmapped from Space.
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.keys()
|
.keys()
|
||||||
.chain(self.space.elements())
|
.chain(self.space.elements())
|
||||||
.find(|window| {
|
.find(|window| {
|
||||||
@@ -185,7 +186,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -198,7 +199,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -215,7 +216,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
surface: ToplevelSurface,
|
surface: ToplevelSurface,
|
||||||
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
|
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
|
||||||
) {
|
) {
|
||||||
if let Some(window) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -228,7 +229,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(window) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -380,7 +381,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let initial = !state.window_ids.contains_key(window);
|
let initial = !state.windows.ids().contains_key(window);
|
||||||
let requested = initial.then(|| {
|
let requested = initial.then(|| {
|
||||||
toplevel.with_pending_state(|pending| {
|
toplevel.with_pending_state(|pending| {
|
||||||
(
|
(
|
||||||
@@ -391,7 +392,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
});
|
});
|
||||||
if initial {
|
if initial {
|
||||||
let parent = toplevel.parent().and_then(|parent| {
|
let parent = toplevel.parent().and_then(|parent| {
|
||||||
state.window_ids.iter().find_map(|(candidate, id)| {
|
state.windows.ids().iter().find_map(|(candidate, id)| {
|
||||||
candidate
|
candidate
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|surface| surface.wl_surface() == &parent)
|
.is_some_and(|surface| surface.wl_surface() == &parent)
|
||||||
@@ -402,7 +403,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
}
|
}
|
||||||
state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient);
|
state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient);
|
||||||
if let Some((maximized, fullscreen)) = requested
|
if let Some((maximized, fullscreen)) = requested
|
||||||
&& let Some(id) = state.window_ids.get(window).copied()
|
&& let Some(id) = state.windows.ids().get(window).copied()
|
||||||
{
|
{
|
||||||
if maximized {
|
if maximized {
|
||||||
state.set_window_maximized(id, true);
|
state.set_window_maximized(id, true);
|
||||||
@@ -424,7 +425,7 @@ impl Ferese {
|
|||||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
||||||
let geometry = (|| {
|
let geometry = (|| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
let id = *self.window_ids.get(window)?;
|
let id = *self.windows.ids().get(window)?;
|
||||||
let output = self
|
let output = self
|
||||||
.space
|
.space
|
||||||
.outputs()
|
.outputs()
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ impl Ferese {
|
|||||||
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let presented = self.presented_window_rect(*id)?;
|
let presented = self.presented_window_rect(*id)?;
|
||||||
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
||||||
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
||||||
@@ -541,7 +541,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(window) = self.window_under_visual(position)
|
if let Some(window) = self.window_under_visual(position)
|
||||||
&& let Some(id) = self.window_ids.get(&window).copied()
|
&& let Some(id) = self.windows.ids().get(&window).copied()
|
||||||
{
|
{
|
||||||
self.select_overview_window(id);
|
self.select_overview_window(id);
|
||||||
} else {
|
} else {
|
||||||
@@ -836,7 +836,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = self.window_under_visual(position) {
|
if let Some(window) = self.window_under_visual(position) {
|
||||||
let focused = self.window_ids.get(&window).copied();
|
let focused = self.windows.ids().get(&window).copied();
|
||||||
if let Some(focused) = focused {
|
if let Some(focused) = focused {
|
||||||
let result = if raise {
|
let result = if raise {
|
||||||
self.workspaces.focus_window(focused)
|
self.workspaces.focus_window(focused)
|
||||||
@@ -904,7 +904,7 @@ impl Ferese {
|
|||||||
let Some(window) = self.window_under_visual(position) else {
|
let Some(window) = self.window_under_visual(position) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if self.window_ids.get(&window).copied() == self.focused_window {
|
if self.windows.ids().get(&window).copied() == self.focused_window {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -827,7 +827,8 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, candidate)| **candidate == id)
|
.find(|(_, candidate)| **candidate == id)
|
||||||
.map(|(window, _)| window.clone())
|
.map(|(window, _)| window.clone())
|
||||||
@@ -936,7 +937,8 @@ impl Ferese {
|
|||||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||||
};
|
};
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.keys()
|
.keys()
|
||||||
.filter_map(|window| window.toplevel())
|
.filter_map(|window| window.toplevel())
|
||||||
.any(|toplevel| belongs_to(toplevel.wl_surface()))
|
.any(|toplevel| belongs_to(toplevel.wl_surface()))
|
||||||
@@ -951,7 +953,8 @@ impl Ferese {
|
|||||||
use smithay::wayland::compositor::with_states;
|
use smithay::wayland::compositor::with_states;
|
||||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||||
let mut windows = self
|
let mut windows = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let toplevel = window.toplevel()?;
|
let toplevel = window.toplevel()?;
|
||||||
|
|||||||
+188
-118
@@ -89,12 +89,9 @@ type OverviewStateLabel =
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct OverviewState {
|
pub(crate) struct OverviewState {
|
||||||
active: bool,
|
motion: OverviewMotion,
|
||||||
selected: Option<WindowId>,
|
selected: Option<WindowId>,
|
||||||
selection_workspace: Option<WorkspaceId>,
|
selection_workspace: Option<WorkspaceId>,
|
||||||
presentations: HashMap<WindowId, AnimatedRect>,
|
|
||||||
opacity: AnimatedValue,
|
|
||||||
exit_transition: Option<(f64, f64)>,
|
|
||||||
strip_offsets: HashMap<OutputId, usize>,
|
strip_offsets: HashMap<OutputId, usize>,
|
||||||
font_family: String,
|
font_family: String,
|
||||||
font_requests: Option<FontRequests>,
|
font_requests: Option<FontRequests>,
|
||||||
@@ -102,7 +99,9 @@ pub(crate) struct OverviewState {
|
|||||||
labels: OverviewStateLabel,
|
labels: OverviewStateLabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct OverviewAnimationSnapshot {
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct OverviewMotion {
|
||||||
|
active: bool,
|
||||||
presentations: HashMap<WindowId, AnimatedRect>,
|
presentations: HashMap<WindowId, AnimatedRect>,
|
||||||
opacity: AnimatedValue,
|
opacity: AnimatedValue,
|
||||||
exit_transition: Option<(f64, f64)>,
|
exit_transition: Option<(f64, f64)>,
|
||||||
@@ -111,12 +110,9 @@ pub(crate) struct OverviewAnimationSnapshot {
|
|||||||
impl Default for OverviewState {
|
impl Default for OverviewState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: false,
|
motion: OverviewMotion::default(),
|
||||||
selected: None,
|
selected: None,
|
||||||
selection_workspace: None,
|
selection_workspace: None,
|
||||||
presentations: HashMap::new(),
|
|
||||||
opacity: AnimatedValue::new(0.0),
|
|
||||||
exit_transition: None,
|
|
||||||
strip_offsets: HashMap::new(),
|
strip_offsets: HashMap::new(),
|
||||||
font_family: "sans-serif".into(),
|
font_family: "sans-serif".into(),
|
||||||
font_requests: None,
|
font_requests: None,
|
||||||
@@ -167,27 +163,17 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
|
|||||||
|
|
||||||
impl OverviewState {
|
impl OverviewState {
|
||||||
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||||
self.exit_transition.is_some()
|
self.motion.is_animating(spring)
|
||||||
|| (self.opacity.current - self.opacity.target).abs() > 0.001
|
|
||||||
|| self.opacity.velocity.abs() > 0.005
|
|
||||||
|| self.presentations.values().any(|rect| !rect.is_settled(spring))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn predict(&mut self, delta: Duration, spring: SpringConfig) -> OverviewAnimationSnapshot {
|
pub(crate) fn sample(&self, delta: Duration, spring: SpringConfig) -> OverviewMotion {
|
||||||
let snapshot = OverviewAnimationSnapshot {
|
let mut motion = self.motion.clone();
|
||||||
presentations: self.presentations.clone(),
|
|
||||||
opacity: self.opacity,
|
|
||||||
exit_transition: self.exit_transition,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.advance(delta, spring, true);
|
if !delta.is_zero() {
|
||||||
snapshot
|
motion.advance(delta, spring, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn restore_prediction(&mut self, snapshot: OverviewAnimationSnapshot) {
|
motion
|
||||||
self.presentations = snapshot.presentations;
|
|
||||||
self.opacity = snapshot.opacity;
|
|
||||||
self.exit_transition = snapshot.exit_transition;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_font_family(&mut self, family: String) {
|
pub(crate) fn set_font_family(&mut self, family: String) {
|
||||||
@@ -271,19 +257,20 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_active(&self) -> bool {
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
self.active
|
self.motion.active
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_presenting(&self) -> bool {
|
pub(crate) fn is_presenting(&self) -> bool {
|
||||||
self.active || !self.presentations.is_empty() || self.opacity.current > 0.001
|
self.motion.is_presenting()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn opacity(&self) -> f32 {
|
pub(crate) fn opacity(&self) -> f32 {
|
||||||
self.opacity.current.clamp(0.0, 1.0) as f32
|
self.motion.opacity()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) {
|
fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) {
|
||||||
let visible = |id: &WindowId| windows.contains(id) && self.presentations.contains_key(id);
|
let visible = |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id);
|
||||||
self.selected = self
|
self.selected = self
|
||||||
.selected
|
.selected
|
||||||
.filter(visible)
|
.filter(visible)
|
||||||
@@ -296,11 +283,11 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn has_window_preview(&self, id: WindowId) -> bool {
|
pub(crate) fn has_window_preview(&self, id: WindowId) -> bool {
|
||||||
self.presentations.contains_key(&id)
|
self.motion.presentations.contains_key(&id)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn select_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn select_window(&mut self, id: WindowId) -> bool {
|
||||||
if !self.presentations.contains_key(&id) {
|
if !self.motion.presentations.contains_key(&id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.selection_workspace = None;
|
self.selection_workspace = None;
|
||||||
@@ -311,13 +298,14 @@ impl OverviewState {
|
|||||||
pub(crate) fn select_direction(&mut self, direction: Direction) -> bool {
|
pub(crate) fn select_direction(&mut self, direction: Direction) -> bool {
|
||||||
let Some(current) = self
|
let Some(current) = self
|
||||||
.selected
|
.selected
|
||||||
.and_then(|id| self.presentations.get(&id))
|
.and_then(|id| self.motion.presentations.get(&id))
|
||||||
.map(|presentation| presentation.current)
|
.map(|presentation| presentation.current)
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let current_center = rect_center(current);
|
let current_center = rect_center(current);
|
||||||
let next = self
|
let next = self
|
||||||
|
.motion
|
||||||
.presentations
|
.presentations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, _)| Some(**id) != self.selected)
|
.filter(|(id, _)| Some(**id) != self.selected)
|
||||||
@@ -337,17 +325,7 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
||||||
let preview = self.presentations.get(&id).map_or(normal, |p| p.current);
|
self.motion.presented_rect(id, normal)
|
||||||
if let Some((elapsed, _)) = self.exit_transition {
|
|
||||||
let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3);
|
|
||||||
return Rect::new(
|
|
||||||
preview.x + (normal.x - preview.x) * t,
|
|
||||||
preview.y + (normal.y - preview.y) * t,
|
|
||||||
preview.width + (normal.width - preview.width) * t,
|
|
||||||
preview.height + (normal.height - preview.height) * t,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
preview
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter(
|
fn enter(
|
||||||
@@ -357,23 +335,23 @@ impl OverviewState {
|
|||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
) {
|
) {
|
||||||
// Reverse a dismissal from the currently visible geometry, not the old grid.
|
// Reverse a dismissal from the currently visible geometry, not the old grid.
|
||||||
if self.exit_transition.is_some() {
|
if self.motion.exit_transition.is_some() {
|
||||||
let visible: Vec<_> = targets
|
let visible: Vec<_> = targets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal)))
|
.map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal)))
|
||||||
.collect();
|
.collect();
|
||||||
for (id, rect) in visible {
|
for (id, rect) in visible {
|
||||||
if let Some(presentation) = self.presentations.get_mut(&id) {
|
if let Some(presentation) = self.motion.presentations.get_mut(&id) {
|
||||||
*presentation = AnimatedRect::new(rect);
|
*presentation = AnimatedRect::new(rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.exit_transition = None;
|
self.motion.exit_transition = None;
|
||||||
self.selection_workspace = None;
|
self.selection_workspace = None;
|
||||||
self.active = true;
|
self.motion.active = true;
|
||||||
self.opacity.retarget_preserving_motion(1.0);
|
self.motion.opacity.retarget_preserving_motion(1.0);
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.opacity.snap();
|
self.motion.opacity.snap();
|
||||||
}
|
}
|
||||||
self.strip_offsets.clear();
|
self.strip_offsets.clear();
|
||||||
self.selected = selected
|
self.selected = selected
|
||||||
@@ -383,10 +361,11 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) {
|
fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) {
|
||||||
self.presentations.retain(|id, _| targets.contains_key(id));
|
self.motion.presentations.retain(|id, _| targets.contains_key(id));
|
||||||
|
|
||||||
for (id, (normal, target)) in targets {
|
for (id, (normal, target)) in targets {
|
||||||
let presentation = self
|
let presentation = self
|
||||||
|
.motion
|
||||||
.presentations
|
.presentations
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| AnimatedRect::new(normal));
|
.or_insert_with(|| AnimatedRect::new(normal));
|
||||||
@@ -396,20 +375,23 @@ impl OverviewState {
|
|||||||
presentation.snap();
|
presentation.snap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) {
|
if self
|
||||||
self.selected = self.presentations.keys().copied().min_by_key(|id| id.0);
|
.selected
|
||||||
|
.is_none_or(|id| !self.motion.presentations.contains_key(&id))
|
||||||
|
{
|
||||||
|
self.selected = self.motion.presentations.keys().copied().min_by_key(|id| id.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
||||||
self.active = false;
|
self.motion.active = false;
|
||||||
self.exit_transition = animations_enabled.then_some((0.0, self.opacity.current));
|
self.motion.exit_transition = animations_enabled.then_some((0.0, self.motion.opacity.current));
|
||||||
self.opacity.retarget_preserving_motion(0.0);
|
self.motion.opacity.retarget_preserving_motion(0.0);
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.opacity.snap();
|
self.motion.opacity.snap();
|
||||||
}
|
}
|
||||||
self.selected = None;
|
self.selected = None;
|
||||||
self.presentations.retain(|id, presentation| {
|
self.motion.presentations.retain(|id, presentation| {
|
||||||
let Some(target) = normal.get(id).copied() else {
|
let Some(target) = normal.get(id).copied() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -422,58 +404,12 @@ impl OverviewState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.presentations.clear();
|
self.motion.presentations.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||||
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
self.motion.advance(delta, spring, animations_enabled)
|
||||||
let elapsed = if animations_enabled {
|
|
||||||
elapsed + delta.as_secs_f64()
|
|
||||||
} else {
|
|
||||||
EXIT_DURATION
|
|
||||||
};
|
|
||||||
let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0);
|
|
||||||
self.opacity.current = initial_opacity * remaining.powi(3);
|
|
||||||
if elapsed >= EXIT_DURATION {
|
|
||||||
self.exit_transition = None;
|
|
||||||
self.presentations.clear();
|
|
||||||
self.opacity.snap();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.exit_transition = Some((elapsed, initial_opacity));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let mut active_animation = false;
|
|
||||||
|
|
||||||
for presentation in self.presentations.values_mut() {
|
|
||||||
if animations_enabled {
|
|
||||||
active_animation |= presentation.advance(delta, spring);
|
|
||||||
} else {
|
|
||||||
presentation.snap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.active && !active_animation {
|
|
||||||
self.presentations.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if animations_enabled {
|
|
||||||
// Geometry's 0.1-pixel tolerance is far too coarse for opacity:
|
|
||||||
// it would abruptly drop the last ten percent of the fade.
|
|
||||||
active_animation |= self.opacity.advance(
|
|
||||||
delta,
|
|
||||||
SpringConfig {
|
|
||||||
position_tolerance: 0.001,
|
|
||||||
velocity_tolerance: 0.005,
|
|
||||||
..spring
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.opacity.snap();
|
|
||||||
}
|
|
||||||
|
|
||||||
active_animation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,7 +420,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
let selected = self
|
let selected = self
|
||||||
.window_under_visual(point)
|
.window_under_visual(point)
|
||||||
.and_then(|w| self.window_ids.get(&w).copied());
|
.and_then(|w| self.windows.ids().get(&w).copied());
|
||||||
|
|
||||||
if selected.is_some() && self.overview.selected != selected {
|
if selected.is_some() && self.overview.selected != selected {
|
||||||
self.overview.selection_workspace = None;
|
self.overview.selection_workspace = None;
|
||||||
@@ -574,7 +510,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool {
|
||||||
if !self.overview.is_active() || !self.overview.presentations.contains_key(&id) {
|
if !self.overview.is_active() || !self.overview.motion.presentations.contains_key(&id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +530,7 @@ impl Ferese {
|
|||||||
let targets = self.overview_targets();
|
let targets = self.overview_targets();
|
||||||
self.overview.retarget(targets, self.animations_enabled());
|
self.overview.retarget(targets, self.animations_enabled());
|
||||||
if let Some(cycle) = self.focus_cycle.as_mut() {
|
if let Some(cycle) = self.focus_cycle.as_mut() {
|
||||||
let available = self.overview.presentations.keys().copied().collect::<Vec<_>>();
|
let available = self.overview.motion.presentations.keys().copied().collect::<Vec<_>>();
|
||||||
if let Some(id) = cycle.reconcile(&available) {
|
if let Some(id) = cycle.reconcile(&available) {
|
||||||
self.overview.select_window(id);
|
self.overview.select_window(id);
|
||||||
}
|
}
|
||||||
@@ -619,7 +555,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option<Rect> {
|
pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option<Rect> {
|
||||||
let normal = self.window_geometry.get(&id)?.visual.current;
|
let normal = self.windows.geometry(&id)?.visual.current;
|
||||||
let mut presented = self.overview.presented_rect(id, normal);
|
let mut presented = self.overview.presented_rect(id, normal);
|
||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
let (x, y) = self.workspace_slide_offset(id);
|
let (x, y) = self.workspace_slide_offset(id);
|
||||||
@@ -630,7 +566,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
||||||
let geometry = self.window_geometry.get(&id)?;
|
let geometry = self.windows.geometry(&id)?;
|
||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
let presented = self.presented_window_rect(id)?;
|
let presented = self.presented_window_rect(id)?;
|
||||||
return Some((x - presented.x, y - presented.y));
|
return Some((x - presented.x, y - presented.y));
|
||||||
@@ -653,6 +589,14 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec<WorkspaceCard> {
|
pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec<WorkspaceCard> {
|
||||||
|
self.overview_workspace_cards_for_frame(output, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overview_workspace_cards_for_frame(
|
||||||
|
&self,
|
||||||
|
output: &Output,
|
||||||
|
frame: Option<&crate::state::FrameScene>,
|
||||||
|
) -> Vec<WorkspaceCard> {
|
||||||
let Some(bounds) = self.output_bounds_for(output) else {
|
let Some(bounds) = self.output_bounds_for(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -698,7 +642,11 @@ impl Ferese {
|
|||||||
.chain(workspace.floating.iter().copied())
|
.chain(workspace.floating.iter().copied())
|
||||||
.take(4)
|
.take(4)
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let source = self.window_geometry.get(&id)?.visual.current;
|
let source = frame
|
||||||
|
.and_then(|frame| frame.windows.get(&id).map(|window| &window.geometry))
|
||||||
|
.or_else(|| self.windows.geometry(&id))?
|
||||||
|
.visual
|
||||||
|
.current;
|
||||||
Some((id, source))
|
Some((id, source))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -813,11 +761,11 @@ impl Ferese {
|
|||||||
if self.focus_preview_output(*id) != Some(output_id) {
|
if self.focus_preview_output(*id) != Some(output_id) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
Some((*id, self.window_geometry.get(id)?.visual.current))
|
Some((*id, self.windows.geometry(id)?.visual.current))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
targets.insert(id, (self.window_geometry[&id].visual.current, target));
|
targets.insert(id, (self.windows.geometry(&id).unwrap().visual.current, target));
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -829,13 +777,13 @@ impl Ferese {
|
|||||||
.window_ids()
|
.window_ids()
|
||||||
.chain(workspace.floating.iter().copied())
|
.chain(workspace.floating.iter().copied())
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let normal = self.window_geometry.get(&id)?.visual.current;
|
let normal = self.windows.geometry(&id)?.visual.current;
|
||||||
Some((id, normal))
|
Some((id, normal))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
let normal = self.window_geometry[&id].visual.current;
|
let normal = self.windows.geometry(&id).unwrap().visual.current;
|
||||||
targets.insert(id, (normal, target));
|
targets.insert(id, (normal, target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -844,8 +792,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normal_window_rects(&self) -> HashMap<WindowId, Rect> {
|
fn normal_window_rects(&self) -> HashMap<WindowId, Rect> {
|
||||||
self.window_geometry
|
self.windows
|
||||||
.iter()
|
.geometries()
|
||||||
.map(|(id, geometry)| (*id, geometry.visual.current))
|
.map(|(id, geometry)| (*id, geometry.visual.current))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -1042,6 +990,36 @@ mod tests {
|
|||||||
assert!(!overview.is_presenting());
|
assert!(!overview.is_presenting());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sampling_overview_exit_does_not_finish_the_live_transition() {
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
let id = WindowId(1);
|
||||||
|
let normal = Rect::new(0., 0., 800., 600.);
|
||||||
|
let thumbnail = Rect::new(100., 100., 200., 150.);
|
||||||
|
overview.enter(HashMap::from([(id, (normal, thumbnail))]), Some(id), false);
|
||||||
|
overview.exit(HashMap::from([(id, normal)]), true);
|
||||||
|
overview.advance(Duration::from_millis(170), SpringConfig::default(), true);
|
||||||
|
let before = overview.presented_rect(id, normal);
|
||||||
|
let opacity = overview.opacity();
|
||||||
|
|
||||||
|
let finished = overview.sample(Duration::from_millis(16), SpringConfig::default());
|
||||||
|
let earlier = overview.sample(Duration::from_millis(4), SpringConfig::default());
|
||||||
|
|
||||||
|
assert!(!finished.is_presenting());
|
||||||
|
assert_eq!(finished.presented_rect(id, normal), normal);
|
||||||
|
assert!(earlier.is_presenting());
|
||||||
|
assert!(earlier.opacity() < opacity);
|
||||||
|
assert!(overview.is_presenting());
|
||||||
|
assert_eq!(overview.presented_rect(id, normal), before);
|
||||||
|
assert_eq!(overview.opacity(), opacity);
|
||||||
|
assert_eq!(
|
||||||
|
overview
|
||||||
|
.sample(Duration::ZERO, SpringConfig::default())
|
||||||
|
.presented_rect(id, normal),
|
||||||
|
before
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reversing_overview_preserves_current_opacity() {
|
fn reversing_overview_preserves_current_opacity() {
|
||||||
let mut overview = OverviewState::default();
|
let mut overview = OverviewState::default();
|
||||||
@@ -1231,3 +1209,95 @@ mod tests {
|
|||||||
assert!(!overview.select_direction(Direction::Right));
|
assert!(!overview.select_direction(Direction::Right));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for OverviewMotion {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
presentations: HashMap::new(),
|
||||||
|
opacity: AnimatedValue::new(0.0),
|
||||||
|
exit_transition: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverviewMotion {
|
||||||
|
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||||
|
self.exit_transition.is_some()
|
||||||
|
|| (self.opacity.current - self.opacity.target).abs() > 0.001
|
||||||
|
|| self.opacity.velocity.abs() > 0.005
|
||||||
|
|| self.presentations.values().any(|rect| !rect.is_settled(spring))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_presenting(&self) -> bool {
|
||||||
|
self.active || !self.presentations.is_empty() || self.opacity.current > 0.001
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn opacity(&self) -> f32 {
|
||||||
|
self.opacity.current.clamp(0.0, 1.0) as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
||||||
|
let preview = self.presentations.get(&id).map_or(normal, |p| p.current);
|
||||||
|
if let Some((elapsed, _)) = self.exit_transition {
|
||||||
|
let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3);
|
||||||
|
return Rect::new(
|
||||||
|
preview.x + (normal.x - preview.x) * t,
|
||||||
|
preview.y + (normal.y - preview.y) * t,
|
||||||
|
preview.width + (normal.width - preview.width) * t,
|
||||||
|
preview.height + (normal.height - preview.height) * t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
preview
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||||
|
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
||||||
|
let elapsed = if animations_enabled {
|
||||||
|
elapsed + delta.as_secs_f64()
|
||||||
|
} else {
|
||||||
|
EXIT_DURATION
|
||||||
|
};
|
||||||
|
let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0);
|
||||||
|
self.opacity.current = initial_opacity * remaining.powi(3);
|
||||||
|
if elapsed >= EXIT_DURATION {
|
||||||
|
self.exit_transition = None;
|
||||||
|
self.presentations.clear();
|
||||||
|
self.opacity.snap();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.exit_transition = Some((elapsed, initial_opacity));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut active_animation = false;
|
||||||
|
|
||||||
|
for presentation in self.presentations.values_mut() {
|
||||||
|
if animations_enabled {
|
||||||
|
active_animation |= presentation.advance(delta, spring);
|
||||||
|
} else {
|
||||||
|
presentation.snap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.active && !active_animation {
|
||||||
|
self.presentations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if animations_enabled {
|
||||||
|
// Geometry's 0.1-pixel tolerance is far too coarse for opacity:
|
||||||
|
// it would abruptly drop the last ten percent of the fade.
|
||||||
|
active_animation |= self.opacity.advance(
|
||||||
|
delta,
|
||||||
|
SpringConfig {
|
||||||
|
position_tolerance: 0.001,
|
||||||
|
velocity_tolerance: 0.005,
|
||||||
|
..spring
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.opacity.snap();
|
||||||
|
}
|
||||||
|
|
||||||
|
active_animation
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,7 +36,8 @@ pub(crate) fn take_output_feedback(
|
|||||||
|
|
||||||
for window in state.space.elements().filter(|window| {
|
for window in state.space.elements().filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<RoundedClipPrograms> {
|
pub(super) fn rounded_clip_program(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<RoundedClipPrograms> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.rounded_clip_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.rounded.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,11 +58,11 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
|
|||||||
border,
|
border,
|
||||||
shadow,
|
shadow,
|
||||||
};
|
};
|
||||||
state.rounded_clip_programs.insert(context, programs.clone());
|
resources.contexts.entry(context).or_default().rounded = Some(programs.clone());
|
||||||
Some(programs)
|
Some(programs)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if state.rounded_clip_warnings.insert(context) {
|
if !std::mem::replace(&mut resources.contexts.entry(context).or_default().rounded_warned, true) {
|
||||||
tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering");
|
tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering");
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -65,7 +72,8 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_border_element(
|
pub(super) fn window_border_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
|
theme: &crate::config::ThemeSettings,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
@@ -98,17 +106,13 @@ pub(super) fn window_border_element(
|
|||||||
),
|
),
|
||||||
None => (color, color, [0.0; 4]),
|
None => (color, color, [0.0; 4]),
|
||||||
};
|
};
|
||||||
let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient {
|
let (focus_from, focus_to, focus_gradient_line) = match theme.focus_ring_gradient {
|
||||||
Some(g) => (
|
Some(g) => (
|
||||||
g.from.0,
|
g.from.0,
|
||||||
g.to.0,
|
g.to.0,
|
||||||
border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle),
|
border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle),
|
||||||
),
|
),
|
||||||
None => (
|
None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]),
|
||||||
state.theme_settings.accent_color.0,
|
|
||||||
state.theme_settings.accent_color.0,
|
|
||||||
[0.0; 4],
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
@@ -124,7 +128,7 @@ pub(super) fn window_border_element(
|
|||||||
focus_mix,
|
focus_mix,
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = state.window_borders.entry(id).or_default();
|
let buffers = &mut resources.windows.entry(id).or_default().borders;
|
||||||
if !buffers.contexts.contains_key(&context) {
|
if !buffers.contexts.contains_key(&context) {
|
||||||
let element = PixelShaderElement::new(
|
let element = PixelShaderElement::new(
|
||||||
programs.border.clone(),
|
programs.border.clone(),
|
||||||
@@ -175,7 +179,7 @@ pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'sta
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_tint_element(
|
pub(super) fn window_tint_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
@@ -186,14 +190,14 @@ pub(super) fn window_tint_element(
|
|||||||
) -> Option<PhysicalShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
if color[3] <= 0.0 {
|
if color[3] <= 0.0 {
|
||||||
if resize_fill {
|
if resize_fill {
|
||||||
state.window_resize_fills.remove(&id);
|
resources.clear_tint(id, true);
|
||||||
} else {
|
} else {
|
||||||
state.window_dims.remove(&id);
|
resources.clear_tint(id, false);
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = material_program(state, renderer)?;
|
let program = material_program(resources, renderer)?;
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
@@ -219,9 +223,9 @@ pub(super) fn window_tint_element(
|
|||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = if resize_fill {
|
let buffers = if resize_fill {
|
||||||
state.window_resize_fills.entry(id).or_default()
|
&mut resources.windows.entry(id).or_default().resize_fill
|
||||||
} else {
|
} else {
|
||||||
state.window_dims.entry(id).or_default()
|
&mut resources.windows.entry(id).or_default().dim
|
||||||
};
|
};
|
||||||
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
||||||
element: PixelShaderElement::new(
|
element: PixelShaderElement::new(
|
||||||
@@ -251,7 +255,7 @@ pub(super) fn window_tint_element(
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_shadow_element(
|
pub(super) fn window_shadow_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
@@ -287,7 +291,7 @@ pub(super) fn window_shadow_element(
|
|||||||
color,
|
color,
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = state.window_shadows.entry(id).or_default();
|
let buffers = &mut resources.windows.entry(id).or_default().shadow;
|
||||||
if !buffers.contexts.contains_key(&context) {
|
if !buffers.contexts.contains_key(&context) {
|
||||||
let element = PixelShaderElement::new(
|
let element = PixelShaderElement::new(
|
||||||
programs.shadow.clone(),
|
programs.shadow.clone(),
|
||||||
|
|||||||
@@ -45,14 +45,14 @@ pub(super) fn append_material_surface(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(buffers) = state.material_buffers.get_mut(surface) {
|
if let Some(buffers) = state.render.surfaces.get_mut(surface) {
|
||||||
buffers.contexts.retain(|(_, index), _| *index < targets.len());
|
buffers.contexts.retain(|(_, index), _| *index < targets.len());
|
||||||
}
|
}
|
||||||
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
||||||
let clip = material.as_ref().and_then(|_| {
|
let clip = material.as_ref().and_then(|_| {
|
||||||
let corners = targets.first()?.1;
|
let corners = targets.first()?.1;
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = rounded_clip_program(state, renderer)?;
|
let program = rounded_clip_program(&mut state.render, renderer)?;
|
||||||
Some((
|
Some((
|
||||||
program,
|
program,
|
||||||
framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
@@ -195,11 +195,13 @@ pub(super) fn material_element(
|
|||||||
visible_framebuffer: framebuffer_clip_rect(corners.rect, mode.size, transform),
|
visible_framebuffer: framebuffer_clip_rect(corners.rect, mode.size, transform),
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let program = material_program(state, renderer)?;
|
let program = material_program(&mut state.render, renderer)?;
|
||||||
let blur_program = (blur > 0.0).then(|| blur_program(state, renderer)).flatten();
|
let blur_program = (blur > 0.0)
|
||||||
|
.then(|| blur_program(&mut state.render, renderer))
|
||||||
|
.flatten();
|
||||||
let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
||||||
let capture_size = Size::from((capture_rect[2], capture_rect[3]));
|
let capture_size = Size::from((capture_rect[2], capture_rect[3]));
|
||||||
let buffers = state.material_buffers.entry(surface.clone()).or_default();
|
let buffers = state.render.surfaces.entry(surface.clone()).or_default();
|
||||||
let capture = if let Some(blur_program) = blur_program {
|
let capture = if let Some(blur_program) = blur_program {
|
||||||
if buffers
|
if buffers
|
||||||
.captures
|
.captures
|
||||||
@@ -317,15 +319,19 @@ pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn blur_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<BlurProgram> {
|
pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesRenderer) -> Option<BlurProgram> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.blur_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.blur.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) {
|
match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) {
|
||||||
Ok(program) => {
|
Ok(program) => {
|
||||||
let program = BlurProgram(program);
|
let program = BlurProgram(program);
|
||||||
state.blur_programs.insert(context, program.clone());
|
resources.contexts.entry(context).or_default().blur = Some(program.clone());
|
||||||
Some(program)
|
Some(program)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -380,9 +386,16 @@ pub(super) fn expanded_blur_region(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<MaterialProgram> {
|
pub(super) fn material_program(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<MaterialProgram> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.material_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.material.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +410,7 @@ pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer)
|
|||||||
match renderer.compile_custom_pixel_shader(corner_shader(MATERIAL_SHADER), &uniforms) {
|
match renderer.compile_custom_pixel_shader(corner_shader(MATERIAL_SHADER), &uniforms) {
|
||||||
Ok(program) => {
|
Ok(program) => {
|
||||||
let program = MaterialProgram(program);
|
let program = MaterialProgram(program);
|
||||||
state.material_programs.insert(context, program.clone());
|
resources.contexts.entry(context).or_default().material = Some(program.clone());
|
||||||
Some(program)
|
Some(program)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ mod capture;
|
|||||||
mod decorations;
|
mod decorations;
|
||||||
mod materials;
|
mod materials;
|
||||||
mod overview;
|
mod overview;
|
||||||
|
mod resources;
|
||||||
mod scene;
|
mod scene;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -13,9 +13,10 @@ pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture
|
|||||||
use decorations::*;
|
use decorations::*;
|
||||||
use materials::*;
|
use materials::*;
|
||||||
use overview::*;
|
use overview::*;
|
||||||
|
pub(crate) use resources::RenderResources;
|
||||||
use scene::cursor_elements;
|
use scene::cursor_elements;
|
||||||
pub(crate) use scene::{
|
pub(crate) use scene::{
|
||||||
animated_window_elements, cursorless_window_elements, frame_effect_metrics, layer_surfaces, output_elements,
|
animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements,
|
||||||
};
|
};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ pub(super) fn overview_chrome_element(
|
|||||||
focus_mix: 0.0,
|
focus_mix: 0.0,
|
||||||
};
|
};
|
||||||
let program = if key.1 == OverviewChromePart::Outline {
|
let program = if key.1 == OverviewChromePart::Outline {
|
||||||
rounded_clip_program(state, renderer)?.border
|
rounded_clip_program(&mut state.render, renderer)?.border
|
||||||
} else {
|
} else {
|
||||||
material_program(state, renderer)?.0
|
material_program(&mut state.render, renderer)?.0
|
||||||
};
|
};
|
||||||
let uniforms = |p: &BorderParameters| {
|
let uniforms = |p: &BorderParameters| {
|
||||||
if key.1 == OverviewChromePart::Outline {
|
if key.1 == OverviewChromePart::Outline {
|
||||||
@@ -51,7 +51,8 @@ pub(super) fn overview_chrome_element(
|
|||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let chrome = &mut state
|
let chrome = &mut state
|
||||||
.overview_scrims
|
.render
|
||||||
|
.outputs
|
||||||
.entry(output_id)
|
.entry(output_id)
|
||||||
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
||||||
.chrome;
|
.chrome;
|
||||||
@@ -89,15 +90,16 @@ pub(super) fn overview_strip_elements(
|
|||||||
output: &Output,
|
output: &Output,
|
||||||
output_geometry: Rectangle<i32, Logical>,
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
|
frame: &crate::state::FrameScene,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
let alpha = state.overview.opacity();
|
let alpha = frame.overview.opacity();
|
||||||
if alpha <= 0.001 {
|
if alpha <= 0.001 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(bounds) = state.output_bounds_for(output) else {
|
let Some(bounds) = state.output_bounds_for(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let cards = state.overview_workspace_cards(output);
|
let cards = state.overview_workspace_cards_for_frame(output, Some(frame));
|
||||||
let strip = crate::overview::workspace_strip(bounds);
|
let strip = crate::overview::workspace_strip(bounds);
|
||||||
let panel = match (cards.first(), cards.last()) {
|
let panel = match (cards.first(), cards.last()) {
|
||||||
(Some(first), Some(last)) => ferese_layout::Rect::new(
|
(Some(first), Some(last)) => ferese_layout::Rect::new(
|
||||||
@@ -116,16 +118,17 @@ pub(super) fn overview_strip_elements(
|
|||||||
)
|
)
|
||||||
.opacity;
|
.opacity;
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
let programs = rounded_clip_program(state, renderer);
|
let programs = rounded_clip_program(&mut state.render, renderer);
|
||||||
// Resolve IDs once per strip, rather than scanning all managed windows
|
// Resolve IDs once per strip, rather than scanning all managed windows
|
||||||
// separately for every miniature. Own the handles so rendering can mutate state.
|
// separately for every miniature. Own the handles so rendering can mutate state.
|
||||||
let windows_by_id: std::collections::HashMap<_, _> = state
|
let windows_by_id: std::collections::HashMap<_, _> = state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(window, id)| (*id, window.clone()))
|
.map(|(window, id)| (*id, window.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(output_id) = state.output_id(output)
|
if let Some(output_id) = state.output_id(output)
|
||||||
&& let Some(cache) = state.overview_scrims.get_mut(&output_id)
|
&& let Some(cache) = state.render.outputs.get_mut(&output_id)
|
||||||
{
|
{
|
||||||
cache.chrome.retain(|(id, part), _| match part {
|
cache.chrome.retain(|(id, part), _| match part {
|
||||||
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
|
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
|
||||||
@@ -139,7 +142,7 @@ pub(super) fn overview_strip_elements(
|
|||||||
if !state.window_belongs_to_output(*id, output) {
|
if !state.window_belongs_to_output(*id, output) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(rect) = state.presented_window_rect(*id) else {
|
let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else {
|
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else {
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use ferese_core::OutputId;
|
||||||
|
use ferese_layout::WindowId;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Resource ownership follows the lifetime of a window, output, surface or GL
|
||||||
|
/// context. Desktop policy and animation values do not live in these caches.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct RenderResources {
|
||||||
|
pub(super) windows: HashMap<WindowId, WindowBuffers>,
|
||||||
|
pub(super) contexts: HashMap<ErasedContextId, ContextPrograms>,
|
||||||
|
pub(super) outputs: HashMap<OutputId, OverviewScrim>,
|
||||||
|
pub(super) surfaces: HashMap<WlSurface, MaterialBuffers>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct WindowBuffers {
|
||||||
|
pub borders: WindowBorderBuffers,
|
||||||
|
pub dim: WindowBorderBuffers,
|
||||||
|
pub resize_fill: WindowBorderBuffers,
|
||||||
|
pub shadow: WindowShadowBuffers,
|
||||||
|
snapshot: Option<ResizeSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct ContextPrograms {
|
||||||
|
pub rounded: Option<RoundedClipPrograms>,
|
||||||
|
pub rounded_warned: bool,
|
||||||
|
pub material: Option<MaterialProgram>,
|
||||||
|
pub blur: Option<BlurProgram>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderResources {
|
||||||
|
pub fn remove_window(&mut self, id: WindowId) {
|
||||||
|
self.windows.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_output(&mut self, id: OutputId) {
|
||||||
|
self.outputs.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self, id: &WindowId) -> Option<&ResizeSnapshot> {
|
||||||
|
self.windows.get(id)?.snapshot.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshots(&self) -> impl Iterator<Item = &ResizeSnapshot> {
|
||||||
|
self.windows.values().filter_map(|window| window.snapshot.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) {
|
||||||
|
self.windows.entry(id).or_default().snapshot = Some(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_snapshot(&mut self, id: &WindowId) {
|
||||||
|
if let Some(window) = self.windows.get_mut(id) {
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retain_snapshots(&mut self, mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool) {
|
||||||
|
for (id, window) in &mut self.windows {
|
||||||
|
if let Some(snapshot) = &mut window.snapshot
|
||||||
|
&& !keep(id, snapshot)
|
||||||
|
{
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_tint(&mut self, id: WindowId, resize_fill: bool) {
|
||||||
|
let Some(window) = self.windows.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if resize_fill {
|
||||||
|
window.resize_fill.contexts.clear();
|
||||||
|
} else {
|
||||||
|
window.dim.contexts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
||||||
|
self.contexts.remove(context);
|
||||||
|
|
||||||
|
for window in self.windows.values_mut() {
|
||||||
|
window.borders.contexts.remove(context);
|
||||||
|
window.dim.contexts.remove(context);
|
||||||
|
window.resize_fill.contexts.remove(context);
|
||||||
|
window.shadow.contexts.remove(context);
|
||||||
|
|
||||||
|
if window
|
||||||
|
.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|snapshot| &snapshot.context == context)
|
||||||
|
{
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for output in self.outputs.values_mut() {
|
||||||
|
for contexts in output.chrome.values_mut() {
|
||||||
|
contexts.remove(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for surface in self.surfaces.values_mut() {
|
||||||
|
surface.contexts.retain(|(id, _), _| id != context);
|
||||||
|
surface.captures.remove(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_removal_releases_all_its_caches_without_removing_other_windows() {
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
resources.windows.insert(WindowId(1), WindowBuffers::default());
|
||||||
|
resources.windows.insert(WindowId(2), WindowBuffers::default());
|
||||||
|
|
||||||
|
resources.remove_window(WindowId(1));
|
||||||
|
resources.clear_snapshot(&WindowId(1));
|
||||||
|
resources.clear_tint(WindowId(1), true);
|
||||||
|
|
||||||
|
assert!(!resources.windows.contains_key(&WindowId(1)));
|
||||||
|
assert!(resources.windows.contains_key(&WindowId(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||||
|
let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||||
|
let first_id = first.context_id().erased();
|
||||||
|
let second_id = second.context_id().erased();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
|
||||||
|
for (id, renderer) in [(WindowId(1), &mut first), (WindowId(2), &mut second)] {
|
||||||
|
rounded_clip_program(&mut resources, renderer).unwrap();
|
||||||
|
material_program(&mut resources, renderer).unwrap();
|
||||||
|
blur_program(&mut resources, renderer).unwrap();
|
||||||
|
let texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, (16, 16).into()).unwrap();
|
||||||
|
resources.set_snapshot(
|
||||||
|
id,
|
||||||
|
ResizeSnapshot {
|
||||||
|
texture,
|
||||||
|
context: renderer.context_id().erased(),
|
||||||
|
id: Id::new(),
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
elapsed: Duration::ZERO,
|
||||||
|
last_tick: None,
|
||||||
|
scale: 1.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.forget_context(&first_id);
|
||||||
|
|
||||||
|
assert!(!resources.contexts.contains_key(&first_id));
|
||||||
|
assert!(resources.contexts.contains_key(&second_id));
|
||||||
|
assert!(resources.snapshot(&WindowId(1)).is_none());
|
||||||
|
assert!(resources.snapshot(&WindowId(2)).is_some());
|
||||||
|
assert_eq!(resources.snapshots().count(), 1);
|
||||||
|
|
||||||
|
// Reconnecting a context can build fresh resources after eviction.
|
||||||
|
rounded_clip_program(&mut resources, &mut first).unwrap();
|
||||||
|
assert!(resources.contexts.contains_key(&first_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,14 +8,6 @@ pub(crate) fn animated_window_elements(
|
|||||||
output_elements(state, renderer, output, true)
|
output_elements(state, renderer, output, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cursorless_window_elements(
|
|
||||||
state: &mut Ferese,
|
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
output: &Output,
|
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
|
||||||
output_elements(state, renderer, output, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics {
|
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics {
|
||||||
FrameEffectMetrics
|
FrameEffectMetrics
|
||||||
}
|
}
|
||||||
@@ -25,9 +17,20 @@ pub(crate) fn output_elements(
|
|||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
include_cursor: bool,
|
include_cursor: bool,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let frame = state.sample_frame(output, Duration::ZERO);
|
||||||
|
|
||||||
|
sampled_output_elements(state, renderer, output, include_cursor, &frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sampled_output_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
include_cursor: bool,
|
||||||
|
frame: &crate::state::FrameScene,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
if state.session_lock.active {
|
if state.session_lock.active {
|
||||||
state.configure_lock_surfaces();
|
|
||||||
let Some(geometry) = state.space.output_geometry(output) else {
|
let Some(geometry) = state.space.output_geometry(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -83,29 +86,36 @@ pub(crate) fn output_elements(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let scale = output.current_scale().fractional_scale();
|
let scale = output.current_scale().fractional_scale();
|
||||||
let rounded_clip_program = rounded_clip_program(state, renderer);
|
let rounded_clip_program = rounded_clip_program(&mut state.render, renderer);
|
||||||
|
|
||||||
let mut elements = if include_cursor {
|
let mut elements = if include_cursor {
|
||||||
cursor_elements(state, renderer, output_geometry, scale)
|
cursor_elements(state, renderer, output_geometry, scale)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
|
let upper_layers: &[Layer] = if state.output_has_fullscreen_for_frame(output, frame.overview.is_presenting()) {
|
||||||
&[Layer::Overlay]
|
&[Layer::Overlay]
|
||||||
} else {
|
} else {
|
||||||
&[Layer::Overlay, Layer::Top]
|
&[Layer::Overlay, Layer::Top]
|
||||||
};
|
};
|
||||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||||
if state.overview.is_presenting() {
|
if frame.overview.is_presenting() {
|
||||||
elements.extend(overview_strip_elements(state, renderer, output, output_geometry, scale));
|
elements.extend(overview_strip_elements(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
frame,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut candidates = if state.overview.is_active() {
|
let mut candidates = if state.overview.is_active() {
|
||||||
state.window_ids.keys().cloned().collect::<Vec<_>>()
|
state.windows.ids().keys().cloned().collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
if state.overview.is_active() {
|
if state.overview.is_active() {
|
||||||
candidates.sort_by_key(|window| std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0)));
|
candidates.sort_by_key(|window| std::cmp::Reverse(state.windows.ids().get(window).map_or(0, |id| id.0)));
|
||||||
}
|
}
|
||||||
let windows = candidates
|
let windows = candidates
|
||||||
.iter()
|
.iter()
|
||||||
@@ -115,20 +125,17 @@ pub(crate) fn output_elements(
|
|||||||
if !state.window_content_ready(window) {
|
if !state.window_content_ready(window) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let id = *state.window_ids.get(window)?;
|
let id = *state.windows.ids().get(window)?;
|
||||||
// Scrolling columns may sit outside their monitor's rectangle.
|
// Scrolling columns may sit outside their monitor's rectangle.
|
||||||
// They must not reappear on a neighboring output just because
|
// They must not reappear on a neighboring output just because
|
||||||
// their global animated coordinates overlap it.
|
// their global animated coordinates overlap it.
|
||||||
if !state.window_belongs_to_output(id, output) {
|
if !state.window_belongs_to_output(id, output) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let visual = state.presented_window_rect(id)?;
|
let sample = frame.windows.get(&id)?;
|
||||||
let (close_scale, close_alpha) = state.closing_visual(id);
|
let visual = scaled_visual_rect(sample.rect, sample.close_scale);
|
||||||
let visual = scaled_visual_rect(visual, close_scale);
|
let close_alpha = sample.close_alpha;
|
||||||
let decoration_progress = state
|
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
|
||||||
.window_geometry
|
|
||||||
.get(&id)
|
|
||||||
.map_or(1.0, |geometry| geometry.decorations.clamp(0.0, 1.0));
|
|
||||||
|
|
||||||
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
||||||
})
|
})
|
||||||
@@ -151,12 +158,12 @@ pub(crate) fn output_elements(
|
|||||||
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
|
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
|
||||||
let pixels = corners.rect;
|
let pixels = corners.rect;
|
||||||
// Only overview/close intentionally scale the complete application.
|
// Only overview/close intentionally scale the complete application.
|
||||||
let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0;
|
let scale_content = frame.overview.is_presenting() || frame.windows[&id].close_scale != 1.0;
|
||||||
let behavior = resize_content_behavior(scale_content);
|
let behavior = resize_content_behavior(scale_content);
|
||||||
|
|
||||||
let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current);
|
let dim = frame.windows[&id].dim;
|
||||||
if let Some(overlay) = window_tint_element(
|
if let Some(overlay) = window_tint_element(
|
||||||
state,
|
&mut state.render,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
@@ -175,22 +182,15 @@ pub(crate) fn output_elements(
|
|||||||
let shadow_blur = state.theme_settings.shadow_blur;
|
let shadow_blur = state.theme_settings.shadow_blur;
|
||||||
let shadow_opacity = state.theme_settings.shadow_opacity;
|
let shadow_opacity = state.theme_settings.shadow_opacity;
|
||||||
let shadow_color = state.theme_settings.shadow_color.0;
|
let shadow_color = state.theme_settings.shadow_color.0;
|
||||||
let focused = if state.overview.is_active() {
|
let focus = frame.windows[&id].focus;
|
||||||
state.overview_selected(id)
|
|
||||||
} else {
|
|
||||||
state.focused_window == Some(id)
|
|
||||||
};
|
|
||||||
let focus = state
|
|
||||||
.window_focus
|
|
||||||
.get(&id)
|
|
||||||
.map_or(if focused { 1.0 } else { 0.0 }, |v| v.current);
|
|
||||||
let border_width = state.theme_settings.border_width
|
let border_width = state.theme_settings.border_width
|
||||||
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
|
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
|
||||||
let border_color = state.theme_settings.border_color.0;
|
let border_color = state.theme_settings.border_color.0;
|
||||||
let gradient = state.theme_settings.border_gradient;
|
let gradient = state.theme_settings.border_gradient;
|
||||||
|
|
||||||
if let Some(border) = window_border_element(
|
if let Some(border) = window_border_element(
|
||||||
state,
|
&mut state.render,
|
||||||
|
&state.theme_settings,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
@@ -207,7 +207,7 @@ pub(crate) fn output_elements(
|
|||||||
elements.push(border.into());
|
elements.push(border.into());
|
||||||
}
|
}
|
||||||
let shadow = window_shadow_element(
|
let shadow = window_shadow_element(
|
||||||
state,
|
&mut state.render,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
@@ -221,7 +221,7 @@ pub(crate) fn output_elements(
|
|||||||
&programs,
|
&programs,
|
||||||
);
|
);
|
||||||
if !scale_content
|
if !scale_content
|
||||||
&& let Some(snapshot) = state.resize_snapshots.get(&id)
|
&& let Some(snapshot) = state.render.snapshot(&id)
|
||||||
&& snapshot.context == renderer.context_id().erased()
|
&& snapshot.context == renderer.context_id().erased()
|
||||||
&& (snapshot.scale - scale).abs() < 0.001
|
&& (snapshot.scale - scale).abs() < 0.001
|
||||||
{
|
{
|
||||||
@@ -260,10 +260,7 @@ pub(crate) fn output_elements(
|
|||||||
corners,
|
corners,
|
||||||
scale,
|
scale,
|
||||||
close_alpha,
|
close_alpha,
|
||||||
state
|
frame.windows[&id].geometry.presentation_changed,
|
||||||
.window_geometry
|
|
||||||
.get(&id)
|
|
||||||
.is_some_and(|geometry| geometry.presentation_changed),
|
|
||||||
output,
|
output,
|
||||||
programs.clone(),
|
programs.clone(),
|
||||||
behavior,
|
behavior,
|
||||||
@@ -275,13 +272,15 @@ pub(crate) fn output_elements(
|
|||||||
{
|
{
|
||||||
let mut color = state.theme_settings.surface_base_color.0;
|
let mut color = state.theme_settings.surface_base_color.0;
|
||||||
color[3] = close_alpha;
|
color[3] = close_alpha;
|
||||||
if let Some(fill) = window_tint_element(state, renderer, id, constrain, corners, color, true, output) {
|
if let Some(fill) =
|
||||||
|
window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output)
|
||||||
|
{
|
||||||
// Front-to-back: fill uncovered strips behind the native
|
// Front-to-back: fill uncovered strips behind the native
|
||||||
// content instead of stretching it or exposing wallpaper.
|
// content instead of stretching it or exposing wallpaper.
|
||||||
elements.push(fill.into());
|
elements.push(fill.into());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.window_resize_fills.remove(&id);
|
state.render.clear_tint(id, true);
|
||||||
}
|
}
|
||||||
if let Some(surface) = material_surface
|
if let Some(surface) = material_surface
|
||||||
&& let Some((background, _)) = material_element(
|
&& let Some((background, _)) = material_element(
|
||||||
@@ -370,7 +369,7 @@ pub(super) fn layer_elements(
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
state.material_buffers.retain(|surface, _| surface.is_alive());
|
state.render.surfaces.retain(|surface, _| surface.is_alive());
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
for (geometry, layer) in layers {
|
for (geometry, layer) in layers {
|
||||||
|
|||||||
@@ -338,7 +338,8 @@ impl Ferese {
|
|||||||
|
|
||||||
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
||||||
let mut snapshots = self
|
let mut snapshots = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
||||||
|
|||||||
+8
-63
@@ -6,9 +6,10 @@ mod layout;
|
|||||||
mod navigation;
|
mod navigation;
|
||||||
mod outputs;
|
mod outputs;
|
||||||
mod prediction;
|
mod prediction;
|
||||||
|
pub(crate) use prediction::FrameScene;
|
||||||
|
mod window_registry;
|
||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@@ -28,7 +29,7 @@ use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
|||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::drm::{DrmEventTime, DrmNode};
|
use smithay::backend::drm::{DrmEventTime, DrmNode};
|
||||||
use smithay::backend::input::Keycode;
|
use smithay::backend::input::Keycode;
|
||||||
use smithay::backend::renderer::{ErasedContextId, ImportDma};
|
use smithay::backend::renderer::ImportDma;
|
||||||
use smithay::desktop::space::SpaceElement;
|
use smithay::desktop::space::SpaceElement;
|
||||||
use smithay::desktop::utils::send_frames_surface_tree;
|
use smithay::desktop::utils::send_frames_surface_tree;
|
||||||
use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output};
|
use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output};
|
||||||
@@ -71,6 +72,7 @@ use smithay::wayland::viewporter::ViewporterState;
|
|||||||
use smithay::wayland::xdg_activation::XdgActivationState;
|
use smithay::wayland::xdg_activation::XdgActivationState;
|
||||||
use smithay::wayland::xdg_foreign::XdgForeignState;
|
use smithay::wayland::xdg_foreign::XdgForeignState;
|
||||||
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
|
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
|
||||||
|
use window_registry::WindowRegistry;
|
||||||
|
|
||||||
use crate::backends::direct::DirectBackendState;
|
use crate::backends::direct::DirectBackendState;
|
||||||
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
|
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
|
||||||
@@ -86,11 +88,6 @@ use crate::ipc::IpcSocketGuard;
|
|||||||
use crate::overview::OverviewState;
|
use crate::overview::OverviewState;
|
||||||
use crate::portal_session::PortalSession;
|
use crate::portal_session::PortalSession;
|
||||||
use crate::portal_shortcuts::PortalShortcuts;
|
use crate::portal_shortcuts::PortalShortcuts;
|
||||||
use crate::render::{
|
|
||||||
BlurProgram, MaterialBuffers, MaterialProgram, OverviewScrim, ResizeSnapshot, RoundedClipPrograms,
|
|
||||||
WindowBorderBuffers, WindowShadowBuffers,
|
|
||||||
};
|
|
||||||
use crate::resize_transaction::ResizeTransaction;
|
|
||||||
use crate::session_lock::{IdleSettings, Lock};
|
use crate::session_lock::{IdleSettings, Lock};
|
||||||
use crate::shell_control::ShellSnapshot;
|
use crate::shell_control::ShellSnapshot;
|
||||||
use crate::stacking::WindowStack;
|
use crate::stacking::WindowStack;
|
||||||
@@ -326,47 +323,23 @@ pub struct Ferese {
|
|||||||
pub output_workspaces: OutputWorkspaceMap,
|
pub output_workspaces: OutputWorkspaceMap,
|
||||||
pub(crate) output_ids: HashMap<Output, OutputId>,
|
pub(crate) output_ids: HashMap<Output, OutputId>,
|
||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub(crate) windows: WindowRegistry<Window>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
pub(crate) render: crate::render::RenderResources,
|
||||||
resize_transactions: HashMap<WindowId, ResizeTransaction>,
|
|
||||||
pub(crate) resize_snapshots: HashMap<WindowId, ResizeSnapshot>,
|
|
||||||
pub(crate) nested_backend: Option<NestedBackend>,
|
pub(crate) nested_backend: Option<NestedBackend>,
|
||||||
pub(crate) wallpaper: WallpaperState,
|
pub(crate) wallpaper: WallpaperState,
|
||||||
maximized_windows: HashSet<WindowId>,
|
|
||||||
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
|
|
||||||
window_stack: WindowStack,
|
window_stack: WindowStack,
|
||||||
floating_above_fullscreen: HashMap<WindowId, WindowId>,
|
floating_above_fullscreen: HashMap<WindowId, WindowId>,
|
||||||
natural_floating_pending: HashSet<WindowId>,
|
|
||||||
floating_memory: crate::floating::Memory,
|
floating_memory: crate::floating::Memory,
|
||||||
floating_save_worker: crate::floating::SaveWorker,
|
floating_save_worker: crate::floating::SaveWorker,
|
||||||
floating_window_memory: HashMap<WindowId, crate::floating::Remembered>,
|
|
||||||
floating_placement_anchors: HashMap<WindowId, Rect>,
|
|
||||||
floating_cascade: crate::floating::Cascade,
|
floating_cascade: crate::floating::Cascade,
|
||||||
pub(crate) floating_resize_anchors: HashMap<WindowId, (bool, bool, Rect)>,
|
|
||||||
pub(crate) window_borders: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_dims: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_resize_fills: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_dimming: HashMap<WindowId, DimAnimation>,
|
|
||||||
pub(crate) window_focus: HashMap<WindowId, DimAnimation>,
|
|
||||||
pub(crate) window_shadows: HashMap<WindowId, WindowShadowBuffers>,
|
|
||||||
pub(crate) rounded_clip_warnings: HashSet<ErasedContextId>,
|
|
||||||
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, RoundedClipPrograms>,
|
|
||||||
pub(crate) overview_scrims: HashMap<OutputId, OverviewScrim>,
|
|
||||||
pub(crate) material_programs: HashMap<ErasedContextId, MaterialProgram>,
|
|
||||||
pub(crate) material_buffers: HashMap<WlSurface, MaterialBuffers>,
|
|
||||||
pub(crate) portal_shortcuts: PortalShortcuts,
|
pub(crate) portal_shortcuts: PortalShortcuts,
|
||||||
pub(crate) pending_logout: Option<u32>,
|
pub(crate) pending_logout: Option<u32>,
|
||||||
pub(crate) logout_query: Option<u32>,
|
pub(crate) logout_query: Option<u32>,
|
||||||
pub(crate) logout_owner: Option<ObjectId>,
|
pub(crate) logout_owner: Option<ObjectId>,
|
||||||
pub(crate) blur_programs: HashMap<ErasedContextId, BlurProgram>,
|
|
||||||
pub(crate) backdrop_generation: u64,
|
pub(crate) backdrop_generation: u64,
|
||||||
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
|
||||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||||
focus_swipe: Option<FocusSwipe>,
|
focus_swipe: Option<FocusSwipe>,
|
||||||
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
||||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
|
||||||
viewport_coupled_widths: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
|
||||||
pending_column_width_cycles: HashSet<WindowId>,
|
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
pub(crate) focus_history: ferese_core::FocusHistory,
|
pub(crate) focus_history: ferese_core::FocusHistory,
|
||||||
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
|
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
|
||||||
@@ -376,7 +349,6 @@ pub struct Ferese {
|
|||||||
pub(crate) bindings: Vec<Binding>,
|
pub(crate) bindings: Vec<Binding>,
|
||||||
workspace_auto_back_and_forth: bool,
|
workspace_auto_back_and_forth: bool,
|
||||||
window_rules: Vec<WindowRule>,
|
window_rules: Vec<WindowRule>,
|
||||||
window_rules_applied: HashSet<WindowId>,
|
|
||||||
pub(crate) theme_settings: ThemeSettings,
|
pub(crate) theme_settings: ThemeSettings,
|
||||||
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
@@ -411,7 +383,6 @@ pub struct Ferese {
|
|||||||
pub(crate) shell_snapshot_serial: u32,
|
pub(crate) shell_snapshot_serial: u32,
|
||||||
pub(crate) last_shell_snapshot: Option<ShellSnapshot>,
|
pub(crate) last_shell_snapshot: Option<ShellSnapshot>,
|
||||||
pub(crate) overview: crate::overview::OverviewState,
|
pub(crate) overview: crate::overview::OverviewState,
|
||||||
next_window_id: u64,
|
|
||||||
next_output_id: u64,
|
next_output_id: u64,
|
||||||
last_animation_tick: Instant,
|
last_animation_tick: Instant,
|
||||||
pub popups: PopupManager,
|
pub popups: PopupManager,
|
||||||
@@ -559,49 +530,25 @@ impl Ferese {
|
|||||||
output_workspaces: OutputWorkspaceMap::default(),
|
output_workspaces: OutputWorkspaceMap::default(),
|
||||||
output_ids: HashMap::new(),
|
output_ids: HashMap::new(),
|
||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
windows: WindowRegistry::default(),
|
||||||
window_geometry: HashMap::new(),
|
render: Default::default(),
|
||||||
resize_transactions: HashMap::new(),
|
|
||||||
resize_snapshots: HashMap::new(),
|
|
||||||
nested_backend: None,
|
nested_backend: None,
|
||||||
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
|
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
|
||||||
maximized_windows: HashSet::new(),
|
|
||||||
maximized_column_widths: HashMap::new(),
|
|
||||||
window_stack: WindowStack::default(),
|
window_stack: WindowStack::default(),
|
||||||
floating_above_fullscreen: HashMap::new(),
|
floating_above_fullscreen: HashMap::new(),
|
||||||
natural_floating_pending: HashSet::new(),
|
|
||||||
floating_memory: crate::floating::Memory::path()
|
floating_memory: crate::floating::Memory::path()
|
||||||
.map(|p| crate::floating::Memory::load(&p))
|
.map(|p| crate::floating::Memory::load(&p))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
floating_save_worker: Default::default(),
|
floating_save_worker: Default::default(),
|
||||||
floating_window_memory: HashMap::new(),
|
|
||||||
floating_placement_anchors: HashMap::new(),
|
|
||||||
floating_cascade: Default::default(),
|
floating_cascade: Default::default(),
|
||||||
floating_resize_anchors: HashMap::new(),
|
|
||||||
window_borders: HashMap::new(),
|
|
||||||
window_dims: HashMap::new(),
|
|
||||||
window_resize_fills: HashMap::new(),
|
|
||||||
window_dimming: HashMap::new(),
|
|
||||||
window_focus: HashMap::new(),
|
|
||||||
window_shadows: HashMap::new(),
|
|
||||||
rounded_clip_programs: HashMap::new(),
|
|
||||||
rounded_clip_warnings: HashSet::new(),
|
|
||||||
overview_scrims: HashMap::new(),
|
|
||||||
material_programs: HashMap::new(),
|
|
||||||
material_buffers: HashMap::new(),
|
|
||||||
portal_shortcuts: PortalShortcuts::default(),
|
portal_shortcuts: PortalShortcuts::default(),
|
||||||
pending_logout: None,
|
pending_logout: None,
|
||||||
logout_query: None,
|
logout_query: None,
|
||||||
logout_owner: None,
|
logout_owner: None,
|
||||||
blur_programs: HashMap::new(),
|
|
||||||
backdrop_generation: 0,
|
backdrop_generation: 0,
|
||||||
closing_windows: HashMap::new(),
|
|
||||||
viewport_animations: HashMap::new(),
|
viewport_animations: HashMap::new(),
|
||||||
focus_swipe: None,
|
focus_swipe: None,
|
||||||
workspace_slides: HashMap::new(),
|
workspace_slides: HashMap::new(),
|
||||||
scrolling_world_x: HashMap::new(),
|
|
||||||
viewport_coupled_widths: HashMap::new(),
|
|
||||||
pending_column_width_cycles: HashSet::new(),
|
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
focus_history: Default::default(),
|
focus_history: Default::default(),
|
||||||
focus_cycle: None,
|
focus_cycle: None,
|
||||||
@@ -611,7 +558,6 @@ impl Ferese {
|
|||||||
bindings: config.bindings,
|
bindings: config.bindings,
|
||||||
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
|
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
|
||||||
window_rules: config.window_rules,
|
window_rules: config.window_rules,
|
||||||
window_rules_applied: HashSet::new(),
|
|
||||||
theme_settings: config.theme_settings,
|
theme_settings: config.theme_settings,
|
||||||
inactive_dim: config.inactive_dim,
|
inactive_dim: config.inactive_dim,
|
||||||
animations_enabled: config.animations_enabled,
|
animations_enabled: config.animations_enabled,
|
||||||
@@ -641,7 +587,6 @@ impl Ferese {
|
|||||||
shell_snapshot_serial: 0,
|
shell_snapshot_serial: 0,
|
||||||
last_shell_snapshot: None,
|
last_shell_snapshot: None,
|
||||||
overview: OverviewState::with_font_family(config.overview_font_family),
|
overview: OverviewState::with_font_family(config.overview_font_family),
|
||||||
next_window_id: 1,
|
|
||||||
next_output_id: 1,
|
next_output_id: 1,
|
||||||
last_animation_tick: start_time,
|
last_animation_tick: start_time,
|
||||||
popups: PopupManager::default(),
|
popups: PopupManager::default(),
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.focus_swipe = None;
|
self.focus_swipe = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible_workspaces = if self.viewport_animations.is_empty() {
|
let visible_workspaces = if self.viewport_animations.is_empty() {
|
||||||
HashSet::new()
|
HashSet::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -38,10 +39,11 @@ impl Ferese {
|
|||||||
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
let windows = self
|
let windows = self
|
||||||
.space
|
.space
|
||||||
.elements()
|
.elements()
|
||||||
.filter_map(|window| self.window_ids.get(window).copied().map(|id| (window.clone(), id)))
|
.filter_map(|window| self.windows.ids().get(window).copied().map(|id| (window.clone(), id)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut active_animation = false;
|
let mut active_animation = false;
|
||||||
let mut completed_slides = Vec::new();
|
let mut completed_slides = Vec::new();
|
||||||
@@ -49,16 +51,19 @@ impl Ferese {
|
|||||||
if slide.held_progress.is_some() {
|
if slide.held_progress.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if slide.advance(delta) {
|
if slide.advance(delta) {
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
} else {
|
} else {
|
||||||
completed_slides.push(*output);
|
completed_slides.push(*output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
|
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
|
||||||
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
|
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
|
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
|
||||||
let active = motion.advance_visual(0.0, delta, duration);
|
let active = motion.advance_visual(0.0, delta, duration);
|
||||||
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
|
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
|
||||||
@@ -75,69 +80,72 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dim_changed = false;
|
let mut dim_changed = false;
|
||||||
// Include hidden workspace windows: overview can present them too.
|
// Include hidden workspace windows: overview can present them too.
|
||||||
// Read selection once so these immutable fields can be borrowed alongside
|
// Read selection once so these immutable fields can be borrowed alongside
|
||||||
// window_focus without allocating a temporary window-ID vector.
|
// each record without allocating a temporary window-ID vector.
|
||||||
let selected_window = if self.overview.is_active() {
|
let selected_window = if self.overview.is_active() {
|
||||||
self.overview.selected()
|
self.overview.selected()
|
||||||
} else {
|
} else {
|
||||||
self.focused_window
|
self.focused_window
|
||||||
};
|
};
|
||||||
for id in self.window_ids.values().copied() {
|
|
||||||
|
for (&id, record) in self.windows.records_mut() {
|
||||||
let selected = selected_window == Some(id);
|
let selected = selected_window == Some(id);
|
||||||
let target = if selected { 1.0 } else { 0.0 };
|
let target = if selected { 1.0 } else { 0.0 };
|
||||||
let focus = self
|
let focus = record
|
||||||
.window_focus
|
.focus
|
||||||
.entry(id)
|
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
|
||||||
let previous = focus.current;
|
let previous = focus.current;
|
||||||
active_animation |= focus.advance_visual(target, delta, duration);
|
active_animation |= focus.advance_visual(target, delta, duration);
|
||||||
dim_changed |= previous != focus.current;
|
dim_changed |= previous != focus.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_, id) in &windows {
|
for (_, id) in &windows {
|
||||||
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
|
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
|
||||||
let dim = self
|
let Some(record) = self.windows.record_mut(*id) else {
|
||||||
.window_dimming
|
continue;
|
||||||
.entry(*id)
|
};
|
||||||
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
|
||||||
|
let dim = record
|
||||||
|
.dimming
|
||||||
|
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
let previous = dim.current;
|
let previous = dim.current;
|
||||||
active_animation |= dim.advance_visual(target, delta, duration);
|
active_animation |= dim.advance_visual(target, delta, duration);
|
||||||
dim_changed |= previous != dim.current;
|
dim_changed |= previous != dim.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ready_to_close = Vec::new();
|
let mut ready_to_close = Vec::new();
|
||||||
for (id, animation) in &mut self.closing_windows {
|
for (id, record) in self.windows.records_mut() {
|
||||||
|
let Some(animation) = &mut record.closing else { continue };
|
||||||
|
|
||||||
if animation.advance(delta, self.animations_enabled) {
|
if animation.advance(delta, self.animations_enabled) {
|
||||||
ready_to_close.push(*id);
|
ready_to_close.push(*id);
|
||||||
} else if !animation.close_sent {
|
} else if !animation.close_sent {
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in ready_to_close {
|
for id in ready_to_close {
|
||||||
self.send_window_close(id);
|
self.send_window_close(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = self.start_time.elapsed();
|
let now = self.start_time.elapsed();
|
||||||
self.resize_transactions.retain(|id, transaction| {
|
self.windows.expire_transactions(now);
|
||||||
if transaction.expired(now) {
|
|
||||||
tracing::warn!(?id, "resize presentation deadline reached");
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let blocked_workspaces = self
|
let blocked_workspaces = self
|
||||||
.resize_transactions
|
.windows
|
||||||
.keys()
|
.resizing()
|
||||||
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
let animations_enabled = self.animations_enabled;
|
let animations_enabled = self.animations_enabled;
|
||||||
let animation_speed = self.animation_speed;
|
let animation_speed = self.animation_speed;
|
||||||
self.resize_snapshots.retain(|id, snapshot| {
|
self.render.retain_snapshots(|id, snapshot| {
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let waiting_for_client = self
|
let waiting_for_client = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace_for_window(*id)
|
.workspace_for_window(*id)
|
||||||
@@ -145,7 +153,7 @@ impl Ferese {
|
|||||||
// A shrinking client's destination buffer arrives before the
|
// A shrinking client's destination buffer arrives before the
|
||||||
// animated bounds reach it. Keep the old native pixels covering
|
// animated bounds reach it. Keep the old native pixels covering
|
||||||
// that strip rather than fading them into the neutral resize fill.
|
// that strip rather than fading them into the neutral resize fill.
|
||||||
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
|
let uncovered = self.windows.geometry(id).is_some_and(|geometry| {
|
||||||
geometry.client.committed_size.is_some_and(|size| {
|
geometry.client.committed_size.is_some_and(|size| {
|
||||||
crate::presentation::resize_needs_old_frame(
|
crate::presentation::resize_needs_old_frame(
|
||||||
geometry.visual.current,
|
geometry.visual.current,
|
||||||
@@ -166,21 +174,24 @@ impl Ferese {
|
|||||||
if !blocked {
|
if !blocked {
|
||||||
snapshot.commit.increment();
|
snapshot.commit.increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !active {
|
if !active {
|
||||||
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
|
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
|
||||||
}
|
}
|
||||||
active
|
active
|
||||||
});
|
});
|
||||||
active_animation |= !self.resize_snapshots.is_empty();
|
active_animation |= self.render.snapshots().next().is_some();
|
||||||
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
||||||
active_animation |= !blocked_workspaces.is_empty();
|
active_animation |= !blocked_workspaces.is_empty();
|
||||||
for (workspace, viewport) in &mut self.viewport_animations {
|
for (workspace, viewport) in &mut self.viewport_animations {
|
||||||
if !visible_workspaces.contains(workspace) {
|
if !visible_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked_workspaces.contains(workspace) {
|
if blocked_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
||||||
viewport.current = swipe.position();
|
viewport.current = swipe.position();
|
||||||
viewport.velocity = 0.0;
|
viewport.velocity = 0.0;
|
||||||
@@ -200,12 +211,19 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
|
||||||
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let natural_pending = record.natural_floating_pending;
|
||||||
|
let Some(geometry) = record.geometry.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let zooming = geometry.is_zooming();
|
let zooming = geometry.is_zooming();
|
||||||
|
|
||||||
let coupled_target = self.viewport_coupled_widths.get(&id).map(|(_, width)| width.target);
|
let coupled_target = record.coupled_width.as_ref().map(|(_, width)| width.target);
|
||||||
if coupled_target.is_some() {
|
if coupled_target.is_some() {
|
||||||
geometry.visual.target.width = geometry.visual.current.width;
|
geometry.visual.target.width = geometry.visual.current.width;
|
||||||
geometry.visual.velocity.width = 0.0;
|
geometry.visual.velocity.width = 0.0;
|
||||||
@@ -214,7 +232,8 @@ impl Ferese {
|
|||||||
if let Some(target) = coupled_target {
|
if let Some(target) = coupled_target {
|
||||||
geometry.visual.target.width = target;
|
geometry.visual.target.width = target;
|
||||||
}
|
}
|
||||||
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
|
||||||
|
if let Some((workspace, world_x)) = record.world_x.as_mut()
|
||||||
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
||||||
{
|
{
|
||||||
if zooming {
|
if zooming {
|
||||||
@@ -225,12 +244,14 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
world_x.snap();
|
world_x.snap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !zooming {
|
if !zooming {
|
||||||
geometry.visual.current.x = world_x.current - viewport.current;
|
geometry.visual.current.x = world_x.current - viewport.current;
|
||||||
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
|
|
||||||
|
if let Some((_, width)) = record.coupled_width.as_mut() {
|
||||||
let width_active = if self.animations_enabled {
|
let width_active = if self.animations_enabled {
|
||||||
width.advance(delta, self.viewport_spring_config)
|
width.advance(delta, self.viewport_spring_config)
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +265,7 @@ impl Ferese {
|
|||||||
settled_coupled_widths.push(id);
|
settled_coupled_widths.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(size) = geometry.client.expire_wait(now) {
|
if let Some(size) = geometry.client.expire_wait(now) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
?id,
|
?id,
|
||||||
@@ -253,8 +275,9 @@ impl Ferese {
|
|||||||
"client did not commit the final configured size within 500 ms"
|
"client did not commit the final configured size within 500 ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(size) = geometry.presentation_size_request(now)
|
if let Some(size) = geometry.presentation_size_request(now)
|
||||||
&& !self.natural_floating_pending.contains(&id)
|
&& !natural_pending
|
||||||
&& let Some(toplevel) = window.toplevel()
|
&& let Some(toplevel) = window.toplevel()
|
||||||
{
|
{
|
||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
@@ -262,20 +285,25 @@ impl Ferese {
|
|||||||
});
|
});
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
|
|
||||||
let visual = geometry.visual.current;
|
let visual = geometry.visual.current;
|
||||||
self.space
|
self.space
|
||||||
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
|
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in settled_coupled_widths {
|
for id in settled_coupled_widths {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.windows.update(id, |record| record.coupled_width = None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !completed_slides.is_empty() {
|
if !completed_slides.is_empty() {
|
||||||
for output in completed_slides {
|
for output in completed_slides {
|
||||||
self.workspace_slides.remove(&output);
|
self.workspace_slides.remove(&output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible = self.visible_workspace_ids();
|
let visible = self.visible_workspace_ids();
|
||||||
let visible_windows = self
|
let visible_windows = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.values()
|
.values()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|id| {
|
.filter(|id| {
|
||||||
@@ -308,19 +336,15 @@ impl Ferese {
|
|||||||
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
|
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
|
||||||
let Some(animation) = self.closing_windows.get(&id) else {
|
self.workspace_slide_offset_at(window, Duration::ZERO)
|
||||||
return (1.0, 1.0);
|
|
||||||
};
|
|
||||||
let eased = smoothstep(animation.progress);
|
|
||||||
|
|
||||||
(1.0 - eased * 0.02, (1.0 - eased) as f32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
|
pub(super) fn workspace_slide_offset_at(&self, window: WindowId, delta: Duration) -> (f64, f64) {
|
||||||
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(output_id) = self
|
let Some(output_id) = self
|
||||||
.workspace_slides
|
.workspace_slides
|
||||||
.iter()
|
.iter()
|
||||||
@@ -329,9 +353,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(size) = self
|
let Some(size) = self
|
||||||
.output_ids
|
.output_ids
|
||||||
.iter()
|
.iter()
|
||||||
@@ -341,6 +367,12 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut slide = slide.clone();
|
||||||
|
if slide.held_progress.is_none() && !delta.is_zero() {
|
||||||
|
slide.advance(delta);
|
||||||
|
}
|
||||||
|
|
||||||
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
|
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ impl Ferese {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self
|
if let Some(handle) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
|
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
|
||||||
{
|
{
|
||||||
@@ -75,7 +76,7 @@ impl Ferese {
|
|||||||
.workspace_for_window(window)
|
.workspace_for_window(window)
|
||||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
||||||
let enabled = fullscreen || !self.maximized_windows.contains(&window);
|
let enabled = fullscreen || !self.windows.record(window).is_some_and(|w| w.maximized);
|
||||||
self.set_window_maximized(window, enabled);
|
self.set_window_maximized(window, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +86,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
if enabled {
|
if enabled {
|
||||||
let _ = self.workspaces.set_fullscreen(window, false);
|
let _ = self.workspaces.set_fullscreen(window, false);
|
||||||
self.maximized_windows.insert(window);
|
self.windows.update(window, |w| w.maximized = true);
|
||||||
} else {
|
} else {
|
||||||
self.maximized_windows.remove(&window);
|
self.windows.update(window, |w| w.maximized = false);
|
||||||
}
|
}
|
||||||
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
||||||
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
||||||
@@ -96,10 +97,16 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
if enabled {
|
if enabled {
|
||||||
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
|
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
|
||||||
self.maximized_column_widths.entry(window).or_insert(column.width);
|
self.windows.update(window, |w| {
|
||||||
|
w.maximized_column_width.get_or_insert(column.width);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
|
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
|
||||||
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
|
} else if let Some(width) = self
|
||||||
|
.windows
|
||||||
|
.update(window, |w| w.maximized_column_width.take())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
let _ = layout.set_column_width(window, width);
|
let _ = layout.set_column_width(window, width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +115,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
||||||
!self.overview.is_presenting()
|
self.output_has_fullscreen_for_frame(output, self.overview.is_presenting())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_has_fullscreen_for_frame(&self, output: &Output, overview: bool) -> bool {
|
||||||
|
!overview
|
||||||
&& self
|
&& self
|
||||||
.output_id(output)
|
.output_id(output)
|
||||||
.and_then(|id| self.output_workspaces.active_workspace(id))
|
.and_then(|id| self.output_workspaces.active_workspace(id))
|
||||||
@@ -127,7 +138,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_floating_window(&self, window: &Window) -> bool {
|
pub fn is_floating_window(&self, window: &Window) -> bool {
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.get(window)
|
.get(window)
|
||||||
.and_then(|id| self.workspaces.placement(*id))
|
.and_then(|id| self.workspaces.placement(*id))
|
||||||
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
|
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
|
||||||
@@ -139,10 +151,10 @@ impl Ferese {
|
|||||||
location: Point<i32, Logical>,
|
location: Point<i32, Logical>,
|
||||||
size: Size<i32, Logical>,
|
size: Size<i32, Logical>,
|
||||||
) {
|
) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.floating_resize_anchors.remove(&id);
|
self.windows.update(id, |w| w.resize_anchor.take()).flatten();
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
location.x as f64,
|
location.x as f64,
|
||||||
location.y as f64,
|
location.y as f64,
|
||||||
@@ -157,12 +169,15 @@ impl Ferese {
|
|||||||
|
|
||||||
// Direct manipulation follows the pointer, including while the client is
|
// Direct manipulation follows the pointer, including while the client is
|
||||||
// still drawing its next buffer. Rendering and hit testing share this rect.
|
// still drawing its next buffer. Rendering and hit testing share this rect.
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.resize_snapshots.remove(&id);
|
self.render.clear_snapshot(&id);
|
||||||
let geometry = self
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
.window_geometry
|
return;
|
||||||
.entry(id)
|
};
|
||||||
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
let geometry = record
|
||||||
|
.geometry
|
||||||
|
.get_or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
||||||
|
|
||||||
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
||||||
&& let Some(toplevel) = window.toplevel()
|
&& let Some(toplevel) = window.toplevel()
|
||||||
{
|
{
|
||||||
@@ -171,8 +186,8 @@ impl Ferese {
|
|||||||
});
|
});
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
self.scrolling_world_x.remove(&id);
|
self.windows.update(id, |record| record.world_x = None);
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.windows.update(id, |record| record.coupled_width = None);
|
||||||
self.space.map_element(window.clone(), location, false);
|
self.space.map_element(window.clone(), location, false);
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
||||||
@@ -212,7 +227,7 @@ impl Ferese {
|
|||||||
self.space
|
self.space
|
||||||
.elements()
|
.elements()
|
||||||
.filter_map(|window| {
|
.filter_map(|window| {
|
||||||
let id = *self.window_ids.get(window)?;
|
let id = *self.windows.ids().get(window)?;
|
||||||
if Some(id) == exclude {
|
if Some(id) == exclude {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -255,7 +270,7 @@ impl Ferese {
|
|||||||
let parent_rect = parent
|
let parent_rect = parent
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|surface| {
|
.and_then(|surface| {
|
||||||
self.window_ids.iter().find_map(|(window, id)| {
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|top| top.wl_surface() == surface)
|
.is_some_and(|top| top.wl_surface() == surface)
|
||||||
@@ -266,7 +281,7 @@ impl Ferese {
|
|||||||
.and_then(|rect| intersection(rect, work));
|
.and_then(|rect| intersection(rect, work));
|
||||||
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
|
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
|
||||||
let remembered = id
|
let remembered = id
|
||||||
.and_then(|id| self.floating_window_memory.get(&id))
|
.and_then(|id| self.windows.record(id).and_then(|w| w.floating_memory.as_ref()))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
if parent.is_none() {
|
if parent.is_none() {
|
||||||
app.as_deref().and_then(|app| self.floating_memory.get(app))
|
app.as_deref().and_then(|app| self.floating_memory.get(app))
|
||||||
@@ -317,7 +332,7 @@ impl Ferese {
|
|||||||
.map(|(_, r)| r)
|
.map(|(_, r)| r)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let anchor = id
|
let anchor = id
|
||||||
.and_then(|id| self.floating_placement_anchors.get(&id).copied())
|
.and_then(|id| self.windows.record(id).and_then(|w| w.placement_anchor))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
self.focused_window
|
self.focused_window
|
||||||
.filter(|focused| Some(*focused) != id)
|
.filter(|focused| Some(*focused) != id)
|
||||||
@@ -337,7 +352,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remember_floating(&mut self, window: &Window) {
|
pub(crate) fn remember_floating(&mut self, window: &Window) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
||||||
@@ -360,7 +375,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let entry = crate::floating::Remembered::new(output, rect, work);
|
let entry = crate::floating::Remembered::new(output, rect, work);
|
||||||
self.floating_window_memory.insert(id, entry.clone());
|
self.windows.update(id, |w| w.floating_memory = Some(entry.clone()));
|
||||||
if let Some(app) = app {
|
if let Some(app) = app {
|
||||||
self.floating_memory.save(app, entry);
|
self.floating_memory.save(app, entry);
|
||||||
}
|
}
|
||||||
@@ -377,7 +392,7 @@ impl Ferese {
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
raw: Rect,
|
raw: Rect,
|
||||||
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
|
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
|
||||||
let id = self.window_ids.get(window).copied();
|
let id = self.windows.ids().get(window).copied();
|
||||||
let work = self
|
let work = self
|
||||||
.floating_outputs()
|
.floating_outputs()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -2,22 +2,23 @@ use super::*;
|
|||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
pub(super) fn focus_candidates(&self) -> Vec<WindowId> {
|
pub(super) fn focus_candidates(&self) -> Vec<WindowId> {
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
self.workspaces.workspace_for_window(*id)?;
|
self.workspaces.workspace_for_window(*id)?;
|
||||||
(window.toplevel().is_some()
|
(window.toplevel().is_some()
|
||||||
&& self.window_content_ready(window)
|
&& self.window_content_ready(window)
|
||||||
&& self.window_geometry.contains_key(id)
|
&& self.windows.geometry(id).is_some()
|
||||||
&& self.output_workspaces.focused_output().is_some()
|
&& self.output_workspaces.focused_output().is_some()
|
||||||
&& !self.closing_windows.contains_key(id))
|
&& !self.windows.record(*id).is_some_and(|record| record.closing.is_some()))
|
||||||
.then_some(*id)
|
.then_some(*id)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn focus_preview_output(&self, id: WindowId) -> Option<OutputId> {
|
pub(crate) fn focus_preview_output(&self, id: WindowId) -> Option<OutputId> {
|
||||||
if self.closing_windows.contains_key(&id) {
|
if self.windows.record(id).is_some_and(|record| record.closing.is_some()) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
self.workspaces
|
self.workspaces
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ impl Ferese {
|
|||||||
|
|
||||||
let workspace = self.workspace_under_pointer(position)?;
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
self.space.elements().rev().find_map(|window| {
|
self.space.elements().rev().find_map(|window| {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
|
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
|
||||||
self.maximized_windows.contains(&id)
|
self.windows.record(id).is_some_and(|w| w.maximized)
|
||||||
|| self
|
|| self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace(workspace)
|
.workspace(workspace)
|
||||||
@@ -137,7 +137,7 @@ impl Ferese {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -166,9 +166,9 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if overview_active {
|
if overview_active {
|
||||||
let mut candidates = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
let mut candidates = self.windows.ids().keys().cloned().collect::<Vec<_>>();
|
||||||
// Match Overview's front-to-back render order during overlapping motion.
|
// Match Overview's front-to-back render order during overlapping motion.
|
||||||
candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0)));
|
candidates.sort_by_key(|window| std::cmp::Reverse(self.windows.ids().get(window).map_or(0, |id| id.0)));
|
||||||
candidates.iter().find_map(hit)
|
candidates.iter().find_map(hit)
|
||||||
} else {
|
} else {
|
||||||
self.space.elements().rev().find_map(hit)
|
self.space.elements().rev().find_map(hit)
|
||||||
@@ -199,8 +199,8 @@ impl Ferese {
|
|||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
return Some((1.0, 1.0));
|
return Some((1.0, 1.0));
|
||||||
}
|
}
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let geometry = self.window_geometry.get(id)?;
|
let geometry = self.windows.geometry(id)?;
|
||||||
let source = geometry.client.committed_size?;
|
let source = geometry.client.committed_size?;
|
||||||
let presented = self.presented_window_rect(*id)?;
|
let presented = self.presented_window_rect(*id)?;
|
||||||
|
|
||||||
@@ -215,7 +215,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
|
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let rect = self.presented_window_rect(*id)?;
|
let rect = self.presented_window_rect(*id)?;
|
||||||
let size = ClientSize::from_rect(rect);
|
let size = ClientSize::from_rect(rect);
|
||||||
Some(Rectangle::new(
|
Some(Rectangle::new(
|
||||||
@@ -227,9 +227,10 @@ impl Ferese {
|
|||||||
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
|
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
|
||||||
window_has_buffer(window)
|
window_has_buffer(window)
|
||||||
&& self
|
&& self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(window)
|
.get(window)
|
||||||
.and_then(|id| self.window_geometry.get(id))
|
.and_then(|id| self.windows.geometry(id))
|
||||||
.is_some_and(|geometry| geometry.client.committed_size.is_some())
|
.is_some_and(|geometry| geometry.client.committed_size.is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,16 +26,25 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
|
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for (window, id) in &self.window_ids {
|
let windows = self
|
||||||
if visible.contains(id) {
|
.windows
|
||||||
|
.ids()
|
||||||
|
.iter()
|
||||||
|
.map(|(window, id)| (window.clone(), *id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (window, id) in windows {
|
||||||
|
if visible.contains(&id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let was_mapped = self.space.element_location(window).is_some();
|
|
||||||
|
let was_mapped = self.space.element_location(&window).is_some();
|
||||||
changed |= was_mapped;
|
changed |= was_mapped;
|
||||||
if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) {
|
if was_mapped && let Some(geometry) = self.windows.geometry_mut(&id) {
|
||||||
geometry.settle_presentation();
|
geometry.settle_presentation();
|
||||||
}
|
}
|
||||||
self.space.unmap_elem(window);
|
|
||||||
|
self.space.unmap_elem(&window);
|
||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
@@ -51,8 +60,8 @@ impl Ferese {
|
|||||||
// large first animation step after a keypress on an idle desktop.
|
// large first animation step after a keypress on an idle desktop.
|
||||||
self.advance_animations(Instant::now());
|
self.advance_animations(Instant::now());
|
||||||
self.arrange_layers();
|
self.arrange_layers();
|
||||||
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
|
let mut previous_scrolling_world_x = self.windows.take_world_positions();
|
||||||
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
|
let pending_column_width_cycles = self.windows.take_column_width_requests();
|
||||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
let constraints = self.window_constraints();
|
let constraints = self.window_constraints();
|
||||||
let mut visible = HashSet::new();
|
let mut visible = HashSet::new();
|
||||||
@@ -63,6 +72,7 @@ impl Ferese {
|
|||||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
|
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -80,13 +90,16 @@ impl Ferese {
|
|||||||
let Some(bounds) = self.output_bounds_for(&output) else {
|
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
||||||
for layer in layer_map_for_output(&output).layers() {
|
for layer in layer_map_for_output(&output).layers() {
|
||||||
crate::handlers::set_surface_tree_output(layer.wl_surface(), &output);
|
crate::handlers::set_surface_tree_output(layer.wl_surface(), &output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let workspace_fullscreen = workspace.fullscreen;
|
let workspace_fullscreen = workspace.fullscreen;
|
||||||
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
|
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
|
||||||
let workspace_focus = workspace.last_focused;
|
let workspace_focus = workspace.last_focused;
|
||||||
@@ -97,6 +110,7 @@ impl Ferese {
|
|||||||
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let layout =
|
let layout =
|
||||||
match workspace
|
match workspace
|
||||||
.layout
|
.layout
|
||||||
@@ -134,15 +148,17 @@ impl Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (window, id) in &self.window_ids {
|
for (window, id) in self.windows.ids() {
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(toplevel) = window.toplevel() {
|
if let Some(toplevel) = window.toplevel() {
|
||||||
crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output);
|
crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
|
let is_maximized =
|
||||||
|
self.windows.record(*id).is_some_and(|w| w.maximized) && workspace_fullscreen != Some(*id);
|
||||||
let rect = if workspace_fullscreen == Some(*id) {
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
fullscreen_bounds
|
fullscreen_bounds
|
||||||
} else if is_maximized && !is_scrolling_layout {
|
} else if is_maximized && !is_scrolling_layout {
|
||||||
@@ -166,6 +182,7 @@ impl Ferese {
|
|||||||
None => continue,
|
None => continue,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_fullscreen = workspace_fullscreen == Some(*id);
|
let is_fullscreen = workspace_fullscreen == Some(*id);
|
||||||
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
|
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
|
||||||
|
|
||||||
@@ -195,19 +212,25 @@ impl Ferese {
|
|||||||
|
|
||||||
let now = self.start_time.elapsed();
|
let now = self.start_time.elapsed();
|
||||||
|
|
||||||
let mut scrolling_world_x = HashMap::new();
|
|
||||||
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
||||||
|
|
||||||
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements {
|
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements {
|
||||||
let was_mapped = self.space.element_location(&window).is_some();
|
let was_mapped = self.space.element_location(&window).is_some();
|
||||||
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// A viewport-coupled width must never override fullscreen/floating geometry.
|
// A viewport-coupled width must never override fullscreen/floating geometry.
|
||||||
if scrolling.is_none() {
|
if scrolling.is_none() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
let (geometry, had_geometry) = match self.window_geometry.entry(id) {
|
|
||||||
Entry::Occupied(entry) => (entry.into_mut(), true),
|
let natural_pending = record.natural_floating_pending && is_floating && !is_fullscreen && !is_maximized;
|
||||||
Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false),
|
let had_geometry = record.geometry.is_some();
|
||||||
};
|
let geometry = record
|
||||||
|
.geometry
|
||||||
|
.get_or_insert_with(|| WindowGeometry::new(rect, client_size(&window)));
|
||||||
|
|
||||||
let mode = if is_fullscreen {
|
let mode = if is_fullscreen {
|
||||||
PresentationMode::Fullscreen
|
PresentationMode::Fullscreen
|
||||||
} else if is_maximized {
|
} else if is_maximized {
|
||||||
@@ -220,13 +243,16 @@ impl Ferese {
|
|||||||
if !was_mapped && (had_geometry || is_fullscreen) {
|
if !was_mapped && (had_geometry || is_fullscreen) {
|
||||||
geometry.settle_presentation();
|
geometry.settle_presentation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||||
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
if geometry.is_zooming() {
|
if geometry.is_zooming() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
||||||
let restored_world_x =
|
let restored_world_x =
|
||||||
restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x);
|
restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x);
|
||||||
@@ -239,23 +265,23 @@ impl Ferese {
|
|||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
animated_world_x.snap();
|
animated_world_x.snap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !geometry.is_zooming() {
|
if !geometry.is_zooming() {
|
||||||
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
||||||
geometry.visual.velocity.x = animated_world_x.velocity;
|
geometry.visual.velocity.x = animated_world_x.velocity;
|
||||||
}
|
}
|
||||||
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
record.world_x = Some((workspace, animated_world_x));
|
||||||
|
|
||||||
let coupled = !geometry.is_zooming()
|
let coupled = !geometry.is_zooming()
|
||||||
&& (couple_width
|
&& (couple_width
|
||||||
|| self
|
|| record
|
||||||
.viewport_coupled_widths
|
.coupled_width
|
||||||
.get(&id)
|
.as_ref()
|
||||||
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace));
|
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace));
|
||||||
if coupled {
|
if coupled {
|
||||||
let width = self
|
let width = record
|
||||||
.viewport_coupled_widths
|
.coupled_width
|
||||||
.entry(id)
|
.get_or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width)));
|
||||||
.or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width)));
|
|
||||||
if width.0 != workspace {
|
if width.0 != workspace {
|
||||||
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
|
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
|
||||||
}
|
}
|
||||||
@@ -266,15 +292,15 @@ impl Ferese {
|
|||||||
geometry.visual.current.width = width.1.current;
|
geometry.visual.current.width = width.1.current;
|
||||||
geometry.visual.velocity.width = width.1.velocity;
|
geometry.visual.velocity.width = width.1.velocity;
|
||||||
} else if pending_column_width_cycles.contains(&id) {
|
} else if pending_column_width_cycles.contains(&id) {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visual = geometry.visual.current;
|
let visual = geometry.visual.current;
|
||||||
let natural_pending =
|
|
||||||
self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized;
|
|
||||||
if natural_pending {
|
if natural_pending {
|
||||||
requested_size = None;
|
requested_size = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
||||||
|
|
||||||
self.space.map_element(window.clone(), location, false);
|
self.space.map_element(window.clone(), location, false);
|
||||||
@@ -297,6 +323,7 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
state.states.unset(xdg_toplevel::State::Maximized)
|
state.states.unset(xdg_toplevel::State::Maximized)
|
||||||
};
|
};
|
||||||
|
|
||||||
let tiled = !is_floating && !is_fullscreen;
|
let tiled = !is_floating && !is_fullscreen;
|
||||||
let tiled_changed = if tiled {
|
let tiled_changed = if tiled {
|
||||||
state.states.set(xdg_toplevel::State::TiledLeft)
|
state.states.set(xdg_toplevel::State::TiledLeft)
|
||||||
@@ -319,7 +346,7 @@ impl Ferese {
|
|||||||
&& had_geometry
|
&& had_geometry
|
||||||
&& self.animations_enabled
|
&& self.animations_enabled
|
||||||
{
|
{
|
||||||
self.resize_transactions.insert(
|
self.windows.set_transaction(
|
||||||
id,
|
id,
|
||||||
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
||||||
.with_source_geometry(window.geometry()),
|
.with_source_geometry(window.geometry()),
|
||||||
@@ -327,10 +354,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.scrolling_world_x = scrolling_world_x;
|
|
||||||
if layout_changed {
|
if layout_changed {
|
||||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
self.retarget_overview();
|
self.retarget_overview();
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
@@ -339,9 +367,10 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||||
if let Some(id) = self.window_ids.get(window) {
|
if let Some(id) = self.windows.ids().get(window) {
|
||||||
self.window_stack.raise(*id);
|
self.window_stack.raise(*id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.raise_element(window, activate);
|
self.space.raise_element(window, activate);
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
}
|
}
|
||||||
@@ -349,8 +378,8 @@ impl Ferese {
|
|||||||
pub(crate) fn sync_window_stacking(&mut self) {
|
pub(crate) fn sync_window_stacking(&mut self) {
|
||||||
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
||||||
windows.sort_by_key(|window| {
|
windows.sort_by_key(|window| {
|
||||||
let id = self.window_ids.get(window).copied();
|
let id = self.windows.ids().get(window).copied();
|
||||||
let geometry = id.and_then(|id| self.window_geometry.get(&id));
|
let geometry = id.and_then(|id| self.windows.geometry(&id));
|
||||||
let floating =
|
let floating =
|
||||||
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
|
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
|
||||||
let priority = crate::stacking::layer_priority(
|
let priority = crate::stacking::layer_priority(
|
||||||
@@ -372,6 +401,7 @@ impl Ferese {
|
|||||||
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for window in windows {
|
for window in windows {
|
||||||
self.space.raise_element(&window, false);
|
self.space.raise_element(&window, false);
|
||||||
}
|
}
|
||||||
@@ -387,7 +417,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let toplevel = window.toplevel()?;
|
let toplevel = window.toplevel()?;
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ impl Ferese {
|
|||||||
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
|
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
|
||||||
}
|
}
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
||||||
else {
|
else {
|
||||||
@@ -277,7 +278,7 @@ impl Ferese {
|
|||||||
|
|
||||||
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
|
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
self.pending_column_width_cycles.insert(window);
|
self.windows.update(window, |w| w.column_width_pending = true);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
@@ -306,7 +307,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
||||||
else {
|
else {
|
||||||
@@ -560,7 +562,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let surface = self.focused_window.and_then(|focused| {
|
let surface = self.focused_window.and_then(|focused| {
|
||||||
self.window_ids.iter().find_map(|(window, id)| {
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
(*id == focused)
|
(*id == focused)
|
||||||
.then(|| window.toplevel())
|
.then(|| window.toplevel())
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.workspace_slides.remove(&output_id);
|
self.workspace_slides.remove(&output_id);
|
||||||
self.overview_scrims.remove(&output_id);
|
self.render.remove_output(output_id);
|
||||||
self.pending_screencopies.retain(|capture| {
|
self.pending_screencopies.retain(|capture| {
|
||||||
if capture.output == *output {
|
if capture.output == *output {
|
||||||
capture.fail();
|
capture.fail();
|
||||||
@@ -82,7 +82,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
|
pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
|
||||||
let floats = self
|
let floats = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let workspace = self.workspaces.workspace_for_window(*id)?;
|
let workspace = self.workspaces.workspace_for_window(*id)?;
|
||||||
|
|||||||
+116
-126
@@ -1,49 +1,19 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Rendering samples an isolated forecast. Restore authoritative state before
|
/// Output-local presentation values. Sampling never changes authoritative
|
||||||
/// returning to input, client commits, or another monitor's render deadline.
|
/// geometry, configure barriers, animation clocks, or another output's view.
|
||||||
pub(crate) struct RenderForecast<'a> {
|
pub(crate) struct FrameScene {
|
||||||
state: &'a mut Ferese,
|
pub windows: HashMap<WindowId, WindowFrame>,
|
||||||
geometry: Vec<(WindowId, WindowGeometry)>,
|
pub overview: crate::overview::OverviewMotion,
|
||||||
slides: HashMap<OutputId, WorkspaceSlide>,
|
|
||||||
closing: HashMap<WindowId, ClosingAnimation>,
|
|
||||||
focus: HashMap<WindowId, crate::dimming::DimAnimation>,
|
|
||||||
dimming: HashMap<WindowId, crate::dimming::DimAnimation>,
|
|
||||||
overview: Option<crate::overview::OverviewAnimationSnapshot>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::ops::Deref for RenderForecast<'_> {
|
pub(crate) struct WindowFrame {
|
||||||
type Target = Ferese;
|
pub geometry: WindowGeometry,
|
||||||
|
pub rect: Rect,
|
||||||
fn deref(&self) -> &Ferese {
|
pub close_scale: f64,
|
||||||
self.state
|
pub close_alpha: f32,
|
||||||
}
|
pub focus: f64,
|
||||||
}
|
pub dim: f64,
|
||||||
|
|
||||||
impl std::ops::DerefMut for RenderForecast<'_> {
|
|
||||||
fn deref_mut(&mut self) -> &mut Ferese {
|
|
||||||
self.state
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for RenderForecast<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if self.overview.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (id, geometry) in self.geometry.drain(..) {
|
|
||||||
self.state.window_geometry.insert(id, geometry);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.state.workspace_slides = std::mem::take(&mut self.slides);
|
|
||||||
self.state.closing_windows = std::mem::take(&mut self.closing);
|
|
||||||
self.state.window_focus = std::mem::take(&mut self.focus);
|
|
||||||
self.state.window_dimming = std::mem::take(&mut self.dimming);
|
|
||||||
if let Some(overview) = self.overview.take() {
|
|
||||||
self.state.overview.restore_prediction(overview);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Ferese {
|
impl Ferese {
|
||||||
@@ -60,21 +30,24 @@ impl Ferese {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window_geometry.iter().any(|(id, geometry)| {
|
self.windows.records().any(|(id, record)| {
|
||||||
|
let Some(mut geometry) = record.geometry else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
if !self.window_belongs_to_output(*id, output) {
|
if !self.window_belongs_to_output(*id, output) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.resize_transactions.contains_key(id)
|
if record.resize.is_some()
|
||||||
|| self.resize_snapshots.contains_key(id)
|
|| self.render.snapshot(id).is_some()
|
||||||
|| self.closing_windows.get(id).is_some_and(|close| !close.close_sent)
|
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|
||||||
|| self.window_focus.get(id).is_some_and(|focus| focus.is_animating())
|
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
||||||
|| self.window_dimming.get(id).is_some_and(|dim| dim.is_animating())
|
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut geometry = *geometry;
|
|
||||||
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
|
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -90,104 +63,104 @@ impl Ferese {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|swipe| swipe.workspace == workspace);
|
.is_some_and(|swipe| swipe.workspace == workspace);
|
||||||
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
|
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
}) || self.viewport_coupled_widths.get(id).is_some_and(|(_, width)| {
|
}) || record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
||||||
let mut width = *width;
|
let mut width = *width;
|
||||||
width.advance(Duration::ZERO, self.viewport_spring_config)
|
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn forecast_render(&mut self, output: &Output, horizon: Duration) -> RenderForecast<'_> {
|
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
|
||||||
if horizon.is_zero() || !self.animations_enabled || !self.output_has_animations(output) {
|
let delta = if self.animations_enabled && self.output_has_animations(output) {
|
||||||
return RenderForecast {
|
|
||||||
state: self,
|
|
||||||
geometry: Vec::new(),
|
|
||||||
slides: HashMap::new(),
|
|
||||||
closing: HashMap::new(),
|
|
||||||
focus: HashMap::new(),
|
|
||||||
dimming: HashMap::new(),
|
|
||||||
overview: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let delta = if self.animations_enabled {
|
|
||||||
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
|
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
|
||||||
} else {
|
} else {
|
||||||
Duration::ZERO
|
Duration::ZERO
|
||||||
};
|
};
|
||||||
|
|
||||||
let geometry = self
|
let overview = self.overview.sample(delta, self.spring_config);
|
||||||
.window_geometry
|
|
||||||
.iter()
|
|
||||||
.filter(|(id, _)| self.window_belongs_to_output(**id, output))
|
|
||||||
.map(|(id, geometry)| (*id, *geometry))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let slides = self.workspace_slides.clone();
|
|
||||||
let closing = self.closing_windows.clone();
|
|
||||||
let focus = self.window_focus.clone();
|
|
||||||
let dimming = self.window_dimming.clone();
|
|
||||||
let overview = self.overview.predict(delta, self.spring_config);
|
|
||||||
let blocked = self
|
let blocked = self
|
||||||
.resize_transactions
|
.windows
|
||||||
.keys()
|
.resizing()
|
||||||
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
for (id, original) in &geometry {
|
let mut windows = HashMap::new();
|
||||||
let workspace = self.workspaces.workspace_for_window(*id);
|
|
||||||
if workspace.is_some_and(|workspace| blocked.contains(&workspace)) {
|
for (&id, record) in self.windows.records() {
|
||||||
|
let Some(original) = record.geometry else { continue };
|
||||||
|
|
||||||
|
if !self.window_belongs_to_output(id, output) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let world = self.scrolling_world_x.get(id).and_then(|(workspace, world)| {
|
let workspace = self.workspaces.workspace_for_window(id);
|
||||||
|
let world = record.world_x.as_ref().and_then(|(workspace, world)| {
|
||||||
let viewport = self.viewport_animations.get(workspace)?;
|
let viewport = self.viewport_animations.get(workspace)?;
|
||||||
let held = self
|
let held = self
|
||||||
.focus_swipe
|
.focus_swipe
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|swipe| swipe.workspace == *workspace);
|
.is_some_and(|swipe| swipe.workspace == *workspace);
|
||||||
|
|
||||||
Some((*world, *viewport, held))
|
Some((*world, *viewport, held))
|
||||||
});
|
});
|
||||||
|
let width = record.coupled_width.as_ref().map(|(_, width)| *width);
|
||||||
|
let geometry = if delta.is_zero() || workspace.is_some_and(|workspace| blocked.contains(&workspace)) {
|
||||||
|
original
|
||||||
|
} else {
|
||||||
|
predict_geometry(
|
||||||
|
original,
|
||||||
|
delta,
|
||||||
|
self.spring_config,
|
||||||
|
self.viewport_spring_config,
|
||||||
|
world,
|
||||||
|
width,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
let width = self.viewport_coupled_widths.get(id).map(|(_, width)| *width);
|
let mut rect = overview.presented_rect(id, geometry.visual.current);
|
||||||
let predicted = predict_geometry(
|
if !overview.is_presenting() {
|
||||||
*original,
|
let (x, y) = self.workspace_slide_offset_at(id, delta);
|
||||||
delta,
|
rect.x += x;
|
||||||
self.spring_config,
|
rect.y += y;
|
||||||
self.viewport_spring_config,
|
}
|
||||||
world,
|
|
||||||
width,
|
let mut close = record.closing.as_ref().copied().unwrap_or_default();
|
||||||
|
if record.closing.is_some() && !delta.is_zero() {
|
||||||
|
close.advance(delta, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let eased = smoothstep(close.progress);
|
||||||
|
let focused = if self.overview.is_active() {
|
||||||
|
self.overview_selected(id)
|
||||||
|
} else {
|
||||||
|
self.focused_window == Some(id)
|
||||||
|
};
|
||||||
|
|
||||||
|
let sample_dim = |motion: Option<&DimAnimation>, fallback| {
|
||||||
|
let Some(mut motion) = motion.cloned() else {
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !delta.is_zero() {
|
||||||
|
motion.predict(delta, self.inactive_dim.duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
motion.current
|
||||||
|
};
|
||||||
|
|
||||||
|
windows.insert(
|
||||||
|
id,
|
||||||
|
WindowFrame {
|
||||||
|
geometry,
|
||||||
|
rect,
|
||||||
|
close_scale: 1.0 - eased * 0.02,
|
||||||
|
close_alpha: (1.0 - eased) as f32,
|
||||||
|
focus: sample_dim(record.focus.as_ref(), if focused { 1.0 } else { 0.0 }),
|
||||||
|
dim: sample_dim(record.dimming.as_ref(), 0.0),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
self.window_geometry.insert(*id, predicted);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for slide in self
|
FrameScene { windows, overview }
|
||||||
.workspace_slides
|
|
||||||
.values_mut()
|
|
||||||
.filter(|slide| slide.held_progress.is_none())
|
|
||||||
{
|
|
||||||
slide.advance(delta);
|
|
||||||
}
|
|
||||||
|
|
||||||
for close in self.closing_windows.values_mut() {
|
|
||||||
close.advance(delta, self.animations_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
for focus in self.window_focus.values_mut() {
|
|
||||||
focus.predict(delta, self.inactive_dim.duration_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
for dim in self.window_dimming.values_mut() {
|
|
||||||
dim.predict(delta, self.inactive_dim.duration_ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
RenderForecast {
|
|
||||||
state: self,
|
|
||||||
geometry,
|
|
||||||
slides,
|
|
||||||
closing,
|
|
||||||
focus,
|
|
||||||
dimming,
|
|
||||||
overview: Some(overview),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +208,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||||
fn render_forecast_restores_compositor_state_between_output_deadlines() {
|
fn frame_sampling_preserves_compositor_state_between_output_deadlines() {
|
||||||
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||||
assert!(
|
assert!(
|
||||||
runtime.starts_with(std::env::temp_dir()),
|
runtime.starts_with(std::env::temp_dir()),
|
||||||
@@ -269,7 +242,14 @@ mod tests {
|
|||||||
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
|
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
|
||||||
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
|
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
|
||||||
geometry.visual.set_target(Rect::new(600., 0., 400., 300.));
|
geometry.visual.set_target(Rect::new(600., 0., 400., 300.));
|
||||||
state.window_geometry.insert(id, geometry);
|
// This test isolates frame sampling from the protocol client fixture.
|
||||||
|
state.windows.records.insert(
|
||||||
|
id,
|
||||||
|
super::super::window_registry::WindowRecord {
|
||||||
|
geometry: Some(geometry),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
let tick = state.last_animation_tick;
|
let tick = state.last_animation_tick;
|
||||||
for horizon in [
|
for horizon in [
|
||||||
Duration::from_millis(16),
|
Duration::from_millis(16),
|
||||||
@@ -277,17 +257,27 @@ mod tests {
|
|||||||
Duration::from_millis(8),
|
Duration::from_millis(8),
|
||||||
] {
|
] {
|
||||||
{
|
{
|
||||||
let forecast = state.forecast_render(&output, horizon);
|
let forecast = state.sample_frame(&output, horizon);
|
||||||
assert!(forecast.window_geometry[&id].visual.current.x > geometry.visual.current.x);
|
assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(state.window_geometry[&id], geometry);
|
assert_eq!(*state.windows.geometry(&id).unwrap(), geometry);
|
||||||
assert_eq!(state.last_animation_tick, tick);
|
assert_eq!(state.last_animation_tick, tick);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A prediction must neither advance nor expire a client resize barrier.
|
||||||
|
state.windows.set_transaction(
|
||||||
|
id,
|
||||||
|
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
|
||||||
|
);
|
||||||
|
let blocked = state.sample_frame(&output, Duration::from_millis(16));
|
||||||
|
assert_eq!(blocked.windows[&id].geometry, geometry);
|
||||||
|
assert!(state.windows.transaction(&id).is_some());
|
||||||
|
state.windows.clear_transaction(&id);
|
||||||
|
|
||||||
state.animations_enabled = false;
|
state.animations_enabled = false;
|
||||||
let forecast = state.forecast_render(&output, Duration::from_millis(16));
|
let forecast = state.sample_frame(&output, Duration::from_millis(16));
|
||||||
assert_eq!(forecast.window_geometry[&id], geometry);
|
assert_eq!(forecast.windows[&id].geometry, geometry);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
use ferese_animation::{AnimatedValue, WindowGeometry};
|
||||||
|
use ferese_core::WorkspaceId;
|
||||||
|
use ferese_layout::{ColumnWidth, Rect, WindowId};
|
||||||
|
|
||||||
|
use crate::resize_transaction::ResizeTransaction;
|
||||||
|
|
||||||
|
/// Metadata with exactly the lifetime of a managed window. Layouts and focus
|
||||||
|
/// history reference its ID; they do not own or duplicate this metadata.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct WindowRecord {
|
||||||
|
pub geometry: Option<WindowGeometry>,
|
||||||
|
pub resize: Option<ResizeTransaction>,
|
||||||
|
pub focus: Option<crate::dimming::DimAnimation>,
|
||||||
|
pub dimming: Option<crate::dimming::DimAnimation>,
|
||||||
|
pub(super) closing: Option<super::ClosingAnimation>,
|
||||||
|
pub world_x: Option<(WorkspaceId, AnimatedValue)>,
|
||||||
|
pub coupled_width: Option<(WorkspaceId, AnimatedValue)>,
|
||||||
|
pub maximized: bool,
|
||||||
|
pub maximized_column_width: Option<ColumnWidth>,
|
||||||
|
pub natural_floating_pending: bool,
|
||||||
|
pub floating_memory: Option<crate::floating::Remembered>,
|
||||||
|
pub placement_anchor: Option<Rect>,
|
||||||
|
pub resize_anchor: Option<(bool, bool, Rect)>,
|
||||||
|
pub column_width_pending: bool,
|
||||||
|
pub rules_applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handle index is read-only outside this owner. Registering and removing a
|
||||||
|
/// handle always updates its record as well. IDs are never reused on remapping.
|
||||||
|
pub(crate) struct WindowRegistry<W> {
|
||||||
|
ids: HashMap<W, WindowId>,
|
||||||
|
pub(super) records: HashMap<WindowId, WindowRecord>,
|
||||||
|
next_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> Default for WindowRegistry<W> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: HashMap::new(),
|
||||||
|
records: HashMap::new(),
|
||||||
|
next_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Eq + Hash> WindowRegistry<W> {
|
||||||
|
pub fn allocate_id(&mut self) -> WindowId {
|
||||||
|
let id = WindowId(self.next_id);
|
||||||
|
self.next_id = self.next_id.checked_add(1).expect("window IDs exhausted");
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&mut self, window: W, id: WindowId) {
|
||||||
|
assert!(!self.ids.contains_key(&window), "window already registered");
|
||||||
|
assert!(!self.records.contains_key(&id), "window ID already registered");
|
||||||
|
|
||||||
|
self.ids.insert(window, id);
|
||||||
|
self.records.insert(id, WindowRecord::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> &HashMap<W, WindowId> {
|
||||||
|
&self.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record(&self, id: WindowId) -> Option<&WindowRecord> {
|
||||||
|
self.records.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_mut(&mut self, id: WindowId) -> Option<&mut WindowRecord> {
|
||||||
|
self.records.get_mut(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn records_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut WindowRecord)> {
|
||||||
|
self.records.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn records(&self) -> impl Iterator<Item = (&WindowId, &WindowRecord)> {
|
||||||
|
self.records.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_world_positions(&mut self) -> HashMap<WindowId, (WorkspaceId, AnimatedValue)> {
|
||||||
|
self.records
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|(id, record)| Some((*id, record.world_x.take()?)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry(&self, id: &WindowId) -> Option<&WindowGeometry> {
|
||||||
|
self.records.get(id)?.geometry.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry_mut(&mut self, id: &WindowId) -> Option<&mut WindowGeometry> {
|
||||||
|
self.records.get_mut(id)?.geometry.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometries(&self) -> impl Iterator<Item = (&WindowId, &WindowGeometry)> {
|
||||||
|
self.records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, record)| Some((id, record.geometry.as_ref()?)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_geometry(&mut self, id: WindowId, geometry: WindowGeometry) {
|
||||||
|
self.update(id, |record| record.geometry = Some(geometry));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transaction(&self, id: &WindowId) -> Option<&ResizeTransaction> {
|
||||||
|
self.records.get(id)?.resize.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) {
|
||||||
|
self.update(id, |record| record.resize = Some(transaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_transaction(&mut self, id: &WindowId) {
|
||||||
|
self.update(*id, |record| record.resize = None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resizing(&self) -> impl Iterator<Item = &WindowId> {
|
||||||
|
self.records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, record)| record.resize.is_some().then_some(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expire_transactions(&mut self, now: std::time::Duration) {
|
||||||
|
for (id, record) in &mut self.records {
|
||||||
|
if record.resize.is_some_and(|transaction| transaction.expired(now)) {
|
||||||
|
tracing::warn!(?id, "resize presentation deadline reached");
|
||||||
|
record.resize = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update<R>(&mut self, id: WindowId, update: impl FnOnce(&mut WindowRecord) -> R) -> Option<R> {
|
||||||
|
self.records.get_mut(&id).map(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, window: &W) -> Option<WindowId> {
|
||||||
|
let id = self.ids.remove(window)?;
|
||||||
|
self.records.remove(&id);
|
||||||
|
|
||||||
|
Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_column_width_requests(&mut self) -> HashSet<WindowId> {
|
||||||
|
self.records
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|(id, record)| std::mem::take(&mut record.column_width_pending).then_some(*id))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let id = windows.allocate_id();
|
||||||
|
windows.register("surface", id);
|
||||||
|
windows.update(id, |record| {
|
||||||
|
record.maximized = true;
|
||||||
|
record.resize_anchor = Some((true, false, Rect::new(0., 0., 800., 600.)));
|
||||||
|
record.column_width_pending = true;
|
||||||
|
record.rules_applied = true;
|
||||||
|
record.focus = Some(crate::dimming::DimAnimation::new(1.0));
|
||||||
|
record.closing = Some(super::super::ClosingAnimation::default());
|
||||||
|
record.world_x = Some((WorkspaceId(1), AnimatedValue::new(50.0)));
|
||||||
|
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(800.0)));
|
||||||
|
});
|
||||||
|
let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None);
|
||||||
|
let resize = ResizeTransaction::new(12.into(), std::time::Duration::ZERO);
|
||||||
|
windows.set_geometry(id, geometry);
|
||||||
|
windows.set_transaction(id, resize);
|
||||||
|
|
||||||
|
assert_eq!(windows.remove(&"surface"), Some(id));
|
||||||
|
assert_eq!(windows.remove(&"surface"), None);
|
||||||
|
assert!(windows.record(id).is_none());
|
||||||
|
assert!(windows.update(id, |record| record.maximized = true).is_none());
|
||||||
|
|
||||||
|
windows.set_geometry(id, geometry);
|
||||||
|
windows.set_transaction(id, resize);
|
||||||
|
|
||||||
|
assert!(windows.geometry(&id).is_none());
|
||||||
|
assert!(windows.transaction(&id).is_none());
|
||||||
|
assert_eq!(windows.resizing().count(), 0);
|
||||||
|
assert!(windows.ids().is_empty());
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remap_has_new_identity_and_does_not_inherit_old_requests() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let old = windows.allocate_id();
|
||||||
|
windows.register("surface", old);
|
||||||
|
windows.update(old, |record| record.rules_applied = true);
|
||||||
|
windows.remove(&"surface");
|
||||||
|
|
||||||
|
let new = windows.allocate_id();
|
||||||
|
windows.register("surface", new);
|
||||||
|
assert_ne!(old, new);
|
||||||
|
assert!(
|
||||||
|
windows
|
||||||
|
.update(old, |record| record.column_width_pending = true)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(!windows.record(new).unwrap().rules_applied);
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_width_changes_are_consumed_once_and_survive_other_window_removal() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let first = windows.allocate_id();
|
||||||
|
let second = windows.allocate_id();
|
||||||
|
windows.register("first", first);
|
||||||
|
windows.register("second", second);
|
||||||
|
windows.update(first, |record| record.column_width_pending = true);
|
||||||
|
windows.update(second, |record| record.column_width_pending = true);
|
||||||
|
|
||||||
|
windows.remove(&"first");
|
||||||
|
|
||||||
|
assert_eq!(windows.take_column_width_requests(), HashSet::from([second]));
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
assert!(windows.record(second).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expired_resize_releases_only_its_barrier_and_preserves_geometry() {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let first = windows.allocate_id();
|
||||||
|
let second = windows.allocate_id();
|
||||||
|
windows.register("first", first);
|
||||||
|
windows.register("second", second);
|
||||||
|
let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None);
|
||||||
|
windows.set_geometry(first, geometry);
|
||||||
|
windows.set_transaction(first, ResizeTransaction::new(1.into(), Duration::ZERO));
|
||||||
|
windows.set_transaction(second, ResizeTransaction::new(2.into(), Duration::from_millis(100)));
|
||||||
|
|
||||||
|
windows.expire_transactions(Duration::from_millis(300));
|
||||||
|
|
||||||
|
assert_eq!(windows.geometry(&first), Some(&geometry));
|
||||||
|
assert!(windows.transaction(&first).is_none());
|
||||||
|
assert!(windows.transaction(&second).unwrap().accepts(Some(2.into())));
|
||||||
|
assert_eq!(windows.resizing().copied().collect::<Vec<_>>(), vec![second]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outputs = self.floating_outputs();
|
let outputs = self.floating_outputs();
|
||||||
let Some((remembered, work)) = app_id
|
let Some((remembered, work)) = app_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -65,13 +66,16 @@ impl Ferese {
|
|||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
self.add_transient_window(window, parent);
|
self.add_transient_window(window, parent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
self.focus_output_at(pointer.current_location());
|
self.focus_output_at(pointer.current_location());
|
||||||
}
|
}
|
||||||
|
|
||||||
let remembered_work = app_id
|
let remembered_work = app_id
|
||||||
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
||||||
.and_then(|app| self.floating_memory.get(app))
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
@@ -86,12 +90,13 @@ impl Ferese {
|
|||||||
if let Some(work) = remembered_work {
|
if let Some(work) = remembered_work {
|
||||||
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
|
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(bounds) = self.output_bounds() else {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let id = WindowId(self.next_window_id);
|
|
||||||
self.next_window_id += 1;
|
let id = self.windows.allocate_id();
|
||||||
let size_rect = client_size(&window)
|
let size_rect = client_size(&window)
|
||||||
.map(|size| natural_floating_rect(bounds, size))
|
.map(|size| natural_floating_rect(bounds, size))
|
||||||
.unwrap_or_else(|| centered_floating_rect(bounds));
|
.unwrap_or_else(|| centered_floating_rect(bounds));
|
||||||
@@ -100,7 +105,6 @@ impl Ferese {
|
|||||||
.focused_window
|
.focused_window
|
||||||
.and_then(|focused| self.presented_window_rect(focused))
|
.and_then(|focused| self.presented_window_rect(focused))
|
||||||
.unwrap_or(bounds);
|
.unwrap_or(bounds);
|
||||||
self.floating_placement_anchors.insert(id, anchor);
|
|
||||||
let fullscreen = self.workspaces.active().fullscreen;
|
let fullscreen = self.workspaces.active().fullscreen;
|
||||||
let focus = true;
|
let focus = true;
|
||||||
if let Err(error) = self
|
if let Err(error) = self
|
||||||
@@ -110,25 +114,30 @@ impl Ferese {
|
|||||||
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
|
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.windows.register(window.clone(), id);
|
||||||
|
self.windows.update(id, |w| w.placement_anchor = Some(anchor));
|
||||||
|
|
||||||
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
|
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
|
||||||
{
|
{
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
self.window_ids.insert(window.clone(), id);
|
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if let Some(fullscreen) = fullscreen {
|
if let Some(fullscreen) = fullscreen {
|
||||||
self.floating_above_fullscreen.insert(id, fullscreen);
|
self.floating_above_fullscreen.insert(id, fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus);
|
self.space.map_element(window, (0, 0), focus);
|
||||||
// apply_initial_window_rules performs the first relayout/configure.
|
// apply_initial_window_rules performs the first relayout/configure.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_tiled_window(&mut self, window: Window) {
|
pub fn add_tiled_window(&mut self, window: Window) {
|
||||||
let id = WindowId(self.next_window_id);
|
let id = self.windows.allocate_id();
|
||||||
self.next_window_id += 1;
|
|
||||||
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
||||||
|
|
||||||
let axis = self
|
let axis = self
|
||||||
@@ -147,11 +156,12 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.windows.register(window.clone(), id);
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if focus_new_window {
|
if focus_new_window {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus_new_window);
|
self.space.map_element(window, (0, 0), focus_new_window);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
|
|
||||||
@@ -165,18 +175,19 @@ impl Ferese {
|
|||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
let size_rect = client_size(&window)
|
let size_rect = client_size(&window)
|
||||||
.map(|s| natural_floating_rect(bounds, s))
|
.map(|s| natural_floating_rect(bounds, s))
|
||||||
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
||||||
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
||||||
let id = WindowId(self.next_window_id);
|
let id = self.windows.allocate_id();
|
||||||
self.next_window_id += 1;
|
|
||||||
|
|
||||||
let focus = workspace == self.workspaces.active_id()
|
let focus = workspace == self.workspaces.active_id()
|
||||||
&& (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent));
|
&& (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent));
|
||||||
@@ -185,11 +196,12 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.windows.register(window.clone(), id);
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if client_size(&window).is_none() {
|
if client_size(&window).is_none() {
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace(workspace)
|
.workspace(workspace)
|
||||||
@@ -197,9 +209,11 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.floating_above_fullscreen.insert(id, parent);
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus);
|
self.space.map_element(window, (0, 0), focus);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
if focus {
|
if focus {
|
||||||
@@ -214,10 +228,15 @@ impl Ferese {
|
|||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
transient: bool,
|
transient: bool,
|
||||||
) {
|
) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !self.window_rules_applied.insert(id) {
|
|
||||||
|
if !self
|
||||||
|
.windows
|
||||||
|
.update(id, |w| !std::mem::replace(&mut w.rules_applied, true))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,11 +245,12 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
|
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
|
||||||
let windows = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
let windows = self.windows.ids().keys().cloned().collect::<Vec<_>>();
|
||||||
for window in windows {
|
for window in windows {
|
||||||
let Some(toplevel) = window.toplevel() else {
|
let Some(toplevel) = window.toplevel() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
||||||
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
||||||
(
|
(
|
||||||
@@ -248,15 +268,18 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) {
|
pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(id) else {
|
let Some(bounds) = self.floating_bounds_for_window(id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if rule == Default::default() {
|
if rule == Default::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let axis = self
|
let axis = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.active()
|
.active()
|
||||||
@@ -269,6 +292,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule");
|
tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule");
|
||||||
}
|
}
|
||||||
|
|
||||||
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
|
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
|
||||||
|
|
||||||
let should_float = rule
|
let should_float = rule
|
||||||
@@ -287,7 +311,7 @@ impl Ferese {
|
|||||||
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
|
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
|
||||||
@@ -309,6 +333,7 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
tracing::warn!(%error, ?id, "failed to apply floating window rule");
|
tracing::warn!(%error, ?id, "failed to apply floating window rule");
|
||||||
}
|
}
|
||||||
@@ -331,16 +356,19 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) {
|
pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) {
|
if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) {
|
||||||
tracing::warn!(?id, ?parent, "ignored transient parent on another workspace");
|
tracing::warn!(?id, ?parent, "ignored transient parent on another workspace");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
let size_rect = client_size(window)
|
let size_rect = client_size(window)
|
||||||
@@ -369,6 +397,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.floating_above_fullscreen.insert(id, parent);
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.focused_window == Some(parent) {
|
if self.focused_window == Some(parent) {
|
||||||
if let Err(error) = self.workspaces.focus_window(id) {
|
if let Err(error) = self.workspaces.focus_window(id) {
|
||||||
tracing::warn!(%error, ?id, "failed to focus transient window");
|
tracing::warn!(%error, ?id, "failed to focus transient window");
|
||||||
@@ -385,35 +414,18 @@ impl Ferese {
|
|||||||
pub fn remove_tiled_window(&mut self, window: &Window) {
|
pub fn remove_tiled_window(&mut self, window: &Window) {
|
||||||
// Uncommitted toplevels have not entered a workspace yet.
|
// Uncommitted toplevels have not entered a workspace yet.
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
let Some(id) = self.window_ids.remove(window) else {
|
let Some(id) = self.windows.remove(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.focus_history.remove(id);
|
self.focus_history.remove(id);
|
||||||
self.window_geometry.remove(&id);
|
self.render.remove_window(id);
|
||||||
self.resize_transactions.remove(&id);
|
|
||||||
self.resize_snapshots.remove(&id);
|
|
||||||
self.window_resize_fills.remove(&id);
|
|
||||||
self.maximized_windows.remove(&id);
|
|
||||||
self.maximized_column_widths.remove(&id);
|
|
||||||
self.window_stack.remove(id);
|
self.window_stack.remove(id);
|
||||||
self.floating_above_fullscreen.remove(&id);
|
self.floating_above_fullscreen.remove(&id);
|
||||||
self.natural_floating_pending.remove(&id);
|
|
||||||
self.floating_window_memory.remove(&id);
|
|
||||||
self.floating_placement_anchors.remove(&id);
|
|
||||||
self.floating_resize_anchors.remove(&id);
|
|
||||||
self.window_borders.remove(&id);
|
|
||||||
self.window_dims.remove(&id);
|
|
||||||
self.window_dimming.remove(&id);
|
|
||||||
self.window_focus.remove(&id);
|
|
||||||
self.window_shadows.remove(&id);
|
|
||||||
self.closing_windows.remove(&id);
|
|
||||||
self.window_rules_applied.remove(&id);
|
|
||||||
self.scrolling_world_x.remove(&id);
|
|
||||||
self.viewport_coupled_widths.remove(&id);
|
|
||||||
self.pending_column_width_cycles.remove(&id);
|
|
||||||
if let Err(error) = self.workspaces.remove_window(id) {
|
if let Err(error) = self.workspaces.remove_window(id) {
|
||||||
tracing::error!(%error, ?id, "failed to remove window from layout");
|
tracing::error!(%error, ?id, "failed to remove window from layout");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.focused_window == Some(id) {
|
if self.focused_window == Some(id) {
|
||||||
self.focused_window = self.workspaces.active().last_focused;
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
}
|
}
|
||||||
@@ -423,7 +435,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
||||||
let Some((window, id)) = self
|
let Some((window, id)) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(window, _)| {
|
.find(|(window, _)| {
|
||||||
window
|
window
|
||||||
@@ -434,9 +447,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(transaction) = self.resize_transactions.get(&id) else {
|
|
||||||
|
let Some(transaction) = self.windows.transaction(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let serial = with_states(surface, |states| {
|
let serial = with_states(surface, |states| {
|
||||||
states
|
states
|
||||||
.data_map
|
.data_map
|
||||||
@@ -446,6 +461,7 @@ impl Ferese {
|
|||||||
if !transaction.accepts(serial) {
|
if !transaction.accepts(serial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(output) = self
|
let Some(output) = self
|
||||||
.space
|
.space
|
||||||
.outputs()
|
.outputs()
|
||||||
@@ -454,10 +470,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry());
|
let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry());
|
||||||
let scale = output.current_scale().fractional_scale();
|
let scale = output.current_scale().fractional_scale();
|
||||||
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
||||||
if self.resize_snapshots.get(&id).is_some_and(|snapshot| {
|
if self.render.snapshot(&id).is_some_and(|snapshot| {
|
||||||
crate::presentation::snapshot_covers_source(
|
crate::presentation::snapshot_covers_source(
|
||||||
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
||||||
source_size,
|
source_size,
|
||||||
@@ -467,7 +484,8 @@ impl Ferese {
|
|||||||
}) {
|
}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let used: usize = self.resize_snapshots.values().map(|snapshot| snapshot.bytes()).sum();
|
|
||||||
|
let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum();
|
||||||
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||||
let result = if let Some(backend) = &self.nested_backend {
|
let result = if let Some(backend) = &self.nested_backend {
|
||||||
// Commit dispatch does not run inside the Winit event callback;
|
// Commit dispatch does not run inside the Winit event callback;
|
||||||
@@ -490,7 +508,7 @@ impl Ferese {
|
|||||||
match result {
|
match result {
|
||||||
Ok(Some(snapshot)) => {
|
Ok(Some(snapshot)) => {
|
||||||
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
|
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
|
||||||
self.resize_snapshots.insert(id, snapshot);
|
self.render.set_snapshot(id, snapshot);
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -500,7 +518,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_client_commit(&mut self, window: &Window) {
|
pub fn record_client_commit(&mut self, window: &Window) {
|
||||||
let Some(&id) = self.window_ids.get(window) else {
|
let Some(&id) = self.windows.ids().get(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// current_serial is promoted by Smithay on commit, unlike
|
// current_serial is promoted by Smithay on commit, unlike
|
||||||
@@ -514,28 +532,30 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
if self
|
if self
|
||||||
.resize_transactions
|
.windows
|
||||||
.get(&id)
|
.transaction(&id)
|
||||||
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
||||||
{
|
{
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(size) = client_size(window) else {
|
let Some(size) = client_size(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let fullscreen = self
|
let fullscreen = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace_for_window(id)
|
.workspace_for_window(id)
|
||||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||||
let normal_geometry = !fullscreen
|
let normal_geometry = !fullscreen
|
||||||
&& !self.maximized_windows.contains(&id)
|
&& !self.windows.record(id).is_some_and(|w| w.maximized)
|
||||||
&& self
|
&& self.windows.geometry(&id).is_none_or(|geometry| !geometry.is_zooming());
|
||||||
.window_geometry
|
let initial_floating = self
|
||||||
.get(&id)
|
.windows
|
||||||
.is_none_or(|geometry| !geometry.is_zooming());
|
.update(id, |w| std::mem::take(&mut w.natural_floating_pending))
|
||||||
let initial_floating =
|
.unwrap_or(false)
|
||||||
self.natural_floating_pending.remove(&id) || self.floating_placement_anchors.contains_key(&id);
|
|| self.windows.record(id).is_some_and(|w| w.placement_anchor.is_some());
|
||||||
if initial_floating
|
if initial_floating
|
||||||
&& normal_geometry
|
&& normal_geometry
|
||||||
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
||||||
@@ -548,12 +568,12 @@ impl Ferese {
|
|||||||
(f64::from(size.width), f64::from(size.height)),
|
(f64::from(size.width), f64::from(size.height)),
|
||||||
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
|
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
|
||||||
);
|
);
|
||||||
self.floating_placement_anchors.remove(&id);
|
self.windows.update(id, |w| w.placement_anchor = None);
|
||||||
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
// This is the first mapped client buffer, not a user resize.
|
// This is the first mapped client buffer, not a user resize.
|
||||||
// Do not animate from the temporary placement box.
|
// Do not animate from the temporary placement box.
|
||||||
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -561,7 +581,7 @@ impl Ferese {
|
|||||||
// terminal cell grids, or a dialog changing its contents). Its committed
|
// terminal cell grids, or a dialog changing its contents). Its committed
|
||||||
// window geometry is authoritative once the latest configure is committed.
|
// window geometry is authoritative once the latest configure is committed.
|
||||||
// Never let an old buffer undo a newer resize or a fullscreen transition.
|
// Never let an old buffer undo a newer resize or a fullscreen transition.
|
||||||
self.floating_placement_anchors.remove(&id);
|
self.windows.update(id, |w| w.placement_anchor = None);
|
||||||
let settled_configure = window.toplevel().is_some_and(|toplevel| {
|
let settled_configure = window.toplevel().is_some_and(|toplevel| {
|
||||||
with_states(toplevel.wl_surface(), |states| {
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
states.data_map.get::<XdgToplevelSurfaceData>().is_some_and(|data| {
|
states.data_map.get::<XdgToplevelSurfaceData>().is_some_and(|data| {
|
||||||
@@ -581,22 +601,25 @@ impl Ferese {
|
|||||||
&& ClientSize::from_rect(rect) != size
|
&& ClientSize::from_rect(rect) != size
|
||||||
{
|
{
|
||||||
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
|
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
|
||||||
if let Some((left, top, anchor)) = self.floating_resize_anchors.get(&id) {
|
if let Some((left, top, anchor)) = self.windows.record(id).and_then(|w| w.resize_anchor.as_ref()) {
|
||||||
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
|
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
|
||||||
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
|
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
|
||||||
rect = crate::floating::clamp_to_work(rect, work);
|
rect = crate::floating::clamp_to_work(rect, work);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.resize_snapshots.remove(&id);
|
self.render.clear_snapshot(&id);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if settled_configure && self.floating_resize_anchors.remove(&id).is_some() {
|
|
||||||
|
if settled_configure && self.windows.update(id, |w| w.resize_anchor.take()).flatten().is_some() {
|
||||||
self.remember_floating(window);
|
self.remember_floating(window);
|
||||||
}
|
}
|
||||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
|
||||||
|
let Some(geometry) = self.windows.geometry_mut(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -614,17 +637,21 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.closing_windows.entry(focused).or_default();
|
self.windows.update(focused, |record| {
|
||||||
|
record.closing.get_or_insert_default();
|
||||||
|
});
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
if !self.window_ids.values().any(|window_id| *window_id == id) {
|
if !self.windows.ids().values().any(|window_id| *window_id == id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.animations_enabled {
|
if self.animations_enabled {
|
||||||
self.closing_windows.entry(id).or_default();
|
self.windows.update(id, |record| {
|
||||||
|
record.closing.get_or_insert_default();
|
||||||
|
});
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
} else {
|
} else {
|
||||||
self.send_window_close(id);
|
self.send_window_close(id);
|
||||||
@@ -635,7 +662,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(super) fn send_window_close(&self, id: WindowId) {
|
pub(super) fn send_window_close(&self, id: WindowId) {
|
||||||
let Some(toplevel) = self
|
let Some(toplevel) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -229,7 +229,8 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Dur
|
|||||||
let time = state.start_time.elapsed();
|
let time = state.start_time.elapsed();
|
||||||
for window in state.space.elements().filter(|window| {
|
for window in state.space.elements().filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
visible_ids = []
|
||||||
for phase, visible in [
|
for phase, visible in [
|
||||||
("initial-empty", False),
|
("initial-empty", False),
|
||||||
("visible", True),
|
("visible", True),
|
||||||
@@ -110,9 +111,35 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
if time.monotonic() >= deadline:
|
if time.monotonic() >= deadline:
|
||||||
self.fail(f"{phase}: unexpected focused window {focused}")
|
self.fail(f"{phase}: unexpected focused window {focused}")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
if visible:
|
||||||
|
visible_ids.append(focused["id"])
|
||||||
|
if phase == "visible":
|
||||||
|
# Leave a configure/resize outstanding when the buffer is detached.
|
||||||
|
subprocess.run([str(ctl_binary), "toggle-maximized"], env=env,
|
||||||
|
capture_output=True, check=True)
|
||||||
|
|
||||||
client.stdin.write("\n")
|
client.stdin.write("\n")
|
||||||
client.stdin.flush()
|
client.stdin.flush()
|
||||||
|
self.assertNotEqual(visible_ids[0], visible_ids[1], "remapping must allocate a fresh window identity")
|
||||||
self.assertEqual(client.wait(timeout=5), 0)
|
self.assertEqual(client.wait(timeout=5), 0)
|
||||||
|
|
||||||
|
deadline = time.monotonic() + 3
|
||||||
|
while True:
|
||||||
|
remaining = json.loads(subprocess.check_output(
|
||||||
|
[str(ctl_binary), "get-windows"], env=env, text=True
|
||||||
|
))
|
||||||
|
if not remaining:
|
||||||
|
break
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
self.fail(f"destroyed window still managed: {remaining}")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
subprocess.run([str(ctl_binary), "focus-last-window"], env=env,
|
||||||
|
capture_output=True, check=True)
|
||||||
|
focused = json.loads(subprocess.check_output(
|
||||||
|
[str(ctl_binary), "get-focused-window"], env=env, text=True
|
||||||
|
))
|
||||||
|
self.assertIsNone(focused, "focus history must not restore a destroyed window")
|
||||||
finally:
|
finally:
|
||||||
if client and client.poll() is None:
|
if client and client.poll() is None:
|
||||||
client.terminate()
|
client.terminate()
|
||||||
|
|||||||
Reference in new issue
Block a user