Overhaul resize presentation and wallpaper rendering
Coordinate native-pixel resize handoffs, fractional geometry and shared animation timing. Delegate wallpaper rendering to the compositor, advertise DMA-BUF device feedback, and improve callback pacing and render metrics. Handle monitor-local shell surfaces, lid-driven output evacuation and workspace restoration.
This commit is contained in:
1 parent
48ac963ffe
commit
0709484efb
20 files changed
+2093
-319
No files matched your search
Generated
+2
@@ -1576,6 +1576,7 @@ dependencies = [
|
||||
"ferese-ipc",
|
||||
"ferese-layout",
|
||||
"ferese-protocols",
|
||||
"image",
|
||||
"libc",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -2674,6 +2675,7 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png 0.18.1",
|
||||
|
||||
@@ -185,11 +185,26 @@ shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under
|
||||
`[theme.geometry]` adds optional extra space (default 0). Solid and translucent
|
||||
shell surfaces also omit outlines. The dark startup fallback matches the material palette.
|
||||
|
||||
The shell decodes wallpaper in parallel with startup and prefers the lightweight
|
||||
The compositor decodes wallpaper in parallel with startup. Small shell surfaces prefer the lightweight
|
||||
software renderer, with GPU rendering available as a fallback. Set `ICED_BACKEND=wgpu` to explicitly use GPU
|
||||
rendering for shell content. Use release builds when evaluating startup and
|
||||
animation performance; debug image processing is considerably slower.
|
||||
|
||||
Resize presentation configures the destination once and coordinates the
|
||||
matching client commits with the workspace motion (300 ms maximum wait).
|
||||
Application content stays at native pixel size; animated bounds crop/reveal
|
||||
it rather than stretching text. A resize-only previous-frame snapshot fades
|
||||
out over 80 ms, with a 64 MiB total snapshot budget and automatic release.
|
||||
All monitors share one elapsed-time animation clock, and window content,
|
||||
clips, borders and shadows use the same physical-pixel edges.
|
||||
|
||||
Ferese renders `[theme.background]` wallpapers directly: `path` selects the
|
||||
image and `mode = "fill"` or `"fit"` controls crop/letterboxing. One decoded
|
||||
image and one texture per GPU context serve every output; resizing requires
|
||||
no image reload or full-screen shell buffers. The launched shell receives
|
||||
`FERESE_COMPOSITOR_WALLPAPER=1` to disable its redundant wallpaper surfaces.
|
||||
When no valid image path is configured, the shell's existing fallback remains.
|
||||
|
||||
Keyboard and direct-session touchpad settings are applied at startup:
|
||||
|
||||
```toml
|
||||
@@ -245,6 +260,13 @@ transforms are `normal`, `rotate_90`, `rotate_180`, `rotate_270`, `flipped`,
|
||||
and their rotated flipped variants. Output configuration is applied at startup
|
||||
and on hotplug; live configuration reload remains deferred.
|
||||
|
||||
On DRM, closing the laptop lid disables internal panels when a usable external
|
||||
output is available. With no usable external output, the panel stays enabled;
|
||||
Ferese does not change the system's suspend policy. Workspaces are evacuated
|
||||
to a remaining output without merging their windows, and return to their
|
||||
original output when the lid reopens or a disconnected monitor returns.
|
||||
Normal use of an evacuated workspace does not cancel its automatic return.
|
||||
|
||||
Commands are argv arrays and are started directly, without a shell. The
|
||||
default `Super+Enter` binding runs the built-in `terminal = ["foot"]` command:
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ ferese-ipc = { path = "../crates/ferese-ipc" }
|
||||
ferese-layout = { path = "../crates/ferese-layout" }
|
||||
ferese-protocols = { path = "../crates/ferese-protocols", features = ["server"] }
|
||||
libc.workspace = true
|
||||
image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||
smithay.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -12,7 +12,9 @@ use smithay::{
|
||||
Fourcc,
|
||||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||||
},
|
||||
drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface},
|
||||
drm::{
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType,
|
||||
},
|
||||
egl::{EGLContext, EGLDisplay},
|
||||
input::InputEvent,
|
||||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||||
@@ -36,7 +38,7 @@ use smithay::{
|
||||
wayland_server::backend::GlobalId,
|
||||
},
|
||||
utils::{DeviceFd, Monotonic, Time, Transform},
|
||||
wayland::presentation::Refresh,
|
||||
wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -51,11 +53,39 @@ use crate::{
|
||||
pub struct DirectBackendState {
|
||||
pub session: LibSeatSession,
|
||||
pub active: bool,
|
||||
lid_closed: bool,
|
||||
devices: HashMap<DrmNode, DirectDevice>,
|
||||
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
||||
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
|
||||
}
|
||||
|
||||
impl DirectBackendState {
|
||||
pub(crate) fn capture_resize_snapshot(
|
||||
&mut self,
|
||||
window: &smithay::desktop::Window,
|
||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||
output: &Output,
|
||||
remaining: usize,
|
||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError>
|
||||
{
|
||||
let Some(device) = self.devices.values_mut().find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
}) else {
|
||||
return Ok(None);
|
||||
};
|
||||
crate::winit::capture_resize_snapshot(
|
||||
&mut device.renderer,
|
||||
window,
|
||||
geometry,
|
||||
output.current_scale().fractional_scale(),
|
||||
remaining,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ConnectedOutputInfo {
|
||||
pub connector: String,
|
||||
@@ -94,6 +124,7 @@ struct DirectDevice {
|
||||
}
|
||||
|
||||
struct DirectOutput {
|
||||
internal: bool,
|
||||
connector: connector::Handle,
|
||||
mode: DrmMode,
|
||||
settings: OutputSettings,
|
||||
@@ -143,6 +174,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
state.direct_backend = Some(DirectBackendState {
|
||||
session,
|
||||
active: session_active,
|
||||
lid_closed: initial_lid_closed(),
|
||||
devices: HashMap::new(),
|
||||
presentation: HashMap::new(),
|
||||
connected_outputs: Vec::new(),
|
||||
@@ -279,10 +311,25 @@ fn open_primary_device(
|
||||
state.shm_state.update_formats(renderer.shm_formats());
|
||||
let dmabuf_formats = renderer.dmabuf_formats();
|
||||
let display_handle = state.display_handle.clone();
|
||||
// Mesa's Wayland EGL path needs the main device, not just a v3 format
|
||||
// list, to choose a hardware render node. Without feedback, nested EGL
|
||||
// clients can fall back to llvmpipe despite a GPU-backed DRM compositor.
|
||||
let render_node = node
|
||||
.node_with_type(NodeType::Render)
|
||||
.and_then(Result::ok)
|
||||
.unwrap_or(node);
|
||||
let feedback = DmabufFeedbackBuilder::new(render_node.dev_id(), dmabuf_formats).build()?;
|
||||
state
|
||||
.dmabuf_state
|
||||
.create_global::<Ferese>(&display_handle, dmabuf_formats);
|
||||
let scan = select_outputs(&drm, &state.output_profiles)?;
|
||||
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
|
||||
let mut scan = select_outputs(&drm, &state.output_profiles)?;
|
||||
apply_lid_policy(
|
||||
&mut scan,
|
||||
state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.lid_closed),
|
||||
);
|
||||
if scan.selections.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
@@ -418,6 +465,68 @@ pub fn switch_vt(state: &mut Ferese, vt: i32) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if backend.lid_closed == closed {
|
||||
return;
|
||||
}
|
||||
backend.lid_closed = closed;
|
||||
tracing::info!(closed, "laptop lid state changed");
|
||||
let nodes = backend.devices.keys().copied().collect::<Vec<_>>();
|
||||
for node in nodes {
|
||||
rescan_device(state, node);
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_lid_closed() -> bool {
|
||||
std::fs::read_dir("/proc/acpi/button/lid")
|
||||
.ok()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Result::ok)
|
||||
.filter_map(|entry| std::fs::read_to_string(entry.path().join("state")).ok())
|
||||
.any(|state| state.split_whitespace().last() == Some("closed"))
|
||||
}
|
||||
|
||||
fn internal_connector(interface: connector::Interface) -> bool {
|
||||
matches!(
|
||||
interface,
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
| connector::Interface::LVDS
|
||||
| connector::Interface::DSI
|
||||
)
|
||||
}
|
||||
|
||||
fn apply_lid_policy(scan: &mut OutputScan, closed: bool) {
|
||||
let external_available = scan
|
||||
.selections
|
||||
.iter()
|
||||
.any(|selection| !internal_connector(selection.connector.interface()));
|
||||
if !lid_hides_panel(closed, external_available) {
|
||||
return;
|
||||
}
|
||||
let internal = scan
|
||||
.selections
|
||||
.iter()
|
||||
.filter(|selection| internal_connector(selection.connector.interface()))
|
||||
.map(|selection| selection.connector.to_string())
|
||||
.collect::<HashSet<_>>();
|
||||
scan.selections
|
||||
.retain(|selection| !internal_connector(selection.connector.interface()));
|
||||
for output in &mut scan.connected_outputs {
|
||||
if internal.contains(&output.connector) {
|
||||
output.enabled = false;
|
||||
output.current_mode = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
|
||||
closed && external_available
|
||||
}
|
||||
|
||||
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
let missed_deadlines = state
|
||||
.direct_backend
|
||||
@@ -570,7 +679,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
return;
|
||||
};
|
||||
|
||||
let scan = match select_outputs(&device.drm, &state.output_profiles) {
|
||||
let mut scan = match select_outputs(&device.drm, &state.output_profiles) {
|
||||
Ok(scan) => scan,
|
||||
Err(error) => {
|
||||
tracing::error!(?node, %error, "failed to scan DRM connectors");
|
||||
@@ -578,6 +687,11 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let lid_closed = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.lid_closed);
|
||||
apply_lid_policy(&mut scan, lid_closed);
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.connected_outputs = scan.connected_outputs;
|
||||
}
|
||||
@@ -587,6 +701,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
.map(|selection| (selection.crtc, selection))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
|
||||
let mut deferred_removals = Vec::new();
|
||||
|
||||
for crtc in existing {
|
||||
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
||||
@@ -601,6 +716,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add replacements on free CRTCs before removing vanished outputs so
|
||||
// workspace evacuation always has a real destination. Reused CRTCs
|
||||
// still need their old surface released first.
|
||||
if !selections.contains_key(&crtc) {
|
||||
deferred_removals.push(crtc);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(output) = device.outputs.remove(&crtc) {
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
state.unregister_output(&output.output);
|
||||
@@ -643,7 +766,48 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
}
|
||||
}
|
||||
|
||||
for crtc in deferred_removals {
|
||||
let keep_internal = device
|
||||
.outputs
|
||||
.get(&crtc)
|
||||
.is_some_and(|output| output.internal)
|
||||
&& lid_closed
|
||||
&& state.direct_backend.as_ref().is_some_and(|backend| {
|
||||
device.outputs.get(&crtc).is_some_and(|output| {
|
||||
backend
|
||||
.connected_outputs
|
||||
.iter()
|
||||
.any(|info| info.connector == output.output.name())
|
||||
})
|
||||
})
|
||||
&& !device.outputs.values().any(|output| !output.internal);
|
||||
if keep_internal {
|
||||
tracing::warn!(
|
||||
?crtc,
|
||||
"keeping laptop panel enabled: external output activation failed"
|
||||
);
|
||||
if let Some(output) = device.outputs.get(&crtc)
|
||||
&& let Some(backend) = state.direct_backend.as_mut()
|
||||
&& let Some(info) = backend
|
||||
.connected_outputs
|
||||
.iter_mut()
|
||||
.find(|info| info.connector == output.output.name())
|
||||
{
|
||||
info.enabled = true;
|
||||
info.current_mode = Some(connected_mode_info(output.mode));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(output) = device.outputs.remove(&crtc) {
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
state.unregister_output(&output.output);
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.presentation.remove(&(node, crtc));
|
||||
}
|
||||
}
|
||||
}
|
||||
restore_device(state, node, device);
|
||||
state.restore_output_focus();
|
||||
state.relayout();
|
||||
render_all(state);
|
||||
}
|
||||
@@ -657,6 +821,11 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
||||
return;
|
||||
};
|
||||
|
||||
let context = device.renderer.context_id().erased();
|
||||
state.wallpaper.forget_context(&context);
|
||||
state
|
||||
.resize_snapshots
|
||||
.retain(|_, snapshot| snapshot.context != context);
|
||||
for (crtc, output) in device.outputs {
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
state.unregister_output(&output.output);
|
||||
@@ -901,7 +1070,6 @@ fn create_direct_output(
|
||||
settings: &OutputSettings,
|
||||
) -> Result<DirectOutput, Box<dyn Error>> {
|
||||
let identity = connector_identity(drm, &connector);
|
||||
let (output, global) = create_output(state, &connector, mode, identity, settings);
|
||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||
let allocator = GbmAllocator::new(
|
||||
gbm.clone(),
|
||||
@@ -913,11 +1081,15 @@ fn create_direct_output(
|
||||
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
||||
renderer.dmabuf_formats(),
|
||||
)?;
|
||||
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
||||
// leave a phantom output/workspace that could receive evacuated windows.
|
||||
let (output, global) = create_output(state, &connector, mode, identity, settings);
|
||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
let render_metrics = RenderMetrics::from_environment(output.name());
|
||||
|
||||
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
||||
Ok(DirectOutput {
|
||||
internal: internal_connector(connector.interface()),
|
||||
connector: connector.handle(),
|
||||
mode,
|
||||
settings: settings.clone(),
|
||||
@@ -1090,6 +1262,25 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn lid_only_hides_panels_with_a_usable_external_output() {
|
||||
assert!(lid_hides_panel(true, true));
|
||||
assert!(!lid_hides_panel(false, true));
|
||||
assert!(!lid_hides_panel(true, false));
|
||||
assert!(!lid_hides_panel(false, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panel_detection_uses_connector_type_not_monitor_name() {
|
||||
assert!(internal_connector(
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
));
|
||||
assert!(internal_connector(connector::Interface::LVDS));
|
||||
assert!(internal_connector(connector::Interface::DSI));
|
||||
assert!(!internal_connector(connector::Interface::HDMIA));
|
||||
assert!(!internal_connector(connector::Interface::DisplayPort));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_identity_hash_is_stable() {
|
||||
assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b);
|
||||
|
||||
@@ -167,6 +167,8 @@ impl Default for InactiveDimConfig {
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ThemeConfig {
|
||||
#[serde(default)]
|
||||
background: crate::wallpaper::WallpaperConfig,
|
||||
#[serde(default)]
|
||||
surface: ThemeSurfaceConfig,
|
||||
#[serde(default)]
|
||||
@@ -179,6 +181,12 @@ struct ThemeConfig {
|
||||
material: ThemeMaterialConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
||||
self.theme.background.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct ThemeSurfaceConfig {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -18,6 +18,30 @@ use smithay::{
|
||||
use super::{layer_shell, xdg_shell};
|
||||
use crate::{Ferese, state::ClientState};
|
||||
|
||||
fn layer_affects_backdrop(layer: smithay::wayland::shell::wlr_layer::Layer) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
smithay::wayland::shell::wlr_layer::Layer::Background
|
||||
| smithay::wayland::shell::wlr_layer::Layer::Bottom
|
||||
)
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn surface_affects_backdrop(&self, surface: &WlSurface) -> bool {
|
||||
let mut root = surface.clone();
|
||||
while let Some(parent) = get_parent(&root) {
|
||||
root = parent;
|
||||
}
|
||||
if matches!(&self.cursor_status, smithay::input::pointer::CursorImageStatus::Surface(cursor) if cursor == &root)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
self.space
|
||||
.layer_for_surface(&root, smithay::desktop::WindowSurfaceType::ALL)
|
||||
.is_none_or(|layer| layer_affects_backdrop(layer.cached_state().layer))
|
||||
}
|
||||
}
|
||||
|
||||
impl CompositorHandler for Ferese {
|
||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||
&mut self.compositor_state
|
||||
@@ -31,7 +55,10 @@ impl CompositorHandler for Ferese {
|
||||
}
|
||||
|
||||
fn commit(&mut self, surface: &WlSurface) {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
self.capture_resize_before_commit(surface);
|
||||
if self.surface_affects_backdrop(surface) {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
}
|
||||
on_commit_buffer_handler::<Self>(surface);
|
||||
if !is_sync_subsurface(surface) {
|
||||
let mut root = surface.clone();
|
||||
@@ -77,6 +104,19 @@ impl CompositorHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
#[test]
|
||||
fn panel_commits_do_not_invalidate_their_own_backdrop() {
|
||||
assert!(!layer_affects_backdrop(Layer::Top));
|
||||
assert!(!layer_affects_backdrop(Layer::Overlay));
|
||||
assert!(layer_affects_backdrop(Layer::Bottom));
|
||||
assert!(layer_affects_backdrop(Layer::Background));
|
||||
}
|
||||
}
|
||||
|
||||
impl BufferHandler for Ferese {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
|
||||
+14
-4
@@ -4,7 +4,7 @@ use smithay::{
|
||||
backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
||||
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||
TouchEvent,
|
||||
Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
},
|
||||
input::{
|
||||
keyboard::{FilterResult, keysyms},
|
||||
@@ -27,6 +27,9 @@ impl Ferese {
|
||||
self.idle_notifier_state.notify_activity(&seat);
|
||||
|
||||
match event {
|
||||
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
|
||||
crate::backends::direct::set_lid_closed(self, event.state() == SwitchState::On);
|
||||
}
|
||||
InputEvent::Keyboard { event, .. } => {
|
||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||
let keycode = event.key_code();
|
||||
@@ -339,12 +342,14 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if let Some(window) = self.window_under_visual(position) {
|
||||
self.focused_window = self.window_ids.get(&window).copied();
|
||||
if let Some(focused) = self.focused_window
|
||||
let focused = self.window_ids.get(&window).copied();
|
||||
if let Some(focused) = focused
|
||||
&& let Err(error) = self.workspaces.focus_window(focused)
|
||||
{
|
||||
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
||||
return;
|
||||
}
|
||||
self.focused_window = focused;
|
||||
self.raise_window(&window, true);
|
||||
let surface = window
|
||||
.toplevel()
|
||||
@@ -481,7 +486,12 @@ impl Ferese {
|
||||
BindingAction::Spawn(mut argv) => {
|
||||
let program = argv.remove(0);
|
||||
|
||||
match Command::new(&program).args(argv).spawn() {
|
||||
match Command::new(&program)
|
||||
.args(argv)
|
||||
.env("WAYLAND_DISPLAY", &self.socket_name)
|
||||
.env_remove("WAYLAND_SOCKET")
|
||||
.spawn()
|
||||
{
|
||||
Ok(child) => {
|
||||
tracing::info!(%program, pid = child.id(), "spawned binding command")
|
||||
}
|
||||
|
||||
+10
-4
@@ -11,10 +11,13 @@ mod input;
|
||||
mod ipc;
|
||||
mod metrics;
|
||||
mod overview;
|
||||
mod presentation;
|
||||
mod private_client;
|
||||
mod resize_transaction;
|
||||
mod shell_control;
|
||||
mod stacking;
|
||||
mod state;
|
||||
mod wallpaper;
|
||||
mod window_rules;
|
||||
mod winit;
|
||||
|
||||
@@ -68,14 +71,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
spring_config: config.spring_config()?,
|
||||
viewport_spring_config: config.viewport_spring_config()?,
|
||||
output_profiles,
|
||||
wallpaper: config.wallpaper_settings(),
|
||||
};
|
||||
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||
|
||||
// SAFETY: the backend has already read the host display and Ferese is
|
||||
// single-threaded before the event loop and optional child start.
|
||||
unsafe { std::env::set_var("WAYLAND_DISPLAY", &state.socket_name) };
|
||||
|
||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||
let result = event_loop.run(None, &mut state, |state| {
|
||||
@@ -118,6 +118,12 @@ fn spawn_client(
|
||||
|
||||
let mut command = Command::new(&program);
|
||||
command.args(args);
|
||||
// Wallpaper decoding and drivers may already have worker threads. Set
|
||||
// the child's display explicitly instead of mutating the process env.
|
||||
command.env("WAYLAND_DISPLAY", &state.socket_name);
|
||||
if state.wallpaper.owns_background() {
|
||||
command.env("FERESE_COMPOSITOR_WALLPAPER", "1");
|
||||
}
|
||||
let private_connection = if capabilities.is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) struct RenderMetrics {
|
||||
damaged_pixels: u64,
|
||||
total_render_time: Duration,
|
||||
longest_render_time: Duration,
|
||||
frame_times: Vec<Duration>,
|
||||
}
|
||||
|
||||
impl RenderMetrics {
|
||||
@@ -36,6 +37,7 @@ impl RenderMetrics {
|
||||
damaged_pixels: 0,
|
||||
total_render_time: Duration::ZERO,
|
||||
longest_render_time: Duration::ZERO,
|
||||
frame_times: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +56,10 @@ impl RenderMetrics {
|
||||
self.damaged_pixels += damage.iter().map(rectangle_area).sum::<u64>();
|
||||
self.total_render_time += render_time;
|
||||
self.longest_render_time = self.longest_render_time.max(render_time);
|
||||
if self.frame_times.len() == 512 {
|
||||
self.frame_times.remove(0);
|
||||
}
|
||||
self.frame_times.push(render_time);
|
||||
|
||||
let now = Instant::now();
|
||||
let elapsed = now.saturating_duration_since(self.interval_started);
|
||||
@@ -66,6 +72,17 @@ impl RenderMetrics {
|
||||
.as_micros()
|
||||
.checked_div(u128::from(self.rendered_frames))
|
||||
.unwrap_or(0);
|
||||
self.frame_times.sort_unstable();
|
||||
let percentile = |percent: usize| {
|
||||
let index = (self.frame_times.len() * percent)
|
||||
.div_ceil(100)
|
||||
.saturating_sub(1);
|
||||
self.frame_times
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.as_micros()
|
||||
};
|
||||
tracing::info!(
|
||||
target: "ferese::render",
|
||||
output = %self.output,
|
||||
@@ -74,6 +91,8 @@ impl RenderMetrics {
|
||||
damaged_pixels = self.damaged_pixels,
|
||||
average_render_us,
|
||||
longest_render_us = self.longest_render_time.as_micros(),
|
||||
p95_render_us = percentile(95),
|
||||
p99_render_us = percentile(99),
|
||||
missed_deadlines,
|
||||
"render performance"
|
||||
);
|
||||
@@ -83,6 +102,7 @@ impl RenderMetrics {
|
||||
self.damaged_pixels = 0;
|
||||
self.total_render_time = Duration::ZERO;
|
||||
self.longest_render_time = Duration::ZERO;
|
||||
self.frame_times.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,5 +148,15 @@ mod tests {
|
||||
assert_eq!(metrics.damaged_pixels, 5_200);
|
||||
assert_eq!(metrics.total_render_time, Duration::from_millis(2));
|
||||
assert_eq!(metrics.longest_render_time, Duration::from_millis(2));
|
||||
assert_eq!(metrics.frame_times, vec![Duration::from_millis(2)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiling_samples_are_bounded() {
|
||||
let mut metrics = RenderMetrics::new("test", true, Instant::now());
|
||||
for _ in 0..2000 {
|
||||
metrics.record_frame(Duration::from_millis(1), &[], 0, FrameEffectMetrics);
|
||||
}
|
||||
assert_eq!(metrics.frame_times.len(), 512);
|
||||
}
|
||||
}
|
||||
@@ -209,6 +209,9 @@ impl Ferese {
|
||||
y: f64,
|
||||
) -> Option<(f64, f64)> {
|
||||
let geometry = self.window_geometry.get(&id)?;
|
||||
if !self.overview.is_presenting() {
|
||||
return Some((x - geometry.visual.current.x, y - geometry.visual.current.y));
|
||||
}
|
||||
let source = geometry.client.committed_size?;
|
||||
let presented = self.overview.presented_rect(id, geometry.visual.current);
|
||||
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
//! Presentation geometry is rounded once, in output pixels, never first in
|
||||
//! logical pixels. Content, clips and decorations share those exact edges.
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
element::{Element, Id, Kind, RenderElement},
|
||||
gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
|
||||
element::PixelShaderElement,
|
||||
},
|
||||
utils::{CommitCounter, DamageSet},
|
||||
},
|
||||
utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform},
|
||||
};
|
||||
|
||||
pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
|
||||
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
|
||||
|
||||
pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
|
||||
let delta = now.saturating_duration_since(*last);
|
||||
*last = (*last).max(now);
|
||||
delta
|
||||
}
|
||||
|
||||
pub(crate) fn physical_rect(
|
||||
rect: ferese_layout::Rect,
|
||||
origin: Point<i32, Logical>,
|
||||
scale: f64,
|
||||
) -> Rectangle<i32, Physical> {
|
||||
let left = ((rect.x - f64::from(origin.x)) * scale).round() as i32;
|
||||
let top = ((rect.y - f64::from(origin.y)) * scale).round() as i32;
|
||||
let right = ((rect.x + rect.width - f64::from(origin.x)) * scale).round() as i32;
|
||||
let bottom = ((rect.y + rect.height - f64::from(origin.y)) * scale).round() as i32;
|
||||
Rectangle::new(
|
||||
(left, top).into(),
|
||||
((right - left).max(1), (bottom - top).max(1)).into(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn handoff_alpha(elapsed: Duration) -> f32 {
|
||||
let progress = (elapsed.as_secs_f64() / HANDOFF.as_secs_f64()).clamp(0.0, 1.0);
|
||||
(1.0 - progress * progress * (3.0 - 2.0 * progress)) as f32
|
||||
}
|
||||
|
||||
pub(crate) fn resize_needs_old_frame(
|
||||
visual: ferese_layout::Rect,
|
||||
target: ferese_layout::Rect,
|
||||
buffer_width: i32,
|
||||
buffer_height: i32,
|
||||
) -> bool {
|
||||
// Some clients round their configured dimensions to a character grid.
|
||||
// Do not retain a snapshot forever for that permanent size difference.
|
||||
visual.width > target.width.max(f64::from(buffer_width)) + 0.5
|
||||
|| visual.height > target.height.max(f64::from(buffer_height)) + 0.5
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot_covers_source(
|
||||
snapshot: smithay::utils::Size<i32, Buffer>,
|
||||
source: smithay::utils::Size<i32, Physical>,
|
||||
snapshot_scale: f64,
|
||||
scale: f64,
|
||||
) -> bool {
|
||||
(snapshot_scale - scale).abs() < 0.001 && snapshot.w >= source.w && snapshot.h >= source.h
|
||||
}
|
||||
|
||||
pub(crate) fn advance_handoff(
|
||||
elapsed: &mut Duration,
|
||||
last: &mut Option<Duration>,
|
||||
now: Duration,
|
||||
blocked: bool,
|
||||
speed: f64,
|
||||
) -> bool {
|
||||
if blocked {
|
||||
// A retarget must not consume its client's waiting time as fade time.
|
||||
*last = None;
|
||||
} else {
|
||||
if let Some(previous) = last.replace(now) {
|
||||
*elapsed += now.saturating_sub(previous).mul_f64(speed);
|
||||
}
|
||||
}
|
||||
*elapsed < HANDOFF
|
||||
}
|
||||
|
||||
/// A shader canvas whose damage/paint destination uses the shared pixel edges.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct PhysicalShaderElement {
|
||||
pub inner: PixelShaderElement,
|
||||
pub geometry: Rectangle<i32, Physical>,
|
||||
}
|
||||
|
||||
impl Element for PhysicalShaderElement {
|
||||
fn id(&self) -> &Id {
|
||||
self.inner.id()
|
||||
}
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.inner.current_commit()
|
||||
}
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.inner.src()
|
||||
}
|
||||
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.geometry
|
||||
}
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.current_commit()) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
DamageSet::from_slice(&[Rectangle::from_size(self.geometry.size)])
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RenderElement<GlesRenderer> for PhysicalShaderElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
self.inner.draw(frame, src, dst, damage, opaque)
|
||||
}
|
||||
}
|
||||
|
||||
/// Stable native-pixel texture element. Unlike a resized client buffer, an old
|
||||
/// frame is only translated/cropped; it never changes the size of its glyphs.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct NativeTextureElement {
|
||||
pub id: Id,
|
||||
pub commit: CommitCounter,
|
||||
pub texture: GlesTexture,
|
||||
pub geometry: Rectangle<i32, Physical>,
|
||||
pub source: Rectangle<f64, Buffer>,
|
||||
pub alpha: f32,
|
||||
pub program: Option<GlesTexProgram>,
|
||||
pub uniforms: Vec<Uniform<'static>>,
|
||||
}
|
||||
impl Element for NativeTextureElement {
|
||||
fn id(&self) -> &Id {
|
||||
&self.id
|
||||
}
|
||||
fn current_commit(&self) -> CommitCounter {
|
||||
self.commit
|
||||
}
|
||||
fn src(&self) -> Rectangle<f64, Buffer> {
|
||||
self.source
|
||||
}
|
||||
fn geometry(&self, _: Scale<f64>) -> Rectangle<i32, Physical> {
|
||||
self.geometry
|
||||
}
|
||||
fn alpha(&self) -> f32 {
|
||||
self.alpha
|
||||
}
|
||||
fn kind(&self) -> Kind {
|
||||
Kind::Unspecified
|
||||
}
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.commit) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
DamageSet::from_slice(&[Rectangle::from_size(self.geometry.size)])
|
||||
}
|
||||
}
|
||||
}
|
||||
impl RenderElement<GlesRenderer> for NativeTextureElement {
|
||||
fn draw(
|
||||
&self,
|
||||
frame: &mut GlesFrame<'_, '_>,
|
||||
src: Rectangle<f64, Buffer>,
|
||||
dst: Rectangle<i32, Physical>,
|
||||
damage: &[Rectangle<i32, Physical>],
|
||||
opaque: &[Rectangle<i32, Physical>],
|
||||
) -> Result<(), GlesError> {
|
||||
frame.render_texture_from_to(
|
||||
&self.texture,
|
||||
src,
|
||||
dst,
|
||||
damage,
|
||||
opaque,
|
||||
Transform::Normal,
|
||||
self.alpha,
|
||||
self.program.as_ref(),
|
||||
&self.uniforms,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reversing_a_grow_replaces_the_smaller_snapshot() {
|
||||
assert!(!snapshot_covers_source(
|
||||
(500, 600).into(),
|
||||
(1000, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(!snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.8
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrinking_keeps_old_pixels_until_destination_covers_bounds() {
|
||||
let target = ferese_layout::Rect::new(0.0, 0.0, 500.0, 600.0);
|
||||
let visual = ferese_layout::Rect::new(0.0, 0.0, 750.0, 600.0);
|
||||
assert!(resize_needs_old_frame(visual, target, 500, 600));
|
||||
assert!(!resize_needs_old_frame(target, target, 500, 600));
|
||||
// Character-grid clients must still release the bounded snapshot.
|
||||
assert!(!resize_needs_old_frame(target, target, 492, 592));
|
||||
let growing_target = ferese_layout::Rect::new(0.0, 0.0, 1000.0, 600.0);
|
||||
assert!(!resize_needs_old_frame(visual, growing_target, 1000, 600));
|
||||
}
|
||||
#[test]
|
||||
fn fresh_snapshot_and_retarget_wait_do_not_skip_the_handoff() {
|
||||
let mut elapsed = Duration::ZERO;
|
||||
let mut last = None;
|
||||
assert!(advance_handoff(
|
||||
&mut elapsed,
|
||||
&mut last,
|
||||
Duration::from_secs(5),
|
||||
false,
|
||||
1.0
|
||||
));
|
||||
assert_eq!(elapsed, Duration::ZERO);
|
||||
assert!(advance_handoff(
|
||||
&mut elapsed,
|
||||
&mut last,
|
||||
Duration::from_millis(5010),
|
||||
false,
|
||||
1.0
|
||||
));
|
||||
assert_eq!(elapsed, Duration::from_millis(10));
|
||||
advance_handoff(&mut elapsed, &mut last, Duration::from_secs(6), true, 1.0);
|
||||
advance_handoff(&mut elapsed, &mut last, Duration::from_secs(7), false, 1.0);
|
||||
assert_eq!(elapsed, Duration::from_millis(10));
|
||||
assert!(!advance_handoff(
|
||||
&mut elapsed,
|
||||
&mut last,
|
||||
Duration::from_millis(7070),
|
||||
false,
|
||||
1.0
|
||||
));
|
||||
}
|
||||
#[test]
|
||||
fn mixed_refresh_outputs_do_not_double_the_animation_clock() {
|
||||
let start = Instant::now();
|
||||
let mut last = start;
|
||||
let mut elapsed = Duration::ZERO;
|
||||
for ms in [8, 16, 16, 24, 32, 32] {
|
||||
elapsed += frame_delta(&mut last, start + Duration::from_millis(ms));
|
||||
}
|
||||
assert_eq!(elapsed, Duration::from_millis(32));
|
||||
assert_eq!(frame_delta(&mut last, start), Duration::ZERO);
|
||||
assert_eq!(last, start + Duration::from_millis(32));
|
||||
}
|
||||
#[test]
|
||||
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
||||
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into());
|
||||
assert_eq!(
|
||||
physical_rect(rect, (0, 0).into(), 1.8).size,
|
||||
(180, 144).into()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn adjacent_frames_share_edges_at_all_output_scales() {
|
||||
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||
let left = physical_rect(
|
||||
ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let right = physical_rect(
|
||||
ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn handoff_has_bounded_lifetime_and_no_alpha_jump() {
|
||||
assert_eq!(handoff_alpha(Duration::ZERO), 1.0);
|
||||
assert_eq!(handoff_alpha(HANDOFF), 0.0);
|
||||
assert_eq!(handoff_alpha(Duration::from_secs(1)), 0.0);
|
||||
assert!(handoff_alpha(Duration::from_millis(1)) > 0.99);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
//! A presentation barrier, not an event-loop or client-commit blocker.
|
||||
//! Grouping barriers by workspace keeps viewport and neighboring tiles on the
|
||||
//! same clock. A serial identifies readiness even for cell-grid clients whose
|
||||
//! committed size is smaller than the configure, and after rapid reversals.
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::utils::{Logical, Rectangle, Serial};
|
||||
|
||||
const DEADLINE: Duration = Duration::from_millis(300);
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct ResizeTransaction {
|
||||
serial: Serial,
|
||||
started: Duration,
|
||||
source_geometry: Option<Rectangle<i32, Logical>>,
|
||||
}
|
||||
|
||||
impl ResizeTransaction {
|
||||
pub(crate) fn new(serial: Serial, started: Duration) -> Self {
|
||||
Self {
|
||||
serial,
|
||||
started,
|
||||
source_geometry: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_source_geometry(mut self, geometry: Rectangle<i32, Logical>) -> Self {
|
||||
self.source_geometry = Some(geometry);
|
||||
self
|
||||
}
|
||||
pub(crate) fn source_geometry(self) -> Option<Rectangle<i32, Logical>> {
|
||||
self.source_geometry
|
||||
}
|
||||
|
||||
pub(crate) fn accepts(self, committed: Option<Serial>) -> bool {
|
||||
committed.is_some_and(|serial| serial >= self.serial)
|
||||
}
|
||||
|
||||
pub(crate) fn expired(self, now: Duration) -> bool {
|
||||
now.saturating_sub(self.started) >= DEADLINE
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn acknowledgement_alone_or_stale_commit_does_not_release_resize() {
|
||||
let transaction = ResizeTransaction::new(12.into(), Duration::ZERO);
|
||||
assert!(!transaction.accepts(None));
|
||||
assert!(!transaction.accepts(Some(11.into())));
|
||||
assert!(transaction.accepts(Some(12.into())));
|
||||
assert!(transaction.accepts(Some(13.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serial_comparison_handles_wraparound() {
|
||||
let transaction = ResizeTransaction::new(u32::MAX.into(), Duration::ZERO);
|
||||
assert!(transaction.accepts(Some(0.into())));
|
||||
assert!(!transaction.accepts(Some((u32::MAX - 1).into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresponsive_client_has_bounded_wait() {
|
||||
let transaction = ResizeTransaction::new(1.into(), Duration::from_millis(10));
|
||||
assert!(!transaction.expired(Duration::from_millis(309)));
|
||||
assert!(transaction.expired(Duration::from_millis(310)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shrinking_commit_retains_the_previous_full_frame_geometry() {
|
||||
let source = Rectangle::new((4, 8).into(), (1200, 800).into());
|
||||
let transaction =
|
||||
ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
|
||||
assert_eq!(transaction.source_geometry(), Some(source));
|
||||
}
|
||||
}
|
||||
+257
-10
@@ -64,7 +64,7 @@ use smithay::{
|
||||
shell::wlr_layer::Layer,
|
||||
shell::wlr_layer::WlrLayerShellState,
|
||||
shell::xdg::decoration::XdgDecorationState,
|
||||
shell::xdg::{SurfaceCachedState, XdgShellState},
|
||||
shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData},
|
||||
shm::ShmState,
|
||||
single_pixel_buffer::SinglePixelBufferState,
|
||||
socket::ListeningSocketSource,
|
||||
@@ -118,11 +118,16 @@ pub struct Ferese {
|
||||
output_identity_ids: HashMap<String, OutputId>,
|
||||
pub window_ids: HashMap<Window, WindowId>,
|
||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
||||
resize_transactions: HashMap<WindowId, crate::resize_transaction::ResizeTransaction>,
|
||||
pub(crate) resize_snapshots: HashMap<WindowId, crate::winit::ResizeSnapshot>,
|
||||
pub(crate) nested_backend: Option<crate::winit::NestedBackend>,
|
||||
pub(crate) wallpaper: crate::wallpaper::WallpaperState,
|
||||
maximized_windows: HashSet<WindowId>,
|
||||
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
|
||||
window_stack: crate::stacking::WindowStack,
|
||||
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||
pub(crate) window_dims: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||
pub(crate) window_resize_fills: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||
pub(crate) window_dimming: HashMap<WindowId, crate::dimming::DimAnimation>,
|
||||
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
||||
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
|
||||
@@ -203,6 +208,7 @@ pub struct Ferese {
|
||||
}
|
||||
|
||||
pub struct RuntimeConfig {
|
||||
pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
|
||||
pub layout_mode: LayoutMode,
|
||||
pub gap_config: GapConfig,
|
||||
pub input_settings: InputSettings,
|
||||
@@ -321,6 +327,15 @@ impl Ferese {
|
||||
.find_map(|(output, id)| (*id == focused).then_some(output))
|
||||
}
|
||||
|
||||
pub(crate) fn restore_output_focus(&mut self) {
|
||||
if let Some(output) = self.output_workspaces.focused_output()
|
||||
&& let Some(workspace) = self.output_workspaces.active_workspace(output)
|
||||
{
|
||||
self.activate_output_workspace(output, workspace);
|
||||
self.restore_keyboard_focus();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn output_id(&self, output: &Output) -> Option<OutputId> {
|
||||
self.output_ids.get(output).copied()
|
||||
}
|
||||
@@ -340,12 +355,14 @@ impl Ferese {
|
||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||
return;
|
||||
};
|
||||
if self.output_workspaces.focused_output() == Some(output_id) {
|
||||
return;
|
||||
}
|
||||
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||
return;
|
||||
};
|
||||
if self.output_workspaces.focused_output() == Some(output_id)
|
||||
&& self.workspaces.active_id() == workspace
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.activate_output_workspace(output_id, workspace);
|
||||
self.relayout();
|
||||
@@ -453,11 +470,16 @@ impl Ferese {
|
||||
output_identity_ids: HashMap::new(),
|
||||
window_ids: HashMap::new(),
|
||||
window_geometry: HashMap::new(),
|
||||
resize_transactions: HashMap::new(),
|
||||
resize_snapshots: HashMap::new(),
|
||||
nested_backend: None,
|
||||
wallpaper: crate::wallpaper::WallpaperState::new(config.wallpaper),
|
||||
maximized_windows: HashSet::new(),
|
||||
maximized_column_widths: HashMap::new(),
|
||||
window_stack: crate::stacking::WindowStack::default(),
|
||||
window_borders: HashMap::new(),
|
||||
window_dims: HashMap::new(),
|
||||
window_resize_fills: HashMap::new(),
|
||||
window_dimming: HashMap::new(),
|
||||
window_shadows: HashMap::new(),
|
||||
rounded_clip_programs: HashMap::new(),
|
||||
@@ -596,8 +618,12 @@ impl Ferese {
|
||||
return None;
|
||||
}
|
||||
|
||||
let workspace = self.workspace_under_pointer(position)?;
|
||||
self.space.elements().rev().find_map(|window| {
|
||||
let id = self.window_ids.get(window)?;
|
||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||
return None;
|
||||
}
|
||||
let visual = self.presented_window_rect(*id)?;
|
||||
let inside_visual = position.x >= visual.x
|
||||
&& position.y >= visual.y
|
||||
@@ -666,8 +692,12 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
|
||||
let workspace = self.workspace_under_pointer(position)?;
|
||||
self.space.elements().rev().find_map(|window| {
|
||||
let id = self.window_ids.get(window)?;
|
||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||
return None;
|
||||
}
|
||||
let visual = self.presented_window_rect(*id)?;
|
||||
|
||||
(position.x >= visual.x
|
||||
@@ -678,6 +708,19 @@ impl Ferese {
|
||||
})
|
||||
}
|
||||
|
||||
fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> {
|
||||
let output = self.space.output_under(position).next()?;
|
||||
self.output_workspaces
|
||||
.active_workspace(self.output_id(output)?)
|
||||
}
|
||||
|
||||
pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool {
|
||||
let workspace = self
|
||||
.output_id(output)
|
||||
.and_then(|output| self.output_workspaces.active_workspace(output));
|
||||
workspace.is_some() && self.workspaces.workspace_for_window(window) == workspace
|
||||
}
|
||||
|
||||
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||
if let Some(id) = self.window_ids.get(window) {
|
||||
self.window_stack.raise(*id);
|
||||
@@ -704,6 +747,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
|
||||
if !self.overview.is_presenting() {
|
||||
return Some((1.0, 1.0));
|
||||
}
|
||||
let id = self.window_ids.get(window)?;
|
||||
let geometry = self.window_geometry.get(id)?;
|
||||
let source = geometry.client.committed_size?;
|
||||
@@ -916,6 +962,9 @@ impl Ferese {
|
||||
|
||||
self.space.unmap_elem(window);
|
||||
self.window_geometry.remove(&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);
|
||||
@@ -939,7 +988,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn relayout(&mut self) {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
// Consume idle time before setting new targets; it must not become a
|
||||
// large first animation step after a keypress on an idle desktop.
|
||||
self.advance_animations(Instant::now());
|
||||
self.arrange_layers();
|
||||
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
|
||||
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
|
||||
@@ -1071,6 +1122,9 @@ impl Ferese {
|
||||
.map(|(window, _)| window.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut layout_changed = hidden
|
||||
.iter()
|
||||
.any(|window| self.space.element_location(window).is_some());
|
||||
for window in hidden {
|
||||
self.space.unmap_elem(&window);
|
||||
}
|
||||
@@ -1100,6 +1154,7 @@ impl Ferese {
|
||||
} else {
|
||||
PresentationMode::Normal
|
||||
};
|
||||
layout_changed |= !had_geometry || geometry.logical != rect;
|
||||
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
|
||||
if !self.animations_enabled {
|
||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||
@@ -1198,11 +1253,24 @@ impl Ferese {
|
||||
});
|
||||
|
||||
if requested_size.is_some() || state_changed {
|
||||
toplevel.send_pending_configure();
|
||||
if let Some(serial) = toplevel.send_pending_configure()
|
||||
&& requested_size.is_some()
|
||||
&& had_geometry
|
||||
&& self.animations_enabled
|
||||
{
|
||||
self.resize_transactions.insert(
|
||||
id,
|
||||
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
||||
.with_source_geometry(window.geometry()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrolling_world_x = scrolling_world_x;
|
||||
if layout_changed {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
}
|
||||
self.sync_window_stacking();
|
||||
self.retarget_overview();
|
||||
self.send_shell_snapshots();
|
||||
@@ -1211,8 +1279,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub fn advance_animations(&mut self, now: Instant) -> bool {
|
||||
let delta = now.saturating_duration_since(self.last_animation_tick);
|
||||
self.last_animation_tick = now;
|
||||
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
|
||||
self.advance_animations_by(delta)
|
||||
}
|
||||
|
||||
@@ -1227,8 +1294,11 @@ impl Ferese {
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
.and_then(|backend| backend.record_presentation(node, crtc, time, sequence));
|
||||
if let Some(delta) = delta {
|
||||
self.advance_animations_by(delta);
|
||||
if delta.is_some() {
|
||||
// All outputs animate the same scene. Summing each CRTC's frame
|
||||
// interval makes motion run faster with two monitors (and uneven
|
||||
// with mixed 60/120 Hz). Advance once by actual elapsed time.
|
||||
self.advance_animations(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1287,10 +1357,73 @@ impl Ferese {
|
||||
self.send_window_close(id);
|
||||
}
|
||||
|
||||
let now = self.start_time.elapsed();
|
||||
self.resize_transactions.retain(|id, transaction| {
|
||||
if transaction.expired(now) {
|
||||
tracing::warn!(?id, "resize presentation deadline reached");
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
let blocked_workspaces = self
|
||||
.resize_transactions
|
||||
.keys()
|
||||
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||
.collect::<HashSet<_>>();
|
||||
let animations_enabled = self.animations_enabled;
|
||||
let animation_speed = self.animation_speed;
|
||||
self.resize_snapshots.retain(|id, snapshot| {
|
||||
if !animations_enabled {
|
||||
return false;
|
||||
}
|
||||
let waiting_for_client = self
|
||||
.workspaces
|
||||
.workspace_for_window(*id)
|
||||
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
|
||||
// A shrinking client's destination buffer arrives before the
|
||||
// animated bounds reach it. Keep the old native pixels covering
|
||||
// that strip rather than fading them into the neutral resize fill.
|
||||
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
|
||||
geometry.client.committed_size.is_some_and(|size| {
|
||||
crate::presentation::resize_needs_old_frame(
|
||||
geometry.visual.current,
|
||||
geometry.logical,
|
||||
size.width,
|
||||
size.height,
|
||||
)
|
||||
})
|
||||
});
|
||||
let blocked = waiting_for_client || uncovered;
|
||||
let active = crate::presentation::advance_handoff(
|
||||
&mut snapshot.elapsed,
|
||||
&mut snapshot.last_tick,
|
||||
now,
|
||||
blocked,
|
||||
animation_speed,
|
||||
);
|
||||
if !blocked {
|
||||
snapshot.commit.increment();
|
||||
}
|
||||
if !active {
|
||||
tracing::debug!(
|
||||
?id,
|
||||
bytes = snapshot.bytes(),
|
||||
"released resize handoff snapshot"
|
||||
);
|
||||
}
|
||||
active
|
||||
});
|
||||
active_animation |= !self.resize_snapshots.is_empty();
|
||||
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
||||
active_animation |= !blocked_workspaces.is_empty();
|
||||
for (workspace, viewport) in &mut self.viewport_animations {
|
||||
if !visible_workspaces.contains(workspace) {
|
||||
continue;
|
||||
}
|
||||
if blocked_workspaces.contains(workspace) {
|
||||
continue;
|
||||
}
|
||||
if self.animations_enabled {
|
||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||
} else {
|
||||
@@ -1300,6 +1433,13 @@ impl Ferese {
|
||||
|
||||
let mut settled_coupled_widths = Vec::new();
|
||||
for (window, id) in windows {
|
||||
if self
|
||||
.workspaces
|
||||
.workspace_for_window(id)
|
||||
.is_some_and(|workspace| blocked_workspaces.contains(&workspace))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||
continue;
|
||||
};
|
||||
@@ -1390,10 +1530,115 @@ impl Ferese {
|
||||
self.animations_enabled
|
||||
}
|
||||
|
||||
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
||||
let Some((window, id)) = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.find(|(window, _)| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
||||
})
|
||||
.map(|(window, id)| (window.clone(), *id))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(transaction) = self.resize_transactions.get(&id) else {
|
||||
return;
|
||||
};
|
||||
let serial = with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
|
||||
});
|
||||
if !transaction.accepts(serial) {
|
||||
return;
|
||||
}
|
||||
let Some(output) = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| self.window_belongs_to_output(id, output))
|
||||
.cloned()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let source_geometry = transaction
|
||||
.source_geometry()
|
||||
.unwrap_or_else(|| window.geometry());
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
||||
if self.resize_snapshots.get(&id).is_some_and(|snapshot| {
|
||||
crate::presentation::snapshot_covers_source(
|
||||
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
||||
source_size,
|
||||
snapshot.scale,
|
||||
scale,
|
||||
)
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let used: usize = self
|
||||
.resize_snapshots
|
||||
.values()
|
||||
.map(|snapshot| snapshot.bytes())
|
||||
.sum();
|
||||
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
// Commit dispatch does not run inside the Winit event callback;
|
||||
// still never risk reentrant renderer access or a compositor panic.
|
||||
match backend.try_borrow_mut() {
|
||||
Ok(mut backend) => crate::winit::capture_resize_snapshot(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
source_geometry,
|
||||
output.current_scale().fractional_scale(),
|
||||
remaining,
|
||||
),
|
||||
Err(_) => return,
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
backend.capture_resize_snapshot(&window, source_geometry, &output, remaining)
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
match result {
|
||||
Ok(Some(snapshot)) => {
|
||||
tracing::debug!(
|
||||
?id,
|
||||
bytes = snapshot.bytes(),
|
||||
"captured native resize handoff"
|
||||
);
|
||||
self.resize_snapshots.insert(id, snapshot);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(error) => {
|
||||
tracing::debug!(%error, ?id, "resize snapshot unavailable; using native crop")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_client_commit(&mut self, window: &Window) {
|
||||
let Some(id) = self.window_ids.get(window) else {
|
||||
return;
|
||||
};
|
||||
// current_serial is promoted by Smithay on commit, unlike
|
||||
// configure_serial, which only records an acknowledgement.
|
||||
let committed_serial = window.toplevel().and_then(|toplevel| {
|
||||
with_states(toplevel.wl_surface(), |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()
|
||||
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
|
||||
})
|
||||
});
|
||||
if self
|
||||
.resize_transactions
|
||||
.get(id)
|
||||
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
||||
{
|
||||
self.resize_transactions.remove(id);
|
||||
}
|
||||
let Some(size) = client_size(window) else {
|
||||
return;
|
||||
};
|
||||
@@ -1869,6 +2114,8 @@ impl Ferese {
|
||||
|
||||
// Direct manipulation follows the pointer, including while the client is
|
||||
// still drawing its next buffer. Rendering and hit testing share this rect.
|
||||
self.resize_transactions.remove(&id);
|
||||
self.resize_snapshots.remove(&id);
|
||||
let geometry = self
|
||||
.window_geometry
|
||||
.entry(id)
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
//! One decoded image and one texture per GPU context, not double-buffered
|
||||
//! full-screen UI surfaces per monitor. Resize changes sampling only.
|
||||
use crate::presentation::NativeTextureElement;
|
||||
use serde::Deserialize;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{
|
||||
ErasedContextId, ImportMem, Renderer,
|
||||
element::Id,
|
||||
gles::{GlesRenderer, GlesTexture},
|
||||
utils::CommitCounter,
|
||||
},
|
||||
},
|
||||
output::Output,
|
||||
utils::{Buffer, Physical, Rectangle, Size},
|
||||
};
|
||||
use std::{collections::HashMap, path::PathBuf, sync::mpsc};
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub(crate) struct WallpaperConfig {
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub mode: WallpaperMode,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum WallpaperMode {
|
||||
#[default]
|
||||
Fill,
|
||||
Fit,
|
||||
}
|
||||
|
||||
struct WallpaperTexture {
|
||||
texture: GlesTexture,
|
||||
id: Id,
|
||||
}
|
||||
pub(crate) struct WallpaperState {
|
||||
owned: bool,
|
||||
mode: WallpaperMode,
|
||||
receiver: Option<mpsc::Receiver<Result<image::RgbaImage, String>>>,
|
||||
pixels: Option<image::RgbaImage>,
|
||||
textures: HashMap<ErasedContextId, WallpaperTexture>,
|
||||
}
|
||||
|
||||
impl WallpaperState {
|
||||
pub fn new(config: WallpaperConfig) -> Self {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let owned = config.path.as_ref().is_some_and(|path| path.is_file());
|
||||
if owned {
|
||||
let path = config.path.unwrap();
|
||||
std::thread::spawn(move || {
|
||||
let load = || -> Result<image::RgbaImage, String> {
|
||||
let (width, height) =
|
||||
image::image_dimensions(&path).map_err(|e| e.to_string())?;
|
||||
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
||||
return Err("wallpaper exceeds the 256 MiB decode limit".into());
|
||||
}
|
||||
image::open(path)
|
||||
.map(|image| image.into_rgba8())
|
||||
.map_err(|e| e.to_string())
|
||||
};
|
||||
let _ = sender.send(load());
|
||||
});
|
||||
}
|
||||
Self {
|
||||
owned,
|
||||
mode: config.mode,
|
||||
receiver: owned.then_some(receiver),
|
||||
pixels: None,
|
||||
textures: HashMap::new(),
|
||||
}
|
||||
}
|
||||
pub fn owns_background(&self) -> bool {
|
||||
self.owned
|
||||
}
|
||||
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
||||
self.textures.remove(context);
|
||||
}
|
||||
pub fn poll(&mut self) -> bool {
|
||||
let Some(receiver) = &self.receiver else {
|
||||
return false;
|
||||
};
|
||||
match receiver.try_recv() {
|
||||
Ok(result) => {
|
||||
self.receiver = None;
|
||||
match result {
|
||||
Ok(pixels) => {
|
||||
tracing::info!(
|
||||
width = pixels.width(),
|
||||
height = pixels.height(),
|
||||
bytes = pixels.as_raw().len(),
|
||||
"decoded compositor wallpaper once"
|
||||
);
|
||||
self.pixels = Some(pixels);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "wallpaper unavailable; using output clear color")
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(mpsc::TryRecvError::Disconnected) => {
|
||||
self.receiver = None;
|
||||
false
|
||||
}
|
||||
Err(mpsc::TryRecvError::Empty) => false,
|
||||
}
|
||||
}
|
||||
pub fn element(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
) -> Option<NativeTextureElement> {
|
||||
let pixels = self.pixels.as_ref()?;
|
||||
let context = renderer.context_id().erased();
|
||||
if !self.textures.contains_key(&context) {
|
||||
let size = Size::from((pixels.width() as i32, pixels.height() as i32));
|
||||
match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) {
|
||||
Ok(texture) => {
|
||||
tracing::debug!(
|
||||
bytes = pixels.as_raw().len(),
|
||||
"uploaded shared wallpaper texture"
|
||||
);
|
||||
self.textures.insert(
|
||||
context.clone(),
|
||||
WallpaperTexture {
|
||||
texture,
|
||||
id: Id::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::debug!(%error, "wallpaper texture import failed");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let cached = self.textures.get(&context)?;
|
||||
let output_size = output
|
||||
.current_transform()
|
||||
.transform_size(output.current_mode()?.size);
|
||||
let (geometry, source) = image_geometry(
|
||||
(pixels.width() as i32, pixels.height() as i32).into(),
|
||||
output_size,
|
||||
self.mode,
|
||||
);
|
||||
Some(NativeTextureElement {
|
||||
id: cached.id.clone(),
|
||||
commit: CommitCounter::default(),
|
||||
texture: cached.texture.clone(),
|
||||
geometry,
|
||||
source,
|
||||
alpha: 1.0,
|
||||
program: None,
|
||||
uniforms: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn image_geometry(
|
||||
image: Size<i32, Buffer>,
|
||||
output: Size<i32, Physical>,
|
||||
mode: WallpaperMode,
|
||||
) -> (Rectangle<i32, Physical>, Rectangle<f64, Buffer>) {
|
||||
let sx = f64::from(output.w) / f64::from(image.w);
|
||||
let sy = f64::from(output.h) / f64::from(image.h);
|
||||
match mode {
|
||||
WallpaperMode::Fill => {
|
||||
let scale = sx.max(sy);
|
||||
let width = f64::from(output.w) / scale;
|
||||
let height = f64::from(output.h) / scale;
|
||||
(
|
||||
Rectangle::from_size(output),
|
||||
Rectangle::new(
|
||||
(
|
||||
(f64::from(image.w) - width) / 2.0,
|
||||
(f64::from(image.h) - height) / 2.0,
|
||||
)
|
||||
.into(),
|
||||
(width, height).into(),
|
||||
),
|
||||
)
|
||||
}
|
||||
WallpaperMode::Fit => {
|
||||
let scale = sx.min(sy);
|
||||
let size: Size<i32, Physical> = (
|
||||
(f64::from(image.w) * scale).round() as i32,
|
||||
(f64::from(image.h) * scale).round() as i32,
|
||||
)
|
||||
.into();
|
||||
(
|
||||
Rectangle::new(
|
||||
((output.w - size.w) / 2, (output.h - size.h) / 2).into(),
|
||||
size,
|
||||
),
|
||||
Rectangle::from_size(image.to_f64()),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
|
||||
let (geometry, source) = image_geometry(
|
||||
(3840, 2160).into(),
|
||||
(1600, 1000).into(),
|
||||
WallpaperMode::Fill,
|
||||
);
|
||||
assert_eq!(geometry.size, (1600, 1000).into());
|
||||
assert_eq!(source.size.h, 2160.0);
|
||||
assert!(source.loc.x > 0.0);
|
||||
let (geometry, source) =
|
||||
image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
|
||||
assert_eq!(geometry.size, (1600, 900).into());
|
||||
assert_eq!(geometry.loc, (0, 50).into());
|
||||
assert_eq!(source.size, (3840.0, 2160.0).into());
|
||||
}
|
||||
#[test]
|
||||
fn missing_wallpaper_preserves_external_shell_fallback() {
|
||||
let state = WallpaperState::new(WallpaperConfig::default());
|
||||
assert!(!state.owns_background());
|
||||
}
|
||||
}
|
||||
+449
-169
@@ -1,6 +1,8 @@
|
||||
use std::{
|
||||
cell::{Cell, RefCell},
|
||||
collections::HashMap,
|
||||
error::Error,
|
||||
rc::Rc,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -12,7 +14,7 @@ use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{
|
||||
Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
|
||||
Bind, Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
|
||||
damage::OutputDamageTracker,
|
||||
element::{
|
||||
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
||||
@@ -43,7 +45,10 @@ use smithay::{
|
||||
},
|
||||
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
||||
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
|
||||
reexports::calloop::EventLoop,
|
||||
reexports::calloop::{
|
||||
EventLoop,
|
||||
timer::{TimeoutAction, Timer},
|
||||
},
|
||||
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
|
||||
reexports::wayland_server::Resource,
|
||||
utils::{
|
||||
@@ -57,6 +62,7 @@ use smithay::{
|
||||
use crate::{
|
||||
Ferese,
|
||||
metrics::{FrameEffectMetrics, RenderMetrics},
|
||||
presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect},
|
||||
};
|
||||
|
||||
type SurfaceRenderElement = CropRenderElement<
|
||||
@@ -81,6 +87,85 @@ render_elements! {
|
||||
Solid=SolidColorRenderElement,
|
||||
Border=PixelShaderElement,
|
||||
Blur=BlurRenderElement,
|
||||
Effect=PhysicalShaderElement,
|
||||
Native=NativeTextureElement,
|
||||
}
|
||||
|
||||
pub(crate) type NestedBackend = Rc<RefCell<winit::WinitGraphicsBackend<GlesRenderer>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ResizeSnapshot {
|
||||
pub texture: GlesTexture,
|
||||
pub context: ErasedContextId,
|
||||
pub id: Id,
|
||||
pub commit: CommitCounter,
|
||||
pub elapsed: Duration,
|
||||
pub last_tick: Option<Duration>,
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl ResizeSnapshot {
|
||||
pub(crate) fn bytes(&self) -> usize {
|
||||
self.texture.size().w as usize * self.texture.size().h as usize * 4
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn capture_resize_snapshot(
|
||||
renderer: &mut GlesRenderer,
|
||||
window: &smithay::desktop::Window,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
scale: f64,
|
||||
remaining: usize,
|
||||
) -> Result<Option<ResizeSnapshot>, GlesError> {
|
||||
let Some(toplevel) = window.toplevel() else {
|
||||
return Ok(None);
|
||||
};
|
||||
let size: Size<i32, Physical> = geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = (size.w.max(0) as usize)
|
||||
.saturating_mul(size.h.max(0) as usize)
|
||||
.saturating_mul(4);
|
||||
if bytes == 0 || bytes > remaining {
|
||||
return Ok(None);
|
||||
}
|
||||
// Called before on_commit_buffer_handler: renderer surface state still
|
||||
// owns the previous buffer. Copy only during an actual resize handoff.
|
||||
let content = render_elements_from_surface_tree::<
|
||||
GlesRenderer,
|
||||
WaylandSurfaceRenderElement<GlesRenderer>,
|
||||
>(
|
||||
renderer,
|
||||
toplevel.wl_surface(),
|
||||
Point::<i32, Logical>::from((-geometry.loc.x, -geometry.loc.y))
|
||||
.to_physical_precise_round(scale),
|
||||
scale,
|
||||
1.0,
|
||||
RenderElementKind::Unspecified,
|
||||
);
|
||||
if content.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let mut texture = Offscreen::<GlesTexture>::create_buffer(
|
||||
renderer,
|
||||
Fourcc::Abgr8888,
|
||||
(size.w, size.h).into(),
|
||||
)?;
|
||||
{
|
||||
let mut target = renderer.bind(&mut texture)?;
|
||||
let mut frame = renderer.render(&mut target, size, Transform::Normal)?;
|
||||
let damage = Rectangle::from_size(size);
|
||||
frame.clear(Color32F::new(0.0, 0.0, 0.0, 0.0), &[damage])?;
|
||||
draw_render_elements(&mut frame, scale, &content, &[damage])?;
|
||||
let _ = frame.finish()?;
|
||||
}
|
||||
Ok(Some(ResizeSnapshot {
|
||||
texture,
|
||||
context: renderer.context_id().erased(),
|
||||
id: Id::new(),
|
||||
commit: CommitCounter::default(),
|
||||
elapsed: Duration::ZERO,
|
||||
last_tick: None,
|
||||
scale,
|
||||
}))
|
||||
}
|
||||
|
||||
render_elements! {
|
||||
@@ -664,7 +749,11 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let initial_scale = normalized_scale(backend.scale_factor());
|
||||
let mode = Mode {
|
||||
size: backend.window_size(),
|
||||
refresh: 60_000,
|
||||
refresh: backend
|
||||
.window()
|
||||
.current_monitor()
|
||||
.and_then(|monitor| monitor.refresh_rate_millihertz())
|
||||
.unwrap_or(60_000) as i32,
|
||||
};
|
||||
let output = Output::new(
|
||||
"ferese-winit".into(),
|
||||
@@ -689,163 +778,192 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let mut damage_tracker = OutputDamageTracker::from_output(&output);
|
||||
let clock = Clock::<Monotonic>::new();
|
||||
let mut sequence = 0_u64;
|
||||
let mut missed_deadlines = 0_u64;
|
||||
let mut output_scale = initial_scale;
|
||||
let mut render_metrics = RenderMetrics::from_environment(output.name());
|
||||
|
||||
// Do not request a redraw recursively: a no-damage redraw has no EGL
|
||||
// submission to pace it and otherwise spins at full CPU. Independently
|
||||
// scheduled frames also let clients receive callbacks without damage.
|
||||
let refresh = Rc::new(Cell::new(Duration::from_nanos(
|
||||
1_000_000_000_000 / mode.refresh.max(1) as u64,
|
||||
)));
|
||||
let backend = Rc::new(RefCell::new(backend));
|
||||
state.nested_backend = Some(backend.clone());
|
||||
let redraw_backend = backend.clone();
|
||||
let redraw_refresh = refresh.clone();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(event_source, move |event, _, state| match event {
|
||||
WinitEvent::Resized { size, scale_factor } => {
|
||||
let scale = normalized_scale(scale_factor);
|
||||
.insert_source(Timer::from_duration(refresh.get()), move |_, _, _| {
|
||||
redraw_backend.borrow().window().request_redraw();
|
||||
TimeoutAction::ToDuration(redraw_refresh.get())
|
||||
})?;
|
||||
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size,
|
||||
refresh: 60_000,
|
||||
}),
|
||||
None,
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(event_source, move |event, _, state| {
|
||||
let mut backend = backend.borrow_mut();
|
||||
match event {
|
||||
WinitEvent::Resized { size, scale_factor } => {
|
||||
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
|
||||
let scale = normalized_scale(scale_factor);
|
||||
let rate = backend
|
||||
.window()
|
||||
.current_monitor()
|
||||
.and_then(|monitor| monitor.refresh_rate_millihertz())
|
||||
.unwrap_or(60_000) as i32;
|
||||
refresh.set(Duration::from_nanos(1_000_000_000_000 / rate.max(1) as u64));
|
||||
|
||||
if scale != output_scale {
|
||||
output_scale = scale;
|
||||
state.update_fractional_scale(scale);
|
||||
output.change_current_state(
|
||||
Some(Mode {
|
||||
size,
|
||||
refresh: rate,
|
||||
}),
|
||||
None,
|
||||
Some(Scale::Fractional(scale)),
|
||||
None,
|
||||
);
|
||||
|
||||
if scale != output_scale {
|
||||
output_scale = scale;
|
||||
state.update_fractional_scale(scale);
|
||||
}
|
||||
|
||||
state.relayout();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush output-resize configure");
|
||||
}
|
||||
backend.window().request_redraw();
|
||||
}
|
||||
|
||||
state.relayout();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush output-resize configure");
|
||||
}
|
||||
backend.window().request_redraw();
|
||||
}
|
||||
WinitEvent::Input(event) => state.process_input_event(event),
|
||||
WinitEvent::Redraw => {
|
||||
state.advance_animations(Instant::now());
|
||||
let age = backend.buffer_age().unwrap_or(0);
|
||||
let render_started = Instant::now();
|
||||
let rendered = (|| -> DamageRenderResult {
|
||||
{
|
||||
let (renderer, mut framebuffer) = backend.bind()?;
|
||||
state.process_dmabuf_imports(renderer);
|
||||
let elements = animated_window_elements(state, renderer, &output);
|
||||
let effects = frame_effect_metrics(
|
||||
&elements,
|
||||
output.current_scale().fractional_scale(),
|
||||
);
|
||||
let result = damage_tracker.render_output(
|
||||
renderer,
|
||||
&mut framebuffer,
|
||||
age,
|
||||
&elements,
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
let cursorless_capture = state.has_pending_screencopy(&output, false);
|
||||
let captured_with_cursor =
|
||||
state.process_screencopies(renderer, &framebuffer, &output, true);
|
||||
|
||||
if cursorless_capture {
|
||||
let cursorless_elements =
|
||||
output_elements(state, renderer, &output, false);
|
||||
redraw_output(
|
||||
WinitEvent::Input(event) => state.process_input_event(event),
|
||||
WinitEvent::Redraw => {
|
||||
state.advance_animations(Instant::now());
|
||||
let age = backend.buffer_age().unwrap_or(0);
|
||||
let render_started = Instant::now();
|
||||
let rendered = (|| -> DamageRenderResult {
|
||||
{
|
||||
let (renderer, mut framebuffer) = backend.bind()?;
|
||||
state.process_dmabuf_imports(renderer);
|
||||
let elements = animated_window_elements(state, renderer, &output);
|
||||
let effects = frame_effect_metrics(
|
||||
&elements,
|
||||
output.current_scale().fractional_scale(),
|
||||
);
|
||||
let result = damage_tracker.render_output(
|
||||
renderer,
|
||||
&mut framebuffer,
|
||||
&output,
|
||||
&cursorless_elements,
|
||||
age,
|
||||
&elements,
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
state.process_screencopies(renderer, &framebuffer, &output, false);
|
||||
redraw_output(renderer, &mut framebuffer, &output, &elements)?;
|
||||
} else if captured_with_cursor {
|
||||
let _ = renderer
|
||||
.render(
|
||||
&mut framebuffer,
|
||||
output.current_mode().expect("output has a mode").size,
|
||||
output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
}
|
||||
let cursorless_capture = state.has_pending_screencopy(&output, false);
|
||||
let captured_with_cursor =
|
||||
state.process_screencopies(renderer, &framebuffer, &output, true);
|
||||
|
||||
Ok((result.damage.cloned(), effects))
|
||||
}
|
||||
})();
|
||||
let (damage, effects) = match rendered {
|
||||
Ok((Some(damage), effects)) => (damage, effects),
|
||||
Ok((None, _)) => {
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
layer_map_for_output(&output).cleanup();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush clients");
|
||||
if cursorless_capture {
|
||||
let cursorless_elements =
|
||||
output_elements(state, renderer, &output, false);
|
||||
redraw_output(
|
||||
renderer,
|
||||
&mut framebuffer,
|
||||
&output,
|
||||
&cursorless_elements,
|
||||
)?;
|
||||
state.process_screencopies(renderer, &framebuffer, &output, false);
|
||||
redraw_output(renderer, &mut framebuffer, &output, &elements)?;
|
||||
} else if captured_with_cursor {
|
||||
let _ = renderer
|
||||
.render(
|
||||
&mut framebuffer,
|
||||
output.current_mode().expect("output has a mode").size,
|
||||
output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
}
|
||||
|
||||
Ok((result.damage.cloned(), effects))
|
||||
}
|
||||
backend.window().request_redraw();
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "nested renderer failed");
|
||||
})();
|
||||
let (damage, effects) = match rendered {
|
||||
Ok((Some(damage), effects)) => (damage, effects),
|
||||
Ok((None, _)) => {
|
||||
// A callback means permission to draw the next client
|
||||
// frame, not proof of a new compositor presentation.
|
||||
// No-damage frames must still unblock layer clients.
|
||||
send_nested_frame_callbacks(state, &output, refresh.get());
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
layer_map_for_output(&output).cleanup();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush clients");
|
||||
}
|
||||
return;
|
||||
}
|
||||
Err(error) => {
|
||||
tracing::error!(%error, "nested renderer failed");
|
||||
state.loop_signal.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(error) = backend.submit(Some(&damage)) {
|
||||
tracing::error!(%error, "nested buffer submission failed");
|
||||
state.loop_signal.stop();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Err(error) = backend.submit(Some(&damage)) {
|
||||
tracing::error!(%error, "nested buffer submission failed");
|
||||
state.loop_signal.stop();
|
||||
return;
|
||||
}
|
||||
render_metrics.record_frame(render_started.elapsed(), &damage, 0, effects);
|
||||
let elapsed = render_started.elapsed();
|
||||
if elapsed > refresh.get() {
|
||||
missed_deadlines += (elapsed.as_nanos() / refresh.get().as_nanos()) as u64;
|
||||
}
|
||||
render_metrics.record_frame(elapsed, &damage, missed_deadlines, effects);
|
||||
|
||||
let mut presentation = OutputPresentationFeedback::new(&output);
|
||||
state.space.elements().for_each(|window| {
|
||||
window.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.clone()),
|
||||
|_, _| PresentationKind::Vsync,
|
||||
let mut presentation = OutputPresentationFeedback::new(&output);
|
||||
state.space.elements().for_each(|window| {
|
||||
window.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.clone()),
|
||||
|_, _| PresentationKind::Vsync,
|
||||
);
|
||||
});
|
||||
layer_surfaces(&output).iter().for_each(|layer| {
|
||||
layer.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.clone()),
|
||||
|_, _| PresentationKind::Vsync,
|
||||
);
|
||||
});
|
||||
sequence = sequence.wrapping_add(1);
|
||||
presentation.presented(
|
||||
clock.now(),
|
||||
Refresh::fixed(refresh.get()),
|
||||
sequence,
|
||||
PresentationKind::Vsync,
|
||||
);
|
||||
});
|
||||
layer_surfaces(&output).iter().for_each(|layer| {
|
||||
layer.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.clone()),
|
||||
|_, _| PresentationKind::Vsync,
|
||||
);
|
||||
});
|
||||
sequence = sequence.wrapping_add(1);
|
||||
presentation.presented(
|
||||
clock.now(),
|
||||
Refresh::fixed(Duration::from_nanos(1_000_000_000 / 60)),
|
||||
sequence,
|
||||
PresentationKind::Vsync,
|
||||
);
|
||||
state.space.elements().for_each(|window| {
|
||||
window.send_frame(
|
||||
&output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
});
|
||||
layer_surfaces(&output).iter().for_each(|layer| {
|
||||
layer.send_frame(
|
||||
&output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
});
|
||||
state.send_cursor_frame(&output);
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
layer_map_for_output(&output).cleanup();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush clients");
|
||||
send_nested_frame_callbacks(state, &output, refresh.get());
|
||||
state.space.refresh();
|
||||
state.popups.cleanup();
|
||||
layer_map_for_output(&output).cleanup();
|
||||
if let Err(error) = state.display_handle.flush_clients() {
|
||||
tracing::debug!(%error, "failed to flush clients");
|
||||
}
|
||||
}
|
||||
backend.window().request_redraw();
|
||||
WinitEvent::CloseRequested => state.loop_signal.stop(),
|
||||
_ => {}
|
||||
}
|
||||
WinitEvent::CloseRequested => state.loop_signal.stop(),
|
||||
_ => {}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Duration) {
|
||||
let time = state.start_time.elapsed();
|
||||
for window in state.space.elements() {
|
||||
window.send_frame(output, time, Some(refresh), |_, _| Some(output.clone()));
|
||||
}
|
||||
for layer in layer_surfaces(output) {
|
||||
layer.send_frame(output, time, Some(refresh), |_, _| Some(output.clone()));
|
||||
}
|
||||
state.send_cursor_frame(output);
|
||||
}
|
||||
|
||||
pub(crate) fn animated_window_elements(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
@@ -875,6 +993,9 @@ fn output_elements(
|
||||
output: &Output,
|
||||
include_cursor: bool,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
if state.wallpaper.poll() {
|
||||
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
|
||||
}
|
||||
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -898,6 +1019,12 @@ fn output_elements(
|
||||
.rev()
|
||||
.filter_map(|window| {
|
||||
let id = *state.window_ids.get(window)?;
|
||||
// Scrolling columns may sit outside their monitor's rectangle.
|
||||
// They must not reappear on a neighboring output just because
|
||||
// their global animated coordinates overlap it.
|
||||
if !state.window_belongs_to_output(id, output) {
|
||||
return None;
|
||||
}
|
||||
let visual = state.presented_window_rect(id)?;
|
||||
let (close_scale, close_alpha) = state.closing_visual(id);
|
||||
let visual = scaled_visual_rect(visual, close_scale);
|
||||
@@ -912,16 +1039,22 @@ fn output_elements(
|
||||
|
||||
for (window, id, visual, decoration_progress, close_alpha) in windows {
|
||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
||||
let pixels = physical_rect(visual, output_geometry.loc, scale);
|
||||
// Only overview/close intentionally scale the complete application.
|
||||
let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0;
|
||||
let behavior = resize_content_behavior(scale_content);
|
||||
|
||||
let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current);
|
||||
if let Some(overlay) = window_dim_element(
|
||||
if let Some(overlay) = window_tint_element(
|
||||
state,
|
||||
renderer,
|
||||
id,
|
||||
constrain,
|
||||
pixels,
|
||||
scale,
|
||||
state.theme_settings.window_radius * decoration_progress,
|
||||
dim as f32 * close_alpha,
|
||||
[0.0, 0.0, 0.0, dim as f32 * close_alpha],
|
||||
false,
|
||||
output,
|
||||
) {
|
||||
// Front-to-back: dim the application and its border, not its shadow
|
||||
@@ -958,6 +1091,7 @@ fn output_elements(
|
||||
renderer,
|
||||
id,
|
||||
constrain,
|
||||
pixels,
|
||||
scale,
|
||||
window_radius,
|
||||
border_width,
|
||||
@@ -972,6 +1106,7 @@ fn output_elements(
|
||||
renderer,
|
||||
id,
|
||||
constrain,
|
||||
pixels,
|
||||
scale,
|
||||
window_radius,
|
||||
shadow_offset_y,
|
||||
@@ -981,10 +1116,52 @@ fn output_elements(
|
||||
output,
|
||||
&programs,
|
||||
);
|
||||
if !scale_content {
|
||||
if let Some(snapshot) = state.resize_snapshots.get(&id)
|
||||
&& snapshot.context == renderer.context_id().erased()
|
||||
&& (snapshot.scale - scale).abs() < 0.001
|
||||
{
|
||||
let size = snapshot.texture.size();
|
||||
let visible =
|
||||
Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels);
|
||||
if let Some(visible) = visible {
|
||||
let clip = framebuffer_clip_rect(
|
||||
pixels,
|
||||
output.current_mode().unwrap().size,
|
||||
output.current_transform().invert(),
|
||||
);
|
||||
elements.push(
|
||||
NativeTextureElement {
|
||||
id: snapshot.id.clone(),
|
||||
commit: snapshot.commit,
|
||||
texture: snapshot.texture.clone(),
|
||||
geometry: visible,
|
||||
source: Rectangle::from_size(Size::from((
|
||||
f64::from(visible.size.w),
|
||||
f64::from(visible.size.h),
|
||||
))),
|
||||
alpha: crate::presentation::handoff_alpha(snapshot.elapsed)
|
||||
* close_alpha,
|
||||
program: Some(programs.texture.clone()),
|
||||
uniforms: vec![
|
||||
Uniform::new("clip_rect", clip).into_owned(),
|
||||
Uniform::new(
|
||||
"radius",
|
||||
scaled_effect_value(window_radius, constrain, scale),
|
||||
)
|
||||
.into_owned(),
|
||||
],
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
elements.extend(rounded_window_elements(
|
||||
renderer,
|
||||
&window,
|
||||
constrain,
|
||||
pixels,
|
||||
scale,
|
||||
window_radius,
|
||||
close_alpha,
|
||||
@@ -994,7 +1171,31 @@ fn output_elements(
|
||||
.is_some_and(|geometry| geometry.presentation_changed),
|
||||
output,
|
||||
programs.texture.clone(),
|
||||
behavior,
|
||||
));
|
||||
let source = window.geometry().size;
|
||||
if !scale_content && (source.w < constrain.size.w || source.h < constrain.size.h) {
|
||||
let mut color = state.theme_settings.surface_base_color.0;
|
||||
color[3] = close_alpha;
|
||||
if let Some(fill) = window_tint_element(
|
||||
state,
|
||||
renderer,
|
||||
id,
|
||||
constrain,
|
||||
pixels,
|
||||
scale,
|
||||
window_radius,
|
||||
color,
|
||||
true,
|
||||
output,
|
||||
) {
|
||||
// Front-to-back: fill uncovered strips behind the native
|
||||
// content instead of stretching it or exposing wallpaper.
|
||||
elements.push(fill.into());
|
||||
}
|
||||
} else {
|
||||
state.window_resize_fills.remove(&id);
|
||||
}
|
||||
if let Some(shadow) = shadow {
|
||||
elements.push(shadow.into());
|
||||
}
|
||||
@@ -1012,7 +1213,7 @@ fn output_elements(
|
||||
constrain,
|
||||
ConstrainBehavior {
|
||||
reference: ConstrainReference::Geometry,
|
||||
behavior: ConstrainScaleBehavior::Stretch,
|
||||
behavior,
|
||||
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||
},
|
||||
));
|
||||
@@ -1029,6 +1230,9 @@ fn output_elements(
|
||||
output,
|
||||
&[Layer::Bottom, Layer::Background],
|
||||
));
|
||||
if let Some(wallpaper) = state.wallpaper.element(renderer, output) {
|
||||
elements.push(wallpaper.into());
|
||||
}
|
||||
elements
|
||||
}
|
||||
|
||||
@@ -1114,13 +1318,14 @@ fn window_border_element(
|
||||
renderer: &GlesRenderer,
|
||||
id: ferese_layout::WindowId,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
physical: Rectangle<i32, Physical>,
|
||||
scale: f64,
|
||||
requested_radius: f64,
|
||||
requested_width: f64,
|
||||
color: [f32; 4],
|
||||
output: &Output,
|
||||
programs: &RoundedClipPrograms,
|
||||
) -> Option<PixelShaderElement> {
|
||||
) -> Option<PhysicalShaderElement> {
|
||||
let mode = output.current_mode()?;
|
||||
let width = scaled_effect_value(requested_width, geometry, scale);
|
||||
if width == 0.0 {
|
||||
@@ -1129,11 +1334,7 @@ fn window_border_element(
|
||||
|
||||
let parameters = BorderParameters {
|
||||
geometry,
|
||||
clip_rect: framebuffer_clip_rect(
|
||||
geometry.to_physical_precise_round(scale),
|
||||
mode.size,
|
||||
output.current_transform().invert(),
|
||||
),
|
||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||
radius: scaled_effect_value(requested_radius, geometry, scale),
|
||||
width,
|
||||
color,
|
||||
@@ -1167,7 +1368,10 @@ fn window_border_element(
|
||||
cached.parameters = parameters;
|
||||
}
|
||||
|
||||
Some(cached.element.clone())
|
||||
Some(PhysicalShaderElement {
|
||||
inner: cached.element.clone(),
|
||||
geometry: physical,
|
||||
})
|
||||
}
|
||||
|
||||
fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
|
||||
@@ -1180,32 +1384,34 @@ fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn window_dim_element(
|
||||
fn window_tint_element(
|
||||
state: &mut Ferese,
|
||||
renderer: &mut GlesRenderer,
|
||||
id: ferese_layout::WindowId,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
physical: Rectangle<i32, Physical>,
|
||||
scale: f64,
|
||||
radius: f64,
|
||||
amount: f32,
|
||||
color: [f32; 4],
|
||||
resize_fill: bool,
|
||||
output: &Output,
|
||||
) -> Option<PixelShaderElement> {
|
||||
if amount <= 0.0 {
|
||||
state.window_dims.remove(&id);
|
||||
) -> Option<PhysicalShaderElement> {
|
||||
if color[3] <= 0.0 {
|
||||
if resize_fill {
|
||||
state.window_resize_fills.remove(&id);
|
||||
} else {
|
||||
state.window_dims.remove(&id);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
let mode = output.current_mode()?;
|
||||
let program = material_program(state, renderer)?;
|
||||
let parameters = BorderParameters {
|
||||
geometry,
|
||||
clip_rect: framebuffer_clip_rect(
|
||||
geometry.to_physical_precise_round(scale),
|
||||
mode.size,
|
||||
output.current_transform().invert(),
|
||||
),
|
||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||
radius: scaled_effect_value(radius, geometry, scale),
|
||||
width: 0.0,
|
||||
color: [0.0, 0.0, 0.0, amount.clamp(0.0, 1.0)],
|
||||
color,
|
||||
};
|
||||
let uniforms = |p: &BorderParameters| {
|
||||
vec![
|
||||
@@ -1218,7 +1424,11 @@ fn window_dim_element(
|
||||
]
|
||||
};
|
||||
let context = renderer.context_id().erased();
|
||||
let buffers = state.window_dims.entry(id).or_default();
|
||||
let buffers = if resize_fill {
|
||||
state.window_resize_fills.entry(id).or_default()
|
||||
} else {
|
||||
state.window_dims.entry(id).or_default()
|
||||
};
|
||||
let cached = buffers
|
||||
.contexts
|
||||
.entry(context)
|
||||
@@ -1242,7 +1452,10 @@ fn window_dim_element(
|
||||
cached.element.update_uniforms(uniforms(¶meters));
|
||||
cached.parameters = parameters;
|
||||
}
|
||||
Some(cached.element.clone())
|
||||
Some(PhysicalShaderElement {
|
||||
inner: cached.element.clone(),
|
||||
geometry: physical,
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -1251,6 +1464,7 @@ fn window_shadow_element(
|
||||
renderer: &GlesRenderer,
|
||||
id: ferese_layout::WindowId,
|
||||
geometry: Rectangle<i32, Logical>,
|
||||
physical: Rectangle<i32, Physical>,
|
||||
scale: f64,
|
||||
requested_radius: f64,
|
||||
offset_y: f64,
|
||||
@@ -1259,22 +1473,26 @@ fn window_shadow_element(
|
||||
color: [f32; 4],
|
||||
output: &Output,
|
||||
programs: &RoundedClipPrograms,
|
||||
) -> Option<PixelShaderElement> {
|
||||
) -> Option<PhysicalShaderElement> {
|
||||
let mode = output.current_mode()?;
|
||||
if opacity == 0.0 || color[3] == 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let shadow_geometry = Rectangle::new(
|
||||
(geometry.loc.x, geometry.loc.y + offset_y.round() as i32).into(),
|
||||
geometry.size,
|
||||
(
|
||||
physical.loc.x,
|
||||
physical.loc.y + (offset_y * scale).round() as i32,
|
||||
)
|
||||
.into(),
|
||||
physical.size,
|
||||
);
|
||||
let bounds = shadow_bounds(geometry, offset_y, blur);
|
||||
let parameters = ShadowParameters {
|
||||
blur: (blur * scale) as f32,
|
||||
bounds,
|
||||
shadow_rect: framebuffer_clip_rect(
|
||||
shadow_geometry.to_physical_precise_round(scale),
|
||||
shadow_geometry,
|
||||
mode.size,
|
||||
output.current_transform().invert(),
|
||||
),
|
||||
@@ -1311,7 +1529,22 @@ fn window_shadow_element(
|
||||
cached.parameters = parameters;
|
||||
}
|
||||
|
||||
Some(cached.element.clone())
|
||||
let extent = (blur * scale * 2.0).ceil() as i32;
|
||||
Some(PhysicalShaderElement {
|
||||
inner: cached.element.clone(),
|
||||
geometry: Rectangle::new(
|
||||
(
|
||||
shadow_geometry.loc.x - extent,
|
||||
shadow_geometry.loc.y - extent,
|
||||
)
|
||||
.into(),
|
||||
(
|
||||
shadow_geometry.size.w + 2 * extent,
|
||||
shadow_geometry.size.h + 2 * extent,
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
|
||||
@@ -1350,12 +1583,14 @@ fn rounded_window_elements(
|
||||
renderer: &mut GlesRenderer,
|
||||
window: &smithay::desktop::Window,
|
||||
constrain: Rectangle<i32, Logical>,
|
||||
physical_constrain: Rectangle<i32, Physical>,
|
||||
scale: f64,
|
||||
requested_radius: f64,
|
||||
alpha: f32,
|
||||
clip_changed: bool,
|
||||
output: &Output,
|
||||
program: GlesTexProgram,
|
||||
behavior: ConstrainScaleBehavior,
|
||||
) -> Vec<AnimatedWindowRenderElement> {
|
||||
let Some(toplevel) = window.toplevel() else {
|
||||
return Vec::new();
|
||||
@@ -1366,8 +1601,7 @@ fn rounded_window_elements(
|
||||
|
||||
let geometry = window.geometry();
|
||||
let reference = geometry.to_physical_precise_round(scale);
|
||||
let physical_constrain = constrain.to_physical_precise_round(scale);
|
||||
let location = (constrain.loc - geometry.loc).to_physical_precise_round(scale);
|
||||
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
||||
let clip = framebuffer_clip_rect(
|
||||
physical_constrain,
|
||||
mode.size,
|
||||
@@ -1426,7 +1660,7 @@ fn rounded_window_elements(
|
||||
location,
|
||||
physical_constrain,
|
||||
reference,
|
||||
ConstrainScaleBehavior::Stretch,
|
||||
behavior,
|
||||
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||
scale,
|
||||
)
|
||||
@@ -1434,6 +1668,14 @@ fn rounded_window_elements(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn resize_content_behavior(intentional_scale: bool) -> ConstrainScaleBehavior {
|
||||
if intentional_scale {
|
||||
ConstrainScaleBehavior::Stretch
|
||||
} else {
|
||||
ConstrainScaleBehavior::CutOff
|
||||
}
|
||||
}
|
||||
|
||||
fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||
let scale = scale.clamp(0.0, 1.0);
|
||||
let width = rect.width * scale;
|
||||
@@ -2056,8 +2298,8 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::{
|
||||
color_with_alpha, framebuffer_clip_rect, normalized_scale, rounded_visual_rect,
|
||||
scaled_visual_rect, shadow_bounds,
|
||||
color_with_alpha, framebuffer_clip_rect, normalized_scale, resize_content_behavior,
|
||||
rounded_visual_rect, scaled_visual_rect, shadow_bounds,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -2114,6 +2356,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_keeps_source_pixels_native_when_growing_and_shrinking() {
|
||||
use smithay::backend::renderer::element::utils::{
|
||||
ConstrainAlign, constrain_render_elements,
|
||||
};
|
||||
for scale in [1.0, 1.5, 1.8, 2.0] {
|
||||
for width in [300, 600, 1200] {
|
||||
let source = Rectangle::<i32, Logical>::from_size((600, 800).into());
|
||||
let destination = Rectangle::<i32, Logical>::from_size((width, 800).into());
|
||||
let element = constrain_render_elements(
|
||||
[DamageElement::new(source)],
|
||||
(0, 0),
|
||||
destination.to_physical_precise_round(scale),
|
||||
source.to_physical_precise_round(scale),
|
||||
resize_content_behavior(false),
|
||||
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||
scale,
|
||||
)
|
||||
.next()
|
||||
.unwrap();
|
||||
// Comparing destination pixels to sampled source pixels catches
|
||||
// stretching, unlike tests of configure timing alone.
|
||||
let physical = element.geometry(scale.into());
|
||||
let sampled = element.src();
|
||||
assert!((f64::from(physical.size.w) / sampled.size.w - scale).abs() < 0.01);
|
||||
assert!((f64::from(physical.size.h) / sampled.size.h - scale).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overview_retains_intentional_content_scaling() {
|
||||
assert!(matches!(
|
||||
resize_content_behavior(true),
|
||||
smithay::backend::renderer::element::utils::ConstrainScaleBehavior::Stretch
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_positive_finite_scale() {
|
||||
assert_eq!(normalized_scale(1.25), 1.25);
|
||||
|
||||
@@ -380,8 +380,9 @@ impl WindowGeometry {
|
||||
self.presentation_size_request(now)
|
||||
}
|
||||
|
||||
/// Configure the destination once. Intermediate sizes make clients reflow
|
||||
/// and replace buffers on every frame, disrupting an otherwise smooth zoom.
|
||||
/// Request the destination once, independently of the presentation clock.
|
||||
/// The compositor coordinates commits; delaying this until settlement
|
||||
/// forces presentation to use an obsolete buffer for the entire resize.
|
||||
pub fn presentation_size_request(&mut self, now: Duration) -> Option<ClientSize> {
|
||||
let size = ClientSize::from_rect(self.logical);
|
||||
self.client.request_size(size, now).then_some(size)
|
||||
@@ -710,7 +711,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zoom_requests_destination_once_even_with_a_responsive_client() {
|
||||
fn zoom_configures_destination_once_before_animating() {
|
||||
let start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||
let end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||
@@ -718,20 +719,72 @@ mod tests {
|
||||
geometry.set_presentation_target(end, true, Duration::ZERO),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
geometry.client.commit(ClientSize::from_rect(end));
|
||||
for frame in 0..240 {
|
||||
let now = Duration::from_millis(frame * 8);
|
||||
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||
assert_eq!(
|
||||
geometry.presentation_size_request(Duration::from_millis(frame * 8)),
|
||||
None
|
||||
);
|
||||
assert_eq!(geometry.presentation_size_request(now), None);
|
||||
}
|
||||
assert_eq!(geometry.visual.current, end);
|
||||
assert_eq!(
|
||||
geometry.set_presentation_target(start, false, Duration::from_secs(2)),
|
||||
Some(ClientSize::from_rect(start))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animated_resize_configures_each_new_target_without_framewise_requests() {
|
||||
let start = Rect::new(10.0, 10.0, 600.0, 800.0);
|
||||
let middle = Rect::new(10.0, 10.0, 900.0, 800.0);
|
||||
let end = Rect::new(10.0, 10.0, 450.0, 800.0);
|
||||
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||
assert_eq!(
|
||||
geometry.set_presentation_mode(middle, PresentationMode::Normal, Duration::ZERO),
|
||||
Some(ClientSize::from_rect(middle))
|
||||
);
|
||||
geometry.advance(Duration::from_millis(64), SpringConfig::default(), true);
|
||||
assert_eq!(
|
||||
geometry.presentation_size_request(Duration::from_millis(64)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
geometry.set_presentation_mode(
|
||||
end,
|
||||
PresentationMode::Normal,
|
||||
Duration::from_millis(64)
|
||||
),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
let mut requests = Vec::new();
|
||||
for frame in 1..240 {
|
||||
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||
if let Some(size) =
|
||||
geometry.presentation_size_request(Duration::from_millis(64 + frame * 8))
|
||||
{
|
||||
assert_eq!(geometry.visual.current.width, end.width);
|
||||
requests.push(size);
|
||||
}
|
||||
}
|
||||
assert!(requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pointer_resize_stays_live_and_reduced_motion_configures_immediately() {
|
||||
let start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||
let end = Rect::new(0.0, 0.0, 900.0, 800.0);
|
||||
let mut pointer = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||
assert_eq!(
|
||||
pointer.follow_pointer(end, Duration::ZERO),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
let mut reduced = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||
assert_eq!(
|
||||
reduced.set_presentation_target(end, true, Duration::ZERO),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
reduced.advance(Duration::ZERO, SpringConfig::default(), false);
|
||||
assert_eq!(reduced.presentation_size_request(Duration::ZERO), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn left_and_right_zoom_edges_do_not_reverse_direction() {
|
||||
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||
|
||||
@@ -271,7 +271,7 @@ impl OutputWorkspaceMap {
|
||||
return Ok(WorkspaceSwitch::FocusedExisting(owner));
|
||||
}
|
||||
|
||||
self.assignments.insert(workspace, output);
|
||||
let reassigned = self.assignments.insert(workspace, output) != Some(output);
|
||||
let state = self
|
||||
.outputs
|
||||
.get_mut(&output)
|
||||
@@ -279,7 +279,9 @@ impl OutputWorkspaceMap {
|
||||
state.workspaces.insert(workspace);
|
||||
state.active = workspace;
|
||||
self.focused = Some(output);
|
||||
self.bump_revision(workspace);
|
||||
if reassigned {
|
||||
self.bump_revision(workspace);
|
||||
}
|
||||
|
||||
debug_assert!(self.validate());
|
||||
Ok(WorkspaceSwitch::Activated(output))
|
||||
@@ -482,7 +484,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_workspace_selection_prevents_automatic_return() {
|
||||
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
@@ -498,14 +500,63 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||
Ok(WorkspaceId(3))
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(1))
|
||||
Some(OutputId(2))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] {
|
||||
let remaining = if removed == OutputId(1) {
|
||||
OutputId(2)
|
||||
} else {
|
||||
OutputId(1)
|
||||
};
|
||||
outputs.focus_output(removed).unwrap();
|
||||
outputs.disconnect(removed).unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(remaining)
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(remaining)
|
||||
);
|
||||
// Switching between handed-off workspaces is normal use, not an
|
||||
// explicit change of their home output.
|
||||
outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap();
|
||||
outputs.switch_workspace(remaining, WorkspaceId(2)).unwrap();
|
||||
outputs
|
||||
.connect(
|
||||
removed,
|
||||
geometry(if removed == OutputId(1) { 0 } else { 1920 }),
|
||||
WorkspaceId(3),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selecting_a_workspace_owned_by_another_output_focuses_that_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) struct ShellSnapshot {
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct OutputSnapshot {
|
||||
pub(crate) id: u64,
|
||||
pub(crate) name: String,
|
||||
pub(crate) active_workspace: u64,
|
||||
pub(crate) focused: bool,
|
||||
}
|
||||
@@ -199,12 +200,13 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
ferese_shell_v1::Event::Output {
|
||||
output_hi,
|
||||
output_lo,
|
||||
name: _,
|
||||
name,
|
||||
active_workspace_hi,
|
||||
active_workspace_lo,
|
||||
focused,
|
||||
} => state.pending.outputs.push(OutputSnapshot {
|
||||
id: join_id(output_hi, output_lo),
|
||||
name,
|
||||
active_workspace: join_id(active_workspace_hi, active_workspace_lo),
|
||||
focused: focused != 0,
|
||||
}),
|
||||
|
||||
+266
-103
@@ -18,8 +18,8 @@ use cosmic::iced::alignment;
|
||||
use cosmic::iced::event::{self, PlatformSpecific, wayland};
|
||||
use cosmic::iced::platform_specific::{
|
||||
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
||||
shell::commands::layer_surface::set_input_zone,
|
||||
shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer},
|
||||
shell::commands::layer_surface::{destroy_layer_surface, set_input_zone},
|
||||
};
|
||||
use cosmic::iced::{
|
||||
Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, window,
|
||||
@@ -34,7 +34,7 @@ use jiff::Zoned;
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::{wl_registry, wl_surface},
|
||||
protocol::{wl_output, wl_registry, wl_surface},
|
||||
};
|
||||
|
||||
use crate::config::{ShellConfig, ShellTheme, WallpaperMode};
|
||||
@@ -102,7 +102,13 @@ fn main() -> cosmic::iced::Result {
|
||||
// toolkit threads are started.
|
||||
unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") };
|
||||
}
|
||||
let config = config::load();
|
||||
let mut config = config::load();
|
||||
let compositor_wallpaper = std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_some();
|
||||
if compositor_wallpaper {
|
||||
// The compositor owns one GPU image, shared across output renderers;
|
||||
// do not decode another copy or allocate full-screen software buffers.
|
||||
config.wallpaper.path = None;
|
||||
}
|
||||
let shell_font = configured_font(config.font_family.as_deref());
|
||||
let _ = SHELL_FONT.set(shell_font);
|
||||
// Decode alongside toolkit/GPU initialization, never during a UI draw.
|
||||
@@ -141,13 +147,22 @@ struct FereseShell {
|
||||
snapshot: ShellSnapshot,
|
||||
overview_active: bool,
|
||||
clock: String,
|
||||
effects: Option<EffectsBinding>,
|
||||
outputs: Vec<OutputSurfaces>,
|
||||
status_service: status::Service,
|
||||
status: status::Snapshot,
|
||||
status_error: Option<String>,
|
||||
menu: Option<status_ui::OpenMenu>,
|
||||
}
|
||||
|
||||
struct OutputSurfaces {
|
||||
output: wl_output::WlOutput,
|
||||
name: Option<String>,
|
||||
bar: window::Id,
|
||||
wallpaper: Option<window::Id>,
|
||||
effects: Option<EffectsBinding>,
|
||||
hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
WallpaperLoaded(Result<image::Handle, String>),
|
||||
@@ -163,6 +178,7 @@ enum Message {
|
||||
StatusTick,
|
||||
AnimateMenu,
|
||||
OpenMenu(status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
||||
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
||||
Control(status::Action),
|
||||
ConfirmPower(status::Action),
|
||||
CancelPower,
|
||||
@@ -185,9 +201,6 @@ impl cosmic::Application for FereseShell {
|
||||
|
||||
fn init(core: Core, (config, wallpaper): Self::Flags) -> (Self, Task<Self::Message>) {
|
||||
let bar_surface_id = window::Id::unique();
|
||||
let wallpaper_surface_id = window::Id::unique();
|
||||
let shell_theme = config.theme;
|
||||
let bar = BarMetrics::from(shell_theme);
|
||||
let app = Self {
|
||||
core,
|
||||
bar_surface_id,
|
||||
@@ -205,58 +218,8 @@ impl cosmic::Application for FereseShell {
|
||||
snapshot: ShellSnapshot::default(),
|
||||
overview_active: false,
|
||||
clock: current_time(),
|
||||
effects: None,
|
||||
outputs: Vec::new(),
|
||||
};
|
||||
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
|_| Default::default(),
|
||||
move |_| SctkLayerSurfaceSettings {
|
||||
id: wallpaper_surface_id,
|
||||
layer: Layer::Background,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
input_zone: Some(Vec::new()),
|
||||
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
|
||||
output: IcedOutput::Active,
|
||||
namespace: "ferese-shell-wallpaper".to_owned(),
|
||||
// Wallpaper covers the full output, including beneath the bar
|
||||
// and its floating margins. Zero would use the remaining workspace.
|
||||
exclusive_zone: -1,
|
||||
size: Some((None, None)),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_wallpaper)),
|
||||
);
|
||||
let bar_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
|_| Default::default(),
|
||||
move |_| SctkLayerSurfaceSettings {
|
||||
id: bar_surface_id,
|
||||
layer: Layer::Top,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
||||
output: IcedOutput::Active,
|
||||
namespace: "ferese-shell-top-bar".to_owned(),
|
||||
margin: IcedMargin {
|
||||
top: shell_theme.bar_margin_top,
|
||||
right: shell_theme.bar_margin_horizontal,
|
||||
bottom: 0,
|
||||
left: shell_theme.bar_margin_horizontal,
|
||||
},
|
||||
size: Some((None, Some(bar.height.round() as u32))),
|
||||
size_limits: Limits::NONE,
|
||||
// Reserve breathing room below the visible bar; layer-shell
|
||||
// accounts for the top margin separately.
|
||||
exclusive_zone: (bar.height.round() as i32)
|
||||
.saturating_add(shell_theme.bar_window_gap),
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_layer)),
|
||||
);
|
||||
|
||||
// Submit the small interactive surface before the full-screen image.
|
||||
let tasks = [bar_action, wallpaper_action]
|
||||
.map(cosmic::Action::Surface)
|
||||
.map(cosmic::task::message);
|
||||
|
||||
let wallpaper_task = match wallpaper {
|
||||
Some(receiver) => cosmic::task::future(async move {
|
||||
cosmic::Action::App(Message::WallpaperLoaded(
|
||||
@@ -267,7 +230,7 @@ impl cosmic::Application for FereseShell {
|
||||
}),
|
||||
None => Task::none(),
|
||||
};
|
||||
(app, Task::batch([Task::batch(tasks), wallpaper_task]))
|
||||
(app, wallpaper_task)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
@@ -281,7 +244,9 @@ impl cosmic::Application for FereseShell {
|
||||
Event::Keyboard(_)
|
||||
| Event::Window(window::Event::Opened { .. } | window::Event::Closed)
|
||||
| Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Popup(..) | wayland::Event::Layer(..),
|
||||
wayland::Event::Popup(..)
|
||||
| wayland::Event::Layer(..)
|
||||
| wayland::Event::Output(..),
|
||||
)) => Some(Message::Event(event, id)),
|
||||
_ => None,
|
||||
}),
|
||||
@@ -314,8 +279,10 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
Message::NativeSurface(id, result) => {
|
||||
match result {
|
||||
Ok((_connection, surface)) if id == self.bar_surface_id => {
|
||||
self.attach_effects(&surface)
|
||||
Ok((_connection, surface))
|
||||
if self.outputs.iter().any(|entry| entry.bar == id) =>
|
||||
{
|
||||
self.attach_effects(id, &surface)
|
||||
}
|
||||
Ok((_connection, surface)) => {
|
||||
if let Some(menu) = &mut self.menu
|
||||
@@ -345,6 +312,16 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
Message::AnimateMenu => Task::none(),
|
||||
Message::OpenMenu(kind, anchor) => self.open_menu(kind, anchor),
|
||||
Message::OpenMenuOn(id, kind, anchor) => {
|
||||
if self.bar_surface_id != id {
|
||||
let destroy = self.destroy_menu();
|
||||
self.bar_surface_id = id;
|
||||
let open = self.open_menu(kind, anchor);
|
||||
Task::batch([destroy, open])
|
||||
} else {
|
||||
self.open_menu(kind, anchor)
|
||||
}
|
||||
}
|
||||
Message::ConfirmPower(action) => {
|
||||
if let Some(menu) = &mut self.menu {
|
||||
menu.confirm = Some(action);
|
||||
@@ -383,25 +360,36 @@ impl cosmic::Application for FereseShell {
|
||||
self.overview_active = active;
|
||||
}
|
||||
if let Some(snapshot) = poll.snapshot {
|
||||
let was_hidden = bar_hidden(&self.snapshot);
|
||||
|
||||
self.snapshot = snapshot;
|
||||
|
||||
let hidden = bar_hidden(&self.snapshot);
|
||||
if hidden != was_hidden {
|
||||
if let Some(effects) = &self.effects
|
||||
&& let Err(error) = effects.set_visible(!hidden)
|
||||
{
|
||||
eprintln!("ferese-shell: could not update panel material: {error}");
|
||||
let mut tasks = Vec::new();
|
||||
for entry in &mut self.outputs {
|
||||
let output = self
|
||||
.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| Some(output.name.as_str()) == entry.name.as_deref())
|
||||
.map(|output| output.id);
|
||||
let hidden = output_bar_hidden(&self.snapshot, output);
|
||||
if entry.hidden == hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let input_zone = if hidden { Some(Vec::new()) } else { None };
|
||||
|
||||
return Task::batch([
|
||||
set_input_zone(self.bar_surface_id, input_zone),
|
||||
self.destroy_menu(),
|
||||
]);
|
||||
entry.hidden = hidden;
|
||||
if let Some(effects) = &entry.effects {
|
||||
if let Err(error) = effects.set_visible(!hidden) {
|
||||
eprintln!(
|
||||
"ferese-shell: could not update panel material: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
tasks.push(set_input_zone(
|
||||
entry.bar,
|
||||
if hidden { Some(Vec::new()) } else { None },
|
||||
));
|
||||
}
|
||||
if self.bar_hidden(self.bar_surface_id) {
|
||||
tasks.push(self.destroy_menu());
|
||||
}
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
@@ -444,10 +432,149 @@ impl cosmic::Application for FereseShell {
|
||||
}
|
||||
|
||||
impl FereseShell {
|
||||
fn output_event(
|
||||
&mut self,
|
||||
event: wayland::OutputEvent,
|
||||
output: wl_output::WlOutput,
|
||||
) -> Task<Message> {
|
||||
if matches!(event, wayland::OutputEvent::Removed) {
|
||||
if let Some(index) = self.outputs.iter().position(|entry| entry.output == output) {
|
||||
let entry = self.outputs.remove(index);
|
||||
let menu = if self.bar_surface_id == entry.bar {
|
||||
self.destroy_menu()
|
||||
} else {
|
||||
Task::none()
|
||||
};
|
||||
let mut tasks = vec![destroy_layer_surface(entry.bar), menu];
|
||||
if let Some(wallpaper) = entry.wallpaper {
|
||||
tasks.push(destroy_layer_surface(wallpaper));
|
||||
}
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let name = match event {
|
||||
wayland::OutputEvent::Created(info) => info.and_then(|info| info.name),
|
||||
wayland::OutputEvent::InfoUpdate(info) => info.name,
|
||||
_ => None,
|
||||
};
|
||||
if let Some(entry) = self.outputs.iter_mut().find(|entry| entry.output == output) {
|
||||
if name.is_some() {
|
||||
entry.name = name;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
let bar_surface_id = window::Id::unique();
|
||||
let wallpaper_surface_id = window::Id::unique();
|
||||
let shell_theme = self.config.theme;
|
||||
let bar = BarMetrics::from(shell_theme);
|
||||
let wallpaper_output = output.clone();
|
||||
let bar_output = output.clone();
|
||||
let hidden = output_bar_hidden(
|
||||
&self.snapshot,
|
||||
self.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| Some(output.name.as_str()) == name.as_deref())
|
||||
.map(|output| output.id),
|
||||
);
|
||||
self.outputs.push(OutputSurfaces {
|
||||
output,
|
||||
name,
|
||||
bar: bar_surface_id,
|
||||
wallpaper: std::env::var_os("FERESE_COMPOSITOR_WALLPAPER")
|
||||
.is_none()
|
||||
.then_some(wallpaper_surface_id),
|
||||
effects: None,
|
||||
hidden,
|
||||
});
|
||||
let wallpaper_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
|_| Default::default(),
|
||||
move |_| SctkLayerSurfaceSettings {
|
||||
id: wallpaper_surface_id,
|
||||
layer: Layer::Background,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
input_zone: Some(Vec::new()),
|
||||
anchor: Anchor::TOP | Anchor::RIGHT | Anchor::BOTTOM | Anchor::LEFT,
|
||||
output: IcedOutput::Output(wallpaper_output.clone()),
|
||||
namespace: "ferese-shell-wallpaper".to_owned(),
|
||||
// Wallpaper covers the full output, including beneath the bar
|
||||
// and its floating margins. Zero would use the remaining workspace.
|
||||
exclusive_zone: -1,
|
||||
size: Some((None, None)),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_wallpaper)),
|
||||
);
|
||||
let bar_action = cosmic::surface::action::app_layer_shell::<Self>(
|
||||
|_| Default::default(),
|
||||
move |_| SctkLayerSurfaceSettings {
|
||||
id: bar_surface_id,
|
||||
input_zone: hidden.then(Vec::new),
|
||||
layer: Layer::Top,
|
||||
keyboard_interactivity: KeyboardInteractivity::None,
|
||||
anchor: Anchor::TOP | Anchor::LEFT | Anchor::RIGHT,
|
||||
output: IcedOutput::Output(bar_output.clone()),
|
||||
namespace: "ferese-shell-top-bar".to_owned(),
|
||||
margin: IcedMargin {
|
||||
top: shell_theme.bar_margin_top,
|
||||
right: shell_theme.bar_margin_horizontal,
|
||||
bottom: 0,
|
||||
left: shell_theme.bar_margin_horizontal,
|
||||
},
|
||||
size: Some((None, Some(bar.height.round() as u32))),
|
||||
size_limits: Limits::NONE,
|
||||
// Reserve breathing room below the visible bar; layer-shell
|
||||
// accounts for the top margin separately.
|
||||
exclusive_zone: (bar.height.round() as i32)
|
||||
.saturating_add(shell_theme.bar_window_gap),
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
|
||||
);
|
||||
|
||||
// Submit the small interactive surface before the full-screen image.
|
||||
let mut surfaces = vec![bar_action];
|
||||
if std::env::var_os("FERESE_COMPOSITOR_WALLPAPER").is_none() {
|
||||
surfaces.push(wallpaper_action);
|
||||
}
|
||||
let tasks = surfaces
|
||||
.into_iter()
|
||||
.map(cosmic::Action::Surface)
|
||||
.map(cosmic::task::message);
|
||||
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> {
|
||||
let name = self
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|entry| entry.bar == id)?
|
||||
.name
|
||||
.as_deref()?;
|
||||
self.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.name == name)
|
||||
}
|
||||
|
||||
fn bar_hidden(&self, id: window::Id) -> bool {
|
||||
output_bar_hidden(
|
||||
&self.snapshot,
|
||||
self.output_for_bar(id).map(|output| output.id),
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
||||
match event {
|
||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||
event,
|
||||
output,
|
||||
))) => self.output_event(event, output),
|
||||
Event::Window(window::Event::Opened { .. })
|
||||
if id == self.bar_surface_id
|
||||
if self.outputs.iter().any(|entry| entry.bar == id)
|
||||
|| self.menu.as_ref().is_some_and(|menu| menu.id == id) =>
|
||||
{
|
||||
window::run(id, native_wayland_surface)
|
||||
@@ -510,9 +637,7 @@ impl FereseShell {
|
||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||
Task::none()
|
||||
}
|
||||
Event::Window(window::Event::Closed) if id == self.bar_surface_id => {
|
||||
cosmic::iced::exit()
|
||||
}
|
||||
Event::Window(window::Event::Closed) => Task::none(),
|
||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
||||
_,
|
||||
surface,
|
||||
@@ -535,49 +660,48 @@ impl FereseShell {
|
||||
_,
|
||||
surface,
|
||||
frame_id,
|
||||
))) if frame_id == self.bar_surface_id => {
|
||||
self.attach_effects(&surface);
|
||||
))) if self.outputs.iter().any(|entry| entry.bar == frame_id) => {
|
||||
self.attach_effects(frame_id, &surface);
|
||||
Task::none()
|
||||
}
|
||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(
|
||||
_,
|
||||
surface,
|
||||
layer_id,
|
||||
))) if layer_id == self.bar_surface_id => {
|
||||
self.attach_effects(&surface);
|
||||
))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => {
|
||||
self.attach_effects(layer_id, &surface);
|
||||
Task::none()
|
||||
}
|
||||
_ => Task::none(),
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_effects(&mut self, surface: &wl_surface::WlSurface) {
|
||||
if self.effects.is_some() {
|
||||
fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
|
||||
let hidden = self.bar_hidden(id);
|
||||
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
|
||||
return;
|
||||
};
|
||||
if entry.effects.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||
|
||||
match EffectsBinding::attach(surface, !bar_hidden(&self.snapshot)) {
|
||||
Ok(binding) => self.effects = Some(binding),
|
||||
match EffectsBinding::attach(surface, !hidden) {
|
||||
Ok(binding) => entry.effects = Some(binding),
|
||||
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn view_layer(&self) -> Element<'_, cosmic::Action<Message>> {
|
||||
if bar_hidden(&self.snapshot) {
|
||||
fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||
if self.bar_hidden(id) {
|
||||
return container(text(""))
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
}
|
||||
|
||||
let focused_output = self
|
||||
.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.focused)
|
||||
.or_else(|| self.snapshot.outputs.first());
|
||||
let focused_output = self.output_for_bar(id);
|
||||
let focused_output_id = focused_output.map(|output| output.id);
|
||||
let shell_theme = self.config.theme.for_bar();
|
||||
let bar = BarMetrics::from(shell_theme);
|
||||
@@ -670,9 +794,16 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(foreground)),
|
||||
)
|
||||
.padding([0, 6]);
|
||||
let right = row![self.view_status_bar(), clock]
|
||||
.spacing(6)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let right = row![
|
||||
self.view_status_bar().map(move |action| match action {
|
||||
cosmic::Action::App(Message::OpenMenu(kind, anchor)) =>
|
||||
cosmic::Action::App(Message::OpenMenuOn(id, kind, anchor)),
|
||||
other => other,
|
||||
}),
|
||||
clock
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(cosmic::iced::Alignment::Center);
|
||||
let content = row![
|
||||
container(left).width(Length::Fill),
|
||||
container(right).width(Length::Shrink),
|
||||
@@ -681,7 +812,10 @@ impl FereseShell {
|
||||
.align_y(cosmic::iced::Alignment::Center)
|
||||
.height(Length::Fill);
|
||||
|
||||
let compositor_material = self.effects.is_some();
|
||||
let compositor_material = self
|
||||
.outputs
|
||||
.iter()
|
||||
.any(|entry| entry.bar == id && entry.effects.is_some());
|
||||
container(content)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -777,12 +911,22 @@ fn overview_control(
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
|
||||
let active_workspace = snapshot
|
||||
let output = snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|output| output.focused)
|
||||
.or_else(|| snapshot.outputs.first())
|
||||
.map(|output| output.id);
|
||||
output_bar_hidden(snapshot, output)
|
||||
}
|
||||
|
||||
fn output_bar_hidden(snapshot: &ShellSnapshot, output: Option<u64>) -> bool {
|
||||
let active_workspace = snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|candidate| Some(candidate.id) == output)
|
||||
.map(|output| output.active_workspace);
|
||||
|
||||
active_workspace.is_some_and(|workspace| {
|
||||
@@ -1250,10 +1394,29 @@ mod tests {
|
||||
assert!(!bar_hidden(&snapshot));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fullscreen_visibility_is_local_to_each_monitor() {
|
||||
let mut snapshot = snapshot_with_fullscreen_window(7);
|
||||
snapshot.outputs.push(OutputSnapshot {
|
||||
id: 2,
|
||||
name: "external-test".to_owned(),
|
||||
active_workspace: 9,
|
||||
focused: false,
|
||||
});
|
||||
assert!(output_bar_hidden(&snapshot, Some(1)));
|
||||
assert!(!output_bar_hidden(&snapshot, Some(2)));
|
||||
assert!(!output_bar_hidden(&snapshot, None));
|
||||
snapshot.outputs[0].focused = false;
|
||||
snapshot.outputs[1].focused = true;
|
||||
assert!(output_bar_hidden(&snapshot, Some(1)));
|
||||
assert!(!output_bar_hidden(&snapshot, Some(2)));
|
||||
}
|
||||
|
||||
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
|
||||
ShellSnapshot {
|
||||
outputs: vec![OutputSnapshot {
|
||||
id: 1,
|
||||
name: "eDP-1".to_owned(),
|
||||
active_workspace: 7,
|
||||
focused: true,
|
||||
}],
|
||||
|
||||
@@ -9,7 +9,9 @@ use ferese_protocols::effects::v1::client::{
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
|
||||
protocol::{wl_buffer, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface},
|
||||
protocol::{
|
||||
wl_buffer, wl_callback, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface,
|
||||
},
|
||||
};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{
|
||||
zwlr_layer_shell_v1::{self, ZwlrLayerShellV1},
|
||||
@@ -25,12 +27,16 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.iter()
|
||||
.any(|argument| argument == "--expect-hidden");
|
||||
let preview = arguments.iter().any(|argument| argument == "--preview");
|
||||
let callbacks_only = arguments
|
||||
.iter()
|
||||
.any(|argument| argument == "--frame-callbacks");
|
||||
let connection = Connection::connect_to_env()?;
|
||||
let mut queue = connection.new_event_queue();
|
||||
let qh = queue.handle();
|
||||
connection.display().get_registry(&qh, ());
|
||||
let mut state = ProbeState {
|
||||
preview,
|
||||
callbacks_only,
|
||||
..ProbeState::default()
|
||||
};
|
||||
|
||||
@@ -43,7 +49,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
while state.effects.is_none() || !state.configured {
|
||||
while (!callbacks_only && state.effects.is_none()) || !state.configured {
|
||||
queue.blocking_dispatch(&mut state)?;
|
||||
state.try_initialize(&qh)?;
|
||||
if state.closed {
|
||||
@@ -51,6 +57,25 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
if callbacks_only {
|
||||
// Request frames without attaching a new buffer or damaging content.
|
||||
// A compositor which ties callbacks to damage deadlocks this sequence.
|
||||
for expected in 1..=8 {
|
||||
state.surface.as_ref().unwrap().frame(&qh, ());
|
||||
state.surface.as_ref().unwrap().commit();
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(2);
|
||||
while state.frame_count < expected {
|
||||
queue.roundtrip(&mut state)?;
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err("frame callback stalled on an unchanged layer buffer".into());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(2));
|
||||
}
|
||||
}
|
||||
println!("PASS eight frame callbacks delivered for an unchanged layer buffer");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let effects = state.effects.clone().expect("effects initialized");
|
||||
let initial_configures = state.configure_count;
|
||||
for role in [
|
||||
@@ -130,6 +155,8 @@ struct ProbeState {
|
||||
preview_background_layer: Option<ZwlrLayerSurfaceV1>,
|
||||
preview_background_buffer: Option<wl_buffer::WlBuffer>,
|
||||
preview: bool,
|
||||
callbacks_only: bool,
|
||||
frame_count: u32,
|
||||
configured: bool,
|
||||
closed: bool,
|
||||
configure_count: u32,
|
||||
@@ -142,14 +169,16 @@ impl ProbeState {
|
||||
if self.surface.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
let (Some(compositor), Some(_shm), Some(layer_shell), Some(effects_manager)) = (
|
||||
let (Some(compositor), Some(_shm), Some(layer_shell)) = (
|
||||
self.compositor.as_ref(),
|
||||
self.shm.as_ref(),
|
||||
self.layer_shell.as_ref(),
|
||||
self.effects_manager.as_ref(),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
if !self.callbacks_only && self.effects_manager.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let surface = compositor.create_surface(qh, ());
|
||||
let layer = if self.preview {
|
||||
@@ -182,8 +211,15 @@ impl ProbeState {
|
||||
layer_surface.set_exclusive_zone(HEIGHT as i32);
|
||||
}
|
||||
|
||||
let effects = effects_manager.get_surface_effects(&surface, qh, ());
|
||||
effects.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||
let effects = (!self.callbacks_only).then(|| {
|
||||
let effects =
|
||||
self.effects_manager
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_surface_effects(&surface, qh, ());
|
||||
effects.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||
effects
|
||||
});
|
||||
|
||||
surface.commit();
|
||||
|
||||
@@ -211,7 +247,7 @@ impl ProbeState {
|
||||
|
||||
self.surface = Some(surface);
|
||||
self.layer_surface = Some(layer_surface);
|
||||
self.effects = Some(effects);
|
||||
self.effects = effects;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -380,6 +416,21 @@ impl Dispatch<ZwlrLayerSurfaceV1, ()> for ProbeState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<wl_callback::WlCallback, ()> for ProbeState {
|
||||
fn event(
|
||||
state: &mut Self,
|
||||
_: &wl_callback::WlCallback,
|
||||
event: wl_callback::Event,
|
||||
_: &(),
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
if matches!(event, wl_callback::Event::Done { .. }) {
|
||||
state.frame_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate_noop!(ProbeState: ignore wl_compositor::WlCompositor);
|
||||
delegate_noop!(ProbeState: ignore wl_shm::WlShm);
|
||||
delegate_noop!(ProbeState: ignore wl_shm_pool::WlShmPool);
|
||||
|
||||
Reference in new issue
Block a user