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
+1976
-202
No files matched your search
Generated
+2
@@ -1576,6 +1576,7 @@ dependencies = [
|
|||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
"ferese-layout",
|
"ferese-layout",
|
||||||
"ferese-protocols",
|
"ferese-protocols",
|
||||||
|
"image",
|
||||||
"libc",
|
"libc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -2674,6 +2675,7 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder-lite",
|
"byteorder-lite",
|
||||||
|
"image-webp",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png 0.18.1",
|
"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
|
`[theme.geometry]` adds optional extra space (default 0). Solid and translucent
|
||||||
shell surfaces also omit outlines. The dark startup fallback matches the material palette.
|
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
|
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
|
rendering for shell content. Use release builds when evaluating startup and
|
||||||
animation performance; debug image processing is considerably slower.
|
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:
|
Keyboard and direct-session touchpad settings are applied at startup:
|
||||||
|
|
||||||
```toml
|
```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 their rotated flipped variants. Output configuration is applied at startup
|
||||||
and on hotplug; live configuration reload remains deferred.
|
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
|
Commands are argv arrays and are started directly, without a shell. The
|
||||||
default `Super+Enter` binding runs the built-in `terminal = ["foot"]` command:
|
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-layout = { path = "../crates/ferese-layout" }
|
||||||
ferese-protocols = { path = "../crates/ferese-protocols", features = ["server"] }
|
ferese-protocols = { path = "../crates/ferese-protocols", features = ["server"] }
|
||||||
libc.workspace = true
|
libc.workspace = true
|
||||||
|
image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "webp"] }
|
||||||
smithay.workspace = true
|
smithay.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ use smithay::{
|
|||||||
Fourcc,
|
Fourcc,
|
||||||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||||||
},
|
},
|
||||||
drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface},
|
drm::{
|
||||||
|
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType,
|
||||||
|
},
|
||||||
egl::{EGLContext, EGLDisplay},
|
egl::{EGLContext, EGLDisplay},
|
||||||
input::InputEvent,
|
input::InputEvent,
|
||||||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||||||
@@ -36,7 +38,7 @@ use smithay::{
|
|||||||
wayland_server::backend::GlobalId,
|
wayland_server::backend::GlobalId,
|
||||||
},
|
},
|
||||||
utils::{DeviceFd, Monotonic, Time, Transform},
|
utils::{DeviceFd, Monotonic, Time, Transform},
|
||||||
wayland::presentation::Refresh,
|
wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -51,11 +53,39 @@ use crate::{
|
|||||||
pub struct DirectBackendState {
|
pub struct DirectBackendState {
|
||||||
pub session: LibSeatSession,
|
pub session: LibSeatSession,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
|
lid_closed: bool,
|
||||||
devices: HashMap<DrmNode, DirectDevice>,
|
devices: HashMap<DrmNode, DirectDevice>,
|
||||||
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
|
||||||
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct ConnectedOutputInfo {
|
pub(crate) struct ConnectedOutputInfo {
|
||||||
pub connector: String,
|
pub connector: String,
|
||||||
@@ -94,6 +124,7 @@ struct DirectDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct DirectOutput {
|
struct DirectOutput {
|
||||||
|
internal: bool,
|
||||||
connector: connector::Handle,
|
connector: connector::Handle,
|
||||||
mode: DrmMode,
|
mode: DrmMode,
|
||||||
settings: OutputSettings,
|
settings: OutputSettings,
|
||||||
@@ -143,6 +174,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
state.direct_backend = Some(DirectBackendState {
|
state.direct_backend = Some(DirectBackendState {
|
||||||
session,
|
session,
|
||||||
active: session_active,
|
active: session_active,
|
||||||
|
lid_closed: initial_lid_closed(),
|
||||||
devices: HashMap::new(),
|
devices: HashMap::new(),
|
||||||
presentation: HashMap::new(),
|
presentation: HashMap::new(),
|
||||||
connected_outputs: Vec::new(),
|
connected_outputs: Vec::new(),
|
||||||
@@ -279,10 +311,25 @@ fn open_primary_device(
|
|||||||
state.shm_state.update_formats(renderer.shm_formats());
|
state.shm_state.update_formats(renderer.shm_formats());
|
||||||
let dmabuf_formats = renderer.dmabuf_formats();
|
let dmabuf_formats = renderer.dmabuf_formats();
|
||||||
let display_handle = state.display_handle.clone();
|
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
|
state
|
||||||
.dmabuf_state
|
.dmabuf_state
|
||||||
.create_global::<Ferese>(&display_handle, dmabuf_formats);
|
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
|
||||||
let scan = select_outputs(&drm, &state.output_profiles)?;
|
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() {
|
if scan.selections.is_empty() {
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::NotFound,
|
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) {
|
fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||||
let missed_deadlines = state
|
let missed_deadlines = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
@@ -570,7 +679,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
return;
|
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,
|
Ok(scan) => scan,
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(?node, %error, "failed to scan DRM connectors");
|
tracing::error!(?node, %error, "failed to scan DRM connectors");
|
||||||
@@ -578,6 +687,11 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
return;
|
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() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.connected_outputs = scan.connected_outputs;
|
backend.connected_outputs = scan.connected_outputs;
|
||||||
}
|
}
|
||||||
@@ -587,6 +701,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
.map(|selection| (selection.crtc, selection))
|
.map(|selection| (selection.crtc, selection))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
|
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
|
||||||
|
let mut deferred_removals = Vec::new();
|
||||||
|
|
||||||
for crtc in existing {
|
for crtc in existing {
|
||||||
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
||||||
@@ -601,6 +716,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
continue;
|
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) {
|
if let Some(output) = device.outputs.remove(&crtc) {
|
||||||
state.display_handle.disable_global::<Ferese>(output.global);
|
state.display_handle.disable_global::<Ferese>(output.global);
|
||||||
state.unregister_output(&output.output);
|
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);
|
restore_device(state, node, device);
|
||||||
|
state.restore_output_focus();
|
||||||
state.relayout();
|
state.relayout();
|
||||||
render_all(state);
|
render_all(state);
|
||||||
}
|
}
|
||||||
@@ -657,6 +821,11 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
return;
|
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 {
|
for (crtc, output) in device.outputs {
|
||||||
state.display_handle.disable_global::<Ferese>(output.global);
|
state.display_handle.disable_global::<Ferese>(output.global);
|
||||||
state.unregister_output(&output.output);
|
state.unregister_output(&output.output);
|
||||||
@@ -901,7 +1070,6 @@ fn create_direct_output(
|
|||||||
settings: &OutputSettings,
|
settings: &OutputSettings,
|
||||||
) -> Result<DirectOutput, Box<dyn Error>> {
|
) -> Result<DirectOutput, Box<dyn Error>> {
|
||||||
let identity = connector_identity(drm, &connector);
|
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 drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
let allocator = GbmAllocator::new(
|
let allocator = GbmAllocator::new(
|
||||||
gbm.clone(),
|
gbm.clone(),
|
||||||
@@ -913,11 +1081,15 @@ fn create_direct_output(
|
|||||||
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
||||||
renderer.dmabuf_formats(),
|
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 damage_tracker = OutputDamageTracker::from_output(&output);
|
||||||
let render_metrics = RenderMetrics::from_environment(output.name());
|
let render_metrics = RenderMetrics::from_environment(output.name());
|
||||||
|
|
||||||
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
tracing::info!(?crtc, connector = %connector, "initialized DRM output");
|
||||||
Ok(DirectOutput {
|
Ok(DirectOutput {
|
||||||
|
internal: internal_connector(connector.interface()),
|
||||||
connector: connector.handle(),
|
connector: connector.handle(),
|
||||||
mode,
|
mode,
|
||||||
settings: settings.clone(),
|
settings: settings.clone(),
|
||||||
@@ -1090,6 +1262,25 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn connector_identity_hash_is_stable() {
|
fn connector_identity_hash_is_stable() {
|
||||||
assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b);
|
assert_eq!(stable_hash(b"hello"), 0xa430_d846_80aa_bd0b);
|
||||||
|
|||||||
@@ -167,6 +167,8 @@ impl Default for InactiveDimConfig {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct ThemeConfig {
|
struct ThemeConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
background: crate::wallpaper::WallpaperConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
surface: ThemeSurfaceConfig,
|
surface: ThemeSurfaceConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -179,6 +181,12 @@ struct ThemeConfig {
|
|||||||
material: ThemeMaterialConfig,
|
material: ThemeMaterialConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub(crate) fn wallpaper_settings(&self) -> crate::wallpaper::WallpaperConfig {
|
||||||
|
self.theme.background.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
struct ThemeSurfaceConfig {
|
struct ThemeSurfaceConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|||||||
@@ -18,6 +18,30 @@ use smithay::{
|
|||||||
use super::{layer_shell, xdg_shell};
|
use super::{layer_shell, xdg_shell};
|
||||||
use crate::{Ferese, state::ClientState};
|
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 {
|
impl CompositorHandler for Ferese {
|
||||||
fn compositor_state(&mut self) -> &mut CompositorState {
|
fn compositor_state(&mut self) -> &mut CompositorState {
|
||||||
&mut self.compositor_state
|
&mut self.compositor_state
|
||||||
@@ -31,7 +55,10 @@ impl CompositorHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn commit(&mut self, surface: &WlSurface) {
|
fn commit(&mut self, surface: &WlSurface) {
|
||||||
|
self.capture_resize_before_commit(surface);
|
||||||
|
if self.surface_affects_backdrop(surface) {
|
||||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
}
|
||||||
on_commit_buffer_handler::<Self>(surface);
|
on_commit_buffer_handler::<Self>(surface);
|
||||||
if !is_sync_subsurface(surface) {
|
if !is_sync_subsurface(surface) {
|
||||||
let mut root = surface.clone();
|
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 {
|
impl BufferHandler for Ferese {
|
||||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-4
@@ -4,7 +4,7 @@ use smithay::{
|
|||||||
backend::input::{
|
backend::input::{
|
||||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, InputBackend, InputEvent,
|
||||||
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
KeyState, KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
|
||||||
TouchEvent,
|
Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
||||||
},
|
},
|
||||||
input::{
|
input::{
|
||||||
keyboard::{FilterResult, keysyms},
|
keyboard::{FilterResult, keysyms},
|
||||||
@@ -27,6 +27,9 @@ impl Ferese {
|
|||||||
self.idle_notifier_state.notify_activity(&seat);
|
self.idle_notifier_state.notify_activity(&seat);
|
||||||
|
|
||||||
match event {
|
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, .. } => {
|
InputEvent::Keyboard { event, .. } => {
|
||||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||||
let keycode = event.key_code();
|
let keycode = event.key_code();
|
||||||
@@ -339,12 +342,14 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = self.window_under_visual(position) {
|
if let Some(window) = self.window_under_visual(position) {
|
||||||
self.focused_window = self.window_ids.get(&window).copied();
|
let focused = self.window_ids.get(&window).copied();
|
||||||
if let Some(focused) = self.focused_window
|
if let Some(focused) = focused
|
||||||
&& let Err(error) = self.workspaces.focus_window(focused)
|
&& let Err(error) = self.workspaces.focus_window(focused)
|
||||||
{
|
{
|
||||||
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
self.focused_window = focused;
|
||||||
self.raise_window(&window, true);
|
self.raise_window(&window, true);
|
||||||
let surface = window
|
let surface = window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
@@ -481,7 +486,12 @@ impl Ferese {
|
|||||||
BindingAction::Spawn(mut argv) => {
|
BindingAction::Spawn(mut argv) => {
|
||||||
let program = argv.remove(0);
|
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) => {
|
Ok(child) => {
|
||||||
tracing::info!(%program, pid = child.id(), "spawned binding command")
|
tracing::info!(%program, pid = child.id(), "spawned binding command")
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-4
@@ -11,10 +11,13 @@ mod input;
|
|||||||
mod ipc;
|
mod ipc;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod overview;
|
mod overview;
|
||||||
|
mod presentation;
|
||||||
mod private_client;
|
mod private_client;
|
||||||
|
mod resize_transaction;
|
||||||
mod shell_control;
|
mod shell_control;
|
||||||
mod stacking;
|
mod stacking;
|
||||||
mod state;
|
mod state;
|
||||||
|
mod wallpaper;
|
||||||
mod window_rules;
|
mod window_rules;
|
||||||
mod winit;
|
mod winit;
|
||||||
|
|
||||||
@@ -68,14 +71,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
spring_config: config.spring_config()?,
|
spring_config: config.spring_config()?,
|
||||||
viewport_spring_config: config.viewport_spring_config()?,
|
viewport_spring_config: config.viewport_spring_config()?,
|
||||||
output_profiles,
|
output_profiles,
|
||||||
|
wallpaper: config.wallpaper_settings(),
|
||||||
};
|
};
|
||||||
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
|
||||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
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");
|
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 mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||||
let result = event_loop.run(None, &mut state, |state| {
|
let result = event_loop.run(None, &mut state, |state| {
|
||||||
@@ -118,6 +118,12 @@ fn spawn_client(
|
|||||||
|
|
||||||
let mut command = Command::new(&program);
|
let mut command = Command::new(&program);
|
||||||
command.args(args);
|
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() {
|
let private_connection = if capabilities.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub(crate) struct RenderMetrics {
|
|||||||
damaged_pixels: u64,
|
damaged_pixels: u64,
|
||||||
total_render_time: Duration,
|
total_render_time: Duration,
|
||||||
longest_render_time: Duration,
|
longest_render_time: Duration,
|
||||||
|
frame_times: Vec<Duration>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderMetrics {
|
impl RenderMetrics {
|
||||||
@@ -36,6 +37,7 @@ impl RenderMetrics {
|
|||||||
damaged_pixels: 0,
|
damaged_pixels: 0,
|
||||||
total_render_time: Duration::ZERO,
|
total_render_time: Duration::ZERO,
|
||||||
longest_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.damaged_pixels += damage.iter().map(rectangle_area).sum::<u64>();
|
||||||
self.total_render_time += render_time;
|
self.total_render_time += render_time;
|
||||||
self.longest_render_time = self.longest_render_time.max(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 now = Instant::now();
|
||||||
let elapsed = now.saturating_duration_since(self.interval_started);
|
let elapsed = now.saturating_duration_since(self.interval_started);
|
||||||
@@ -66,6 +72,17 @@ impl RenderMetrics {
|
|||||||
.as_micros()
|
.as_micros()
|
||||||
.checked_div(u128::from(self.rendered_frames))
|
.checked_div(u128::from(self.rendered_frames))
|
||||||
.unwrap_or(0);
|
.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!(
|
tracing::info!(
|
||||||
target: "ferese::render",
|
target: "ferese::render",
|
||||||
output = %self.output,
|
output = %self.output,
|
||||||
@@ -74,6 +91,8 @@ impl RenderMetrics {
|
|||||||
damaged_pixels = self.damaged_pixels,
|
damaged_pixels = self.damaged_pixels,
|
||||||
average_render_us,
|
average_render_us,
|
||||||
longest_render_us = self.longest_render_time.as_micros(),
|
longest_render_us = self.longest_render_time.as_micros(),
|
||||||
|
p95_render_us = percentile(95),
|
||||||
|
p99_render_us = percentile(99),
|
||||||
missed_deadlines,
|
missed_deadlines,
|
||||||
"render performance"
|
"render performance"
|
||||||
);
|
);
|
||||||
@@ -83,6 +102,7 @@ impl RenderMetrics {
|
|||||||
self.damaged_pixels = 0;
|
self.damaged_pixels = 0;
|
||||||
self.total_render_time = Duration::ZERO;
|
self.total_render_time = Duration::ZERO;
|
||||||
self.longest_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.damaged_pixels, 5_200);
|
||||||
assert_eq!(metrics.total_render_time, Duration::from_millis(2));
|
assert_eq!(metrics.total_render_time, Duration::from_millis(2));
|
||||||
assert_eq!(metrics.longest_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,
|
y: f64,
|
||||||
) -> Option<(f64, f64)> {
|
) -> Option<(f64, f64)> {
|
||||||
let geometry = self.window_geometry.get(&id)?;
|
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 source = geometry.client.committed_size?;
|
||||||
let presented = self.overview.presented_rect(id, geometry.visual.current);
|
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::Layer,
|
||||||
shell::wlr_layer::WlrLayerShellState,
|
shell::wlr_layer::WlrLayerShellState,
|
||||||
shell::xdg::decoration::XdgDecorationState,
|
shell::xdg::decoration::XdgDecorationState,
|
||||||
shell::xdg::{SurfaceCachedState, XdgShellState},
|
shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData},
|
||||||
shm::ShmState,
|
shm::ShmState,
|
||||||
single_pixel_buffer::SinglePixelBufferState,
|
single_pixel_buffer::SinglePixelBufferState,
|
||||||
socket::ListeningSocketSource,
|
socket::ListeningSocketSource,
|
||||||
@@ -118,11 +118,16 @@ pub struct Ferese {
|
|||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub window_ids: HashMap<Window, WindowId>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
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_windows: HashSet<WindowId>,
|
||||||
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
|
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
|
||||||
window_stack: crate::stacking::WindowStack,
|
window_stack: crate::stacking::WindowStack,
|
||||||
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||||
pub(crate) window_dims: 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_dimming: HashMap<WindowId, crate::dimming::DimAnimation>,
|
||||||
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
||||||
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
|
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
|
||||||
@@ -203,6 +208,7 @@ pub struct Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct RuntimeConfig {
|
pub struct RuntimeConfig {
|
||||||
|
pub(crate) wallpaper: crate::wallpaper::WallpaperConfig,
|
||||||
pub layout_mode: LayoutMode,
|
pub layout_mode: LayoutMode,
|
||||||
pub gap_config: GapConfig,
|
pub gap_config: GapConfig,
|
||||||
pub input_settings: InputSettings,
|
pub input_settings: InputSettings,
|
||||||
@@ -321,6 +327,15 @@ impl Ferese {
|
|||||||
.find_map(|(output, id)| (*id == focused).then_some(output))
|
.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> {
|
pub(crate) fn output_id(&self, output: &Output) -> Option<OutputId> {
|
||||||
self.output_ids.get(output).copied()
|
self.output_ids.get(output).copied()
|
||||||
}
|
}
|
||||||
@@ -340,12 +355,14 @@ impl Ferese {
|
|||||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if self.output_workspaces.focused_output() == Some(output_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
|
let Some(workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if self.output_workspaces.focused_output() == Some(output_id)
|
||||||
|
&& self.workspaces.active_id() == workspace
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.activate_output_workspace(output_id, workspace);
|
self.activate_output_workspace(output_id, workspace);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
@@ -453,11 +470,16 @@ impl Ferese {
|
|||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
window_ids: HashMap::new(),
|
||||||
window_geometry: 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_windows: HashSet::new(),
|
||||||
maximized_column_widths: HashMap::new(),
|
maximized_column_widths: HashMap::new(),
|
||||||
window_stack: crate::stacking::WindowStack::default(),
|
window_stack: crate::stacking::WindowStack::default(),
|
||||||
window_borders: HashMap::new(),
|
window_borders: HashMap::new(),
|
||||||
window_dims: HashMap::new(),
|
window_dims: HashMap::new(),
|
||||||
|
window_resize_fills: HashMap::new(),
|
||||||
window_dimming: HashMap::new(),
|
window_dimming: HashMap::new(),
|
||||||
window_shadows: HashMap::new(),
|
window_shadows: HashMap::new(),
|
||||||
rounded_clip_programs: HashMap::new(),
|
rounded_clip_programs: HashMap::new(),
|
||||||
@@ -596,8 +618,12 @@ impl Ferese {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
self.space.elements().rev().find_map(|window| {
|
self.space.elements().rev().find_map(|window| {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.window_ids.get(window)?;
|
||||||
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let visual = self.presented_window_rect(*id)?;
|
let visual = self.presented_window_rect(*id)?;
|
||||||
let inside_visual = position.x >= visual.x
|
let inside_visual = position.x >= visual.x
|
||||||
&& position.y >= visual.y
|
&& position.y >= visual.y
|
||||||
@@ -666,8 +692,12 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
|
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| {
|
self.space.elements().rev().find_map(|window| {
|
||||||
let id = self.window_ids.get(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 visual = self.presented_window_rect(*id)?;
|
||||||
|
|
||||||
(position.x >= visual.x
|
(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) {
|
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||||
if let Some(id) = self.window_ids.get(window) {
|
if let Some(id) = self.window_ids.get(window) {
|
||||||
self.window_stack.raise(*id);
|
self.window_stack.raise(*id);
|
||||||
@@ -704,6 +747,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
|
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 id = self.window_ids.get(window)?;
|
||||||
let geometry = self.window_geometry.get(id)?;
|
let geometry = self.window_geometry.get(id)?;
|
||||||
let source = geometry.client.committed_size?;
|
let source = geometry.client.committed_size?;
|
||||||
@@ -916,6 +962,9 @@ impl Ferese {
|
|||||||
|
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
self.window_geometry.remove(&id);
|
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_windows.remove(&id);
|
||||||
self.maximized_column_widths.remove(&id);
|
self.maximized_column_widths.remove(&id);
|
||||||
self.window_stack.remove(id);
|
self.window_stack.remove(id);
|
||||||
@@ -939,7 +988,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn relayout(&mut self) {
|
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();
|
self.arrange_layers();
|
||||||
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
|
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);
|
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
|
||||||
@@ -1071,6 +1122,9 @@ impl Ferese {
|
|||||||
.map(|(window, _)| window.clone())
|
.map(|(window, _)| window.clone())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let mut layout_changed = hidden
|
||||||
|
.iter()
|
||||||
|
.any(|window| self.space.element_location(window).is_some());
|
||||||
for window in hidden {
|
for window in hidden {
|
||||||
self.space.unmap_elem(&window);
|
self.space.unmap_elem(&window);
|
||||||
}
|
}
|
||||||
@@ -1100,6 +1154,7 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
PresentationMode::Normal
|
PresentationMode::Normal
|
||||||
};
|
};
|
||||||
|
layout_changed |= !had_geometry || geometry.logical != rect;
|
||||||
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
|
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
|
||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||||
@@ -1198,11 +1253,24 @@ impl Ferese {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if requested_size.is_some() || state_changed {
|
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;
|
self.scrolling_world_x = scrolling_world_x;
|
||||||
|
if layout_changed {
|
||||||
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
|
}
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
self.retarget_overview();
|
self.retarget_overview();
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
@@ -1211,8 +1279,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_animations(&mut self, now: Instant) -> bool {
|
pub fn advance_animations(&mut self, now: Instant) -> bool {
|
||||||
let delta = now.saturating_duration_since(self.last_animation_tick);
|
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
|
||||||
self.last_animation_tick = now;
|
|
||||||
self.advance_animations_by(delta)
|
self.advance_animations_by(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,8 +1294,11 @@ impl Ferese {
|
|||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|backend| backend.record_presentation(node, crtc, time, sequence));
|
.and_then(|backend| backend.record_presentation(node, crtc, time, sequence));
|
||||||
if let Some(delta) = delta {
|
if delta.is_some() {
|
||||||
self.advance_animations_by(delta);
|
// 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);
|
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 {
|
for (workspace, viewport) in &mut self.viewport_animations {
|
||||||
if !visible_workspaces.contains(workspace) {
|
if !visible_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if blocked_workspaces.contains(workspace) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if self.animations_enabled {
|
if self.animations_enabled {
|
||||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||||
} else {
|
} else {
|
||||||
@@ -1300,6 +1433,13 @@ impl Ferese {
|
|||||||
|
|
||||||
let mut settled_coupled_widths = Vec::new();
|
let mut settled_coupled_widths = Vec::new();
|
||||||
for (window, id) in windows {
|
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 {
|
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -1390,10 +1530,115 @@ impl Ferese {
|
|||||||
self.animations_enabled
|
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) {
|
pub fn record_client_commit(&mut self, window: &Window) {
|
||||||
let Some(id) = self.window_ids.get(window) else {
|
let Some(id) = self.window_ids.get(window) else {
|
||||||
return;
|
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 {
|
let Some(size) = client_size(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -1869,6 +2114,8 @@ impl Ferese {
|
|||||||
|
|
||||||
// Direct manipulation follows the pointer, including while the client is
|
// Direct manipulation follows the pointer, including while the client is
|
||||||
// still drawing its next buffer. Rendering and hit testing share this rect.
|
// still drawing its next buffer. Rendering and hit testing share this rect.
|
||||||
|
self.resize_transactions.remove(&id);
|
||||||
|
self.resize_snapshots.remove(&id);
|
||||||
let geometry = self
|
let geometry = self
|
||||||
.window_geometry
|
.window_geometry
|
||||||
.entry(id)
|
.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
+338
-58
@@ -1,6 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
cell::{Cell, RefCell},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
Arc,
|
Arc,
|
||||||
atomic::{AtomicBool, Ordering},
|
atomic::{AtomicBool, Ordering},
|
||||||
@@ -12,7 +14,7 @@ use smithay::{
|
|||||||
backend::{
|
backend::{
|
||||||
allocator::Fourcc,
|
allocator::Fourcc,
|
||||||
renderer::{
|
renderer::{
|
||||||
Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
|
Bind, Color32F, ErasedContextId, Frame, ImportDma, Offscreen, Renderer, Texture,
|
||||||
damage::OutputDamageTracker,
|
damage::OutputDamageTracker,
|
||||||
element::{
|
element::{
|
||||||
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage,
|
||||||
@@ -43,7 +45,10 @@ use smithay::{
|
|||||||
},
|
},
|
||||||
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
|
||||||
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
|
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_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
|
||||||
reexports::wayland_server::Resource,
|
reexports::wayland_server::Resource,
|
||||||
utils::{
|
utils::{
|
||||||
@@ -57,6 +62,7 @@ use smithay::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
Ferese,
|
Ferese,
|
||||||
metrics::{FrameEffectMetrics, RenderMetrics},
|
metrics::{FrameEffectMetrics, RenderMetrics},
|
||||||
|
presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect},
|
||||||
};
|
};
|
||||||
|
|
||||||
type SurfaceRenderElement = CropRenderElement<
|
type SurfaceRenderElement = CropRenderElement<
|
||||||
@@ -81,6 +87,85 @@ render_elements! {
|
|||||||
Solid=SolidColorRenderElement,
|
Solid=SolidColorRenderElement,
|
||||||
Border=PixelShaderElement,
|
Border=PixelShaderElement,
|
||||||
Blur=BlurRenderElement,
|
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! {
|
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 initial_scale = normalized_scale(backend.scale_factor());
|
||||||
let mode = Mode {
|
let mode = Mode {
|
||||||
size: backend.window_size(),
|
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(
|
let output = Output::new(
|
||||||
"ferese-winit".into(),
|
"ferese-winit".into(),
|
||||||
@@ -689,19 +778,46 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
let mut damage_tracker = OutputDamageTracker::from_output(&output);
|
let mut damage_tracker = OutputDamageTracker::from_output(&output);
|
||||||
let clock = Clock::<Monotonic>::new();
|
let clock = Clock::<Monotonic>::new();
|
||||||
let mut sequence = 0_u64;
|
let mut sequence = 0_u64;
|
||||||
|
let mut missed_deadlines = 0_u64;
|
||||||
let mut output_scale = initial_scale;
|
let mut output_scale = initial_scale;
|
||||||
let mut render_metrics = RenderMetrics::from_environment(output.name());
|
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
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
.insert_source(event_source, move |event, _, state| match event {
|
.insert_source(Timer::from_duration(refresh.get()), move |_, _, _| {
|
||||||
|
redraw_backend.borrow().window().request_redraw();
|
||||||
|
TimeoutAction::ToDuration(redraw_refresh.get())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(event_source, move |event, _, state| {
|
||||||
|
let mut backend = backend.borrow_mut();
|
||||||
|
match event {
|
||||||
WinitEvent::Resized { size, scale_factor } => {
|
WinitEvent::Resized { size, scale_factor } => {
|
||||||
|
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
|
||||||
let scale = normalized_scale(scale_factor);
|
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));
|
||||||
|
|
||||||
output.change_current_state(
|
output.change_current_state(
|
||||||
Some(Mode {
|
Some(Mode {
|
||||||
size,
|
size,
|
||||||
refresh: 60_000,
|
refresh: rate,
|
||||||
}),
|
}),
|
||||||
None,
|
None,
|
||||||
Some(Scale::Fractional(scale)),
|
Some(Scale::Fractional(scale)),
|
||||||
@@ -771,13 +887,16 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
let (damage, effects) = match rendered {
|
let (damage, effects) = match rendered {
|
||||||
Ok((Some(damage), effects)) => (damage, effects),
|
Ok((Some(damage), effects)) => (damage, effects),
|
||||||
Ok((None, _)) => {
|
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.space.refresh();
|
||||||
state.popups.cleanup();
|
state.popups.cleanup();
|
||||||
layer_map_for_output(&output).cleanup();
|
layer_map_for_output(&output).cleanup();
|
||||||
if let Err(error) = state.display_handle.flush_clients() {
|
if let Err(error) = state.display_handle.flush_clients() {
|
||||||
tracing::debug!(%error, "failed to flush clients");
|
tracing::debug!(%error, "failed to flush clients");
|
||||||
}
|
}
|
||||||
backend.window().request_redraw();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -791,7 +910,11 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
state.loop_signal.stop();
|
state.loop_signal.stop();
|
||||||
return;
|
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);
|
let mut presentation = OutputPresentationFeedback::new(&output);
|
||||||
state.space.elements().for_each(|window| {
|
state.space.elements().for_each(|window| {
|
||||||
@@ -811,41 +934,36 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
sequence = sequence.wrapping_add(1);
|
sequence = sequence.wrapping_add(1);
|
||||||
presentation.presented(
|
presentation.presented(
|
||||||
clock.now(),
|
clock.now(),
|
||||||
Refresh::fixed(Duration::from_nanos(1_000_000_000 / 60)),
|
Refresh::fixed(refresh.get()),
|
||||||
sequence,
|
sequence,
|
||||||
PresentationKind::Vsync,
|
PresentationKind::Vsync,
|
||||||
);
|
);
|
||||||
state.space.elements().for_each(|window| {
|
send_nested_frame_callbacks(state, &output, refresh.get());
|
||||||
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.space.refresh();
|
||||||
state.popups.cleanup();
|
state.popups.cleanup();
|
||||||
layer_map_for_output(&output).cleanup();
|
layer_map_for_output(&output).cleanup();
|
||||||
if let Err(error) = state.display_handle.flush_clients() {
|
if let Err(error) = state.display_handle.flush_clients() {
|
||||||
tracing::debug!(%error, "failed to 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(())
|
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(
|
pub(crate) fn animated_window_elements(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
@@ -875,6 +993,9 @@ fn output_elements(
|
|||||||
output: &Output,
|
output: &Output,
|
||||||
include_cursor: bool,
|
include_cursor: bool,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> 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 {
|
let Some(output_geometry) = state.space.output_geometry(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -898,6 +1019,12 @@ fn output_elements(
|
|||||||
.rev()
|
.rev()
|
||||||
.filter_map(|window| {
|
.filter_map(|window| {
|
||||||
let id = *state.window_ids.get(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 visual = state.presented_window_rect(id)?;
|
||||||
let (close_scale, close_alpha) = state.closing_visual(id);
|
let (close_scale, close_alpha) = state.closing_visual(id);
|
||||||
let visual = scaled_visual_rect(visual, close_scale);
|
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 {
|
for (window, id, visual, decoration_progress, close_alpha) in windows {
|
||||||
let constrain = rounded_visual_rect(visual, output_geometry.loc);
|
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);
|
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,
|
state,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
|
pixels,
|
||||||
scale,
|
scale,
|
||||||
state.theme_settings.window_radius * decoration_progress,
|
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,
|
output,
|
||||||
) {
|
) {
|
||||||
// Front-to-back: dim the application and its border, not its shadow
|
// Front-to-back: dim the application and its border, not its shadow
|
||||||
@@ -958,6 +1091,7 @@ fn output_elements(
|
|||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
|
pixels,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
window_radius,
|
||||||
border_width,
|
border_width,
|
||||||
@@ -972,6 +1106,7 @@ fn output_elements(
|
|||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
|
pixels,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
window_radius,
|
||||||
shadow_offset_y,
|
shadow_offset_y,
|
||||||
@@ -981,10 +1116,52 @@ fn output_elements(
|
|||||||
output,
|
output,
|
||||||
&programs,
|
&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(
|
elements.extend(rounded_window_elements(
|
||||||
renderer,
|
renderer,
|
||||||
&window,
|
&window,
|
||||||
constrain,
|
constrain,
|
||||||
|
pixels,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
window_radius,
|
||||||
close_alpha,
|
close_alpha,
|
||||||
@@ -994,7 +1171,31 @@ fn output_elements(
|
|||||||
.is_some_and(|geometry| geometry.presentation_changed),
|
.is_some_and(|geometry| geometry.presentation_changed),
|
||||||
output,
|
output,
|
||||||
programs.texture.clone(),
|
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 {
|
if let Some(shadow) = shadow {
|
||||||
elements.push(shadow.into());
|
elements.push(shadow.into());
|
||||||
}
|
}
|
||||||
@@ -1012,7 +1213,7 @@ fn output_elements(
|
|||||||
constrain,
|
constrain,
|
||||||
ConstrainBehavior {
|
ConstrainBehavior {
|
||||||
reference: ConstrainReference::Geometry,
|
reference: ConstrainReference::Geometry,
|
||||||
behavior: ConstrainScaleBehavior::Stretch,
|
behavior,
|
||||||
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
align: ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
@@ -1029,6 +1230,9 @@ fn output_elements(
|
|||||||
output,
|
output,
|
||||||
&[Layer::Bottom, Layer::Background],
|
&[Layer::Bottom, Layer::Background],
|
||||||
));
|
));
|
||||||
|
if let Some(wallpaper) = state.wallpaper.element(renderer, output) {
|
||||||
|
elements.push(wallpaper.into());
|
||||||
|
}
|
||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1114,13 +1318,14 @@ fn window_border_element(
|
|||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
requested_radius: f64,
|
||||||
requested_width: f64,
|
requested_width: f64,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
output: &Output,
|
output: &Output,
|
||||||
programs: &RoundedClipPrograms,
|
programs: &RoundedClipPrograms,
|
||||||
) -> Option<PixelShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let width = scaled_effect_value(requested_width, geometry, scale);
|
let width = scaled_effect_value(requested_width, geometry, scale);
|
||||||
if width == 0.0 {
|
if width == 0.0 {
|
||||||
@@ -1129,11 +1334,7 @@ fn window_border_element(
|
|||||||
|
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(
|
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||||
geometry.to_physical_precise_round(scale),
|
|
||||||
mode.size,
|
|
||||||
output.current_transform().invert(),
|
|
||||||
),
|
|
||||||
radius: scaled_effect_value(requested_radius, geometry, scale),
|
radius: scaled_effect_value(requested_radius, geometry, scale),
|
||||||
width,
|
width,
|
||||||
color,
|
color,
|
||||||
@@ -1167,7 +1368,10 @@ fn window_border_element(
|
|||||||
cached.parameters = parameters;
|
cached.parameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(cached.element.clone())
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: physical,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'static>> {
|
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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn window_dim_element(
|
fn window_tint_element(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
radius: f64,
|
radius: f64,
|
||||||
amount: f32,
|
color: [f32; 4],
|
||||||
|
resize_fill: bool,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
) -> Option<PixelShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
if amount <= 0.0 {
|
if color[3] <= 0.0 {
|
||||||
|
if resize_fill {
|
||||||
|
state.window_resize_fills.remove(&id);
|
||||||
|
} else {
|
||||||
state.window_dims.remove(&id);
|
state.window_dims.remove(&id);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = material_program(state, renderer)?;
|
let program = material_program(state, renderer)?;
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(
|
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
||||||
geometry.to_physical_precise_round(scale),
|
|
||||||
mode.size,
|
|
||||||
output.current_transform().invert(),
|
|
||||||
),
|
|
||||||
radius: scaled_effect_value(radius, geometry, scale),
|
radius: scaled_effect_value(radius, geometry, scale),
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
color: [0.0, 0.0, 0.0, amount.clamp(0.0, 1.0)],
|
color,
|
||||||
};
|
};
|
||||||
let uniforms = |p: &BorderParameters| {
|
let uniforms = |p: &BorderParameters| {
|
||||||
vec![
|
vec![
|
||||||
@@ -1218,7 +1424,11 @@ fn window_dim_element(
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
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
|
let cached = buffers
|
||||||
.contexts
|
.contexts
|
||||||
.entry(context)
|
.entry(context)
|
||||||
@@ -1242,7 +1452,10 @@ fn window_dim_element(
|
|||||||
cached.element.update_uniforms(uniforms(¶meters));
|
cached.element.update_uniforms(uniforms(¶meters));
|
||||||
cached.parameters = parameters;
|
cached.parameters = parameters;
|
||||||
}
|
}
|
||||||
Some(cached.element.clone())
|
Some(PhysicalShaderElement {
|
||||||
|
inner: cached.element.clone(),
|
||||||
|
geometry: physical,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
@@ -1251,6 +1464,7 @@ fn window_shadow_element(
|
|||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
physical: Rectangle<i32, Physical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
requested_radius: f64,
|
||||||
offset_y: f64,
|
offset_y: f64,
|
||||||
@@ -1259,22 +1473,26 @@ fn window_shadow_element(
|
|||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
output: &Output,
|
output: &Output,
|
||||||
programs: &RoundedClipPrograms,
|
programs: &RoundedClipPrograms,
|
||||||
) -> Option<PixelShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
if opacity == 0.0 || color[3] == 0.0 {
|
if opacity == 0.0 || color[3] == 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let shadow_geometry = Rectangle::new(
|
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 bounds = shadow_bounds(geometry, offset_y, blur);
|
||||||
let parameters = ShadowParameters {
|
let parameters = ShadowParameters {
|
||||||
blur: (blur * scale) as f32,
|
blur: (blur * scale) as f32,
|
||||||
bounds,
|
bounds,
|
||||||
shadow_rect: framebuffer_clip_rect(
|
shadow_rect: framebuffer_clip_rect(
|
||||||
shadow_geometry.to_physical_precise_round(scale),
|
shadow_geometry,
|
||||||
mode.size,
|
mode.size,
|
||||||
output.current_transform().invert(),
|
output.current_transform().invert(),
|
||||||
),
|
),
|
||||||
@@ -1311,7 +1529,22 @@ fn window_shadow_element(
|
|||||||
cached.parameters = parameters;
|
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>> {
|
fn shadow_uniforms(parameters: &ShadowParameters) -> Vec<Uniform<'static>> {
|
||||||
@@ -1350,12 +1583,14 @@ fn rounded_window_elements(
|
|||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
window: &smithay::desktop::Window,
|
window: &smithay::desktop::Window,
|
||||||
constrain: Rectangle<i32, Logical>,
|
constrain: Rectangle<i32, Logical>,
|
||||||
|
physical_constrain: Rectangle<i32, Physical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
requested_radius: f64,
|
||||||
alpha: f32,
|
alpha: f32,
|
||||||
clip_changed: bool,
|
clip_changed: bool,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
program: GlesTexProgram,
|
program: GlesTexProgram,
|
||||||
|
behavior: ConstrainScaleBehavior,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
let Some(toplevel) = window.toplevel() else {
|
let Some(toplevel) = window.toplevel() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -1366,8 +1601,7 @@ fn rounded_window_elements(
|
|||||||
|
|
||||||
let geometry = window.geometry();
|
let geometry = window.geometry();
|
||||||
let reference = geometry.to_physical_precise_round(scale);
|
let reference = geometry.to_physical_precise_round(scale);
|
||||||
let physical_constrain = constrain.to_physical_precise_round(scale);
|
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
||||||
let location = (constrain.loc - geometry.loc).to_physical_precise_round(scale);
|
|
||||||
let clip = framebuffer_clip_rect(
|
let clip = framebuffer_clip_rect(
|
||||||
physical_constrain,
|
physical_constrain,
|
||||||
mode.size,
|
mode.size,
|
||||||
@@ -1426,7 +1660,7 @@ fn rounded_window_elements(
|
|||||||
location,
|
location,
|
||||||
physical_constrain,
|
physical_constrain,
|
||||||
reference,
|
reference,
|
||||||
ConstrainScaleBehavior::Stretch,
|
behavior,
|
||||||
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
ConstrainAlign::TOP | ConstrainAlign::LEFT,
|
||||||
scale,
|
scale,
|
||||||
)
|
)
|
||||||
@@ -1434,6 +1668,14 @@ fn rounded_window_elements(
|
|||||||
.collect()
|
.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 {
|
fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||||
let scale = scale.clamp(0.0, 1.0);
|
let scale = scale.clamp(0.0, 1.0);
|
||||||
let width = rect.width * scale;
|
let width = rect.width * scale;
|
||||||
@@ -2056,8 +2298,8 @@ mod tests {
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
color_with_alpha, framebuffer_clip_rect, normalized_scale, rounded_visual_rect,
|
color_with_alpha, framebuffer_clip_rect, normalized_scale, resize_content_behavior,
|
||||||
scaled_visual_rect, shadow_bounds,
|
rounded_visual_rect, scaled_visual_rect, shadow_bounds,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[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]
|
#[test]
|
||||||
fn accepts_positive_finite_scale() {
|
fn accepts_positive_finite_scale() {
|
||||||
assert_eq!(normalized_scale(1.25), 1.25);
|
assert_eq!(normalized_scale(1.25), 1.25);
|
||||||
|
|||||||
@@ -380,8 +380,9 @@ impl WindowGeometry {
|
|||||||
self.presentation_size_request(now)
|
self.presentation_size_request(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configure the destination once. Intermediate sizes make clients reflow
|
/// Request the destination once, independently of the presentation clock.
|
||||||
/// and replace buffers on every frame, disrupting an otherwise smooth zoom.
|
/// 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> {
|
pub fn presentation_size_request(&mut self, now: Duration) -> Option<ClientSize> {
|
||||||
let size = ClientSize::from_rect(self.logical);
|
let size = ClientSize::from_rect(self.logical);
|
||||||
self.client.request_size(size, now).then_some(size)
|
self.client.request_size(size, now).then_some(size)
|
||||||
@@ -710,7 +711,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||||
let end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
let end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
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),
|
geometry.set_presentation_target(end, true, Duration::ZERO),
|
||||||
Some(ClientSize::from_rect(end))
|
Some(ClientSize::from_rect(end))
|
||||||
);
|
);
|
||||||
geometry.client.commit(ClientSize::from_rect(end));
|
|
||||||
for frame in 0..240 {
|
for frame in 0..240 {
|
||||||
|
let now = Duration::from_millis(frame * 8);
|
||||||
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
assert_eq!(
|
assert_eq!(geometry.presentation_size_request(now), None);
|
||||||
geometry.presentation_size_request(Duration::from_millis(frame * 8)),
|
|
||||||
None
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
assert_eq!(geometry.visual.current, end);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
geometry.set_presentation_target(start, false, Duration::from_secs(2)),
|
geometry.set_presentation_target(start, false, Duration::from_secs(2)),
|
||||||
Some(ClientSize::from_rect(start))
|
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]
|
#[test]
|
||||||
fn left_and_right_zoom_edges_do_not_reverse_direction() {
|
fn left_and_right_zoom_edges_do_not_reverse_direction() {
|
||||||
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ impl OutputWorkspaceMap {
|
|||||||
return Ok(WorkspaceSwitch::FocusedExisting(owner));
|
return Ok(WorkspaceSwitch::FocusedExisting(owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assignments.insert(workspace, output);
|
let reassigned = self.assignments.insert(workspace, output) != Some(output);
|
||||||
let state = self
|
let state = self
|
||||||
.outputs
|
.outputs
|
||||||
.get_mut(&output)
|
.get_mut(&output)
|
||||||
@@ -279,7 +279,9 @@ impl OutputWorkspaceMap {
|
|||||||
state.workspaces.insert(workspace);
|
state.workspaces.insert(workspace);
|
||||||
state.active = workspace;
|
state.active = workspace;
|
||||||
self.focused = Some(output);
|
self.focused = Some(output);
|
||||||
|
if reassigned {
|
||||||
self.bump_revision(workspace);
|
self.bump_revision(workspace);
|
||||||
|
}
|
||||||
|
|
||||||
debug_assert!(self.validate());
|
debug_assert!(self.validate());
|
||||||
Ok(WorkspaceSwitch::Activated(output))
|
Ok(WorkspaceSwitch::Activated(output))
|
||||||
@@ -482,7 +484,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn explicit_workspace_selection_prevents_automatic_return() {
|
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
|
||||||
let mut outputs = OutputWorkspaceMap::default();
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
outputs
|
outputs
|
||||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||||
@@ -498,12 +500,61 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||||
Ok(WorkspaceId(3))
|
Ok(WorkspaceId(2))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outputs.output_for_workspace(WorkspaceId(2)),
|
outputs.output_for_workspace(WorkspaceId(2)),
|
||||||
|
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))
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub(crate) struct ShellSnapshot {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct OutputSnapshot {
|
pub(crate) struct OutputSnapshot {
|
||||||
pub(crate) id: u64,
|
pub(crate) id: u64,
|
||||||
|
pub(crate) name: String,
|
||||||
pub(crate) active_workspace: u64,
|
pub(crate) active_workspace: u64,
|
||||||
pub(crate) focused: bool,
|
pub(crate) focused: bool,
|
||||||
}
|
}
|
||||||
@@ -199,12 +200,13 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
ferese_shell_v1::Event::Output {
|
ferese_shell_v1::Event::Output {
|
||||||
output_hi,
|
output_hi,
|
||||||
output_lo,
|
output_lo,
|
||||||
name: _,
|
name,
|
||||||
active_workspace_hi,
|
active_workspace_hi,
|
||||||
active_workspace_lo,
|
active_workspace_lo,
|
||||||
focused,
|
focused,
|
||||||
} => state.pending.outputs.push(OutputSnapshot {
|
} => state.pending.outputs.push(OutputSnapshot {
|
||||||
id: join_id(output_hi, output_lo),
|
id: join_id(output_hi, output_lo),
|
||||||
|
name,
|
||||||
active_workspace: join_id(active_workspace_hi, active_workspace_lo),
|
active_workspace: join_id(active_workspace_hi, active_workspace_lo),
|
||||||
focused: focused != 0,
|
focused: focused != 0,
|
||||||
}),
|
}),
|
||||||
|
|||||||
+264
-101
@@ -18,8 +18,8 @@ use cosmic::iced::alignment;
|
|||||||
use cosmic::iced::event::{self, PlatformSpecific, wayland};
|
use cosmic::iced::event::{self, PlatformSpecific, wayland};
|
||||||
use cosmic::iced::platform_specific::{
|
use cosmic::iced::platform_specific::{
|
||||||
runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings},
|
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::{Anchor, KeyboardInteractivity, Layer},
|
||||||
|
shell::commands::layer_surface::{destroy_layer_surface, set_input_zone},
|
||||||
};
|
};
|
||||||
use cosmic::iced::{
|
use cosmic::iced::{
|
||||||
Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, window,
|
Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, window,
|
||||||
@@ -34,7 +34,7 @@ use jiff::Zoned;
|
|||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
|
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
|
||||||
globals::{GlobalListContents, registry_queue_init},
|
globals::{GlobalListContents, registry_queue_init},
|
||||||
protocol::{wl_registry, wl_surface},
|
protocol::{wl_output, wl_registry, wl_surface},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::config::{ShellConfig, ShellTheme, WallpaperMode};
|
use crate::config::{ShellConfig, ShellTheme, WallpaperMode};
|
||||||
@@ -102,7 +102,13 @@ fn main() -> cosmic::iced::Result {
|
|||||||
// toolkit threads are started.
|
// toolkit threads are started.
|
||||||
unsafe { std::env::set_var("ICED_BACKEND", "tiny-skia,wgpu") };
|
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 = configured_font(config.font_family.as_deref());
|
||||||
let _ = SHELL_FONT.set(shell_font);
|
let _ = SHELL_FONT.set(shell_font);
|
||||||
// Decode alongside toolkit/GPU initialization, never during a UI draw.
|
// Decode alongside toolkit/GPU initialization, never during a UI draw.
|
||||||
@@ -141,13 +147,22 @@ struct FereseShell {
|
|||||||
snapshot: ShellSnapshot,
|
snapshot: ShellSnapshot,
|
||||||
overview_active: bool,
|
overview_active: bool,
|
||||||
clock: String,
|
clock: String,
|
||||||
effects: Option<EffectsBinding>,
|
outputs: Vec<OutputSurfaces>,
|
||||||
status_service: status::Service,
|
status_service: status::Service,
|
||||||
status: status::Snapshot,
|
status: status::Snapshot,
|
||||||
status_error: Option<String>,
|
status_error: Option<String>,
|
||||||
menu: Option<status_ui::OpenMenu>,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
WallpaperLoaded(Result<image::Handle, String>),
|
WallpaperLoaded(Result<image::Handle, String>),
|
||||||
@@ -163,6 +178,7 @@ enum Message {
|
|||||||
StatusTick,
|
StatusTick,
|
||||||
AnimateMenu,
|
AnimateMenu,
|
||||||
OpenMenu(status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
OpenMenu(status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
||||||
|
OpenMenuOn(window::Id, status_ui::Menu, cosmic::iced::Rectangle<i32>),
|
||||||
Control(status::Action),
|
Control(status::Action),
|
||||||
ConfirmPower(status::Action),
|
ConfirmPower(status::Action),
|
||||||
CancelPower,
|
CancelPower,
|
||||||
@@ -185,9 +201,6 @@ impl cosmic::Application for FereseShell {
|
|||||||
|
|
||||||
fn init(core: Core, (config, wallpaper): Self::Flags) -> (Self, Task<Self::Message>) {
|
fn init(core: Core, (config, wallpaper): Self::Flags) -> (Self, Task<Self::Message>) {
|
||||||
let bar_surface_id = window::Id::unique();
|
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 {
|
let app = Self {
|
||||||
core,
|
core,
|
||||||
bar_surface_id,
|
bar_surface_id,
|
||||||
@@ -205,58 +218,8 @@ impl cosmic::Application for FereseShell {
|
|||||||
snapshot: ShellSnapshot::default(),
|
snapshot: ShellSnapshot::default(),
|
||||||
overview_active: false,
|
overview_active: false,
|
||||||
clock: current_time(),
|
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 {
|
let wallpaper_task = match wallpaper {
|
||||||
Some(receiver) => cosmic::task::future(async move {
|
Some(receiver) => cosmic::task::future(async move {
|
||||||
cosmic::Action::App(Message::WallpaperLoaded(
|
cosmic::Action::App(Message::WallpaperLoaded(
|
||||||
@@ -267,7 +230,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
}),
|
}),
|
||||||
None => Task::none(),
|
None => Task::none(),
|
||||||
};
|
};
|
||||||
(app, Task::batch([Task::batch(tasks), wallpaper_task]))
|
(app, wallpaper_task)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
@@ -281,7 +244,9 @@ impl cosmic::Application for FereseShell {
|
|||||||
Event::Keyboard(_)
|
Event::Keyboard(_)
|
||||||
| Event::Window(window::Event::Opened { .. } | window::Event::Closed)
|
| Event::Window(window::Event::Opened { .. } | window::Event::Closed)
|
||||||
| Event::PlatformSpecific(PlatformSpecific::Wayland(
|
| Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||||
wayland::Event::Popup(..) | wayland::Event::Layer(..),
|
wayland::Event::Popup(..)
|
||||||
|
| wayland::Event::Layer(..)
|
||||||
|
| wayland::Event::Output(..),
|
||||||
)) => Some(Message::Event(event, id)),
|
)) => Some(Message::Event(event, id)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}),
|
}),
|
||||||
@@ -314,8 +279,10 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
Message::NativeSurface(id, result) => {
|
Message::NativeSurface(id, result) => {
|
||||||
match result {
|
match result {
|
||||||
Ok((_connection, surface)) if id == self.bar_surface_id => {
|
Ok((_connection, surface))
|
||||||
self.attach_effects(&surface)
|
if self.outputs.iter().any(|entry| entry.bar == id) =>
|
||||||
|
{
|
||||||
|
self.attach_effects(id, &surface)
|
||||||
}
|
}
|
||||||
Ok((_connection, surface)) => {
|
Ok((_connection, surface)) => {
|
||||||
if let Some(menu) = &mut self.menu
|
if let Some(menu) = &mut self.menu
|
||||||
@@ -345,6 +312,16 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
Message::AnimateMenu => Task::none(),
|
Message::AnimateMenu => Task::none(),
|
||||||
Message::OpenMenu(kind, anchor) => self.open_menu(kind, anchor),
|
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) => {
|
Message::ConfirmPower(action) => {
|
||||||
if let Some(menu) = &mut self.menu {
|
if let Some(menu) = &mut self.menu {
|
||||||
menu.confirm = Some(action);
|
menu.confirm = Some(action);
|
||||||
@@ -383,26 +360,37 @@ impl cosmic::Application for FereseShell {
|
|||||||
self.overview_active = active;
|
self.overview_active = active;
|
||||||
}
|
}
|
||||||
if let Some(snapshot) = poll.snapshot {
|
if let Some(snapshot) = poll.snapshot {
|
||||||
let was_hidden = bar_hidden(&self.snapshot);
|
|
||||||
|
|
||||||
self.snapshot = snapshot;
|
self.snapshot = snapshot;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
let hidden = bar_hidden(&self.snapshot);
|
for entry in &mut self.outputs {
|
||||||
if hidden != was_hidden {
|
let output = self
|
||||||
if let Some(effects) = &self.effects
|
.snapshot
|
||||||
&& let Err(error) = effects.set_visible(!hidden)
|
.outputs
|
||||||
{
|
.iter()
|
||||||
eprintln!("ferese-shell: could not update panel material: {error}");
|
.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;
|
||||||
}
|
}
|
||||||
|
entry.hidden = hidden;
|
||||||
let input_zone = if hidden { Some(Vec::new()) } else { None };
|
if let Some(effects) = &entry.effects {
|
||||||
|
if let Err(error) = effects.set_visible(!hidden) {
|
||||||
return Task::batch([
|
eprintln!(
|
||||||
set_input_zone(self.bar_surface_id, input_zone),
|
"ferese-shell: could not update panel material: {error}"
|
||||||
self.destroy_menu(),
|
);
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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()
|
Task::none()
|
||||||
}
|
}
|
||||||
@@ -444,10 +432,149 @@ impl cosmic::Application for FereseShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
|
||||||
match event {
|
match event {
|
||||||
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||||
|
event,
|
||||||
|
output,
|
||||||
|
))) => self.output_event(event, output),
|
||||||
Event::Window(window::Event::Opened { .. })
|
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) =>
|
|| self.menu.as_ref().is_some_and(|menu| menu.id == id) =>
|
||||||
{
|
{
|
||||||
window::run(id, native_wayland_surface)
|
window::run(id, native_wayland_surface)
|
||||||
@@ -510,9 +637,7 @@ impl FereseShell {
|
|||||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Event::Window(window::Event::Closed) if id == self.bar_surface_id => {
|
Event::Window(window::Event::Closed) => Task::none(),
|
||||||
cosmic::iced::exit()
|
|
||||||
}
|
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
||||||
_,
|
_,
|
||||||
surface,
|
surface,
|
||||||
@@ -535,49 +660,48 @@ impl FereseShell {
|
|||||||
_,
|
_,
|
||||||
surface,
|
surface,
|
||||||
frame_id,
|
frame_id,
|
||||||
))) if frame_id == self.bar_surface_id => {
|
))) if self.outputs.iter().any(|entry| entry.bar == frame_id) => {
|
||||||
self.attach_effects(&surface);
|
self.attach_effects(frame_id, &surface);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(
|
||||||
_,
|
_,
|
||||||
surface,
|
surface,
|
||||||
layer_id,
|
layer_id,
|
||||||
))) if layer_id == self.bar_surface_id => {
|
))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => {
|
||||||
self.attach_effects(&surface);
|
self.attach_effects(layer_id, &surface);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
_ => Task::none(),
|
_ => Task::none(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attach_effects(&mut self, surface: &wl_surface::WlSurface) {
|
fn attach_effects(&mut self, id: window::Id, surface: &wl_surface::WlSurface) {
|
||||||
if self.effects.is_some() {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
match EffectsBinding::attach(surface, !bar_hidden(&self.snapshot)) {
|
match EffectsBinding::attach(surface, !hidden) {
|
||||||
Ok(binding) => self.effects = Some(binding),
|
Ok(binding) => entry.effects = Some(binding),
|
||||||
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
Err(error) => eprintln!("ferese-shell: panel material unavailable: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn view_layer(&self) -> Element<'_, cosmic::Action<Message>> {
|
fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action<Message>> {
|
||||||
if bar_hidden(&self.snapshot) {
|
if self.bar_hidden(id) {
|
||||||
return container(text(""))
|
return container(text(""))
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
}
|
}
|
||||||
|
|
||||||
let focused_output = self
|
let focused_output = self.output_for_bar(id);
|
||||||
.snapshot
|
|
||||||
.outputs
|
|
||||||
.iter()
|
|
||||||
.find(|output| output.focused)
|
|
||||||
.or_else(|| self.snapshot.outputs.first());
|
|
||||||
let focused_output_id = focused_output.map(|output| output.id);
|
let focused_output_id = focused_output.map(|output| output.id);
|
||||||
let shell_theme = self.config.theme.for_bar();
|
let shell_theme = self.config.theme.for_bar();
|
||||||
let bar = BarMetrics::from(shell_theme);
|
let bar = BarMetrics::from(shell_theme);
|
||||||
@@ -670,7 +794,14 @@ impl FereseShell {
|
|||||||
.class(theme::Text::Color(foreground)),
|
.class(theme::Text::Color(foreground)),
|
||||||
)
|
)
|
||||||
.padding([0, 6]);
|
.padding([0, 6]);
|
||||||
let right = row![self.view_status_bar(), clock]
|
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)
|
.spacing(6)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
let content = row![
|
let content = row![
|
||||||
@@ -681,7 +812,10 @@ impl FereseShell {
|
|||||||
.align_y(cosmic::iced::Alignment::Center)
|
.align_y(cosmic::iced::Alignment::Center)
|
||||||
.height(Length::Fill);
|
.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)
|
container(content)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
@@ -777,12 +911,22 @@ fn overview_control(
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
|
fn bar_hidden(snapshot: &ShellSnapshot) -> bool {
|
||||||
let active_workspace = snapshot
|
let output = snapshot
|
||||||
.outputs
|
.outputs
|
||||||
.iter()
|
.iter()
|
||||||
.find(|output| output.focused)
|
.find(|output| output.focused)
|
||||||
.or_else(|| snapshot.outputs.first())
|
.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);
|
.map(|output| output.active_workspace);
|
||||||
|
|
||||||
active_workspace.is_some_and(|workspace| {
|
active_workspace.is_some_and(|workspace| {
|
||||||
@@ -1250,10 +1394,29 @@ mod tests {
|
|||||||
assert!(!bar_hidden(&snapshot));
|
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 {
|
fn snapshot_with_fullscreen_window(workspace: u64) -> ShellSnapshot {
|
||||||
ShellSnapshot {
|
ShellSnapshot {
|
||||||
outputs: vec![OutputSnapshot {
|
outputs: vec![OutputSnapshot {
|
||||||
id: 1,
|
id: 1,
|
||||||
|
name: "eDP-1".to_owned(),
|
||||||
active_workspace: 7,
|
active_workspace: 7,
|
||||||
focused: true,
|
focused: true,
|
||||||
}],
|
}],
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ use ferese_protocols::effects::v1::client::{
|
|||||||
};
|
};
|
||||||
use wayland_client::{
|
use wayland_client::{
|
||||||
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
|
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::{
|
use wayland_protocols_wlr::layer_shell::v1::client::{
|
||||||
zwlr_layer_shell_v1::{self, ZwlrLayerShellV1},
|
zwlr_layer_shell_v1::{self, ZwlrLayerShellV1},
|
||||||
@@ -25,12 +27,16 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
.iter()
|
.iter()
|
||||||
.any(|argument| argument == "--expect-hidden");
|
.any(|argument| argument == "--expect-hidden");
|
||||||
let preview = arguments.iter().any(|argument| argument == "--preview");
|
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 connection = Connection::connect_to_env()?;
|
||||||
let mut queue = connection.new_event_queue();
|
let mut queue = connection.new_event_queue();
|
||||||
let qh = queue.handle();
|
let qh = queue.handle();
|
||||||
connection.display().get_registry(&qh, ());
|
connection.display().get_registry(&qh, ());
|
||||||
let mut state = ProbeState {
|
let mut state = ProbeState {
|
||||||
preview,
|
preview,
|
||||||
|
callbacks_only,
|
||||||
..ProbeState::default()
|
..ProbeState::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,7 +49,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
while state.effects.is_none() || !state.configured {
|
while (!callbacks_only && state.effects.is_none()) || !state.configured {
|
||||||
queue.blocking_dispatch(&mut state)?;
|
queue.blocking_dispatch(&mut state)?;
|
||||||
state.try_initialize(&qh)?;
|
state.try_initialize(&qh)?;
|
||||||
if state.closed {
|
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 effects = state.effects.clone().expect("effects initialized");
|
||||||
let initial_configures = state.configure_count;
|
let initial_configures = state.configure_count;
|
||||||
for role in [
|
for role in [
|
||||||
@@ -130,6 +155,8 @@ struct ProbeState {
|
|||||||
preview_background_layer: Option<ZwlrLayerSurfaceV1>,
|
preview_background_layer: Option<ZwlrLayerSurfaceV1>,
|
||||||
preview_background_buffer: Option<wl_buffer::WlBuffer>,
|
preview_background_buffer: Option<wl_buffer::WlBuffer>,
|
||||||
preview: bool,
|
preview: bool,
|
||||||
|
callbacks_only: bool,
|
||||||
|
frame_count: u32,
|
||||||
configured: bool,
|
configured: bool,
|
||||||
closed: bool,
|
closed: bool,
|
||||||
configure_count: u32,
|
configure_count: u32,
|
||||||
@@ -142,14 +169,16 @@ impl ProbeState {
|
|||||||
if self.surface.is_some() {
|
if self.surface.is_some() {
|
||||||
return Ok(());
|
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.compositor.as_ref(),
|
||||||
self.shm.as_ref(),
|
self.shm.as_ref(),
|
||||||
self.layer_shell.as_ref(),
|
self.layer_shell.as_ref(),
|
||||||
self.effects_manager.as_ref(),
|
|
||||||
) else {
|
) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
if !self.callbacks_only && self.effects_manager.is_none() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let surface = compositor.create_surface(qh, ());
|
let surface = compositor.create_surface(qh, ());
|
||||||
let layer = if self.preview {
|
let layer = if self.preview {
|
||||||
@@ -182,8 +211,15 @@ impl ProbeState {
|
|||||||
layer_surface.set_exclusive_zone(HEIGHT as i32);
|
layer_surface.set_exclusive_zone(HEIGHT as i32);
|
||||||
}
|
}
|
||||||
|
|
||||||
let effects = effects_manager.get_surface_effects(&surface, qh, ());
|
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.set_role(ferese_surface_effects_v1::Role::Panel);
|
||||||
|
effects
|
||||||
|
});
|
||||||
|
|
||||||
surface.commit();
|
surface.commit();
|
||||||
|
|
||||||
@@ -211,7 +247,7 @@ impl ProbeState {
|
|||||||
|
|
||||||
self.surface = Some(surface);
|
self.surface = Some(surface);
|
||||||
self.layer_surface = Some(layer_surface);
|
self.layer_surface = Some(layer_surface);
|
||||||
self.effects = Some(effects);
|
self.effects = effects;
|
||||||
Ok(())
|
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_compositor::WlCompositor);
|
||||||
delegate_noop!(ProbeState: ignore wl_shm::WlShm);
|
delegate_noop!(ProbeState: ignore wl_shm::WlShm);
|
||||||
delegate_noop!(ProbeState: ignore wl_shm_pool::WlShmPool);
|
delegate_noop!(ProbeState: ignore wl_shm_pool::WlShmPool);
|
||||||
|
|||||||
Reference in new issue
Block a user