Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7203f84b | ||
|
|
cd19691f52 | ||
|
|
1441875096 | ||
|
|
07bda5a21c | ||
|
|
c0010ccbb6 | ||
|
|
3d0c85c70a | ||
|
|
a40b899b8c | ||
|
|
9666e4ff6c | ||
|
|
f110ec80e1 | ||
|
|
0162870cca |
No files matched your search
@@ -62,6 +62,9 @@ Shortcuts** whenever you want to change a shortcut or gesture.
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Super + Enter | Open a terminal |
|
| Super + Enter | Open a terminal |
|
||||||
| Super + H / J / K / L | Focus left / down / up / right |
|
| Super + H / J / K / L | Focus left / down / up / right |
|
||||||
|
| Super + Backspace | Return to the last focused window |
|
||||||
|
| Super + Escape | Toggle between the last two visited workspaces on this monitor |
|
||||||
|
| Alt + Tab / Alt + Shift + Tab | Preview recent windows forward / backward; release Alt to select, Escape to cancel |
|
||||||
| Super + F | Maximize the window |
|
| Super + F | Maximize the window |
|
||||||
| Super + Shift + F | Toggle fullscreen |
|
| Super + Shift + F | Toggle fullscreen |
|
||||||
| Super + R | Cycle column width |
|
| Super + R | Cycle column width |
|
||||||
|
|||||||
+305
-103
@@ -1,24 +1,31 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::os::fd::AsFd;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
mod capture;
|
||||||
|
mod planes;
|
||||||
|
mod scheduling;
|
||||||
|
use scheduling::*;
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
|
use smithay::backend::drm::compositor::{DrmCompositor, PrimaryPlaneElement};
|
||||||
|
use smithay::backend::drm::exporter::gbm::GbmFramebufferExporter;
|
||||||
|
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, NodeType};
|
||||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||||
use smithay::backend::input::InputEvent;
|
use smithay::backend::input::InputEvent;
|
||||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||||
use smithay::backend::renderer::gles::GlesRenderer;
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer};
|
use smithay::backend::renderer::{ImportDma, ImportMemWl, Renderer};
|
||||||
use smithay::backend::session::libseat::LibSeatSession;
|
use smithay::backend::session::libseat::LibSeatSession;
|
||||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||||
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
||||||
use smithay::desktop::layer_map_for_output;
|
use smithay::desktop::layer_map_for_output;
|
||||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||||
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel};
|
use smithay::output::{Mode as OutputMode, Output, OutputModeSource, PhysicalProperties, Scale, Subpixel};
|
||||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||||
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
||||||
use smithay::reexports::drm::control::{
|
use smithay::reexports::drm::control::{
|
||||||
@@ -35,7 +42,7 @@ use smithay::wayland::presentation::Refresh;
|
|||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||||
use crate::metrics::RenderMetrics;
|
use crate::metrics::RenderMetrics;
|
||||||
use crate::render::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
use crate::render::{frame_effect_metrics, sampled_output_elements};
|
||||||
|
|
||||||
pub struct DirectBackendState {
|
pub struct DirectBackendState {
|
||||||
pub session: LibSeatSession,
|
pub session: LibSeatSession,
|
||||||
@@ -145,10 +152,18 @@ struct PresentationClock {
|
|||||||
missed_deadlines: u64,
|
missed_deadlines: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutputCompositor = DrmCompositor<
|
||||||
|
GbmAllocator<DrmDeviceFd>,
|
||||||
|
GbmFramebufferExporter<DrmDeviceFd>,
|
||||||
|
OutputPresentationFeedback,
|
||||||
|
DrmDeviceFd,
|
||||||
|
>;
|
||||||
|
|
||||||
struct DirectDevice {
|
struct DirectDevice {
|
||||||
drm: DrmDevice,
|
drm: DrmDevice,
|
||||||
gbm: GbmDevice<DrmDeviceFd>,
|
gbm: GbmDevice<DrmDeviceFd>,
|
||||||
renderer: GlesRenderer,
|
renderer: GlesRenderer,
|
||||||
|
render_node: DrmNode,
|
||||||
outputs: HashMap<crtc::Handle, DirectOutput>,
|
outputs: HashMap<crtc::Handle, DirectOutput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +174,14 @@ struct DirectOutput {
|
|||||||
settings: OutputSettings,
|
settings: OutputSettings,
|
||||||
output: Output,
|
output: Output,
|
||||||
global: GlobalId,
|
global: GlobalId,
|
||||||
surface: GbmBufferedSurface<GbmAllocator<DrmDeviceFd>, OutputPresentationFeedback>,
|
surface: OutputCompositor,
|
||||||
damage_tracker: OutputDamageTracker,
|
primary_commit: Option<CommitCounter>,
|
||||||
|
capture_texture: Option<GlesTexture>,
|
||||||
render_metrics: RenderMetrics,
|
render_metrics: RenderMetrics,
|
||||||
frame_pending: bool,
|
frame_pending: bool,
|
||||||
|
scheduler: crate::frame_scheduler::FrameScheduler,
|
||||||
|
render_timer: Option<RegistrationToken>,
|
||||||
power_off: bool,
|
power_off: bool,
|
||||||
callback_clock: crate::presentation::CallbackClock,
|
|
||||||
callback_timer: Option<RegistrationToken>,
|
callback_timer: Option<RegistrationToken>,
|
||||||
lock_frame_pending: bool,
|
lock_frame_pending: bool,
|
||||||
}
|
}
|
||||||
@@ -211,6 +228,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
animation_timer: None,
|
animation_timer: None,
|
||||||
animation_active: false,
|
animation_active: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
open_primary_device(event_loop, state, &primary_path)?;
|
open_primary_device(event_loop, state, &primary_path)?;
|
||||||
event_loop
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
@@ -228,17 +246,20 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
backend.input_devices.push(device);
|
backend.input_devices.push(device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InputEvent::DeviceRemoved { device } => {
|
InputEvent::DeviceRemoved { device } => {
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.input_devices.retain(|candidate| candidate != &device);
|
backend.input_devices.retain(|candidate| candidate != &device);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event => state.process_input_event(event),
|
event => state.process_input_event(event),
|
||||||
})?;
|
})?;
|
||||||
event_loop
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
.insert_source(notifier, move |event, _, state| match event {
|
.insert_source(notifier, move |event, _, state| match event {
|
||||||
SessionEvent::PauseSession => {
|
SessionEvent::PauseSession => {
|
||||||
|
let handle = state.loop_handle.clone();
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.active = false;
|
backend.active = false;
|
||||||
backend
|
backend
|
||||||
@@ -248,28 +269,43 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
backend.devices.values_mut().for_each(|device| {
|
backend.devices.values_mut().for_each(|device| {
|
||||||
device.drm.pause();
|
device.drm.pause();
|
||||||
device.outputs.values_mut().for_each(|output| {
|
device.outputs.values_mut().for_each(|output| {
|
||||||
|
cancel_output_timers(&handle, output);
|
||||||
output.surface.reset_buffers();
|
output.surface.reset_buffers();
|
||||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
output.primary_commit = None;
|
||||||
|
output.capture_texture = None;
|
||||||
output.frame_pending = false;
|
output.frame_pending = false;
|
||||||
output.power_off = false;
|
output.power_off = false;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
libinput_context.suspend();
|
libinput_context.suspend();
|
||||||
tracing::info!("direct session paused");
|
tracing::info!("direct session paused");
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionEvent::ActivateSession => {
|
SessionEvent::ActivateSession => {
|
||||||
if libinput_context.resume().is_err() {
|
if libinput_context.resume().is_err() {
|
||||||
tracing::error!("failed to resume libinput");
|
tracing::error!("failed to resume libinput");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.active = true;
|
backend.active = true;
|
||||||
for device in backend.devices.values_mut() {
|
for device in backend.devices.values_mut() {
|
||||||
if let Err(error) = device.drm.activate(true) {
|
if let Err(error) = device.drm.activate(true) {
|
||||||
tracing::error!(%error, "failed to reactivate DRM device");
|
tracing::error!(%error, "failed to reactivate DRM device");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for output in device.outputs.values_mut() {
|
||||||
|
// A suspended page flip may never arrive. Clear the old
|
||||||
|
// pending frame before accepting a fresh submission.
|
||||||
|
if let Err(error) = output.surface.clear().and_then(|()| output.surface.reset_state()) {
|
||||||
|
tracing::error!(%error, "failed to reset DRM output after activation");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update_power_policy(state);
|
update_power_policy(state);
|
||||||
state.reset_animation_clock();
|
state.reset_animation_clock();
|
||||||
tracing::info!("direct session activated");
|
tracing::info!("direct session activated");
|
||||||
@@ -287,11 +323,13 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
tracing::info!(?device_id, ?path, "additional DRM device discovered");
|
tracing::info!(?device_id, ?path, "additional DRM device discovered");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UdevEvent::Changed { device_id } => {
|
UdevEvent::Changed { device_id } => {
|
||||||
if let Some(node) = direct_node_for_device(state, device_id) {
|
if let Some(node) = direct_node_for_device(state, device_id) {
|
||||||
rescan_device(state, node);
|
rescan_device(state, node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UdevEvent::Removed { device_id } => {
|
UdevEvent::Removed { device_id } => {
|
||||||
if let Some(node) = direct_node_for_device(state, device_id) {
|
if let Some(node) = direct_node_for_device(state, device_id) {
|
||||||
remove_device(state, node);
|
remove_device(state, node);
|
||||||
@@ -447,6 +485,7 @@ fn open_primary_device(
|
|||||||
)
|
)
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
state
|
state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -481,14 +520,16 @@ fn open_primary_device(
|
|||||||
.and_then(|device| device.outputs.get_mut(&crtc))
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
.and_then(|output| match output.surface.frame_submitted() {
|
.and_then(|output| match output.surface.frame_submitted() {
|
||||||
Ok(feedback) => {
|
Ok(feedback) => {
|
||||||
retired = true;
|
retired = feedback.is_some();
|
||||||
feedback
|
feedback
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
tracing::error!(?node, ?crtc, %error, "failed to retire DRM frame");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
|
if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) {
|
||||||
feedback.presented(
|
feedback.presented(
|
||||||
Time::<Monotonic>::from(time),
|
Time::<Monotonic>::from(time),
|
||||||
@@ -497,6 +538,7 @@ fn open_primary_device(
|
|||||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let lock_output = state
|
let lock_output = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -509,34 +551,59 @@ fn open_primary_device(
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(output) = lock_output {
|
if let Some(output) = lock_output {
|
||||||
state.lock_frame_presented(&output);
|
state.lock_frame_presented(&output);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(output) = state
|
if let Some(output) = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|backend| backend.devices.get_mut(&node))
|
.and_then(|backend| backend.devices.get_mut(&node))
|
||||||
.and_then(|device| device.outputs.get_mut(&crtc))
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
|
&& retired
|
||||||
{
|
{
|
||||||
output.frame_pending = false;
|
output.frame_pending = false;
|
||||||
|
if let DrmEventTime::Monotonic(timestamp) = metadata.time {
|
||||||
|
let now = monotonic_now();
|
||||||
|
if timestamp <= now + refresh_duration_from_mode(output.mode) {
|
||||||
|
if let Some(plan) = output.scheduler.presented(timestamp)
|
||||||
|
&& std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some()
|
||||||
|
{
|
||||||
|
tracing::debug!(target: "ferese::render", output = %output.output.name(),
|
||||||
|
target_us = plan.present_at.as_micros(), actual_us = timestamp.as_micros(),
|
||||||
|
request_to_present_us = timestamp.saturating_sub(plan.requested_at).as_micros(),
|
||||||
|
missed_deadlines = output.scheduler.missed_deadlines,
|
||||||
|
"DRM presentation schedule");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
output.scheduler.retire_unknown();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
output.scheduler.retire_unknown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
||||||
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
|
tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip");
|
||||||
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active);
|
let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active);
|
||||||
if active {
|
if active && retired {
|
||||||
let was_animating = state
|
schedule_frame_callbacks(state, node, crtc, monotonic_now());
|
||||||
|
let identity = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.is_some_and(|backend| backend.animation_active);
|
.and_then(|backend| backend.devices.get(&node))
|
||||||
let animating = state.advance_animations(Instant::now());
|
.and_then(|device| device.outputs.get(&crtc))
|
||||||
if animating || was_animating {
|
.map(|output| output.output.clone());
|
||||||
render_outputs(state, animating);
|
if identity.is_some_and(|output| state.output_has_animations(&output)) {
|
||||||
|
request_frame(state, node, crtc);
|
||||||
} else {
|
} else {
|
||||||
render_output(state, node, crtc);
|
arm_frame(state, node, crtc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DrmEvent::Error(error) => {
|
DrmEvent::Error(error) => {
|
||||||
tracing::error!(?node, %error, "DRM event error");
|
tracing::error!(?node, %error, "DRM event error");
|
||||||
}
|
}
|
||||||
@@ -553,6 +620,7 @@ fn open_primary_device(
|
|||||||
drm,
|
drm,
|
||||||
gbm,
|
gbm,
|
||||||
renderer,
|
renderer,
|
||||||
|
render_node,
|
||||||
outputs,
|
outputs,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -566,16 +634,23 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
|||||||
if !state.session_lock.active || !state.session_lock.sleeping {
|
if !state.session_lock.active || !state.session_lock.sleeping {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(backend) = state.direct_backend.as_mut() else {
|
let Some(backend) = state.direct_backend.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) {
|
||||||
for output in device.outputs.values_mut() {
|
for output in device.outputs.values_mut() {
|
||||||
if output.power_off || output.frame_pending {
|
if output.power_off || output.frame_pending {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match output.surface.surface().clear() {
|
|
||||||
Ok(()) => output.power_off = true,
|
match output.surface.clear() {
|
||||||
|
Ok(()) => {
|
||||||
|
cancel_output_timers(&state.loop_handle, output);
|
||||||
|
output.power_off = true;
|
||||||
|
}
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display")
|
tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display")
|
||||||
}
|
}
|
||||||
@@ -609,6 +684,7 @@ fn render_outputs(state: &mut Ferese, animating: bool) {
|
|||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.animation_active = animating && !state.session_lock.sleeping;
|
backend.animation_active = animating && !state.session_lock.sleeping;
|
||||||
}
|
}
|
||||||
|
|
||||||
arm_animation_timer(state);
|
arm_animation_timer(state);
|
||||||
let outputs = state
|
let outputs = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
@@ -623,7 +699,7 @@ fn render_outputs(state: &mut Ferese, animating: bool) {
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
for (node, crtc) in outputs {
|
for (node, crtc) in outputs {
|
||||||
render_output(state, node, crtc);
|
request_frame(state, node, crtc);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -697,8 +773,7 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool {
|
|||||||
closed && external_available
|
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, plan: crate::frame_scheduler::FramePlan) {
|
||||||
schedule_frame_callbacks(state, node, crtc);
|
|
||||||
if state.session_lock.active && state.session_lock.sleeping {
|
if state.session_lock.active && state.session_lock.sleeping {
|
||||||
sleep_locked_outputs(state);
|
sleep_locked_outputs(state);
|
||||||
let asleep = state
|
let asleep = state
|
||||||
@@ -711,11 +786,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let missed_deadlines = state
|
let missed_deadlines = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|backend| backend.presentation.get(&(node, crtc)))
|
.and_then(|backend| backend.devices.get(&node))
|
||||||
.map_or(0, |clock| clock.missed_deadlines);
|
.and_then(|device| device.outputs.get(&crtc))
|
||||||
|
.map_or(0, |output| output.scheduler.missed_deadlines);
|
||||||
let Some(mut device) = state
|
let Some(mut device) = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -723,10 +800,12 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(mut output) = device.outputs.remove(&crtc) else {
|
let Some(mut output) = device.outputs.remove(&crtc) else {
|
||||||
restore_device(state, node, device);
|
restore_device(state, node, device);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !device.drm.is_active() || output.frame_pending {
|
if !device.drm.is_active() || output.frame_pending {
|
||||||
device.outputs.insert(crtc, output);
|
device.outputs.insert(crtc, output);
|
||||||
restore_device(state, node, device);
|
restore_device(state, node, device);
|
||||||
@@ -734,73 +813,112 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let render_started = Instant::now();
|
let render_started = Instant::now();
|
||||||
let rendered = (|| -> Result<bool, Box<dyn Error>> {
|
let now = monotonic_now();
|
||||||
state.process_dmabuf_imports(&mut device.renderer);
|
let horizon = output
|
||||||
let (mut buffer, age) = output.surface.next_buffer()?;
|
.scheduler
|
||||||
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
|
.forecast_time(now, plan)
|
||||||
|
.map_or(Duration::ZERO, |target| target.saturating_sub(now));
|
||||||
|
let frame = state.sample_frame(&output.output, horizon);
|
||||||
|
let rendered = {
|
||||||
|
(|| -> Result<bool, Box<dyn Error>> {
|
||||||
|
state.process_dmabuf_imports(&mut device.renderer, Some(device.render_node));
|
||||||
|
let elements = sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame);
|
||||||
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale());
|
||||||
let mut framebuffer = device.renderer.bind(&mut buffer)?;
|
let flags = planes::frame_flags(
|
||||||
let result = output.damage_tracker.render_output(
|
state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()),
|
||||||
&mut device.renderer,
|
state.output_has_animations(&output.output),
|
||||||
&mut framebuffer,
|
state.session_lock.active,
|
||||||
usize::from(age),
|
);
|
||||||
&elements,
|
let result =
|
||||||
[0.035, 0.04, 0.055, 1.0],
|
output
|
||||||
)?;
|
.surface
|
||||||
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
|
.render_frame(&mut device.renderer, &elements, [0.035, 0.04, 0.055, 1.0], flags)?;
|
||||||
let captured_with_cursor = state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
|
||||||
|
|
||||||
if cursorless_capture {
|
if result.needs_sync()
|
||||||
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
|
&& let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element
|
||||||
redraw_output(
|
{
|
||||||
&mut device.renderer,
|
primary.sync.wait()?;
|
||||||
&mut framebuffer,
|
|
||||||
&output.output,
|
|
||||||
&cursorless_elements,
|
|
||||||
)?;
|
|
||||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
|
|
||||||
redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?;
|
|
||||||
} else if captured_with_cursor {
|
|
||||||
let _ = device
|
|
||||||
.renderer
|
|
||||||
.render(
|
|
||||||
&mut framebuffer,
|
|
||||||
output.output.current_mode().expect("output has a mode").size,
|
|
||||||
output.output.current_transform(),
|
|
||||||
)?
|
|
||||||
.finish()?;
|
|
||||||
}
|
}
|
||||||
let Some(damage) = result.damage.cloned() else {
|
|
||||||
|
capture::capture_output(
|
||||||
|
state,
|
||||||
|
&mut device.renderer,
|
||||||
|
&mut output.capture_texture,
|
||||||
|
&output.output,
|
||||||
|
&frame,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if result.is_empty {
|
||||||
output
|
output
|
||||||
.render_metrics
|
.render_metrics
|
||||||
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
.record_no_damage(render_started.elapsed(), missed_deadlines);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
|
// Count GPU composition damage; direct scanout and cursor-only
|
||||||
|
// updates do not redraw the primary swapchain.
|
||||||
|
let (damage, primary_commit) = match &result.primary_element {
|
||||||
|
PrimaryPlaneElement::Swapchain(primary) => {
|
||||||
|
let damage = primary.damage.damage_since(output.primary_commit);
|
||||||
|
let damage = damage.map_or_else(
|
||||||
|
|| {
|
||||||
|
vec![smithay::utils::Rectangle::from_size(
|
||||||
|
output.output.current_mode().unwrap().size,
|
||||||
|
)]
|
||||||
|
},
|
||||||
|
|damage| {
|
||||||
|
damage
|
||||||
|
.iter()
|
||||||
|
.map(|rect| {
|
||||||
|
smithay::utils::Rectangle::new(
|
||||||
|
(rect.loc.x, rect.loc.y).into(),
|
||||||
|
(rect.size.w, rect.size.h).into(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
(damage, Some(primary.damage.current_commit()))
|
||||||
|
}
|
||||||
|
|
||||||
|
PrimaryPlaneElement::Element(_) => (Vec::new(), None),
|
||||||
|
};
|
||||||
let presentation = crate::presentation::take_output_feedback(
|
let presentation = crate::presentation::take_output_feedback(
|
||||||
state,
|
state,
|
||||||
&output.output,
|
&output.output,
|
||||||
&result.states,
|
&result.states,
|
||||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||||
);
|
);
|
||||||
output
|
drop(result);
|
||||||
.surface
|
output.surface.queue_frame(presentation)?;
|
||||||
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
|
output.primary_commit = primary_commit;
|
||||||
output
|
output
|
||||||
.render_metrics
|
.render_metrics
|
||||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||||
output.frame_pending = true;
|
output.frame_pending = true;
|
||||||
output.lock_frame_pending = state.session_lock.active;
|
output.lock_frame_pending = state.session_lock.active;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
})();
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
let submitted = rendered.as_ref().is_ok_and(|submitted| *submitted);
|
||||||
|
record_frame_schedule(&mut output, plan, render_started.elapsed(), submitted);
|
||||||
|
|
||||||
|
if submitted && let Some(token) = output.callback_timer.take() {
|
||||||
|
state.loop_handle.remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
match rendered {
|
match rendered {
|
||||||
Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"),
|
Ok(true) => tracing::trace!(?node, ?crtc, "queued DRM frame"),
|
||||||
Ok(false) => (),
|
Ok(false) => (),
|
||||||
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"),
|
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to render DRM frame"),
|
||||||
}
|
}
|
||||||
|
|
||||||
device.outputs.insert(crtc, output);
|
device.outputs.insert(crtc, output);
|
||||||
restore_device(state, node, device);
|
restore_device(state, node, device);
|
||||||
|
if !submitted {
|
||||||
|
schedule_frame_callbacks(state, node, crtc, plan.present_at);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
|
fn restore_device(state: &mut Ferese, node: DrmNode, device: DirectDevice) {
|
||||||
@@ -840,11 +958,13 @@ 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);
|
let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed);
|
||||||
apply_lid_policy(&mut scan, 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut selections = scan
|
let mut selections = scan
|
||||||
.selections
|
.selections
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -866,10 +986,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let old_geometry = state.space.output_geometry(&output.output);
|
let old_geometry = state.space.output_geometry(&output.output);
|
||||||
output.mode = selection.mode;
|
output.mode = selection.mode;
|
||||||
|
cancel_output_timers(&state.loop_handle, output);
|
||||||
output.settings = selection.settings.clone();
|
output.settings = selection.settings.clone();
|
||||||
let position = selection.settings.position.unwrap_or_else(|| {
|
let position = selection.settings.position.unwrap_or_else(|| {
|
||||||
state
|
state
|
||||||
@@ -878,6 +1000,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
.map(|g| [g.loc.x, g.loc.y])
|
.map(|g| [g.loc.x, g.loc.y])
|
||||||
.unwrap_or([0, 0])
|
.unwrap_or([0, 0])
|
||||||
});
|
});
|
||||||
|
|
||||||
output.output.change_current_state(
|
output.output.change_current_state(
|
||||||
Some(OutputMode::from(output.mode)),
|
Some(OutputMode::from(output.mode)),
|
||||||
Some(output_transform(output.settings.transform)),
|
Some(output_transform(output.settings.transform)),
|
||||||
@@ -910,20 +1033,26 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
|
||||||
|
output.surface.reset_buffer_ages();
|
||||||
|
output.primary_commit = None;
|
||||||
|
output.capture_texture = None;
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
let clock = backend.presentation.entry((node, crtc)).or_default();
|
let clock = backend.presentation.entry((node, crtc)).or_default();
|
||||||
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
clock.set_refresh(OutputMode::from(output.mode).refresh);
|
||||||
clock.reset_timing();
|
clock.reset_timing();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!(?crtc, %error, "display change failed; retaining current output")
|
tracing::warn!(?crtc, %error, "display change failed; retaining current output")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selections.remove(&crtc);
|
selections.remove(&crtc);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
|
||||||
selections.get(&crtc).is_some_and(|selection| {
|
selections.get(&crtc).is_some_and(|selection| {
|
||||||
selection.connector.handle() == output.connector
|
selection.connector.handle() == output.connector
|
||||||
@@ -931,6 +1060,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
&& selection.settings == output.settings
|
&& selection.settings == output.settings
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if unchanged {
|
if unchanged {
|
||||||
selections.remove(&crtc);
|
selections.remove(&crtc);
|
||||||
continue;
|
continue;
|
||||||
@@ -944,12 +1074,14 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(output) = device.outputs.remove(&crtc) {
|
if let Some(mut output) = device.outputs.remove(&crtc) {
|
||||||
|
cancel_output_timers(&state.loop_handle, &mut output);
|
||||||
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);
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.presentation.remove(&(node, crtc));
|
backend.presentation.remove(&(node, crtc));
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(?node, ?crtc, "removed DRM output");
|
tracing::info!(?node, ?crtc, "removed DRM output");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -969,6 +1101,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
.set_refresh(OutputMode::from(mode).refresh);
|
.set_refresh(OutputMode::from(mode).refresh);
|
||||||
device.outputs.insert(crtc, output);
|
device.outputs.insert(crtc, output);
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"),
|
Err(error) => tracing::error!(?node, ?crtc, %error, "failed to add DRM output"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -997,9 +1130,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
info.enabled = true;
|
info.enabled = true;
|
||||||
info.current_mode = Some(connected_mode_info(output.mode));
|
info.current_mode = Some(connected_mode_info(output.mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(output) = device.outputs.remove(&crtc) {
|
|
||||||
|
if let Some(mut output) = device.outputs.remove(&crtc) {
|
||||||
|
cancel_output_timers(&state.loop_handle, &mut output);
|
||||||
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);
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
@@ -1007,6 +1143,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
for output in device.outputs.values() {
|
for output in device.outputs.values() {
|
||||||
if let Some(info) = backend
|
if let Some(info) = backend
|
||||||
@@ -1023,6 +1160,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
restore_device(state, node, device);
|
restore_device(state, node, device);
|
||||||
state.restore_output_focus();
|
state.restore_output_focus();
|
||||||
state.relayout();
|
state.relayout();
|
||||||
@@ -1040,17 +1178,20 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
|
|
||||||
let context = device.renderer.context_id().erased();
|
let context = device.renderer.context_id().erased();
|
||||||
state.wallpaper.forget_context(&context);
|
state.wallpaper.forget_context(&context);
|
||||||
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
|
state.render.forget_context(&context);
|
||||||
for (crtc, output) in device.outputs {
|
for (crtc, mut output) in device.outputs {
|
||||||
|
cancel_output_timers(&state.loop_handle, &mut output);
|
||||||
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);
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.presentation.remove(&(node, crtc));
|
backend.presentation.remove(&(node, crtc));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.connected_outputs.clear();
|
backend.connected_outputs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!(?node, "removed DRM device");
|
tracing::info!(?node, "removed DRM device");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1066,6 +1207,10 @@ fn refresh_duration(state: &Ferese, node: DrmNode, crtc: crtc::Handle) -> Durati
|
|||||||
.unwrap_or_else(|| Duration::from_millis(16))
|
.unwrap_or_else(|| Duration::from_millis(16))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_duration_from_mode(mode: DrmMode) -> Duration {
|
||||||
|
Duration::from_nanos(1_000_000_000_000 / OutputMode::from(mode).refresh.max(1) as u64)
|
||||||
|
}
|
||||||
|
|
||||||
fn animation_interval(state: &Ferese) -> Duration {
|
fn animation_interval(state: &Ferese) -> Duration {
|
||||||
state
|
state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
@@ -1088,6 +1233,7 @@ fn arm_animation_timer(state: &mut Ferese) {
|
|||||||
if !needed {
|
if !needed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
||||||
match state
|
match state
|
||||||
.loop_handle
|
.loop_handle
|
||||||
@@ -1101,7 +1247,30 @@ fn arm_animation_timer(state: &mut Ferese) {
|
|||||||
if Instant::now() < deadline {
|
if Instant::now() < deadline {
|
||||||
return TimeoutAction::ToInstant(deadline);
|
return TimeoutAction::ToInstant(deadline);
|
||||||
}
|
}
|
||||||
render_all(state);
|
|
||||||
|
let animating = state.advance_animations(Instant::now());
|
||||||
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
|
backend.animation_active = animating;
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputs = state
|
||||||
|
.direct_backend
|
||||||
|
.as_ref()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|backend| backend.devices.iter())
|
||||||
|
.flat_map(|(node, device)| {
|
||||||
|
device
|
||||||
|
.outputs
|
||||||
|
.iter()
|
||||||
|
.map(|(crtc, output)| (*node, *crtc, output.output.clone()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (node, crtc, output) in outputs {
|
||||||
|
if state.output_has_animations(&output) {
|
||||||
|
request_frame(state, node, crtc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if state
|
if state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1110,9 +1279,11 @@ fn arm_animation_timer(state: &mut Ferese) {
|
|||||||
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
|
return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(backend) = state.direct_backend.as_mut() {
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
backend.animation_timer = None;
|
backend.animation_timer = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeoutAction::Drop
|
TimeoutAction::Drop
|
||||||
}) {
|
}) {
|
||||||
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
|
Ok(token) => state.direct_backend.as_mut().unwrap().animation_timer = Some(token),
|
||||||
@@ -1120,12 +1291,13 @@ fn arm_animation_timer(state: &mut Ferese) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration) {
|
||||||
if state.session_lock.sleeping {
|
if state.session_lock.sleeping {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let interval = refresh_duration(state, node, crtc);
|
let timestamp = monotonic_now();
|
||||||
let Some(output) = state
|
let Some(output) = state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1136,15 +1308,22 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if output.callback_timer.is_some() {
|
|
||||||
|
if output.callback_timer.is_some() || output.frame_pending {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let identity = output.output.clone();
|
let identity = output.output.clone();
|
||||||
let deadline = output.callback_clock.deadline(now, interval);
|
let Some(callback_at) = output.scheduler.callback_deadline(timestamp, target) else {
|
||||||
if deadline <= now {
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if callback_at <= timestamp {
|
||||||
deliver_frame_callbacks(state, node, crtc, &identity);
|
deliver_frame_callbacks(state, node, crtc, &identity);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let deadline = now + callback_at.saturating_sub(timestamp);
|
||||||
match state
|
match state
|
||||||
.loop_handle
|
.loop_handle
|
||||||
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
|
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
|
||||||
@@ -1161,6 +1340,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
|||||||
output.callback_timer = Some(token);
|
output.callback_timer = Some(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"),
|
Err(error) => tracing::warn!(%error, "could not schedule DRM frame callbacks"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1169,9 +1349,11 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
|
|||||||
let Some(backend) = state.direct_backend.as_mut() else {
|
let Some(backend) = state.direct_backend.as_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(device) = backend.devices.get_mut(&node) else {
|
let Some(device) = backend.devices.get_mut(&node) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(output) = device
|
let Some(output) = device
|
||||||
.outputs
|
.outputs
|
||||||
.get_mut(&crtc)
|
.get_mut(&crtc)
|
||||||
@@ -1179,11 +1361,23 @@ fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
output.callback_timer = None;
|
output.callback_timer = None;
|
||||||
if !backend.active || !device.drm.is_active() || state.session_lock.sleeping {
|
if !backend.active || !device.drm.is_active() || state.session_lock.sleeping || output.frame_pending {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
output.callback_clock.sent(Instant::now());
|
|
||||||
|
let timestamp = monotonic_now();
|
||||||
|
let Some(next) = output.scheduler.callback_deadline(timestamp, timestamp) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if next > timestamp {
|
||||||
|
schedule_frame_callbacks(state, node, crtc, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.scheduler.callback_sent(timestamp);
|
||||||
send_frame_callbacks(state, identity);
|
send_frame_callbacks(state, identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,7 +1391,8 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
|||||||
.elements()
|
.elements()
|
||||||
.filter(|window| {
|
.filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
})
|
})
|
||||||
@@ -1413,16 +1608,30 @@ fn create_direct_output(
|
|||||||
let identity = connector_identity(drm, &connector);
|
let identity = connector_identity(drm, &connector);
|
||||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||||
let surface = GbmBufferedSurface::new(
|
let node = DrmNode::from_file(gbm.as_fd())?;
|
||||||
|
let render_node = node
|
||||||
|
.node_with_type(NodeType::Render)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.unwrap_or(node);
|
||||||
|
let mut surface = DrmCompositor::new(
|
||||||
|
OutputModeSource::Static {
|
||||||
|
size: OutputMode::from(mode).size,
|
||||||
|
scale: settings.scale.into(),
|
||||||
|
transform: output_transform(settings.transform),
|
||||||
|
},
|
||||||
drm_surface,
|
drm_surface,
|
||||||
|
None,
|
||||||
allocator,
|
allocator,
|
||||||
&[Fourcc::Argb8888, Fourcc::Abgr8888],
|
GbmFramebufferExporter::new(gbm.clone(), Some(render_node)),
|
||||||
|
[Fourcc::Argb8888, Fourcc::Abgr8888],
|
||||||
renderer.dmabuf_formats(),
|
renderer.dmabuf_formats(),
|
||||||
|
drm.cursor_size(),
|
||||||
|
Some(gbm.clone()),
|
||||||
)?;
|
)?;
|
||||||
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
||||||
// leave a phantom output/workspace that could receive evacuated windows.
|
// leave a phantom output/workspace that could receive evacuated windows.
|
||||||
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
||||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
surface.set_output_mode_source((&output).into());
|
||||||
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");
|
||||||
@@ -1434,11 +1643,13 @@ fn create_direct_output(
|
|||||||
output,
|
output,
|
||||||
global,
|
global,
|
||||||
surface,
|
surface,
|
||||||
damage_tracker,
|
primary_commit: None,
|
||||||
|
capture_texture: None,
|
||||||
render_metrics,
|
render_metrics,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
power_off: false,
|
power_off: false,
|
||||||
callback_clock: Default::default(),
|
scheduler: crate::frame_scheduler::FrameScheduler::new(refresh_duration_from_mode(mode)),
|
||||||
|
render_timer: None,
|
||||||
callback_timer: None,
|
callback_timer: None,
|
||||||
lock_frame_pending: false,
|
lock_frame_pending: false,
|
||||||
})
|
})
|
||||||
@@ -1540,9 +1751,15 @@ impl DirectBackendState {
|
|||||||
time: DrmEventTime,
|
time: DrmEventTime,
|
||||||
sequence: u32,
|
sequence: u32,
|
||||||
) -> Option<Duration> {
|
) -> Option<Duration> {
|
||||||
self.presentation
|
let missed = self
|
||||||
.get_mut(&(node, crtc))
|
.devices
|
||||||
.and_then(|clock| clock.record(time, sequence))
|
.get(&node)
|
||||||
|
.and_then(|device| device.outputs.get(&crtc))
|
||||||
|
.map_or(0, |output| output.scheduler.missed_deadlines);
|
||||||
|
self.presentation.get_mut(&(node, crtc)).and_then(|clock| {
|
||||||
|
clock.missed_deadlines = missed;
|
||||||
|
clock.record(time, sequence)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1566,21 +1783,6 @@ impl PresentationClock {
|
|||||||
|
|
||||||
self.last_presentation = Some(time);
|
self.last_presentation = Some(time);
|
||||||
self.presented_frames = self.presented_frames.saturating_add(1);
|
self.presented_frames = self.presented_frames.saturating_add(1);
|
||||||
if let (Some(delta), Some(refresh)) = (delta, self.refresh_interval)
|
|
||||||
&& delta > refresh + refresh / 2
|
|
||||||
{
|
|
||||||
let elapsed_frames = (delta.as_nanos() / refresh.as_nanos()).max(1) as u64;
|
|
||||||
let missed = elapsed_frames.saturating_sub(1);
|
|
||||||
self.missed_deadlines = self.missed_deadlines.saturating_add(missed);
|
|
||||||
tracing::warn!(
|
|
||||||
sequence,
|
|
||||||
?delta,
|
|
||||||
?refresh,
|
|
||||||
missed,
|
|
||||||
total = self.missed_deadlines,
|
|
||||||
"missed DRM presentation deadline"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
delta
|
delta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1639,14 +1841,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn presentation_clock_counts_missed_deadlines() {
|
fn presentation_clock_does_not_count_idle_gaps_as_missed_deadlines() {
|
||||||
let mut clock = PresentationClock::default();
|
let mut clock = PresentationClock::default();
|
||||||
clock.set_refresh(60_000);
|
clock.set_refresh(60_000);
|
||||||
|
|
||||||
clock.record(DrmEventTime::Monotonic(Duration::ZERO), 1);
|
clock.record(DrmEventTime::Monotonic(Duration::ZERO), 1);
|
||||||
clock.record(DrmEventTime::Monotonic(Duration::from_millis(50)), 2);
|
clock.record(DrmEventTime::Monotonic(Duration::from_millis(50)), 2);
|
||||||
|
|
||||||
assert_eq!(clock.missed_deadlines, 2);
|
assert_eq!(clock.missed_deadlines, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::renderer::element::RenderElement;
|
||||||
|
use smithay::backend::renderer::gles::{GlesError, GlesRenderer, GlesTarget, GlesTexture};
|
||||||
|
use smithay::backend::renderer::{Bind, Offscreen, Texture};
|
||||||
|
use smithay::output::Output;
|
||||||
|
|
||||||
|
use crate::Ferese;
|
||||||
|
use crate::render::{redraw_output, sampled_output_elements};
|
||||||
|
use crate::state::FrameScene;
|
||||||
|
|
||||||
|
pub(super) fn capture_output(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
texture: &mut Option<GlesTexture>,
|
||||||
|
output: &Output,
|
||||||
|
scene: &FrameScene,
|
||||||
|
) -> Result<(), GlesError> {
|
||||||
|
for include_cursor in [true, false] {
|
||||||
|
if !state.has_pending_screencopy(output, include_cursor) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample the same scene into an independent target: the displayed
|
||||||
|
// primary buffer may omit both a scanned-out client and hardware cursor.
|
||||||
|
let elements = sampled_output_elements(state, renderer, output, include_cursor, scene);
|
||||||
|
let target = render_capture(renderer, texture, output, &elements)?;
|
||||||
|
state.process_screencopies(renderer, &target, output, include_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_capture<'a, E: RenderElement<GlesRenderer>>(
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
texture: &'a mut Option<GlesTexture>,
|
||||||
|
output: &Output,
|
||||||
|
elements: &[E],
|
||||||
|
) -> Result<GlesTarget<'a>, GlesError> {
|
||||||
|
let size = output.current_mode().expect("output has a mode").size;
|
||||||
|
|
||||||
|
if texture
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|texture| texture.size() != (size.w, size.h).into())
|
||||||
|
{
|
||||||
|
*texture = Some(Offscreen::<GlesTexture>::create_buffer(
|
||||||
|
renderer,
|
||||||
|
Fourcc::Abgr8888,
|
||||||
|
(size.w, size.h).into(),
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebinding for each capture restores GL state after readback.
|
||||||
|
let mut target = renderer.bind(texture.as_mut().expect("capture target allocated"))?;
|
||||||
|
redraw_output(renderer, &mut target, output, elements)?;
|
||||||
|
Ok(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
use smithay::backend::renderer::element::solid::SolidColorRenderElement;
|
||||||
|
use smithay::backend::renderer::element::{Id, Kind};
|
||||||
|
use smithay::backend::renderer::utils::CommitCounter;
|
||||||
|
use smithay::backend::renderer::{Color32F, ExportMem};
|
||||||
|
use smithay::output::{Mode, PhysicalProperties, Subpixel};
|
||||||
|
use smithay::utils::{Physical, Rectangle, Transform};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn capture_recomposes_cursor_without_touching_the_display_target() {
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let context = EGLContext::new(&display).unwrap();
|
||||||
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
|
let output = Output::new(
|
||||||
|
"capture".into(),
|
||||||
|
PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (32, 24).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
Some(Transform::Normal),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let background = SolidColorRenderElement::new(
|
||||||
|
Id::new(),
|
||||||
|
Rectangle::<i32, Physical>::from_size((32, 24).into()),
|
||||||
|
CommitCounter::default(),
|
||||||
|
Color32F::new(1.0, 0.0, 0.0, 1.0),
|
||||||
|
Kind::Unspecified,
|
||||||
|
);
|
||||||
|
let cursor = SolidColorRenderElement::new(
|
||||||
|
Id::new(),
|
||||||
|
Rectangle::new((0, 0).into(), (4, 4).into()),
|
||||||
|
CommitCounter::default(),
|
||||||
|
Color32F::new(0.0, 1.0, 0.0, 1.0),
|
||||||
|
Kind::Cursor,
|
||||||
|
);
|
||||||
|
let mut displayed = None;
|
||||||
|
let mut capture = None;
|
||||||
|
{
|
||||||
|
let _ = render_capture(
|
||||||
|
&mut renderer,
|
||||||
|
&mut displayed,
|
||||||
|
&output,
|
||||||
|
std::slice::from_ref(&background),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (elements, expected) in [
|
||||||
|
(vec![cursor, background.clone()], [0, 255, 0, 255]),
|
||||||
|
(vec![background], [255, 0, 0, 255]),
|
||||||
|
] {
|
||||||
|
let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap();
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||||
|
assert_eq!(&pixels[..4], &expected);
|
||||||
|
assert_eq!(&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], &[255, 0, 0, 255]);
|
||||||
|
drop(target);
|
||||||
|
|
||||||
|
let target = renderer.bind(displayed.as_mut().unwrap()).unwrap();
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(&renderer.map_texture(&mapping).unwrap()[..4], &[255, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.change_current_state(
|
||||||
|
Some(Mode {
|
||||||
|
size: (48, 16).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
Some(Transform::_90),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let _ = render_capture::<SolidColorRenderElement>(&mut renderer, &mut capture, &output, &[]).unwrap();
|
||||||
|
assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
use smithay::backend::drm::compositor::FrameFlags;
|
||||||
|
|
||||||
|
pub(super) fn frame_flags(fullscreen: bool, animating: bool, locked: bool) -> FrameFlags {
|
||||||
|
let mut flags = FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT;
|
||||||
|
|
||||||
|
if fullscreen && !animating && !locked {
|
||||||
|
flags |= FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay planes stay disabled until their effects and capture behavior
|
||||||
|
// have been validated on hardware independently of fullscreen scanout.
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn only_settled_unlocked_fullscreen_frames_allow_primary_scanout() {
|
||||||
|
for fullscreen in [false, true] {
|
||||||
|
for animating in [false, true] {
|
||||||
|
for locked in [false, true] {
|
||||||
|
let flags = frame_flags(fullscreen, animating, locked);
|
||||||
|
assert_eq!(
|
||||||
|
flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT),
|
||||||
|
fullscreen && !animating && !locked,
|
||||||
|
);
|
||||||
|
assert!(flags.contains(FrameFlags::ALLOW_CURSOR_PLANE_SCANOUT));
|
||||||
|
assert!(!flags.intersects(
|
||||||
|
FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT_ANY
|
||||||
|
| FrameFlags::ALLOW_OVERLAY_PLANE_SCANOUT
|
||||||
|
| FrameFlags::SKIP_CURSOR_ONLY_UPDATES
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
use super::*;
|
||||||
|
use crate::frame_scheduler::FramePlan;
|
||||||
|
|
||||||
|
pub(super) fn monotonic_now() -> Duration {
|
||||||
|
smithay::utils::Clock::<Monotonic>::new().now().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn cancel_output_timers(
|
||||||
|
handle: &smithay::reexports::calloop::LoopHandle<'static, Ferese>,
|
||||||
|
output: &mut DirectOutput,
|
||||||
|
) {
|
||||||
|
for token in [output.render_timer.take(), output.callback_timer.take()]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
handle.remove(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
output.scheduler.reset(Duration::from_nanos(
|
||||||
|
1_000_000_000_000 / OutputMode::from(output.mode).refresh.max(1) as u64,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn request_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||||
|
if state.session_lock.sleeping {
|
||||||
|
sleep_locked_outputs(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = monotonic_now();
|
||||||
|
let Some(output) = state
|
||||||
|
.direct_backend
|
||||||
|
.as_mut()
|
||||||
|
.filter(|backend| backend.active)
|
||||||
|
.and_then(|backend| backend.devices.get_mut(&node))
|
||||||
|
.filter(|device| device.drm.is_active())
|
||||||
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if output.power_off {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.scheduler.request(now);
|
||||||
|
arm_frame(state, node, crtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn arm_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||||
|
let now = monotonic_now();
|
||||||
|
let Some(output) = state
|
||||||
|
.direct_backend
|
||||||
|
.as_mut()
|
||||||
|
.filter(|backend| backend.active)
|
||||||
|
.and_then(|backend| backend.devices.get_mut(&node))
|
||||||
|
.filter(|device| device.drm.is_active())
|
||||||
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if output.render_timer.is_some() || output.frame_pending || output.power_off || state.session_lock.sleeping {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(plan) = output.scheduler.plan(now) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let identity = output.output.clone();
|
||||||
|
// Even an immediate repaint is queued. Bursts of input and surface commits
|
||||||
|
// in the same event-loop dispatch are folded into one scene build.
|
||||||
|
let deadline = Instant::now() + plan.render_at.saturating_sub(now);
|
||||||
|
match state
|
||||||
|
.loop_handle
|
||||||
|
.insert_source(Timer::from_deadline(deadline), move |_, _, state| {
|
||||||
|
dispatch_frame(state, node, crtc, &identity);
|
||||||
|
TimeoutAction::Drop
|
||||||
|
}) {
|
||||||
|
Ok(token) => {
|
||||||
|
if let Some(output) = state
|
||||||
|
.direct_backend
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|backend| backend.devices.get_mut(&node))
|
||||||
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
|
{
|
||||||
|
output.render_timer = Some(token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(error) => tracing::warn!(%error, "could not schedule DRM frame"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
|
||||||
|
let Some(output) = state
|
||||||
|
.direct_backend
|
||||||
|
.as_mut()
|
||||||
|
.filter(|backend| backend.active)
|
||||||
|
.and_then(|backend| backend.devices.get_mut(&node))
|
||||||
|
.filter(|device| device.drm.is_active())
|
||||||
|
.and_then(|device| device.outputs.get_mut(&crtc))
|
||||||
|
.filter(|output| &output.output == identity)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
output.render_timer = None;
|
||||||
|
if output.frame_pending || output.power_off || state.session_lock.sleeping {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(plan) = output.scheduler.begin() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let animating = state.advance_animations(Instant::now());
|
||||||
|
if let Some(backend) = state.direct_backend.as_mut() {
|
||||||
|
backend.animation_active = animating;
|
||||||
|
}
|
||||||
|
|
||||||
|
render_output(state, node, crtc, plan);
|
||||||
|
if state.output_has_animations(identity) {
|
||||||
|
request_frame(state, node, crtc);
|
||||||
|
}
|
||||||
|
|
||||||
|
arm_animation_timer(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) {
|
||||||
|
output.scheduler.rendered(plan, monotonic_now(), cost, submitted);
|
||||||
|
if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() {
|
||||||
|
let started = monotonic_now().saturating_sub(cost);
|
||||||
|
tracing::debug!(target: "ferese::render", output = %output.output.name(),
|
||||||
|
render_target_us = plan.render_at.as_micros(), presentation_target_us = plan.present_at.as_micros(),
|
||||||
|
target_phase_known = plan.phase_known,
|
||||||
|
render_started_us = started.as_micros(), timer_lateness_us = started.saturating_sub(plan.render_at).as_micros(),
|
||||||
|
render_us = cost.as_micros(), submitted, "DRM frame schedule");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -438,6 +438,43 @@ mod tests {
|
|||||||
assert!(Config::parse_source("workspaces {\n auto-back-and-forth \"yes\"\n}\n").is_err());
|
assert!(Config::parse_source("workspaces {\n auto-back-and-forth \"yes\"\n}\n").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn focus_history_actions_are_bindable_and_have_defaults() {
|
||||||
|
let defaults = Config::default().runtime_config().unwrap();
|
||||||
|
for action in [
|
||||||
|
BindingAction::FocusLastWindow,
|
||||||
|
BindingAction::FocusMru(false),
|
||||||
|
BindingAction::FocusMru(true),
|
||||||
|
] {
|
||||||
|
assert!(defaults.bindings.iter().any(|binding| binding.action == action));
|
||||||
|
}
|
||||||
|
for (name, action) in [
|
||||||
|
("focus-last-window", BindingAction::FocusLastWindow),
|
||||||
|
("focus-mru-next", BindingAction::FocusMru(false)),
|
||||||
|
("focus-mru-previous", BindingAction::FocusMru(true)),
|
||||||
|
] {
|
||||||
|
assert_eq!(parse_action(name, None, &HashMap::new()).unwrap(), action);
|
||||||
|
assert!(parse_action(name, Some("left"), &HashMap::new()).is_err());
|
||||||
|
}
|
||||||
|
let bindings = &defaults.bindings;
|
||||||
|
assert!(bindings.iter().any(|binding| binding.matches(
|
||||||
|
23_u32.into(),
|
||||||
|
&[keysyms::KEY_Tab],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
) && binding.action == BindingAction::FocusMru(false)));
|
||||||
|
assert!(bindings.iter().any(|binding| binding.matches(
|
||||||
|
23_u32.into(),
|
||||||
|
&[keysyms::KEY_Tab],
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
) && binding.action == BindingAction::FocusMru(true)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn packaged_and_custom_kdl_pass_runtime_validation() {
|
fn packaged_and_custom_kdl_pass_runtime_validation() {
|
||||||
Config::parse_source(include_str!("../../packaging/config.kdl"))
|
Config::parse_source(include_str!("../../packaging/config.kdl"))
|
||||||
@@ -888,7 +925,7 @@ mod tests {
|
|||||||
let input = config.input_settings().unwrap();
|
let input = config.input_settings().unwrap();
|
||||||
let bindings = config.bindings(&input).unwrap();
|
let bindings = config.bindings(&input).unwrap();
|
||||||
|
|
||||||
assert_eq!(bindings.len(), 50);
|
assert_eq!(bindings.len(), 53);
|
||||||
for (shift, action) in [
|
for (shift, action) in [
|
||||||
(false, BindingAction::ToggleMaximized),
|
(false, BindingAction::ToggleMaximized),
|
||||||
(true, BindingAction::ToggleFullscreen),
|
(true, BindingAction::ToggleFullscreen),
|
||||||
@@ -931,14 +968,14 @@ mod tests {
|
|||||||
|
|
||||||
let input = replaced.input_settings().unwrap();
|
let input = replaced.input_settings().unwrap();
|
||||||
let bindings = replaced.bindings(&input).unwrap();
|
let bindings = replaced.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 50);
|
assert_eq!(bindings.len(), 53);
|
||||||
assert!(bindings.iter().any(|binding| {
|
assert!(bindings.iter().any(|binding| {
|
||||||
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
|
binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()])
|
||||||
}));
|
}));
|
||||||
|
|
||||||
let input = unbound.input_settings().unwrap();
|
let input = unbound.input_settings().unwrap();
|
||||||
let bindings = unbound.bindings(&input).unwrap();
|
let bindings = unbound.bindings(&input).unwrap();
|
||||||
assert_eq!(bindings.len(), 49);
|
assert_eq!(bindings.len(), 52);
|
||||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,14 @@ pub(super) fn parse_action(
|
|||||||
no_argument()?;
|
no_argument()?;
|
||||||
Ok(BindingAction::WorkspaceBackAndForth)
|
Ok(BindingAction::WorkspaceBackAndForth)
|
||||||
}
|
}
|
||||||
|
"focus-last-window" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::FocusLastWindow)
|
||||||
|
}
|
||||||
|
"focus-mru-next" | "focus-mru-previous" => {
|
||||||
|
no_argument()?;
|
||||||
|
Ok(BindingAction::FocusMru(action == "focus-mru-previous"))
|
||||||
|
}
|
||||||
"spawn" => {
|
"spawn" => {
|
||||||
let command = required_argument()?;
|
let command = required_argument()?;
|
||||||
let argv = commands
|
let argv = commands
|
||||||
@@ -369,6 +377,9 @@ pub(super) fn default_bindings() -> Vec<BindingConfig> {
|
|||||||
binding("Super+Tab", "toggle-overview", None),
|
binding("Super+Tab", "toggle-overview", None),
|
||||||
binding("Super+F1", "toggle-keybinding-guide", None),
|
binding("Super+F1", "toggle-keybinding-guide", None),
|
||||||
binding("Super+Escape", "workspace-back-and-forth", None),
|
binding("Super+Escape", "workspace-back-and-forth", None),
|
||||||
|
binding("Super+BackSpace", "focus-last-window", None),
|
||||||
|
binding("Alt+Tab", "focus-mru-next", None),
|
||||||
|
binding("Alt+Shift+Tab", "focus-mru-previous", None),
|
||||||
binding("Super+Shift+E", "exit", None),
|
binding("Super+Shift+E", "exit", None),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -433,6 +444,8 @@ pub enum BindingAction {
|
|||||||
Close,
|
Close,
|
||||||
Exit,
|
Exit,
|
||||||
Focus(ferese_layout::Direction),
|
Focus(ferese_layout::Direction),
|
||||||
|
FocusLastWindow,
|
||||||
|
FocusMru(bool),
|
||||||
Move(ferese_layout::Direction),
|
Move(ferese_layout::Direction),
|
||||||
Resize(ferese_layout::Direction),
|
Resize(ferese_layout::Direction),
|
||||||
SwitchWorkspace(u8),
|
SwitchWorkspace(u8),
|
||||||
@@ -534,6 +547,13 @@ impl Binding {
|
|||||||
BindingAction::Close => "Close window".into(),
|
BindingAction::Close => "Close window".into(),
|
||||||
BindingAction::Exit => "Log out (with confirmation)".into(),
|
BindingAction::Exit => "Log out (with confirmation)".into(),
|
||||||
BindingAction::Focus(direction) => format!("Focus window {direction:?}"),
|
BindingAction::Focus(direction) => format!("Focus window {direction:?}"),
|
||||||
|
BindingAction::FocusLastWindow => "Return to last focused window".into(),
|
||||||
|
BindingAction::FocusMru(reverse) => if *reverse {
|
||||||
|
"Previous window in focus history"
|
||||||
|
} else {
|
||||||
|
"Next window in focus history"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
BindingAction::Move(direction) => format!("Move window {direction:?}"),
|
||||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub(crate) fn target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A bounded, interruption-safe opacity transition; it never overshoots.
|
/// A bounded, interruption-safe opacity transition; it never overshoots.
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct DimAnimation {
|
pub(crate) struct DimAnimation {
|
||||||
pub current: f64,
|
pub current: f64,
|
||||||
start: f64,
|
start: f64,
|
||||||
@@ -27,6 +27,14 @@ pub(crate) struct DimAnimation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DimAnimation {
|
impl DimAnimation {
|
||||||
|
pub(crate) fn is_animating(&self) -> bool {
|
||||||
|
self.current != self.target
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn predict(&mut self, delta: Duration, duration_ms: f64) {
|
||||||
|
self.advance(self.target, delta, duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(value: f64) -> Self {
|
pub fn new(value: f64) -> Self {
|
||||||
Self {
|
Self {
|
||||||
current: value,
|
current: value,
|
||||||
@@ -42,16 +50,19 @@ impl DimAnimation {
|
|||||||
self.target = target;
|
self.target = target;
|
||||||
self.elapsed = 0.0;
|
self.elapsed = 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if duration_ms == 0.0 {
|
if duration_ms == 0.0 {
|
||||||
self.current = target;
|
self.current = target;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.elapsed += delta.as_secs_f64() * 1000.0;
|
self.elapsed += delta.as_secs_f64() * 1000.0;
|
||||||
let progress = (self.elapsed / duration_ms).min(1.0);
|
let progress = (self.elapsed / duration_ms).min(1.0);
|
||||||
if progress == 1.0 {
|
if progress == 1.0 {
|
||||||
self.current = target;
|
self.current = target;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let eased = progress * progress * (3.0 - 2.0 * progress);
|
let eased = progress * progress * (3.0 - 2.0 * progress);
|
||||||
self.current = self.start + (target - self.start) * eased;
|
self.current = self.start + (target - self.start) * eased;
|
||||||
self.current != target
|
self.current != target
|
||||||
|
|||||||
+24
-11
@@ -56,7 +56,7 @@ struct SurfaceEffectsState {
|
|||||||
attached: AtomicBool,
|
attached: AtomicBool,
|
||||||
generation: AtomicU64,
|
generation: AtomicU64,
|
||||||
role: Mutex<Option<SemanticRole>>,
|
role: Mutex<Option<SemanticRole>>,
|
||||||
regions: Mutex<Option<Vec<[i32; 5]>>>,
|
regions: Mutex<Option<Vec<[f64; 5]>>>,
|
||||||
opacity: Mutex<u32>,
|
opacity: Mutex<u32>,
|
||||||
presentation_supported: AtomicBool,
|
presentation_supported: AtomicBool,
|
||||||
dismissing: AtomicBool,
|
dismissing: AtomicBool,
|
||||||
@@ -183,7 +183,7 @@ impl Dispatch<FereseSurfaceMaterialV1, SurfaceEffectsUserData> for Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
|
pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[f64; 5]>> {
|
||||||
with_states(surface, |states| {
|
with_states(surface, |states| {
|
||||||
states
|
states
|
||||||
.data_map
|
.data_map
|
||||||
@@ -195,7 +195,7 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
|
fn decode_regions(bytes: &[u8]) -> Option<Vec<[f64; 5]>> {
|
||||||
if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 {
|
if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -204,8 +204,11 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
|
|||||||
.0
|
.0
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tuple| {
|
.map(|tuple| {
|
||||||
let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i])));
|
||||||
(values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768))
|
(values[2] > 0.0
|
||||||
|
&& values[3] > 0.0
|
||||||
|
&& values[4] >= 0.0
|
||||||
|
&& values.iter().all(|v| v.is_finite() && v.abs() <= 32768.0))
|
||||||
.then_some(values)
|
.then_some(values)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -439,15 +442,25 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rounded_regions_are_bounded_and_validated() {
|
fn fractional_regions_validate_all_wire_values() {
|
||||||
let region = [12_i32, 8, 120, 48, 11];
|
let region = [0.25f32, -0.75, 100.5, 40.25, 13.5];
|
||||||
let bytes: Vec<_> = region.into_iter().flat_map(i32::to_ne_bytes).collect();
|
let encode = |r: [f32; 5]| r.into_iter().flat_map(f32::to_ne_bytes).collect::<Vec<_>>();
|
||||||
assert_eq!(decode_regions(&bytes), Some(vec![region]));
|
let bytes = encode(region);
|
||||||
|
assert_eq!(decode_regions(&bytes), Some(vec![region.map(f64::from)]));
|
||||||
assert_eq!(decode_regions(&[]), Some(vec![]));
|
assert_eq!(decode_regions(&[]), Some(vec![]));
|
||||||
assert!(decode_regions(&bytes[..19]).is_none());
|
assert!(decode_regions(&bytes[..19]).is_none());
|
||||||
assert!(decode_regions(&bytes.repeat(33)).is_none());
|
assert!(decode_regions(&bytes.repeat(33)).is_none());
|
||||||
for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] {
|
for index in 0..5 {
|
||||||
assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::<Vec<_>>()).is_none());
|
for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 32769.0] {
|
||||||
|
let mut bad = region;
|
||||||
|
bad[index] = invalid;
|
||||||
|
assert!(decode_regions(&encode(bad)).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (index, value) in [(2, 0.0), (3, -1.0), (4, -0.25)] {
|
||||||
|
let mut bad = region;
|
||||||
|
bad[index] = value;
|
||||||
|
assert!(decode_regions(&encode(bad)).is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,402 @@
|
|||||||
|
//! Fixed-refresh scheduling policy, independent of DRM and the event loop.
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const MIN_RENDER: Duration = Duration::from_millis(2);
|
||||||
|
const TIMER_MARGIN: Duration = Duration::from_millis(1);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct FramePlan {
|
||||||
|
pub render_at: Duration,
|
||||||
|
pub present_at: Duration,
|
||||||
|
pub requested_at: Duration,
|
||||||
|
pub phase_known: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct FrameScheduler {
|
||||||
|
interval: Duration,
|
||||||
|
last_presented: Option<Duration>,
|
||||||
|
last_target: Option<Duration>,
|
||||||
|
requested_at: Option<Duration>,
|
||||||
|
scheduled: Option<FramePlan>,
|
||||||
|
submitted: Option<FramePlan>,
|
||||||
|
last_callback: Option<Duration>,
|
||||||
|
render_estimate: Duration,
|
||||||
|
render_variance: Duration,
|
||||||
|
last_sample: Option<Duration>,
|
||||||
|
extra_margin: Duration,
|
||||||
|
pub missed_deadlines: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameScheduler {
|
||||||
|
pub fn new(interval: Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
interval: interval.max(Duration::from_micros(1)),
|
||||||
|
last_presented: None,
|
||||||
|
last_target: None,
|
||||||
|
requested_at: None,
|
||||||
|
scheduled: None,
|
||||||
|
submitted: None,
|
||||||
|
last_callback: None,
|
||||||
|
render_estimate: MIN_RENDER,
|
||||||
|
render_variance: Duration::ZERO,
|
||||||
|
last_sample: None,
|
||||||
|
extra_margin: Duration::ZERO,
|
||||||
|
missed_deadlines: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self, interval: Duration) {
|
||||||
|
let missed = self.missed_deadlines;
|
||||||
|
*self = Self::new(interval);
|
||||||
|
self.missed_deadlines = missed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn request(&mut self, now: Duration) {
|
||||||
|
self.requested_at.get_or_insert(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn budget(&self, now: Duration) -> Duration {
|
||||||
|
let idle = self
|
||||||
|
.last_presented
|
||||||
|
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
|
||||||
|
if idle {
|
||||||
|
// A cold GPU is less predictable. Start immediately on idle wakeup.
|
||||||
|
self.interval
|
||||||
|
} else {
|
||||||
|
(self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin).min(self.interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn phase_at_or_after(&self, time: Duration) -> Duration {
|
||||||
|
let Some(last) = self.last_presented else {
|
||||||
|
return time;
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = time
|
||||||
|
.saturating_sub(last)
|
||||||
|
.as_nanos()
|
||||||
|
.div_ceil(self.interval.as_nanos())
|
||||||
|
.max(1);
|
||||||
|
last + self.interval.mul_f64(count as f64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plan(&mut self, now: Duration) -> Option<FramePlan> {
|
||||||
|
if self.submitted.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(plan) = self.scheduled {
|
||||||
|
return Some(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
let requested_at = self.requested_at?;
|
||||||
|
if self.last_presented.is_some_and(|last| last > now + self.interval) {
|
||||||
|
self.last_presented = None;
|
||||||
|
self.last_target = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let budget = self.budget(now);
|
||||||
|
let idle = self
|
||||||
|
.last_presented
|
||||||
|
.is_none_or(|last| now.saturating_sub(last) >= self.interval * 100);
|
||||||
|
// With one in-flight GBM frame, submitting immediately can hit the
|
||||||
|
// nearest vblank even when the conservative budget is larger than
|
||||||
|
// the remaining time. Do not deliberately skip that presentation.
|
||||||
|
let next = if self.last_presented.is_some() {
|
||||||
|
now + Duration::from_nanos(1)
|
||||||
|
} else {
|
||||||
|
now + self.interval
|
||||||
|
};
|
||||||
|
|
||||||
|
let earliest = next.max(self.last_target.map_or(next, |last| last + self.interval));
|
||||||
|
let present_at = self.phase_at_or_after(earliest);
|
||||||
|
let plan = FramePlan {
|
||||||
|
render_at: if idle {
|
||||||
|
now
|
||||||
|
} else {
|
||||||
|
present_at.saturating_sub(budget).max(now)
|
||||||
|
},
|
||||||
|
present_at,
|
||||||
|
requested_at,
|
||||||
|
phase_known: self.last_presented.is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.scheduled = Some(plan);
|
||||||
|
Some(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin(&mut self) -> Option<FramePlan> {
|
||||||
|
let plan = self.scheduled.take()?;
|
||||||
|
self.requested_at = None;
|
||||||
|
self.last_target = Some(plan.present_at);
|
||||||
|
Some(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rendered(&mut self, plan: FramePlan, now: Duration, cost: Duration, submitted: bool) {
|
||||||
|
if submitted {
|
||||||
|
self.submitted = Some(plan);
|
||||||
|
let elapsed = self
|
||||||
|
.last_sample
|
||||||
|
.map_or(Duration::from_millis(500), |last| now.saturating_sub(last));
|
||||||
|
self.last_sample = Some(now);
|
||||||
|
let sample = cost.max(MIN_RENDER);
|
||||||
|
let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0);
|
||||||
|
let deviation = sample.saturating_sub(self.render_estimate);
|
||||||
|
let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1);
|
||||||
|
self.render_variance = self.render_variance.mul_f64(1.0 - variance_ratio).max(deviation);
|
||||||
|
self.render_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn presented(&mut self, timestamp: Duration) -> Option<FramePlan> {
|
||||||
|
// Regressive or implausibly early events must not shift the phase backward.
|
||||||
|
if self.last_presented.is_some_and(|last| timestamp < last) {
|
||||||
|
self.last_presented = None;
|
||||||
|
self.last_target = None;
|
||||||
|
return self.submitted.take();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_presented = Some(timestamp);
|
||||||
|
let plan = self.submitted.take()?;
|
||||||
|
// The first submission has only an estimated phase. Retiring it on
|
||||||
|
// the actual refresh must not carry that estimate into the next frame.
|
||||||
|
self.last_target = Some(timestamp);
|
||||||
|
let late = timestamp.saturating_sub(plan.present_at);
|
||||||
|
if plan.phase_known && late > self.interval / 2 {
|
||||||
|
let missed = (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos();
|
||||||
|
self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64);
|
||||||
|
// CPU submission timings may omit asynchronous GPU work. Increase
|
||||||
|
// the margin promptly after a real miss; relax it slowly afterward.
|
||||||
|
self.extra_margin = (self.extra_margin + TIMER_MARGIN).min(self.interval / 2);
|
||||||
|
} else {
|
||||||
|
self.extra_margin = self.extra_margin.mul_f64(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(plan)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retire_unknown(&mut self) {
|
||||||
|
self.submitted = None;
|
||||||
|
self.last_presented = None;
|
||||||
|
self.last_target = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forecast_time(&self, now: Duration, plan: FramePlan) -> Option<Duration> {
|
||||||
|
// Before the first real page flip there is no reliable refresh phase.
|
||||||
|
// A late timer still samples the next physical presentation, while
|
||||||
|
// deadline metrics retain the original scheduled target.
|
||||||
|
plan.phase_known.then(|| {
|
||||||
|
plan.present_at
|
||||||
|
.max(self.phase_at_or_after(now + Duration::from_nanos(1)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callback_deadline(&self, now: Duration, target: Duration) -> Option<Duration> {
|
||||||
|
// A timer from a callback-only update must yield to a later submission.
|
||||||
|
// Its page flip will release callbacks using the observed refresh phase.
|
||||||
|
if self.submitted.is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tolerance = Duration::from_micros(500).min(self.interval / 8);
|
||||||
|
if target <= now
|
||||||
|
&& self
|
||||||
|
.last_callback
|
||||||
|
.is_none_or(|last| last + self.interval <= now + tolerance)
|
||||||
|
{
|
||||||
|
return Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
|
let earliest = self
|
||||||
|
.last_callback
|
||||||
|
.map_or(now, |last| last + self.interval)
|
||||||
|
.max(target)
|
||||||
|
.max(now);
|
||||||
|
Some(self.phase_at_or_after(earliest))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn callback_sent(&mut self, now: Duration) {
|
||||||
|
// Record the refresh slot, not delayed event-loop dispatch time.
|
||||||
|
// Otherwise a callback delivered a few microseconds late can make
|
||||||
|
// the next refresh look too early and halve a client's frame rate.
|
||||||
|
self.last_callback = Some(self.last_presented.map_or(now, |last| {
|
||||||
|
let tolerance = Duration::from_micros(500).min(self.interval / 8);
|
||||||
|
let slots = (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos();
|
||||||
|
last + self.interval.mul_f64(slots as f64)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn ms(value: u64) -> Duration {
|
||||||
|
Duration::from_millis(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requests_coalesce_without_postponing_the_frame_and_wait_for_retirement() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
assert_eq!((plan.render_at, plan.present_at), (ms(107), ms(110)));
|
||||||
|
scheduler.request(ms(102));
|
||||||
|
assert_eq!(scheduler.plan(ms(102)), Some(plan));
|
||||||
|
assert_eq!(scheduler.begin(), Some(plan));
|
||||||
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
|
scheduler.request(ms(109));
|
||||||
|
assert_eq!(scheduler.plan(ms(109)), None);
|
||||||
|
scheduler.presented(ms(110));
|
||||||
|
assert_eq!(scheduler.plan(ms(110)).unwrap().present_at, ms(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn idle_wakeup_starts_early_and_is_not_a_missed_deadline() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(10_100));
|
||||||
|
let plan = scheduler.plan(ms(10_100)).unwrap();
|
||||||
|
assert_eq!(plan.render_at, ms(10_100));
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(plan, ms(10_101), ms(1), true);
|
||||||
|
scheduler.presented(ms(10_110));
|
||||||
|
assert_eq!(scheduler.missed_deadlines, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn misses_measure_the_submitted_target_and_expand_the_budget() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
|
assert_eq!(scheduler.presented(ms(130)), Some(plan));
|
||||||
|
assert_eq!(scheduler.missed_deadlines, 2);
|
||||||
|
assert_eq!(scheduler.budget(ms(131)), ms(4));
|
||||||
|
assert_eq!(scheduler.presented(ms(129)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_retirement_aligns_the_next_frame_to_the_observed_refresh() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.request(ms(100));
|
||||||
|
let plan = scheduler.plan(ms(100)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(plan, ms(102), ms(2), true);
|
||||||
|
scheduler.presented(ms(103));
|
||||||
|
|
||||||
|
scheduler.request(ms(104));
|
||||||
|
assert_eq!(scheduler.plan(ms(104)).unwrap().present_at, ms(113));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_damage_attempts_and_callbacks_do_not_spin_within_a_refresh() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(plan, ms(108), ms(1), false);
|
||||||
|
scheduler.callback_sent(ms(110));
|
||||||
|
scheduler.request(ms(108));
|
||||||
|
assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120));
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(111), ms(110)).unwrap(), ms(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn callback_only_timer_waits_for_a_later_submission_even_when_it_misses_its_target() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let unchanged = scheduler.plan(ms(101)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(unchanged, ms(108), ms(1), false);
|
||||||
|
let callback_at = scheduler.callback_deadline(ms(108), unchanged.present_at).unwrap();
|
||||||
|
assert_eq!(callback_at, ms(110));
|
||||||
|
|
||||||
|
scheduler.request(ms(109));
|
||||||
|
let changed = scheduler.plan(ms(109)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(changed, ms(118), ms(1), true);
|
||||||
|
|
||||||
|
// The old timer is dispatched late, after a new frame was queued.
|
||||||
|
// Neither that timer nor the predicted refresh can release callbacks.
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(118), callback_at), None);
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(120), changed.present_at), None);
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(129), callback_at), None);
|
||||||
|
|
||||||
|
scheduler.presented(ms(130));
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(130), ms(130)), Some(ms(130)));
|
||||||
|
scheduler.callback_sent(ms(130));
|
||||||
|
assert_eq!(scheduler.callback_deadline(ms(130), callback_at), Some(ms(140)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn outputs_keep_independent_phases_and_reset_cancels_obsolete_work() {
|
||||||
|
let mut fast = FrameScheduler::new(ms(4));
|
||||||
|
let mut slow = FrameScheduler::new(ms(16));
|
||||||
|
for scheduler in [&mut fast, &mut slow] {
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(fast.plan(ms(101)).unwrap().present_at, ms(104));
|
||||||
|
assert_eq!(slow.plan(ms(101)).unwrap().present_at, ms(116));
|
||||||
|
fast.reset(ms(8));
|
||||||
|
assert_eq!(fast.plan(ms(102)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn callback_dispatch_jitter_does_not_skip_a_refresh_or_duplicate_a_callback() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.callback_sent(ms(100) + Duration::from_micros(80));
|
||||||
|
scheduler.presented(ms(110));
|
||||||
|
let now = ms(110) + Duration::from_micros(10);
|
||||||
|
assert_eq!(scheduler.callback_deadline(now, now).unwrap(), now);
|
||||||
|
scheduler.callback_sent(now);
|
||||||
|
assert_eq!(scheduler.callback_deadline(now, now).unwrap(), ms(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_retirement_and_regressive_timestamps_do_not_strand_pending_work() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(101));
|
||||||
|
let plan = scheduler.plan(ms(101)).unwrap();
|
||||||
|
scheduler.begin();
|
||||||
|
scheduler.rendered(plan, ms(108), ms(1), true);
|
||||||
|
scheduler.request(ms(109));
|
||||||
|
assert_eq!(scheduler.presented(ms(90)), Some(plan));
|
||||||
|
assert!(scheduler.plan(ms(110)).is_some());
|
||||||
|
scheduler.retire_unknown();
|
||||||
|
assert!(scheduler.plan(ms(111)).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cold_gpu_and_late_dispatch_do_not_predict_a_past_or_deliberately_skipped_frame() {
|
||||||
|
let mut scheduler = FrameScheduler::new(ms(10));
|
||||||
|
scheduler.presented(ms(100));
|
||||||
|
scheduler.request(ms(10_101));
|
||||||
|
let plan = scheduler.plan(ms(10_101)).unwrap();
|
||||||
|
assert_eq!(plan.render_at, ms(10_101));
|
||||||
|
assert_eq!(plan.present_at, ms(10_110));
|
||||||
|
assert_eq!(scheduler.forecast_time(ms(10_111), plan), Some(ms(10_120)));
|
||||||
|
|
||||||
|
let mut unknown = FrameScheduler::new(ms(10));
|
||||||
|
unknown.request(ms(100));
|
||||||
|
let plan = unknown.plan(ms(100)).unwrap();
|
||||||
|
assert_eq!(unknown.forecast_time(ms(101), plan), None);
|
||||||
|
unknown.begin();
|
||||||
|
unknown.rendered(plan, ms(102), ms(2), true);
|
||||||
|
unknown.presented(ms(130));
|
||||||
|
assert_eq!(
|
||||||
|
unknown.missed_deadlines, 0,
|
||||||
|
"unknown phase is not a measurable missed deadline"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -159,10 +159,9 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum);
|
||||||
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
|
let rect = resized_rect(self.initial_rect, self.last_size, self.edges);
|
||||||
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
||||||
if let Some(id) = data.window_ids.get(&self.window).copied() {
|
if let Some(id) = data.windows.ids().get(&self.window).copied() {
|
||||||
data.floating_resize_anchors.insert(
|
data.windows.update(id, |w| {
|
||||||
id,
|
w.resize_anchor = Some((
|
||||||
(
|
|
||||||
self.edges.left(),
|
self.edges.left(),
|
||||||
self.edges.top(),
|
self.edges.top(),
|
||||||
ferese_layout::Rect::new(
|
ferese_layout::Rect::new(
|
||||||
@@ -171,8 +170,8 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
initial.size.w as f64,
|
initial.size.w as f64,
|
||||||
initial.size.h as f64,
|
initial.size.h as f64,
|
||||||
),
|
),
|
||||||
),
|
))
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
surface.with_pending_state(|state| {
|
surface.with_pending_state(|state| {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ impl XdgActivationHandler for Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
self.focused_window = self.window_ids.get(&window).copied();
|
self.focused_window = self.windows.ids().get(&window).copied();
|
||||||
if let Some(focused) = self.focused_window
|
if let Some(focused) = self.focused_window
|
||||||
&& let Err(error) = self.workspaces.focus_window(focused)
|
&& let Err(error) = self.workspaces.focus_window(focused)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ impl CompositorHandler for Ferese {
|
|||||||
if surface == &root {
|
if surface == &root {
|
||||||
if crate::state::window_has_buffer(&window) {
|
if crate::state::window_has_buffer(&window) {
|
||||||
xdg_shell::apply_initial_window_rules(self, &window);
|
xdg_shell::apply_initial_window_rules(self, &window);
|
||||||
} else if self.window_ids.contains_key(&window) {
|
} else if self.windows.ids().contains_key(&window) {
|
||||||
self.remove_tiled_window(&window);
|
self.remove_tiled_window(&window);
|
||||||
self.space.map_element(window.clone(), (0, 0), false);
|
self.space.map_element(window.clone(), (0, 0), false);
|
||||||
self.restore_keyboard_focus();
|
self.restore_keyboard_focus();
|
||||||
|
|||||||
@@ -59,6 +59,16 @@ impl SeatHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
fn focus_changed(&mut self, seat: &Seat<Self>, focused: Option<&WlSurface>) {
|
||||||
|
if let Some(id) = focused.and_then(|surface| {
|
||||||
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
|
window
|
||||||
|
.toplevel()
|
||||||
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
|
||||||
|
.then_some(*id)
|
||||||
|
})
|
||||||
|
}) {
|
||||||
|
self.focus_history.record(id);
|
||||||
|
}
|
||||||
if let Some(inhibitor) = self.active_shortcuts_inhibitor.take()
|
if let Some(inhibitor) = self.active_shortcuts_inhibitor.take()
|
||||||
&& inhibitor.is_active()
|
&& inhibitor.is_active()
|
||||||
{
|
{
|
||||||
@@ -142,7 +152,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
root = parent;
|
root = parent;
|
||||||
}
|
}
|
||||||
let window = self.window_ids.iter().find_map(|(window, id)| {
|
let window = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||||
@@ -251,6 +261,18 @@ impl PrimarySelectionHandler for Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese {
|
||||||
|
fn data_control_state(&self) -> &smithay::wayland::selection::wlr_data_control::DataControlState {
|
||||||
|
&self.wlr_data_control_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese {
|
||||||
|
fn data_control_state(&self) -> &smithay::wayland::selection::ext_data_control::DataControlState {
|
||||||
|
&self.ext_data_control_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl XdgForeignHandler for Ferese {
|
impl XdgForeignHandler for Ferese {
|
||||||
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
|
fn xdg_foreign_state(&mut self) -> &mut XdgForeignState {
|
||||||
&mut self.xdg_foreign_state
|
&mut self.xdg_foreign_state
|
||||||
@@ -285,3 +307,6 @@ smithay::delegate_xdg_decoration!(Ferese);
|
|||||||
smithay::delegate_xdg_foreign!(Ferese);
|
smithay::delegate_xdg_foreign!(Ferese);
|
||||||
smithay::delegate_xdg_shell!(Ferese);
|
smithay::delegate_xdg_shell!(Ferese);
|
||||||
smithay::delegate_xdg_toplevel_icon!(Ferese);
|
smithay::delegate_xdg_toplevel_icon!(Ferese);
|
||||||
|
|
||||||
|
smithay::delegate_data_control!(Ferese);
|
||||||
|
smithay::delegate_ext_data_control!(Ferese);
|
||||||
@@ -186,7 +186,8 @@ impl Ferese {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let window = self
|
let window = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, candidate)| **candidate == id)?
|
.find(|(_, candidate)| **candidate == id)?
|
||||||
.0
|
.0
|
||||||
@@ -283,7 +284,7 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da
|
|||||||
};
|
};
|
||||||
if state.session_lock.active
|
if state.session_lock.active
|
||||||
|| !super::screencopy::capture_allowed()
|
|| !super::screencopy::capture_allowed()
|
||||||
|| !state.window_ids.values().any(|id| *id == snapshot.id)
|
|| !state.windows.ids().values().any(|id| *id == snapshot.id)
|
||||||
{
|
{
|
||||||
resource.failed();
|
resource.failed();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
let Some(parent) = surface.parent() else {
|
let Some(parent) = surface.parent() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let parent = self.window_ids.iter().find_map(|(window, id)| {
|
let parent = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
|
.is_some_and(|toplevel| toplevel.wl_surface() == &parent)
|
||||||
@@ -58,9 +58,12 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
fn toplevel_destroyed(&mut self, surface: ToplevelSurface) {
|
||||||
|
// Hidden workspace windows are managed but unmapped from Space.
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.space
|
.windows
|
||||||
.elements()
|
.ids()
|
||||||
|
.keys()
|
||||||
|
.chain(self.space.elements())
|
||||||
.find(|window| {
|
.find(|window| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
@@ -183,7 +186,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
fn maximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -196,7 +199,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(id) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -213,7 +216,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
surface: ToplevelSurface,
|
surface: ToplevelSurface,
|
||||||
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
|
_output: Option<smithay::reexports::wayland_server::protocol::wl_output::WlOutput>,
|
||||||
) {
|
) {
|
||||||
if let Some(window) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -226,7 +229,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
fn unfullscreen_request(&mut self, surface: ToplevelSurface) {
|
||||||
if let Some(window) = self.window_ids.iter().find_map(|(window, id)| {
|
if let Some(window) = self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
|
||||||
@@ -378,7 +381,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let initial = !state.window_ids.contains_key(window);
|
let initial = !state.windows.ids().contains_key(window);
|
||||||
let requested = initial.then(|| {
|
let requested = initial.then(|| {
|
||||||
toplevel.with_pending_state(|pending| {
|
toplevel.with_pending_state(|pending| {
|
||||||
(
|
(
|
||||||
@@ -389,7 +392,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
});
|
});
|
||||||
if initial {
|
if initial {
|
||||||
let parent = toplevel.parent().and_then(|parent| {
|
let parent = toplevel.parent().and_then(|parent| {
|
||||||
state.window_ids.iter().find_map(|(candidate, id)| {
|
state.windows.ids().iter().find_map(|(candidate, id)| {
|
||||||
candidate
|
candidate
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|surface| surface.wl_surface() == &parent)
|
.is_some_and(|surface| surface.wl_surface() == &parent)
|
||||||
@@ -400,7 +403,7 @@ pub fn apply_initial_window_rules(state: &mut Ferese, window: &Window) {
|
|||||||
}
|
}
|
||||||
state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient);
|
state.apply_initial_window_rules(window, app_id.as_deref(), title.as_deref(), transient);
|
||||||
if let Some((maximized, fullscreen)) = requested
|
if let Some((maximized, fullscreen)) = requested
|
||||||
&& let Some(id) = state.window_ids.get(window).copied()
|
&& let Some(id) = state.windows.ids().get(window).copied()
|
||||||
{
|
{
|
||||||
if maximized {
|
if maximized {
|
||||||
state.set_window_maximized(id, true);
|
state.set_window_maximized(id, true);
|
||||||
@@ -422,7 +425,7 @@ impl Ferese {
|
|||||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
||||||
let geometry = (|| {
|
let geometry = (|| {
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
let id = *self.window_ids.get(window)?;
|
let id = *self.windows.ids().get(window)?;
|
||||||
let output = self
|
let output = self
|
||||||
.space
|
.space
|
||||||
.outputs()
|
.outputs()
|
||||||
|
|||||||
+43
-32
@@ -43,7 +43,7 @@ impl Ferese {
|
|||||||
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let presented = self.presented_window_rect(*id)?;
|
let presented = self.presented_window_rect(*id)?;
|
||||||
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
||||||
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
||||||
@@ -275,6 +275,13 @@ impl Ferese {
|
|||||||
SERIAL_COUNTER.next_serial(),
|
SERIAL_COUNTER.next_serial(),
|
||||||
event.time_msec(),
|
event.time_msec(),
|
||||||
|data, modifiers, keysym| {
|
|data, modifiers, keysym| {
|
||||||
|
if data.focus_cycle.is_some() {
|
||||||
|
if data.session_lock.active || data.input_capture.captures(1) {
|
||||||
|
data.cancel_focus_cycle();
|
||||||
|
} else if !modifiers.alt {
|
||||||
|
data.finish_focus_cycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
if state == KeyState::Released {
|
if state == KeyState::Released {
|
||||||
data.portal_shortcuts.release(keycode, Event::time(&event));
|
data.portal_shortcuts.release(keycode, Event::time(&event));
|
||||||
if data.intercepted_keys.remove(&keycode) {
|
if data.intercepted_keys.remove(&keycode) {
|
||||||
@@ -285,6 +292,13 @@ impl Ferese {
|
|||||||
return FilterResult::Forward;
|
return FilterResult::Forward;
|
||||||
}
|
}
|
||||||
let symbol = keysym.modified_sym().raw();
|
let symbol = keysym.modified_sym().raw();
|
||||||
|
if data.focus_cycle.is_some() && symbol == keysyms::KEY_Escape {
|
||||||
|
if state == KeyState::Pressed {
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
data.cancel_focus_cycle();
|
||||||
|
}
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
if data.input_capture.active() && emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
|
if data.input_capture.active() && emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
|
||||||
if state == KeyState::Pressed {
|
if state == KeyState::Pressed {
|
||||||
data.input_capture.disable_all();
|
data.input_capture.disable_all();
|
||||||
@@ -362,6 +376,18 @@ impl Ferese {
|
|||||||
)
|
)
|
||||||
.then(|| binding.action.clone())
|
.then(|| binding.action.clone())
|
||||||
});
|
});
|
||||||
|
if data.focus_cycle.is_some()
|
||||||
|
&& state == KeyState::Pressed
|
||||||
|
&& !matches!(action, Some(BindingAction::FocusMru(_)))
|
||||||
|
&& !matches!(symbol,
|
||||||
|
keysyms::KEY_Alt_L | keysyms::KEY_Alt_R
|
||||||
|
| keysyms::KEY_Shift_L | keysyms::KEY_Shift_R
|
||||||
|
| keysyms::KEY_Control_L | keysyms::KEY_Control_R
|
||||||
|
| keysyms::KEY_Super_L | keysyms::KEY_Super_R)
|
||||||
|
{
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
let Some(action) = action else {
|
let Some(action) = action else {
|
||||||
if state == KeyState::Pressed
|
if state == KeyState::Pressed
|
||||||
&& data.portal_shortcuts.press(
|
&& data.portal_shortcuts.press(
|
||||||
@@ -379,8 +405,12 @@ impl Ferese {
|
|||||||
|
|
||||||
if state == KeyState::Pressed {
|
if state == KeyState::Pressed {
|
||||||
data.intercepted_keys.insert(keycode);
|
data.intercepted_keys.insert(keycode);
|
||||||
|
if let BindingAction::FocusMru(reverse) = action {
|
||||||
|
data.cycle_focus(reverse, modifiers.alt);
|
||||||
|
} else {
|
||||||
data.execute_binding(action);
|
data.execute_binding(action);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FilterResult::Intercept(())
|
FilterResult::Intercept(())
|
||||||
},
|
},
|
||||||
@@ -511,7 +541,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(window) = self.window_under_visual(position)
|
if let Some(window) = self.window_under_visual(position)
|
||||||
&& let Some(id) = self.window_ids.get(&window).copied()
|
&& let Some(id) = self.windows.ids().get(&window).copied()
|
||||||
{
|
{
|
||||||
self.select_overview_window(id);
|
self.select_overview_window(id);
|
||||||
} else {
|
} else {
|
||||||
@@ -751,39 +781,18 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn relative_workspace_target(&mut self, next: bool) -> Option<ferese_core::WorkspaceId> {
|
pub(crate) fn relative_workspace_target(&self, next: bool) -> Option<ferese_core::WorkspaceId> {
|
||||||
let output = self.output_workspaces.focused_output()?;
|
let output = self.output_workspaces.focused_output()?;
|
||||||
let current = self.output_workspaces.active_workspace(output)?;
|
let current = self.output_workspaces.active_workspace(output)?;
|
||||||
let mut candidates = self.workspaces.ordered();
|
let candidates = self.output_workspaces.ordered_workspaces(&self.workspaces, output);
|
||||||
candidates.retain(|workspace| {
|
let index = candidates.iter().position(|id| *id == current)?;
|
||||||
self.output_workspaces
|
|
||||||
.output_for_workspace(workspace.id)
|
|
||||||
.is_none_or(|owner| owner == output)
|
|
||||||
});
|
|
||||||
let index = candidates.iter().position(|workspace| workspace.id == current)?;
|
|
||||||
let neighbor = if next {
|
let neighbor = if next {
|
||||||
index.checked_add(1)
|
index.checked_add(1)?
|
||||||
} else {
|
} else {
|
||||||
index.checked_sub(1)
|
index.checked_sub(1)?
|
||||||
};
|
};
|
||||||
if let Some(workspace) = neighbor.and_then(|index| candidates.get(index)) {
|
|
||||||
return Some(workspace.id);
|
candidates.get(neighbor).copied()
|
||||||
}
|
|
||||||
if next
|
|
||||||
&& self.workspaces.workspace(current).is_some_and(|workspace| {
|
|
||||||
workspace.layout.window_ids().next().is_some() || !workspace.floating.is_empty()
|
|
||||||
})
|
|
||||||
{
|
|
||||||
let number = self
|
|
||||||
.workspaces
|
|
||||||
.iter()
|
|
||||||
.filter_map(|workspace| workspace.name.parse::<u32>().ok())
|
|
||||||
.max()
|
|
||||||
.unwrap_or(1)
|
|
||||||
.checked_add(1)?;
|
|
||||||
return self.workspaces.ensure_numeric(number).ok();
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
||||||
@@ -806,7 +815,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(window) = self.window_under_visual(position) {
|
if let Some(window) = self.window_under_visual(position) {
|
||||||
let focused = self.window_ids.get(&window).copied();
|
let focused = self.windows.ids().get(&window).copied();
|
||||||
if let Some(focused) = focused {
|
if let Some(focused) = focused {
|
||||||
let result = if raise {
|
let result = if raise {
|
||||||
self.workspaces.focus_window(focused)
|
self.workspaces.focus_window(focused)
|
||||||
@@ -874,7 +883,7 @@ impl Ferese {
|
|||||||
let Some(window) = self.window_under_visual(position) else {
|
let Some(window) = self.window_under_visual(position) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if self.window_ids.get(&window).copied() == self.focused_window {
|
if self.windows.ids().get(&window).copied() == self.focused_window {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -998,6 +1007,8 @@ impl Ferese {
|
|||||||
BindingAction::Close => self.close_focused_window(),
|
BindingAction::Close => self.close_focused_window(),
|
||||||
BindingAction::Exit => self.request_logout_confirmation(),
|
BindingAction::Exit => self.request_logout_confirmation(),
|
||||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||||
|
BindingAction::FocusLastWindow => self.focus_last_window(),
|
||||||
|
BindingAction::FocusMru(reverse) => self.cycle_focus(reverse, false),
|
||||||
BindingAction::Move(direction) => self.move_direction(direction),
|
BindingAction::Move(direction) => self.move_direction(direction),
|
||||||
BindingAction::Resize(direction) => self.resize_direction(direction),
|
BindingAction::Resize(direction) => self.resize_direction(direction),
|
||||||
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None),
|
BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None),
|
||||||
|
|||||||
+20
-9
@@ -647,6 +647,9 @@ impl Ferese {
|
|||||||
.reload_config()
|
.reload_config()
|
||||||
.map_err(|e| CommandError::new("invalid_config", e))?,
|
.map_err(|e| CommandError::new("invalid_config", e))?,
|
||||||
"focus" => self.focus_direction(direction_arg(args)?),
|
"focus" => self.focus_direction(direction_arg(args)?),
|
||||||
|
"focus-last-window" => self.focus_last_window(),
|
||||||
|
"focus-mru-next" => self.cycle_focus(false, false),
|
||||||
|
"focus-mru-previous" => self.cycle_focus(true, false),
|
||||||
"move" => self.move_direction(direction_arg(args)?),
|
"move" => self.move_direction(direction_arg(args)?),
|
||||||
"resize" => self.resize_direction(direction_arg(args)?),
|
"resize" => self.resize_direction(direction_arg(args)?),
|
||||||
"workspace" => self.switch_workspace(workspace_arg(args)?),
|
"workspace" => self.switch_workspace(workspace_arg(args)?),
|
||||||
@@ -824,7 +827,8 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(_, candidate)| **candidate == id)
|
.find(|(_, candidate)| **candidate == id)
|
||||||
.map(|(window, _)| window.clone())
|
.map(|(window, _)| window.clone())
|
||||||
@@ -933,7 +937,8 @@ impl Ferese {
|
|||||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||||
};
|
};
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.keys()
|
.keys()
|
||||||
.filter_map(|window| window.toplevel())
|
.filter_map(|window| window.toplevel())
|
||||||
.any(|toplevel| belongs_to(toplevel.wl_surface()))
|
.any(|toplevel| belongs_to(toplevel.wl_surface()))
|
||||||
@@ -948,7 +953,8 @@ impl Ferese {
|
|||||||
use smithay::wayland::compositor::with_states;
|
use smithay::wayland::compositor::with_states;
|
||||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||||
let mut windows = self
|
let mut windows = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let toplevel = window.toplevel()?;
|
let toplevel = window.toplevel()?;
|
||||||
@@ -981,20 +987,25 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn workspaces_json(&self) -> Value {
|
fn workspaces_json(&self) -> Value {
|
||||||
let active = self.workspaces.active_id();
|
|
||||||
let workspaces = self
|
let workspaces = self
|
||||||
.workspaces
|
.output_workspaces
|
||||||
.ordered()
|
.workspace_views(&self.workspaces)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|workspace| {
|
.map(|view| {
|
||||||
|
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists");
|
||||||
let mode = match workspace.layout.mode() {
|
let mode = match workspace.layout.mode() {
|
||||||
LayoutMode::Scrolling => "scrolling",
|
LayoutMode::Scrolling => "scrolling",
|
||||||
LayoutMode::Tree => "tree",
|
LayoutMode::Tree => "tree",
|
||||||
};
|
};
|
||||||
json!({
|
json!({
|
||||||
"id": workspace.id.0,
|
"id": workspace.id.0,
|
||||||
"name": workspace.name,
|
"name": view.index.to_string(),
|
||||||
"active": workspace.id == active,
|
"index": view.index,
|
||||||
|
"output": view.output.0,
|
||||||
|
"window_count": view.window_count,
|
||||||
|
"visible": view.visible,
|
||||||
|
"focused": view.focused,
|
||||||
|
"active": view.focused,
|
||||||
"layout": mode,
|
"layout": mode,
|
||||||
"focused_window": workspace.last_focused.map(|window| window.0),
|
"focused_window": workspace.last_focused.map(|window| window.0),
|
||||||
"fullscreen_window": workspace.fullscreen.map(|window| window.0),
|
"fullscreen_window": workspace.fullscreen.map(|window| window.0),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ mod daemon;
|
|||||||
mod dimming;
|
mod dimming;
|
||||||
mod effects;
|
mod effects;
|
||||||
mod floating;
|
mod floating;
|
||||||
|
mod frame_scheduler;
|
||||||
mod gestures;
|
mod gestures;
|
||||||
mod grabs;
|
mod grabs;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
@@ -116,6 +117,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
|
state.backdrop_generation = state.backdrop_generation.wrapping_add(1);
|
||||||
}
|
}
|
||||||
// All input/Wayland callbacks have returned, releasing seat locks.
|
// All input/Wayland callbacks have returned, releasing seat locks.
|
||||||
|
if state.focus_cycle.is_some() && (state.session_lock.active || state.input_capture.captures(1)) {
|
||||||
|
state.cancel_focus_cycle();
|
||||||
|
}
|
||||||
// Coalesce cursor changes and redraw here, never inside cursor_image.
|
// Coalesce cursor changes and redraw here, never inside cursor_image.
|
||||||
if state.input_capture.restore_focus {
|
if state.input_capture.restore_focus {
|
||||||
state.restore_input_capture_focus();
|
state.restore_input_capture_focus();
|
||||||
|
|||||||
+268
-102
@@ -89,12 +89,9 @@ type OverviewStateLabel =
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) struct OverviewState {
|
pub(crate) struct OverviewState {
|
||||||
active: bool,
|
motion: OverviewMotion,
|
||||||
selected: Option<WindowId>,
|
selected: Option<WindowId>,
|
||||||
selection_workspace: Option<WorkspaceId>,
|
selection_workspace: Option<WorkspaceId>,
|
||||||
presentations: HashMap<WindowId, AnimatedRect>,
|
|
||||||
opacity: AnimatedValue,
|
|
||||||
exit_transition: Option<(f64, f64)>,
|
|
||||||
strip_offsets: HashMap<OutputId, usize>,
|
strip_offsets: HashMap<OutputId, usize>,
|
||||||
font_family: String,
|
font_family: String,
|
||||||
font_requests: Option<FontRequests>,
|
font_requests: Option<FontRequests>,
|
||||||
@@ -102,15 +99,20 @@ pub(crate) struct OverviewState {
|
|||||||
labels: OverviewStateLabel,
|
labels: OverviewStateLabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct OverviewMotion {
|
||||||
|
active: bool,
|
||||||
|
presentations: HashMap<WindowId, AnimatedRect>,
|
||||||
|
opacity: AnimatedValue,
|
||||||
|
exit_transition: Option<(f64, f64)>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for OverviewState {
|
impl Default for OverviewState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
active: false,
|
motion: OverviewMotion::default(),
|
||||||
selected: None,
|
selected: None,
|
||||||
selection_workspace: None,
|
selection_workspace: None,
|
||||||
presentations: HashMap::new(),
|
|
||||||
opacity: AnimatedValue::new(0.0),
|
|
||||||
exit_transition: None,
|
|
||||||
strip_offsets: HashMap::new(),
|
strip_offsets: HashMap::new(),
|
||||||
font_family: "sans-serif".into(),
|
font_family: "sans-serif".into(),
|
||||||
font_requests: None,
|
font_requests: None,
|
||||||
@@ -123,6 +125,7 @@ impl Default for OverviewState {
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct WorkspaceCard {
|
pub(crate) struct WorkspaceCard {
|
||||||
pub workspace: WorkspaceId,
|
pub workspace: WorkspaceId,
|
||||||
|
pub index: u32,
|
||||||
pub rect: Rect,
|
pub rect: Rect,
|
||||||
pub selected: bool,
|
pub selected: bool,
|
||||||
pub windows: Vec<(WindowId, Rect)>,
|
pub windows: Vec<(WindowId, Rect)>,
|
||||||
@@ -160,6 +163,20 @@ fn centered_row_start(strip: Rect, count: usize, card_width: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OverviewState {
|
impl OverviewState {
|
||||||
|
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||||
|
self.motion.is_animating(spring)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sample(&self, delta: Duration, spring: SpringConfig) -> OverviewMotion {
|
||||||
|
let mut motion = self.motion.clone();
|
||||||
|
|
||||||
|
if !delta.is_zero() {
|
||||||
|
motion.advance(delta, spring, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
motion
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_font_family(&mut self, family: String) {
|
pub(crate) fn set_font_family(&mut self, family: String) {
|
||||||
if self.font_family != family {
|
if self.font_family != family {
|
||||||
self.font_family = family;
|
self.font_family = family;
|
||||||
@@ -241,19 +258,20 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_active(&self) -> bool {
|
pub(crate) fn is_active(&self) -> bool {
|
||||||
self.active
|
self.motion.active
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn is_presenting(&self) -> bool {
|
pub(crate) fn is_presenting(&self) -> bool {
|
||||||
self.active || !self.presentations.is_empty() || self.opacity.current > 0.001
|
self.motion.is_presenting()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn opacity(&self) -> f32 {
|
pub(crate) fn opacity(&self) -> f32 {
|
||||||
self.opacity.current.clamp(0.0, 1.0) as f32
|
self.motion.opacity()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) {
|
fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option<WindowId>) {
|
||||||
let visible = |id: &WindowId| windows.contains(id) && self.presentations.contains_key(id);
|
let visible = |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id);
|
||||||
self.selected = self
|
self.selected = self
|
||||||
.selected
|
.selected
|
||||||
.filter(visible)
|
.filter(visible)
|
||||||
@@ -265,16 +283,30 @@ impl OverviewState {
|
|||||||
self.selected
|
self.selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_window_preview(&self, id: WindowId) -> bool {
|
||||||
|
self.motion.presentations.contains_key(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_window(&mut self, id: WindowId) -> bool {
|
||||||
|
if !self.motion.presentations.contains_key(&id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.selection_workspace = None;
|
||||||
|
self.selected = Some(id);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn select_direction(&mut self, direction: Direction) -> bool {
|
pub(crate) fn select_direction(&mut self, direction: Direction) -> bool {
|
||||||
let Some(current) = self
|
let Some(current) = self
|
||||||
.selected
|
.selected
|
||||||
.and_then(|id| self.presentations.get(&id))
|
.and_then(|id| self.motion.presentations.get(&id))
|
||||||
.map(|presentation| presentation.current)
|
.map(|presentation| presentation.current)
|
||||||
else {
|
else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let current_center = rect_center(current);
|
let current_center = rect_center(current);
|
||||||
let next = self
|
let next = self
|
||||||
|
.motion
|
||||||
.presentations
|
.presentations
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(id, _)| Some(**id) != self.selected)
|
.filter(|(id, _)| Some(**id) != self.selected)
|
||||||
@@ -294,17 +326,7 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
||||||
let preview = self.presentations.get(&id).map_or(normal, |p| p.current);
|
self.motion.presented_rect(id, normal)
|
||||||
if let Some((elapsed, _)) = self.exit_transition {
|
|
||||||
let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3);
|
|
||||||
return Rect::new(
|
|
||||||
preview.x + (normal.x - preview.x) * t,
|
|
||||||
preview.y + (normal.y - preview.y) * t,
|
|
||||||
preview.width + (normal.width - preview.width) * t,
|
|
||||||
preview.height + (normal.height - preview.height) * t,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
preview
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enter(
|
fn enter(
|
||||||
@@ -314,23 +336,23 @@ impl OverviewState {
|
|||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
) {
|
) {
|
||||||
// Reverse a dismissal from the currently visible geometry, not the old grid.
|
// Reverse a dismissal from the currently visible geometry, not the old grid.
|
||||||
if self.exit_transition.is_some() {
|
if self.motion.exit_transition.is_some() {
|
||||||
let visible: Vec<_> = targets
|
let visible: Vec<_> = targets
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal)))
|
.map(|(id, (normal, _))| (*id, self.presented_rect(*id, *normal)))
|
||||||
.collect();
|
.collect();
|
||||||
for (id, rect) in visible {
|
for (id, rect) in visible {
|
||||||
if let Some(presentation) = self.presentations.get_mut(&id) {
|
if let Some(presentation) = self.motion.presentations.get_mut(&id) {
|
||||||
*presentation = AnimatedRect::new(rect);
|
*presentation = AnimatedRect::new(rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.exit_transition = None;
|
self.motion.exit_transition = None;
|
||||||
self.selection_workspace = None;
|
self.selection_workspace = None;
|
||||||
self.active = true;
|
self.motion.active = true;
|
||||||
self.opacity.retarget_preserving_motion(1.0);
|
self.motion.opacity.retarget_preserving_motion(1.0);
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.opacity.snap();
|
self.motion.opacity.snap();
|
||||||
}
|
}
|
||||||
self.strip_offsets.clear();
|
self.strip_offsets.clear();
|
||||||
self.selected = selected
|
self.selected = selected
|
||||||
@@ -340,10 +362,11 @@ impl OverviewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) {
|
fn retarget(&mut self, targets: HashMap<WindowId, (Rect, Rect)>, animations_enabled: bool) {
|
||||||
self.presentations.retain(|id, _| targets.contains_key(id));
|
self.motion.presentations.retain(|id, _| targets.contains_key(id));
|
||||||
|
|
||||||
for (id, (normal, target)) in targets {
|
for (id, (normal, target)) in targets {
|
||||||
let presentation = self
|
let presentation = self
|
||||||
|
.motion
|
||||||
.presentations
|
.presentations
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| AnimatedRect::new(normal));
|
.or_insert_with(|| AnimatedRect::new(normal));
|
||||||
@@ -353,20 +376,23 @@ impl OverviewState {
|
|||||||
presentation.snap();
|
presentation.snap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) {
|
if self
|
||||||
self.selected = self.presentations.keys().copied().min_by_key(|id| id.0);
|
.selected
|
||||||
|
.is_none_or(|id| !self.motion.presentations.contains_key(&id))
|
||||||
|
{
|
||||||
|
self.selected = self.motion.presentations.keys().copied().min_by_key(|id| id.0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
fn exit(&mut self, normal: HashMap<WindowId, Rect>, animations_enabled: bool) {
|
||||||
self.active = false;
|
self.motion.active = false;
|
||||||
self.exit_transition = animations_enabled.then_some((0.0, self.opacity.current));
|
self.motion.exit_transition = animations_enabled.then_some((0.0, self.motion.opacity.current));
|
||||||
self.opacity.retarget_preserving_motion(0.0);
|
self.motion.opacity.retarget_preserving_motion(0.0);
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.opacity.snap();
|
self.motion.opacity.snap();
|
||||||
}
|
}
|
||||||
self.selected = None;
|
self.selected = None;
|
||||||
self.presentations.retain(|id, presentation| {
|
self.motion.presentations.retain(|id, presentation| {
|
||||||
let Some(target) = normal.get(id).copied() else {
|
let Some(target) = normal.get(id).copied() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
@@ -379,58 +405,12 @@ impl OverviewState {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.presentations.clear();
|
self.motion.presentations.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||||
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
self.motion.advance(delta, spring, animations_enabled)
|
||||||
let elapsed = if animations_enabled {
|
|
||||||
elapsed + delta.as_secs_f64()
|
|
||||||
} else {
|
|
||||||
EXIT_DURATION
|
|
||||||
};
|
|
||||||
let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0);
|
|
||||||
self.opacity.current = initial_opacity * remaining.powi(3);
|
|
||||||
if elapsed >= EXIT_DURATION {
|
|
||||||
self.exit_transition = None;
|
|
||||||
self.presentations.clear();
|
|
||||||
self.opacity.snap();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
self.exit_transition = Some((elapsed, initial_opacity));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
let mut active_animation = false;
|
|
||||||
|
|
||||||
for presentation in self.presentations.values_mut() {
|
|
||||||
if animations_enabled {
|
|
||||||
active_animation |= presentation.advance(delta, spring);
|
|
||||||
} else {
|
|
||||||
presentation.snap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.active && !active_animation {
|
|
||||||
self.presentations.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if animations_enabled {
|
|
||||||
// Geometry's 0.1-pixel tolerance is far too coarse for opacity:
|
|
||||||
// it would abruptly drop the last ten percent of the fade.
|
|
||||||
active_animation |= self.opacity.advance(
|
|
||||||
delta,
|
|
||||||
SpringConfig {
|
|
||||||
position_tolerance: 0.001,
|
|
||||||
velocity_tolerance: 0.005,
|
|
||||||
..spring
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
self.opacity.snap();
|
|
||||||
}
|
|
||||||
|
|
||||||
active_animation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,7 +421,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
let selected = self
|
let selected = self
|
||||||
.window_under_visual(point)
|
.window_under_visual(point)
|
||||||
.and_then(|w| self.window_ids.get(&w).copied());
|
.and_then(|w| self.windows.ids().get(&w).copied());
|
||||||
|
|
||||||
if selected.is_some() && self.overview.selected != selected {
|
if selected.is_some() && self.overview.selected != selected {
|
||||||
self.overview.selection_workspace = None;
|
self.overview.selection_workspace = None;
|
||||||
@@ -480,15 +460,17 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn overview_workspace_label(
|
pub(crate) fn overview_workspace_label(
|
||||||
&mut self,
|
&mut self,
|
||||||
workspace: WorkspaceId,
|
index: u32,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||||
let name = self.workspaces.workspace(workspace)?.name.clone();
|
|
||||||
self.overview
|
self.overview
|
||||||
.label(&name, scale, self.theme_settings.text_primary_color.0)
|
.label(&index.to_string(), scale, self.theme_settings.text_primary_color.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
pub(crate) fn set_overview_active(&mut self, active: bool) {
|
||||||
|
if !active {
|
||||||
|
self.focus_cycle = None;
|
||||||
|
}
|
||||||
if self.overview.is_active() == active {
|
if self.overview.is_active() == active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -528,7 +510,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn select_overview_window(&mut self, id: WindowId) -> bool {
|
||||||
if !self.overview.is_active() || !self.overview.presentations.contains_key(&id) {
|
if !self.overview.is_active() || !self.overview.motion.presentations.contains_key(&id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -547,6 +529,13 @@ impl Ferese {
|
|||||||
|
|
||||||
let targets = self.overview_targets();
|
let targets = self.overview_targets();
|
||||||
self.overview.retarget(targets, self.animations_enabled());
|
self.overview.retarget(targets, self.animations_enabled());
|
||||||
|
if let Some(cycle) = self.focus_cycle.as_mut() {
|
||||||
|
let available = self.overview.motion.presentations.keys().copied().collect::<Vec<_>>();
|
||||||
|
if let Some(id) = cycle.reconcile(&available) {
|
||||||
|
self.overview.select_window(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.overview.selection_workspace.is_some() {
|
if self.overview.selection_workspace.is_some() {
|
||||||
let id = self.workspaces.active_id();
|
let id = self.workspaces.active_id();
|
||||||
self.overview.selection_workspace = Some(id);
|
self.overview.selection_workspace = Some(id);
|
||||||
@@ -566,7 +555,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option<Rect> {
|
pub(crate) fn presented_window_rect(&self, id: WindowId) -> Option<Rect> {
|
||||||
let normal = self.window_geometry.get(&id)?.visual.current;
|
let normal = self.windows.geometry(&id)?.visual.current;
|
||||||
let mut presented = self.overview.presented_rect(id, normal);
|
let mut presented = self.overview.presented_rect(id, normal);
|
||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
let (x, y) = self.workspace_slide_offset(id);
|
let (x, y) = self.workspace_slide_offset(id);
|
||||||
@@ -577,7 +566,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> {
|
||||||
let geometry = self.window_geometry.get(&id)?;
|
let geometry = self.windows.geometry(&id)?;
|
||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
let presented = self.presented_window_rect(id)?;
|
let presented = self.presented_window_rect(id)?;
|
||||||
return Some((x - presented.x, y - presented.y));
|
return Some((x - presented.x, y - presented.y));
|
||||||
@@ -600,6 +589,14 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec<WorkspaceCard> {
|
pub(crate) fn overview_workspace_cards(&self, output: &Output) -> Vec<WorkspaceCard> {
|
||||||
|
self.overview_workspace_cards_for_frame(output, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overview_workspace_cards_for_frame(
|
||||||
|
&self,
|
||||||
|
output: &Output,
|
||||||
|
frame: Option<&crate::state::FrameScene>,
|
||||||
|
) -> Vec<WorkspaceCard> {
|
||||||
let Some(bounds) = self.output_bounds_for(output) else {
|
let Some(bounds) = self.output_bounds_for(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -609,7 +606,12 @@ impl Ferese {
|
|||||||
let selected = self.output_workspaces.active_workspace(output_id);
|
let selected = self.output_workspaces.active_workspace(output_id);
|
||||||
let strip = workspace_strip(bounds);
|
let strip = workspace_strip(bounds);
|
||||||
let capacity = strip_capacity(strip);
|
let capacity = strip_capacity(strip);
|
||||||
let workspaces = self.workspaces.ordered();
|
let workspaces = self
|
||||||
|
.output_workspaces
|
||||||
|
.workspace_views(&self.workspaces)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|view| view.output == output_id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
let selected_index = workspaces
|
let selected_index = workspaces
|
||||||
.iter()
|
.iter()
|
||||||
.position(|workspace| Some(workspace.id) == selected)
|
.position(|workspace| Some(workspace.id) == selected)
|
||||||
@@ -632,7 +634,9 @@ impl Ferese {
|
|||||||
.skip(offset)
|
.skip(offset)
|
||||||
.take(capacity)
|
.take(capacity)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, workspace)| {
|
.map(|(index, view)| {
|
||||||
|
let workspace = self.workspaces.workspace(view.id).expect("workspace view exists");
|
||||||
|
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
|
start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP),
|
||||||
strip.y + 12.0,
|
strip.y + 12.0,
|
||||||
@@ -645,7 +649,11 @@ impl Ferese {
|
|||||||
.chain(workspace.floating.iter().copied())
|
.chain(workspace.floating.iter().copied())
|
||||||
.take(4)
|
.take(4)
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let source = self.window_geometry.get(&id)?.visual.current;
|
let source = frame
|
||||||
|
.and_then(|frame| frame.windows.get(&id).map(|window| &window.geometry))
|
||||||
|
.or_else(|| self.windows.geometry(&id))?
|
||||||
|
.visual
|
||||||
|
.current;
|
||||||
Some((id, source))
|
Some((id, source))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
@@ -658,6 +666,7 @@ impl Ferese {
|
|||||||
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0).into_iter().collect();
|
let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0).into_iter().collect();
|
||||||
WorkspaceCard {
|
WorkspaceCard {
|
||||||
workspace: workspace.id,
|
workspace: workspace.id,
|
||||||
|
index: view.index,
|
||||||
rect,
|
rect,
|
||||||
selected: Some(workspace.id) == selected,
|
selected: Some(workspace.id) == selected,
|
||||||
windows,
|
windows,
|
||||||
@@ -720,10 +729,10 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let capacity = strip_capacity(workspace_strip(bounds));
|
let capacity = strip_capacity(workspace_strip(bounds));
|
||||||
let cards = self.overview_workspace_cards(&output);
|
let cards = self.overview_workspace_cards(&output);
|
||||||
let ordered = self.workspaces.ordered();
|
let ordered = self.output_workspaces.ordered_workspaces(&self.workspaces, output_id);
|
||||||
let current = cards
|
let current = cards
|
||||||
.first()
|
.first()
|
||||||
.and_then(|card| ordered.iter().position(|workspace| workspace.id == card.workspace))
|
.and_then(|card| ordered.iter().position(|id| *id == card.workspace))
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let next = if delta > 0.0 {
|
let next = if delta > 0.0 {
|
||||||
current.saturating_add(1)
|
current.saturating_add(1)
|
||||||
@@ -752,6 +761,22 @@ impl Ferese {
|
|||||||
let Some(bounds) = self.output_bounds_for(output) else {
|
let Some(bounds) = self.output_bounds_for(output) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if let Some(cycle) = &self.focus_cycle {
|
||||||
|
let windows = cycle
|
||||||
|
.windows()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|id| {
|
||||||
|
if self.focus_preview_output(*id) != Some(output_id) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((*id, self.windows.geometry(id)?.visual.current))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
|
targets.insert(id, (self.windows.geometry(&id).unwrap().visual.current, target));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let Some(workspace) = self.workspaces.workspace(workspace) else {
|
let Some(workspace) = self.workspaces.workspace(workspace) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -760,13 +785,13 @@ impl Ferese {
|
|||||||
.window_ids()
|
.window_ids()
|
||||||
.chain(workspace.floating.iter().copied())
|
.chain(workspace.floating.iter().copied())
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let normal = self.window_geometry.get(&id)?.visual.current;
|
let normal = self.windows.geometry(&id)?.visual.current;
|
||||||
Some((id, normal))
|
Some((id, normal))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
let normal = self.window_geometry[&id].visual.current;
|
let normal = self.windows.geometry(&id).unwrap().visual.current;
|
||||||
targets.insert(id, (normal, target));
|
targets.insert(id, (normal, target));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -775,8 +800,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn normal_window_rects(&self) -> HashMap<WindowId, Rect> {
|
fn normal_window_rects(&self) -> HashMap<WindowId, Rect> {
|
||||||
self.window_geometry
|
self.windows
|
||||||
.iter()
|
.geometries()
|
||||||
.map(|(id, geometry)| (*id, geometry.visual.current))
|
.map(|(id, geometry)| (*id, geometry.visual.current))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -973,6 +998,36 @@ mod tests {
|
|||||||
assert!(!overview.is_presenting());
|
assert!(!overview.is_presenting());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sampling_overview_exit_does_not_finish_the_live_transition() {
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
let id = WindowId(1);
|
||||||
|
let normal = Rect::new(0., 0., 800., 600.);
|
||||||
|
let thumbnail = Rect::new(100., 100., 200., 150.);
|
||||||
|
overview.enter(HashMap::from([(id, (normal, thumbnail))]), Some(id), false);
|
||||||
|
overview.exit(HashMap::from([(id, normal)]), true);
|
||||||
|
overview.advance(Duration::from_millis(170), SpringConfig::default(), true);
|
||||||
|
let before = overview.presented_rect(id, normal);
|
||||||
|
let opacity = overview.opacity();
|
||||||
|
|
||||||
|
let finished = overview.sample(Duration::from_millis(16), SpringConfig::default());
|
||||||
|
let earlier = overview.sample(Duration::from_millis(4), SpringConfig::default());
|
||||||
|
|
||||||
|
assert!(!finished.is_presenting());
|
||||||
|
assert_eq!(finished.presented_rect(id, normal), normal);
|
||||||
|
assert!(earlier.is_presenting());
|
||||||
|
assert!(earlier.opacity() < opacity);
|
||||||
|
assert!(overview.is_presenting());
|
||||||
|
assert_eq!(overview.presented_rect(id, normal), before);
|
||||||
|
assert_eq!(overview.opacity(), opacity);
|
||||||
|
assert_eq!(
|
||||||
|
overview
|
||||||
|
.sample(Duration::ZERO, SpringConfig::default())
|
||||||
|
.presented_rect(id, normal),
|
||||||
|
before
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reversing_overview_preserves_current_opacity() {
|
fn reversing_overview_preserves_current_opacity() {
|
||||||
let mut overview = OverviewState::default();
|
let mut overview = OverviewState::default();
|
||||||
@@ -1053,6 +1108,25 @@ mod tests {
|
|||||||
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
|
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mru_selection_can_cross_workspace_selection_and_rejects_missing_previews() {
|
||||||
|
let first = WindowId(1);
|
||||||
|
let second = WindowId(2);
|
||||||
|
let normal = Rect::new(0.0, 0.0, 800.0, 600.0);
|
||||||
|
let mut overview = OverviewState::default();
|
||||||
|
overview.enter(
|
||||||
|
HashMap::from([(first, (normal, normal)), (second, (normal, normal))]),
|
||||||
|
Some(first),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
overview.selection_workspace = Some(WorkspaceId(1));
|
||||||
|
assert!(overview.select_window(second));
|
||||||
|
assert_eq!(overview.selected(), Some(second));
|
||||||
|
assert_eq!(overview.selection_workspace, None);
|
||||||
|
assert!(!overview.select_window(WindowId(99)));
|
||||||
|
assert_eq!(overview.selected(), Some(second));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn exiting_overview_tracks_a_moving_layout_and_finishes_in_180ms() {
|
fn exiting_overview_tracks_a_moving_layout_and_finishes_in_180ms() {
|
||||||
let id = WindowId(1);
|
let id = WindowId(1);
|
||||||
@@ -1143,3 +1217,95 @@ mod tests {
|
|||||||
assert!(!overview.select_direction(Direction::Right));
|
assert!(!overview.select_direction(Direction::Right));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for OverviewMotion {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
presentations: HashMap::new(),
|
||||||
|
opacity: AnimatedValue::new(0.0),
|
||||||
|
exit_transition: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OverviewMotion {
|
||||||
|
pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool {
|
||||||
|
self.exit_transition.is_some()
|
||||||
|
|| (self.opacity.current - self.opacity.target).abs() > 0.001
|
||||||
|
|| self.opacity.velocity.abs() > 0.005
|
||||||
|
|| self.presentations.values().any(|rect| !rect.is_settled(spring))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_presenting(&self) -> bool {
|
||||||
|
self.active || !self.presentations.is_empty() || self.opacity.current > 0.001
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn opacity(&self) -> f32 {
|
||||||
|
self.opacity.current.clamp(0.0, 1.0) as f32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn presented_rect(&self, id: WindowId, normal: Rect) -> Rect {
|
||||||
|
let preview = self.presentations.get(&id).map_or(normal, |p| p.current);
|
||||||
|
if let Some((elapsed, _)) = self.exit_transition {
|
||||||
|
let t = 1.0 - (1.0 - (elapsed / EXIT_DURATION).clamp(0.0, 1.0)).powi(3);
|
||||||
|
return Rect::new(
|
||||||
|
preview.x + (normal.x - preview.x) * t,
|
||||||
|
preview.y + (normal.y - preview.y) * t,
|
||||||
|
preview.width + (normal.width - preview.width) * t,
|
||||||
|
preview.height + (normal.height - preview.height) * t,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
preview
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool {
|
||||||
|
if let Some((elapsed, initial_opacity)) = self.exit_transition {
|
||||||
|
let elapsed = if animations_enabled {
|
||||||
|
elapsed + delta.as_secs_f64()
|
||||||
|
} else {
|
||||||
|
EXIT_DURATION
|
||||||
|
};
|
||||||
|
let remaining = (1.0 - elapsed / EXIT_DURATION).clamp(0.0, 1.0);
|
||||||
|
self.opacity.current = initial_opacity * remaining.powi(3);
|
||||||
|
if elapsed >= EXIT_DURATION {
|
||||||
|
self.exit_transition = None;
|
||||||
|
self.presentations.clear();
|
||||||
|
self.opacity.snap();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.exit_transition = Some((elapsed, initial_opacity));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let mut active_animation = false;
|
||||||
|
|
||||||
|
for presentation in self.presentations.values_mut() {
|
||||||
|
if animations_enabled {
|
||||||
|
active_animation |= presentation.advance(delta, spring);
|
||||||
|
} else {
|
||||||
|
presentation.snap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.active && !active_animation {
|
||||||
|
self.presentations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if animations_enabled {
|
||||||
|
// Geometry's 0.1-pixel tolerance is far too coarse for opacity:
|
||||||
|
// it would abruptly drop the last ten percent of the fade.
|
||||||
|
active_animation |= self.opacity.advance(
|
||||||
|
delta,
|
||||||
|
SpringConfig {
|
||||||
|
position_tolerance: 0.001,
|
||||||
|
velocity_tolerance: 0.005,
|
||||||
|
..spring
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
self.opacity.snap();
|
||||||
|
}
|
||||||
|
|
||||||
|
active_animation
|
||||||
|
}
|
||||||
|
}
|
||||||
+101
-32
@@ -36,7 +36,8 @@ pub(crate) fn take_output_feedback(
|
|||||||
|
|
||||||
for window in state.space.elements().filter(|window| {
|
for window in state.space.elements().filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
}) {
|
}) {
|
||||||
@@ -59,21 +60,6 @@ fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementSta
|
|||||||
.then(|| output.clone())
|
.then(|| output.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub(crate) struct CallbackClock {
|
|
||||||
last_sent: Option<Instant>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CallbackClock {
|
|
||||||
pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant {
|
|
||||||
self.last_sent.map_or(now, |last| (last + interval).max(now))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sent(&mut self, now: Instant) {
|
|
||||||
self.last_sent = Some(now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
|
pub(crate) fn frame_delta(last: &mut Instant, now: Instant) -> Duration {
|
||||||
let delta = now.saturating_duration_since(*last);
|
let delta = now.saturating_duration_since(*last);
|
||||||
*last = (*last).max(now);
|
*last = (*last).max(now);
|
||||||
@@ -96,6 +82,41 @@ pub(crate) fn physical_rect(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single physical outline shared by content and all of its decorations.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub(crate) struct RoundedRect {
|
||||||
|
pub rect: Rectangle<i32, Physical>,
|
||||||
|
pub radius: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoundedRect {
|
||||||
|
pub(crate) fn new(rect: ferese_layout::Rect, origin: Point<i32, Logical>, scale: f64, radius: f64) -> Self {
|
||||||
|
let rect = physical_rect(rect, origin, scale);
|
||||||
|
Self {
|
||||||
|
radius: clamp_radius(radius * scale, rect.size),
|
||||||
|
rect,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_logical(rect: Rectangle<i32, Logical>, scale: f64, radius: f64) -> Self {
|
||||||
|
Self::new(
|
||||||
|
ferese_layout::Rect::new(
|
||||||
|
rect.loc.x as f64,
|
||||||
|
rect.loc.y as f64,
|
||||||
|
rect.size.w as f64,
|
||||||
|
rect.size.h as f64,
|
||||||
|
),
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
radius,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clamp_radius(requested: f64, size: smithay::utils::Size<i32, Physical>) -> f32 {
|
||||||
|
requested.min(f64::from(size.w.min(size.h)) / 2.0).max(0.0) as f32
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn handoff_alpha(elapsed: Duration) -> f32 {
|
pub(crate) fn handoff_alpha(elapsed: Duration) -> f32 {
|
||||||
let progress = (elapsed.as_secs_f64() / HANDOFF.as_secs_f64()).clamp(0.0, 1.0);
|
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
|
(1.0 - progress * progress * (3.0 - 2.0 * progress)) as f32
|
||||||
@@ -259,6 +280,51 @@ impl RenderElement<GlesRenderer> for NativeTextureElement {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn rounded_outline_clamps_after_physical_snapping() {
|
||||||
|
// Logical clamping would give 1.125; the snapped 3px rect allows radius 1.5.
|
||||||
|
let outline = RoundedRect::new(ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0), (0, 0).into(), 1.5, 20.0);
|
||||||
|
assert_eq!(outline.rect, Rectangle::new((0, 0).into(), (3, 6).into()));
|
||||||
|
assert_eq!(outline.radius, 1.5);
|
||||||
|
for scale in [1.0, 1.25, 1.5, 2.0] {
|
||||||
|
let rect = ferese_layout::Rect::new(-20.4, -10.2, 8.8, 6.4);
|
||||||
|
let outline = RoundedRect::new(rect, (-10, -5).into(), scale, 100.0);
|
||||||
|
assert_eq!(outline.rect, physical_rect(rect, (-10, -5).into(), scale));
|
||||||
|
assert_eq!(
|
||||||
|
outline.radius,
|
||||||
|
outline.rect.size.w.min(outline.rect.size.h) as f32 / 2.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn physical_radius_preserves_fractions_and_handles_zero() {
|
||||||
|
let size = (11, 7).into();
|
||||||
|
assert_eq!(clamp_radius(100.0, size), 3.5);
|
||||||
|
assert_eq!(clamp_radius(2.25, size), 2.25);
|
||||||
|
assert_eq!(clamp_radius(0.0, size), 0.0);
|
||||||
|
assert_eq!(clamp_radius(-1.0, size), 0.0);
|
||||||
|
assert_eq!(clamp_radius(2.0, (0, 10).into()), 0.0);
|
||||||
|
let outline = RoundedRect::new(ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0), (0, 0).into(), 1.5, 1.5);
|
||||||
|
assert_eq!(outline.radius, 2.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn material_and_window_outline_agree_for_the_same_rect() {
|
||||||
|
let logical = Rectangle::new((-3, 7).into(), (11, 9).into());
|
||||||
|
for scale in [1.0, 1.25, 1.5, 2.0] {
|
||||||
|
assert_eq!(
|
||||||
|
RoundedRect::from_logical(logical, scale, 100.0),
|
||||||
|
RoundedRect::new(
|
||||||
|
ferese_layout::Rect::new(-3.0, 7.0, 11.0, 9.0),
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
100.0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -362,37 +428,40 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_damage_requests_are_paced_without_buffer_submission() {
|
fn no_damage_requests_are_paced_without_buffer_submission() {
|
||||||
let start = Instant::now();
|
let start = Duration::ZERO;
|
||||||
let refresh = Duration::from_millis(16);
|
let refresh = Duration::from_millis(16);
|
||||||
let mut clock = CallbackClock::default();
|
let mut clock = crate::frame_scheduler::FrameScheduler::new(refresh);
|
||||||
assert_eq!(clock.deadline(start, refresh), start);
|
assert_eq!(clock.callback_deadline(start, start).unwrap(), start);
|
||||||
clock.sent(start);
|
clock.callback_sent(start);
|
||||||
for millisecond in 1..16 {
|
for millisecond in 1..16 {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
clock.deadline(start + Duration::from_millis(millisecond), refresh),
|
clock
|
||||||
|
.callback_deadline(start + Duration::from_millis(millisecond), start)
|
||||||
|
.unwrap(),
|
||||||
start + refresh
|
start + refresh
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let late = start + Duration::from_millis(50);
|
let late = start + Duration::from_millis(50);
|
||||||
assert_eq!(clock.deadline(late, refresh), late);
|
assert_eq!(clock.callback_deadline(late, late).unwrap(), late);
|
||||||
clock.sent(late);
|
clock.callback_sent(late);
|
||||||
assert_eq!(clock.deadline(late, refresh), late + refresh);
|
assert_eq!(clock.callback_deadline(late, late).unwrap(), late + refresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn independent_output_callback_clocks_do_not_sum_their_rates() {
|
fn independent_output_callback_clocks_do_not_sum_their_rates() {
|
||||||
let start = Instant::now();
|
let start = Duration::ZERO;
|
||||||
let mut slow = CallbackClock::default();
|
let mut slow = crate::frame_scheduler::FrameScheduler::new(Duration::from_millis(16));
|
||||||
let mut fast = CallbackClock::default();
|
let mut fast = crate::frame_scheduler::FrameScheduler::new(Duration::from_millis(8));
|
||||||
slow.sent(start);
|
slow.callback_sent(start);
|
||||||
fast.sent(start);
|
fast.callback_sent(start);
|
||||||
fast.sent(start + Duration::from_millis(8));
|
fast.callback_sent(start + Duration::from_millis(8));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
slow.deadline(start + Duration::from_millis(8), Duration::from_millis(16)),
|
slow.callback_deadline(start + Duration::from_millis(8), start).unwrap(),
|
||||||
start + Duration::from_millis(16)
|
start + Duration::from_millis(16)
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fast.deadline(start + Duration::from_millis(8), Duration::from_millis(8)),
|
fast.callback_deadline(start + Duration::from_millis(8), start).unwrap(),
|
||||||
start + Duration::from_millis(16)
|
start + Duration::from_millis(16)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<RoundedClipPrograms> {
|
pub(super) fn rounded_clip_program(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<RoundedClipPrograms> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.rounded_clip_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.rounded.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,22 +36,35 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
|
|||||||
UniformName::new("opacity", UniformType::_1f),
|
UniformName::new("opacity", UniformType::_1f),
|
||||||
UniformName::new("shadow_color", UniformType::_4f),
|
UniformName::new("shadow_color", UniformType::_4f),
|
||||||
];
|
];
|
||||||
let texture = renderer.compile_custom_texture_shader(ROUNDED_TEXTURE_SHADER, &texture_uniforms);
|
let texture = renderer.compile_custom_texture_shader(corner_shader(ROUNDED_TEXTURE_SHADER), &texture_uniforms);
|
||||||
let border = renderer.compile_custom_pixel_shader(ROUNDED_BORDER_SHADER, &border_uniforms);
|
let border = renderer.compile_custom_pixel_shader(corner_shader(ROUNDED_BORDER_SHADER), &border_uniforms);
|
||||||
let shadow = renderer.compile_custom_pixel_shader(WINDOW_SHADOW_SHADER, &shadow_uniforms);
|
let shadow = renderer.compile_custom_pixel_shader(corner_shader(WINDOW_SHADOW_SHADER), &shadow_uniforms);
|
||||||
let compiled = texture.and_then(|texture| border.and_then(|border| shadow.map(|shadow| (texture, border, shadow))));
|
let solid = renderer.compile_custom_pixel_shader(
|
||||||
|
corner_shader(ROUNDED_SOLID_SHADER),
|
||||||
|
&[
|
||||||
|
UniformName::new("clip_rect", UniformType::_4f),
|
||||||
|
UniformName::new("radius", UniformType::_1f),
|
||||||
|
UniformName::new("color", UniformType::_4f),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
let compiled = texture.and_then(|texture| {
|
||||||
|
border.and_then(|border| shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid))))
|
||||||
|
});
|
||||||
match compiled {
|
match compiled {
|
||||||
Ok((texture, border, shadow)) => {
|
Ok((texture, border, shadow, solid)) => {
|
||||||
let programs = RoundedClipPrograms {
|
let programs = RoundedClipPrograms {
|
||||||
texture,
|
texture,
|
||||||
|
solid,
|
||||||
border,
|
border,
|
||||||
shadow,
|
shadow,
|
||||||
};
|
};
|
||||||
state.rounded_clip_programs.insert(context, programs.clone());
|
resources.contexts.entry(context).or_default().rounded = Some(programs.clone());
|
||||||
Some(programs)
|
Some(programs)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::error!(%error, "failed to compile rounded-window shader");
|
if !std::mem::replace(&mut resources.contexts.entry(context).or_default().rounded_warned, true) {
|
||||||
|
tracing::warn!(%error, "rounded-window shader unavailable; falling back to unrounded rendering");
|
||||||
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,13 +72,13 @@ pub(super) fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRender
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_border_element(
|
pub(super) fn window_border_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
|
theme: &crate::config::ThemeSettings,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
physical: Rectangle<i32, Physical>,
|
corners: RoundedRect,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
|
||||||
requested_width: f64,
|
requested_width: f64,
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
gradient: Option<crate::config::BorderGradient>,
|
gradient: Option<crate::config::BorderGradient>,
|
||||||
@@ -68,7 +88,7 @@ pub(super) fn window_border_element(
|
|||||||
programs: &RoundedClipPrograms,
|
programs: &RoundedClipPrograms,
|
||||||
) -> Option<PhysicalShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let width = scaled_effect_value(requested_width, geometry, scale);
|
let width = clamp_radius(requested_width * scale, corners.rect.size);
|
||||||
if width == 0.0 {
|
if width == 0.0 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -77,26 +97,27 @@ pub(super) fn window_border_element(
|
|||||||
Some(gradient) => (
|
Some(gradient) => (
|
||||||
gradient.from.0,
|
gradient.from.0,
|
||||||
gradient.to.0,
|
gradient.to.0,
|
||||||
border_gradient_line(physical, mode.size, output.current_transform().invert(), gradient.angle),
|
border_gradient_line(
|
||||||
|
corners.rect,
|
||||||
|
mode.size,
|
||||||
|
output.current_transform().invert(),
|
||||||
|
gradient.angle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
None => (color, color, [0.0; 4]),
|
None => (color, color, [0.0; 4]),
|
||||||
};
|
};
|
||||||
let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient {
|
let (focus_from, focus_to, focus_gradient_line) = match theme.focus_ring_gradient {
|
||||||
Some(g) => (
|
Some(g) => (
|
||||||
g.from.0,
|
g.from.0,
|
||||||
g.to.0,
|
g.to.0,
|
||||||
border_gradient_line(physical, mode.size, output.current_transform().invert(), g.angle),
|
border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle),
|
||||||
),
|
|
||||||
None => (
|
|
||||||
state.theme_settings.accent_color.0,
|
|
||||||
state.theme_settings.accent_color.0,
|
|
||||||
[0.0; 4],
|
|
||||||
),
|
),
|
||||||
|
None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]),
|
||||||
};
|
};
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
radius: scaled_effect_value(requested_radius, geometry, scale),
|
radius: corners.radius,
|
||||||
width,
|
width,
|
||||||
color: color_with_alpha(from, opacity),
|
color: color_with_alpha(from, opacity),
|
||||||
color_to: color_with_alpha(to, opacity),
|
color_to: color_with_alpha(to, opacity),
|
||||||
@@ -107,7 +128,7 @@ pub(super) fn window_border_element(
|
|||||||
focus_mix,
|
focus_mix,
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = state.window_borders.entry(id).or_default();
|
let buffers = &mut resources.windows.entry(id).or_default().borders;
|
||||||
if !buffers.contexts.contains_key(&context) {
|
if !buffers.contexts.contains_key(&context) {
|
||||||
let element = PixelShaderElement::new(
|
let element = PixelShaderElement::new(
|
||||||
programs.border.clone(),
|
programs.border.clone(),
|
||||||
@@ -137,7 +158,7 @@ pub(super) fn window_border_element(
|
|||||||
|
|
||||||
Some(PhysicalShaderElement {
|
Some(PhysicalShaderElement {
|
||||||
inner: cached.element.clone(),
|
inner: cached.element.clone(),
|
||||||
geometry: physical,
|
geometry: corners.rect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,31 +179,29 @@ pub(super) fn border_uniforms(parameters: &BorderParameters) -> Vec<Uniform<'sta
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_tint_element(
|
pub(super) fn window_tint_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
physical: Rectangle<i32, Physical>,
|
corners: RoundedRect,
|
||||||
scale: f64,
|
|
||||||
radius: f64,
|
|
||||||
color: [f32; 4],
|
color: [f32; 4],
|
||||||
resize_fill: bool,
|
resize_fill: bool,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
) -> Option<PhysicalShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
if color[3] <= 0.0 {
|
if color[3] <= 0.0 {
|
||||||
if resize_fill {
|
if resize_fill {
|
||||||
state.window_resize_fills.remove(&id);
|
resources.clear_tint(id, true);
|
||||||
} else {
|
} else {
|
||||||
state.window_dims.remove(&id);
|
resources.clear_tint(id, false);
|
||||||
}
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = material_program(state, renderer)?;
|
let program = material_program(resources, renderer)?;
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
radius: scaled_effect_value(radius, geometry, scale),
|
radius: corners.radius,
|
||||||
width: 0.0,
|
width: 0.0,
|
||||||
color,
|
color,
|
||||||
color_to: color,
|
color_to: color,
|
||||||
@@ -204,9 +223,9 @@ pub(super) fn window_tint_element(
|
|||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = if resize_fill {
|
let buffers = if resize_fill {
|
||||||
state.window_resize_fills.entry(id).or_default()
|
&mut resources.windows.entry(id).or_default().resize_fill
|
||||||
} else {
|
} else {
|
||||||
state.window_dims.entry(id).or_default()
|
&mut resources.windows.entry(id).or_default().dim
|
||||||
};
|
};
|
||||||
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder {
|
||||||
element: PixelShaderElement::new(
|
element: PixelShaderElement::new(
|
||||||
@@ -230,19 +249,18 @@ pub(super) fn window_tint_element(
|
|||||||
}
|
}
|
||||||
Some(PhysicalShaderElement {
|
Some(PhysicalShaderElement {
|
||||||
inner: cached.element.clone(),
|
inner: cached.element.clone(),
|
||||||
geometry: physical,
|
geometry: corners.rect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn window_shadow_element(
|
pub(super) fn window_shadow_element(
|
||||||
state: &mut Ferese,
|
resources: &mut RenderResources,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
id: ferese_layout::WindowId,
|
id: ferese_layout::WindowId,
|
||||||
geometry: Rectangle<i32, Logical>,
|
geometry: Rectangle<i32, Logical>,
|
||||||
physical: Rectangle<i32, Physical>,
|
corners: RoundedRect,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
|
||||||
offset_y: f64,
|
offset_y: f64,
|
||||||
blur: f64,
|
blur: f64,
|
||||||
opacity: f64,
|
opacity: f64,
|
||||||
@@ -256,20 +274,24 @@ pub(super) fn window_shadow_element(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let shadow_geometry = Rectangle::new(
|
let shadow_geometry = Rectangle::new(
|
||||||
(physical.loc.x, physical.loc.y + (offset_y * scale).round() as i32).into(),
|
(
|
||||||
physical.size,
|
corners.rect.loc.x,
|
||||||
|
corners.rect.loc.y + (offset_y * scale).round() as i32,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
corners.rect.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_geometry, mode.size, output.current_transform().invert()),
|
shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, output.current_transform().invert()),
|
||||||
radius: scaled_effect_value(requested_radius, geometry, scale),
|
radius: corners.radius,
|
||||||
opacity: opacity as f32,
|
opacity: opacity as f32,
|
||||||
color,
|
color,
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let buffers = state.window_shadows.entry(id).or_default();
|
let buffers = &mut resources.windows.entry(id).or_default().shadow;
|
||||||
if !buffers.contexts.contains_key(&context) {
|
if !buffers.contexts.contains_key(&context) {
|
||||||
let element = PixelShaderElement::new(
|
let element = PixelShaderElement::new(
|
||||||
programs.shadow.clone(),
|
programs.shadow.clone(),
|
||||||
@@ -327,25 +349,16 @@ pub(super) fn shadow_bounds(geometry: Rectangle<i32, Logical>, offset_y: f64, bl
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn scaled_effect_value(requested: f64, geometry: Rectangle<i32, Logical>, scale: f64) -> f32 {
|
|
||||||
requested
|
|
||||||
.min(f64::from(geometry.size.w.min(geometry.size.h)) / 2.0)
|
|
||||||
.max(0.0) as f32
|
|
||||||
* scale as f32
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(super) fn rounded_window_elements(
|
pub(super) fn rounded_window_elements(
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
window: &smithay::desktop::Window,
|
window: &smithay::desktop::Window,
|
||||||
constrain: Rectangle<i32, Logical>,
|
corners: RoundedRect,
|
||||||
physical_constrain: Rectangle<i32, Physical>,
|
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
|
||||||
alpha: f32,
|
alpha: f32,
|
||||||
clip_changed: bool,
|
clip_changed: bool,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
program: GlesTexProgram,
|
programs: RoundedClipPrograms,
|
||||||
behavior: ConstrainScaleBehavior,
|
behavior: ConstrainScaleBehavior,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
let Some(toplevel) = window.toplevel() else {
|
let Some(toplevel) = window.toplevel() else {
|
||||||
@@ -355,11 +368,12 @@ pub(super) fn rounded_window_elements(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let physical_constrain = corners.rect;
|
||||||
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 location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale);
|
||||||
let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert());
|
let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert());
|
||||||
let radius = scaled_effect_value(requested_radius, constrain, scale);
|
let radius = corners.radius;
|
||||||
let surface = toplevel.wl_surface();
|
let surface = toplevel.wl_surface();
|
||||||
|
|
||||||
let mut content = PopupManager::popups_for_surface(surface)
|
let mut content = PopupManager::popups_for_surface(surface)
|
||||||
@@ -389,14 +403,20 @@ pub(super) fn rounded_window_elements(
|
|||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|inner| {
|
.map(|inner| {
|
||||||
|
if radius == 0.0 {
|
||||||
|
// Rectangular clipping is already enforced by the outer crop
|
||||||
|
// element. Keep the raw surface available for fullscreen scanout.
|
||||||
|
WindowContentRenderElement::from(inner)
|
||||||
|
} else {
|
||||||
RoundedSurfaceRenderElement {
|
RoundedSurfaceRenderElement {
|
||||||
inner,
|
inner,
|
||||||
program: program.clone(),
|
programs: programs.clone(),
|
||||||
clip_rect: clip,
|
clip_rect: clip,
|
||||||
radius,
|
radius,
|
||||||
clip_changed,
|
clip_changed,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -20,23 +20,16 @@ pub(super) fn append_material_surface(
|
|||||||
};
|
};
|
||||||
let regions = crate::effects::surface_regions(surface);
|
let regions = crate::effects::surface_regions(surface);
|
||||||
let targets: Vec<_> = match ®ions {
|
let targets: Vec<_> = match ®ions {
|
||||||
None => vec![(geometry, radius as f32)],
|
None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius))],
|
||||||
Some(regions) => regions
|
Some(regions) => regions
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|r| {
|
.filter_map(|r| material_region_geometry(*r, geometry, content_origin, scale))
|
||||||
Rectangle::new(
|
|
||||||
(content_origin.x + r[0], content_origin.y + r[1]).into(),
|
|
||||||
(r[2], r[3]).into(),
|
|
||||||
)
|
|
||||||
.intersection(geometry)
|
|
||||||
.map(|rect| (rect, r[4] as f32))
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let materials: Vec<_> = targets
|
let materials: Vec<_> = targets
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(index, (rect, radius))| {
|
.filter_map(|(index, (rect, corners))| {
|
||||||
material_element(
|
material_element(
|
||||||
state,
|
state,
|
||||||
renderer,
|
renderer,
|
||||||
@@ -44,7 +37,7 @@ pub(super) fn append_material_surface(
|
|||||||
surface,
|
surface,
|
||||||
MaterialSurface {
|
MaterialSurface {
|
||||||
geometry: *rect,
|
geometry: *rect,
|
||||||
radius: *radius,
|
corners: *corners,
|
||||||
index,
|
index,
|
||||||
capture_geometry: geometry,
|
capture_geometry: geometry,
|
||||||
alpha: 1.0,
|
alpha: 1.0,
|
||||||
@@ -52,21 +45,18 @@ pub(super) fn append_material_surface(
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(buffers) = state.material_buffers.get_mut(surface) {
|
if let Some(buffers) = state.render.surfaces.get_mut(surface) {
|
||||||
buffers.contexts.retain(|(_, index), _| *index < targets.len());
|
buffers.contexts.retain(|(_, index), _| *index < targets.len());
|
||||||
}
|
}
|
||||||
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
let material = (!materials.is_empty() && regions.is_none()).then_some(());
|
||||||
let clip = material.as_ref().and_then(|_| {
|
let clip = material.as_ref().and_then(|_| {
|
||||||
|
let corners = targets.first()?.1;
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let program = rounded_clip_program(state, renderer)?;
|
let program = rounded_clip_program(&mut state.render, renderer)?;
|
||||||
Some((
|
Some((
|
||||||
program.texture,
|
program,
|
||||||
framebuffer_clip_rect(
|
framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
geometry.to_physical_precise_round(scale),
|
corners.radius,
|
||||||
mode.size,
|
|
||||||
output.current_transform().invert(),
|
|
||||||
),
|
|
||||||
scaled_effect_value(radius, geometry, scale),
|
|
||||||
))
|
))
|
||||||
});
|
});
|
||||||
// Smithay element lists are front to back: content, material, shadow.
|
// Smithay element lists are front to back: content, material, shadow.
|
||||||
@@ -83,7 +73,7 @@ pub(super) fn append_material_surface(
|
|||||||
let element = if let Some((program, clip_rect, radius)) = &clip {
|
let element = if let Some((program, clip_rect, radius)) = &clip {
|
||||||
WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement {
|
WindowContentRenderElement::Rounded(RoundedSurfaceRenderElement {
|
||||||
inner: element,
|
inner: element,
|
||||||
program: program.clone(),
|
programs: program.clone(),
|
||||||
clip_rect: *clip_rect,
|
clip_rect: *clip_rect,
|
||||||
radius: *radius,
|
radius: *radius,
|
||||||
clip_changed: false,
|
clip_changed: false,
|
||||||
@@ -101,9 +91,30 @@ pub(super) fn append_material_surface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preserve the surface-local fractional edges until the shared physical snap.
|
||||||
|
fn material_region_geometry(
|
||||||
|
region: [f64; 5],
|
||||||
|
geometry: Rectangle<i32, Logical>,
|
||||||
|
origin: Point<i32, Logical>,
|
||||||
|
scale: f64,
|
||||||
|
) -> Option<(Rectangle<i32, Logical>, RoundedRect)> {
|
||||||
|
let rect = Rectangle::<f64, Logical>::new(
|
||||||
|
(f64::from(origin.x) + region[0], f64::from(origin.y) + region[1]).into(),
|
||||||
|
(region[2], region[3]).into(),
|
||||||
|
)
|
||||||
|
.intersection(geometry.to_f64())?;
|
||||||
|
let corners = RoundedRect::new(
|
||||||
|
ferese_layout::Rect::new(rect.loc.x, rect.loc.y, rect.size.w, rect.size.h),
|
||||||
|
(0, 0).into(),
|
||||||
|
scale,
|
||||||
|
region[4],
|
||||||
|
);
|
||||||
|
Some((rect.to_i32_up(), corners))
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct MaterialSurface {
|
pub(super) struct MaterialSurface {
|
||||||
pub geometry: Rectangle<i32, Logical>,
|
pub geometry: Rectangle<i32, Logical>,
|
||||||
pub radius: f32,
|
pub corners: RoundedRect,
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub capture_geometry: Rectangle<i32, Logical>,
|
pub capture_geometry: Rectangle<i32, Logical>,
|
||||||
pub alpha: f32,
|
pub alpha: f32,
|
||||||
@@ -118,7 +129,7 @@ pub(super) fn material_element(
|
|||||||
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
||||||
let MaterialSurface {
|
let MaterialSurface {
|
||||||
geometry,
|
geometry,
|
||||||
radius,
|
corners,
|
||||||
index,
|
index,
|
||||||
capture_geometry,
|
capture_geometry,
|
||||||
alpha,
|
alpha,
|
||||||
@@ -139,7 +150,7 @@ pub(super) fn material_element(
|
|||||||
state.theme_settings.surface_base_color
|
state.theme_settings.surface_base_color
|
||||||
};
|
};
|
||||||
let [red, green, blue, _] = background.0;
|
let [red, green, blue, _] = background.0;
|
||||||
let radius = radius.min(geometry.size.w.min(geometry.size.h) as f32 * 0.5);
|
let radius = corners.radius;
|
||||||
let [offset_y, shadow_blur, shadow_opacity] = material.shadow;
|
let [offset_y, shadow_blur, shadow_opacity] = material.shadow;
|
||||||
let edge_bar = role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0;
|
let edge_bar = role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0;
|
||||||
let shadow_opacity = if edge_bar {
|
let shadow_opacity = if edge_bar {
|
||||||
@@ -148,8 +159,12 @@ pub(super) fn material_element(
|
|||||||
shadow_opacity * (state.theme_settings.shadow_opacity / 0.2) * f64::from(presentation_alpha)
|
shadow_opacity * (state.theme_settings.shadow_opacity / 0.2) * f64::from(presentation_alpha)
|
||||||
};
|
};
|
||||||
let shadow_geometry = Rectangle::new(
|
let shadow_geometry = Rectangle::new(
|
||||||
(geometry.loc.x, geometry.loc.y + offset_y.round() as i32).into(),
|
(
|
||||||
geometry.size,
|
corners.rect.loc.x,
|
||||||
|
corners.rect.loc.y + (offset_y * scale).round() as i32,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
corners.rect.size,
|
||||||
);
|
);
|
||||||
let background_opacity = material.opacity;
|
let background_opacity = material.opacity;
|
||||||
let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur);
|
let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur);
|
||||||
@@ -169,22 +184,24 @@ pub(super) fn material_element(
|
|||||||
sample_geometry,
|
sample_geometry,
|
||||||
sample_physical,
|
sample_physical,
|
||||||
sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform),
|
sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform),
|
||||||
radius: radius * scale as f32,
|
radius,
|
||||||
shadow_rect: framebuffer_clip_rect(shadow_geometry.to_physical_precise_round(scale), mode.size, transform),
|
shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, transform),
|
||||||
shadow_values: [(shadow_blur * scale) as f32, shadow_opacity as f32],
|
shadow_values: [(shadow_blur * scale) as f32, shadow_opacity as f32],
|
||||||
shadow_bounds: shadow_bounds(geometry, offset_y, shadow_blur),
|
shadow_bounds: shadow_bounds(geometry, offset_y, shadow_blur),
|
||||||
geometry,
|
geometry,
|
||||||
tint: [red, green, blue, material.opacity * presentation_alpha],
|
tint: [red, green, blue, material.opacity * presentation_alpha],
|
||||||
generation,
|
generation,
|
||||||
opaque: material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0,
|
opaque: material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0,
|
||||||
visible_framebuffer: framebuffer_clip_rect(geometry.to_physical_precise_round(scale), mode.size, transform),
|
visible_framebuffer: framebuffer_clip_rect(corners.rect, mode.size, transform),
|
||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let program = material_program(state, renderer)?;
|
let program = material_program(&mut state.render, renderer)?;
|
||||||
let blur_program = (blur > 0.0).then(|| blur_program(state, renderer)).flatten();
|
let blur_program = (blur > 0.0)
|
||||||
|
.then(|| blur_program(&mut state.render, renderer))
|
||||||
|
.flatten();
|
||||||
let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer);
|
||||||
let capture_size = Size::from((capture_rect[2], capture_rect[3]));
|
let capture_size = Size::from((capture_rect[2], capture_rect[3]));
|
||||||
let buffers = state.material_buffers.entry(surface.clone()).or_default();
|
let buffers = state.render.surfaces.entry(surface.clone()).or_default();
|
||||||
let capture = if let Some(blur_program) = blur_program {
|
let capture = if let Some(blur_program) = blur_program {
|
||||||
if buffers
|
if buffers
|
||||||
.captures
|
.captures
|
||||||
@@ -281,7 +298,14 @@ pub(super) fn material_element(
|
|||||||
cached.parameters = parameters;
|
cached.parameters = parameters;
|
||||||
}
|
}
|
||||||
let background = match &cached.element {
|
let background = match &cached.element {
|
||||||
MaterialElement::Fill(element) => element.clone().into(),
|
// Keep the shader's opaque regions when its canvas already has the
|
||||||
|
// shared edges. Animated modal geometry may need a physical override.
|
||||||
|
MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => element.clone().into(),
|
||||||
|
MaterialElement::Fill(element) => PhysicalShaderElement {
|
||||||
|
inner: element.clone(),
|
||||||
|
geometry: corners.rect,
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
MaterialElement::Blur(element) => element.clone().into(),
|
MaterialElement::Blur(element) => element.clone().into(),
|
||||||
};
|
};
|
||||||
Some((background, cached.shadow.clone().into()))
|
Some((background, cached.shadow.clone().into()))
|
||||||
@@ -295,15 +319,19 @@ pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn blur_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<BlurProgram> {
|
pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesRenderer) -> Option<BlurProgram> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.blur_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.blur.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
match renderer.compile_custom_texture_shader(BLUR_SHADER, &blur_uniform_names()) {
|
match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) {
|
||||||
Ok(program) => {
|
Ok(program) => {
|
||||||
let program = BlurProgram(program);
|
let program = BlurProgram(program);
|
||||||
state.blur_programs.insert(context, program.clone());
|
resources.contexts.entry(context).or_default().blur = Some(program.clone());
|
||||||
Some(program)
|
Some(program)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -358,9 +386,16 @@ pub(super) fn expanded_blur_region(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option<MaterialProgram> {
|
pub(super) fn material_program(
|
||||||
|
resources: &mut RenderResources,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
) -> Option<MaterialProgram> {
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
if let Some(program) = state.material_programs.get(&context) {
|
if let Some(program) = resources
|
||||||
|
.contexts
|
||||||
|
.get(&context)
|
||||||
|
.and_then(|programs| programs.material.as_ref())
|
||||||
|
{
|
||||||
return Some(program.clone());
|
return Some(program.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,10 +407,10 @@ pub(super) fn material_program(state: &mut Ferese, renderer: &mut GlesRenderer)
|
|||||||
UniformName::new("shadow_rect", UniformType::_4f),
|
UniformName::new("shadow_rect", UniformType::_4f),
|
||||||
UniformName::new("shadow_values", UniformType::_2f),
|
UniformName::new("shadow_values", UniformType::_2f),
|
||||||
];
|
];
|
||||||
match renderer.compile_custom_pixel_shader(MATERIAL_SHADER, &uniforms) {
|
match renderer.compile_custom_pixel_shader(corner_shader(MATERIAL_SHADER), &uniforms) {
|
||||||
Ok(program) => {
|
Ok(program) => {
|
||||||
let program = MaterialProgram(program);
|
let program = MaterialProgram(program);
|
||||||
state.material_programs.insert(context, program.clone());
|
resources.contexts.entry(context).or_default().material = Some(program.clone());
|
||||||
Some(program)
|
Some(program)
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -412,3 +447,22 @@ pub(super) fn blur_damage(
|
|||||||
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
|
DamageSet::from_slice(&[Rectangle::from_size((size.w, size.h).into())])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod region_geometry_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractional_regions_snap_once_in_output_pixels() {
|
||||||
|
let bounds = Rectangle::from_size((200, 100).into());
|
||||||
|
let (_, corners) = material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5).unwrap();
|
||||||
|
assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into()));
|
||||||
|
assert_eq!(corners.radius, 21.0);
|
||||||
|
// The old logical rounding would put this at physical (0, 0).
|
||||||
|
let (_, clipped) =
|
||||||
|
material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5).unwrap();
|
||||||
|
assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into()));
|
||||||
|
assert_eq!(clipped.radius, 30.0);
|
||||||
|
assert!(material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ mod capture;
|
|||||||
mod decorations;
|
mod decorations;
|
||||||
mod materials;
|
mod materials;
|
||||||
mod overview;
|
mod overview;
|
||||||
|
mod resources;
|
||||||
mod scene;
|
mod scene;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
@@ -13,9 +13,10 @@ pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture
|
|||||||
use decorations::*;
|
use decorations::*;
|
||||||
use materials::*;
|
use materials::*;
|
||||||
use overview::*;
|
use overview::*;
|
||||||
|
pub(crate) use resources::RenderResources;
|
||||||
use scene::cursor_elements;
|
use scene::cursor_elements;
|
||||||
pub(crate) use scene::{
|
pub(crate) use scene::{
|
||||||
animated_window_elements, cursorless_window_elements, frame_effect_metrics, layer_surfaces, output_elements,
|
animated_window_elements, frame_effect_metrics, layer_surfaces, output_elements, sampled_output_elements,
|
||||||
};
|
};
|
||||||
use smithay::backend::allocator::Fourcc;
|
use smithay::backend::allocator::Fourcc;
|
||||||
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement;
|
||||||
@@ -48,7 +49,7 @@ use smithay::wayland::shell::wlr_layer::Layer;
|
|||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
use crate::metrics::FrameEffectMetrics;
|
use crate::metrics::FrameEffectMetrics;
|
||||||
use crate::presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect};
|
use crate::presentation::{NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, physical_rect};
|
||||||
|
|
||||||
type SurfaceRenderElement =
|
type SurfaceRenderElement =
|
||||||
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
CropRenderElement<RelocateRenderElement<RescaleRenderElement<WaylandSurfaceRenderElement<GlesRenderer>>>>;
|
||||||
@@ -96,6 +97,8 @@ render_elements! {
|
|||||||
|
|
||||||
const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag");
|
const ROUNDED_TEXTURE_SHADER: &str = include_str!("shaders/rounded_texture_shader.frag");
|
||||||
|
|
||||||
|
const ROUNDED_SOLID_SHADER: &str = include_str!("shaders/rounded_solid_shader.frag");
|
||||||
|
|
||||||
const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag");
|
const ROUNDED_BORDER_SHADER: &str = include_str!("shaders/rounded_border_shader.frag");
|
||||||
|
|
||||||
const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag");
|
const WINDOW_SHADOW_SHADER: &str = include_str!("shaders/window_shadow_shader.frag");
|
||||||
@@ -106,6 +109,10 @@ const MATERIAL_SHADER: &str = include_str!("shaders/material_shader.frag");
|
|||||||
|
|
||||||
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
const BLUR_SHADER: &str = include_str!("shaders/blur_shader.frag");
|
||||||
|
|
||||||
|
fn corner_shader(source: &str) -> String {
|
||||||
|
source.replace("//_CORNERS_", include_str!("shaders/corners.glsl"))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct BlurProgram(GlesTexProgram);
|
pub(crate) struct BlurProgram(GlesTexProgram);
|
||||||
|
|
||||||
@@ -276,6 +283,7 @@ impl RenderElement<GlesRenderer> for BlurRenderElement {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct RoundedClipPrograms {
|
pub(crate) struct RoundedClipPrograms {
|
||||||
|
solid: GlesPixelProgram,
|
||||||
texture: GlesTexProgram,
|
texture: GlesTexProgram,
|
||||||
border: GlesPixelProgram,
|
border: GlesPixelProgram,
|
||||||
shadow: GlesPixelProgram,
|
shadow: GlesPixelProgram,
|
||||||
@@ -386,7 +394,7 @@ pub(crate) struct WindowShadowBuffers {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct RoundedSurfaceRenderElement {
|
struct RoundedSurfaceRenderElement {
|
||||||
inner: WaylandSurfaceRenderElement<GlesRenderer>,
|
inner: WaylandSurfaceRenderElement<GlesRenderer>,
|
||||||
program: GlesTexProgram,
|
programs: RoundedClipPrograms,
|
||||||
clip_rect: [f32; 4],
|
clip_rect: [f32; 4],
|
||||||
radius: f32,
|
radius: f32,
|
||||||
clip_changed: bool,
|
clip_changed: bool,
|
||||||
@@ -455,18 +463,31 @@ impl RenderElement<GlesRenderer> for RoundedSurfaceRenderElement {
|
|||||||
opaque_regions,
|
opaque_regions,
|
||||||
self.transform(),
|
self.transform(),
|
||||||
self.alpha(),
|
self.alpha(),
|
||||||
Some(&self.program),
|
Some(&self.programs.texture),
|
||||||
&[
|
&[
|
||||||
Uniform::new("clip_rect", self.clip_rect),
|
Uniform::new("clip_rect", self.clip_rect),
|
||||||
Uniform::new("radius", self.radius),
|
Uniform::new("radius", self.radius),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
WaylandSurfaceTexture::SolidColor(_) => self.inner.draw(frame, src, dst, damage, opaque_regions),
|
WaylandSurfaceTexture::SolidColor(color) => frame.render_pixel_shader_to(
|
||||||
|
&self.programs.solid,
|
||||||
|
src,
|
||||||
|
dst,
|
||||||
|
(dst.size.w, dst.size.h).into(),
|
||||||
|
Some(damage),
|
||||||
|
self.alpha(),
|
||||||
|
&[
|
||||||
|
Uniform::new("clip_rect", self.clip_rect),
|
||||||
|
Uniform::new("radius", self.radius),
|
||||||
|
Uniform::new("color", color.components()),
|
||||||
|
],
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn underlying_storage(&self, renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
fn underlying_storage(&self, _renderer: &mut GlesRenderer) -> Option<UnderlyingStorage<'_>> {
|
||||||
self.inner.underlying_storage(renderer)
|
// Storage is deliberately hidden: plane scanout would bypass clipping.
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,6 +531,91 @@ mod tests {
|
|||||||
scaled_visual_rect, shadow_bounds,
|
scaled_visual_rect, shadow_bounds,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn solid_corner_mask_preserves_premultiplied_color() {
|
||||||
|
use smithay::backend::allocator::Fourcc;
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform, UniformName, UniformType};
|
||||||
|
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer};
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let context = EGLContext::new(&display).unwrap();
|
||||||
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
|
let program = renderer
|
||||||
|
.compile_custom_pixel_shader(
|
||||||
|
super::corner_shader(super::ROUNDED_SOLID_SHADER),
|
||||||
|
&[
|
||||||
|
UniformName::new("clip_rect", UniformType::_4f),
|
||||||
|
UniformName::new("radius", UniformType::_1f),
|
||||||
|
UniformName::new("color", UniformType::_4f),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let size = (32, 32).into();
|
||||||
|
let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap();
|
||||||
|
let rect = Rectangle::<i32, Physical>::from_size((32, 32).into());
|
||||||
|
for (radius, corner_alpha, inset) in [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] {
|
||||||
|
let mut target = renderer.bind(&mut texture).unwrap();
|
||||||
|
{
|
||||||
|
let mut frame = renderer
|
||||||
|
.render(&mut target, (32, 32).into(), Transform::Normal)
|
||||||
|
.unwrap();
|
||||||
|
frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap();
|
||||||
|
frame
|
||||||
|
.render_pixel_shader_to(
|
||||||
|
&program,
|
||||||
|
Rectangle::from_size(size.to_f64()),
|
||||||
|
rect,
|
||||||
|
size,
|
||||||
|
Some(&[rect]),
|
||||||
|
0.5,
|
||||||
|
&[
|
||||||
|
Uniform::new("clip_rect", [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset]),
|
||||||
|
Uniform::new("radius", radius),
|
||||||
|
Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let _ = frame.finish().unwrap();
|
||||||
|
}
|
||||||
|
let mapping = renderer
|
||||||
|
.copy_framebuffer(&target, Rectangle::from_size(size), Fourcc::Abgr8888)
|
||||||
|
.unwrap();
|
||||||
|
let pixels = renderer.map_texture(&mapping).unwrap();
|
||||||
|
let center = (16 * 32 + 16) * 4;
|
||||||
|
assert!(pixels[center].abs_diff(64) <= 1);
|
||||||
|
assert!(pixels[center + 3].abs_diff(64) <= 1);
|
||||||
|
assert!(pixels[3].abs_diff(corner_alpha) <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn shared_corner_shaders_compile() {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
use smithay::backend::renderer::gles::GlesRenderer;
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let context = EGLContext::new(&display).unwrap();
|
||||||
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
|
for source in [super::ROUNDED_TEXTURE_SHADER, super::BLUR_SHADER] {
|
||||||
|
renderer
|
||||||
|
.compile_custom_texture_shader(super::corner_shader(source), &[])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
for source in [
|
||||||
|
super::ROUNDED_SOLID_SHADER,
|
||||||
|
super::ROUNDED_BORDER_SHADER,
|
||||||
|
super::WINDOW_SHADOW_SHADER,
|
||||||
|
super::MATERIAL_SHADER,
|
||||||
|
] {
|
||||||
|
renderer
|
||||||
|
.compile_custom_pixel_shader(super::corner_shader(source), &[])
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires an EGL rendering device"]
|
#[ignore = "requires an EGL rendering device"]
|
||||||
fn translucent_blur_does_not_leak_the_sharp_backdrop() {
|
fn translucent_blur_does_not_leak_the_sharp_backdrop() {
|
||||||
@@ -523,7 +629,7 @@ mod tests {
|
|||||||
let context = EGLContext::new(&display).unwrap();
|
let context = EGLContext::new(&display).unwrap();
|
||||||
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
|
||||||
let program = renderer
|
let program = renderer
|
||||||
.compile_custom_texture_shader(super::BLUR_SHADER, &super::blur_uniform_names())
|
.compile_custom_texture_shader(super::corner_shader(super::BLUR_SHADER), &super::blur_uniform_names())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let size = (32, 32).into();
|
let size = (32, 32).into();
|
||||||
let mut pixels = vec![128u8; 32 * 32 * 4];
|
let mut pixels = vec![128u8; 32 * 32 * 4];
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ pub(super) fn overview_chrome_element(
|
|||||||
) -> Option<PhysicalShaderElement> {
|
) -> Option<PhysicalShaderElement> {
|
||||||
let output_id = state.output_id(output)?;
|
let output_id = state.output_id(output)?;
|
||||||
let geometry = rounded_visual_rect(rect, output_geometry.loc);
|
let geometry = rounded_visual_rect(rect, output_geometry.loc);
|
||||||
let physical = physical_rect(rect, output_geometry.loc, scale);
|
let corners = RoundedRect::new(rect, output_geometry.loc, scale, radius);
|
||||||
let mode = output.current_mode()?;
|
let mode = output.current_mode()?;
|
||||||
let parameters = BorderParameters {
|
let parameters = BorderParameters {
|
||||||
geometry,
|
geometry,
|
||||||
clip_rect: framebuffer_clip_rect(physical, mode.size, output.current_transform().invert()),
|
clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()),
|
||||||
radius: (radius * scale) as f32,
|
radius: corners.radius,
|
||||||
width: (width * scale) as f32,
|
width: (width * scale) as f32,
|
||||||
color,
|
color,
|
||||||
color_to: color,
|
color_to: color,
|
||||||
@@ -31,9 +31,9 @@ pub(super) fn overview_chrome_element(
|
|||||||
focus_mix: 0.0,
|
focus_mix: 0.0,
|
||||||
};
|
};
|
||||||
let program = if key.1 == OverviewChromePart::Outline {
|
let program = if key.1 == OverviewChromePart::Outline {
|
||||||
rounded_clip_program(state, renderer)?.border
|
rounded_clip_program(&mut state.render, renderer)?.border
|
||||||
} else {
|
} else {
|
||||||
material_program(state, renderer)?.0
|
material_program(&mut state.render, renderer)?.0
|
||||||
};
|
};
|
||||||
let uniforms = |p: &BorderParameters| {
|
let uniforms = |p: &BorderParameters| {
|
||||||
if key.1 == OverviewChromePart::Outline {
|
if key.1 == OverviewChromePart::Outline {
|
||||||
@@ -51,7 +51,8 @@ pub(super) fn overview_chrome_element(
|
|||||||
};
|
};
|
||||||
let context = renderer.context_id().erased();
|
let context = renderer.context_id().erased();
|
||||||
let chrome = &mut state
|
let chrome = &mut state
|
||||||
.overview_scrims
|
.render
|
||||||
|
.outputs
|
||||||
.entry(output_id)
|
.entry(output_id)
|
||||||
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
.or_insert_with(|| OverviewScrim { chrome: HashMap::new() })
|
||||||
.chrome;
|
.chrome;
|
||||||
@@ -79,7 +80,7 @@ pub(super) fn overview_chrome_element(
|
|||||||
}
|
}
|
||||||
Some(PhysicalShaderElement {
|
Some(PhysicalShaderElement {
|
||||||
inner: cached.element.clone(),
|
inner: cached.element.clone(),
|
||||||
geometry: physical,
|
geometry: corners.rect,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,15 +90,16 @@ pub(super) fn overview_strip_elements(
|
|||||||
output: &Output,
|
output: &Output,
|
||||||
output_geometry: Rectangle<i32, Logical>,
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
|
frame: &crate::state::FrameScene,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
let alpha = state.overview.opacity();
|
let alpha = frame.overview.opacity();
|
||||||
if alpha <= 0.001 {
|
if alpha <= 0.001 {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let Some(bounds) = state.output_bounds_for(output) else {
|
let Some(bounds) = state.output_bounds_for(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let cards = state.overview_workspace_cards(output);
|
let cards = state.overview_workspace_cards_for_frame(output, Some(frame));
|
||||||
let strip = crate::overview::workspace_strip(bounds);
|
let strip = crate::overview::workspace_strip(bounds);
|
||||||
let panel = match (cards.first(), cards.last()) {
|
let panel = match (cards.first(), cards.last()) {
|
||||||
(Some(first), Some(last)) => ferese_layout::Rect::new(
|
(Some(first), Some(last)) => ferese_layout::Rect::new(
|
||||||
@@ -116,16 +118,17 @@ pub(super) fn overview_strip_elements(
|
|||||||
)
|
)
|
||||||
.opacity;
|
.opacity;
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
let programs = rounded_clip_program(state, renderer);
|
let programs = rounded_clip_program(&mut state.render, renderer);
|
||||||
// Resolve IDs once per strip, rather than scanning all managed windows
|
// Resolve IDs once per strip, rather than scanning all managed windows
|
||||||
// separately for every miniature. Own the handles so rendering can mutate state.
|
// separately for every miniature. Own the handles so rendering can mutate state.
|
||||||
let windows_by_id: std::collections::HashMap<_, _> = state
|
let windows_by_id: std::collections::HashMap<_, _> = state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(window, id)| (*id, window.clone()))
|
.map(|(window, id)| (*id, window.clone()))
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(output_id) = state.output_id(output)
|
if let Some(output_id) = state.output_id(output)
|
||||||
&& let Some(cache) = state.overview_scrims.get_mut(&output_id)
|
&& let Some(cache) = state.render.outputs.get_mut(&output_id)
|
||||||
{
|
{
|
||||||
cache.chrome.retain(|(id, part), _| match part {
|
cache.chrome.retain(|(id, part), _| match part {
|
||||||
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
|
OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id),
|
||||||
@@ -139,7 +142,7 @@ pub(super) fn overview_strip_elements(
|
|||||||
if !state.window_belongs_to_output(*id, output) {
|
if !state.window_belongs_to_output(*id, output) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(rect) = state.presented_window_rect(*id) else {
|
let Some(rect) = frame.windows.get(id).map(|sample| sample.rect) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else {
|
let Some((buffer, size)) = state.overview_window_label(window, scale, rect.width) else {
|
||||||
@@ -223,20 +226,18 @@ pub(super) fn overview_strip_elements(
|
|||||||
elements.extend(rounded_window_elements(
|
elements.extend(rounded_window_elements(
|
||||||
renderer,
|
renderer,
|
||||||
window,
|
window,
|
||||||
rounded_visual_rect(*rect, output_geometry.loc),
|
RoundedRect::new(*rect, output_geometry.loc, scale, 4.0),
|
||||||
physical_rect(*rect, output_geometry.loc, scale),
|
|
||||||
scale,
|
scale,
|
||||||
4.0,
|
|
||||||
alpha,
|
alpha,
|
||||||
true,
|
true,
|
||||||
output,
|
output,
|
||||||
programs.texture.clone(),
|
programs.clone(),
|
||||||
ConstrainScaleBehavior::Stretch,
|
ConstrainScaleBehavior::Stretch,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((buffer, size)) = state.overview_workspace_label(card.workspace, scale) {
|
if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) {
|
||||||
let location = Point::<i32, Physical>::from((
|
let location = Point::<i32, Physical>::from((
|
||||||
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32
|
((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32
|
||||||
- size.w / 2,
|
- size.w / 2,
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
use ferese_core::OutputId;
|
||||||
|
use ferese_layout::WindowId;
|
||||||
|
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Resource ownership follows the lifetime of a window, output, surface or GL
|
||||||
|
/// context. Desktop policy and animation values do not live in these caches.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct RenderResources {
|
||||||
|
pub(super) windows: HashMap<WindowId, WindowBuffers>,
|
||||||
|
pub(super) contexts: HashMap<ErasedContextId, ContextPrograms>,
|
||||||
|
pub(super) outputs: HashMap<OutputId, OverviewScrim>,
|
||||||
|
pub(super) surfaces: HashMap<WlSurface, MaterialBuffers>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct WindowBuffers {
|
||||||
|
pub borders: WindowBorderBuffers,
|
||||||
|
pub dim: WindowBorderBuffers,
|
||||||
|
pub resize_fill: WindowBorderBuffers,
|
||||||
|
pub shadow: WindowShadowBuffers,
|
||||||
|
snapshot: Option<ResizeSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(super) struct ContextPrograms {
|
||||||
|
pub rounded: Option<RoundedClipPrograms>,
|
||||||
|
pub rounded_warned: bool,
|
||||||
|
pub material: Option<MaterialProgram>,
|
||||||
|
pub blur: Option<BlurProgram>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderResources {
|
||||||
|
pub fn remove_window(&mut self, id: WindowId) {
|
||||||
|
self.windows.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_output(&mut self, id: OutputId) {
|
||||||
|
self.outputs.remove(&id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshot(&self, id: &WindowId) -> Option<&ResizeSnapshot> {
|
||||||
|
self.windows.get(id)?.snapshot.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn snapshots(&self) -> impl Iterator<Item = &ResizeSnapshot> {
|
||||||
|
self.windows.values().filter_map(|window| window.snapshot.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) {
|
||||||
|
self.windows.entry(id).or_default().snapshot = Some(snapshot);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_snapshot(&mut self, id: &WindowId) {
|
||||||
|
if let Some(window) = self.windows.get_mut(id) {
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn retain_snapshots(&mut self, mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool) {
|
||||||
|
for (id, window) in &mut self.windows {
|
||||||
|
if let Some(snapshot) = &mut window.snapshot
|
||||||
|
&& !keep(id, snapshot)
|
||||||
|
{
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn clear_tint(&mut self, id: WindowId, resize_fill: bool) {
|
||||||
|
let Some(window) = self.windows.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if resize_fill {
|
||||||
|
window.resize_fill.contexts.clear();
|
||||||
|
} else {
|
||||||
|
window.dim.contexts.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn forget_context(&mut self, context: &ErasedContextId) {
|
||||||
|
self.contexts.remove(context);
|
||||||
|
|
||||||
|
for window in self.windows.values_mut() {
|
||||||
|
window.borders.contexts.remove(context);
|
||||||
|
window.dim.contexts.remove(context);
|
||||||
|
window.resize_fill.contexts.remove(context);
|
||||||
|
window.shadow.contexts.remove(context);
|
||||||
|
|
||||||
|
if window
|
||||||
|
.snapshot
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|snapshot| &snapshot.context == context)
|
||||||
|
{
|
||||||
|
window.snapshot = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for output in self.outputs.values_mut() {
|
||||||
|
for contexts in output.chrome.values_mut() {
|
||||||
|
contexts.remove(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for surface in self.surfaces.values_mut() {
|
||||||
|
surface.contexts.retain(|(id, _), _| id != context);
|
||||||
|
surface.captures.remove(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn window_removal_releases_all_its_caches_without_removing_other_windows() {
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
resources.windows.insert(WindowId(1), WindowBuffers::default());
|
||||||
|
resources.windows.insert(WindowId(2), WindowBuffers::default());
|
||||||
|
|
||||||
|
resources.remove_window(WindowId(1));
|
||||||
|
resources.clear_snapshot(&WindowId(1));
|
||||||
|
resources.clear_tint(WindowId(1), true);
|
||||||
|
|
||||||
|
assert!(!resources.windows.contains_key(&WindowId(1)));
|
||||||
|
assert!(resources.windows.contains_key(&WindowId(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires an EGL rendering device"]
|
||||||
|
fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() {
|
||||||
|
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
|
||||||
|
|
||||||
|
let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device");
|
||||||
|
let display = unsafe { EGLDisplay::new(device).unwrap() };
|
||||||
|
let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||||
|
let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() };
|
||||||
|
let first_id = first.context_id().erased();
|
||||||
|
let second_id = second.context_id().erased();
|
||||||
|
let mut resources = RenderResources::default();
|
||||||
|
|
||||||
|
for (id, renderer) in [(WindowId(1), &mut first), (WindowId(2), &mut second)] {
|
||||||
|
rounded_clip_program(&mut resources, renderer).unwrap();
|
||||||
|
material_program(&mut resources, renderer).unwrap();
|
||||||
|
blur_program(&mut resources, renderer).unwrap();
|
||||||
|
let texture = Offscreen::<GlesTexture>::create_buffer(renderer, Fourcc::Abgr8888, (16, 16).into()).unwrap();
|
||||||
|
resources.set_snapshot(
|
||||||
|
id,
|
||||||
|
ResizeSnapshot {
|
||||||
|
texture,
|
||||||
|
context: renderer.context_id().erased(),
|
||||||
|
id: Id::new(),
|
||||||
|
commit: CommitCounter::default(),
|
||||||
|
elapsed: Duration::ZERO,
|
||||||
|
last_tick: None,
|
||||||
|
scale: 1.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.forget_context(&first_id);
|
||||||
|
|
||||||
|
assert!(!resources.contexts.contains_key(&first_id));
|
||||||
|
assert!(resources.contexts.contains_key(&second_id));
|
||||||
|
assert!(resources.snapshot(&WindowId(1)).is_none());
|
||||||
|
assert!(resources.snapshot(&WindowId(2)).is_some());
|
||||||
|
assert_eq!(resources.snapshots().count(), 1);
|
||||||
|
|
||||||
|
// Reconnecting a context can build fresh resources after eviction.
|
||||||
|
rounded_clip_program(&mut resources, &mut first).unwrap();
|
||||||
|
assert!(resources.contexts.contains_key(&first_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,14 +8,6 @@ pub(crate) fn animated_window_elements(
|
|||||||
output_elements(state, renderer, output, true)
|
output_elements(state, renderer, output, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cursorless_window_elements(
|
|
||||||
state: &mut Ferese,
|
|
||||||
renderer: &mut GlesRenderer,
|
|
||||||
output: &Output,
|
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
|
||||||
output_elements(state, renderer, output, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics {
|
pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics {
|
||||||
FrameEffectMetrics
|
FrameEffectMetrics
|
||||||
}
|
}
|
||||||
@@ -25,9 +17,20 @@ pub(crate) fn output_elements(
|
|||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
include_cursor: bool,
|
include_cursor: bool,
|
||||||
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
let frame = state.sample_frame(output, Duration::ZERO);
|
||||||
|
|
||||||
|
sampled_output_elements(state, renderer, output, include_cursor, &frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sampled_output_elements(
|
||||||
|
state: &mut Ferese,
|
||||||
|
renderer: &mut GlesRenderer,
|
||||||
|
output: &Output,
|
||||||
|
include_cursor: bool,
|
||||||
|
frame: &crate::state::FrameScene,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
if state.session_lock.active {
|
if state.session_lock.active {
|
||||||
state.configure_lock_surfaces();
|
|
||||||
let Some(geometry) = state.space.output_geometry(output) else {
|
let Some(geometry) = state.space.output_geometry(output) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
@@ -83,29 +86,36 @@ pub(crate) fn output_elements(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let scale = output.current_scale().fractional_scale();
|
let scale = output.current_scale().fractional_scale();
|
||||||
let rounded_clip_program = rounded_clip_program(state, renderer);
|
let rounded_clip_program = rounded_clip_program(&mut state.render, renderer);
|
||||||
|
|
||||||
let mut elements = if include_cursor {
|
let mut elements = if include_cursor {
|
||||||
cursor_elements(state, renderer, output_geometry, scale)
|
cursor_elements(state, renderer, output_geometry, scale)
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
|
let upper_layers: &[Layer] = if state.output_has_fullscreen_for_frame(output, frame.overview.is_presenting()) {
|
||||||
&[Layer::Overlay]
|
&[Layer::Overlay]
|
||||||
} else {
|
} else {
|
||||||
&[Layer::Overlay, Layer::Top]
|
&[Layer::Overlay, Layer::Top]
|
||||||
};
|
};
|
||||||
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
elements.extend(layer_elements(state, renderer, output, upper_layers));
|
||||||
if state.overview.is_presenting() {
|
if frame.overview.is_presenting() {
|
||||||
elements.extend(overview_strip_elements(state, renderer, output, output_geometry, scale));
|
elements.extend(overview_strip_elements(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
output_geometry,
|
||||||
|
scale,
|
||||||
|
frame,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut candidates = if state.overview.is_active() {
|
let mut candidates = if state.overview.is_active() {
|
||||||
state.window_ids.keys().cloned().collect::<Vec<_>>()
|
state.windows.ids().keys().cloned().collect::<Vec<_>>()
|
||||||
} else {
|
} else {
|
||||||
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
state.space.elements().rev().cloned().collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
if state.overview.is_active() {
|
if state.overview.is_active() {
|
||||||
candidates.sort_by_key(|window| std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0)));
|
candidates.sort_by_key(|window| std::cmp::Reverse(state.windows.ids().get(window).map_or(0, |id| id.0)));
|
||||||
}
|
}
|
||||||
let windows = candidates
|
let windows = candidates
|
||||||
.iter()
|
.iter()
|
||||||
@@ -115,20 +125,17 @@ pub(crate) fn output_elements(
|
|||||||
if !state.window_content_ready(window) {
|
if !state.window_content_ready(window) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let id = *state.window_ids.get(window)?;
|
let id = *state.windows.ids().get(window)?;
|
||||||
// Scrolling columns may sit outside their monitor's rectangle.
|
// Scrolling columns may sit outside their monitor's rectangle.
|
||||||
// They must not reappear on a neighboring output just because
|
// They must not reappear on a neighboring output just because
|
||||||
// their global animated coordinates overlap it.
|
// their global animated coordinates overlap it.
|
||||||
if !state.window_belongs_to_output(id, output) {
|
if !state.window_belongs_to_output(id, output) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let visual = state.presented_window_rect(id)?;
|
let sample = frame.windows.get(&id)?;
|
||||||
let (close_scale, close_alpha) = state.closing_visual(id);
|
let visual = scaled_visual_rect(sample.rect, sample.close_scale);
|
||||||
let visual = scaled_visual_rect(visual, close_scale);
|
let close_alpha = sample.close_alpha;
|
||||||
let decoration_progress = state
|
let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0);
|
||||||
.window_geometry
|
|
||||||
.get(&id)
|
|
||||||
.map_or(1.0, |geometry| geometry.decorations.clamp(0.0, 1.0));
|
|
||||||
|
|
||||||
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
||||||
})
|
})
|
||||||
@@ -136,7 +143,6 @@ pub(crate) 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);
|
|
||||||
let material_surface = window
|
let material_surface = window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.map(|toplevel| toplevel.wl_surface())
|
.map(|toplevel| toplevel.wl_surface())
|
||||||
@@ -149,19 +155,19 @@ pub(crate) fn output_elements(
|
|||||||
} else {
|
} else {
|
||||||
state.theme_settings.window_radius
|
state.theme_settings.window_radius
|
||||||
} * decoration_progress;
|
} * decoration_progress;
|
||||||
|
let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius);
|
||||||
|
let pixels = corners.rect;
|
||||||
// Only overview/close intentionally scale the complete application.
|
// Only overview/close intentionally scale the complete application.
|
||||||
let scale_content = state.overview.is_presenting() || state.closing_visual(id).0 != 1.0;
|
let scale_content = frame.overview.is_presenting() || frame.windows[&id].close_scale != 1.0;
|
||||||
let behavior = resize_content_behavior(scale_content);
|
let behavior = resize_content_behavior(scale_content);
|
||||||
|
|
||||||
let dim = state.window_dimming.get(&id).map_or(0.0, |dim| dim.current);
|
let dim = frame.windows[&id].dim;
|
||||||
if let Some(overlay) = window_tint_element(
|
if let Some(overlay) = window_tint_element(
|
||||||
state,
|
&mut state.render,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
pixels,
|
corners,
|
||||||
scale,
|
|
||||||
window_radius,
|
|
||||||
[0.0, 0.0, 0.0, dim as f32 * close_alpha],
|
[0.0, 0.0, 0.0, dim as f32 * close_alpha],
|
||||||
false,
|
false,
|
||||||
output,
|
output,
|
||||||
@@ -176,28 +182,20 @@ pub(crate) fn output_elements(
|
|||||||
let shadow_blur = state.theme_settings.shadow_blur;
|
let shadow_blur = state.theme_settings.shadow_blur;
|
||||||
let shadow_opacity = state.theme_settings.shadow_opacity;
|
let shadow_opacity = state.theme_settings.shadow_opacity;
|
||||||
let shadow_color = state.theme_settings.shadow_color.0;
|
let shadow_color = state.theme_settings.shadow_color.0;
|
||||||
let focused = if state.overview.is_active() {
|
let focus = frame.windows[&id].focus;
|
||||||
state.overview_selected(id)
|
|
||||||
} else {
|
|
||||||
state.focused_window == Some(id)
|
|
||||||
};
|
|
||||||
let focus = state
|
|
||||||
.window_focus
|
|
||||||
.get(&id)
|
|
||||||
.map_or(if focused { 1.0 } else { 0.0 }, |v| v.current);
|
|
||||||
let border_width = state.theme_settings.border_width
|
let border_width = state.theme_settings.border_width
|
||||||
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
|
+ (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus;
|
||||||
let border_color = state.theme_settings.border_color.0;
|
let border_color = state.theme_settings.border_color.0;
|
||||||
let gradient = state.theme_settings.border_gradient;
|
let gradient = state.theme_settings.border_gradient;
|
||||||
|
|
||||||
if let Some(border) = window_border_element(
|
if let Some(border) = window_border_element(
|
||||||
state,
|
&mut state.render,
|
||||||
|
&state.theme_settings,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
pixels,
|
corners,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
|
||||||
border_width,
|
border_width,
|
||||||
border_color,
|
border_color,
|
||||||
gradient,
|
gradient,
|
||||||
@@ -209,13 +207,12 @@ pub(crate) fn output_elements(
|
|||||||
elements.push(border.into());
|
elements.push(border.into());
|
||||||
}
|
}
|
||||||
let shadow = window_shadow_element(
|
let shadow = window_shadow_element(
|
||||||
state,
|
&mut state.render,
|
||||||
renderer,
|
renderer,
|
||||||
id,
|
id,
|
||||||
constrain,
|
constrain,
|
||||||
pixels,
|
corners,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
|
||||||
shadow_offset_y,
|
shadow_offset_y,
|
||||||
shadow_blur,
|
shadow_blur,
|
||||||
shadow_opacity * f64::from(close_alpha) * decoration_progress,
|
shadow_opacity * f64::from(close_alpha) * decoration_progress,
|
||||||
@@ -224,7 +221,7 @@ pub(crate) fn output_elements(
|
|||||||
&programs,
|
&programs,
|
||||||
);
|
);
|
||||||
if !scale_content
|
if !scale_content
|
||||||
&& let Some(snapshot) = state.resize_snapshots.get(&id)
|
&& let Some(snapshot) = state.render.snapshot(&id)
|
||||||
&& snapshot.context == renderer.context_id().erased()
|
&& snapshot.context == renderer.context_id().erased()
|
||||||
&& (snapshot.scale - scale).abs() < 0.001
|
&& (snapshot.scale - scale).abs() < 0.001
|
||||||
{
|
{
|
||||||
@@ -250,8 +247,7 @@ pub(crate) fn output_elements(
|
|||||||
program: Some(programs.texture.clone()),
|
program: Some(programs.texture.clone()),
|
||||||
uniforms: vec![
|
uniforms: vec![
|
||||||
Uniform::new("clip_rect", clip).into_owned(),
|
Uniform::new("clip_rect", clip).into_owned(),
|
||||||
Uniform::new("radius", scaled_effect_value(window_radius, constrain, scale))
|
Uniform::new("radius", corners.radius).into_owned(),
|
||||||
.into_owned(),
|
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
@@ -261,17 +257,12 @@ pub(crate) fn output_elements(
|
|||||||
elements.extend(rounded_window_elements(
|
elements.extend(rounded_window_elements(
|
||||||
renderer,
|
renderer,
|
||||||
&window,
|
&window,
|
||||||
constrain,
|
corners,
|
||||||
pixels,
|
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
|
||||||
close_alpha,
|
close_alpha,
|
||||||
state
|
frame.windows[&id].geometry.presentation_changed,
|
||||||
.window_geometry
|
|
||||||
.get(&id)
|
|
||||||
.is_some_and(|geometry| geometry.presentation_changed),
|
|
||||||
output,
|
output,
|
||||||
programs.texture.clone(),
|
programs.clone(),
|
||||||
behavior,
|
behavior,
|
||||||
));
|
));
|
||||||
let source = window.geometry().size;
|
let source = window.geometry().size;
|
||||||
@@ -281,24 +272,15 @@ pub(crate) fn output_elements(
|
|||||||
{
|
{
|
||||||
let mut color = state.theme_settings.surface_base_color.0;
|
let mut color = state.theme_settings.surface_base_color.0;
|
||||||
color[3] = close_alpha;
|
color[3] = close_alpha;
|
||||||
if let Some(fill) = window_tint_element(
|
if let Some(fill) =
|
||||||
state,
|
window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output)
|
||||||
renderer,
|
{
|
||||||
id,
|
|
||||||
constrain,
|
|
||||||
pixels,
|
|
||||||
scale,
|
|
||||||
window_radius,
|
|
||||||
color,
|
|
||||||
true,
|
|
||||||
output,
|
|
||||||
) {
|
|
||||||
// Front-to-back: fill uncovered strips behind the native
|
// Front-to-back: fill uncovered strips behind the native
|
||||||
// content instead of stretching it or exposing wallpaper.
|
// content instead of stretching it or exposing wallpaper.
|
||||||
elements.push(fill.into());
|
elements.push(fill.into());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.window_resize_fills.remove(&id);
|
state.render.clear_tint(id, true);
|
||||||
}
|
}
|
||||||
if let Some(surface) = material_surface
|
if let Some(surface) = material_surface
|
||||||
&& let Some((background, _)) = material_element(
|
&& let Some((background, _)) = material_element(
|
||||||
@@ -308,7 +290,7 @@ pub(crate) fn output_elements(
|
|||||||
surface,
|
surface,
|
||||||
MaterialSurface {
|
MaterialSurface {
|
||||||
geometry: constrain,
|
geometry: constrain,
|
||||||
radius: window_radius as f32,
|
corners,
|
||||||
index: 0,
|
index: 0,
|
||||||
capture_geometry: constrain,
|
capture_geometry: constrain,
|
||||||
alpha: close_alpha,
|
alpha: close_alpha,
|
||||||
@@ -387,7 +369,7 @@ pub(super) fn layer_elements(
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
state.material_buffers.retain(|surface, _| surface.is_alive());
|
state.render.surfaces.retain(|surface, _| surface.is_alive());
|
||||||
|
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
for (geometry, layer) in layers {
|
for (geometry, layer) in layers {
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ uniform float presentation_alpha;
|
|||||||
uniform float background_opacity;
|
uniform float background_opacity;
|
||||||
uniform vec4 tint;
|
uniform vec4 tint;
|
||||||
varying vec2 v_coords;
|
varying vec2 v_coords;
|
||||||
|
//_CORNERS_
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 half_size = visible_rect.zw * 0.5;
|
float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius);
|
||||||
vec2 d = abs(gl_FragCoord.xy - visible_rect.xy - half_size) - (half_size - vec2(material_radius));
|
float coverage = alpha * presentation_alpha * edge_coverage(sdf);
|
||||||
float sdf = length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - material_radius;
|
|
||||||
float coverage = alpha * presentation_alpha * (1.0 - smoothstep(-0.5, 0.5, sdf));
|
|
||||||
if (coverage <= 0.0) { gl_FragColor = vec4(0.0); return; }
|
if (coverage <= 0.0) { gl_FragColor = vec4(0.0); return; }
|
||||||
vec2 coords = (gl_FragCoord.xy - capture_origin) / texture_size;
|
vec2 coords = (gl_FragCoord.xy - capture_origin) / texture_size;
|
||||||
vec4 color = vec4(0.0);
|
vec4 color = vec4(0.0);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Coordinates and radius are physical pixels. Rust clamps the outer radius.
|
||||||
|
float rounded_rect_distance(vec2 point, vec4 rect, float radius) {
|
||||||
|
vec2 half_size = rect.zw * 0.5;
|
||||||
|
vec2 d = abs(point - rect.xy - half_size) - (half_size - vec2(radius));
|
||||||
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
float edge_coverage(float distance) {
|
||||||
|
// Match Iced/WGPU quad edges: a one-physical-pixel linear ramp.
|
||||||
|
return clamp(0.5 - distance, 0.0, 1.0);
|
||||||
|
}
|
||||||
@@ -9,17 +9,13 @@ uniform vec4 shadow_rect;
|
|||||||
uniform vec2 shadow_values;
|
uniform vec2 shadow_values;
|
||||||
varying vec2 v_coords;
|
varying vec2 v_coords;
|
||||||
|
|
||||||
float rounded_distance(vec2 point, vec4 rect) {
|
//_CORNERS_
|
||||||
vec2 half_size = rect.zw * 0.5;
|
|
||||||
float radius = min(material_radius, min(half_size.x, half_size.y));
|
|
||||||
vec2 d = abs(point - rect.xy - half_size) - (half_size - vec2(radius));
|
|
||||||
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0) - radius;
|
|
||||||
}
|
|
||||||
void main() {
|
void main() {
|
||||||
float sdf = rounded_distance(gl_FragCoord.xy, visible_rect);
|
float sdf = rounded_rect_distance(gl_FragCoord.xy, visible_rect, material_radius);
|
||||||
float coverage = 1.0 - smoothstep(-0.5, 0.5, sdf);
|
float coverage = edge_coverage(sdf);
|
||||||
if (paint_mode > 0.5) {
|
if (paint_mode > 0.5) {
|
||||||
float distance = max(rounded_distance(gl_FragCoord.xy, shadow_rect), 0.0);
|
float distance = max(rounded_rect_distance(gl_FragCoord.xy, shadow_rect, material_radius), 0.0);
|
||||||
float sigma = max(shadow_values.x * 0.5, 0.5);
|
float sigma = max(shadow_values.x * 0.5, 0.5);
|
||||||
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
float opacity = exp(-0.5 * distance * distance / (sigma * sigma))
|
||||||
* shadow_values.y * (1.0 - coverage) * alpha;
|
* shadow_values.y * (1.0 - coverage) * alpha;
|
||||||
|
|||||||
@@ -18,23 +18,16 @@ varying vec2 v_coords;
|
|||||||
uniform float tint;
|
uniform float tint;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void main() {
|
//_CORNERS_
|
||||||
vec2 point = gl_FragCoord.xy - clip_rect.xy;
|
|
||||||
vec2 half_size = clip_rect.zw * 0.5;
|
|
||||||
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
|
||||||
float signed_distance = length(max(distance, 0.0))
|
|
||||||
+ min(max(distance.x, distance.y), 0.0)
|
|
||||||
- radius;
|
|
||||||
float outer_coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance);
|
|
||||||
|
|
||||||
vec2 inner_half_size = max(half_size - vec2(border_width), vec2(0.0));
|
void main() {
|
||||||
|
float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
|
||||||
|
float outer_coverage = edge_coverage(signed_distance);
|
||||||
|
|
||||||
|
vec2 inner_size = max(clip_rect.zw - vec2(2.0 * border_width), vec2(0.0));
|
||||||
|
vec4 inner_rect = vec4(clip_rect.xy + (clip_rect.zw - inner_size) * 0.5, inner_size);
|
||||||
float inner_radius = max(radius - border_width, 0.0);
|
float inner_radius = max(radius - border_width, 0.0);
|
||||||
vec2 inner_distance = abs(point - half_size)
|
float inner_coverage = edge_coverage(rounded_rect_distance(gl_FragCoord.xy, inner_rect, inner_radius));
|
||||||
- (inner_half_size - vec2(inner_radius));
|
|
||||||
float inner_signed_distance = length(max(inner_distance, 0.0))
|
|
||||||
+ min(max(inner_distance.x, inner_distance.y), 0.0)
|
|
||||||
- inner_radius;
|
|
||||||
float inner_coverage = 1.0 - smoothstep(-0.5, 0.5, inner_signed_distance);
|
|
||||||
float coverage = max(outer_coverage - inner_coverage, 0.0);
|
float coverage = max(outer_coverage - inner_coverage, 0.0);
|
||||||
float progress = clamp(dot(gl_FragCoord.xy - gradient_line.xy, gradient_line.zw), 0.0, 1.0);
|
float progress = clamp(dot(gl_FragCoord.xy - gradient_line.xy, gradient_line.zw), 0.0, 1.0);
|
||||||
// Interpolate premultiplied endpoints: a transparent endpoint must not
|
// Interpolate premultiplied endpoints: a transparent endpoint must not
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
precision highp float;
|
||||||
|
uniform float alpha;
|
||||||
|
uniform vec4 clip_rect;
|
||||||
|
uniform float radius;
|
||||||
|
uniform vec4 color;
|
||||||
|
|
||||||
|
//_CORNERS_
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
|
||||||
|
// Single-pixel buffer colors are already premultiplied.
|
||||||
|
gl_FragColor = color * alpha * edge_coverage(distance);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ varying vec2 v_coords;
|
|||||||
uniform float tint;
|
uniform float tint;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
//_CORNERS_
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec4 color = texture2D(tex, v_coords);
|
vec4 color = texture2D(tex, v_coords);
|
||||||
|
|
||||||
@@ -31,13 +33,8 @@ void main() {
|
|||||||
color = color * alpha;
|
color = color * alpha;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
vec2 point = gl_FragCoord.xy - clip_rect.xy;
|
float signed_distance = rounded_rect_distance(gl_FragCoord.xy, clip_rect, radius);
|
||||||
vec2 half_size = clip_rect.zw * 0.5;
|
float coverage = edge_coverage(signed_distance);
|
||||||
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
|
||||||
float signed_distance = length(max(distance, 0.0))
|
|
||||||
+ min(max(distance.x, distance.y), 0.0)
|
|
||||||
- radius;
|
|
||||||
float coverage = 1.0 - smoothstep(-0.5, 0.5, signed_distance);
|
|
||||||
color *= coverage;
|
color *= coverage;
|
||||||
|
|
||||||
#if defined(DEBUG_FLAGS)
|
#if defined(DEBUG_FLAGS)
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ varying vec2 v_coords;
|
|||||||
uniform float tint;
|
uniform float tint;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
//_CORNERS_
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
vec2 point = gl_FragCoord.xy - shadow_rect.xy;
|
float signed_distance = rounded_rect_distance(gl_FragCoord.xy, shadow_rect, radius);
|
||||||
vec2 half_size = shadow_rect.zw * 0.5;
|
|
||||||
vec2 distance = abs(point - half_size) - (half_size - vec2(radius));
|
|
||||||
float signed_distance = length(max(distance, 0.0))
|
|
||||||
+ min(max(distance.x, distance.y), 0.0)
|
|
||||||
- radius;
|
|
||||||
float sigma = max(blur * 0.5, 0.5);
|
float sigma = max(blur * 0.5, 0.5);
|
||||||
float normalized_distance = max(signed_distance, 0.0) / sigma;
|
float normalized_distance = max(signed_distance, 0.0) / sigma;
|
||||||
float coverage = exp(-0.5 * normalized_distance * normalized_distance);
|
float coverage = exp(-0.5 * normalized_distance * normalized_distance);
|
||||||
|
|||||||
@@ -318,27 +318,25 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
|
fn workspace_snapshots(&self) -> Vec<WorkspaceSnapshot> {
|
||||||
self.workspaces
|
self.output_workspaces
|
||||||
.ordered()
|
.workspace_views(&self.workspaces)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|workspace| {
|
.map(|view| WorkspaceSnapshot {
|
||||||
let output = self.output_workspaces.output_for_workspace(workspace.id);
|
id: view.id,
|
||||||
let active =
|
output: Some(view.output),
|
||||||
output.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace.id));
|
name: view.index.to_string(),
|
||||||
|
index: view.index,
|
||||||
WorkspaceSnapshot {
|
window_count: view.window_count as u32,
|
||||||
id: workspace.id,
|
visible: view.visible,
|
||||||
output,
|
focused: view.focused,
|
||||||
name: workspace.name.clone(),
|
|
||||||
active,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
fn managed_window_snapshots(&self) -> Vec<ManagedWindowSnapshot> {
|
||||||
let mut snapshots = self
|
let mut snapshots = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
let workspace = self.workspaces.workspace_for_window(*id)?.0;
|
||||||
@@ -455,7 +453,10 @@ impl ShellSnapshot {
|
|||||||
output_hi,
|
output_hi,
|
||||||
output_lo,
|
output_lo,
|
||||||
workspace.name.clone(),
|
workspace.name.clone(),
|
||||||
u32::from(workspace.active),
|
workspace.index,
|
||||||
|
workspace.window_count,
|
||||||
|
u32::from(workspace.visible),
|
||||||
|
u32::from(workspace.focused),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for snapshot in &self.windows {
|
for snapshot in &self.windows {
|
||||||
@@ -498,7 +499,10 @@ struct WorkspaceSnapshot {
|
|||||||
id: WorkspaceId,
|
id: WorkspaceId,
|
||||||
output: Option<OutputId>,
|
output: Option<OutputId>,
|
||||||
name: String,
|
name: String,
|
||||||
active: bool,
|
index: u32,
|
||||||
|
window_count: u32,
|
||||||
|
visible: bool,
|
||||||
|
focused: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn split_id(id: u64) -> (u32, u32) {
|
fn split_id(id: u64) -> (u32, u32) {
|
||||||
@@ -558,7 +562,10 @@ mod tests {
|
|||||||
id: WorkspaceId(1),
|
id: WorkspaceId(1),
|
||||||
output: Some(OutputId(1)),
|
output: Some(OutputId(1)),
|
||||||
name: "1".into(),
|
name: "1".into(),
|
||||||
active: true,
|
index: 1,
|
||||||
|
window_count: 1,
|
||||||
|
visible: true,
|
||||||
|
focused: true,
|
||||||
}],
|
}],
|
||||||
windows: vec![ManagedWindowSnapshot {
|
windows: vec![ManagedWindowSnapshot {
|
||||||
id: WindowId(1),
|
id: WindowId(1),
|
||||||
@@ -574,7 +581,10 @@ mod tests {
|
|||||||
|s| s.outputs[0].active_workspace = WorkspaceId(2),
|
|s| s.outputs[0].active_workspace = WorkspaceId(2),
|
||||||
|s| s.outputs.clear(),
|
|s| s.outputs.clear(),
|
||||||
|s| s.workspaces[0].output = None,
|
|s| s.workspaces[0].output = None,
|
||||||
|s| s.workspaces[0].active = false,
|
|s| s.workspaces[0].visible = false,
|
||||||
|
|s| s.workspaces[0].focused = false,
|
||||||
|
|s| s.workspaces[0].index = 2,
|
||||||
|
|s| s.workspaces[0].window_count = 0,
|
||||||
|s| s.workspaces[0].name = "renamed".into(),
|
|s| s.workspaces[0].name = "renamed".into(),
|
||||||
|s| s.windows[0].workspace = 2,
|
|s| s.windows[0].workspace = 2,
|
||||||
|s| s.windows[0].title = "new title".into(),
|
|s| s.windows[0].title = "new title".into(),
|
||||||
|
|||||||
+39
-64
@@ -1,12 +1,15 @@
|
|||||||
mod animation;
|
mod animation;
|
||||||
mod floating;
|
mod floating;
|
||||||
|
mod focus;
|
||||||
mod hit_testing;
|
mod hit_testing;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod navigation;
|
mod navigation;
|
||||||
mod outputs;
|
mod outputs;
|
||||||
|
mod prediction;
|
||||||
|
pub(crate) use prediction::FrameScene;
|
||||||
|
mod window_registry;
|
||||||
mod windows;
|
mod windows;
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
@@ -26,7 +29,7 @@ use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
|||||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||||
use smithay::backend::drm::{DrmEventTime, DrmNode};
|
use smithay::backend::drm::{DrmEventTime, DrmNode};
|
||||||
use smithay::backend::input::Keycode;
|
use smithay::backend::input::Keycode;
|
||||||
use smithay::backend::renderer::{ErasedContextId, ImportDma};
|
use smithay::backend::renderer::ImportDma;
|
||||||
use smithay::desktop::space::SpaceElement;
|
use smithay::desktop::space::SpaceElement;
|
||||||
use smithay::desktop::utils::send_frames_surface_tree;
|
use smithay::desktop::utils::send_frames_surface_tree;
|
||||||
use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output};
|
use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output};
|
||||||
@@ -57,6 +60,7 @@ use smithay::wayland::presentation::PresentationState;
|
|||||||
use smithay::wayland::relative_pointer::RelativePointerManagerState;
|
use smithay::wayland::relative_pointer::RelativePointerManagerState;
|
||||||
use smithay::wayland::selection::data_device::DataDeviceState;
|
use smithay::wayland::selection::data_device::DataDeviceState;
|
||||||
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
|
use smithay::wayland::selection::primary_selection::PrimarySelectionState;
|
||||||
|
use smithay::wayland::selection::{ext_data_control, wlr_data_control};
|
||||||
use smithay::wayland::session_lock::SessionLockManagerState;
|
use smithay::wayland::session_lock::SessionLockManagerState;
|
||||||
use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState};
|
use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState};
|
||||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
|
use smithay::wayland::shell::xdg::decoration::XdgDecorationState;
|
||||||
@@ -69,6 +73,7 @@ use smithay::wayland::viewporter::ViewporterState;
|
|||||||
use smithay::wayland::xdg_activation::XdgActivationState;
|
use smithay::wayland::xdg_activation::XdgActivationState;
|
||||||
use smithay::wayland::xdg_foreign::XdgForeignState;
|
use smithay::wayland::xdg_foreign::XdgForeignState;
|
||||||
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
|
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager;
|
||||||
|
use window_registry::WindowRegistry;
|
||||||
|
|
||||||
use crate::backends::direct::DirectBackendState;
|
use crate::backends::direct::DirectBackendState;
|
||||||
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
|
use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
|
||||||
@@ -84,11 +89,6 @@ use crate::ipc::IpcSocketGuard;
|
|||||||
use crate::overview::OverviewState;
|
use crate::overview::OverviewState;
|
||||||
use crate::portal_session::PortalSession;
|
use crate::portal_session::PortalSession;
|
||||||
use crate::portal_shortcuts::PortalShortcuts;
|
use crate::portal_shortcuts::PortalShortcuts;
|
||||||
use crate::render::{
|
|
||||||
BlurProgram, MaterialBuffers, MaterialProgram, OverviewScrim, ResizeSnapshot, RoundedClipPrograms,
|
|
||||||
WindowBorderBuffers, WindowShadowBuffers,
|
|
||||||
};
|
|
||||||
use crate::resize_transaction::ResizeTransaction;
|
|
||||||
use crate::session_lock::{IdleSettings, Lock};
|
use crate::session_lock::{IdleSettings, Lock};
|
||||||
use crate::shell_control::ShellSnapshot;
|
use crate::shell_control::ShellSnapshot;
|
||||||
use crate::stacking::WindowStack;
|
use crate::stacking::WindowStack;
|
||||||
@@ -168,12 +168,14 @@ impl FocusSwipe {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct WorkspaceSlideItem {
|
struct WorkspaceSlideItem {
|
||||||
workspace: WorkspaceId,
|
workspace: WorkspaceId,
|
||||||
start: SlideOffset,
|
start: SlideOffset,
|
||||||
target: SlideOffset,
|
target: SlideOffset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct WorkspaceSlide {
|
struct WorkspaceSlide {
|
||||||
items: Vec<WorkspaceSlideItem>,
|
items: Vec<WorkspaceSlideItem>,
|
||||||
elapsed: Duration,
|
elapsed: Duration,
|
||||||
@@ -322,54 +324,32 @@ pub struct Ferese {
|
|||||||
pub output_workspaces: OutputWorkspaceMap,
|
pub output_workspaces: OutputWorkspaceMap,
|
||||||
pub(crate) output_ids: HashMap<Output, OutputId>,
|
pub(crate) output_ids: HashMap<Output, OutputId>,
|
||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub(crate) windows: WindowRegistry<Window>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
pub(crate) render: crate::render::RenderResources,
|
||||||
resize_transactions: HashMap<WindowId, ResizeTransaction>,
|
|
||||||
pub(crate) resize_snapshots: HashMap<WindowId, ResizeSnapshot>,
|
|
||||||
pub(crate) nested_backend: Option<NestedBackend>,
|
pub(crate) nested_backend: Option<NestedBackend>,
|
||||||
pub(crate) wallpaper: WallpaperState,
|
pub(crate) wallpaper: WallpaperState,
|
||||||
maximized_windows: HashSet<WindowId>,
|
|
||||||
maximized_column_widths: HashMap<WindowId, ColumnWidth>,
|
|
||||||
window_stack: WindowStack,
|
window_stack: WindowStack,
|
||||||
floating_above_fullscreen: HashMap<WindowId, WindowId>,
|
floating_above_fullscreen: HashMap<WindowId, WindowId>,
|
||||||
natural_floating_pending: HashSet<WindowId>,
|
|
||||||
floating_memory: crate::floating::Memory,
|
floating_memory: crate::floating::Memory,
|
||||||
floating_save_worker: crate::floating::SaveWorker,
|
floating_save_worker: crate::floating::SaveWorker,
|
||||||
floating_window_memory: HashMap<WindowId, crate::floating::Remembered>,
|
|
||||||
floating_placement_anchors: HashMap<WindowId, Rect>,
|
|
||||||
floating_cascade: crate::floating::Cascade,
|
floating_cascade: crate::floating::Cascade,
|
||||||
pub(crate) floating_resize_anchors: HashMap<WindowId, (bool, bool, Rect)>,
|
|
||||||
pub(crate) window_borders: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_dims: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_resize_fills: HashMap<WindowId, WindowBorderBuffers>,
|
|
||||||
pub(crate) window_dimming: HashMap<WindowId, DimAnimation>,
|
|
||||||
pub(crate) window_focus: HashMap<WindowId, DimAnimation>,
|
|
||||||
pub(crate) window_shadows: HashMap<WindowId, WindowShadowBuffers>,
|
|
||||||
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, RoundedClipPrograms>,
|
|
||||||
pub(crate) overview_scrims: HashMap<OutputId, OverviewScrim>,
|
|
||||||
pub(crate) material_programs: HashMap<ErasedContextId, MaterialProgram>,
|
|
||||||
pub(crate) material_buffers: HashMap<WlSurface, MaterialBuffers>,
|
|
||||||
pub(crate) portal_shortcuts: PortalShortcuts,
|
pub(crate) portal_shortcuts: PortalShortcuts,
|
||||||
pub(crate) pending_logout: Option<u32>,
|
pub(crate) pending_logout: Option<u32>,
|
||||||
pub(crate) logout_query: Option<u32>,
|
pub(crate) logout_query: Option<u32>,
|
||||||
pub(crate) logout_owner: Option<ObjectId>,
|
pub(crate) logout_owner: Option<ObjectId>,
|
||||||
pub(crate) blur_programs: HashMap<ErasedContextId, BlurProgram>,
|
|
||||||
pub(crate) backdrop_generation: u64,
|
pub(crate) backdrop_generation: u64,
|
||||||
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
|
||||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||||
focus_swipe: Option<FocusSwipe>,
|
focus_swipe: Option<FocusSwipe>,
|
||||||
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
||||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
|
||||||
viewport_coupled_widths: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
|
||||||
pending_column_width_cycles: HashSet<WindowId>,
|
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
|
pub(crate) focus_history: ferese_core::FocusHistory,
|
||||||
|
pub(crate) focus_cycle: Option<ferese_core::FocusCycle>,
|
||||||
column_width_presets: Vec<ColumnWidth>,
|
column_width_presets: Vec<ColumnWidth>,
|
||||||
gap_config: GapConfig,
|
gap_config: GapConfig,
|
||||||
pub(crate) input_settings: InputSettings,
|
pub(crate) input_settings: InputSettings,
|
||||||
pub(crate) bindings: Vec<Binding>,
|
pub(crate) bindings: Vec<Binding>,
|
||||||
workspace_auto_back_and_forth: bool,
|
workspace_auto_back_and_forth: bool,
|
||||||
window_rules: Vec<WindowRule>,
|
window_rules: Vec<WindowRule>,
|
||||||
window_rules_applied: HashSet<WindowId>,
|
|
||||||
pub(crate) theme_settings: ThemeSettings,
|
pub(crate) theme_settings: ThemeSettings,
|
||||||
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
pub(crate) inactive_dim: crate::config::InactiveDimSettings,
|
||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
@@ -404,7 +384,6 @@ pub struct Ferese {
|
|||||||
pub(crate) shell_snapshot_serial: u32,
|
pub(crate) shell_snapshot_serial: u32,
|
||||||
pub(crate) last_shell_snapshot: Option<ShellSnapshot>,
|
pub(crate) last_shell_snapshot: Option<ShellSnapshot>,
|
||||||
pub(crate) overview: crate::overview::OverviewState,
|
pub(crate) overview: crate::overview::OverviewState,
|
||||||
next_window_id: u64,
|
|
||||||
next_output_id: u64,
|
next_output_id: u64,
|
||||||
last_animation_tick: Instant,
|
last_animation_tick: Instant,
|
||||||
pub popups: PopupManager,
|
pub popups: PopupManager,
|
||||||
@@ -425,6 +404,8 @@ pub struct Ferese {
|
|||||||
pub pointer_constraints_state: PointerConstraintsState,
|
pub pointer_constraints_state: PointerConstraintsState,
|
||||||
pub presentation_state: PresentationState,
|
pub presentation_state: PresentationState,
|
||||||
pub primary_selection_state: PrimarySelectionState,
|
pub primary_selection_state: PrimarySelectionState,
|
||||||
|
pub wlr_data_control_state: wlr_data_control::DataControlState,
|
||||||
|
pub ext_data_control_state: ext_data_control::DataControlState,
|
||||||
pub relative_pointer_state: RelativePointerManagerState,
|
pub relative_pointer_state: RelativePointerManagerState,
|
||||||
pub seat_state: SeatState<Self>,
|
pub seat_state: SeatState<Self>,
|
||||||
pub shm_state: ShmState,
|
pub shm_state: ShmState,
|
||||||
@@ -501,6 +482,17 @@ impl Ferese {
|
|||||||
let presentation_state = PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
|
let presentation_state = PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
|
||||||
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
|
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
|
||||||
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
|
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
|
||||||
|
// Clipboard tools need direct selection access. Without data control,
|
||||||
|
// wl-copy maps a temporary window to obtain keyboard focus.
|
||||||
|
let wlr_data_control_state =
|
||||||
|
wlr_data_control::DataControlState::new::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
|
||||||
|
true
|
||||||
|
});
|
||||||
|
let ext_data_control_state =
|
||||||
|
ext_data_control::DataControlState::new::<Self, _>(&display_handle, Some(&primary_selection_state), |_| {
|
||||||
|
true
|
||||||
|
});
|
||||||
|
|
||||||
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
|
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
|
||||||
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
|
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
|
||||||
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
|
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
|
||||||
@@ -552,56 +544,34 @@ impl Ferese {
|
|||||||
output_workspaces: OutputWorkspaceMap::default(),
|
output_workspaces: OutputWorkspaceMap::default(),
|
||||||
output_ids: HashMap::new(),
|
output_ids: HashMap::new(),
|
||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
windows: WindowRegistry::default(),
|
||||||
window_geometry: HashMap::new(),
|
render: Default::default(),
|
||||||
resize_transactions: HashMap::new(),
|
|
||||||
resize_snapshots: HashMap::new(),
|
|
||||||
nested_backend: None,
|
nested_backend: None,
|
||||||
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
|
wallpaper: WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())),
|
||||||
maximized_windows: HashSet::new(),
|
|
||||||
maximized_column_widths: HashMap::new(),
|
|
||||||
window_stack: WindowStack::default(),
|
window_stack: WindowStack::default(),
|
||||||
floating_above_fullscreen: HashMap::new(),
|
floating_above_fullscreen: HashMap::new(),
|
||||||
natural_floating_pending: HashSet::new(),
|
|
||||||
floating_memory: crate::floating::Memory::path()
|
floating_memory: crate::floating::Memory::path()
|
||||||
.map(|p| crate::floating::Memory::load(&p))
|
.map(|p| crate::floating::Memory::load(&p))
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
floating_save_worker: Default::default(),
|
floating_save_worker: Default::default(),
|
||||||
floating_window_memory: HashMap::new(),
|
|
||||||
floating_placement_anchors: HashMap::new(),
|
|
||||||
floating_cascade: Default::default(),
|
floating_cascade: Default::default(),
|
||||||
floating_resize_anchors: HashMap::new(),
|
|
||||||
window_borders: HashMap::new(),
|
|
||||||
window_dims: HashMap::new(),
|
|
||||||
window_resize_fills: HashMap::new(),
|
|
||||||
window_dimming: HashMap::new(),
|
|
||||||
window_focus: HashMap::new(),
|
|
||||||
window_shadows: HashMap::new(),
|
|
||||||
rounded_clip_programs: HashMap::new(),
|
|
||||||
overview_scrims: HashMap::new(),
|
|
||||||
material_programs: HashMap::new(),
|
|
||||||
material_buffers: HashMap::new(),
|
|
||||||
portal_shortcuts: PortalShortcuts::default(),
|
portal_shortcuts: PortalShortcuts::default(),
|
||||||
pending_logout: None,
|
pending_logout: None,
|
||||||
logout_query: None,
|
logout_query: None,
|
||||||
logout_owner: None,
|
logout_owner: None,
|
||||||
blur_programs: HashMap::new(),
|
|
||||||
backdrop_generation: 0,
|
backdrop_generation: 0,
|
||||||
closing_windows: HashMap::new(),
|
|
||||||
viewport_animations: HashMap::new(),
|
viewport_animations: HashMap::new(),
|
||||||
focus_swipe: None,
|
focus_swipe: None,
|
||||||
workspace_slides: HashMap::new(),
|
workspace_slides: HashMap::new(),
|
||||||
scrolling_world_x: HashMap::new(),
|
|
||||||
viewport_coupled_widths: HashMap::new(),
|
|
||||||
pending_column_width_cycles: HashSet::new(),
|
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
|
focus_history: Default::default(),
|
||||||
|
focus_cycle: None,
|
||||||
column_width_presets: config.column_width_presets,
|
column_width_presets: config.column_width_presets,
|
||||||
gap_config: config.gap_config,
|
gap_config: config.gap_config,
|
||||||
input_settings: config.input_settings,
|
input_settings: config.input_settings,
|
||||||
bindings: config.bindings,
|
bindings: config.bindings,
|
||||||
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
|
workspace_auto_back_and_forth: config.workspace_auto_back_and_forth,
|
||||||
window_rules: config.window_rules,
|
window_rules: config.window_rules,
|
||||||
window_rules_applied: HashSet::new(),
|
|
||||||
theme_settings: config.theme_settings,
|
theme_settings: config.theme_settings,
|
||||||
inactive_dim: config.inactive_dim,
|
inactive_dim: config.inactive_dim,
|
||||||
animations_enabled: config.animations_enabled,
|
animations_enabled: config.animations_enabled,
|
||||||
@@ -631,7 +601,6 @@ impl Ferese {
|
|||||||
shell_snapshot_serial: 0,
|
shell_snapshot_serial: 0,
|
||||||
last_shell_snapshot: None,
|
last_shell_snapshot: None,
|
||||||
overview: OverviewState::with_font_family(config.overview_font_family),
|
overview: OverviewState::with_font_family(config.overview_font_family),
|
||||||
next_window_id: 1,
|
|
||||||
next_output_id: 1,
|
next_output_id: 1,
|
||||||
last_animation_tick: start_time,
|
last_animation_tick: start_time,
|
||||||
popups: PopupManager::default(),
|
popups: PopupManager::default(),
|
||||||
@@ -652,6 +621,8 @@ impl Ferese {
|
|||||||
pointer_constraints_state,
|
pointer_constraints_state,
|
||||||
presentation_state,
|
presentation_state,
|
||||||
primary_selection_state,
|
primary_selection_state,
|
||||||
|
wlr_data_control_state,
|
||||||
|
ext_data_control_state,
|
||||||
relative_pointer_state,
|
relative_pointer_state,
|
||||||
seat_state,
|
seat_state,
|
||||||
shm_state,
|
shm_state,
|
||||||
@@ -778,12 +749,16 @@ impl Ferese {
|
|||||||
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
self.pending_dmabuf_imports.push((dmabuf, notifier));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn process_dmabuf_imports<R>(&mut self, renderer: &mut R)
|
pub(crate) fn process_dmabuf_imports<R>(&mut self, renderer: &mut R, node: Option<DrmNode>)
|
||||||
where
|
where
|
||||||
R: ImportDma,
|
R: ImportDma,
|
||||||
{
|
{
|
||||||
for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) {
|
for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) {
|
||||||
if renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
if renderer.import_dmabuf(&dmabuf, None).is_ok() {
|
||||||
|
if let Some(node) = node {
|
||||||
|
dmabuf.set_node(node);
|
||||||
|
}
|
||||||
|
|
||||||
if let Err(error) = notifier.successful::<Self>() {
|
if let Err(error) = notifier.successful::<Self>() {
|
||||||
tracing::debug!(?error, "dma-buf client disappeared before import completed");
|
tracing::debug!(?error, "dma-buf client disappeared before import completed");
|
||||||
}
|
}
|
||||||
@@ -1061,7 +1036,7 @@ mod tests {
|
|||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
let first = workspaces.active_id();
|
let first = workspaces.active_id();
|
||||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
let second = workspaces.ensure_numeric(2).unwrap();
|
let second = workspaces.create_workspace();
|
||||||
workspaces.activate(second).unwrap();
|
workspaces.activate(second).unwrap();
|
||||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap();
|
||||||
@@ -1072,7 +1047,7 @@ mod tests {
|
|||||||
.connect(OutputId(1), OutputGeometry::new(0, 0, 1920, 1080), first)
|
.connect(OutputId(1), OutputGeometry::new(0, 0, 1920, 1080), first)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let owner = if assignment == Some(OutputId(2)) {
|
let owner = if assignment == Some(OutputId(2)) {
|
||||||
let third = workspaces.ensure_numeric(3).unwrap();
|
let third = workspaces.create_workspace();
|
||||||
outputs
|
outputs
|
||||||
.connect(OutputId(2), OutputGeometry::new(1920, 0, 1920, 1080), third)
|
.connect(OutputId(2), OutputGeometry::new(1920, 0, 1920, 1080), third)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.focus_swipe = None;
|
self.focus_swipe = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible_workspaces = if self.viewport_animations.is_empty() {
|
let visible_workspaces = if self.viewport_animations.is_empty() {
|
||||||
HashSet::new()
|
HashSet::new()
|
||||||
} else {
|
} else {
|
||||||
@@ -38,10 +39,11 @@ impl Ferese {
|
|||||||
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
.filter_map(|output| self.output_workspaces.active_workspace(*output))
|
||||||
.collect::<HashSet<_>>()
|
.collect::<HashSet<_>>()
|
||||||
};
|
};
|
||||||
|
|
||||||
let windows = self
|
let windows = self
|
||||||
.space
|
.space
|
||||||
.elements()
|
.elements()
|
||||||
.filter_map(|window| self.window_ids.get(window).copied().map(|id| (window.clone(), id)))
|
.filter_map(|window| self.windows.ids().get(window).copied().map(|id| (window.clone(), id)))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut active_animation = false;
|
let mut active_animation = false;
|
||||||
let mut completed_slides = Vec::new();
|
let mut completed_slides = Vec::new();
|
||||||
@@ -49,16 +51,19 @@ impl Ferese {
|
|||||||
if slide.held_progress.is_some() {
|
if slide.held_progress.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if slide.advance(delta) {
|
if slide.advance(delta) {
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
} else {
|
} else {
|
||||||
completed_slides.push(*output);
|
completed_slides.push(*output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
|
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
|
||||||
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
|
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
|
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
|
||||||
let active = motion.advance_visual(0.0, delta, duration);
|
let active = motion.advance_visual(0.0, delta, duration);
|
||||||
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
|
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
|
||||||
@@ -75,69 +80,72 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut dim_changed = false;
|
let mut dim_changed = false;
|
||||||
// Include hidden workspace windows: overview can present them too.
|
// Include hidden workspace windows: overview can present them too.
|
||||||
// Read selection once so these immutable fields can be borrowed alongside
|
// Read selection once so these immutable fields can be borrowed alongside
|
||||||
// window_focus without allocating a temporary window-ID vector.
|
// each record without allocating a temporary window-ID vector.
|
||||||
let selected_window = if self.overview.is_active() {
|
let selected_window = if self.overview.is_active() {
|
||||||
self.overview.selected()
|
self.overview.selected()
|
||||||
} else {
|
} else {
|
||||||
self.focused_window
|
self.focused_window
|
||||||
};
|
};
|
||||||
for id in self.window_ids.values().copied() {
|
|
||||||
|
for (&id, record) in self.windows.records_mut() {
|
||||||
let selected = selected_window == Some(id);
|
let selected = selected_window == Some(id);
|
||||||
let target = if selected { 1.0 } else { 0.0 };
|
let target = if selected { 1.0 } else { 0.0 };
|
||||||
let focus = self
|
let focus = record
|
||||||
.window_focus
|
.focus
|
||||||
.entry(id)
|
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
|
||||||
let previous = focus.current;
|
let previous = focus.current;
|
||||||
active_animation |= focus.advance_visual(target, delta, duration);
|
active_animation |= focus.advance_visual(target, delta, duration);
|
||||||
dim_changed |= previous != focus.current;
|
dim_changed |= previous != focus.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_, id) in &windows {
|
for (_, id) in &windows {
|
||||||
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
|
let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting());
|
||||||
let dim = self
|
let Some(record) = self.windows.record_mut(*id) else {
|
||||||
.window_dimming
|
continue;
|
||||||
.entry(*id)
|
};
|
||||||
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
|
||||||
|
let dim = record
|
||||||
|
.dimming
|
||||||
|
.get_or_insert_with(|| crate::dimming::DimAnimation::new(target));
|
||||||
let previous = dim.current;
|
let previous = dim.current;
|
||||||
active_animation |= dim.advance_visual(target, delta, duration);
|
active_animation |= dim.advance_visual(target, delta, duration);
|
||||||
dim_changed |= previous != dim.current;
|
dim_changed |= previous != dim.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut ready_to_close = Vec::new();
|
let mut ready_to_close = Vec::new();
|
||||||
for (id, animation) in &mut self.closing_windows {
|
for (id, record) in self.windows.records_mut() {
|
||||||
|
let Some(animation) = &mut record.closing else { continue };
|
||||||
|
|
||||||
if animation.advance(delta, self.animations_enabled) {
|
if animation.advance(delta, self.animations_enabled) {
|
||||||
ready_to_close.push(*id);
|
ready_to_close.push(*id);
|
||||||
} else if !animation.close_sent {
|
} else if !animation.close_sent {
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in ready_to_close {
|
for id in ready_to_close {
|
||||||
self.send_window_close(id);
|
self.send_window_close(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = self.start_time.elapsed();
|
let now = self.start_time.elapsed();
|
||||||
self.resize_transactions.retain(|id, transaction| {
|
self.windows.expire_transactions(now);
|
||||||
if transaction.expired(now) {
|
|
||||||
tracing::warn!(?id, "resize presentation deadline reached");
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let blocked_workspaces = self
|
let blocked_workspaces = self
|
||||||
.resize_transactions
|
.windows
|
||||||
.keys()
|
.resizing()
|
||||||
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
let animations_enabled = self.animations_enabled;
|
let animations_enabled = self.animations_enabled;
|
||||||
let animation_speed = self.animation_speed;
|
let animation_speed = self.animation_speed;
|
||||||
self.resize_snapshots.retain(|id, snapshot| {
|
self.render.retain_snapshots(|id, snapshot| {
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let waiting_for_client = self
|
let waiting_for_client = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace_for_window(*id)
|
.workspace_for_window(*id)
|
||||||
@@ -145,7 +153,7 @@ impl Ferese {
|
|||||||
// A shrinking client's destination buffer arrives before the
|
// A shrinking client's destination buffer arrives before the
|
||||||
// animated bounds reach it. Keep the old native pixels covering
|
// animated bounds reach it. Keep the old native pixels covering
|
||||||
// that strip rather than fading them into the neutral resize fill.
|
// that strip rather than fading them into the neutral resize fill.
|
||||||
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
|
let uncovered = self.windows.geometry(id).is_some_and(|geometry| {
|
||||||
geometry.client.committed_size.is_some_and(|size| {
|
geometry.client.committed_size.is_some_and(|size| {
|
||||||
crate::presentation::resize_needs_old_frame(
|
crate::presentation::resize_needs_old_frame(
|
||||||
geometry.visual.current,
|
geometry.visual.current,
|
||||||
@@ -166,21 +174,24 @@ impl Ferese {
|
|||||||
if !blocked {
|
if !blocked {
|
||||||
snapshot.commit.increment();
|
snapshot.commit.increment();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !active {
|
if !active {
|
||||||
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
|
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
|
||||||
}
|
}
|
||||||
active
|
active
|
||||||
});
|
});
|
||||||
active_animation |= !self.resize_snapshots.is_empty();
|
active_animation |= self.render.snapshots().next().is_some();
|
||||||
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
||||||
active_animation |= !blocked_workspaces.is_empty();
|
active_animation |= !blocked_workspaces.is_empty();
|
||||||
for (workspace, viewport) in &mut self.viewport_animations {
|
for (workspace, viewport) in &mut self.viewport_animations {
|
||||||
if !visible_workspaces.contains(workspace) {
|
if !visible_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if blocked_workspaces.contains(workspace) {
|
if blocked_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
||||||
viewport.current = swipe.position();
|
viewport.current = swipe.position();
|
||||||
viewport.velocity = 0.0;
|
viewport.velocity = 0.0;
|
||||||
@@ -200,12 +211,19 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
|
||||||
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let natural_pending = record.natural_floating_pending;
|
||||||
|
let Some(geometry) = record.geometry.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
let zooming = geometry.is_zooming();
|
let zooming = geometry.is_zooming();
|
||||||
|
|
||||||
let coupled_target = self.viewport_coupled_widths.get(&id).map(|(_, width)| width.target);
|
let coupled_target = record.coupled_width.as_ref().map(|(_, width)| width.target);
|
||||||
if coupled_target.is_some() {
|
if coupled_target.is_some() {
|
||||||
geometry.visual.target.width = geometry.visual.current.width;
|
geometry.visual.target.width = geometry.visual.current.width;
|
||||||
geometry.visual.velocity.width = 0.0;
|
geometry.visual.velocity.width = 0.0;
|
||||||
@@ -214,7 +232,8 @@ impl Ferese {
|
|||||||
if let Some(target) = coupled_target {
|
if let Some(target) = coupled_target {
|
||||||
geometry.visual.target.width = target;
|
geometry.visual.target.width = target;
|
||||||
}
|
}
|
||||||
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
|
||||||
|
if let Some((workspace, world_x)) = record.world_x.as_mut()
|
||||||
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
||||||
{
|
{
|
||||||
if zooming {
|
if zooming {
|
||||||
@@ -225,12 +244,14 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
world_x.snap();
|
world_x.snap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !zooming {
|
if !zooming {
|
||||||
geometry.visual.current.x = world_x.current - viewport.current;
|
geometry.visual.current.x = world_x.current - viewport.current;
|
||||||
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
|
|
||||||
|
if let Some((_, width)) = record.coupled_width.as_mut() {
|
||||||
let width_active = if self.animations_enabled {
|
let width_active = if self.animations_enabled {
|
||||||
width.advance(delta, self.viewport_spring_config)
|
width.advance(delta, self.viewport_spring_config)
|
||||||
} else {
|
} else {
|
||||||
@@ -244,6 +265,7 @@ impl Ferese {
|
|||||||
settled_coupled_widths.push(id);
|
settled_coupled_widths.push(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(size) = geometry.client.expire_wait(now) {
|
if let Some(size) = geometry.client.expire_wait(now) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
?id,
|
?id,
|
||||||
@@ -253,8 +275,9 @@ impl Ferese {
|
|||||||
"client did not commit the final configured size within 500 ms"
|
"client did not commit the final configured size within 500 ms"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(size) = geometry.presentation_size_request(now)
|
if let Some(size) = geometry.presentation_size_request(now)
|
||||||
&& !self.natural_floating_pending.contains(&id)
|
&& !natural_pending
|
||||||
&& let Some(toplevel) = window.toplevel()
|
&& let Some(toplevel) = window.toplevel()
|
||||||
{
|
{
|
||||||
toplevel.with_pending_state(|state| {
|
toplevel.with_pending_state(|state| {
|
||||||
@@ -262,20 +285,25 @@ impl Ferese {
|
|||||||
});
|
});
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
|
|
||||||
let visual = geometry.visual.current;
|
let visual = geometry.visual.current;
|
||||||
self.space
|
self.space
|
||||||
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
|
.map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
for id in settled_coupled_widths {
|
for id in settled_coupled_widths {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.windows.update(id, |record| record.coupled_width = None);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !completed_slides.is_empty() {
|
if !completed_slides.is_empty() {
|
||||||
for output in completed_slides {
|
for output in completed_slides {
|
||||||
self.workspace_slides.remove(&output);
|
self.workspace_slides.remove(&output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let visible = self.visible_workspace_ids();
|
let visible = self.visible_workspace_ids();
|
||||||
let visible_windows = self
|
let visible_windows = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.values()
|
.values()
|
||||||
.copied()
|
.copied()
|
||||||
.filter(|id| {
|
.filter(|id| {
|
||||||
@@ -285,7 +313,7 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
.collect::<HashSet<_>>();
|
.collect::<HashSet<_>>();
|
||||||
self.unmap_invisible_windows(&visible_windows);
|
self.unmap_invisible_windows(&visible_windows);
|
||||||
self.prune_empty_workspaces(&visible);
|
self.reconcile_workspaces(&visible);
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
}
|
}
|
||||||
@@ -308,19 +336,15 @@ impl Ferese {
|
|||||||
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
|
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
|
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
|
||||||
let Some(animation) = self.closing_windows.get(&id) else {
|
self.workspace_slide_offset_at(window, Duration::ZERO)
|
||||||
return (1.0, 1.0);
|
|
||||||
};
|
|
||||||
let eased = smoothstep(animation.progress);
|
|
||||||
|
|
||||||
(1.0 - eased * 0.02, (1.0 - eased) as f32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
|
pub(super) fn workspace_slide_offset_at(&self, window: WindowId, delta: Duration) -> (f64, f64) {
|
||||||
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(output_id) = self
|
let Some(output_id) = self
|
||||||
.workspace_slides
|
.workspace_slides
|
||||||
.iter()
|
.iter()
|
||||||
@@ -329,9 +353,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
let Some(slide) = self.workspace_slides.get(&output_id) else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(size) = self
|
let Some(size) = self
|
||||||
.output_ids
|
.output_ids
|
||||||
.iter()
|
.iter()
|
||||||
@@ -341,6 +367,12 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return (0.0, 0.0);
|
return (0.0, 0.0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut slide = slide.clone();
|
||||||
|
if slide.held_progress.is_none() && !delta.is_zero() {
|
||||||
|
slide.advance(delta);
|
||||||
|
}
|
||||||
|
|
||||||
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
|
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ impl Ferese {
|
|||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
if let Some(handle) = self
|
if let Some(handle) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
|
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
|
||||||
{
|
{
|
||||||
@@ -75,7 +76,7 @@ impl Ferese {
|
|||||||
.workspace_for_window(window)
|
.workspace_for_window(window)
|
||||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
.is_some_and(|workspace| workspace.fullscreen == Some(window));
|
||||||
let enabled = fullscreen || !self.maximized_windows.contains(&window);
|
let enabled = fullscreen || !self.windows.record(window).is_some_and(|w| w.maximized);
|
||||||
self.set_window_maximized(window, enabled);
|
self.set_window_maximized(window, enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,9 +86,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
if enabled {
|
if enabled {
|
||||||
let _ = self.workspaces.set_fullscreen(window, false);
|
let _ = self.workspaces.set_fullscreen(window, false);
|
||||||
self.maximized_windows.insert(window);
|
self.windows.update(window, |w| w.maximized = true);
|
||||||
} else {
|
} else {
|
||||||
self.maximized_windows.remove(&window);
|
self.windows.update(window, |w| w.maximized = false);
|
||||||
}
|
}
|
||||||
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
||||||
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
||||||
@@ -96,10 +97,16 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
if enabled {
|
if enabled {
|
||||||
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
|
if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) {
|
||||||
self.maximized_column_widths.entry(window).or_insert(column.width);
|
self.windows.update(window, |w| {
|
||||||
|
w.maximized_column_width.get_or_insert(column.width);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
|
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
|
||||||
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
|
} else if let Some(width) = self
|
||||||
|
.windows
|
||||||
|
.update(window, |w| w.maximized_column_width.take())
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
let _ = layout.set_column_width(window, width);
|
let _ = layout.set_column_width(window, width);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +115,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
|
||||||
!self.overview.is_presenting()
|
self.output_has_fullscreen_for_frame(output, self.overview.is_presenting())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn output_has_fullscreen_for_frame(&self, output: &Output, overview: bool) -> bool {
|
||||||
|
!overview
|
||||||
&& self
|
&& self
|
||||||
.output_id(output)
|
.output_id(output)
|
||||||
.and_then(|id| self.output_workspaces.active_workspace(id))
|
.and_then(|id| self.output_workspaces.active_workspace(id))
|
||||||
@@ -127,7 +138,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_floating_window(&self, window: &Window) -> bool {
|
pub fn is_floating_window(&self, window: &Window) -> bool {
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.get(window)
|
.get(window)
|
||||||
.and_then(|id| self.workspaces.placement(*id))
|
.and_then(|id| self.workspaces.placement(*id))
|
||||||
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
|
.is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. }))
|
||||||
@@ -139,10 +151,10 @@ impl Ferese {
|
|||||||
location: Point<i32, Logical>,
|
location: Point<i32, Logical>,
|
||||||
size: Size<i32, Logical>,
|
size: Size<i32, Logical>,
|
||||||
) {
|
) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.floating_resize_anchors.remove(&id);
|
self.windows.update(id, |w| w.resize_anchor.take()).flatten();
|
||||||
let rect = Rect::new(
|
let rect = Rect::new(
|
||||||
location.x as f64,
|
location.x as f64,
|
||||||
location.y as f64,
|
location.y as f64,
|
||||||
@@ -157,12 +169,15 @@ impl Ferese {
|
|||||||
|
|
||||||
// Direct manipulation follows the pointer, including while the client is
|
// Direct manipulation follows the pointer, including while the client is
|
||||||
// still drawing its next buffer. Rendering and hit testing share this rect.
|
// still drawing its next buffer. Rendering and hit testing share this rect.
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.resize_snapshots.remove(&id);
|
self.render.clear_snapshot(&id);
|
||||||
let geometry = self
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
.window_geometry
|
return;
|
||||||
.entry(id)
|
};
|
||||||
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
let geometry = record
|
||||||
|
.geometry
|
||||||
|
.get_or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
||||||
|
|
||||||
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
||||||
&& let Some(toplevel) = window.toplevel()
|
&& let Some(toplevel) = window.toplevel()
|
||||||
{
|
{
|
||||||
@@ -171,8 +186,8 @@ impl Ferese {
|
|||||||
});
|
});
|
||||||
toplevel.send_pending_configure();
|
toplevel.send_pending_configure();
|
||||||
}
|
}
|
||||||
self.scrolling_world_x.remove(&id);
|
self.windows.update(id, |record| record.world_x = None);
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.windows.update(id, |record| record.coupled_width = None);
|
||||||
self.space.map_element(window.clone(), location, false);
|
self.space.map_element(window.clone(), location, false);
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
|
||||||
@@ -212,7 +227,7 @@ impl Ferese {
|
|||||||
self.space
|
self.space
|
||||||
.elements()
|
.elements()
|
||||||
.filter_map(|window| {
|
.filter_map(|window| {
|
||||||
let id = *self.window_ids.get(window)?;
|
let id = *self.windows.ids().get(window)?;
|
||||||
if Some(id) == exclude {
|
if Some(id) == exclude {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -255,7 +270,7 @@ impl Ferese {
|
|||||||
let parent_rect = parent
|
let parent_rect = parent
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|surface| {
|
.and_then(|surface| {
|
||||||
self.window_ids.iter().find_map(|(window, id)| {
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
window
|
window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.is_some_and(|top| top.wl_surface() == surface)
|
.is_some_and(|top| top.wl_surface() == surface)
|
||||||
@@ -266,7 +281,7 @@ impl Ferese {
|
|||||||
.and_then(|rect| intersection(rect, work));
|
.and_then(|rect| intersection(rect, work));
|
||||||
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
|
let dialog = crate::window_rules::is_native_dialog(app.as_deref());
|
||||||
let remembered = id
|
let remembered = id
|
||||||
.and_then(|id| self.floating_window_memory.get(&id))
|
.and_then(|id| self.windows.record(id).and_then(|w| w.floating_memory.as_ref()))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
if parent.is_none() {
|
if parent.is_none() {
|
||||||
app.as_deref().and_then(|app| self.floating_memory.get(app))
|
app.as_deref().and_then(|app| self.floating_memory.get(app))
|
||||||
@@ -317,7 +332,7 @@ impl Ferese {
|
|||||||
.map(|(_, r)| r)
|
.map(|(_, r)| r)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let anchor = id
|
let anchor = id
|
||||||
.and_then(|id| self.floating_placement_anchors.get(&id).copied())
|
.and_then(|id| self.windows.record(id).and_then(|w| w.placement_anchor))
|
||||||
.or_else(|| {
|
.or_else(|| {
|
||||||
self.focused_window
|
self.focused_window
|
||||||
.filter(|focused| Some(*focused) != id)
|
.filter(|focused| Some(*focused) != id)
|
||||||
@@ -337,7 +352,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remember_floating(&mut self, window: &Window) {
|
pub(crate) fn remember_floating(&mut self, window: &Window) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
|
||||||
@@ -360,7 +375,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let entry = crate::floating::Remembered::new(output, rect, work);
|
let entry = crate::floating::Remembered::new(output, rect, work);
|
||||||
self.floating_window_memory.insert(id, entry.clone());
|
self.windows.update(id, |w| w.floating_memory = Some(entry.clone()));
|
||||||
if let Some(app) = app {
|
if let Some(app) = app {
|
||||||
self.floating_memory.save(app, entry);
|
self.floating_memory.save(app, entry);
|
||||||
}
|
}
|
||||||
@@ -377,7 +392,7 @@ impl Ferese {
|
|||||||
window: &Window,
|
window: &Window,
|
||||||
raw: Rect,
|
raw: Rect,
|
||||||
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
|
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
|
||||||
let id = self.window_ids.get(window).copied();
|
let id = self.windows.ids().get(window).copied();
|
||||||
let work = self
|
let work = self
|
||||||
.floating_outputs()
|
.floating_outputs()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub(super) fn focus_candidates(&self) -> Vec<WindowId> {
|
||||||
|
self.windows
|
||||||
|
.ids()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(window, id)| {
|
||||||
|
self.workspaces.workspace_for_window(*id)?;
|
||||||
|
(window.toplevel().is_some()
|
||||||
|
&& self.window_content_ready(window)
|
||||||
|
&& self.windows.geometry(id).is_some()
|
||||||
|
&& self.output_workspaces.focused_output().is_some()
|
||||||
|
&& !self.windows.record(*id).is_some_and(|record| record.closing.is_some()))
|
||||||
|
.then_some(*id)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_preview_output(&self, id: WindowId) -> Option<OutputId> {
|
||||||
|
if self.windows.record(id).is_some_and(|record| record.closing.is_some()) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace))
|
||||||
|
.or_else(|| self.output_workspaces.focused_output())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn focus_last_window(&mut self) {
|
||||||
|
if self.session_lock.active || self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.cancel_focus_cycle();
|
||||||
|
let target = self
|
||||||
|
.focus_history
|
||||||
|
.candidates(self.focused_window, self.focus_candidates())
|
||||||
|
.into_iter()
|
||||||
|
.find(|id| Some(*id) != self.focused_window);
|
||||||
|
if let Some(id) = target {
|
||||||
|
self.activate_managed_window(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cycle_focus(&mut self, reverse: bool, preview: bool) {
|
||||||
|
if self.session_lock.active || self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let available = self.focus_candidates();
|
||||||
|
if !preview {
|
||||||
|
self.cancel_focus_cycle();
|
||||||
|
let order = self.focus_history.candidates(self.focused_window, available.clone());
|
||||||
|
let mut cycle = ferese_core::FocusCycle::new(order, self.focused_window);
|
||||||
|
if let Some(id) = cycle.advance(reverse, &available) {
|
||||||
|
self.activate_managed_window(id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if self.focus_cycle.is_none() {
|
||||||
|
let order = self.focus_history.candidates(self.focused_window, available.clone());
|
||||||
|
if order.len() < 2 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.focus_cycle = Some(ferese_core::FocusCycle::new(order, self.focused_window));
|
||||||
|
if self.overview.is_active() {
|
||||||
|
self.retarget_overview();
|
||||||
|
} else {
|
||||||
|
self.set_overview_active(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(id) = self
|
||||||
|
.focus_cycle
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|cycle| cycle.advance(reverse, &available))
|
||||||
|
{
|
||||||
|
self.overview.select_window(id);
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
} else {
|
||||||
|
self.cancel_focus_cycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn finish_focus_cycle(&mut self) {
|
||||||
|
let Some(mut cycle) = self.focus_cycle.take() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !self.session_lock.active
|
||||||
|
&& !self.input_capture.captures(1)
|
||||||
|
&& let Some(id) = cycle.reconcile(&self.focus_candidates())
|
||||||
|
{
|
||||||
|
self.activate_managed_window(id);
|
||||||
|
}
|
||||||
|
self.set_overview_active(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn cancel_focus_cycle(&mut self) {
|
||||||
|
if self.focus_cycle.take().is_some() {
|
||||||
|
self.set_overview_active(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ impl Ferese {
|
|||||||
|
|
||||||
let workspace = self.workspace_under_pointer(position)?;
|
let workspace = self.workspace_under_pointer(position)?;
|
||||||
self.space.elements().rev().find_map(|window| {
|
self.space.elements().rev().find_map(|window| {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -71,7 +71,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
|
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
|
||||||
self.maximized_windows.contains(&id)
|
self.windows.record(id).is_some_and(|w| w.maximized)
|
||||||
|| self
|
|| self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace(workspace)
|
.workspace(workspace)
|
||||||
@@ -137,7 +137,7 @@ impl Ferese {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -166,9 +166,9 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if overview_active {
|
if overview_active {
|
||||||
let mut candidates = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
let mut candidates = self.windows.ids().keys().cloned().collect::<Vec<_>>();
|
||||||
// Match Overview's front-to-back render order during overlapping motion.
|
// Match Overview's front-to-back render order during overlapping motion.
|
||||||
candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0)));
|
candidates.sort_by_key(|window| std::cmp::Reverse(self.windows.ids().get(window).map_or(0, |id| id.0)));
|
||||||
candidates.iter().find_map(hit)
|
candidates.iter().find_map(hit)
|
||||||
} else {
|
} else {
|
||||||
self.space.elements().rev().find_map(hit)
|
self.space.elements().rev().find_map(hit)
|
||||||
@@ -184,6 +184,9 @@ impl Ferese {
|
|||||||
let Some(output_id) = self.output_id(output) else {
|
let Some(output_id) = self.output_id(output) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if self.focus_cycle.is_some() && self.overview.is_active() {
|
||||||
|
return self.overview.has_window_preview(window) && self.focus_preview_output(window) == Some(output_id);
|
||||||
|
}
|
||||||
let workspace = self.workspaces.workspace_for_window(window);
|
let workspace = self.workspaces.workspace_for_window(window);
|
||||||
workspace == self.output_workspaces.active_workspace(output_id)
|
workspace == self.output_workspaces.active_workspace(output_id)
|
||||||
|| self
|
|| self
|
||||||
@@ -196,8 +199,8 @@ impl Ferese {
|
|||||||
if !self.overview.is_presenting() {
|
if !self.overview.is_presenting() {
|
||||||
return Some((1.0, 1.0));
|
return Some((1.0, 1.0));
|
||||||
}
|
}
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let geometry = self.window_geometry.get(id)?;
|
let geometry = self.windows.geometry(id)?;
|
||||||
let source = geometry.client.committed_size?;
|
let source = geometry.client.committed_size?;
|
||||||
let presented = self.presented_window_rect(*id)?;
|
let presented = self.presented_window_rect(*id)?;
|
||||||
|
|
||||||
@@ -212,7 +215,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
|
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.windows.ids().get(window)?;
|
||||||
let rect = self.presented_window_rect(*id)?;
|
let rect = self.presented_window_rect(*id)?;
|
||||||
let size = ClientSize::from_rect(rect);
|
let size = ClientSize::from_rect(rect);
|
||||||
Some(Rectangle::new(
|
Some(Rectangle::new(
|
||||||
@@ -224,9 +227,10 @@ impl Ferese {
|
|||||||
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
|
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
|
||||||
window_has_buffer(window)
|
window_has_buffer(window)
|
||||||
&& self
|
&& self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(window)
|
.get(window)
|
||||||
.and_then(|id| self.window_geometry.get(id))
|
.and_then(|id| self.windows.geometry(id))
|
||||||
.is_some_and(|geometry| geometry.client.committed_size.is_some())
|
.is_some_and(|geometry| geometry.client.committed_size.is_some())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,27 +15,36 @@ impl Ferese {
|
|||||||
visible
|
visible
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn prune_empty_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
|
pub(super) fn reconcile_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
|
||||||
let mut protected = visible.clone();
|
for workspace in self
|
||||||
protected.extend(self.output_workspaces.history_workspaces());
|
.output_workspaces
|
||||||
for workspace in self.workspaces.prune_empty(&protected) {
|
.reconcile_workspaces(&mut self.workspaces, visible)
|
||||||
self.output_workspaces.forget_workspace(workspace);
|
{
|
||||||
self.viewport_animations.remove(&workspace);
|
self.viewport_animations.remove(&workspace);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
|
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for (window, id) in &self.window_ids {
|
let windows = self
|
||||||
if visible.contains(id) {
|
.windows
|
||||||
|
.ids()
|
||||||
|
.iter()
|
||||||
|
.map(|(window, id)| (window.clone(), *id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for (window, id) in windows {
|
||||||
|
if visible.contains(&id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let was_mapped = self.space.element_location(window).is_some();
|
|
||||||
|
let was_mapped = self.space.element_location(&window).is_some();
|
||||||
changed |= was_mapped;
|
changed |= was_mapped;
|
||||||
if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) {
|
if was_mapped && let Some(geometry) = self.windows.geometry_mut(&id) {
|
||||||
geometry.settle_presentation();
|
geometry.settle_presentation();
|
||||||
}
|
}
|
||||||
self.space.unmap_elem(window);
|
|
||||||
|
self.space.unmap_elem(&window);
|
||||||
}
|
}
|
||||||
changed
|
changed
|
||||||
}
|
}
|
||||||
@@ -43,7 +52,7 @@ impl Ferese {
|
|||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
self.refresh_input_capture_zones();
|
self.refresh_input_capture_zones();
|
||||||
let visible_workspaces = self.visible_workspace_ids();
|
let visible_workspaces = self.visible_workspace_ids();
|
||||||
self.prune_empty_workspaces(&visible_workspaces);
|
self.reconcile_workspaces(&visible_workspaces);
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
self.configure_lock_surfaces();
|
self.configure_lock_surfaces();
|
||||||
}
|
}
|
||||||
@@ -51,8 +60,8 @@ impl Ferese {
|
|||||||
// large first animation step after a keypress on an idle desktop.
|
// large first animation step after a keypress on an idle desktop.
|
||||||
self.advance_animations(Instant::now());
|
self.advance_animations(Instant::now());
|
||||||
self.arrange_layers();
|
self.arrange_layers();
|
||||||
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
|
let mut previous_scrolling_world_x = self.windows.take_world_positions();
|
||||||
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
|
let pending_column_width_cycles = self.windows.take_column_width_requests();
|
||||||
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
|
||||||
let constraints = self.window_constraints();
|
let constraints = self.window_constraints();
|
||||||
let mut visible = HashSet::new();
|
let mut visible = HashSet::new();
|
||||||
@@ -63,6 +72,7 @@ impl Ferese {
|
|||||||
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
let Some(output_id) = self.output_ids.get(&output).copied() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
|
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -80,13 +90,16 @@ impl Ferese {
|
|||||||
let Some(bounds) = self.output_bounds_for(&output) else {
|
let Some(bounds) = self.output_bounds_for(&output) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
|
||||||
for layer in layer_map_for_output(&output).layers() {
|
for layer in layer_map_for_output(&output).layers() {
|
||||||
crate::handlers::set_surface_tree_output(layer.wl_surface(), &output);
|
crate::handlers::set_surface_tree_output(layer.wl_surface(), &output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let workspace_fullscreen = workspace.fullscreen;
|
let workspace_fullscreen = workspace.fullscreen;
|
||||||
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
|
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
|
||||||
let workspace_focus = workspace.last_focused;
|
let workspace_focus = workspace.last_focused;
|
||||||
@@ -97,6 +110,7 @@ impl Ferese {
|
|||||||
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let layout =
|
let layout =
|
||||||
match workspace
|
match workspace
|
||||||
.layout
|
.layout
|
||||||
@@ -134,15 +148,17 @@ impl Ferese {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (window, id) in &self.window_ids {
|
for (window, id) in self.windows.ids() {
|
||||||
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
if self.workspaces.workspace_for_window(*id) != Some(workspace_id) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(toplevel) = window.toplevel() {
|
if let Some(toplevel) = window.toplevel() {
|
||||||
crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output);
|
crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output);
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
|
let is_maximized =
|
||||||
|
self.windows.record(*id).is_some_and(|w| w.maximized) && workspace_fullscreen != Some(*id);
|
||||||
let rect = if workspace_fullscreen == Some(*id) {
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
fullscreen_bounds
|
fullscreen_bounds
|
||||||
} else if is_maximized && !is_scrolling_layout {
|
} else if is_maximized && !is_scrolling_layout {
|
||||||
@@ -166,6 +182,7 @@ impl Ferese {
|
|||||||
None => continue,
|
None => continue,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let is_fullscreen = workspace_fullscreen == Some(*id);
|
let is_fullscreen = workspace_fullscreen == Some(*id);
|
||||||
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
|
let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }));
|
||||||
|
|
||||||
@@ -195,19 +212,25 @@ impl Ferese {
|
|||||||
|
|
||||||
let now = self.start_time.elapsed();
|
let now = self.start_time.elapsed();
|
||||||
|
|
||||||
let mut scrolling_world_x = HashMap::new();
|
|
||||||
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
||||||
|
|
||||||
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements {
|
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements {
|
||||||
let was_mapped = self.space.element_location(&window).is_some();
|
let was_mapped = self.space.element_location(&window).is_some();
|
||||||
|
let Some(record) = self.windows.record_mut(id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
// A viewport-coupled width must never override fullscreen/floating geometry.
|
// A viewport-coupled width must never override fullscreen/floating geometry.
|
||||||
if scrolling.is_none() {
|
if scrolling.is_none() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
let (geometry, had_geometry) = match self.window_geometry.entry(id) {
|
|
||||||
Entry::Occupied(entry) => (entry.into_mut(), true),
|
let natural_pending = record.natural_floating_pending && is_floating && !is_fullscreen && !is_maximized;
|
||||||
Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false),
|
let had_geometry = record.geometry.is_some();
|
||||||
};
|
let geometry = record
|
||||||
|
.geometry
|
||||||
|
.get_or_insert_with(|| WindowGeometry::new(rect, client_size(&window)));
|
||||||
|
|
||||||
let mode = if is_fullscreen {
|
let mode = if is_fullscreen {
|
||||||
PresentationMode::Fullscreen
|
PresentationMode::Fullscreen
|
||||||
} else if is_maximized {
|
} else if is_maximized {
|
||||||
@@ -220,13 +243,16 @@ impl Ferese {
|
|||||||
if !was_mapped && (had_geometry || is_fullscreen) {
|
if !was_mapped && (had_geometry || is_fullscreen) {
|
||||||
geometry.settle_presentation();
|
geometry.settle_presentation();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
geometry.advance(Duration::ZERO, self.spring_config, false);
|
||||||
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
requested_size = geometry.presentation_size_request(now).or(requested_size);
|
||||||
}
|
}
|
||||||
|
|
||||||
if geometry.is_zooming() {
|
if geometry.is_zooming() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
||||||
let restored_world_x =
|
let restored_world_x =
|
||||||
restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x);
|
restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x);
|
||||||
@@ -239,23 +265,23 @@ impl Ferese {
|
|||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
animated_world_x.snap();
|
animated_world_x.snap();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !geometry.is_zooming() {
|
if !geometry.is_zooming() {
|
||||||
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
||||||
geometry.visual.velocity.x = animated_world_x.velocity;
|
geometry.visual.velocity.x = animated_world_x.velocity;
|
||||||
}
|
}
|
||||||
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
record.world_x = Some((workspace, animated_world_x));
|
||||||
|
|
||||||
let coupled = !geometry.is_zooming()
|
let coupled = !geometry.is_zooming()
|
||||||
&& (couple_width
|
&& (couple_width
|
||||||
|| self
|
|| record
|
||||||
.viewport_coupled_widths
|
.coupled_width
|
||||||
.get(&id)
|
.as_ref()
|
||||||
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace));
|
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace));
|
||||||
if coupled {
|
if coupled {
|
||||||
let width = self
|
let width = record
|
||||||
.viewport_coupled_widths
|
.coupled_width
|
||||||
.entry(id)
|
.get_or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width)));
|
||||||
.or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width)));
|
|
||||||
if width.0 != workspace {
|
if width.0 != workspace {
|
||||||
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
|
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
|
||||||
}
|
}
|
||||||
@@ -266,15 +292,15 @@ impl Ferese {
|
|||||||
geometry.visual.current.width = width.1.current;
|
geometry.visual.current.width = width.1.current;
|
||||||
geometry.visual.velocity.width = width.1.velocity;
|
geometry.visual.velocity.width = width.1.velocity;
|
||||||
} else if pending_column_width_cycles.contains(&id) {
|
} else if pending_column_width_cycles.contains(&id) {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
record.coupled_width = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let visual = geometry.visual.current;
|
let visual = geometry.visual.current;
|
||||||
let natural_pending =
|
|
||||||
self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized;
|
|
||||||
if natural_pending {
|
if natural_pending {
|
||||||
requested_size = None;
|
requested_size = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
let location = (visual.x.round() as i32, visual.y.round() as i32);
|
||||||
|
|
||||||
self.space.map_element(window.clone(), location, false);
|
self.space.map_element(window.clone(), location, false);
|
||||||
@@ -297,6 +323,7 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
state.states.unset(xdg_toplevel::State::Maximized)
|
state.states.unset(xdg_toplevel::State::Maximized)
|
||||||
};
|
};
|
||||||
|
|
||||||
let tiled = !is_floating && !is_fullscreen;
|
let tiled = !is_floating && !is_fullscreen;
|
||||||
let tiled_changed = if tiled {
|
let tiled_changed = if tiled {
|
||||||
state.states.set(xdg_toplevel::State::TiledLeft)
|
state.states.set(xdg_toplevel::State::TiledLeft)
|
||||||
@@ -319,7 +346,7 @@ impl Ferese {
|
|||||||
&& had_geometry
|
&& had_geometry
|
||||||
&& self.animations_enabled
|
&& self.animations_enabled
|
||||||
{
|
{
|
||||||
self.resize_transactions.insert(
|
self.windows.set_transaction(
|
||||||
id,
|
id,
|
||||||
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
crate::resize_transaction::ResizeTransaction::new(serial, now)
|
||||||
.with_source_geometry(window.geometry()),
|
.with_source_geometry(window.geometry()),
|
||||||
@@ -327,10 +354,11 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.scrolling_world_x = scrolling_world_x;
|
|
||||||
if layout_changed {
|
if layout_changed {
|
||||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
self.retarget_overview();
|
self.retarget_overview();
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
@@ -339,9 +367,10 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
|
||||||
if let Some(id) = self.window_ids.get(window) {
|
if let Some(id) = self.windows.ids().get(window) {
|
||||||
self.window_stack.raise(*id);
|
self.window_stack.raise(*id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.raise_element(window, activate);
|
self.space.raise_element(window, activate);
|
||||||
self.sync_window_stacking();
|
self.sync_window_stacking();
|
||||||
}
|
}
|
||||||
@@ -349,8 +378,8 @@ impl Ferese {
|
|||||||
pub(crate) fn sync_window_stacking(&mut self) {
|
pub(crate) fn sync_window_stacking(&mut self) {
|
||||||
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
|
||||||
windows.sort_by_key(|window| {
|
windows.sort_by_key(|window| {
|
||||||
let id = self.window_ids.get(window).copied();
|
let id = self.windows.ids().get(window).copied();
|
||||||
let geometry = id.and_then(|id| self.window_geometry.get(&id));
|
let geometry = id.and_then(|id| self.windows.geometry(&id));
|
||||||
let floating =
|
let floating =
|
||||||
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
|
id.is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })));
|
||||||
let priority = crate::stacking::layer_priority(
|
let priority = crate::stacking::layer_priority(
|
||||||
@@ -372,6 +401,7 @@ impl Ferese {
|
|||||||
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for window in windows {
|
for window in windows {
|
||||||
self.space.raise_element(&window, false);
|
self.space.raise_element(&window, false);
|
||||||
}
|
}
|
||||||
@@ -387,7 +417,8 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||||
self.window_ids
|
self.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(window, id)| {
|
.filter_map(|(window, id)| {
|
||||||
let toplevel = window.toplevel()?;
|
let toplevel = window.toplevel()?;
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ impl Ferese {
|
|||||||
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
|
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
|
||||||
}
|
}
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
.find_map(|(window, id)| (*id == next).then(|| window.clone()))
|
||||||
else {
|
else {
|
||||||
@@ -277,7 +278,7 @@ impl Ferese {
|
|||||||
|
|
||||||
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
|
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
self.pending_column_width_cycles.insert(window);
|
self.windows.update(window, |w| w.column_width_pending = true);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
Ok(false) => {}
|
Ok(false) => {}
|
||||||
@@ -306,7 +307,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
let Some(window) = self
|
let Some(window) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
|
||||||
else {
|
else {
|
||||||
@@ -392,16 +394,13 @@ impl Ferese {
|
|||||||
slide_direction: Option<SwipeDirection>,
|
slide_direction: Option<SwipeDirection>,
|
||||||
auto_back_and_forth: bool,
|
auto_back_and_forth: bool,
|
||||||
) {
|
) {
|
||||||
let workspace = match self.workspaces.ensure_numeric(index) {
|
|
||||||
Ok(workspace) => workspace,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(%error, index, "failed to switch workspace");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let Some(output) = self.output_workspaces.focused_output() else {
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let previous = self.output_workspaces.active_workspace(output);
|
let previous = self.output_workspaces.active_workspace(output);
|
||||||
let owner = match self
|
let owner = match self
|
||||||
.output_workspaces
|
.output_workspaces
|
||||||
@@ -524,27 +523,24 @@ impl Ferese {
|
|||||||
let Some(window) = self.focused_window else {
|
let Some(window) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let Some(output) = self.output_workspaces.focused_output() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(destination) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
let axis = self
|
let axis = self
|
||||||
.output_bounds()
|
.output_bounds()
|
||||||
.and_then(|bounds| {
|
.and_then(|bounds| {
|
||||||
let workspace = self.workspaces.ensure_numeric(index).ok()?;
|
let target = self.workspaces.workspace(destination)?;
|
||||||
let target = self.workspaces.workspace(workspace)?;
|
|
||||||
target.layout.automatic_axis(target.last_focused, bounds).ok()
|
target.layout.automatic_axis(target.last_focused, bounds).ok()
|
||||||
})
|
})
|
||||||
.unwrap_or(Axis::Horizontal);
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
let destination = match self.workspaces.move_window_to_numeric(window, index, axis, 0.5) {
|
if let Err(error) = self.workspaces.move_window_to_workspace(window, destination, axis, 0.5) {
|
||||||
Ok(destination) => destination,
|
|
||||||
Err(error) => {
|
|
||||||
tracing::error!(%error, ?window, index, "failed to move window to workspace");
|
tracing::error!(%error, ?window, index, "failed to move window to workspace");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
if let Some(output) = self.output_workspaces.focused_output()
|
|
||||||
&& let Err(error) = self.output_workspaces.assign_workspace(output, destination)
|
|
||||||
{
|
|
||||||
tracing::error!(%error, ?destination, "failed to assign destination workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.focused_window = self.workspaces.active().last_focused;
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
self.relayout();
|
self.relayout();
|
||||||
@@ -560,7 +556,7 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let surface = self.focused_window.and_then(|focused| {
|
let surface = self.focused_window.and_then(|focused| {
|
||||||
self.window_ids.iter().find_map(|(window, id)| {
|
self.windows.ids().iter().find_map(|(window, id)| {
|
||||||
(*id == focused)
|
(*id == focused)
|
||||||
.then(|| window.toplevel())
|
.then(|| window.toplevel())
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|||||||
@@ -15,25 +15,23 @@ impl Ferese {
|
|||||||
self.next_output_id = self.next_output_id.saturating_add(1);
|
self.next_output_id = self.next_output_id.saturating_add(1);
|
||||||
id
|
id
|
||||||
});
|
});
|
||||||
let fallback_workspace = (1..)
|
let fallback_workspace = self
|
||||||
.find_map(|index| {
|
.workspaces
|
||||||
let workspace = self.workspaces.ensure_numeric(index).ok()?;
|
.iter()
|
||||||
self.output_workspaces
|
.filter(|workspace| self.output_workspaces.output_for_workspace(workspace.id).is_none())
|
||||||
.output_for_workspace(workspace)
|
.map(|workspace| workspace.id)
|
||||||
.is_none()
|
.min_by_key(|id| id.0)
|
||||||
.then_some(workspace)
|
.unwrap_or_else(|| self.workspaces.create_workspace());
|
||||||
})
|
|
||||||
.expect("numeric workspaces are inexhaustible");
|
|
||||||
let geometry = OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h);
|
let geometry = OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h);
|
||||||
|
|
||||||
let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace);
|
let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace);
|
||||||
|
|
||||||
match registration {
|
match registration {
|
||||||
Ok(workspace) => {
|
Ok(_) => {
|
||||||
self.output_ids.insert(output.clone(), output_id);
|
self.output_ids.insert(output.clone(), output_id);
|
||||||
if self.output_workspaces.focused_output() == Some(output_id) {
|
self.restore_output_focus();
|
||||||
self.activate_output_workspace(output_id, workspace);
|
let visible = self.visible_workspace_ids();
|
||||||
}
|
self.reconcile_workspaces(&visible);
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -48,7 +46,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.workspace_slides.remove(&output_id);
|
self.workspace_slides.remove(&output_id);
|
||||||
self.overview_scrims.remove(&output_id);
|
self.render.remove_output(output_id);
|
||||||
self.pending_screencopies.retain(|capture| {
|
self.pending_screencopies.retain(|capture| {
|
||||||
if capture.output == *output {
|
if capture.output == *output {
|
||||||
capture.fail();
|
capture.fail();
|
||||||
@@ -82,7 +80,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
|
pub(crate) fn reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
|
||||||
let floats = self
|
let floats = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|id| {
|
.filter_map(|id| {
|
||||||
let workspace = self.workspaces.workspace_for_window(*id)?;
|
let workspace = self.workspaces.workspace_for_window(*id)?;
|
||||||
|
|||||||
@@ -0,0 +1,335 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// Output-local presentation values. Sampling never changes authoritative
|
||||||
|
/// geometry, configure barriers, animation clocks, or another output's view.
|
||||||
|
pub(crate) struct FrameScene {
|
||||||
|
pub windows: HashMap<WindowId, WindowFrame>,
|
||||||
|
pub overview: crate::overview::OverviewMotion,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WindowFrame {
|
||||||
|
pub geometry: WindowGeometry,
|
||||||
|
pub rect: Rect,
|
||||||
|
pub close_scale: f64,
|
||||||
|
pub close_alpha: f32,
|
||||||
|
pub focus: f64,
|
||||||
|
pub dim: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ferese {
|
||||||
|
pub(crate) fn output_has_animations(&self, output: &Output) -> bool {
|
||||||
|
if self.overview.is_animating(self.spring_config) || !self.dismissing_popups.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.output_id(output)
|
||||||
|
.and_then(|id| self.workspace_slides.get(&id))
|
||||||
|
.is_some_and(|slide| slide.held_progress.is_none() && slide.elapsed < WORKSPACE_SLIDE_DURATION)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.windows.records().any(|(id, record)| {
|
||||||
|
let Some(mut geometry) = record.geometry else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !self.window_belongs_to_output(*id, output) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.resize.is_some()
|
||||||
|
|| self.render.snapshot(id).is_some()
|
||||||
|
|| record.closing.as_ref().is_some_and(|close| !close.close_sent)
|
||||||
|
|| record.focus.as_ref().is_some_and(|focus| focus.is_animating())
|
||||||
|
|| record.dimming.as_ref().is_some_and(|dim| dim.is_animating())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(workspace) = self.workspaces.workspace_for_window(*id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.viewport_animations.get(&workspace).is_some_and(|viewport| {
|
||||||
|
let mut viewport = *viewport;
|
||||||
|
let held = self
|
||||||
|
.focus_swipe
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|swipe| swipe.workspace == workspace);
|
||||||
|
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
|
}) || record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
||||||
|
let mut width = *width;
|
||||||
|
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene {
|
||||||
|
let delta = if self.animations_enabled && self.output_has_animations(output) {
|
||||||
|
horizon.min(Duration::from_millis(100)).mul_f64(self.animation_speed)
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
};
|
||||||
|
|
||||||
|
let overview = self.overview.sample(delta, self.spring_config);
|
||||||
|
let blocked = self
|
||||||
|
.windows
|
||||||
|
.resizing()
|
||||||
|
.filter_map(|id| self.workspaces.workspace_for_window(*id))
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
let mut windows = HashMap::new();
|
||||||
|
|
||||||
|
for (&id, record) in self.windows.records() {
|
||||||
|
let Some(original) = record.geometry else { continue };
|
||||||
|
|
||||||
|
if !self.window_belongs_to_output(id, output) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace = self.workspaces.workspace_for_window(id);
|
||||||
|
let world = record.world_x.as_ref().and_then(|(workspace, world)| {
|
||||||
|
let viewport = self.viewport_animations.get(workspace)?;
|
||||||
|
let held = self
|
||||||
|
.focus_swipe
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|swipe| swipe.workspace == *workspace);
|
||||||
|
|
||||||
|
Some((*world, *viewport, held))
|
||||||
|
});
|
||||||
|
let width = record.coupled_width.as_ref().map(|(_, width)| *width);
|
||||||
|
let geometry = if delta.is_zero() || workspace.is_some_and(|workspace| blocked.contains(&workspace)) {
|
||||||
|
original
|
||||||
|
} else {
|
||||||
|
predict_geometry(
|
||||||
|
original,
|
||||||
|
delta,
|
||||||
|
self.spring_config,
|
||||||
|
self.viewport_spring_config,
|
||||||
|
world,
|
||||||
|
width,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rect = overview.presented_rect(id, geometry.visual.current);
|
||||||
|
if !overview.is_presenting() {
|
||||||
|
let (x, y) = self.workspace_slide_offset_at(id, delta);
|
||||||
|
rect.x += x;
|
||||||
|
rect.y += y;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut close = record.closing.as_ref().copied().unwrap_or_default();
|
||||||
|
if record.closing.is_some() && !delta.is_zero() {
|
||||||
|
close.advance(delta, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let eased = smoothstep(close.progress);
|
||||||
|
let focused = if self.overview.is_active() {
|
||||||
|
self.overview_selected(id)
|
||||||
|
} else {
|
||||||
|
self.focused_window == Some(id)
|
||||||
|
};
|
||||||
|
|
||||||
|
let sample_dim = |motion: Option<&DimAnimation>, fallback| {
|
||||||
|
let Some(mut motion) = motion.cloned() else {
|
||||||
|
return fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !delta.is_zero() {
|
||||||
|
motion.predict(delta, self.inactive_dim.duration_ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
motion.current
|
||||||
|
};
|
||||||
|
|
||||||
|
windows.insert(
|
||||||
|
id,
|
||||||
|
WindowFrame {
|
||||||
|
geometry,
|
||||||
|
rect,
|
||||||
|
close_scale: 1.0 - eased * 0.02,
|
||||||
|
close_alpha: (1.0 - eased) as f32,
|
||||||
|
focus: sample_dim(record.focus.as_ref(), if focused { 1.0 } else { 0.0 }),
|
||||||
|
dim: sample_dim(record.dimming.as_ref(), 0.0),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameScene { windows, overview }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn predict_geometry(
|
||||||
|
original: WindowGeometry,
|
||||||
|
delta: Duration,
|
||||||
|
spring: SpringConfig,
|
||||||
|
viewport_spring: SpringConfig,
|
||||||
|
world: Option<(AnimatedValue, AnimatedValue, bool)>,
|
||||||
|
width: Option<AnimatedValue>,
|
||||||
|
) -> WindowGeometry {
|
||||||
|
let mut predicted = original;
|
||||||
|
let zooming = predicted.is_zooming();
|
||||||
|
if width.is_some() {
|
||||||
|
predicted.visual.target.width = predicted.visual.current.width;
|
||||||
|
predicted.visual.velocity.width = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
predicted.advance(delta, spring, true);
|
||||||
|
predicted.visual.target.width = original.visual.target.width;
|
||||||
|
if let Some((mut world, mut viewport, held)) = world
|
||||||
|
&& !zooming
|
||||||
|
{
|
||||||
|
world.advance(delta, spring);
|
||||||
|
if !held {
|
||||||
|
viewport.advance(delta, viewport_spring);
|
||||||
|
}
|
||||||
|
|
||||||
|
predicted.visual.current.x = world.current - viewport.current;
|
||||||
|
predicted.visual.velocity.x = world.velocity - viewport.velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(mut width) = width {
|
||||||
|
width.advance(delta, viewport_spring);
|
||||||
|
predicted.visual.current.width = width.current;
|
||||||
|
predicted.visual.velocity.width = width.velocity;
|
||||||
|
}
|
||||||
|
|
||||||
|
predicted
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||||
|
fn frame_sampling_preserves_compositor_state_between_output_deadlines() {
|
||||||
|
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||||
|
assert!(
|
||||||
|
runtime.starts_with(std::env::temp_dir()),
|
||||||
|
"use a disposable runtime directory"
|
||||||
|
);
|
||||||
|
let mut event_loop = EventLoop::try_new().unwrap();
|
||||||
|
let display = Display::new().unwrap();
|
||||||
|
let config = crate::config::Config::default().runtime_config().unwrap();
|
||||||
|
let mut state = Ferese::new(&mut event_loop, display, config).unwrap();
|
||||||
|
let output = Output::new(
|
||||||
|
"forecast-test".into(),
|
||||||
|
smithay::output::PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: smithay::output::Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(smithay::output::Mode {
|
||||||
|
size: (1920, 1080).into(),
|
||||||
|
refresh: 60_000,
|
||||||
|
}),
|
||||||
|
Some(smithay::utils::Transform::Normal),
|
||||||
|
None,
|
||||||
|
Some((0, 0).into()),
|
||||||
|
);
|
||||||
|
state.space.map_output(&output, (0, 0));
|
||||||
|
state.register_output(&output, "forecast-test".into());
|
||||||
|
let id = WindowId(1);
|
||||||
|
state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap();
|
||||||
|
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
|
||||||
|
geometry.visual.set_target(Rect::new(600., 0., 400., 300.));
|
||||||
|
// This test isolates frame sampling from the protocol client fixture.
|
||||||
|
state.windows.records.insert(
|
||||||
|
id,
|
||||||
|
super::super::window_registry::WindowRecord {
|
||||||
|
geometry: Some(geometry),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let tick = state.last_animation_tick;
|
||||||
|
for horizon in [
|
||||||
|
Duration::from_millis(16),
|
||||||
|
Duration::from_millis(4),
|
||||||
|
Duration::from_millis(8),
|
||||||
|
] {
|
||||||
|
{
|
||||||
|
let forecast = state.sample_frame(&output, horizon);
|
||||||
|
assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(*state.windows.geometry(&id).unwrap(), geometry);
|
||||||
|
assert_eq!(state.last_animation_tick, tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A prediction must neither advance nor expire a client resize barrier.
|
||||||
|
state.windows.set_transaction(
|
||||||
|
id,
|
||||||
|
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
|
||||||
|
);
|
||||||
|
let blocked = state.sample_frame(&output, Duration::from_millis(16));
|
||||||
|
assert_eq!(blocked.windows[&id].geometry, geometry);
|
||||||
|
assert!(state.windows.transaction(&id).is_some());
|
||||||
|
state.windows.clear_transaction(&id);
|
||||||
|
|
||||||
|
state.animations_enabled = false;
|
||||||
|
let forecast = state.sample_frame(&output, Duration::from_millis(16));
|
||||||
|
assert_eq!(forecast.windows[&id].geometry, geometry);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mixed_refresh_forecasts_do_not_change_authoritative_geometry_or_client_state() {
|
||||||
|
let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None);
|
||||||
|
geometry.visual.set_target(Rect::new(600., 0., 500., 300.));
|
||||||
|
let original = geometry;
|
||||||
|
let sample = |delta| {
|
||||||
|
predict_geometry(
|
||||||
|
geometry,
|
||||||
|
delta,
|
||||||
|
SpringConfig::default(),
|
||||||
|
SpringConfig::default(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let fast = sample(Duration::from_nanos(1_000_000_000 / 240));
|
||||||
|
let slow = sample(Duration::from_nanos(1_000_000_000 / 60));
|
||||||
|
assert!(fast.visual.current.x > 0.);
|
||||||
|
assert!(slow.visual.current.x > fast.visual.current.x);
|
||||||
|
assert_eq!(sample(Duration::from_nanos(1_000_000_000 / 240)), fast);
|
||||||
|
assert_eq!(geometry, original);
|
||||||
|
assert_eq!(slow.client, original.client);
|
||||||
|
assert_eq!(slow.logical, original.logical);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scrolling_coordinates_and_coupled_width_use_the_same_forecast_time() {
|
||||||
|
let geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None);
|
||||||
|
let mut world = AnimatedValue::new(500.);
|
||||||
|
world.set_target(600.);
|
||||||
|
let mut viewport = AnimatedValue::new(0.);
|
||||||
|
viewport.set_target(200.);
|
||||||
|
let mut width = AnimatedValue::new(400.);
|
||||||
|
width.set_target(500.);
|
||||||
|
let spring = SpringConfig::default();
|
||||||
|
let delta = Duration::from_millis(10);
|
||||||
|
let predicted = predict_geometry(
|
||||||
|
geometry,
|
||||||
|
delta,
|
||||||
|
spring,
|
||||||
|
spring,
|
||||||
|
Some((world, viewport, false)),
|
||||||
|
Some(width),
|
||||||
|
);
|
||||||
|
world.advance(delta, spring);
|
||||||
|
viewport.advance(delta, spring);
|
||||||
|
width.advance(delta, spring);
|
||||||
|
assert_eq!(predicted.visual.current.x, world.current - viewport.current);
|
||||||
|
assert_eq!(predicted.visual.current.width, width.current);
|
||||||
|
assert_eq!(predicted.visual.target, geometry.visual.target);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
use ferese_animation::{AnimatedValue, WindowGeometry};
|
||||||
|
use ferese_core::WorkspaceId;
|
||||||
|
use ferese_layout::{ColumnWidth, Rect, WindowId};
|
||||||
|
|
||||||
|
use crate::resize_transaction::ResizeTransaction;
|
||||||
|
|
||||||
|
/// Metadata with exactly the lifetime of a managed window. Layouts and focus
|
||||||
|
/// history reference its ID; they do not own or duplicate this metadata.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct WindowRecord {
|
||||||
|
pub geometry: Option<WindowGeometry>,
|
||||||
|
pub resize: Option<ResizeTransaction>,
|
||||||
|
pub focus: Option<crate::dimming::DimAnimation>,
|
||||||
|
pub dimming: Option<crate::dimming::DimAnimation>,
|
||||||
|
pub(super) closing: Option<super::ClosingAnimation>,
|
||||||
|
pub world_x: Option<(WorkspaceId, AnimatedValue)>,
|
||||||
|
pub coupled_width: Option<(WorkspaceId, AnimatedValue)>,
|
||||||
|
pub maximized: bool,
|
||||||
|
pub maximized_column_width: Option<ColumnWidth>,
|
||||||
|
pub natural_floating_pending: bool,
|
||||||
|
pub floating_memory: Option<crate::floating::Remembered>,
|
||||||
|
pub placement_anchor: Option<Rect>,
|
||||||
|
pub resize_anchor: Option<(bool, bool, Rect)>,
|
||||||
|
pub column_width_pending: bool,
|
||||||
|
pub rules_applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The handle index is read-only outside this owner. Registering and removing a
|
||||||
|
/// handle always updates its record as well. IDs are never reused on remapping.
|
||||||
|
pub(crate) struct WindowRegistry<W> {
|
||||||
|
ids: HashMap<W, WindowId>,
|
||||||
|
pub(super) records: HashMap<WindowId, WindowRecord>,
|
||||||
|
next_id: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W> Default for WindowRegistry<W> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
ids: HashMap::new(),
|
||||||
|
records: HashMap::new(),
|
||||||
|
next_id: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: Eq + Hash> WindowRegistry<W> {
|
||||||
|
pub fn allocate_id(&mut self) -> WindowId {
|
||||||
|
let id = WindowId(self.next_id);
|
||||||
|
self.next_id = self.next_id.checked_add(1).expect("window IDs exhausted");
|
||||||
|
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&mut self, window: W, id: WindowId) {
|
||||||
|
assert!(!self.ids.contains_key(&window), "window already registered");
|
||||||
|
assert!(!self.records.contains_key(&id), "window ID already registered");
|
||||||
|
|
||||||
|
self.ids.insert(window, id);
|
||||||
|
self.records.insert(id, WindowRecord::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ids(&self) -> &HashMap<W, WindowId> {
|
||||||
|
&self.ids
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record(&self, id: WindowId) -> Option<&WindowRecord> {
|
||||||
|
self.records.get(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn record_mut(&mut self, id: WindowId) -> Option<&mut WindowRecord> {
|
||||||
|
self.records.get_mut(&id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn records_mut(&mut self) -> impl Iterator<Item = (&WindowId, &mut WindowRecord)> {
|
||||||
|
self.records.iter_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn records(&self) -> impl Iterator<Item = (&WindowId, &WindowRecord)> {
|
||||||
|
self.records.iter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_world_positions(&mut self) -> HashMap<WindowId, (WorkspaceId, AnimatedValue)> {
|
||||||
|
self.records
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|(id, record)| Some((*id, record.world_x.take()?)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry(&self, id: &WindowId) -> Option<&WindowGeometry> {
|
||||||
|
self.records.get(id)?.geometry.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometry_mut(&mut self, id: &WindowId) -> Option<&mut WindowGeometry> {
|
||||||
|
self.records.get_mut(id)?.geometry.as_mut()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn geometries(&self) -> impl Iterator<Item = (&WindowId, &WindowGeometry)> {
|
||||||
|
self.records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, record)| Some((id, record.geometry.as_ref()?)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_geometry(&mut self, id: WindowId, geometry: WindowGeometry) {
|
||||||
|
self.update(id, |record| record.geometry = Some(geometry));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transaction(&self, id: &WindowId) -> Option<&ResizeTransaction> {
|
||||||
|
self.records.get(id)?.resize.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_transaction(&mut self, id: WindowId, transaction: ResizeTransaction) {
|
||||||
|
self.update(id, |record| record.resize = Some(transaction));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_transaction(&mut self, id: &WindowId) {
|
||||||
|
self.update(*id, |record| record.resize = None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resizing(&self) -> impl Iterator<Item = &WindowId> {
|
||||||
|
self.records
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, record)| record.resize.is_some().then_some(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expire_transactions(&mut self, now: std::time::Duration) {
|
||||||
|
for (id, record) in &mut self.records {
|
||||||
|
if record.resize.is_some_and(|transaction| transaction.expired(now)) {
|
||||||
|
tracing::warn!(?id, "resize presentation deadline reached");
|
||||||
|
record.resize = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update<R>(&mut self, id: WindowId, update: impl FnOnce(&mut WindowRecord) -> R) -> Option<R> {
|
||||||
|
self.records.get_mut(&id).map(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, window: &W) -> Option<WindowId> {
|
||||||
|
let id = self.ids.remove(window)?;
|
||||||
|
self.records.remove(&id);
|
||||||
|
|
||||||
|
Some(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_column_width_requests(&mut self) -> HashSet<WindowId> {
|
||||||
|
self.records
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|(id, record)| std::mem::take(&mut record.column_width_pending).then_some(*id))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unmap_drops_metadata_and_late_updates_cannot_resurrect_it() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let id = windows.allocate_id();
|
||||||
|
windows.register("surface", id);
|
||||||
|
windows.update(id, |record| {
|
||||||
|
record.maximized = true;
|
||||||
|
record.resize_anchor = Some((true, false, Rect::new(0., 0., 800., 600.)));
|
||||||
|
record.column_width_pending = true;
|
||||||
|
record.rules_applied = true;
|
||||||
|
record.focus = Some(crate::dimming::DimAnimation::new(1.0));
|
||||||
|
record.closing = Some(super::super::ClosingAnimation::default());
|
||||||
|
record.world_x = Some((WorkspaceId(1), AnimatedValue::new(50.0)));
|
||||||
|
record.coupled_width = Some((WorkspaceId(1), AnimatedValue::new(800.0)));
|
||||||
|
});
|
||||||
|
let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None);
|
||||||
|
let resize = ResizeTransaction::new(12.into(), std::time::Duration::ZERO);
|
||||||
|
windows.set_geometry(id, geometry);
|
||||||
|
windows.set_transaction(id, resize);
|
||||||
|
|
||||||
|
assert_eq!(windows.remove(&"surface"), Some(id));
|
||||||
|
assert_eq!(windows.remove(&"surface"), None);
|
||||||
|
assert!(windows.record(id).is_none());
|
||||||
|
assert!(windows.update(id, |record| record.maximized = true).is_none());
|
||||||
|
|
||||||
|
windows.set_geometry(id, geometry);
|
||||||
|
windows.set_transaction(id, resize);
|
||||||
|
|
||||||
|
assert!(windows.geometry(&id).is_none());
|
||||||
|
assert!(windows.transaction(&id).is_none());
|
||||||
|
assert_eq!(windows.resizing().count(), 0);
|
||||||
|
assert!(windows.ids().is_empty());
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remap_has_new_identity_and_does_not_inherit_old_requests() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let old = windows.allocate_id();
|
||||||
|
windows.register("surface", old);
|
||||||
|
windows.update(old, |record| record.rules_applied = true);
|
||||||
|
windows.remove(&"surface");
|
||||||
|
|
||||||
|
let new = windows.allocate_id();
|
||||||
|
windows.register("surface", new);
|
||||||
|
assert_ne!(old, new);
|
||||||
|
assert!(
|
||||||
|
windows
|
||||||
|
.update(old, |record| record.column_width_pending = true)
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(!windows.record(new).unwrap().rules_applied);
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_width_changes_are_consumed_once_and_survive_other_window_removal() {
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let first = windows.allocate_id();
|
||||||
|
let second = windows.allocate_id();
|
||||||
|
windows.register("first", first);
|
||||||
|
windows.register("second", second);
|
||||||
|
windows.update(first, |record| record.column_width_pending = true);
|
||||||
|
windows.update(second, |record| record.column_width_pending = true);
|
||||||
|
|
||||||
|
windows.remove(&"first");
|
||||||
|
|
||||||
|
assert_eq!(windows.take_column_width_requests(), HashSet::from([second]));
|
||||||
|
assert!(windows.take_column_width_requests().is_empty());
|
||||||
|
assert!(windows.record(second).is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn expired_resize_releases_only_its_barrier_and_preserves_geometry() {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
let mut windows = WindowRegistry::default();
|
||||||
|
let first = windows.allocate_id();
|
||||||
|
let second = windows.allocate_id();
|
||||||
|
windows.register("first", first);
|
||||||
|
windows.register("second", second);
|
||||||
|
let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None);
|
||||||
|
windows.set_geometry(first, geometry);
|
||||||
|
windows.set_transaction(first, ResizeTransaction::new(1.into(), Duration::ZERO));
|
||||||
|
windows.set_transaction(second, ResizeTransaction::new(2.into(), Duration::from_millis(100)));
|
||||||
|
|
||||||
|
windows.expire_transactions(Duration::from_millis(300));
|
||||||
|
|
||||||
|
assert_eq!(windows.geometry(&first), Some(&geometry));
|
||||||
|
assert!(windows.transaction(&first).is_none());
|
||||||
|
assert!(windows.transaction(&second).unwrap().accepts(Some(2.into())));
|
||||||
|
assert_eq!(windows.resizing().copied().collect::<Vec<_>>(), vec![second]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+106
-72
@@ -24,6 +24,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outputs = self.floating_outputs();
|
let outputs = self.floating_outputs();
|
||||||
let Some((remembered, work)) = app_id
|
let Some((remembered, work)) = app_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -65,13 +66,16 @@ impl Ferese {
|
|||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
self.add_transient_window(window, parent);
|
self.add_transient_window(window, parent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
self.focus_output_at(pointer.current_location());
|
self.focus_output_at(pointer.current_location());
|
||||||
}
|
}
|
||||||
|
|
||||||
let remembered_work = app_id
|
let remembered_work = app_id
|
||||||
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
.filter(|app| !crate::window_rules::is_native_dialog(Some(app)))
|
||||||
.and_then(|app| self.floating_memory.get(app))
|
.and_then(|app| self.floating_memory.get(app))
|
||||||
@@ -86,12 +90,13 @@ impl Ferese {
|
|||||||
if let Some(work) = remembered_work {
|
if let Some(work) = remembered_work {
|
||||||
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
|
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(bounds) = self.output_bounds() else {
|
let Some(bounds) = self.output_bounds() else {
|
||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let id = WindowId(self.next_window_id);
|
|
||||||
self.next_window_id += 1;
|
let id = self.windows.allocate_id();
|
||||||
let size_rect = client_size(&window)
|
let size_rect = client_size(&window)
|
||||||
.map(|size| natural_floating_rect(bounds, size))
|
.map(|size| natural_floating_rect(bounds, size))
|
||||||
.unwrap_or_else(|| centered_floating_rect(bounds));
|
.unwrap_or_else(|| centered_floating_rect(bounds));
|
||||||
@@ -100,7 +105,6 @@ impl Ferese {
|
|||||||
.focused_window
|
.focused_window
|
||||||
.and_then(|focused| self.presented_window_rect(focused))
|
.and_then(|focused| self.presented_window_rect(focused))
|
||||||
.unwrap_or(bounds);
|
.unwrap_or(bounds);
|
||||||
self.floating_placement_anchors.insert(id, anchor);
|
|
||||||
let fullscreen = self.workspaces.active().fullscreen;
|
let fullscreen = self.workspaces.active().fullscreen;
|
||||||
let focus = true;
|
let focus = true;
|
||||||
if let Err(error) = self
|
if let Err(error) = self
|
||||||
@@ -110,25 +114,30 @@ impl Ferese {
|
|||||||
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
|
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.windows.register(window.clone(), id);
|
||||||
|
self.windows.update(id, |w| w.placement_anchor = Some(anchor));
|
||||||
|
|
||||||
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
|
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
|
||||||
{
|
{
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
self.window_ids.insert(window.clone(), id);
|
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if let Some(fullscreen) = fullscreen {
|
if let Some(fullscreen) = fullscreen {
|
||||||
self.floating_above_fullscreen.insert(id, fullscreen);
|
self.floating_above_fullscreen.insert(id, fullscreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus);
|
self.space.map_element(window, (0, 0), focus);
|
||||||
// apply_initial_window_rules performs the first relayout/configure.
|
// apply_initial_window_rules performs the first relayout/configure.
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_tiled_window(&mut self, window: Window) {
|
pub fn add_tiled_window(&mut self, window: Window) {
|
||||||
let id = WindowId(self.next_window_id);
|
let id = self.windows.allocate_id();
|
||||||
self.next_window_id += 1;
|
|
||||||
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
let focus_new_window = self.workspaces.active().fullscreen.is_none();
|
||||||
|
|
||||||
let axis = self
|
let axis = self
|
||||||
@@ -147,11 +156,12 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.windows.register(window.clone(), id);
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if focus_new_window {
|
if focus_new_window {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus_new_window);
|
self.space.map_element(window, (0, 0), focus_new_window);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
|
|
||||||
@@ -165,18 +175,19 @@ impl Ferese {
|
|||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
self.add_tiled_window(window);
|
self.add_tiled_window(window);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
let size_rect = client_size(&window)
|
let size_rect = client_size(&window)
|
||||||
.map(|s| natural_floating_rect(bounds, s))
|
.map(|s| natural_floating_rect(bounds, s))
|
||||||
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
.unwrap_or_else(|| centered_transient_rect(visible_parent));
|
||||||
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
|
||||||
let id = WindowId(self.next_window_id);
|
let id = self.windows.allocate_id();
|
||||||
self.next_window_id += 1;
|
|
||||||
|
|
||||||
let focus = workspace == self.workspaces.active_id()
|
let focus = workspace == self.workspaces.active_id()
|
||||||
&& (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent));
|
&& (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent));
|
||||||
@@ -185,11 +196,12 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.windows.register(window.clone(), id);
|
||||||
self.window_stack.insert(id);
|
self.window_stack.insert(id);
|
||||||
if client_size(&window).is_none() {
|
if client_size(&window).is_none() {
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self
|
if self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace(workspace)
|
.workspace(workspace)
|
||||||
@@ -197,9 +209,11 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.floating_above_fullscreen.insert(id, parent);
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus {
|
if focus {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.space.map_element(window, (0, 0), focus);
|
self.space.map_element(window, (0, 0), focus);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
if focus {
|
if focus {
|
||||||
@@ -214,10 +228,15 @@ impl Ferese {
|
|||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
transient: bool,
|
transient: bool,
|
||||||
) {
|
) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !self.window_rules_applied.insert(id) {
|
|
||||||
|
if !self
|
||||||
|
.windows
|
||||||
|
.update(id, |w| !std::mem::replace(&mut w.rules_applied, true))
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,11 +245,12 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
|
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
|
||||||
let windows = self.window_ids.keys().cloned().collect::<Vec<_>>();
|
let windows = self.windows.ids().keys().cloned().collect::<Vec<_>>();
|
||||||
for window in windows {
|
for window in windows {
|
||||||
let Some(toplevel) = window.toplevel() else {
|
let Some(toplevel) = window.toplevel() else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
|
||||||
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
|
||||||
(
|
(
|
||||||
@@ -248,15 +268,18 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) {
|
pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(id) else {
|
let Some(bounds) = self.floating_bounds_for_window(id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if rule == Default::default() {
|
if rule == Default::default() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let axis = self
|
let axis = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.active()
|
.active()
|
||||||
@@ -264,11 +287,17 @@ impl Ferese {
|
|||||||
.automatic_axis(self.focused_window, bounds)
|
.automatic_axis(self.focused_window, bounds)
|
||||||
.unwrap_or(Axis::Horizontal);
|
.unwrap_or(Axis::Horizontal);
|
||||||
|
|
||||||
if let Some(workspace) = rule.workspace
|
if let Some(index) = rule.workspace
|
||||||
&& let Err(error) = self.workspaces.move_window_to_numeric(id, workspace, axis, 0.5)
|
&& let Some(output) = self
|
||||||
|
.workspaces
|
||||||
|
.workspace_for_window(id)
|
||||||
|
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace))
|
||||||
|
&& let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index)
|
||||||
|
&& let Err(error) = self.workspaces.move_window_to_workspace(id, workspace, axis, 0.5)
|
||||||
{
|
{
|
||||||
tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule");
|
tracing::warn!(%error, ?id, ?workspace, "failed to apply window workspace rule");
|
||||||
}
|
}
|
||||||
|
|
||||||
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
|
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
|
||||||
|
|
||||||
let should_float = rule
|
let should_float = rule
|
||||||
@@ -287,7 +316,7 @@ impl Ferese {
|
|||||||
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
.and_then(|entry| entry.restore(&self.floating_outputs()))
|
||||||
.is_none()
|
.is_none()
|
||||||
{
|
{
|
||||||
self.natural_floating_pending.insert(id);
|
self.windows.update(id, |w| w.natural_floating_pending = true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
|
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
|
||||||
@@ -309,6 +338,7 @@ impl Ferese {
|
|||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
tracing::warn!(%error, ?id, "failed to apply floating window rule");
|
tracing::warn!(%error, ?id, "failed to apply floating window rule");
|
||||||
}
|
}
|
||||||
@@ -331,16 +361,19 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) {
|
pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) {
|
||||||
let Some(id) = self.window_ids.get(window).copied() else {
|
let Some(id) = self.windows.ids().get(window).copied() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) {
|
if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) {
|
||||||
tracing::warn!(?id, ?parent, "ignored transient parent on another workspace");
|
tracing::warn!(?id, ?parent, "ignored transient parent on another workspace");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
let Some(bounds) = self.floating_bounds_for_window(parent) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
|
||||||
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
|
||||||
let size_rect = client_size(window)
|
let size_rect = client_size(window)
|
||||||
@@ -369,6 +402,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
self.floating_above_fullscreen.insert(id, parent);
|
self.floating_above_fullscreen.insert(id, parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.focused_window == Some(parent) {
|
if self.focused_window == Some(parent) {
|
||||||
if let Err(error) = self.workspaces.focus_window(id) {
|
if let Err(error) = self.workspaces.focus_window(id) {
|
||||||
tracing::warn!(%error, ?id, "failed to focus transient window");
|
tracing::warn!(%error, ?id, "failed to focus transient window");
|
||||||
@@ -385,34 +419,18 @@ impl Ferese {
|
|||||||
pub fn remove_tiled_window(&mut self, window: &Window) {
|
pub fn remove_tiled_window(&mut self, window: &Window) {
|
||||||
// Uncommitted toplevels have not entered a workspace yet.
|
// Uncommitted toplevels have not entered a workspace yet.
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
let Some(id) = self.window_ids.remove(window) else {
|
let Some(id) = self.windows.remove(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
self.window_geometry.remove(&id);
|
|
||||||
self.resize_transactions.remove(&id);
|
self.focus_history.remove(id);
|
||||||
self.resize_snapshots.remove(&id);
|
self.render.remove_window(id);
|
||||||
self.window_resize_fills.remove(&id);
|
|
||||||
self.maximized_windows.remove(&id);
|
|
||||||
self.maximized_column_widths.remove(&id);
|
|
||||||
self.window_stack.remove(id);
|
self.window_stack.remove(id);
|
||||||
self.floating_above_fullscreen.remove(&id);
|
self.floating_above_fullscreen.remove(&id);
|
||||||
self.natural_floating_pending.remove(&id);
|
|
||||||
self.floating_window_memory.remove(&id);
|
|
||||||
self.floating_placement_anchors.remove(&id);
|
|
||||||
self.floating_resize_anchors.remove(&id);
|
|
||||||
self.window_borders.remove(&id);
|
|
||||||
self.window_dims.remove(&id);
|
|
||||||
self.window_dimming.remove(&id);
|
|
||||||
self.window_focus.remove(&id);
|
|
||||||
self.window_shadows.remove(&id);
|
|
||||||
self.closing_windows.remove(&id);
|
|
||||||
self.window_rules_applied.remove(&id);
|
|
||||||
self.scrolling_world_x.remove(&id);
|
|
||||||
self.viewport_coupled_widths.remove(&id);
|
|
||||||
self.pending_column_width_cycles.remove(&id);
|
|
||||||
if let Err(error) = self.workspaces.remove_window(id) {
|
if let Err(error) = self.workspaces.remove_window(id) {
|
||||||
tracing::error!(%error, ?id, "failed to remove window from layout");
|
tracing::error!(%error, ?id, "failed to remove window from layout");
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.focused_window == Some(id) {
|
if self.focused_window == Some(id) {
|
||||||
self.focused_window = self.workspaces.active().last_focused;
|
self.focused_window = self.workspaces.active().last_focused;
|
||||||
}
|
}
|
||||||
@@ -422,7 +440,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
pub(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
|
||||||
let Some((window, id)) = self
|
let Some((window, id)) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|(window, _)| {
|
.find(|(window, _)| {
|
||||||
window
|
window
|
||||||
@@ -433,9 +452,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(transaction) = self.resize_transactions.get(&id) else {
|
|
||||||
|
let Some(transaction) = self.windows.transaction(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let serial = with_states(surface, |states| {
|
let serial = with_states(surface, |states| {
|
||||||
states
|
states
|
||||||
.data_map
|
.data_map
|
||||||
@@ -445,6 +466,7 @@ impl Ferese {
|
|||||||
if !transaction.accepts(serial) {
|
if !transaction.accepts(serial) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(output) = self
|
let Some(output) = self
|
||||||
.space
|
.space
|
||||||
.outputs()
|
.outputs()
|
||||||
@@ -453,10 +475,11 @@ impl Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry());
|
let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry());
|
||||||
let scale = output.current_scale().fractional_scale();
|
let scale = output.current_scale().fractional_scale();
|
||||||
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
let source_size = source_geometry.size.to_physical_precise_round(scale);
|
||||||
if self.resize_snapshots.get(&id).is_some_and(|snapshot| {
|
if self.render.snapshot(&id).is_some_and(|snapshot| {
|
||||||
crate::presentation::snapshot_covers_source(
|
crate::presentation::snapshot_covers_source(
|
||||||
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
smithay::backend::renderer::Texture::size(&snapshot.texture),
|
||||||
source_size,
|
source_size,
|
||||||
@@ -466,7 +489,8 @@ impl Ferese {
|
|||||||
}) {
|
}) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let used: usize = self.resize_snapshots.values().map(|snapshot| snapshot.bytes()).sum();
|
|
||||||
|
let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum();
|
||||||
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used);
|
||||||
let result = if let Some(backend) = &self.nested_backend {
|
let result = if let Some(backend) = &self.nested_backend {
|
||||||
// Commit dispatch does not run inside the Winit event callback;
|
// Commit dispatch does not run inside the Winit event callback;
|
||||||
@@ -489,7 +513,7 @@ impl Ferese {
|
|||||||
match result {
|
match result {
|
||||||
Ok(Some(snapshot)) => {
|
Ok(Some(snapshot)) => {
|
||||||
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
|
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
|
||||||
self.resize_snapshots.insert(id, snapshot);
|
self.render.set_snapshot(id, snapshot);
|
||||||
}
|
}
|
||||||
Ok(None) => {}
|
Ok(None) => {}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -499,7 +523,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_client_commit(&mut self, window: &Window) {
|
pub fn record_client_commit(&mut self, window: &Window) {
|
||||||
let Some(&id) = self.window_ids.get(window) else {
|
let Some(&id) = self.windows.ids().get(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// current_serial is promoted by Smithay on commit, unlike
|
// current_serial is promoted by Smithay on commit, unlike
|
||||||
@@ -513,28 +537,30 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
if self
|
if self
|
||||||
.resize_transactions
|
.windows
|
||||||
.get(&id)
|
.transaction(&id)
|
||||||
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
.is_some_and(|transaction| transaction.accepts(committed_serial))
|
||||||
{
|
{
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(size) = client_size(window) else {
|
let Some(size) = client_size(window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let fullscreen = self
|
let fullscreen = self
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace_for_window(id)
|
.workspace_for_window(id)
|
||||||
.and_then(|workspace| self.workspaces.workspace(workspace))
|
.and_then(|workspace| self.workspaces.workspace(workspace))
|
||||||
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||||
let normal_geometry = !fullscreen
|
let normal_geometry = !fullscreen
|
||||||
&& !self.maximized_windows.contains(&id)
|
&& !self.windows.record(id).is_some_and(|w| w.maximized)
|
||||||
&& self
|
&& self.windows.geometry(&id).is_none_or(|geometry| !geometry.is_zooming());
|
||||||
.window_geometry
|
let initial_floating = self
|
||||||
.get(&id)
|
.windows
|
||||||
.is_none_or(|geometry| !geometry.is_zooming());
|
.update(id, |w| std::mem::take(&mut w.natural_floating_pending))
|
||||||
let initial_floating =
|
.unwrap_or(false)
|
||||||
self.natural_floating_pending.remove(&id) || self.floating_placement_anchors.contains_key(&id);
|
|| self.windows.record(id).is_some_and(|w| w.placement_anchor.is_some());
|
||||||
if initial_floating
|
if initial_floating
|
||||||
&& normal_geometry
|
&& normal_geometry
|
||||||
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
|
||||||
@@ -547,12 +573,12 @@ impl Ferese {
|
|||||||
(f64::from(size.width), f64::from(size.height)),
|
(f64::from(size.width), f64::from(size.height)),
|
||||||
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
|
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
|
||||||
);
|
);
|
||||||
self.floating_placement_anchors.remove(&id);
|
self.windows.update(id, |w| w.placement_anchor = None);
|
||||||
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
// This is the first mapped client buffer, not a user resize.
|
// This is the first mapped client buffer, not a user resize.
|
||||||
// Do not animate from the temporary placement box.
|
// Do not animate from the temporary placement box.
|
||||||
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -560,7 +586,7 @@ impl Ferese {
|
|||||||
// terminal cell grids, or a dialog changing its contents). Its committed
|
// terminal cell grids, or a dialog changing its contents). Its committed
|
||||||
// window geometry is authoritative once the latest configure is committed.
|
// window geometry is authoritative once the latest configure is committed.
|
||||||
// Never let an old buffer undo a newer resize or a fullscreen transition.
|
// Never let an old buffer undo a newer resize or a fullscreen transition.
|
||||||
self.floating_placement_anchors.remove(&id);
|
self.windows.update(id, |w| w.placement_anchor = None);
|
||||||
let settled_configure = window.toplevel().is_some_and(|toplevel| {
|
let settled_configure = window.toplevel().is_some_and(|toplevel| {
|
||||||
with_states(toplevel.wl_surface(), |states| {
|
with_states(toplevel.wl_surface(), |states| {
|
||||||
states.data_map.get::<XdgToplevelSurfaceData>().is_some_and(|data| {
|
states.data_map.get::<XdgToplevelSurfaceData>().is_some_and(|data| {
|
||||||
@@ -580,22 +606,25 @@ impl Ferese {
|
|||||||
&& ClientSize::from_rect(rect) != size
|
&& ClientSize::from_rect(rect) != size
|
||||||
{
|
{
|
||||||
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
|
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
|
||||||
if let Some((left, top, anchor)) = self.floating_resize_anchors.get(&id) {
|
if let Some((left, top, anchor)) = self.windows.record(id).and_then(|w| w.resize_anchor.as_ref()) {
|
||||||
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
|
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
|
||||||
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
|
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
|
||||||
rect = crate::floating::clamp_to_work(rect, work);
|
rect = crate::floating::clamp_to_work(rect, work);
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
if self.workspaces.set_floating_rect(id, rect).is_ok() {
|
||||||
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
|
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
|
||||||
self.resize_transactions.remove(&id);
|
self.windows.clear_transaction(&id);
|
||||||
self.resize_snapshots.remove(&id);
|
self.render.clear_snapshot(&id);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if settled_configure && self.floating_resize_anchors.remove(&id).is_some() {
|
|
||||||
|
if settled_configure && self.windows.update(id, |w| w.resize_anchor.take()).flatten().is_some() {
|
||||||
self.remember_floating(window);
|
self.remember_floating(window);
|
||||||
}
|
}
|
||||||
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
|
||||||
|
let Some(geometry) = self.windows.geometry_mut(&id) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -613,17 +642,21 @@ impl Ferese {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.closing_windows.entry(focused).or_default();
|
self.windows.update(focused, |record| {
|
||||||
|
record.closing.get_or_insert_default();
|
||||||
|
});
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
|
||||||
if !self.window_ids.values().any(|window_id| *window_id == id) {
|
if !self.windows.ids().values().any(|window_id| *window_id == id) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.animations_enabled {
|
if self.animations_enabled {
|
||||||
self.closing_windows.entry(id).or_default();
|
self.windows.update(id, |record| {
|
||||||
|
record.closing.get_or_insert_default();
|
||||||
|
});
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
} else {
|
} else {
|
||||||
self.send_window_close(id);
|
self.send_window_close(id);
|
||||||
@@ -634,7 +667,8 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(super) fn send_window_close(&self, id: WindowId) {
|
pub(super) fn send_window_close(&self, id: WindowId) {
|
||||||
let Some(toplevel) = self
|
let Some(toplevel) = self
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.iter()
|
.iter()
|
||||||
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
|
.find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten())
|
||||||
else {
|
else {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
|||||||
let rendered = (|| -> DamageRenderResult {
|
let rendered = (|| -> DamageRenderResult {
|
||||||
{
|
{
|
||||||
let (renderer, mut framebuffer) = backend.bind()?;
|
let (renderer, mut framebuffer) = backend.bind()?;
|
||||||
state.process_dmabuf_imports(renderer);
|
state.process_dmabuf_imports(renderer, None);
|
||||||
let elements = animated_window_elements(state, renderer, &output);
|
let elements = animated_window_elements(state, renderer, &output);
|
||||||
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
|
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
|
||||||
let result = damage_tracker.render_output(
|
let result = damage_tracker.render_output(
|
||||||
@@ -229,7 +229,8 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Dur
|
|||||||
let time = state.start_time.elapsed();
|
let time = state.start_time.elapsed();
|
||||||
for window in state.space.elements().filter(|window| {
|
for window in state.space.elements().filter(|window| {
|
||||||
state
|
state
|
||||||
.window_ids
|
.windows
|
||||||
|
.ids()
|
||||||
.get(*window)
|
.get(*window)
|
||||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1297,6 +1297,22 @@ pub fn import_family(id: &str, source: &str) -> Result<ImportedFamily, String> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn resolved_shell_radius_preserves_fractional_values() {
|
||||||
|
for (requested, expected) in [(0.0, 0.0), (0.4, 0.4), (13.4, 13.4), (13.5, 13.5)] {
|
||||||
|
let document = crate::Document::parse(&format!(
|
||||||
|
"theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}"
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let result = super::resolve(&document, std::path::Path::new("/tmp"), jiff::Timestamp::now(), |_| {
|
||||||
|
Err("unexpected theme file".into())
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.theme.tokens.geometry.shell_radius, expected);
|
||||||
|
assert_eq!(result.theme.tokens.geometry.window_radius, 7.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn candidate(source: &str) -> Result<Candidate, String> {
|
fn candidate(source: &str) -> Result<Candidate, String> {
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
use ferese_layout::WindowId;
|
||||||
|
|
||||||
|
/// Most recent focus first. Layer surfaces and temporary selections do not enter this list.
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct FocusHistory {
|
||||||
|
windows: Vec<WindowId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusHistory {
|
||||||
|
pub fn record(&mut self, window: WindowId) {
|
||||||
|
if self.windows.first() == Some(&window) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.remove(window);
|
||||||
|
self.windows.insert(0, window);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, window: WindowId) {
|
||||||
|
self.windows.retain(|id| *id != window);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn candidates(&self, current: Option<WindowId>, mut available: Vec<WindowId>) -> Vec<WindowId> {
|
||||||
|
available.sort_unstable_by_key(|id| id.0);
|
||||||
|
available.dedup();
|
||||||
|
let mut ordered = Vec::with_capacity(available.len());
|
||||||
|
for id in current
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.windows.iter().copied())
|
||||||
|
.chain(available.iter().copied())
|
||||||
|
{
|
||||||
|
if available.contains(&id) && !ordered.contains(&id) {
|
||||||
|
ordered.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frozen traversal order: previewing does not reorder focus history.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FocusCycle {
|
||||||
|
windows: Vec<WindowId>,
|
||||||
|
selected: Option<WindowId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusCycle {
|
||||||
|
pub fn new(windows: Vec<WindowId>, current: Option<WindowId>) -> Self {
|
||||||
|
let selected = current.filter(|id| windows.contains(id));
|
||||||
|
Self { windows, selected }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn windows(&self) -> &[WindowId] {
|
||||||
|
&self.windows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reconcile(&mut self, available: &[WindowId]) -> Option<WindowId> {
|
||||||
|
if self.selected.is_some_and(|id| available.contains(&id)) {
|
||||||
|
self.selected
|
||||||
|
} else {
|
||||||
|
self.advance(false, available)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance(&mut self, reverse: bool, available: &[WindowId]) -> Option<WindowId> {
|
||||||
|
// Keep the old index when the selected window disappears so forward
|
||||||
|
// traversal selects its successor rather than skipping over it.
|
||||||
|
let old = self
|
||||||
|
.selected
|
||||||
|
.and_then(|id| self.windows.iter().position(|candidate| *candidate == id));
|
||||||
|
let before = old.map(|index| self.windows[..index].iter().filter(|id| available.contains(id)).count());
|
||||||
|
let survives = self.selected.is_some_and(|id| available.contains(&id));
|
||||||
|
self.windows.retain(|id| available.contains(id));
|
||||||
|
if self.windows.is_empty() {
|
||||||
|
self.selected = None;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let len = self.windows.len();
|
||||||
|
let index = match (before, reverse, survives) {
|
||||||
|
(Some(index), true, _) => (index + len - 1) % len,
|
||||||
|
(Some(index), false, true) => (index + 1) % len,
|
||||||
|
(Some(index), false, false) => index % len,
|
||||||
|
(None, true, _) => len - 1,
|
||||||
|
(None, false, _) => 0,
|
||||||
|
};
|
||||||
|
self.selected = Some(self.windows[index]);
|
||||||
|
self.selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn history_is_unique_filters_unavailable_and_last_focus_toggles() {
|
||||||
|
let mut history = FocusHistory::default();
|
||||||
|
for id in [1, 2, 3, 3] {
|
||||||
|
history.record(WindowId(id));
|
||||||
|
}
|
||||||
|
let available = vec![WindowId(1), WindowId(2), WindowId(3), WindowId(4)];
|
||||||
|
assert_eq!(
|
||||||
|
history.candidates(Some(WindowId(3)), available.clone()),
|
||||||
|
[3, 2, 1, 4].map(WindowId)
|
||||||
|
);
|
||||||
|
history.record(WindowId(2));
|
||||||
|
assert_eq!(history.candidates(Some(WindowId(2)), available)[1], WindowId(3));
|
||||||
|
history.remove(WindowId(3));
|
||||||
|
assert_eq!(
|
||||||
|
history.candidates(None, vec![WindowId(1), WindowId(4)]),
|
||||||
|
[1, 4].map(WindowId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cycle_freezes_order_wraps_reverses_and_skips_closed_windows() {
|
||||||
|
let available = [3, 2, 1].map(WindowId);
|
||||||
|
let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3)));
|
||||||
|
assert_eq!(cycle.advance(false, &available), Some(WindowId(2)));
|
||||||
|
assert_eq!(cycle.advance(false, &available), Some(WindowId(1)));
|
||||||
|
assert_eq!(cycle.advance(false, &available), Some(WindowId(3)));
|
||||||
|
assert_eq!(cycle.advance(true, &available), Some(WindowId(1)));
|
||||||
|
assert_eq!(cycle.advance(false, &[WindowId(3), WindowId(2)]), Some(WindowId(3)));
|
||||||
|
assert_eq!(cycle.advance(false, &[]), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_current_focus_starts_at_most_recent_and_new_windows_do_not_join_cycle() {
|
||||||
|
let mut cycle = FocusCycle::new(vec![WindowId(2), WindowId(1)], None);
|
||||||
|
assert_eq!(
|
||||||
|
cycle.advance(false, &[WindowId(3), WindowId(2), WindowId(1)]),
|
||||||
|
Some(WindowId(2))
|
||||||
|
);
|
||||||
|
assert_eq!(cycle.advance(true, &[WindowId(2)]), Some(WindowId(2)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn closing_the_preview_selects_its_successor_before_commit() {
|
||||||
|
let available = [3, 2, 1].map(WindowId);
|
||||||
|
let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3)));
|
||||||
|
assert_eq!(cycle.advance(false, &available), Some(WindowId(2)));
|
||||||
|
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
|
||||||
|
assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
pub mod desktop;
|
pub mod desktop;
|
||||||
|
mod focus;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
mod output;
|
mod output;
|
||||||
mod workspace;
|
mod workspace;
|
||||||
|
mod workspace_policy;
|
||||||
|
|
||||||
|
pub use focus::{FocusCycle, FocusHistory};
|
||||||
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
||||||
pub use workspace::{
|
pub use workspace::{
|
||||||
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
|
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
|
||||||
workspace_order_key,
|
|
||||||
};
|
};
|
||||||
|
pub use workspace_policy::WorkspaceView;
|
||||||
@@ -107,10 +107,6 @@ impl OutputWorkspaceMap {
|
|||||||
self.outputs.get(&output).and_then(|state| state.previous)
|
self.outputs.get(&output).and_then(|state| state.previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn history_workspaces(&self) -> impl Iterator<Item = WorkspaceId> + '_ {
|
|
||||||
self.outputs.values().filter_map(|state| state.previous)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> {
|
pub fn output_for_workspace(&self, workspace: WorkspaceId) -> Option<OutputId> {
|
||||||
self.assignments.get(&workspace).copied()
|
self.assignments.get(&workspace).copied()
|
||||||
}
|
}
|
||||||
@@ -171,6 +167,19 @@ impl OutputWorkspaceMap {
|
|||||||
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
|
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
|
||||||
|
|
||||||
if assignment_matches && revision_matches {
|
if assignment_matches && revision_matches {
|
||||||
|
// Cleanup may have removed every other workspace while this
|
||||||
|
// monitor was disconnected. Never strand the donor output;
|
||||||
|
// the reconnecting monitor can use its fresh fallback.
|
||||||
|
if evacuated
|
||||||
|
.target
|
||||||
|
.and_then(|target| self.outputs.get(&target))
|
||||||
|
.is_some_and(|state| {
|
||||||
|
state.workspaces.len() == 1 && state.workspaces.contains(&evacuated.workspace)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(target) = evacuated.target
|
if let Some(target) = evacuated.target
|
||||||
&& let Some(target_state) = self.outputs.get_mut(&target)
|
&& let Some(target_state) = self.outputs.get_mut(&target)
|
||||||
{
|
{
|
||||||
@@ -413,6 +422,22 @@ impl OutputWorkspaceMap {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn reconnect_cannot_reclaim_the_donor_monitors_only_workspace() {
|
||||||
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
|
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||||
|
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
|
||||||
|
outputs.focus_output(OutputId(2)).unwrap();
|
||||||
|
outputs.disconnect(OutputId(2)).unwrap();
|
||||||
|
assert!(outputs.forget_workspace(WorkspaceId(1)));
|
||||||
|
|
||||||
|
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(3)).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
|
||||||
|
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(3)));
|
||||||
|
assert!(outputs.validate());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn forgetting_an_inactive_workspace_prevents_hotplug_resurrection() {
|
fn forgetting_an_inactive_workspace_prevents_hotplug_resurrection() {
|
||||||
let mut outputs = super::OutputWorkspaceMap::default();
|
let mut outputs = super::OutputWorkspaceMap::default();
|
||||||
@@ -530,33 +555,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_previous_workspace_survives_pruning_until_history_moves_on() {
|
|
||||||
let mut workspaces = crate::WorkspaceSet::default();
|
|
||||||
let first = workspaces.active_id();
|
|
||||||
let second = workspaces.ensure_numeric(2).unwrap();
|
|
||||||
let third = workspaces.ensure_numeric(3).unwrap();
|
|
||||||
let mut outputs = OutputWorkspaceMap::default();
|
|
||||||
let display = OutputId(1);
|
|
||||||
outputs.connect(display, geometry(0), first).unwrap();
|
|
||||||
outputs.switch_workspace(display, second).unwrap();
|
|
||||||
workspaces.activate(second).unwrap();
|
|
||||||
let protected = outputs.history_workspaces().chain([second]).collect();
|
|
||||||
let removed = workspaces.prune_empty(&protected);
|
|
||||||
assert!(removed.contains(&third));
|
|
||||||
assert!(workspaces.workspace(first).is_some());
|
|
||||||
let third = workspaces.ensure_numeric(3).unwrap();
|
|
||||||
outputs.switch_workspace(display, third).unwrap();
|
|
||||||
workspaces.activate(third).unwrap();
|
|
||||||
let protected = outputs.history_workspaces().chain([third]).collect();
|
|
||||||
for workspace in workspaces.prune_empty(&protected) {
|
|
||||||
outputs.forget_workspace(workspace);
|
|
||||||
}
|
|
||||||
assert!(workspaces.workspace(first).is_none());
|
|
||||||
assert!(workspaces.workspace(second).is_some());
|
|
||||||
assert_eq!(outputs.previous_workspace(display), Some(second));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_outputs_receive_distinct_workspaces() {
|
fn new_outputs_receive_distinct_workspaces() {
|
||||||
let mut outputs = OutputWorkspaceMap::default();
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
|
|||||||
@@ -10,14 +10,9 @@ use ferese_layout::{
|
|||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
pub struct WorkspaceId(pub u64);
|
pub struct WorkspaceId(pub u64);
|
||||||
|
|
||||||
pub fn workspace_order_key(name: &str, id: WorkspaceId) -> (u64, u64) {
|
|
||||||
(name.parse::<u64>().unwrap_or(u64::MAX), id.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Workspace {
|
pub struct Workspace {
|
||||||
pub id: WorkspaceId,
|
pub id: WorkspaceId,
|
||||||
pub name: String,
|
|
||||||
pub layout: WorkspaceLayout,
|
pub layout: WorkspaceLayout,
|
||||||
pub floating: Vec<WindowId>,
|
pub floating: Vec<WindowId>,
|
||||||
pub last_focused: Option<WindowId>,
|
pub last_focused: Option<WindowId>,
|
||||||
@@ -339,7 +334,6 @@ impl From<LayoutError> for WorkspaceError {
|
|||||||
pub struct WorkspaceSet {
|
pub struct WorkspaceSet {
|
||||||
active: WorkspaceId,
|
active: WorkspaceId,
|
||||||
workspaces: HashMap<WorkspaceId, Workspace>,
|
workspaces: HashMap<WorkspaceId, Workspace>,
|
||||||
names: HashMap<String, WorkspaceId>,
|
|
||||||
window_workspaces: HashMap<WindowId, WorkspaceId>,
|
window_workspaces: HashMap<WindowId, WorkspaceId>,
|
||||||
placements: HashMap<WindowId, WindowPlacement>,
|
placements: HashMap<WindowId, WindowPlacement>,
|
||||||
default_layout_mode: LayoutMode,
|
default_layout_mode: LayoutMode,
|
||||||
@@ -436,19 +430,16 @@ impl WorkspaceSet {
|
|||||||
let active = WorkspaceId(1);
|
let active = WorkspaceId(1);
|
||||||
let workspace = Workspace {
|
let workspace = Workspace {
|
||||||
id: active,
|
id: active,
|
||||||
name: "1".to_owned(),
|
|
||||||
layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy),
|
layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy),
|
||||||
floating: Vec::new(),
|
floating: Vec::new(),
|
||||||
last_focused: None,
|
last_focused: None,
|
||||||
fullscreen: None,
|
fullscreen: None,
|
||||||
};
|
};
|
||||||
let workspaces = HashMap::from([(active, workspace)]);
|
let workspaces = HashMap::from([(active, workspace)]);
|
||||||
let names = HashMap::from([("1".to_owned(), active)]);
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
active,
|
active,
|
||||||
workspaces,
|
workspaces,
|
||||||
names,
|
|
||||||
window_workspaces: HashMap::new(),
|
window_workspaces: HashMap::new(),
|
||||||
placements: HashMap::new(),
|
placements: HashMap::new(),
|
||||||
default_layout_mode,
|
default_layout_mode,
|
||||||
@@ -500,36 +491,6 @@ impl WorkspaceSet {
|
|||||||
self.workspaces.values()
|
self.workspaces.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ordered(&self) -> Vec<&Workspace> {
|
|
||||||
let mut workspaces = self.iter().collect::<Vec<_>>();
|
|
||||||
workspaces.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
|
|
||||||
workspaces
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prune_empty(&mut self, protected: &HashSet<WorkspaceId>) -> Vec<WorkspaceId> {
|
|
||||||
let mut empty = self
|
|
||||||
.workspaces
|
|
||||||
.values()
|
|
||||||
.filter(|workspace| workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty())
|
|
||||||
.map(|workspace| workspace.id)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
empty.sort_by_key(|id| id.0);
|
|
||||||
let visible_empty = empty.iter().any(|id| *id == self.active || protected.contains(id));
|
|
||||||
let spare = if visible_empty { None } else { empty.first().copied() };
|
|
||||||
let mut removed = Vec::new();
|
|
||||||
for id in empty {
|
|
||||||
if id == self.active || protected.contains(&id) || Some(id) == spare {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(workspace) = self.workspaces.remove(&id) {
|
|
||||||
self.names.remove(&workspace.name);
|
|
||||||
removed.push(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug_assert!(self.validate().is_ok());
|
|
||||||
removed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
pub fn workspace_for_window(&self, window: WindowId) -> Option<WorkspaceId> {
|
||||||
self.window_workspaces.get(&window).copied()
|
self.window_workspaces.get(&window).copied()
|
||||||
}
|
}
|
||||||
@@ -680,25 +641,39 @@ impl WorkspaceSet {
|
|||||||
Ok(changed)
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ensure_numeric(&mut self, index: u32) -> Result<WorkspaceId, WorkspaceError> {
|
pub fn remove_empty(&mut self, id: WorkspaceId) -> bool {
|
||||||
|
if id == self.active || !self.workspace(id).is_some_and(Workspace::is_empty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.workspaces.remove(&id);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) fn fixture_workspace(&mut self, index: u32) -> Result<WorkspaceId, WorkspaceError> {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
return Err(WorkspaceError::InvalidNumericName(index));
|
return Err(WorkspaceError::InvalidNumericName(index));
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = index.to_string();
|
while self.next_id <= u64::from(index) {
|
||||||
|
self.create_workspace();
|
||||||
if let Some(id) = self.names.get(&name) {
|
|
||||||
return Ok(*id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let id = WorkspaceId(u64::from(index));
|
||||||
|
self.workspace(id)
|
||||||
|
.map(|_| id)
|
||||||
|
.ok_or(WorkspaceError::UnknownWorkspace(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates an identity; display positions are assigned by the output owner.
|
||||||
|
pub fn create_workspace(&mut self) -> WorkspaceId {
|
||||||
let id = WorkspaceId(self.next_id);
|
let id = WorkspaceId(self.next_id);
|
||||||
self.next_id += 1;
|
self.next_id = self.next_id.checked_add(1).expect("workspace IDs exhausted");
|
||||||
self.names.insert(name.clone(), id);
|
|
||||||
self.workspaces.insert(
|
self.workspaces.insert(
|
||||||
id,
|
id,
|
||||||
Workspace {
|
Workspace {
|
||||||
id,
|
id,
|
||||||
name,
|
|
||||||
layout: WorkspaceLayout::new(
|
layout: WorkspaceLayout::new(
|
||||||
self.default_layout_mode,
|
self.default_layout_mode,
|
||||||
self.default_column_width,
|
self.default_column_width,
|
||||||
@@ -711,11 +686,12 @@ impl WorkspaceSet {
|
|||||||
);
|
);
|
||||||
|
|
||||||
debug_assert!(self.validate().is_ok());
|
debug_assert!(self.validate().is_ok());
|
||||||
Ok(id)
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn switch_to_numeric(&mut self, index: u32) -> Result<Option<WindowId>, WorkspaceError> {
|
#[cfg(test)]
|
||||||
let workspace = self.ensure_numeric(index)?;
|
fn switch_to_fixture(&mut self, index: u32) -> Result<Option<WindowId>, WorkspaceError> {
|
||||||
|
let workspace = self.fixture_workspace(index)?;
|
||||||
self.activate(workspace)
|
self.activate(workspace)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -798,17 +774,32 @@ impl WorkspaceSet {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_window_to_numeric(
|
#[cfg(test)]
|
||||||
|
fn move_window_to_fixture(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: WindowId,
|
window: WindowId,
|
||||||
index: u32,
|
index: u32,
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
ratio: f64,
|
ratio: f64,
|
||||||
) -> Result<WorkspaceId, WorkspaceError> {
|
) -> Result<WorkspaceId, WorkspaceError> {
|
||||||
|
let destination = self.fixture_workspace(index)?;
|
||||||
|
self.move_window_to_workspace(window, destination, axis, ratio)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_window_to_workspace(
|
||||||
|
&mut self,
|
||||||
|
window: WindowId,
|
||||||
|
destination_id: WorkspaceId,
|
||||||
|
axis: Axis,
|
||||||
|
ratio: f64,
|
||||||
|
) -> Result<WorkspaceId, WorkspaceError> {
|
||||||
|
if self.workspace(destination_id).is_none() {
|
||||||
|
return Err(WorkspaceError::UnknownWorkspace(destination_id));
|
||||||
|
}
|
||||||
|
|
||||||
let source_id = self
|
let source_id = self
|
||||||
.workspace_for_window(window)
|
.workspace_for_window(window)
|
||||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||||
let destination_id = self.ensure_numeric(index)?;
|
|
||||||
|
|
||||||
if source_id == destination_id {
|
if source_id == destination_id {
|
||||||
return Ok(destination_id);
|
return Ok(destination_id);
|
||||||
@@ -943,14 +934,11 @@ impl WorkspaceSet {
|
|||||||
if !self.workspaces.contains_key(&self.active) {
|
if !self.workspaces.contains_key(&self.active) {
|
||||||
return Err(WorkspaceError::InvalidState("active workspace is missing"));
|
return Err(WorkspaceError::InvalidState("active workspace is missing"));
|
||||||
}
|
}
|
||||||
if self.names.len() != self.workspaces.len() {
|
|
||||||
return Err(WorkspaceError::InvalidState("workspace name index is inconsistent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut seen_windows = HashSet::new();
|
let mut seen_windows = HashSet::new();
|
||||||
|
|
||||||
for (id, workspace) in &self.workspaces {
|
for (id, workspace) in &self.workspaces {
|
||||||
if workspace.id != *id || self.names.get(&workspace.name) != Some(id) {
|
if workspace.id != *id {
|
||||||
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
|
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1033,70 +1021,6 @@ fn finite_or_zero(value: f64) -> f64 {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
|
||||||
fn numeric_order_survives_pruning_and_recreation() {
|
|
||||||
let mut set = super::WorkspaceSet::default();
|
|
||||||
let original_first = set.active_id();
|
|
||||||
let second = set.ensure_numeric(2).unwrap();
|
|
||||||
set.activate(second).unwrap();
|
|
||||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![original_first]);
|
|
||||||
|
|
||||||
let tenth = set.ensure_numeric(10).unwrap();
|
|
||||||
let first = set.ensure_numeric(1).unwrap();
|
|
||||||
assert_ne!(first, original_first);
|
|
||||||
assert!(first.0 > tenth.0);
|
|
||||||
assert_eq!(
|
|
||||||
set.ordered()
|
|
||||||
.into_iter()
|
|
||||||
.map(|workspace| (workspace.name.as_str(), workspace.id))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![("1", first), ("2", second), ("10", tenth)]
|
|
||||||
);
|
|
||||||
set.activate(first).unwrap();
|
|
||||||
assert_eq!(set.active().name, "1");
|
|
||||||
assert!(set.validate().is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pruning_keeps_windows_and_one_empty_workspace() {
|
|
||||||
let mut set = super::WorkspaceSet::default();
|
|
||||||
let first = set.active_id();
|
|
||||||
set.insert_window(super::WindowId(1), super::Axis::Horizontal, 0.5)
|
|
||||||
.unwrap();
|
|
||||||
let floating = set.ensure_numeric(2).unwrap();
|
|
||||||
set.insert_floating_window(
|
|
||||||
super::WindowId(2),
|
|
||||||
floating,
|
|
||||||
super::Rect::new(0., 0., 100., 100.),
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let spare = set.ensure_numeric(3).unwrap();
|
|
||||||
let excess = set.ensure_numeric(4).unwrap();
|
|
||||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![excess]);
|
|
||||||
assert!(set.workspace(first).is_some());
|
|
||||||
assert!(set.workspace(floating).is_some());
|
|
||||||
assert!(set.workspace(spare).is_some());
|
|
||||||
set.activate(spare).unwrap();
|
|
||||||
set.remove_window(super::WindowId(1)).unwrap();
|
|
||||||
assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![first]);
|
|
||||||
assert!(set.validate().is_ok());
|
|
||||||
assert_ne!(set.ensure_numeric(1).unwrap(), first);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn pruning_never_removes_another_displays_active_workspace() {
|
|
||||||
let mut set = super::WorkspaceSet::default();
|
|
||||||
let first = set.active_id();
|
|
||||||
let second = set.ensure_numeric(2).unwrap();
|
|
||||||
let third = set.ensure_numeric(3).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
set.prune_empty(&std::collections::HashSet::from([first, second])),
|
|
||||||
vec![third]
|
|
||||||
);
|
|
||||||
assert!(set.workspace(second).is_some());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
@@ -1149,21 +1073,22 @@ mod tests {
|
|||||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
|
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
|
||||||
assert_eq!(layout.default_width(), ColumnWidth::Full);
|
assert_eq!(layout.default_width(), ColumnWidth::Full);
|
||||||
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_fixture(2).unwrap();
|
||||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
|
||||||
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace));
|
||||||
}
|
}
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn numeric_workspaces_are_created_lazily_with_stable_ids() {
|
fn workspace_creation_uses_fresh_ids() {
|
||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
let first = workspaces.active_id();
|
let first = workspaces.active_id();
|
||||||
let ninth = workspaces.ensure_numeric(9).unwrap();
|
let second = workspaces.create_workspace();
|
||||||
|
assert!(workspaces.remove_empty(second));
|
||||||
|
|
||||||
assert_eq!(workspaces.ensure_numeric(1), Ok(first));
|
let third = workspaces.create_workspace();
|
||||||
assert_eq!(workspaces.ensure_numeric(9), Ok(ninth));
|
assert_ne!(first, third);
|
||||||
assert_ne!(first, ninth);
|
assert_ne!(second, third);
|
||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1184,7 +1109,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0));
|
assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0));
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_fixture(2).unwrap();
|
||||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1193,11 +1118,11 @@ mod tests {
|
|||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
|
||||||
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), None);
|
assert_eq!(workspaces.switch_to_fixture(2).unwrap(), None);
|
||||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
|
||||||
assert_eq!(workspaces.switch_to_numeric(1).unwrap(), Some(WindowId(1)));
|
assert_eq!(workspaces.switch_to_fixture(1).unwrap(), Some(WindowId(1)));
|
||||||
assert_eq!(workspaces.switch_to_numeric(2).unwrap(), Some(WindowId(2)));
|
assert_eq!(workspaces.switch_to_fixture(2).unwrap(), Some(WindowId(2)));
|
||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1208,7 +1133,7 @@ mod tests {
|
|||||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
let first = workspaces.active_id();
|
let first = workspaces.active_id();
|
||||||
let second = workspaces
|
let second = workspaces
|
||||||
.move_window_to_numeric(WindowId(2), 2, Axis::Horizontal, 0.5)
|
.move_window_to_fixture(WindowId(2), 2, Axis::Horizontal, 0.5)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(workspaces.active_id(), first);
|
assert_eq!(workspaces.active_id(), first);
|
||||||
@@ -1247,7 +1172,7 @@ mod tests {
|
|||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
workspaces
|
workspaces
|
||||||
.move_window_to_numeric(WindowId(1), 2, Axis::Horizontal, 0.5)
|
.move_window_to_fixture(WindowId(1), 2, Axis::Horizontal, 0.5)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1402,7 +1327,7 @@ mod tests {
|
|||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
let parent_workspace = workspaces.active_id();
|
let parent_workspace = workspaces.active_id();
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_fixture(2).unwrap();
|
||||||
workspaces
|
workspaces
|
||||||
.insert_floating_window(
|
.insert_floating_window(
|
||||||
WindowId(2),
|
WindowId(2),
|
||||||
@@ -1463,7 +1388,7 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||||
assert!(workspaces.focus_window(WindowId(2)).is_ok());
|
assert!(workspaces.focus_window(WindowId(2)).is_ok());
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_fixture(2).unwrap();
|
||||||
assert_eq!(workspaces.activate(workspace).unwrap(), Some(WindowId(2)));
|
assert_eq!(workspaces.activate(workspace).unwrap(), Some(WindowId(2)));
|
||||||
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
||||||
assert!(workspaces.validate().is_ok());
|
assert!(workspaces.validate().is_ok());
|
||||||
@@ -1474,13 +1399,13 @@ mod tests {
|
|||||||
let mut workspaces = WorkspaceSet::default();
|
let mut workspaces = WorkspaceSet::default();
|
||||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
workspaces.toggle_fullscreen(WindowId(1)).unwrap();
|
||||||
workspaces.switch_to_numeric(2).unwrap();
|
workspaces.switch_to_fixture(2).unwrap();
|
||||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
workspaces
|
workspaces
|
||||||
.move_window_to_numeric(WindowId(2), 1, Axis::Horizontal, 0.5)
|
.move_window_to_fixture(WindowId(2), 1, Axis::Horizontal, 0.5)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(workspaces.switch_to_numeric(1).unwrap(), Some(WindowId(1)));
|
assert_eq!(workspaces.switch_to_fixture(1).unwrap(), Some(WindowId(1)));
|
||||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
workspaces.focus_window(WindowId(2)),
|
workspaces.focus_window(WindowId(2)),
|
||||||
@@ -1586,13 +1511,13 @@ mod tests {
|
|||||||
}
|
}
|
||||||
2 => {
|
2 => {
|
||||||
workspaces
|
workspaces
|
||||||
.switch_to_numeric((random.rotate_left(11) % 4 + 1) as u32)
|
.switch_to_fixture((random.rotate_left(11) % 4 + 1) as u32)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
3 if !windows.is_empty() => {
|
3 if !windows.is_empty() => {
|
||||||
let window = windows[random as usize % windows.len()];
|
let window = windows[random as usize % windows.len()];
|
||||||
workspaces
|
workspaces
|
||||||
.move_window_to_numeric(
|
.move_window_to_fixture(
|
||||||
window,
|
window,
|
||||||
(random.rotate_left(17) % 4 + 1) as u32,
|
(random.rotate_left(17) % 4 + 1) as u32,
|
||||||
random_axis(random),
|
random_axis(random),
|
||||||
@@ -1651,3 +1576,13 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Workspace {
|
||||||
|
pub fn window_count(&self) -> usize {
|
||||||
|
self.layout.window_ids().count() + self.floating.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.layout.window_ids().next().is_none() && self.floating.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::{OutputId, OutputWorkspaceMap, WorkspaceId, WorkspaceSet};
|
||||||
|
|
||||||
|
/// Shared presentation model. Identity never changes when positions compact.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct WorkspaceView {
|
||||||
|
pub id: WorkspaceId,
|
||||||
|
pub output: OutputId,
|
||||||
|
pub index: u32,
|
||||||
|
pub window_count: usize,
|
||||||
|
pub visible: bool,
|
||||||
|
pub focused: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OutputWorkspaceMap {
|
||||||
|
pub fn workspace_views(&self, workspaces: &WorkspaceSet) -> Vec<WorkspaceView> {
|
||||||
|
let mut outputs = self.connected_outputs().collect::<Vec<_>>();
|
||||||
|
outputs.sort_unstable();
|
||||||
|
|
||||||
|
outputs
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|output| {
|
||||||
|
self.ordered_workspaces(workspaces, output)
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(index, id)| {
|
||||||
|
let visible = self.active_workspace(output) == Some(id);
|
||||||
|
WorkspaceView {
|
||||||
|
id,
|
||||||
|
output,
|
||||||
|
index: index as u32 + 1,
|
||||||
|
window_count: workspaces.workspace(id).unwrap().window_count(),
|
||||||
|
visible,
|
||||||
|
focused: visible && self.focused_output() == Some(output),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec<WorkspaceId> {
|
||||||
|
let mut ids = workspaces
|
||||||
|
.iter()
|
||||||
|
.filter(|workspace| self.output_for_workspace(workspace.id) == Some(output))
|
||||||
|
.map(|workspace| workspace.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ids.sort_unstable_by_key(|id| id.0);
|
||||||
|
ids
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Out-of-range numeric selection resolves to the trailing spare. Looking
|
||||||
|
/// up a gesture target never creates workspaces or changes history.
|
||||||
|
pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option<WorkspaceId> {
|
||||||
|
let index = index.checked_sub(1)? as usize;
|
||||||
|
let ids = self.ordered_workspaces(workspaces, output);
|
||||||
|
ids.get(index).or_else(|| ids.last()).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reconcile after membership, selection, output, or transition changes.
|
||||||
|
/// History is a reference, not a reason to keep an empty workspace alive.
|
||||||
|
pub fn reconcile_workspaces(
|
||||||
|
&mut self,
|
||||||
|
workspaces: &mut WorkspaceSet,
|
||||||
|
transitions: &HashSet<WorkspaceId>,
|
||||||
|
) -> Vec<WorkspaceId> {
|
||||||
|
if let Some(active) = self.focused_output().and_then(|output| self.active_workspace(output)) {
|
||||||
|
workspaces.activate(active).expect("output's active workspace exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut protected = transitions.clone();
|
||||||
|
let outputs = self.connected_outputs().collect::<Vec<_>>();
|
||||||
|
for output in outputs {
|
||||||
|
let ordered = self.ordered_workspaces(workspaces, output);
|
||||||
|
if let Some(active) = self.active_workspace(output) {
|
||||||
|
protected.insert(active);
|
||||||
|
}
|
||||||
|
|
||||||
|
let spare = ordered
|
||||||
|
.last()
|
||||||
|
.copied()
|
||||||
|
.filter(|id| workspaces.workspace(*id).is_some_and(|w| w.is_empty()))
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let id = workspaces.create_workspace();
|
||||||
|
self.assign_workspace(output, id).expect("connected output");
|
||||||
|
id
|
||||||
|
});
|
||||||
|
protected.insert(spare);
|
||||||
|
}
|
||||||
|
|
||||||
|
let removed = workspaces
|
||||||
|
.iter()
|
||||||
|
.filter(|workspace| {
|
||||||
|
workspace.is_empty() && !protected.contains(&workspace.id) && workspace.id != workspaces.active_id()
|
||||||
|
})
|
||||||
|
.map(|workspace| workspace.id)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for id in &removed {
|
||||||
|
self.forget_workspace(*id);
|
||||||
|
assert!(workspaces.remove_empty(*id));
|
||||||
|
}
|
||||||
|
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ferese_layout::{Axis, Rect, WindowId};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::OutputGeometry;
|
||||||
|
|
||||||
|
fn setup() -> (WorkspaceSet, OutputWorkspaceMap, OutputId) {
|
||||||
|
let set = WorkspaceSet::default();
|
||||||
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
|
let output = OutputId(1);
|
||||||
|
outputs
|
||||||
|
.connect(output, OutputGeometry::new(0, 0, 1000, 800), set.active_id())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
(set, outputs, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reconcile(set: &mut WorkspaceSet, outputs: &mut OutputWorkspaceMap) {
|
||||||
|
outputs.reconcile_workspaces(set, &HashSet::new());
|
||||||
|
assert!(set.validate().is_ok());
|
||||||
|
|
||||||
|
for output in outputs.connected_outputs() {
|
||||||
|
let ids = outputs.ordered_workspaces(set, output);
|
||||||
|
assert!(set.workspace(*ids.last().unwrap()).unwrap().is_empty());
|
||||||
|
let empties = ids.iter().filter(|id| set.workspace(**id).unwrap().is_empty()).count();
|
||||||
|
let active_middle_empty = outputs
|
||||||
|
.active_workspace(output)
|
||||||
|
.is_some_and(|id| Some(&id) != ids.last() && set.workspace(id).unwrap().is_empty());
|
||||||
|
assert_eq!(empties, 1 + usize::from(active_middle_empty));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn occupied_workspaces_get_one_spare_and_positions_compact_without_changing_ids() {
|
||||||
|
let (mut set, mut outputs, output) = setup();
|
||||||
|
let first = set.active_id();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert_eq!(set.iter().count(), 1);
|
||||||
|
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let second = outputs.workspace_at(&set, output, 2).unwrap();
|
||||||
|
outputs.switch_workspace(output, second).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let spare = outputs.workspace_at(&set, output, 3).unwrap();
|
||||||
|
|
||||||
|
set.remove_window(WindowId(1)).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
|
||||||
|
assert!(set.workspace(first).is_none());
|
||||||
|
assert_eq!(outputs.previous_workspace(output), None);
|
||||||
|
assert_eq!(outputs.workspace_at(&set, output, 1), Some(second));
|
||||||
|
assert_eq!(outputs.workspace_at(&set, output, 99), Some(spare));
|
||||||
|
assert_eq!(outputs.workspace_at(&set, output, 0), None);
|
||||||
|
assert_eq!(
|
||||||
|
outputs
|
||||||
|
.workspace_views(&set)
|
||||||
|
.iter()
|
||||||
|
.map(|view| view.index)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![1, 2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn selected_empty_survives_until_leaving_but_history_does_not_pin_it() {
|
||||||
|
let (mut set, mut outputs, output) = setup();
|
||||||
|
let first = set.active_id();
|
||||||
|
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let spare = outputs.workspace_at(&set, output, 2).unwrap();
|
||||||
|
set.remove_window(WindowId(1)).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert!(set.workspace(first).is_some());
|
||||||
|
|
||||||
|
outputs.switch_workspace(output, spare).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
|
||||||
|
assert_eq!(outputs.ordered_workspaces(&set, output), vec![spare]);
|
||||||
|
assert_eq!(outputs.previous_workspace(output), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transitions_temporarily_protect_empty_workspaces_and_cancelled_lookup_has_no_effect() {
|
||||||
|
let (mut set, mut outputs, output) = setup();
|
||||||
|
let first = set.active_id();
|
||||||
|
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let spare = outputs.workspace_at(&set, output, 2).unwrap();
|
||||||
|
let before = outputs.workspace_views(&set);
|
||||||
|
for _ in 0..10 {
|
||||||
|
assert_eq!(outputs.workspace_at(&set, output, 2), Some(spare));
|
||||||
|
}
|
||||||
|
assert_eq!(outputs.workspace_views(&set), before);
|
||||||
|
assert_eq!(outputs.previous_workspace(output), None);
|
||||||
|
|
||||||
|
outputs.switch_workspace(output, spare).unwrap();
|
||||||
|
set.remove_window(WindowId(1)).unwrap();
|
||||||
|
outputs.reconcile_workspaces(&mut set, &HashSet::from([first]));
|
||||||
|
assert!(set.workspace(first).is_some());
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert!(set.workspace(first).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn floating_windows_count_as_occupied_and_last_close_preserves_active_identity() {
|
||||||
|
let (mut set, mut outputs, output) = setup();
|
||||||
|
let first = set.active_id();
|
||||||
|
set.insert_floating_window(WindowId(1), first, Rect::new(0., 0., 100., 100.), true)
|
||||||
|
.unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert_eq!(outputs.workspace_views(&set)[0].window_count, 1);
|
||||||
|
assert_eq!(set.iter().count(), 2);
|
||||||
|
|
||||||
|
set.remove_window(WindowId(1)).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert_eq!(outputs.active_workspace(output), Some(first));
|
||||||
|
let spare = outputs.workspace_at(&set, output, 2).unwrap();
|
||||||
|
outputs.switch_workspace(output, spare).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert_eq!(set.iter().count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn monitors_have_independent_positions_and_reconnect_restores_occupied_workspaces() {
|
||||||
|
let (mut set, mut outputs, left) = setup();
|
||||||
|
let first = set.active_id();
|
||||||
|
set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let right = OutputId(2);
|
||||||
|
let second = set.create_workspace();
|
||||||
|
outputs
|
||||||
|
.connect(right, OutputGeometry::new(1000, 0, 1000, 800), second)
|
||||||
|
.unwrap();
|
||||||
|
outputs.focus_output(right).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
assert_eq!(outputs.workspace_at(&set, left, 1), Some(first));
|
||||||
|
assert_eq!(outputs.workspace_at(&set, right, 1), Some(second));
|
||||||
|
assert_eq!(
|
||||||
|
outputs.workspace_views(&set).iter().filter(|view| view.visible).count(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
outputs.workspace_views(&set).iter().filter(|view| view.focused).count(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
outputs.disconnect(right).unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
let fallback = set.create_workspace();
|
||||||
|
outputs
|
||||||
|
.connect(right, OutputGeometry::new(1000, 0, 1000, 800), fallback)
|
||||||
|
.unwrap();
|
||||||
|
reconcile(&mut set, &mut outputs);
|
||||||
|
|
||||||
|
assert_eq!(outputs.output_for_workspace(first), Some(left));
|
||||||
|
assert_eq!(outputs.output_for_workspace(second), Some(right));
|
||||||
|
assert!(set.workspace(fallback).is_none());
|
||||||
|
assert_eq!(outputs.workspace_at(&set, right, 1), Some(second));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod sequences {
|
||||||
|
use ferese_layout::{Axis, WindowId};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::OutputGeometry;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn repeated_switch_move_close_and_hotplug_keep_workspace_invariants() {
|
||||||
|
let mut set = WorkspaceSet::default();
|
||||||
|
let mut outputs = OutputWorkspaceMap::default();
|
||||||
|
outputs
|
||||||
|
.connect(OutputId(1), OutputGeometry::new(0, 0, 1000, 800), set.active_id())
|
||||||
|
.unwrap();
|
||||||
|
let mut next_window = 1;
|
||||||
|
let mut seed = 0x1234_5678_u64;
|
||||||
|
|
||||||
|
for step in 0..500 {
|
||||||
|
outputs.reconcile_workspaces(&mut set, &HashSet::new());
|
||||||
|
let output = outputs.focused_output().unwrap();
|
||||||
|
let windows = set
|
||||||
|
.iter()
|
||||||
|
.flat_map(|workspace| workspace.layout.window_ids())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
seed ^= seed << 13;
|
||||||
|
seed ^= seed >> 7;
|
||||||
|
seed ^= seed << 17;
|
||||||
|
|
||||||
|
match seed % 5 {
|
||||||
|
0 => {
|
||||||
|
set.insert_window(WindowId(next_window), Axis::Horizontal, 0.5).unwrap();
|
||||||
|
next_window += 1;
|
||||||
|
}
|
||||||
|
1 if !windows.is_empty() => {
|
||||||
|
set.remove_window(windows[seed as usize % windows.len()]).unwrap();
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let index = (seed % 9 + 1) as u32;
|
||||||
|
let target = outputs.workspace_at(&set, output, index).unwrap();
|
||||||
|
outputs.switch_workspace(output, target).unwrap();
|
||||||
|
}
|
||||||
|
3 if !windows.is_empty() => {
|
||||||
|
let target = outputs.workspace_at(&set, output, u32::MAX).unwrap();
|
||||||
|
set.move_window_to_workspace(windows[seed as usize % windows.len()], target, Axis::Horizontal, 0.5)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if outputs.active_workspace(OutputId(2)).is_some() {
|
||||||
|
outputs.disconnect(OutputId(2)).unwrap();
|
||||||
|
} else {
|
||||||
|
let fallback = set.create_workspace();
|
||||||
|
outputs
|
||||||
|
.connect(OutputId(2), OutputGeometry::new(1000, 0, 1000, 800), fallback)
|
||||||
|
.unwrap();
|
||||||
|
outputs.focus_output(OutputId(2)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs.reconcile_workspaces(&mut set, &HashSet::new());
|
||||||
|
assert!(set.validate().is_ok(), "step {step}");
|
||||||
|
let views = outputs.workspace_views(&set);
|
||||||
|
assert_eq!(views.iter().filter(|view| view.focused).count(), 1);
|
||||||
|
assert_eq!(views.len(), set.iter().count());
|
||||||
|
for output in outputs.connected_outputs() {
|
||||||
|
let rows = views.iter().filter(|view| view.output == output).collect::<Vec<_>>();
|
||||||
|
assert_eq!(rows.last().unwrap().window_count, 0);
|
||||||
|
for (index, row) in rows.iter().enumerate() {
|
||||||
|
assert_eq!(row.index, index as u32 + 1);
|
||||||
|
if row.window_count == 0 && index + 1 != rows.len() {
|
||||||
|
assert!(row.visible, "abandoned empty workspace at step {step}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(previous) = outputs.previous_workspace(output) {
|
||||||
|
assert!(rows.iter().any(|row| row.id == previous));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/// Radius of a concentric inset, in the same units as the outer radius.
|
||||||
|
pub fn inner_radius(outer: f32, padding: f32) -> f32 {
|
||||||
|
(outer - padding).max(0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inset_corners_preserve_the_center_and_saturate() {
|
||||||
|
assert_eq!(inner_radius(14.0, 4.0), 10.0);
|
||||||
|
assert_eq!(inner_radius(7.5, 2.25), 5.25);
|
||||||
|
assert_eq!(inner_radius(14.0, 0.0), 14.0);
|
||||||
|
assert_eq!(inner_radius(14.0, 14.0), 0.0);
|
||||||
|
assert_eq!(inner_radius(14.0, 20.0), 0.0);
|
||||||
|
assert_eq!(inner_radius(0.0, 4.0), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub mod calendar;
|
|||||||
mod contrast;
|
mod contrast;
|
||||||
pub mod controls;
|
pub mod controls;
|
||||||
pub mod gallery;
|
pub mod gallery;
|
||||||
|
mod geometry;
|
||||||
pub mod icons;
|
pub mod icons;
|
||||||
pub mod material;
|
pub mod material;
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
@@ -14,6 +15,7 @@ mod typography;
|
|||||||
|
|
||||||
pub use button::accent_button;
|
pub use button::accent_button;
|
||||||
pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance};
|
pub use contrast::{accent_color, accent_pair, apply, composite, contrast, foreground, luminance};
|
||||||
|
pub use geometry::inner_radius;
|
||||||
pub use palette::{Palette, mix, parse_color, surface_shade};
|
pub use palette::{Palette, mix, parse_color, surface_shade};
|
||||||
pub use presets::{PRESETS, Preset};
|
pub use presets::{PRESETS, Preset};
|
||||||
pub use typography::{font, text};
|
pub use typography::{font, text};
|
||||||
@@ -78,20 +78,25 @@ pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
|
|||||||
|
|
||||||
pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -> Element<'a, M> {
|
pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -> Element<'a, M> {
|
||||||
let (track, thumb) = crate::controls::switch_colors(palette, enabled, false);
|
let (track, thumb) = crate::controls::switch_colors(palette, enabled, false);
|
||||||
|
let track_radius = 10.;
|
||||||
|
let padding = 2.;
|
||||||
let knob = container(cosmic::widget::Space::new().width(16).height(16))
|
let knob = container(cosmic::widget::Space::new().width(16).height(16))
|
||||||
.width(16)
|
.width(16)
|
||||||
.height(16)
|
.height(16)
|
||||||
.class(crate::controls::surface(Color { a: opacity, ..thumb }, 8.));
|
.class(crate::controls::surface(
|
||||||
|
Color { a: opacity, ..thumb },
|
||||||
|
crate::inner_radius(track_radius, padding),
|
||||||
|
));
|
||||||
container(knob)
|
container(knob)
|
||||||
.width(36)
|
.width(36)
|
||||||
.height(20)
|
.height(20)
|
||||||
.padding(2)
|
.padding(padding)
|
||||||
.align_x(if enabled {
|
.align_x(if enabled {
|
||||||
cosmic::iced::alignment::Horizontal::Right
|
cosmic::iced::alignment::Horizontal::Right
|
||||||
} else {
|
} else {
|
||||||
cosmic::iced::alignment::Horizontal::Left
|
cosmic::iced::alignment::Horizontal::Left
|
||||||
})
|
})
|
||||||
.class(crate::controls::surface(track.scale_alpha(opacity), 10.))
|
.class(crate::controls::surface(track.scale_alpha(opacity), track_radius))
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
|
|
||||||
- [Shortcuts and gestures](shortcuts.md): keyboard navigation, Super-drag, resize, and window grouping.
|
- [Shortcuts and gestures](shortcuts.md): keyboard navigation, Super-drag, resize, and window grouping.
|
||||||
|
|
||||||
- [Desktop widgets](desktop-widgets.md): clocks and sticky notes.
|
|
||||||
- [Notifications](notifications.md): popups, history, and Do Not Disturb.
|
|
||||||
- [Lock screen](locking.md): appearance, automatic locking, and display sleep.
|
- [Lock screen](locking.md): appearance, automatic locking, and display sleep.
|
||||||
- [Screen sharing and recording](screen-sharing.md): choosing sources and stopping a session.
|
- [Screen sharing and recording](screen-sharing.md): choosing sources and stopping a session.
|
||||||
- [Desktop portals](portals.md): application integration and supported services.
|
- [Desktop portals](portals.md): application integration and supported services.
|
||||||
@@ -21,10 +19,6 @@
|
|||||||
## Develop Ferese
|
## Develop Ferese
|
||||||
|
|
||||||
- [Development](development.md): builds, tests, and portal checks.
|
- [Development](development.md): builds, tests, and portal checks.
|
||||||
- [Compositor architecture](ferese-window-manager-spec.md).
|
|
||||||
- [Shell behavior](ferese-shell-design.md).
|
|
||||||
- [Theme components](ferese-theme-ui-spec.md).
|
|
||||||
- [Animation checks](animation-validation.md).
|
|
||||||
- [Nested resize checks](nested-resize-testing.md).
|
- [Nested resize checks](nested-resize-testing.md).
|
||||||
- [Client soak testing](native-soak-testing.md).
|
- [Client soak testing](native-soak-testing.md).
|
||||||
- [Performance measurements](performance-baseline.md).
|
- [Performance measurements](performance-baseline.md).
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
# Animation regression checks
|
|
||||||
|
|
||||||
Run `cargo test -p ferese-animation -p ferese --offline` for transition,
|
|
||||||
resize requests, reduced motion, stacking, and refresh-rate regression tests.
|
|
||||||
|
|
||||||
For visual validation, use a disposable Ferese session with three tiled windows
|
|
||||||
containing text and at least one floating window. Test both the nested and DRM
|
|
||||||
backends. Record the session at its native refresh rate when comparing builds;
|
|
||||||
single screenshots cannot establish smoothness.
|
|
||||||
|
|
||||||
| Scenario | Expected result |
|
|
||||||
| --- | --- |
|
|
||||||
| Super+F on the middle column, then exit | Neighbours remain behind the expanding window. The shrinking window stays above them until the transition finishes. |
|
|
||||||
| Toggle Super+F repeatedly at 40–80 ms intervals | Every reversal starts at the presented frame. Corners, border and shadow follow the same zoom progress. The final state matches the last input. |
|
|
||||||
| Cycle width, then immediately Super+F | The old column-width spring cannot override maximized width. |
|
|
||||||
| Exit maximize or fullscreen and immediately resize or scroll | The current frame is retained when the target changes; scrolling resumes without a position jump. |
|
|
||||||
| Drag a floating window; resize from its top-left corner | The window follows the pointer directly. The opposite edge stays anchored. Delayed client commits do not move or raise it. |
|
|
||||||
| Zoom responsive and slow-redrawing clients | Each zoom requests the destination size once. Client redraws do not generate intermediate resize requests or restart the motion. |
|
|
||||||
| Zoom the leftmost and rightmost columns in and out | Each outer edge moves monotonically toward its destination; neither side trembles or reverses near the screen edge. |
|
|
||||||
| Disable borders and shadows, then zoom | Rounded corner pixels repaint through the final frame; no corner remnants remain. |
|
|
||||||
| Repeat on 60 Hz and 144 Hz outputs; move focus between them | Similar elapsed-time motion with no double advancement. Check frame timing on actual DRM hardware. |
|
|
||||||
| Enable reduced motion | Geometry and decorations reach their final values together. |
|
|
||||||
|
|
||||||
Zoom requests the destination client size once and scales the available buffer
|
|
||||||
along the transition. This avoids repeated content reflow during motion. It is
|
|
||||||
not a buffer snapshot or a guarantee of perfectly sharp intermediate text.
|
|
||||||
+36
-21
@@ -6,8 +6,7 @@ which includes app and media shortcuts, appearance, widgets, and optional startu
|
|||||||
and display settings. Its browser/editor shortcuts need Firefox/Zed; brightness
|
and display settings. Its browser/editor shortcuts need Firefox/Zed; brightness
|
||||||
keys need `brightnessctl` and audio keys need `wpctl`.
|
keys need `brightnessctl` and audio keys need `wpctl`.
|
||||||
This reference covers layouts, appearance, input, and desktop behavior.
|
This reference covers layouts, appearance, input, and desktop behavior.
|
||||||
See [Desktop widgets](desktop-widgets.md), [Notifications](notifications.md),
|
See [Desktop portals](portals.md) for application integration options.
|
||||||
and [Desktop portals](portals.md) for their additional options.
|
|
||||||
|
|
||||||
## KDL syntax
|
## KDL syntax
|
||||||
|
|
||||||
@@ -135,8 +134,7 @@ Choose Light, Dark, or Auto in Settings or Control Center. Auto follows local
|
|||||||
07:00 and 19:00 boundaries unless you change its schedule.
|
07:00 and 19:00 boundaries unless you change its schedule.
|
||||||
|
|
||||||
Selections store preset names. Partial KDL files can override shared colors or
|
Selections store preset names. Partial KDL files can override shared colors or
|
||||||
one appearance. See [Themes](theme-engine.md) for files, accessibility options,
|
one appearance.
|
||||||
and live previews.
|
|
||||||
|
|
||||||
| Section / key | Type | Default | Meaning |
|
| Section / key | Type | Default | Meaning |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
@@ -145,7 +143,7 @@ and live previews.
|
|||||||
| `theme.split` | boolean | `false` | Use separate light/dark families; retains both selections when disabled |
|
| `theme.split` | boolean | `false` | Use separate light/dark families; retains both selections when disabled |
|
||||||
| `theme.light.family`, `theme.dark.family` | family id | `"ferese-blue"` | Stored selections used when split is enabled |
|
| `theme.light.family`, `theme.dark.family` | family id | `"ferese-blue"` | Stored selections used when split is enabled |
|
||||||
| `theme.custom-themes.<id>.file` | path | unset | Imported KDL family with explicit light and/or dark sections |
|
| `theme.custom-themes.<id>.file` | path | unset | Imported KDL family with explicit light and/or dark sections |
|
||||||
| `theme.light.preset` | preset name | `"ferese-blue-light"` | Light palette; see the [preset pairs](theme-engine.md#preset-pairs) |
|
| `theme.light.preset` | preset name | `"ferese-blue-light"` | Light palette |
|
||||||
| `theme.dark.preset` | preset name | `"ferese-blue"` | Dark palette |
|
| `theme.dark.preset` | preset name | `"ferese-blue"` | Dark palette |
|
||||||
| `theme.file` | path | unset | Shared partial KDL theme |
|
| `theme.file` | path | unset | Shared partial KDL theme |
|
||||||
| `theme.light.file`, `theme.dark.file` | path | unset | Partial KDL theme for one appearance |
|
| `theme.light.file`, `theme.dark.file` | path | unset | Partial KDL theme for one appearance |
|
||||||
@@ -183,7 +181,7 @@ Filled controls choose a contrasting text color automatically.
|
|||||||
| `border-width` | number ≥ 0 | `1` | Window border thickness |
|
| `border-width` | number ≥ 0 | `1` | Window border thickness |
|
||||||
| `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness |
|
| `focus-ring-width` | number ≥ 0 | `2` | Focused border thickness |
|
||||||
| `window-radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell |
|
| `window-radius` | number ≥ 0 | `14` | Managed-window corners, independent of the shell |
|
||||||
| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; 0 makes them square |
|
| `shell-radius` | number ≥ 0 | `14` | All shell surfaces, cards, widgets and interaction backgrounds; fractional radii are preserved; 0 makes them square |
|
||||||
| `top-bar-height` | number > 0 | `28` | Menu-bar height |
|
| `top-bar-height` | number > 0 | `28` | Menu-bar height |
|
||||||
| `top-bar-margin-top` | integer ≥ 0 | `0` | Space above bar |
|
| `top-bar-margin-top` | integer ≥ 0 | `0` | Space above bar |
|
||||||
| `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar |
|
| `top-bar-window-gap` | integer ≥ 0 | `0` | Clearance below bar |
|
||||||
@@ -223,18 +221,20 @@ theme {
|
|||||||
|
|
||||||
## Workspaces
|
## Workspaces
|
||||||
|
|
||||||
A single-output session starts with workspace 1. Numbered workspace shortcuts create
|
Each monitor has an independent list of workspaces, numbered from 1. Its bar,
|
||||||
workspaces on demand; switching forward past the last workspace also creates the
|
overview strip, and numeric shortcuts use that same order. Numbers change when
|
||||||
next one when the current workspace contains windows. Empty workspaces are
|
empty workspaces are removed; internal workspace IDs stay stable.
|
||||||
cleaned up automatically, keeping at most one spare empty workspace in addition to
|
|
||||||
visible workspaces and each monitor's previous workspace. Additional displays
|
|
||||||
receive their own workspace.
|
|
||||||
|
|
||||||
`Super+Escape` returns to the previous workspace on the focused monitor. Repeat
|
One empty workspace is always available at the end of each monitor's list.
|
||||||
it to switch between the two most recently visited workspaces. Switching to a
|
Opening or moving a window there creates another spare. An empty workspace stays
|
||||||
workspace on another monitor updates that monitor's history. History is cleared
|
while displayed or participating in a swipe, then disappears after you leave it.
|
||||||
for a disconnected monitor; it does not jump to a workspace reclaimed by another
|
Selecting a number beyond the current list selects the trailing empty workspace.
|
||||||
monitor on reconnect. With no previous workspace, the action does nothing.
|
Window-rule workspace numbers also refer to positions on the window's monitor.
|
||||||
|
|
||||||
|
`Super+Escape` returns to the previous surviving workspace on the focused monitor.
|
||||||
|
History does not keep abandoned empty workspaces alive. With no previous workspace,
|
||||||
|
the action does nothing. Disconnecting a monitor clears its history; occupied
|
||||||
|
workspaces move to another monitor and can return when it reconnects.
|
||||||
|
|
||||||
Enable **Settings → Windows → Toggle back with the same workspace shortcut**, or:
|
Enable **Settings → Windows → Toggle back with the same workspace shortcut**, or:
|
||||||
|
|
||||||
@@ -250,6 +250,22 @@ commands and overview selection still select the requested workspace directly.
|
|||||||
Use `feresectl workspace-back-and-forth` for an explicit toggle, or bind
|
Use `feresectl workspace-back-and-forth` for an explicit toggle, or bind
|
||||||
`workspace-back-and-forth` to another key or gesture. The action takes no argument.
|
`workspace-back-and-forth` to another key or gesture. The action takes no argument.
|
||||||
|
|
||||||
|
`Super+BackSpace` focuses the last focused window, including windows on other
|
||||||
|
workspaces or monitors. Repeating it toggles between the two most recently
|
||||||
|
focused windows. Layer surfaces such as launchers do not replace this history.
|
||||||
|
|
||||||
|
`Alt+Tab` previews windows in focus order using overview. Keep Alt held and press
|
||||||
|
Tab to advance, or Shift+Tab to go backward. Release Alt to focus the selection;
|
||||||
|
Escape cancels without changing focus. The order stays fixed until selection is
|
||||||
|
committed. Closed windows are skipped and newly opened windows join the next
|
||||||
|
cycle. Preview transitions follow the configured animation speed and reduced
|
||||||
|
motion settings.
|
||||||
|
|
||||||
|
Bind `focus-last-window`, `focus-mru-next`, or `focus-mru-previous` to customize
|
||||||
|
these shortcuts. MRU bindings with Alt held preview until Alt is released;
|
||||||
|
bindings without Alt, gestures, and the corresponding `feresectl` commands
|
||||||
|
switch immediately. All three actions take no arguments.
|
||||||
|
|
||||||
## Input
|
## Input
|
||||||
|
|
||||||
| `input` key | Type | Default | Meaning |
|
| `input` key | Type | Default | Meaning |
|
||||||
@@ -331,6 +347,8 @@ invalid. A disabled binding must omit `action` and `argument`.
|
|||||||
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
| `workspace`, `move-to-workspace` | Workspace number string, 1–255 |
|
||||||
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
|
| `workspace-next`, `workspace-previous` | None; next/previous workspace on this monitor |
|
||||||
| `workspace-back-and-forth` | None; return to the previous workspace on this monitor |
|
| `workspace-back-and-forth` | None; return to the previous workspace on this monitor |
|
||||||
|
| `focus-last-window` | None; return to the last focused window |
|
||||||
|
| `focus-mru-next`, `focus-mru-previous` | None; cycle windows in focus order |
|
||||||
| `none` | None; ignore this trigger |
|
| `none` | None; ignore this trigger |
|
||||||
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
| `close`, `exit`, `toggle-maximized`, `toggle-fullscreen`, `toggle-layout`, `cycle-column-width`, `center-column`, `consume`, `expel`, `toggle-floating`, `toggle-overview` | None |
|
||||||
|
|
||||||
@@ -347,6 +365,7 @@ Super+Shift+1–9 move to workspace; Super+R width cycle; Super+C center;
|
|||||||
Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
|
Super+[/] consume/expel; Super+F maximize; Super+Shift+F fullscreen;
|
||||||
Super+M layout; Super+Shift+Space floating; Super+Tab overview;
|
Super+M layout; Super+Shift+Space floating; Super+Tab overview;
|
||||||
Super+Escape previous visited workspace;
|
Super+Escape previous visited workspace;
|
||||||
|
Super+BackSpace last focused window; Alt+Tab/Alt+Shift+Tab MRU switching;
|
||||||
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
|
Super+Shift+S area screenshot; Print Screen whole-screen screenshot;
|
||||||
Super+Shift+E logout confirmation.
|
Super+Shift+E logout confirmation.
|
||||||
|
|
||||||
@@ -548,7 +567,3 @@ override shell rounding.
|
|||||||
Power actions use a centered confirmation. **Super+Shift+E** and
|
Power actions use a centered confirmation. **Super+Shift+E** and
|
||||||
`feresectl request-logout` ask to log out. `feresectl exit` ends the session
|
`feresectl request-logout` ask to log out. `feresectl exit` ends the session
|
||||||
immediately.
|
immediately.
|
||||||
|
|
||||||
## Notifications
|
|
||||||
|
|
||||||
See [Notifications](notifications.md) for popups, Do Not Disturb, and timeout options.
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
# Desktop widgets
|
|
||||||
|
|
||||||
Clock and sticky-note widgets sit above the wallpaper and behind windows.
|
|
||||||
Drag the clock or a note's title to move it. Release to save, or press Escape to
|
|
||||||
cancel. Widgets stop at each other's edges on the same display.
|
|
||||||
|
|
||||||
Enable it in Settings → Desktop widgets or in your Ferese config:
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
desktop-widgets {
|
|
||||||
clock enabled=#true anchor="top_left" margin-x=64 margin-y=80 {
|
|
||||||
style "pixel" // or "minimal"
|
|
||||||
outputs // every output; or outputs "DP-2"
|
|
||||||
width 440
|
|
||||||
height 320
|
|
||||||
font-family "" // style default; or "Comfortaa"
|
|
||||||
bold #false
|
|
||||||
time-size 128.0
|
|
||||||
date-size 18.0
|
|
||||||
time-format "%-I:%M %p" // use "%H:%M" for 24-hour time
|
|
||||||
date-format "%a, %b %-d"
|
|
||||||
time-zone "" // system zone; or "Africa/Lagos"
|
|
||||||
show-date #true
|
|
||||||
lowercase #true
|
|
||||||
color "" // theme primary text
|
|
||||||
date-color "" // theme muted text
|
|
||||||
opacity 0.9
|
|
||||||
alignment "center" // left, center, right
|
|
||||||
gap 4.0
|
|
||||||
padding 12.0
|
|
||||||
background "" // transparent; or "#10101080"
|
|
||||||
radius 16.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Positions: `top_left`, `top_center`, `top_right`, `center_left`, `center`,
|
|
||||||
`center_right`, `bottom_left`, `bottom_center`, `bottom_right`.
|
|
||||||
Margins affect anchored axes only: a centered horizontal position ignores
|
|
||||||
`margin-x`; a centered vertical position ignores `margin-y`.
|
|
||||||
All geometry and text sizes use logical pixels and follow the output's scale.
|
|
||||||
Increase the widget width/height for longer formats or larger text.
|
|
||||||
|
|
||||||
Choose **Pixel** for stacked hours and minutes with alternating theme colors,
|
|
||||||
or **Minimal** for a plain time/date layout. Pixel zero-pads hours and hides am/pm.
|
|
||||||
Formats containing seconds or extra text use a single line. Both styles shrink
|
|
||||||
time text to fit. Select a different font in Settings, or set `font-family`.
|
|
||||||
|
|
||||||
Changes apply live. Colors and fonts follow the theme unless overridden.
|
|
||||||
Formatting uses [strftime-compatible formats](https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html).
|
|
||||||
Use `%H:%M` for 24-hour time or `%-I:%M %p` for 12-hour time.
|
|
||||||
|
|
||||||
## Clock limits
|
|
||||||
|
|
||||||
Clock and note corners follow the shared shell radius. Their `radius` fields
|
|
||||||
are accepted for compatibility but do not override it.
|
|
||||||
|
|
||||||
All clock keys are optional. The example enables the clock (disabled by default);
|
|
||||||
the other values illustrate defaults or their empty-string equivalents.
|
|
||||||
`outputs` is an array of strings; `enabled`, `bold`, `show-date`, `lowercase` are
|
|
||||||
booleans; `width`, `height`, and margins are integers. Other sizes/opacity are numbers.
|
|
||||||
|
|
||||||
| Fields | Accepted range |
|
|
||||||
| --- | --- |
|
|
||||||
| `width`, `height` | 64–1600, 32–800 |
|
|
||||||
| `margin-x`, `margin-y` | 0–8192 |
|
|
||||||
| `time-size`, `date-size` | 8–240, 8–96 |
|
|
||||||
| `opacity` | 0–1 |
|
|
||||||
| `gap`, `padding`, `radius` | 0–64, 0–64, 0–128 |
|
|
||||||
| `font-family`, `time-format`, `date-format` | At most 128 bytes each |
|
|
||||||
| `outputs` | At most 32 exact output names, each 1–128 bytes |
|
|
||||||
|
|
||||||
Formats must be valid; `time-zone` must be empty or a recognized IANA name.
|
|
||||||
Colors are empty (inherit), `#RRGGBB` or `#RRGGBBAA`; all numeric values must be finite.
|
|
||||||
|
|
||||||
## Sticky notes
|
|
||||||
|
|
||||||
Settings → Desktop widgets → Add note creates a card. Drag its title to move it;
|
|
||||||
click Edit for its multiline editor, then Done to finish. Text autosaves after
|
|
||||||
500 ms without typing. Escape cancels a drag or finishes editing. Position saves
|
|
||||||
on release; dragging changes the anchor to `top_left` and saves pixel margins.
|
|
||||||
Title, size and style are also editable in Settings. Notes persist in the config.
|
|
||||||
Choose a note’s font in Settings or set `font-family "Comfortaa"` in its `note`
|
|
||||||
node. Without an override, notes use the interface font.
|
|
||||||
|
|
||||||
These are plain-text notes, not Markdown or HTML.
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
desktop-widgets {
|
|
||||||
note id="today" title="Today" text="Review the pull request\nTake a break\n" anchor="top_right" margin-x=48 margin-y=80
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Repeat the `note` node for additional notes; IDs must be unique. Cards stay behind windows
|
|
||||||
and take keyboard focus only when clicked. Set `interactive #false` for a fully
|
|
||||||
click-through card. Long text wraps; increase height if needed.
|
|
||||||
|
|
||||||
| Key | Type | Default / accepted values |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| `id` | string | `"note"`; unique, 1–64 ASCII letters/digits/`-`/`_` |
|
|
||||||
| `enabled` | boolean | `true` |
|
|
||||||
| `interactive` | boolean | `true`; drag/edit controls; `false` is click-through |
|
|
||||||
| `title` | string | `"Note"`; empty hides it; maximum 256 bytes |
|
|
||||||
| `text` | string | `""`; multiline plain text, maximum 16 KiB |
|
|
||||||
| `outputs` | string array | `[]` means all; at most 32 exact names, each 1–128 bytes |
|
|
||||||
| `anchor` | string | `"top_right"`; same nine positions as clock |
|
|
||||||
| `margin-x`, `margin-y` | integer | `48`, `80`; 0–8192; anchored edges only |
|
|
||||||
| `width`, `height` | integer | `320`, `240`; 120–1200, 80–1200 |
|
|
||||||
| `font-family` | string | omitted/empty follows desktop font; maximum 128 bytes |
|
|
||||||
| `text-size`, `title-size` | number | `16`, `18`; 8–96 |
|
|
||||||
| `color` | string | omitted/empty/`"theme"` follows primary text; or hex color |
|
|
||||||
| `background` | string | omitted/`"theme"` follows surface base; empty is transparent; or hex color |
|
|
||||||
| `opacity` | number | `0.9`; 0–1, multiplies text/background alpha |
|
|
||||||
| `padding`, `gap` | number | `20`, `8`; 0–64 |
|
|
||||||
| `radius` | number | `16`; 0–128 |
|
|
||||||
| `alignment` | string | `"left"`; `left`, `center`, `right` |
|
|
||||||
|
|
||||||
At most 32 notes; the entire live config is limited to 60 KiB.
|
|
||||||
@@ -14,14 +14,10 @@ Keep another desktop available for direct-session recovery.
|
|||||||
|
|
||||||
## Test guides
|
## Test guides
|
||||||
|
|
||||||
- [Animation checks](animation-validation.md): transitions, interruption, and hit testing.
|
|
||||||
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
|
- [Nested resize checks](nested-resize-testing.md): wallpaper and bar responsiveness.
|
||||||
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
|
- [Client soak testing](native-soak-testing.md): repeated client lifecycle and resource growth.
|
||||||
- [Performance measurements](performance-baseline.md): sampling and historical results.
|
- [Performance measurements](performance-baseline.md): sampling and historical results.
|
||||||
|
|
||||||
The [compositor](ferese-window-manager-spec.md), [shell](ferese-shell-design.md),
|
|
||||||
and [theme](ferese-theme-ui-spec.md) references describe the main code boundaries.
|
|
||||||
|
|
||||||
## Portal checks
|
## Portal checks
|
||||||
|
|
||||||
Run backend unit tests and compare D-Bus contracts against installed portal XML:
|
Run backend unit tests and compare D-Bus contracts against installed portal XML:
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
# Shell behavior
|
|
||||||
|
|
||||||
This is a maintainer reference. User options are documented in
|
|
||||||
[Configuration](configuration.md). Common UI belongs in
|
|
||||||
[ferese-theme](../crates/ferese-theme/README.md).
|
|
||||||
|
|
||||||
## Bar and popovers
|
|
||||||
|
|
||||||
The shell creates a bar for each output. Workspace buttons use compositor state
|
|
||||||
and appear in numeric order. The active window title occupies the center when
|
|
||||||
space allows. Date and time form one calendar button. Right-side controls open
|
|
||||||
sound, battery, notifications, recording, and Control Center popovers.
|
|
||||||
|
|
||||||
A popover is anchored to its control and constrained to the same output.
|
|
||||||
Opening another primary popover replaces the current one. Popovers reserve no
|
|
||||||
workspace space; the bar reserves its configured height and margins.
|
|
||||||
True fullscreen hides the bar without changing the preserved tiled layout.
|
|
||||||
|
|
||||||
Use real service availability for controls. Handle missing services and failed
|
|
||||||
actions without blocking the UI. Settings opens as a separate application.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The compositor presents window previews; the shell supplies labels and workspace
|
|
||||||
controls. Do not continuously copy windows into the shell process.
|
|
||||||
|
|
||||||
Clicking a workspace background selects that workspace and keeps overview open
|
|
||||||
so its windows can be selected. Clicking a window activates that exact window.
|
|
||||||
Directional keys change selection, Enter activates it, and Escape dismisses.
|
|
||||||
Activation must use the selected window ID, not the window focused before overview.
|
|
||||||
|
|
||||||
## Focus and dialogs
|
|
||||||
|
|
||||||
The bar and notification popups do not normally take keyboard focus. Interactive
|
|
||||||
shell surfaces restore the previous valid app focus when dismissed.
|
|
||||||
|
|
||||||
Power actions and logout use a centered system confirmation, including blockers
|
|
||||||
when present. Input outside the confirmation must not reach applications.
|
|
||||||
Authentication, screen-sharing, and recording dialogs are separate themed windows.
|
|
||||||
Use one visible frame, content-sized height, and an output-sized height limit.
|
|
||||||
Scrollable lists must leave headings and action buttons accessible.
|
|
||||||
|
|
||||||
Attach dialog material to the content card using the shared theme helper.
|
|
||||||
Match blur regions to the displayed card throughout opening and resize; do not
|
|
||||||
paint an opaque window behind a translucent card.
|
|
||||||
|
|
||||||
## Notifications and widgets
|
|
||||||
|
|
||||||
The shell provides the standard notification service. Normal popups respect
|
|
||||||
Do Not Disturb; critical messages bypass it. History is held in memory and clears
|
|
||||||
when the shell exits. See [Notifications](notifications.md).
|
|
||||||
|
|
||||||
Clock and note widgets sit behind application windows, reserve no workspace space,
|
|
||||||
and save positions after dragging. Only interactive notes take keyboard focus.
|
|
||||||
See [Desktop widgets](desktop-widgets.md).
|
|
||||||
|
|
||||||
## Lifecycle
|
|
||||||
|
|
||||||
Shell failure must not destroy managed windows or workspace state. Reconstruct
|
|
||||||
bars and controls from compositor state after reconnecting, and clear dead shell
|
|
||||||
focus. Notification history is not reconstructed from disk.
|
|
||||||
|
|
||||||
Keep slow service queries, image loading, and filesystem work away from UI event
|
|
||||||
handling. Stop animation work once the UI settles. Validate popover placement,
|
|
||||||
fullscreen transitions, dialog dismissal, and shell restart in a nested session.
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# Theme components
|
|
||||||
|
|
||||||
Ferese's native apps use [ferese-theme](../crates/ferese-theme/README.md) for
|
|
||||||
colors, fonts, controls, menus, icons, calendar elements, and dialog materials.
|
|
||||||
Use [Configuration → Appearance](configuration.md#appearance) for supported keys
|
|
||||||
and defaults.
|
|
||||||
|
|
||||||
## Shared appearance
|
|
||||||
|
|
||||||
The six theme families are Ferese Blue, Catppuccin, Gruvbox, Rosé Pine, Tokyo
|
|
||||||
Night, and Everforest. Each has a paired light and dark palette. Ferese Blue uses the
|
|
||||||
logo accent, `#3D7BE6`. Presets supply colors and materials; explicit wallpaper,
|
|
||||||
typography, geometry, and material overrides remain in place.
|
|
||||||
|
|
||||||
The interface font applies to shell text and native dialogs. Clock and note
|
|
||||||
widgets can override it. Managed-window corners and shell corners are separate
|
|
||||||
settings; small controls cap their corners to fit their size.
|
|
||||||
|
|
||||||
## Materials
|
|
||||||
|
|
||||||
Two material styles are supported:
|
|
||||||
|
|
||||||
| Style | Background |
|
|
||||||
| --- | --- |
|
|
||||||
| `solid` | Opaque theme surface |
|
|
||||||
| `translucent` | Theme tint over blurred background content |
|
|
||||||
|
|
||||||
Bars, popovers, notifications, and themed dialogs share `theme.material.opacity`
|
|
||||||
and `blur-radius`. Text and icons stay opaque. An opacity of zero hides the
|
|
||||||
material; blur radius zero disables blur. Solid mode stays opaque.
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
theme {
|
|
||||||
colors {
|
|
||||||
accent "#3D7BE6"
|
|
||||||
}
|
|
||||||
material {
|
|
||||||
style "translucent"
|
|
||||||
opacity 0.78
|
|
||||||
blur-radius 12
|
|
||||||
}
|
|
||||||
geometry {
|
|
||||||
shell-radius 14
|
|
||||||
window-radius 14
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The compositor owns blur and tint. `material::ModalMaterial` attaches native
|
|
||||||
dialogs to that rendering path. Keep the attachment alive with the window and
|
|
||||||
make the attached client background transparent. Outside Ferese, retain a client
|
|
||||||
background so the dialog remains usable.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
| Module | Use |
|
|
||||||
| --- | --- |
|
|
||||||
| `palette` | Resolve configuration colors and build the native toolkit theme |
|
|
||||||
| `controls` | Surfaces, navigation, text buttons, and input styles |
|
|
||||||
| `menus` | Rows, headings, sections, separators, switches, and sliders |
|
|
||||||
| `gallery` | Split theme previews, selected badges and accessible radio navigation |
|
|
||||||
| `calendar` | Date/time labels, clock digits, and calendar grids |
|
|
||||||
| `icons` | Shared SVG assets, sizing, and tint |
|
|
||||||
| `typography` | Shared font selection and text |
|
|
||||||
| `contrast` | Readable labels on filled controls |
|
|
||||||
| `material` | Attach modal cards to compositor materials |
|
|
||||||
|
|
||||||
Applications provide data, layout, actions, and animation progress. Extend shared
|
|
||||||
components when a style is needed in multiple apps. Avoid hardcoded app-specific
|
|
||||||
fonts, foreground colors, or opacity values.
|
|
||||||
|
|
||||||
## UI checks
|
|
||||||
|
|
||||||
- Check text and icons in all twelve light and dark variants.
|
|
||||||
- Check rest, hover, pressed, focused, selected, and disabled controls.
|
|
||||||
- Keep accent-button labels readable after typing or changing focus.
|
|
||||||
- Keep controls usable with solid materials and blur disabled.
|
|
||||||
- Align material regions with the visible card at every output scale.
|
|
||||||
- Give dialogs one frame; cap their height and scroll their content, preserving actions.
|
|
||||||
- Respect reduced motion and stop requesting animation frames after settling.
|
|
||||||
|
|
||||||
Theme changes affect appearance; they do not change focus, workspace ownership,
|
|
||||||
input permissions, or authentication policy.
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# Compositor architecture
|
|
||||||
|
|
||||||
This is a maintainer reference. For supported configuration and shortcuts, use
|
|
||||||
[Configuration](configuration.md); for builds and checks, use [Development](development.md).
|
|
||||||
|
|
||||||
## Processes and crates
|
|
||||||
|
|
||||||
| Component | Responsibility |
|
|
||||||
| --- | --- |
|
|
||||||
| `ferese` | Wayland clients, windows, workspaces, input, outputs, and rendering |
|
|
||||||
| `ferese-shell` | Bar, popovers, notifications, widgets, and system confirmations |
|
|
||||||
| `ferese-settings` | Configuration editor |
|
|
||||||
| `feresectl` | Command-line control |
|
|
||||||
| `ferese-lock` | Lock-screen UI and PAM authentication |
|
|
||||||
| `ferese-polkit-agent` | System authentication prompts |
|
|
||||||
| `xdg-desktop-portal-ferese` | Desktop portal requests and PipeWire streams |
|
|
||||||
| `ferese-record` | Video encoding and saving |
|
|
||||||
|
|
||||||
The compositor uses Smithay and calloop. The direct backend renders through
|
|
||||||
DRM/KMS; the nested backend renders into a window on another Wayland desktop.
|
|
||||||
Native UIs use libcosmic. Layout, animation, configuration, IPC, protocols, and
|
|
||||||
[theme components](../crates/ferese-theme/README.md) live in separate crates.
|
|
||||||
|
|
||||||
## State and ownership
|
|
||||||
|
|
||||||
The event loop owns compositor state and Wayland objects. Background workers
|
|
||||||
return results through bounded channels; results must be checked against current
|
|
||||||
window, output, and request identities before application.
|
|
||||||
|
|
||||||
Keep these relationships valid after a state change:
|
|
||||||
|
|
||||||
- Each window belongs to one workspace and appears once in its layout or floating list.
|
|
||||||
- Each attached workspace belongs to one output; each output has one active workspace.
|
|
||||||
- A window's output follows workspace ownership.
|
|
||||||
- Fullscreen preserves the underlying layout or floating placement.
|
|
||||||
- Focus points to a live window or authorized shell surface.
|
|
||||||
- Workspace focus history is distinct from current keyboard focus.
|
|
||||||
|
|
||||||
Monitor removal migrates workspaces to a remaining output. Returning monitors
|
|
||||||
restore eligible workspaces; explicit user moves take priority. Selecting a
|
|
||||||
workspace visible on another monitor focuses that monitor.
|
|
||||||
|
|
||||||
## Layout and presentation
|
|
||||||
|
|
||||||
Scrolling layouts arrange columns horizontally. Windows within a column divide
|
|
||||||
its height. A workspace owns its viewport; presentation subtracts that viewport
|
|
||||||
from window positions. Tree layouts use nested splits and stacks. Layout changes
|
|
||||||
preserve window membership and floating placement.
|
|
||||||
|
|
||||||
Keep layout targets, animated rectangles, and committed client sizes separate.
|
|
||||||
A layout transition requests the destination client size rather than sending
|
|
||||||
one configure per animation frame. Available content is scaled and clipped until
|
|
||||||
the matching client buffer arrives. Hit testing uses the same presented geometry.
|
|
||||||
Interrupted animations continue from their current position.
|
|
||||||
|
|
||||||
Input and layout use logical pixels. Rendering converts them using the output's
|
|
||||||
scale and transform. Application popups use their parent's output and presented
|
|
||||||
position when calculating bounds.
|
|
||||||
|
|
||||||
## Rendering and frame delivery
|
|
||||||
|
|
||||||
Render damage from client commits, animations, effects, cursor motion, and output
|
|
||||||
changes. Movement damages both old and new bounds, including shadows and blur.
|
|
||||||
Output-specific callbacks follow window ownership. Presentation feedback is
|
|
||||||
collected only for surfaces the renderer actually displayed on that output.
|
|
||||||
An unchanged frame still permits paced client callbacks without submitting a
|
|
||||||
new display buffer.
|
|
||||||
|
|
||||||
Animations use elapsed time across outputs, with a timer while transitions remain
|
|
||||||
active. An idle focused monitor must not stall another monitor's animation.
|
|
||||||
|
|
||||||
The compositor renders blur from content behind the requesting surface. Material
|
|
||||||
regions and client content must share coordinates during placement, scaling,
|
|
||||||
and animation. See [Theme components](ferese-theme-ui-spec.md).
|
|
||||||
|
|
||||||
## Input
|
|
||||||
|
|
||||||
Use milliseconds for keyboard, pointer, touch, and gesture protocol events;
|
|
||||||
relative-motion timestamps retain microseconds. Keep keyboard focus, pointer
|
|
||||||
focus, and window stacking separate. Hover focus does not scroll or raise windows.
|
|
||||||
|
|
||||||
Pointer grabs and constraints must end cleanly when their surface disappears.
|
|
||||||
A locked pointer's position hint is stored while locked, then resolved against
|
|
||||||
current surface geometry and clamped to an output when the lock is released.
|
|
||||||
|
|
||||||
## IPC and private protocols
|
|
||||||
|
|
||||||
User control uses `$XDG_RUNTIME_DIR/ferese/control.sock`, restricted to the
|
|
||||||
session user. The parent directory is private and peers are checked by UID.
|
|
||||||
IPC frames are length-prefixed JSON with bounded message sizes, workers, and queues.
|
|
||||||
See `feresectl --help` for commands.
|
|
||||||
|
|
||||||
Private Wayland capabilities grant shell control or material effects to trusted
|
|
||||||
components. Public layer-shell access alone grants neither. Trusted connections
|
|
||||||
and descriptors must not leak to launched applications. Capture paths check
|
|
||||||
session policy and reject capture while locked.
|
|
||||||
|
|
||||||
Session locking must protect every output, including hotplug, and remain secure
|
|
||||||
if the lock UI crashes. [Portal integration](portals.md) documents application
|
|
||||||
consent and the currently supported services.
|
|
||||||
|
|
||||||
## Review and testing
|
|
||||||
|
|
||||||
Check output ownership, presented geometry, focus restoration, request lifetimes,
|
|
||||||
and bounded work whenever changing a protocol or rendering path. Test both
|
|
||||||
backends; a nested result does not establish hardware behavior.
|
|
||||||
|
|
||||||
Use the [animation](animation-validation.md), [resize](nested-resize-testing.md),
|
|
||||||
and [soak](native-soak-testing.md) checks alongside the workspace tests.
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Notifications
|
|
||||||
|
|
||||||
Ferese shows compact notification popups and a history panel opened from the bar.
|
|
||||||
Their colors, font, corners, and background follow the shell theme.
|
|
||||||
|
|
||||||
## Popups and history
|
|
||||||
|
|
||||||
Up to three popups are visible at once. Hovering pauses a popup's timer.
|
|
||||||
History keeps the latest 100 notifications and clears when the shell exits.
|
|
||||||
App action buttons work when provided by the sender. Transient messages leave
|
|
||||||
history when closed.
|
|
||||||
|
|
||||||
Critical notifications bypass Do Not Disturb and remain visible unless the app
|
|
||||||
provides a timeout. Other apps can also request a timeout or no timeout.
|
|
||||||
|
|
||||||
## Settings
|
|
||||||
|
|
||||||
**Settings → Notifications** controls popup visibility, the default timeout, and
|
|
||||||
the Do Not Disturb preference. The bar toggle changes Do Not Disturb for the
|
|
||||||
current session; Settings saves it for future sessions. Hidden popups still enter
|
|
||||||
history.
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
notifications {
|
|
||||||
show-popups #true
|
|
||||||
do-not-disturb #false
|
|
||||||
timeout-ms 6000
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The default timeout accepts 1000–30000 milliseconds.
|
|
||||||
|
|
||||||
## Send a test notification
|
|
||||||
|
|
||||||
```sh
|
|
||||||
notify-send --app-name=Ferese --icon=dialog-information "Hello" "Your desktop notification"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
Only one notification server can run on the session bus. The session launcher
|
|
||||||
stops `swaync.service` before starting the shell. When launching the shell manually,
|
|
||||||
stop any existing notification server first. If another server owns the interface,
|
|
||||||
Ferese logs that native notifications are unavailable.
|
|
||||||
|
|
||||||
Ferese supports plain text, app icons, actions, replacement, and urgency. Rich
|
|
||||||
markup, embedded images, sounds, and saved notification history are not supported.
|
|
||||||
@@ -21,6 +21,27 @@ frames. Keep build work and unrelated activity outside the sampling interval.
|
|||||||
CPU percentages below use one core as 100%. Short samples are observations,
|
CPU percentages below use one core as 100%. Short samples are observations,
|
||||||
not guarantees or hardware-session benchmarks.
|
not guarantees or hardware-session benchmarks.
|
||||||
|
|
||||||
|
## Direct frame timing
|
||||||
|
|
||||||
|
The DRM backend schedules each output against its own predicted presentation
|
||||||
|
time. Render cost and a safety margin determine when compositing starts. Idle
|
||||||
|
outputs wake on demand; callback-only updates are paced to the output refresh
|
||||||
|
cycle without submitting unchanged buffers. Animation forecasts are restored
|
||||||
|
before processing input or rendering another output.
|
||||||
|
|
||||||
|
For a direct session, enable `FERESE_TRACE_PERFORMANCE=1` and
|
||||||
|
`RUST_LOG=ferese::render=debug` before starting Ferese. The timing records include
|
||||||
|
the planned render start, actual render start, timer lateness, presentation
|
||||||
|
target, DRM presentation timestamp, and request-to-presentation duration.
|
||||||
|
Missed deadlines compare submitted frames with their targets; idle gaps do not
|
||||||
|
count. Request-to-presentation starts at the compositor's redraw request, so it
|
||||||
|
does not include device input latency, client rendering before the request, or
|
||||||
|
the display's pixel response. Render cost measures CPU preparation and queueing;
|
||||||
|
asynchronous GPU work is covered conservatively by feedback-driven margins.
|
||||||
|
|
||||||
|
Check sustained animation, idle wakeup, callback-only clients, and mixed-refresh
|
||||||
|
outputs separately. Nested sessions do not exercise the DRM scheduler.
|
||||||
|
|
||||||
## Historical results
|
## Historical results
|
||||||
|
|
||||||
On 2026-09-27 at `08a5866`, two nested runs used a 1422 × 1696 output at 2× scale,
|
On 2026-09-27 at `08a5866`, two nested runs used a 1422 × 1696 output at 2× scale,
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
# Themes
|
|
||||||
|
|
||||||
The compositor resolves theme files and publishes a versioned snapshot. Native
|
|
||||||
apps consume that snapshot; Settings edits references and requests isolated previews.
|
|
||||||
|
|
||||||
## Preset pairs
|
|
||||||
|
|
||||||
| Family | Dark preset | Light preset |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Ferese Blue (default) | `ferese-blue` | `ferese-blue-light` |
|
|
||||||
| Catppuccin | `catppuccin-mocha` | `catppuccin-latte` |
|
|
||||||
| Gruvbox | `gruvbox` | `gruvbox-light` |
|
|
||||||
| Rosé Pine | `rose-pine-moon` | `rose-pine-dawn` |
|
|
||||||
| Tokyo Night | `tokyo-night` | `tokyo-night-day` |
|
|
||||||
| Everforest | `everforest-dark` | `everforest-light` |
|
|
||||||
|
|
||||||
Each variant supplies its own surfaces, tint strength, opacity, blur, borders,
|
|
||||||
and shadows. Shared material overrides apply to both appearances; appearance
|
|
||||||
specific overrides take precedence. [Palette sources and adjustments](theme-palette-sources.md)
|
|
||||||
record the upstream colors and contrast tuning.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
theme {
|
|
||||||
mode "auto"
|
|
||||||
family "catppuccin"
|
|
||||||
split #false
|
|
||||||
file "themes/custom_theme.kdl"
|
|
||||||
light {
|
|
||||||
preset "ferese-blue-light"
|
|
||||||
file "themes/custom_theme_light.kdl"
|
|
||||||
material { opacity 0.94; tint-strength 1; }
|
|
||||||
}
|
|
||||||
dark { preset "ferese-blue"; file "themes/custom_theme_dark.kdl"; }
|
|
||||||
schedule { source "schedule"; timezone "system"; light-at "07:00"; dark-at "19:00"; }
|
|
||||||
accessibility { increase-contrast #false; reduce-transparency #false; }
|
|
||||||
accent "#3D7BE6"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Paths are relative to the configuration directory. Partial theme files contain
|
|
||||||
`theme { colors { accent "#3D7BE6"; } }`, or the token sections directly.
|
|
||||||
Merge order is preset, shared file, appearance file, shared inline tokens,
|
|
||||||
appearance inline tokens, then accent and accessibility adjustments. Unknown tokens warn; invalid known values reject
|
|
||||||
an entire update and retain the last working snapshot.
|
|
||||||
|
|
||||||
## Snapshot
|
|
||||||
|
|
||||||
`ResolvedTheme` contains effective appearance and typed colors, surfaces, material,
|
|
||||||
geometry, typography, wallpaper, border paints, and shadow tokens. Base, raised,
|
|
||||||
and application backgrounds are separate colors. Selected mode,
|
|
||||||
accessibility inputs, requested accent, adjusted accent, warnings, schema version,
|
|
||||||
and revision accompany it. Consumers do not infer appearance from brightness.
|
|
||||||
|
|
||||||
Material tokens differ by appearance. Light translucent surfaces use full tint strength and
|
|
||||||
0.94 default opacity to stay pale over dark wallpapers; dark surfaces use a softer
|
|
||||||
0.5 tint. Shared material overrides apply to both unless the appearance overrides
|
|
||||||
them. Reduce Transparency resolves to solid
|
|
||||||
material with zero blur. Increase Contrast adjusts text, borders, and translucent
|
|
||||||
surface tint for 7:1 text contrast, and enables native high-contrast controls.
|
|
||||||
Accents are adjusted against surfaces independently for each appearance, then
|
|
||||||
on-accent text is selected for readability.
|
|
||||||
|
|
||||||
## Scheduling
|
|
||||||
|
|
||||||
Auto selects the most recent light or dark boundary in the configured timezone.
|
|
||||||
The interval is circular, so light may start after dark. Equal boundaries are
|
|
||||||
invalid. Repeated hours use their first occurrence; skipped hours shift forward
|
|
||||||
by the timezone gap. The next transition arms a realtime timer. Resume, timezone
|
|
||||||
replacement, and realtime clock jumps trigger recalculation without polling.
|
|
||||||
|
|
||||||
## Transitions
|
|
||||||
|
|
||||||
Prepare the incoming wallpaper before starting a 250 ms eased transition. Decode
|
|
||||||
or upload failures and a bounded readiness timeout retain the old wallpaper and
|
|
||||||
allow the theme change. Reduced motion and accessibility changes apply immediately.
|
|
||||||
Transitions preserve their exact resolved endpoints. Changes within one appearance
|
|
||||||
keep the resolved material settings instead of adding temporary tint corrections.
|
|
||||||
|
|
||||||
Surface, tint, border, shadow, and accent colors can transition. Fonts and geometry
|
|
||||||
switch atomically. Foregrounds remain readable across dark and light backdrops. Near the neutral
|
|
||||||
midpoint, the transition temporarily strengthens material tint instead of fading
|
|
||||||
text through the background color. Tests sample primary, muted,
|
|
||||||
and on-accent text throughout the transition. Wallpaper changes use prepared
|
|
||||||
images rather than a placeholder.
|
|
||||||
|
|
||||||
Publish effective appearance at transition start. The portal emits changed
|
|
||||||
`color-scheme`, `accent-color`, `contrast`, and `reduced-motion` values then.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
| Command | Action |
|
|
||||||
| --- | --- |
|
|
||||||
| `feresectl theme get` | Read the current mode, effective appearance, and colors |
|
|
||||||
| `feresectl theme status` | Read warnings and the last reload error |
|
|
||||||
| `feresectl theme subscribe` | Stream changes as JSON; no polling |
|
|
||||||
| `feresectl theme mode light` | Save Light mode; also accepts `dark` and `auto` |
|
|
||||||
| `feresectl theme preview PATH` | Resolve a candidate config without applying it |
|
|
||||||
|
|
||||||
Settings shows six paired previews, with Light on the left and Dark on the right.
|
|
||||||
Auto uses a switch in the page header. The adjacent button shows its source and,
|
|
||||||
when enabled, the effective appearance. It opens source and schedule controls
|
|
||||||
below the gallery, which can be edited before enabling Auto. Enabling Auto
|
|
||||||
immediately follows the chosen source; disabling it keeps the current appearance.
|
|
||||||
Manual appearance controls are in Control Center. Selecting a family
|
|
||||||
applies the matching variant live and offers Undo for five
|
|
||||||
seconds. Hovering does not change the desktop. Arrow keys navigate the gallery.
|
|
||||||
Enable **Use different themes for light and dark** for two independent galleries.
|
|
||||||
Disabling it uses the active family and retains the stored pair. A missing
|
|
||||||
variant uses Ferese Blue for that appearance and displays a fallback note.
|
|
||||||
|
|
||||||
**Follow system** reads `org.gnome.desktop.interface color-scheme` and monitors
|
|
||||||
changes. `default` and `prefer-light` mean Light; `prefer-dark` means Dark. If the
|
|
||||||
setting is unavailable, Ferese uses Dark and displays a note. This source does
|
|
||||||
not read Ferese's own appearance portal, avoiding a feedback loop. Sunrise/sunset
|
|
||||||
is deferred.
|
|
||||||
|
|
||||||
The gallery’s Import button adds a family to the gallery without selecting it. Give each
|
|
||||||
variant an explicit section; omitted tokens inherit Ferese Blue for that
|
|
||||||
appearance. For example:
|
|
||||||
|
|
||||||
```kdl
|
|
||||||
theme {
|
|
||||||
name "My theme"
|
|
||||||
light { colors { accent "#8F5300"; }; }
|
|
||||||
dark { colors { accent "#CBA6F7"; }; }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Settings stores the file reference under `theme.custom-themes.<id>.file`.
|
|
||||||
The compositor validates it and publishes its palettes alongside built-ins.
|
|
||||||
Omit a variant to make a single-variant family. Token-only partial files remain
|
|
||||||
supported as shared/per-appearance overrides; imports require explicit variants
|
|
||||||
so appearance is never guessed from brightness. Imported and override files
|
|
||||||
reload after a short debounce, including saves that replace the file. Invalid
|
|
||||||
updates retain the last working theme and produce one desktop notification.
|
|
||||||
|
|
||||||
High contrast, Reduce Transparency and Reduce Motion are in Accessibility.
|
|
||||||
Material, accent, font and geometry controls stay visible below the gallery. Theme
|
|
||||||
overrides are under Advanced customization, in one file-picker row with a
|
|
||||||
Shared/Light/Dark selector; each stored
|
|
||||||
reference is retained when selecting another scope. Settings and shell switches
|
|
||||||
share the same palette. Switch and slider thumbs stay circular regardless of
|
|
||||||
shell corner radius.
|
|
||||||
|
|
||||||
When an older config first adopts mode or preset selection, Ferese moves its
|
|
||||||
inline palette into the dark selection and preserves comments. Shared wallpaper,
|
|
||||||
font, and geometry settings continue to apply to both appearances. For a wallpaper
|
|
||||||
pair, set `background { path "..."; }` inside each appearance or its theme file.
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Theme palette sources
|
|
||||||
Ferese maps upstream palettes to desktop roles. Materials are authored for Ferese;
|
|
||||||
text and accents have the adjustments below to meet desktop contrast requirements.
|
|
||||||
| Family | Source revision | License |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| catppuccin | [07d02aa110ef](https://github.com/catppuccin/palette/blob/07d02aa110ef9eb7e7427afca5c73ba9cf7f8ebd/palette.json) | MIT |
|
|
||||||
| gruvbox | [ef8864bb42bf](https://github.com/morhetz/gruvbox/blob/ef8864bb42bf244f0295d1c5a403b27e3d139695/colors/gruvbox.vim) | MIT/X11 |
|
|
||||||
| rose-pine | [ff483051a47e](https://github.com/rose-pine/neovim/blob/ff483051a47e27d84bdef47703538df1ed9f4a47/lua/rose-pine/palette.lua) | MIT |
|
|
||||||
| tokyo-night | [cdc07ac78467](https://github.com/folke/tokyonight.nvim/blob/cdc07ac78467a233fd62c493de29a17e0cf2b2b6/extras/kitty/tokyonight_day.conf) | Apache-2.0; palette exports marked MIT |
|
|
||||||
| everforest | [85a86eb62409](https://github.com/sainnhe/everforest/blob/85a86eb62409e3ec88713bff3d1b9d7374e112e4/autoload/everforest.vim) | MIT |
|
|
||||||
|
|
||||||
Tokyo Night Night also uses its [official export](https://github.com/folke/tokyonight.nvim/blob/cdc07ac78467a233fd62c493de29a17e0cf2b2b6/extras/kitty/tokyonight_night.conf).
|
|
||||||
|
|
||||||
## Color adjustments
|
|
||||||
|
|
||||||
| Variant | Token | Upstream | Ferese |
|
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| rose-pine-moon | muted | `#908CAA` | `#918DAA` |
|
|
||||||
| rose-pine-dawn | muted | `#797593` | `#716D89` |
|
|
||||||
| tokyo-night-day | text | `#3760BF` | `#3358B0` |
|
|
||||||
| tokyo-night-day | muted | `#6172B0` | `#4D5A8C` |
|
|
||||||
| tokyo-night-day | accent | `#2E7DE9` | `#2C76DD` |
|
|
||||||
| everforest-light | muted | `#829181` | `#677367` |
|
|
||||||
| everforest-light | accent | `#8DA101` | `#849601` |
|
|
||||||
|
|
||||||
Catppuccin Latte uses upstream subtext1 for muted labels. Gruvbox uses readable
|
|
||||||
foreground shades rather than its comment gray. Rosé Pine uses its subtle role
|
|
||||||
for secondary labels and its love accent in Dawn. Everforest uses the Hard pair.
|
|
||||||
Tokyo Night uses the upstream Night and Day exports.
|
|
||||||
|
|
||||||
License texts are bundled in [assets/themes/licenses](../assets/themes/licenses)
|
|
||||||
and copied into each installed release. Older saved Monochrome, Dracula, Ayu Light,
|
|
||||||
and Monokai selections remain readable, but are absent from the gallery.
|
|
||||||
@@ -159,7 +159,10 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
|||||||
| "reload-config"
|
| "reload-config"
|
||||||
| "exit"
|
| "exit"
|
||||||
| "request-logout"
|
| "request-logout"
|
||||||
| "workspace-back-and-forth" => {
|
| "workspace-back-and-forth"
|
||||||
|
| "focus-last-window"
|
||||||
|
| "focus-mru-next"
|
||||||
|
| "focus-mru-previous" => {
|
||||||
if !positional.is_empty() {
|
if !positional.is_empty() {
|
||||||
return Err(format!("{command} does not accept arguments"));
|
return Err(format!("{command} does not accept arguments"));
|
||||||
}
|
}
|
||||||
@@ -188,7 +191,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn usage() -> String {
|
fn usage() -> String {
|
||||||
"usage: feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview|toggle-keybinding-guide>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
|
"usage: feresectl autostart\n feresectl screenshot [--geometry \"x,y WxH\"]\n feresectl screenshot-window <window-id>\n feresectl <focus|move|resize> <direction>\n feresectl <workspace|move-to-workspace> <index>\n feresectl workspace-back-and-forth\n feresectl <focus-last-window|focus-mru-next|focus-mru-previous>\n feresectl <toggle-floating|toggle-maximized|toggle-fullscreen|toggle-layout|toggle-overview|toggle-keybinding-guide>\n feresectl <cycle-column-width|center-column|consume|expel|close|exit|request-logout>\n feresectl <get-focused-window|get-windows|get-workspaces|get-outputs|reload-config>".to_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -231,6 +234,10 @@ mod tests {
|
|||||||
("workspace-back-and-forth".into(), json!({}))
|
("workspace-back-and-forth".into(), json!({}))
|
||||||
);
|
);
|
||||||
assert!(parse_args(["workspace-back-and-forth".into(), "2".into()]).is_err());
|
assert!(parse_args(["workspace-back-and-forth".into(), "2".into()]).is_err());
|
||||||
|
for command in ["focus-last-window", "focus-mru-next", "focus-mru-previous"] {
|
||||||
|
assert_eq!(parse_args([command.into()]).unwrap(), (command.into(), json!({})));
|
||||||
|
assert!(parse_args([command.into(), "2".into()]).is_err());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -81,8 +81,11 @@
|
|||||||
|
|
||||||
<request name="set_regions" since="2">
|
<request name="set_regions" since="2">
|
||||||
<description summary="restrict material to rounded surface-local rectangles">
|
<description summary="restrict material to rounded surface-local rectangles">
|
||||||
An array of native-endian signed 32-bit tuples (x, y, width, height,
|
An array of native-endian IEEE 754 binary32 tuples (x, y, width, height,
|
||||||
radius), measured in logical pixels. At most 32 rectangles are allowed.
|
radius) in logical pixels. All values must be finite and have magnitude
|
||||||
|
at most 32768; width and height must be positive and radius nonnegative.
|
||||||
|
At most 32 rectangles are allowed. Invalid arrays are ignored.
|
||||||
|
Coordinates are rounded only after output scaling.
|
||||||
An empty array disables material painting without clearing the role.
|
An empty array disables material painting without clearing the role.
|
||||||
Surfaces which never send this request retain full-surface materials.
|
Surfaces which never send this request retain full-surface materials.
|
||||||
</description>
|
</description>
|
||||||
|
|||||||
@@ -110,7 +110,10 @@
|
|||||||
<arg name="output_hi" type="uint" summary="zero when the workspace has no output"/>
|
<arg name="output_hi" type="uint" summary="zero when the workspace has no output"/>
|
||||||
<arg name="output_lo" type="uint" summary="zero when the workspace has no output"/>
|
<arg name="output_lo" type="uint" summary="zero when the workspace has no output"/>
|
||||||
<arg name="name" type="string"/>
|
<arg name="name" type="string"/>
|
||||||
<arg name="active" type="uint" summary="one when visible on its output"/>
|
<arg name="index" type="uint" summary="one-based position on this output"/>
|
||||||
|
<arg name="window_count" type="uint" summary="managed windows, including floating windows"/>
|
||||||
|
<arg name="visible" type="uint" summary="one when displayed on its output"/>
|
||||||
|
<arg name="focused" type="uint" summary="one when visible on the focused output"/>
|
||||||
</event>
|
</event>
|
||||||
|
|
||||||
<event name="snapshot_end">
|
<event name="snapshot_end">
|
||||||
|
|||||||
@@ -17,11 +17,7 @@ PAGES = [
|
|||||||
('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'),
|
('why-ferese', 'Why Ferese', ROOT / 'docs/why-ferese.md'),
|
||||||
('installation', 'Installation', ROOT / 'docs/installation.md'),
|
('installation', 'Installation', ROOT / 'docs/installation.md'),
|
||||||
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
|
('configuration', 'Configuration', ROOT / 'docs/configuration.md'),
|
||||||
('theme-engine', 'Themes', ROOT / 'docs/theme-engine.md'),
|
|
||||||
('theme-palette-sources', 'Theme palette sources', ROOT / 'docs/theme-palette-sources.md'),
|
|
||||||
('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.md'),
|
('shortcuts', 'Shortcuts and gestures', ROOT / 'docs/shortcuts.md'),
|
||||||
('desktop-widgets', 'Desktop widgets', ROOT / 'docs/desktop-widgets.md'),
|
|
||||||
('notifications', 'Notifications', ROOT / 'docs/notifications.md'),
|
|
||||||
('locking', 'Lock screen', ROOT / 'docs/locking.md'),
|
('locking', 'Lock screen', ROOT / 'docs/locking.md'),
|
||||||
('screen-sharing', 'Screen sharing', ROOT / 'docs/screen-sharing.md'),
|
('screen-sharing', 'Screen sharing', ROOT / 'docs/screen-sharing.md'),
|
||||||
('portals', 'Desktop portals', ROOT / 'docs/portals.md'),
|
('portals', 'Desktop portals', ROOT / 'docs/portals.md'),
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
"""Focus history against real windows on a disposable nested compositor.
|
||||||
|
|
||||||
|
FERESE_TEST_FOCUS_HISTORY=1 python3 scripts/tests/test_focus_history_isolated.py
|
||||||
|
Requires built debug ferese/feresectl, foot, dbus-daemon, and a Wayland host.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(os.environ.get("FERESE_TEST_FOCUS_HISTORY") == "1", "nested focus test is opt-in")
|
||||||
|
class FocusHistoryTest(unittest.TestCase):
|
||||||
|
def test_last_focus_crosses_workspaces_and_skips_closed_windows(self):
|
||||||
|
repo = Path(__file__).resolve().parents[2]
|
||||||
|
with tempfile.TemporaryDirectory(prefix="ferese-focus-test-") as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
runtime = root / "runtime"
|
||||||
|
runtime.mkdir(mode=0o700)
|
||||||
|
config = root / "config/ferese"
|
||||||
|
config.mkdir(parents=True)
|
||||||
|
(config / "config.kdl").write_text("animations { enabled #false; }\n")
|
||||||
|
display = Path(os.environ["WAYLAND_DISPLAY"])
|
||||||
|
if not display.is_absolute():
|
||||||
|
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
||||||
|
env = dict(os.environ, WAYLAND_DISPLAY=str(display), XDG_RUNTIME_DIR=str(runtime),
|
||||||
|
XDG_CONFIG_HOME=str(root / "config"), XDG_STATE_HOME=str(root / "state"))
|
||||||
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
|
children = []
|
||||||
|
|
||||||
|
def launch(command, **kwargs):
|
||||||
|
child = subprocess.Popen(command, env=env, start_new_session=True, **kwargs)
|
||||||
|
children.append(child)
|
||||||
|
return child
|
||||||
|
|
||||||
|
def command(*args):
|
||||||
|
return json.loads(subprocess.check_output(
|
||||||
|
[str(repo / "target/debug/feresectl"), *args], env=env, timeout=5
|
||||||
|
))
|
||||||
|
|
||||||
|
def wait_for(predicate):
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
result = predicate()
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
time.sleep(.025)
|
||||||
|
self.fail("Timed out waiting for compositor state: " + (root / "compositor.log").read_text())
|
||||||
|
|
||||||
|
def focused():
|
||||||
|
result = command("get-focused-window")
|
||||||
|
return result["id"] if result else None
|
||||||
|
|
||||||
|
with (root / "compositor.log").open("w") as log:
|
||||||
|
try:
|
||||||
|
bus = launch(["dbus-daemon", "--session", "--nofork", "--print-address=1"],
|
||||||
|
stdout=subprocess.PIPE, text=True)
|
||||||
|
env["DBUS_SESSION_BUS_ADDRESS"] = bus.stdout.readline().strip()
|
||||||
|
launch([str(repo / "target/debug/ferese"), "--backend", "nested"], stdout=log, stderr=log)
|
||||||
|
wait_for(lambda: (runtime / "ferese/control.sock").is_socket())
|
||||||
|
command("focus-last-window")
|
||||||
|
self.assertIsNone(focused())
|
||||||
|
env["WAYLAND_DISPLAY"] = str(next(
|
||||||
|
path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")
|
||||||
|
))
|
||||||
|
windows = []
|
||||||
|
for index in range(3):
|
||||||
|
app = f"ferese.test.focus.{index}"
|
||||||
|
child = launch(["foot", "--app-id", app, "sleep", "120"], stdout=log, stderr=log)
|
||||||
|
window = wait_for(lambda: next((w for w in command("get-windows")
|
||||||
|
if w["app_id"] == app and w["width"] > 0), None))
|
||||||
|
wait_for(lambda: focused() == window["id"])
|
||||||
|
windows.append((child, window["id"]))
|
||||||
|
if index == 1:
|
||||||
|
command("workspace", "2")
|
||||||
|
first, second, third = [window for _, window in windows]
|
||||||
|
command("focus-last-window")
|
||||||
|
self.assertEqual(focused(), second)
|
||||||
|
self.assertEqual(next(w["name"] for w in command("get-workspaces") if w["active"]), "1")
|
||||||
|
command("focus-last-window")
|
||||||
|
self.assertEqual(focused(), third)
|
||||||
|
self.assertEqual(next(w["name"] for w in command("get-workspaces") if w["active"]), "2")
|
||||||
|
command("focus-mru-previous")
|
||||||
|
self.assertEqual(focused(), first, "reverse traversal reaches least recent window")
|
||||||
|
command("focus-mru-next")
|
||||||
|
self.assertEqual(focused(), third)
|
||||||
|
windows[1][0].terminate()
|
||||||
|
windows[1][0].wait(timeout=3)
|
||||||
|
wait_for(lambda: not any(w["id"] == second for w in command("get-windows")))
|
||||||
|
command("focus-last-window")
|
||||||
|
self.assertEqual(focused(), first, "closed windows must not consume a focus action")
|
||||||
|
command("reload-config")
|
||||||
|
command("focus-last-window")
|
||||||
|
self.assertEqual(focused(), third, "reload must preserve focus history")
|
||||||
|
finally:
|
||||||
|
for child in reversed(children):
|
||||||
|
if child.poll() is None:
|
||||||
|
os.killpg(child.pid, signal.SIGTERM)
|
||||||
|
try:
|
||||||
|
child.wait(timeout=3)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(child.pid, signal.SIGKILL)
|
||||||
|
child.wait(timeout=3)
|
||||||
|
if child.stdout is not None:
|
||||||
|
child.stdout.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -86,6 +86,7 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
visible_ids = []
|
||||||
for phase, visible in [
|
for phase, visible in [
|
||||||
("initial-empty", False),
|
("initial-empty", False),
|
||||||
("visible", True),
|
("visible", True),
|
||||||
@@ -110,9 +111,35 @@ class UnmappedToplevelTest(unittest.TestCase):
|
|||||||
if time.monotonic() >= deadline:
|
if time.monotonic() >= deadline:
|
||||||
self.fail(f"{phase}: unexpected focused window {focused}")
|
self.fail(f"{phase}: unexpected focused window {focused}")
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
|
if visible:
|
||||||
|
visible_ids.append(focused["id"])
|
||||||
|
if phase == "visible":
|
||||||
|
# Leave a configure/resize outstanding when the buffer is detached.
|
||||||
|
subprocess.run([str(ctl_binary), "toggle-maximized"], env=env,
|
||||||
|
capture_output=True, check=True)
|
||||||
|
|
||||||
client.stdin.write("\n")
|
client.stdin.write("\n")
|
||||||
client.stdin.flush()
|
client.stdin.flush()
|
||||||
|
self.assertNotEqual(visible_ids[0], visible_ids[1], "remapping must allocate a fresh window identity")
|
||||||
self.assertEqual(client.wait(timeout=5), 0)
|
self.assertEqual(client.wait(timeout=5), 0)
|
||||||
|
|
||||||
|
deadline = time.monotonic() + 3
|
||||||
|
while True:
|
||||||
|
remaining = json.loads(subprocess.check_output(
|
||||||
|
[str(ctl_binary), "get-windows"], env=env, text=True
|
||||||
|
))
|
||||||
|
if not remaining:
|
||||||
|
break
|
||||||
|
if time.monotonic() >= deadline:
|
||||||
|
self.fail(f"destroyed window still managed: {remaining}")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
subprocess.run([str(ctl_binary), "focus-last-window"], env=env,
|
||||||
|
capture_output=True, check=True)
|
||||||
|
focused = json.loads(subprocess.check_output(
|
||||||
|
[str(ctl_binary), "get-focused-window"], env=env, text=True
|
||||||
|
))
|
||||||
|
self.assertIsNone(focused, "focus history must not restore a destroyed window")
|
||||||
finally:
|
finally:
|
||||||
if client and client.poll() is None:
|
if client and client.poll() is None:
|
||||||
client.terminate()
|
client.terminate()
|
||||||
|
|||||||
@@ -35,10 +35,19 @@ class WindowCapture(unittest.TestCase):
|
|||||||
config = root / "config/ferese/config.kdl"
|
config = root / "config/ferese/config.kdl"
|
||||||
config.parent.mkdir(parents=True)
|
config.parent.mkdir(parents=True)
|
||||||
config.write_text('window-rule app-id="ferese.test.window-capture" floating=#true\n')
|
config.write_text('window-rule app-id="ferese.test.window-capture" floating=#true\n')
|
||||||
|
# Remembered geometry deliberately places both clients together;
|
||||||
|
# ordinary placement now avoids overlap. Keep host state isolated.
|
||||||
|
state = root / "state/ferese"
|
||||||
|
state.mkdir(parents=True)
|
||||||
|
(state / "floating.json").write_text(json.dumps({
|
||||||
|
"ferese.test.window-capture": {
|
||||||
|
"output": "ferese-winit", "fractions": [.1, .1, .4, .4],
|
||||||
|
},
|
||||||
|
}))
|
||||||
display = Path(os.environ["WAYLAND_DISPLAY"])
|
display = Path(os.environ["WAYLAND_DISPLAY"])
|
||||||
if not display.is_absolute():
|
if not display.is_absolute():
|
||||||
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
display = Path(os.environ["XDG_RUNTIME_DIR"]) / display
|
||||||
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), WAYLAND_DISPLAY=str(display), FERESE_ENABLE_SCREENCOPY="1")
|
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), XDG_CONFIG_HOME=str(root / "config"), XDG_STATE_HOME=str(root / "state"), WAYLAND_DISPLAY=str(display), FERESE_ENABLE_SCREENCOPY="1")
|
||||||
env.pop("WAYLAND_SOCKET", None)
|
env.pop("WAYLAND_SOCKET", None)
|
||||||
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
env.pop("FERESE_SHELL_CONTROL_SOCKET", None)
|
||||||
log = (root / "compositor.log").open("w")
|
log = (root / "compositor.log").open("w")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Workspace history against a temporary nested compositor.
|
"""Workspace history against a temporary nested compositor.
|
||||||
|
|
||||||
FERESE_TEST_WORKSPACE_HISTORY=1 python3 scripts/tests/test_workspace_history_isolated.py
|
FERESE_TEST_WORKSPACE_HISTORY=1 python3 scripts/tests/test_workspace_history_isolated.py
|
||||||
Requires target/debug/ferese, feresectl, dbus-daemon, and a Wayland host.
|
Requires target/debug/ferese, feresectl, foot, dbus-daemon, and a Wayland host.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -15,7 +15,7 @@ import unittest
|
|||||||
|
|
||||||
@unittest.skipUnless(os.environ.get("FERESE_TEST_WORKSPACE_HISTORY") == "1", "nested workspace test is opt-in")
|
@unittest.skipUnless(os.environ.get("FERESE_TEST_WORKSPACE_HISTORY") == "1", "nested workspace test is opt-in")
|
||||||
class WorkspaceHistoryTest(unittest.TestCase):
|
class WorkspaceHistoryTest(unittest.TestCase):
|
||||||
def test_empty_workspace_history_and_explicit_ipc_selection(self):
|
def test_dynamic_positions_spare_cleanup_and_history(self):
|
||||||
repo = Path(__file__).resolve().parents[2]
|
repo = Path(__file__).resolve().parents[2]
|
||||||
with tempfile.TemporaryDirectory(prefix="ferese-workspace-test-") as temporary:
|
with tempfile.TemporaryDirectory(prefix="ferese-workspace-test-") as temporary:
|
||||||
root = Path(temporary)
|
root = Path(temporary)
|
||||||
@@ -59,28 +59,73 @@ class WorkspaceHistoryTest(unittest.TestCase):
|
|||||||
if compositor.poll() is not None or time.monotonic() >= deadline:
|
if compositor.poll() is not None or time.monotonic() >= deadline:
|
||||||
self.fail((root / "compositor.log").read_text())
|
self.fail((root / "compositor.log").read_text())
|
||||||
time.sleep(.025)
|
time.sleep(.025)
|
||||||
self.assertEqual(active(), "1")
|
def wait_for(predicate):
|
||||||
command("workspace-back-and-forth")
|
deadline = time.monotonic() + 10
|
||||||
self.assertEqual(active(), "1", "no history is a no-op")
|
while time.monotonic() < deadline:
|
||||||
|
value = predicate()
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
time.sleep(.025)
|
||||||
|
self.fail("Timed out: " + (root / "compositor.log").read_text())
|
||||||
|
|
||||||
|
def workspaces():
|
||||||
|
return command("get-workspaces")
|
||||||
|
|
||||||
|
initial = workspaces()[0]["id"]
|
||||||
|
self.assertEqual(len(workspaces()), 1)
|
||||||
|
command("workspace", "9")
|
||||||
|
self.assertEqual(workspaces()[0]["id"], initial)
|
||||||
|
self.assertEqual(len(workspaces()), 1, "numeric lookup must not create gaps")
|
||||||
|
env["WAYLAND_DISPLAY"] = str(next(
|
||||||
|
path for path in runtime.glob("wayland-*") if not path.name.endswith(".lock")
|
||||||
|
))
|
||||||
|
|
||||||
|
first = launch(["foot", "--app-id", "ferese.test.workspace.first", "sleep", "120"],
|
||||||
|
stdout=log, stderr=log)
|
||||||
|
wait_for(lambda: len(workspaces()) == 2 and workspaces()[0]["window_count"] == 1)
|
||||||
command("workspace", "2")
|
command("workspace", "2")
|
||||||
self.assertEqual(active(), "2")
|
second_id = next(w["id"] for w in workspaces() if w["focused"])
|
||||||
self.assertEqual({w["name"] for w in command("get-workspaces")}, {"1", "2"})
|
second = launch(["foot", "--app-id", "ferese.test.workspace.second", "sleep", "120"],
|
||||||
|
stdout=log, stderr=log)
|
||||||
|
wait_for(lambda: len(workspaces()) == 3 and workspaces()[1]["window_count"] == 1)
|
||||||
for expected in ["1", "2", "1", "2"]:
|
for expected in ["1", "2", "1", "2"]:
|
||||||
command("workspace-back-and-forth")
|
command("workspace-back-and-forth")
|
||||||
self.assertEqual(active(), expected)
|
self.assertEqual(active(), expected)
|
||||||
command("workspace", "3")
|
|
||||||
self.assertEqual({w["name"] for w in command("get-workspaces")}, {"2", "3"},
|
os.killpg(first.pid, signal.SIGTERM)
|
||||||
"old empty history is pruned")
|
first.wait(timeout=3)
|
||||||
|
wait_for(lambda: len(workspaces()) == 2)
|
||||||
|
self.assertEqual(workspaces()[0]["id"], second_id, "compaction preserves identity")
|
||||||
|
self.assertEqual([w["index"] for w in workspaces()], [1, 2])
|
||||||
|
command("workspace-back-and-forth")
|
||||||
|
self.assertEqual(active(), "1", "removed history cannot resurrect an empty workspace")
|
||||||
|
spare_id = workspaces()[1]["id"]
|
||||||
|
command("workspace", "99")
|
||||||
|
self.assertEqual(active(), "2")
|
||||||
|
self.assertEqual(next(w["id"] for w in workspaces() if w["focused"]), spare_id)
|
||||||
|
command("workspace-back-and-forth")
|
||||||
|
self.assertEqual(active(), "1")
|
||||||
|
|
||||||
replacement = source.with_suffix(".new")
|
replacement = source.with_suffix(".new")
|
||||||
replacement.write_text("animations { enabled #false; }\nworkspaces { auto-back-and-forth #true; }\n")
|
replacement.write_text("animations { enabled #false; }\nworkspaces { auto-back-and-forth #true; }\n")
|
||||||
replacement.replace(source)
|
replacement.replace(source)
|
||||||
command("reload-config")
|
command("reload-config")
|
||||||
command("workspace", "3")
|
command("workspace", "1")
|
||||||
self.assertEqual(active(), "3", "literal IPC selection must not auto-toggle")
|
self.assertEqual(active(), "1", "explicit IPC selection must not auto-toggle")
|
||||||
command("workspace-back-and-forth")
|
command("workspace-back-and-forth")
|
||||||
self.assertEqual(active(), "2", "reload must preserve history")
|
self.assertEqual(active(), "2")
|
||||||
command("workspace-back-and-forth")
|
command("workspace-back-and-forth")
|
||||||
self.assertEqual(active(), "3")
|
self.assertEqual(active(), "1")
|
||||||
|
|
||||||
|
os.killpg(second.pid, signal.SIGTERM)
|
||||||
|
second.wait(timeout=3)
|
||||||
|
wait_for(lambda: all(w["window_count"] == 0 for w in workspaces()))
|
||||||
|
command("workspace", "2")
|
||||||
|
wait_for(lambda: len(workspaces()) == 1)
|
||||||
|
self.assertEqual(workspaces()[0]["id"], spare_id)
|
||||||
|
self.assertEqual(workspaces()[0]["name"], "1")
|
||||||
|
self.assertTrue(workspaces()[0]["visible"])
|
||||||
|
self.assertTrue(workspaces()[0]["focused"])
|
||||||
finally:
|
finally:
|
||||||
for child in reversed(children):
|
for child in reversed(children):
|
||||||
if child.poll() is None:
|
if child.poll() is None:
|
||||||
|
|||||||
@@ -38,20 +38,19 @@ impl FereseShell {
|
|||||||
.spacing(1)
|
.spacing(1)
|
||||||
.align_y(cosmic::iced::Alignment::Center);
|
.align_y(cosmic::iced::Alignment::Center);
|
||||||
|
|
||||||
for workspace in &self.snapshot.workspaces {
|
for workspace in self
|
||||||
|
.snapshot
|
||||||
|
.workspaces_for_output(focused_output.map(|output| output.id))
|
||||||
|
{
|
||||||
let active = workspace_active_on_bar(workspace.id, focused_output);
|
let active = workspace_active_on_bar(workspace.id, focused_output);
|
||||||
let owner = workspace
|
let owner = workspace
|
||||||
.output
|
.output
|
||||||
.and_then(|id| self.snapshot.outputs.iter().find(|output| output.id == id));
|
.and_then(|id| self.snapshot.outputs.iter().find(|output| output.id == id));
|
||||||
let occupied = self
|
let occupied = workspace.window_count > 0;
|
||||||
.snapshot
|
|
||||||
.windows
|
|
||||||
.iter()
|
|
||||||
.any(|window| window.workspace == workspace.id);
|
|
||||||
let indicator = workspace_indicator(
|
let indicator = workspace_indicator(
|
||||||
&workspace.name,
|
&workspace.name,
|
||||||
active,
|
active,
|
||||||
workspace.active && !active,
|
workspace.visible && !active,
|
||||||
occupied,
|
occupied,
|
||||||
bar,
|
bar,
|
||||||
shell_theme,
|
shell_theme,
|
||||||
@@ -59,8 +58,14 @@ impl FereseShell {
|
|||||||
let workspace_button = button::custom(indicator)
|
let workspace_button = button::custom(indicator)
|
||||||
.name(format!(
|
.name(format!(
|
||||||
"Workspace {}{}{}",
|
"Workspace {}{}{}",
|
||||||
workspace.name,
|
workspace.index,
|
||||||
if active { ", active" } else { "" },
|
if workspace.focused {
|
||||||
|
", focused"
|
||||||
|
} else if active {
|
||||||
|
", visible"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
owner.map_or(String::new(), |output| format!(", on {}", output.name))
|
owner.map_or(String::new(), |output| format!(", on {}", output.name))
|
||||||
))
|
))
|
||||||
.height(bar.control_height)
|
.height(bar.control_height)
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ pub(crate) struct ShellSnapshot {
|
|||||||
pub(crate) windows: Vec<WindowSnapshot>,
|
pub(crate) windows: Vec<WindowSnapshot>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ShellSnapshot {
|
||||||
|
pub(crate) fn workspaces_for_output(&self, output: Option<u64>) -> impl Iterator<Item = &WorkspaceSnapshot> {
|
||||||
|
self.workspaces
|
||||||
|
.iter()
|
||||||
|
.filter(move |workspace| output.is_some() && workspace.output == output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct OutputSnapshot {
|
pub(crate) struct OutputSnapshot {
|
||||||
pub(crate) id: u64,
|
pub(crate) id: u64,
|
||||||
@@ -32,7 +40,10 @@ pub(crate) struct WorkspaceSnapshot {
|
|||||||
pub(crate) id: u64,
|
pub(crate) id: u64,
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) output: Option<u64>,
|
pub(crate) output: Option<u64>,
|
||||||
pub(crate) active: bool,
|
pub(crate) index: u32,
|
||||||
|
pub(crate) window_count: u32,
|
||||||
|
pub(crate) visible: bool,
|
||||||
|
pub(crate) focused: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -249,7 +260,10 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
output_hi,
|
output_hi,
|
||||||
output_lo,
|
output_lo,
|
||||||
name,
|
name,
|
||||||
active,
|
index,
|
||||||
|
window_count,
|
||||||
|
visible,
|
||||||
|
focused,
|
||||||
} => {
|
} => {
|
||||||
let output = join_id(output_hi, output_lo);
|
let output = join_id(output_hi, output_lo);
|
||||||
|
|
||||||
@@ -257,7 +271,10 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
id: join_id(workspace_hi, workspace_lo),
|
id: join_id(workspace_hi, workspace_lo),
|
||||||
name,
|
name,
|
||||||
output: (output != 0).then_some(output),
|
output: (output != 0).then_some(output),
|
||||||
active: active != 0,
|
index,
|
||||||
|
window_count,
|
||||||
|
visible: visible != 0,
|
||||||
|
focused: focused != 0,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::Window {
|
ferese_shell_v1::Event::Window {
|
||||||
@@ -286,9 +303,6 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::SnapshotEnd { serial } if state.serial.take() == Some(serial) => {
|
ferese_shell_v1::Event::SnapshotEnd { serial } if state.serial.take() == Some(serial) => {
|
||||||
state.pending.workspaces.sort_unstable_by_key(|workspace| {
|
|
||||||
ferese_core::workspace_order_key(&workspace.name, ferese_core::WorkspaceId(workspace.id))
|
|
||||||
});
|
|
||||||
let _ = state.sender.send(ControlUpdate::Snapshot(state.pending.clone()));
|
let _ = state.sender.send(ControlUpdate::Snapshot(state.pending.clone()));
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
||||||
@@ -348,3 +362,41 @@ mod tests {
|
|||||||
assert!(config.is_none());
|
assert!(config.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod workspace_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_bar_uses_its_own_order_and_authoritative_occupancy() {
|
||||||
|
let workspace = |id, output, index, window_count| WorkspaceSnapshot {
|
||||||
|
id,
|
||||||
|
output: Some(output),
|
||||||
|
index,
|
||||||
|
name: index.to_string(),
|
||||||
|
window_count,
|
||||||
|
visible: index == 1,
|
||||||
|
focused: output == 1 && index == 1,
|
||||||
|
};
|
||||||
|
let snapshot = ShellSnapshot {
|
||||||
|
workspaces: vec![workspace(7, 1, 1, 2), workspace(9, 1, 2, 0), workspace(3, 2, 1, 0)],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let left = snapshot.workspaces_for_output(Some(1)).collect::<Vec<_>>();
|
||||||
|
assert_eq!(
|
||||||
|
left.iter().map(|workspace| workspace.id).collect::<Vec<_>>(),
|
||||||
|
vec![7, 9]
|
||||||
|
);
|
||||||
|
assert_eq!(left[0].window_count, 2);
|
||||||
|
assert_eq!(
|
||||||
|
snapshot
|
||||||
|
.workspaces_for_output(Some(2))
|
||||||
|
.map(|workspace| workspace.id)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![3]
|
||||||
|
);
|
||||||
|
assert_eq!(snapshot.workspaces_for_output(None).count(), 0);
|
||||||
|
assert_eq!(snapshot.workspaces_for_output(Some(99)).count(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -361,12 +361,13 @@ impl FereseShell {
|
|||||||
&& editor.id == id
|
&& editor.id == id
|
||||||
{
|
{
|
||||||
let id = id.to_owned();
|
let id = id.to_owned();
|
||||||
|
let editor_radius = ferese_theme::inner_radius(self.config.theme.material_radius, note.padding);
|
||||||
body = body.push(
|
body = body.push(
|
||||||
cosmic::widget::TextEditor::new(&editor.content)
|
cosmic::widget::TextEditor::new(&editor.content)
|
||||||
.style(|theme, status| {
|
.style(move |theme, status| {
|
||||||
use cosmic::iced::widget::text_editor::Catalog;
|
use cosmic::iced::widget::text_editor::Catalog;
|
||||||
let mut style = theme.style(&<cosmic::Theme as Catalog>::default(), status);
|
let mut style = theme.style(&<cosmic::Theme as Catalog>::default(), status);
|
||||||
style.border.radius = motion::radius(f32::MAX).into();
|
style.border.radius = editor_radius.into();
|
||||||
style
|
style
|
||||||
})
|
})
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use cosmic::iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget, la
|
|||||||
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
|
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
|
||||||
use cosmic::{Element, Theme};
|
use cosmic::{Element, Theme};
|
||||||
|
|
||||||
pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[i32; 5]>>>;
|
pub type Regions = std::sync::Arc<std::sync::Mutex<Vec<[f32; 5]>>>;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -463,18 +463,18 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
|||||||
struct CollectRegions(Vec<Rectangle>);
|
struct CollectRegions(Vec<Rectangle>);
|
||||||
|
|
||||||
impl CollectRegions {
|
impl CollectRegions {
|
||||||
fn into_regions(self, translation: Vector, radius: f32) -> Vec<[i32; 5]> {
|
fn into_regions(self, translation: Vector, radius: f32) -> Vec<[f32; 5]> {
|
||||||
self.0
|
self.0
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.take(32)
|
.take(32)
|
||||||
.filter(|r| r.width > 0.0 && r.height > 0.0)
|
.filter(|r| r.width > 0.0 && r.height > 0.0)
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
[
|
[
|
||||||
(r.x + translation.x).round() as i32,
|
r.x + translation.x,
|
||||||
(r.y + translation.y).round() as i32,
|
r.y + translation.y,
|
||||||
r.width.round() as i32,
|
r.width,
|
||||||
r.height.round() as i32,
|
r.height,
|
||||||
radius.round().max(0.0) as i32,
|
radius.max(0.0),
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -508,6 +508,15 @@ impl widget::Operation for CollectRegions {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn material_regions_preserve_fractional_layout_and_translation() {
|
||||||
|
let collector = CollectRegions(vec![Rectangle::new((0.25, 2.5).into(), (100.25, 40.5).into())]);
|
||||||
|
assert_eq!(
|
||||||
|
collector.into_regions(Vector::new(0.125, -0.25), 13.5),
|
||||||
|
vec![[0.375, 2.25, 100.25, 40.5, 13.5]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn centered_modal_material_tracks_surface_position_and_animation() {
|
fn centered_modal_material_tracks_surface_position_and_animation() {
|
||||||
let mut collector = CollectRegions(Vec::new());
|
let mut collector = CollectRegions(Vec::new());
|
||||||
@@ -515,7 +524,7 @@ mod tests {
|
|||||||
widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card);
|
widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
collector.into_regions(Vector::new(0.0, -2.0), 14.0),
|
collector.into_regions(Vector::new(0.0, -2.0), 14.0),
|
||||||
vec![[380, 418, 440, 220, 14]]
|
vec![[380., 418., 440., 220., 14.]]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -467,6 +467,7 @@ impl FereseShell {
|
|||||||
width: if history { 0.0 } else { 1.0 },
|
width: if history { 0.0 } else { 1.0 },
|
||||||
radius: palette.material_radius.into(),
|
radius: palette.material_radius.into(),
|
||||||
},
|
},
|
||||||
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
let card: Element<'_, cosmic::Action<Message>> = card.into();
|
let card: Element<'_, cosmic::Action<Message>> = card.into();
|
||||||
@@ -738,6 +739,7 @@ impl FereseShell {
|
|||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: palette.material_radius.into(),
|
radius: palette.material_radius.into(),
|
||||||
},
|
},
|
||||||
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
.into();
|
.into();
|
||||||
|
|||||||
@@ -222,6 +222,7 @@ impl FereseShell {
|
|||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: theme.material_radius.into(),
|
radius: theme.material_radius.into(),
|
||||||
},
|
},
|
||||||
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -433,12 +433,19 @@ pub(super) fn wallpaper_fallback_style(_theme: &cosmic::Theme) -> container::Sty
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn encode_regions(regions: &[[f32; 5]]) -> Vec<u8> {
|
||||||
|
regions
|
||||||
|
.iter()
|
||||||
|
.flat_map(|region| region.iter().flat_map(|value| value.to_ne_bytes()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct EffectsBinding {
|
pub(super) struct EffectsBinding {
|
||||||
connection: Connection,
|
connection: Connection,
|
||||||
_manager: FereseEffectsManagerV1,
|
_manager: FereseEffectsManagerV1,
|
||||||
pub(super) surface: FereseSurfaceEffectsV1,
|
pub(super) surface: FereseSurfaceEffectsV1,
|
||||||
_queue: EventQueue<EffectsState>,
|
_queue: EventQueue<EffectsState>,
|
||||||
regions: std::cell::RefCell<Option<Vec<[i32; 5]>>>,
|
regions: std::cell::RefCell<Option<Vec<[f32; 5]>>>,
|
||||||
opacity: std::cell::Cell<Option<u32>>,
|
opacity: std::cell::Cell<Option<u32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,13 +470,11 @@ impl EffectsBinding {
|
|||||||
let connection = Connection::from_backend(backend);
|
let connection = Connection::from_backend(backend);
|
||||||
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
|
let (globals, queue) = registry_queue_init::<EffectsState>(&connection)?;
|
||||||
let qh = queue.handle();
|
let qh = queue.handle();
|
||||||
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 1..=3, ())?;
|
let manager = globals.bind::<FereseEffectsManagerV1, _, _>(&qh, 3..=3, ())?;
|
||||||
let effects = manager.get_surface_effects(surface, &qh, ());
|
let effects = manager.get_surface_effects(surface, &qh, ());
|
||||||
|
|
||||||
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||||
if effects.version() >= 3 {
|
|
||||||
effects.set_opacity(opacity);
|
effects.set_opacity(opacity);
|
||||||
}
|
|
||||||
if let Some(role) = role {
|
if let Some(role) = role {
|
||||||
effects.set_role(role);
|
effects.set_role(role);
|
||||||
} else {
|
} else {
|
||||||
@@ -487,24 +492,19 @@ impl EffectsBinding {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_regions(&self, regions: &[[i32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
|
pub(super) fn set_regions(&self, regions: &[[f32; 5]]) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
|
self.set_material_regions(regions, ferese_surface_effects_v1::Role::Popover)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn set_material_regions(
|
pub(super) fn set_material_regions(
|
||||||
&self,
|
&self,
|
||||||
regions: &[[i32; 5]],
|
regions: &[[f32; 5]],
|
||||||
role: ferese_surface_effects_v1::Role,
|
role: ferese_surface_effects_v1::Role,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
if self.surface.version() < 2 || self.regions.borrow().as_deref() == Some(regions) {
|
if self.regions.borrow().as_deref() == Some(regions) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.surface.set_regions(
|
self.surface.set_regions(encode_regions(regions));
|
||||||
regions
|
|
||||||
.iter()
|
|
||||||
.flat_map(|r| r.iter().flat_map(|v| v.to_ne_bytes()))
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
if regions.is_empty() {
|
if regions.is_empty() {
|
||||||
self.surface.clear_role();
|
self.surface.clear_role();
|
||||||
} else {
|
} else {
|
||||||
@@ -529,7 +529,7 @@ impl EffectsBinding {
|
|||||||
|
|
||||||
pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
|
pub(super) fn set_opacity(&self, opacity: f32) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
let opacity = (opacity.clamp(0.0, 1.0) * 1000.0).round() as u32;
|
||||||
if self.surface.version() >= 3 && self.opacity.get() != Some(opacity) {
|
if self.opacity.get() != Some(opacity) {
|
||||||
self.surface.set_opacity(opacity);
|
self.surface.set_opacity(opacity);
|
||||||
self.connection.flush()?;
|
self.connection.flush()?;
|
||||||
self.opacity.set(Some(opacity));
|
self.opacity.set(Some(opacity));
|
||||||
@@ -554,3 +554,19 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for EffectsState {
|
|||||||
|
|
||||||
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
delegate_noop!(EffectsState: ignore FereseEffectsManagerV1);
|
||||||
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
|
delegate_noop!(EffectsState: ignore FereseSurfaceEffectsV1);
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod region_encoding_tests {
|
||||||
|
#[test]
|
||||||
|
fn fractional_region_encoding_preserves_values() {
|
||||||
|
let regions = [[0.25, -0.75, 100.5, 40.25, 14.0]];
|
||||||
|
let bytes = super::encode_regions(®ions);
|
||||||
|
let decoded: Vec<_> = bytes
|
||||||
|
.as_chunks::<4>()
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|&v| f32::from_ne_bytes(v))
|
||||||
|
.collect();
|
||||||
|
assert_eq!(decoded, regions[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -541,6 +541,7 @@ impl FereseShell {
|
|||||||
width: 1.0,
|
width: 1.0,
|
||||||
radius: theme.material_radius.into(),
|
radius: theme.material_radius.into(),
|
||||||
},
|
},
|
||||||
|
snap: true,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
1. [Install Ferese](installation.md), or try a [nested preview](installation.md#preview-and-logs).
|
1. [Install Ferese](installation.md), or try a [nested preview](installation.md#preview-and-logs).
|
||||||
2. Choose Light, Dark, or Auto in **Control Center**. Open **Settings** for [theme files and presets](theme-engine.md).
|
2. Choose Light, Dark, or Auto in **Control Center**. Open **Settings** for theme files and presets.
|
||||||
3. Use the [configuration reference](configuration.md) for shortcuts, gestures, window rules, and displays.
|
3. Use the [configuration reference](configuration.md) for shortcuts, gestures, window rules, and displays.
|
||||||
|
|
||||||
## Default shortcuts
|
## Default shortcuts
|
||||||
@@ -40,8 +40,6 @@ group windows, resize them and move between workspaces.
|
|||||||
|
|
||||||
## Desktop guides
|
## Desktop guides
|
||||||
|
|
||||||
- [Desktop widgets](desktop-widgets.md): clocks and sticky notes.
|
|
||||||
- [Notifications](notifications.md): history and Do Not Disturb.
|
|
||||||
- [Lock screen](locking.md): appearance, automatic locking, and display sleep.
|
- [Lock screen](locking.md): appearance, automatic locking, and display sleep.
|
||||||
- [Screen sharing and recording](screen-sharing.md): share an app or display, or save a video.
|
- [Screen sharing and recording](screen-sharing.md): share an app or display, or save a video.
|
||||||
- [Desktop portals](portals.md): integration with applications.
|
- [Desktop portals](portals.md): integration with applications.
|
||||||
|
|||||||
Reference in new issue
Block a user