Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
+113
-281
@@ -1,57 +1,41 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
io,
|
||||
path::Path,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::{
|
||||
Fourcc,
|
||||
gbm::{GbmAllocator, GbmBufferFlags, GbmDevice},
|
||||
},
|
||||
drm::{
|
||||
DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType,
|
||||
},
|
||||
egl::{EGLContext, EGLDisplay},
|
||||
input::InputEvent,
|
||||
libinput::{LibinputInputBackend, LibinputSessionInterface},
|
||||
renderer::{
|
||||
Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker,
|
||||
gles::GlesRenderer,
|
||||
},
|
||||
session::{Event as SessionEvent, Session, libseat::LibSeatSession},
|
||||
udev::{UdevBackend, UdevEvent, primary_gpu},
|
||||
},
|
||||
desktop::{layer_map_for_output, utils::OutputPresentationFeedback},
|
||||
output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel},
|
||||
reexports::{
|
||||
calloop::{
|
||||
EventLoop, RegistrationToken,
|
||||
timer::{TimeoutAction, Timer},
|
||||
},
|
||||
drm::control::{
|
||||
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
|
||||
},
|
||||
input::{Device as LibinputDevice, Libinput},
|
||||
rustix::fs::OFlags,
|
||||
wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind,
|
||||
wayland_server::backend::GlobalId,
|
||||
},
|
||||
utils::{DeviceFd, Monotonic, Time, Transform},
|
||||
wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh},
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType};
|
||||
use smithay::backend::egl::{EGLContext, EGLDisplay};
|
||||
use smithay::backend::input::InputEvent;
|
||||
use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer};
|
||||
use smithay::backend::session::libseat::LibSeatSession;
|
||||
use smithay::backend::session::{Event as SessionEvent, Session};
|
||||
use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu};
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::calloop::{EventLoop, RegistrationToken};
|
||||
use smithay::reexports::drm::control::{
|
||||
Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property,
|
||||
};
|
||||
use smithay::reexports::input::{Device as LibinputDevice, Libinput};
|
||||
use smithay::reexports::rustix::fs::OFlags;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind;
|
||||
use smithay::reexports::wayland_server::backend::GlobalId;
|
||||
use smithay::utils::{DeviceFd, Monotonic, Time, Transform};
|
||||
use smithay::wayland::dmabuf::DmabufFeedbackBuilder;
|
||||
use smithay::wayland::presentation::Refresh;
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform},
|
||||
metrics::RenderMetrics,
|
||||
winit::{
|
||||
animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output,
|
||||
},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform};
|
||||
use crate::metrics::RenderMetrics;
|
||||
use crate::winit::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output};
|
||||
|
||||
pub struct DirectBackendState {
|
||||
pub session: LibSeatSession,
|
||||
@@ -72,14 +56,12 @@ impl DirectBackendState {
|
||||
geometry: smithay::utils::Rectangle<i32, smithay::utils::Logical>,
|
||||
output: &Output,
|
||||
remaining: usize,
|
||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError>
|
||||
{
|
||||
let Some(device) = self.devices.values_mut().find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
}) else {
|
||||
) -> Result<Option<crate::winit::ResizeSnapshot>, smithay::backend::renderer::gles::GlesError> {
|
||||
let Some(device) = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
crate::winit::capture_resize_snapshot(
|
||||
@@ -100,12 +82,7 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_buffer(
|
||||
&mut device.renderer,
|
||||
@@ -124,12 +101,7 @@ impl DirectBackendState {
|
||||
let device = self
|
||||
.devices
|
||||
.values_mut()
|
||||
.find(|device| {
|
||||
device
|
||||
.outputs
|
||||
.values()
|
||||
.any(|candidate| &candidate.output == output)
|
||||
})
|
||||
.find(|device| device.outputs.values().any(|candidate| &candidate.output == output))
|
||||
.ok_or("Window output is unavailable")?;
|
||||
crate::winit::capture_window_frame(
|
||||
&mut device.renderer,
|
||||
@@ -212,12 +184,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
let seat_name = session.seat();
|
||||
let udev_backend = UdevBackend::new(&seat_name)?;
|
||||
let primary_path = primary_gpu(&seat_name)?
|
||||
.or_else(|| {
|
||||
udev_backend
|
||||
.device_list()
|
||||
.next()
|
||||
.map(|(_, path)| path.to_owned())
|
||||
})
|
||||
.or_else(|| udev_backend.device_list().next().map(|(_, path)| path.to_owned()))
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?;
|
||||
let mut libinput_context =
|
||||
Libinput::new_with_udev::<LibinputSessionInterface<LibSeatSession>>(session.clone().into());
|
||||
@@ -254,9 +221,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
}
|
||||
InputEvent::DeviceRemoved { device } => {
|
||||
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),
|
||||
@@ -275,8 +240,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
|
||||
device.drm.pause();
|
||||
device.outputs.values_mut().for_each(|output| {
|
||||
output.surface.reset_buffers();
|
||||
output.damage_tracker =
|
||||
OutputDamageTracker::from_output(&output.output);
|
||||
output.damage_tracker = OutputDamageTracker::from_output(&output.output);
|
||||
output.frame_pending = false;
|
||||
output.power_off = false;
|
||||
});
|
||||
@@ -335,20 +299,12 @@ fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) {
|
||||
if device.config_tap_finger_count() > 0
|
||||
&& let Err(error) = device.config_tap_set_enabled(settings.tap)
|
||||
{
|
||||
tracing::warn!(
|
||||
device = device.name(),
|
||||
?error,
|
||||
"failed to configure tap-to-click"
|
||||
);
|
||||
tracing::warn!(device = device.name(), ?error, "failed to configure tap-to-click");
|
||||
}
|
||||
if device.config_scroll_has_natural_scroll()
|
||||
&& let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll)
|
||||
{
|
||||
tracing::warn!(
|
||||
device = device.name(),
|
||||
?error,
|
||||
"failed to configure natural scrolling"
|
||||
);
|
||||
tracing::warn!(device = device.name(), ?error, "failed to configure natural scrolling");
|
||||
}
|
||||
if device.config_dwt_is_available()
|
||||
&& let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing)
|
||||
@@ -383,10 +339,7 @@ pub(crate) fn reload_outputs(state: &mut Ferese) {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_live_outputs(
|
||||
state: &Ferese,
|
||||
profiles: &[OutputProfile],
|
||||
) -> Result<(), String> {
|
||||
pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> {
|
||||
let Some(backend) = state.direct_backend.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -414,10 +367,7 @@ fn open_primary_device(
|
||||
.session
|
||||
.clone();
|
||||
let node = DrmNode::from_path(path)?;
|
||||
let fd = session.open(
|
||||
path,
|
||||
OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK,
|
||||
)?;
|
||||
let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?;
|
||||
let fd = DrmDeviceFd::new(DeviceFd::from(fd));
|
||||
let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?;
|
||||
let gbm = GbmDevice::new(fd)?;
|
||||
@@ -443,10 +393,7 @@ fn open_primary_device(
|
||||
let mut scan = select_outputs(&drm, &state.output_profiles)?;
|
||||
apply_lid_policy(
|
||||
&mut scan,
|
||||
state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.lid_closed),
|
||||
state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed),
|
||||
);
|
||||
if scan.selections.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
@@ -468,9 +415,7 @@ fn open_primary_device(
|
||||
mode,
|
||||
settings,
|
||||
} = selection;
|
||||
let output = create_direct_output(
|
||||
state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings,
|
||||
)?;
|
||||
let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings)?;
|
||||
state
|
||||
.direct_backend
|
||||
.as_mut()
|
||||
@@ -503,9 +448,7 @@ fn open_primary_device(
|
||||
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(
|
||||
Time::<Monotonic>::from(time),
|
||||
Refresh::fixed(refresh_duration(state, node, crtc)),
|
||||
@@ -538,10 +481,7 @@ fn open_primary_device(
|
||||
}
|
||||
state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence);
|
||||
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 {
|
||||
let was_animating = state
|
||||
.direct_backend
|
||||
@@ -588,11 +528,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
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() {
|
||||
if output.power_off || output.frame_pending {
|
||||
continue;
|
||||
@@ -621,11 +557,7 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) {
|
||||
}
|
||||
|
||||
pub fn render_all(state: &mut Ferese) {
|
||||
if state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_none_or(|backend| !backend.active)
|
||||
{
|
||||
if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) {
|
||||
return;
|
||||
}
|
||||
let animating = state.advance_animations(Instant::now());
|
||||
@@ -692,9 +624,7 @@ fn initial_lid_closed() -> bool {
|
||||
fn internal_connector(interface: connector::Interface) -> bool {
|
||||
matches!(
|
||||
interface,
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
| connector::Interface::LVDS
|
||||
| connector::Interface::DSI
|
||||
connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI
|
||||
)
|
||||
}
|
||||
|
||||
@@ -767,8 +697,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
state.process_dmabuf_imports(&mut device.renderer);
|
||||
let (mut buffer, age) = output.surface.next_buffer()?;
|
||||
let elements = animated_window_elements(state, &mut device.renderer, &output.output);
|
||||
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 result = output.damage_tracker.render_output(
|
||||
&mut device.renderer,
|
||||
@@ -778,12 +707,10 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
[0.035, 0.04, 0.055, 1.0],
|
||||
)?;
|
||||
let cursorless_capture = state.has_pending_screencopy(&output.output, false);
|
||||
let captured_with_cursor =
|
||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
||||
let captured_with_cursor = state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true);
|
||||
|
||||
if cursorless_capture {
|
||||
let cursorless_elements =
|
||||
cursorless_window_elements(state, &mut device.renderer, &output.output);
|
||||
let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output);
|
||||
redraw_output(
|
||||
&mut device.renderer,
|
||||
&mut framebuffer,
|
||||
@@ -791,22 +718,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
&cursorless_elements,
|
||||
)?;
|
||||
state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false);
|
||||
redraw_output(
|
||||
&mut device.renderer,
|
||||
&mut framebuffer,
|
||||
&output.output,
|
||||
&elements,
|
||||
)?;
|
||||
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_mode().expect("output has a mode").size,
|
||||
output.output.current_transform(),
|
||||
)?
|
||||
.finish()?;
|
||||
@@ -818,34 +736,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) {
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
let mut presentation = OutputPresentationFeedback::new(&output.output);
|
||||
state.space.elements().for_each(|window| {
|
||||
window.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.output.clone()),
|
||||
|_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
});
|
||||
let layers = layer_map_for_output(&output.output)
|
||||
.layers()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
layers.iter().for_each(|layer| {
|
||||
layer.take_presentation_feedback(
|
||||
&mut presentation,
|
||||
|_, _| Some(output.output.clone()),
|
||||
|_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
});
|
||||
let presentation = crate::presentation::take_output_feedback(
|
||||
state,
|
||||
&output.output,
|
||||
&result.states,
|
||||
Kind::Vsync | Kind::HwClock | Kind::HwCompletion,
|
||||
);
|
||||
output
|
||||
.surface
|
||||
.queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?;
|
||||
output.render_metrics.record_frame(
|
||||
render_started.elapsed(),
|
||||
&damage,
|
||||
missed_deadlines,
|
||||
effects,
|
||||
);
|
||||
output
|
||||
.render_metrics
|
||||
.record_frame(render_started.elapsed(), &damage, missed_deadlines, effects);
|
||||
output.frame_pending = true;
|
||||
output.lock_frame_pending = state.session_lock.active;
|
||||
Ok(true)
|
||||
@@ -893,10 +795,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
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);
|
||||
if let Some(backend) = state.direct_backend.as_mut() {
|
||||
backend.connected_outputs = scan.connected_outputs;
|
||||
@@ -940,9 +839,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
Some(Scale::Fractional(output.settings.scale)),
|
||||
Some((position[0], position[1]).into()),
|
||||
);
|
||||
state
|
||||
.space
|
||||
.map_output(&output.output, (position[0], position[1]));
|
||||
state.space.map_output(&output.output, (position[0], position[1]));
|
||||
if let Some(id) = state.output_id(&output.output)
|
||||
&& let Some(g) = state.space.output_geometry(&output.output)
|
||||
{
|
||||
@@ -1045,10 +942,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
}
|
||||
|
||||
for crtc in deferred_removals {
|
||||
let keep_internal = device
|
||||
.outputs
|
||||
.get(&crtc)
|
||||
.is_some_and(|output| output.internal)
|
||||
let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal)
|
||||
&& lid_closed
|
||||
&& state.direct_backend.as_ref().is_some_and(|backend| {
|
||||
device.outputs.get(&crtc).is_some_and(|output| {
|
||||
@@ -1060,10 +954,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
||||
})
|
||||
&& !device.outputs.values().any(|output| !output.internal);
|
||||
if keep_internal {
|
||||
tracing::warn!(
|
||||
?crtc,
|
||||
"keeping laptop panel enabled: external output activation failed"
|
||||
);
|
||||
tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed");
|
||||
if let Some(output) = device.outputs.get(&crtc)
|
||||
&& let Some(backend) = state.direct_backend.as_mut()
|
||||
&& let Some(info) = backend
|
||||
@@ -1116,9 +1007,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
|
||||
|
||||
let context = device.renderer.context_id().erased();
|
||||
state.wallpaper.forget_context(&context);
|
||||
state
|
||||
.resize_snapshots
|
||||
.retain(|_, snapshot| snapshot.context != context);
|
||||
state.resize_snapshots.retain(|_, snapshot| snapshot.context != context);
|
||||
for (crtc, output) in device.outputs {
|
||||
state.display_handle.disable_global::<Ferese>(output.global);
|
||||
state.unregister_output(&output.output);
|
||||
@@ -1170,9 +1059,10 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
match state
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
|
||||
let active = state.direct_backend.as_ref().is_some_and(|backend| {
|
||||
backend.active && backend.animation_active && !state.session_lock.sleeping
|
||||
});
|
||||
let active = state
|
||||
.direct_backend
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping);
|
||||
if active {
|
||||
let deadline = state.animation_fallback_deadline(animation_interval(state));
|
||||
if Instant::now() < deadline {
|
||||
@@ -1184,9 +1074,7 @@ fn arm_animation_timer(state: &mut Ferese) {
|
||||
.as_ref()
|
||||
.is_some_and(|backend| backend.animation_active)
|
||||
{
|
||||
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() {
|
||||
@@ -1244,12 +1132,7 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl
|
||||
}
|
||||
}
|
||||
|
||||
fn deliver_frame_callbacks(
|
||||
state: &mut Ferese,
|
||||
node: DrmNode,
|
||||
crtc: crtc::Handle,
|
||||
identity: &Output,
|
||||
) {
|
||||
fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) {
|
||||
let Some(backend) = state.direct_backend.as_mut() else {
|
||||
return;
|
||||
};
|
||||
@@ -1286,24 +1169,15 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
|
||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||
})
|
||||
.for_each(|window| {
|
||||
window.send_frame(
|
||||
output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
window.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
||||
Some(output.clone())
|
||||
});
|
||||
});
|
||||
let layers = layer_map_for_output(output)
|
||||
.layers()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let layers = layer_map_for_output(output).layers().cloned().collect::<Vec<_>>();
|
||||
layers.iter().for_each(|layer| {
|
||||
layer.send_frame(
|
||||
output,
|
||||
state.start_time.elapsed(),
|
||||
Some(Duration::ZERO),
|
||||
|_, _| Some(output.clone()),
|
||||
);
|
||||
layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| {
|
||||
Some(output.clone())
|
||||
});
|
||||
});
|
||||
state.send_cursor_frame(output);
|
||||
state.space.refresh();
|
||||
@@ -1365,12 +1239,7 @@ fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result<Out
|
||||
profile: active_profile.map(|profile| profile.name.clone()),
|
||||
physical_size: connector.size(),
|
||||
current_mode: selected_mode.map(connected_mode_info),
|
||||
available_modes: connector
|
||||
.modes()
|
||||
.iter()
|
||||
.copied()
|
||||
.map(connected_mode_info)
|
||||
.collect(),
|
||||
available_modes: connector.modes().iter().copied().map(connected_mode_info).collect(),
|
||||
scale: settings.scale,
|
||||
transform: settings.transform,
|
||||
configured_position: settings.position,
|
||||
@@ -1438,10 +1307,7 @@ fn default_output_settings(matcher: String) -> OutputSettings {
|
||||
}
|
||||
}
|
||||
|
||||
fn select_mode(
|
||||
connector: &connector::Info,
|
||||
requested: Option<OutputModeRequest>,
|
||||
) -> Option<DrmMode> {
|
||||
fn select_mode(connector: &connector::Info, requested: Option<OutputModeRequest>) -> Option<DrmMode> {
|
||||
let fallback = || {
|
||||
connector
|
||||
.modes()
|
||||
@@ -1460,24 +1326,11 @@ fn select_mode(
|
||||
.filter(|mode| mode.size() == (requested.width, requested.height));
|
||||
let selected = if let Some(refresh) = requested.refresh_millihertz {
|
||||
matching
|
||||
.min_by_key(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
})
|
||||
.filter(|mode| {
|
||||
OutputMode::from(**mode)
|
||||
.refresh
|
||||
.unsigned_abs()
|
||||
.abs_diff(refresh)
|
||||
<= 1_000
|
||||
})
|
||||
.min_by_key(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh))
|
||||
.filter(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh) <= 1_000)
|
||||
.copied()
|
||||
} else {
|
||||
matching
|
||||
.max_by_key(|mode| OutputMode::from(**mode).refresh)
|
||||
.copied()
|
||||
matching.max_by_key(|mode| OutputMode::from(**mode).refresh).copied()
|
||||
};
|
||||
if selected.is_none() {
|
||||
tracing::warn!(
|
||||
@@ -1504,10 +1357,7 @@ fn create_direct_output(
|
||||
) -> Result<DirectOutput, Box<dyn Error>> {
|
||||
let identity = connector_identity(drm, &connector);
|
||||
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(
|
||||
drm_surface,
|
||||
allocator,
|
||||
@@ -1602,21 +1452,18 @@ fn output_transform(transform: OutputTransform) -> Transform {
|
||||
}
|
||||
|
||||
fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String {
|
||||
let edid = drm
|
||||
.get_properties(connector.handle())
|
||||
.ok()
|
||||
.and_then(|properties| {
|
||||
properties.iter().find_map(|(handle, raw)| {
|
||||
let info = drm.get_property(*handle).ok()?;
|
||||
if info.name().to_bytes() != b"EDID" {
|
||||
return None;
|
||||
}
|
||||
match info.value_type().convert_value(*raw) {
|
||||
property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
let edid = drm.get_properties(connector.handle()).ok().and_then(|properties| {
|
||||
properties.iter().find_map(|(handle, raw)| {
|
||||
let info = drm.get_property(*handle).ok()?;
|
||||
if info.name().to_bytes() != b"EDID" {
|
||||
return None;
|
||||
}
|
||||
match info.value_type().convert_value(*raw) {
|
||||
property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
match edid {
|
||||
Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)),
|
||||
@@ -1654,23 +1501,16 @@ impl PresentationClock {
|
||||
|
||||
fn set_refresh(&mut self, refresh_millihertz: i32) {
|
||||
if refresh_millihertz > 0 {
|
||||
self.refresh_interval = Some(Duration::from_nanos(
|
||||
1_000_000_000_000_u64 / refresh_millihertz as u64,
|
||||
));
|
||||
self.refresh_interval = Some(Duration::from_nanos(1_000_000_000_000_u64 / refresh_millihertz as u64));
|
||||
}
|
||||
}
|
||||
|
||||
fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option<Duration> {
|
||||
let DrmEventTime::Monotonic(time) = time else {
|
||||
tracing::warn!(
|
||||
sequence,
|
||||
"DRM driver reported a realtime page-flip timestamp"
|
||||
);
|
||||
tracing::warn!(sequence, "DRM driver reported a realtime page-flip timestamp");
|
||||
return None;
|
||||
};
|
||||
let delta = self
|
||||
.last_presentation
|
||||
.map(|previous| time.saturating_sub(previous));
|
||||
let delta = self.last_presentation.map(|previous| time.saturating_sub(previous));
|
||||
|
||||
self.last_presentation = Some(time);
|
||||
self.presented_frames = self.presented_frames.saturating_add(1);
|
||||
@@ -1709,9 +1549,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn panel_detection_uses_connector_type_not_monitor_name() {
|
||||
assert!(internal_connector(
|
||||
connector::Interface::EmbeddedDisplayPort
|
||||
));
|
||||
assert!(internal_connector(connector::Interface::EmbeddedDisplayPort));
|
||||
assert!(internal_connector(connector::Interface::LVDS));
|
||||
assert!(internal_connector(connector::Interface::DSI));
|
||||
assert!(!internal_connector(connector::Interface::HDMIA));
|
||||
@@ -1744,10 +1582,7 @@ mod tests {
|
||||
fn presentation_clock_rejects_realtime_timestamps() {
|
||||
let mut clock = PresentationClock::default();
|
||||
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Realtime(SystemTime::now()), 1),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None);
|
||||
assert_eq!(clock.presented_frames, 0);
|
||||
}
|
||||
|
||||
@@ -1770,10 +1605,7 @@ mod tests {
|
||||
|
||||
clock.reset_timing();
|
||||
|
||||
assert_eq!(
|
||||
clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2),
|
||||
None
|
||||
);
|
||||
assert_eq!(clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None);
|
||||
assert_eq!(clock.presented_frames, 2);
|
||||
assert_eq!(clock.missed_deadlines, 0);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use std::ffi::OsString;
|
||||
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities};
|
||||
use crate::Ferese;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum BackendKind {
|
||||
@@ -21,16 +22,12 @@ pub struct LaunchConfig {
|
||||
|
||||
impl LaunchConfig {
|
||||
pub fn from_environment() -> Result<Self, String> {
|
||||
let graphical_session =
|
||||
std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
let graphical_session = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some();
|
||||
|
||||
Self::parse(std::env::args_os().skip(1), graphical_session)
|
||||
}
|
||||
|
||||
fn parse(
|
||||
args: impl IntoIterator<Item = OsString>,
|
||||
graphical_session: bool,
|
||||
) -> Result<Self, String> {
|
||||
fn parse(args: impl IntoIterator<Item = OsString>, graphical_session: bool) -> Result<Self, String> {
|
||||
let mut args = args.into_iter().peekable();
|
||||
let mut requested_backend = None;
|
||||
let mut client = Vec::new();
|
||||
@@ -99,10 +96,7 @@ fn parse_backend(value: &OsString) -> Result<Option<BackendKind>, String> {
|
||||
Some("auto") => Ok(None),
|
||||
Some("nested") => Ok(Some(BackendKind::Nested)),
|
||||
Some("drm") => Ok(Some(BackendKind::Drm)),
|
||||
_ => Err(format!(
|
||||
"unknown backend {:?}; expected auto, nested, or drm",
|
||||
value
|
||||
)),
|
||||
_ => Err(format!("unknown backend {:?}; expected auto, nested, or drm", value)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,42 +155,25 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-effects-probe")]);
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicitly_grants_shell_control_to_the_launched_private_client() {
|
||||
let config = LaunchConfig::parse(
|
||||
[
|
||||
OsString::from("--grant-shell-control"),
|
||||
OsString::from("ferese-shell"),
|
||||
],
|
||||
[OsString::from("--grant-shell-control"), OsString::from("ferese-shell")],
|
||||
true,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(config.client, [OsString::from("ferese-shell")]);
|
||||
assert!(
|
||||
config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||
);
|
||||
assert!(
|
||||
!config
|
||||
.client_capabilities
|
||||
.contains(ClientCapabilities::EFFECTS)
|
||||
);
|
||||
assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL));
|
||||
assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_an_unknown_backend() {
|
||||
let error =
|
||||
LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true)
|
||||
.unwrap_err();
|
||||
let error = LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true).unwrap_err();
|
||||
|
||||
assert!(error.contains("unknown backend"));
|
||||
}
|
||||
|
||||
+113
-312
@@ -1,9 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{fmt, fs, io};
|
||||
|
||||
use ferese_animation::SpringConfig;
|
||||
use ferese_core::LayoutMode;
|
||||
@@ -11,7 +9,8 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy};
|
||||
use serde::Deserialize;
|
||||
use smithay::input::keyboard::{Keycode, keysyms, xkb};
|
||||
|
||||
use crate::window_rules::{self, WindowRule, WindowRuleConfig};
|
||||
use crate::window_rules;
|
||||
use crate::window_rules::{WindowRule, WindowRuleConfig};
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
pub struct Config {
|
||||
@@ -163,11 +162,7 @@ impl BorderPaintConfig {
|
||||
"focus_ring.gradient.angle",
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"border.gradient.from",
|
||||
"border.gradient.to",
|
||||
"border.gradient.angle",
|
||||
)
|
||||
("border.gradient.from", "border.gradient.to", "border.gradient.angle")
|
||||
};
|
||||
self.gradient
|
||||
.as_ref()
|
||||
@@ -274,18 +269,10 @@ impl Config {
|
||||
}
|
||||
|
||||
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
|
||||
self.notifications
|
||||
.validate()
|
||||
.map_err(ConfigError::InvalidBinding)?;
|
||||
self.desktop_widgets
|
||||
.validate()
|
||||
.map_err(ConfigError::InvalidBinding)?;
|
||||
self.notifications.validate().map_err(ConfigError::InvalidBinding)?;
|
||||
self.desktop_widgets.validate().map_err(ConfigError::InvalidBinding)?;
|
||||
for daemon in &self.autostart {
|
||||
if daemon
|
||||
.command
|
||||
.first()
|
||||
.is_none_or(|program| program.trim().is_empty())
|
||||
{
|
||||
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"autostart command must contain a program".into(),
|
||||
));
|
||||
@@ -510,15 +497,10 @@ impl Binding {
|
||||
let symbol = parse_keysym(&key)?;
|
||||
if (modifiers.ctrl
|
||||
&& modifiers.alt
|
||||
&& matches!(
|
||||
symbol,
|
||||
keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12
|
||||
))
|
||||
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
||||
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||
{
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"Reserved system shortcut".into(),
|
||||
));
|
||||
return Err(ConfigError::InvalidBinding("Reserved system shortcut".into()));
|
||||
}
|
||||
Ok(Self {
|
||||
modifiers,
|
||||
@@ -534,34 +516,19 @@ impl Binding {
|
||||
match (&self.trigger, &other.trigger) {
|
||||
(BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right,
|
||||
(BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code))
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => {
|
||||
(0..map.num_layouts_for_key(*code)).any(|layout| {
|
||||
| (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code))
|
||||
.any(|layout| {
|
||||
map.key_get_syms_by_level(*code, layout, 0)
|
||||
.iter()
|
||||
.any(|key| key.raw() == *symbol)
|
||||
})
|
||||
}
|
||||
}),
|
||||
(BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn matches(
|
||||
&self,
|
||||
keycode: Keycode,
|
||||
keysyms: &[u32],
|
||||
logo: bool,
|
||||
ctrl: bool,
|
||||
alt: bool,
|
||||
shift: bool,
|
||||
) -> bool {
|
||||
self.modifiers
|
||||
== BindingModifiers {
|
||||
logo,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool {
|
||||
self.modifiers == BindingModifiers { logo, ctrl, alt, shift }
|
||||
&& match self.trigger {
|
||||
BindingTrigger::Keysym(expected) => keysyms.contains(&expected),
|
||||
BindingTrigger::Physical(expected) => expected == keycode,
|
||||
@@ -572,11 +539,7 @@ impl Binding {
|
||||
pub(crate) fn guide_entry(&self, keymap: Option<&xkb::Keymap>) -> Option<serde_json::Value> {
|
||||
let description = match &self.action {
|
||||
BindingAction::None => return None,
|
||||
BindingAction::Spawn(argv)
|
||||
if argv
|
||||
.first()
|
||||
.is_some_and(|program| program == "ferese-screenshot") =>
|
||||
{
|
||||
BindingAction::Spawn(argv) if argv.first().is_some_and(|program| program == "ferese-screenshot") => {
|
||||
if argv.iter().any(|arg| arg == "--full") {
|
||||
"Capture whole screen"
|
||||
} else {
|
||||
@@ -598,12 +561,9 @@ impl Binding {
|
||||
BindingAction::Resize(direction) => format!("Resize window {direction:?}"),
|
||||
BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"),
|
||||
BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"),
|
||||
BindingAction::SwitchRelativeWorkspace(next) => if *next {
|
||||
"Next workspace"
|
||||
} else {
|
||||
"Previous workspace"
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
if *next { "Next workspace" } else { "Previous workspace" }.into()
|
||||
}
|
||||
.into(),
|
||||
BindingAction::ToggleFullscreen => "Toggle fullscreen".into(),
|
||||
BindingAction::ToggleMaximized => "Toggle full-width window".into(),
|
||||
BindingAction::ToggleLayout => "Switch workspace layout".into(),
|
||||
@@ -651,11 +611,7 @@ impl Binding {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count)
|
||||
}
|
||||
|
||||
pub(crate) fn matches_swipe(
|
||||
&self,
|
||||
count: u32,
|
||||
target: crate::gestures::SwipeDirection,
|
||||
) -> bool {
|
||||
pub(crate) fn matches_swipe(&self, count: u32, target: crate::gestures::SwipeDirection) -> bool {
|
||||
matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target)
|
||||
}
|
||||
}
|
||||
@@ -1022,11 +978,7 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy {
|
||||
match self
|
||||
.scrolling
|
||||
.focus_strategy
|
||||
.unwrap_or(FocusStrategyValue::Minimal)
|
||||
{
|
||||
match self.scrolling.focus_strategy.unwrap_or(FocusStrategyValue::Minimal) {
|
||||
FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal,
|
||||
FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center,
|
||||
FocusStrategyValue::Paged => ViewportFocusStrategy::Paged,
|
||||
@@ -1078,10 +1030,7 @@ impl Config {
|
||||
pub fn bindings(&self, input: &InputSettings) -> Result<Vec<Binding>, ConfigError> {
|
||||
let mut commands = HashMap::from([
|
||||
("terminal".to_owned(), vec!["foot".to_owned()]),
|
||||
(
|
||||
"screenshot".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned()],
|
||||
),
|
||||
("screenshot".to_owned(), vec!["ferese-screenshot".to_owned()]),
|
||||
(
|
||||
"screenshot-full".to_owned(),
|
||||
vec!["ferese-screenshot".to_owned(), "--full".to_owned()],
|
||||
@@ -1197,19 +1146,13 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn theme_settings(&self) -> Result<ThemeSettings, ConfigError> {
|
||||
let border_width =
|
||||
nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width = nonnegative_theme_value(
|
||||
self.theme.geometry.focus_ring_width,
|
||||
"geometry.focus_ring_width",
|
||||
)?;
|
||||
let window_radius =
|
||||
nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y =
|
||||
finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?;
|
||||
let focus_ring_width =
|
||||
nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?;
|
||||
let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?;
|
||||
let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?;
|
||||
let shadow_blur = nonnegative_theme_value(self.theme.shadow.soft.blur, "shadow.soft.blur")?;
|
||||
let shadow_opacity =
|
||||
unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
let shadow_opacity = unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?;
|
||||
|
||||
let material_radius = nonnegative_theme_value(
|
||||
self.theme
|
||||
@@ -1224,25 +1167,16 @@ impl Config {
|
||||
border_width,
|
||||
focus_ring_width,
|
||||
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
|
||||
text_primary_color: parse_color(
|
||||
&self.theme.colors.text_primary,
|
||||
"colors.text_primary",
|
||||
)?,
|
||||
text_primary_color: parse_color(&self.theme.colors.text_primary, "colors.text_primary")?,
|
||||
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
|
||||
border_gradient: self.theme.border.settings("border")?,
|
||||
focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?,
|
||||
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
|
||||
surface_base_color: parse_color(
|
||||
&self.theme.colors.surface_base,
|
||||
"colors.surface_base",
|
||||
)?,
|
||||
surface_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?,
|
||||
shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?,
|
||||
inactive_dim: InactiveDimSettings {
|
||||
enabled: self.appearance.inactive_dim.enabled,
|
||||
amount: unit_theme_value(
|
||||
self.appearance.inactive_dim.amount,
|
||||
"appearance.inactive_dim.amount",
|
||||
)?,
|
||||
amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?,
|
||||
duration_ms: nonnegative_theme_value(
|
||||
self.appearance.inactive_dim.duration_ms,
|
||||
"appearance.inactive_dim.duration_ms",
|
||||
@@ -1253,11 +1187,7 @@ impl Config {
|
||||
shadow_blur,
|
||||
shadow_opacity,
|
||||
material_style: self.theme.material.style,
|
||||
backdrop_blur: nonnegative_theme_value(
|
||||
self.theme.material.blur_radius,
|
||||
"material.blur_radius",
|
||||
)?
|
||||
.min(32.0),
|
||||
backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0),
|
||||
material_radius,
|
||||
panel_radius: material_radius,
|
||||
})
|
||||
@@ -1273,8 +1203,7 @@ impl Config {
|
||||
|
||||
pub fn spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?;
|
||||
let stiffness =
|
||||
positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||
let damping = self.animations.spring.damping;
|
||||
if !damping.is_finite() || damping < 0.0 {
|
||||
return Err(ConfigError::InvalidAnimationValue {
|
||||
@@ -1292,12 +1221,9 @@ impl Config {
|
||||
}
|
||||
|
||||
pub fn viewport_spring_config(&self) -> Result<SpringConfig, ConfigError> {
|
||||
let mass =
|
||||
positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||
let stiffness = positive_animation_value(
|
||||
self.animations.viewport_spring.stiffness,
|
||||
"viewport_spring.stiffness",
|
||||
)?;
|
||||
let mass = positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?;
|
||||
let stiffness =
|
||||
positive_animation_value(self.animations.viewport_spring.stiffness, "viewport_spring.stiffness")?;
|
||||
let damping_ratio = positive_animation_value(
|
||||
self.animations.viewport_spring.damping_ratio,
|
||||
"viewport_spring.damping_ratio",
|
||||
@@ -1322,9 +1248,7 @@ struct BindingIdentity {
|
||||
fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> {
|
||||
for (name, argv) in commands {
|
||||
if name.trim().is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"command names cannot be empty".to_owned(),
|
||||
));
|
||||
return Err(ConfigError::InvalidBinding("command names cannot be empty".to_owned()));
|
||||
}
|
||||
if argv.is_empty() || argv[0].is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
@@ -1365,9 +1289,10 @@ fn parse_binding(
|
||||
}
|
||||
|
||||
let identity = binding_identity(configured, keymap)?;
|
||||
let action = configured.action.as_deref().ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys))
|
||||
})?;
|
||||
let action = configured
|
||||
.action
|
||||
.as_deref()
|
||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)))?;
|
||||
let action = parse_action(action, configured.argument.as_deref(), commands)?;
|
||||
let trigger = match identity.match_mode {
|
||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||
@@ -1390,15 +1315,10 @@ fn parse_binding(
|
||||
})
|
||||
}
|
||||
|
||||
fn binding_identity(
|
||||
configured: &BindingConfig,
|
||||
keymap: &xkb::Keymap,
|
||||
) -> Result<BindingIdentity, ConfigError> {
|
||||
fn binding_identity(configured: &BindingConfig, keymap: &xkb::Keymap) -> Result<BindingIdentity, ConfigError> {
|
||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||
if configured.match_mode == BindingMatch::Physical
|
||||
|| modifiers != BindingModifiers::default()
|
||||
{
|
||||
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
||||
return Err(ConfigError::InvalidBinding(
|
||||
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
||||
));
|
||||
@@ -1438,9 +1358,7 @@ fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity {
|
||||
let (match_mode, key) = match binding.trigger {
|
||||
BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key),
|
||||
BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()),
|
||||
BindingTrigger::Swipe { fingers, direction } => {
|
||||
(BindingMatch::Swipe, fingers * 4 + direction as u32)
|
||||
}
|
||||
BindingTrigger::Swipe { fingers, direction } => (BindingMatch::Swipe, fingers * 4 + direction as u32),
|
||||
};
|
||||
|
||||
BindingIdentity {
|
||||
@@ -1456,9 +1374,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
|
||||
for component in chord.split('+').map(str::trim) {
|
||||
if component.is_empty() {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid key chord {chord:?}"
|
||||
)));
|
||||
return Err(ConfigError::InvalidBinding(format!("invalid key chord {chord:?}")));
|
||||
}
|
||||
|
||||
let slot = match component.to_ascii_lowercase().as_str() {
|
||||
@@ -1470,9 +1386,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
};
|
||||
if let Some(slot) = slot {
|
||||
if *slot {
|
||||
return Err(ConfigError::InvalidBinding(format!(
|
||||
"duplicate modifier in {chord:?}"
|
||||
)));
|
||||
return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}")));
|
||||
}
|
||||
*slot = true;
|
||||
} else if key.replace(component.to_owned()).is_some() {
|
||||
@@ -1482,9 +1396,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> {
|
||||
}
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key"))
|
||||
})?;
|
||||
let key = key.ok_or_else(|| ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")))?;
|
||||
Ok((modifiers, key))
|
||||
}
|
||||
|
||||
@@ -1520,9 +1432,7 @@ fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
||||
symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE);
|
||||
}
|
||||
if symbol.raw() == keysyms::KEY_NoSymbol {
|
||||
Err(ConfigError::InvalidBinding(format!(
|
||||
"unknown keysym {name:?}"
|
||||
)))
|
||||
Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}")))
|
||||
} else {
|
||||
Ok(symbol.raw())
|
||||
}
|
||||
@@ -1542,11 +1452,8 @@ fn parse_action(
|
||||
Ok(())
|
||||
}
|
||||
};
|
||||
let required_argument = || {
|
||||
argument.ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("action {action:?} requires an argument"))
|
||||
})
|
||||
};
|
||||
let required_argument =
|
||||
|| argument.ok_or_else(|| ConfigError::InvalidBinding(format!("action {action:?} requires an argument")));
|
||||
|
||||
match action {
|
||||
"none" => {
|
||||
@@ -1555,15 +1462,13 @@ fn parse_action(
|
||||
}
|
||||
"workspace-next" | "workspace-previous" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(
|
||||
action == "workspace-next",
|
||||
))
|
||||
Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next"))
|
||||
}
|
||||
"spawn" => {
|
||||
let command = required_argument()?;
|
||||
let argv = commands.get(command).ok_or_else(|| {
|
||||
ConfigError::InvalidBinding(format!("unknown command {command:?}"))
|
||||
})?;
|
||||
let argv = commands
|
||||
.get(command)
|
||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("unknown command {command:?}")))?;
|
||||
Ok(BindingAction::Spawn(argv.clone()))
|
||||
}
|
||||
"close" => {
|
||||
@@ -1576,15 +1481,9 @@ fn parse_action(
|
||||
}
|
||||
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
|
||||
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
|
||||
"resize" => Ok(BindingAction::Resize(
|
||||
parse_direction(required_argument()?)?,
|
||||
)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
|
||||
required_argument()?,
|
||||
)?)),
|
||||
"resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)),
|
||||
"workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)),
|
||||
"toggle-maximized" => {
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleMaximized)
|
||||
@@ -1621,9 +1520,7 @@ fn parse_action(
|
||||
no_argument()?;
|
||||
Ok(BindingAction::ToggleOverview)
|
||||
}
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"unknown action {action:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1633,18 +1530,14 @@ fn parse_direction(argument: &str) -> Result<Direction, ConfigError> {
|
||||
"right" => Ok(Direction::Right),
|
||||
"up" => Ok(Direction::Up),
|
||||
"down" => Ok(Direction::Down),
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid direction {argument:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("invalid direction {argument:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||
match argument.parse() {
|
||||
Ok(workspace) if workspace > 0 => Ok(workspace),
|
||||
_ => Err(ConfigError::InvalidBinding(format!(
|
||||
"invalid workspace {argument:?}"
|
||||
))),
|
||||
_ => Err(ConfigError::InvalidBinding(format!("invalid workspace {argument:?}"))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1672,25 +1565,13 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
|
||||
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
|
||||
bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction)));
|
||||
bindings.push(binding(
|
||||
"Super+Shift+".to_owned() + key,
|
||||
"move",
|
||||
Some(direction),
|
||||
));
|
||||
bindings.push(binding(
|
||||
"Super+Ctrl+".to_owned() + key,
|
||||
"resize",
|
||||
Some(direction),
|
||||
));
|
||||
bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction)));
|
||||
bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction)));
|
||||
}
|
||||
|
||||
for workspace in 1..=9 {
|
||||
let workspace = workspace.to_string();
|
||||
bindings.push(binding(
|
||||
format!("Super+{workspace}"),
|
||||
"workspace",
|
||||
Some(&workspace),
|
||||
));
|
||||
bindings.push(binding(format!("Super+{workspace}"), "workspace", Some(&workspace)));
|
||||
bindings.push(binding(
|
||||
format!("Super+Shift+{workspace}"),
|
||||
"move-to-workspace",
|
||||
@@ -1701,11 +1582,7 @@ fn default_bindings() -> Vec<BindingConfig> {
|
||||
bindings
|
||||
}
|
||||
|
||||
fn binding(
|
||||
keys: impl Into<String>,
|
||||
action: impl Into<String>,
|
||||
argument: Option<&str>,
|
||||
) -> BindingConfig {
|
||||
fn binding(keys: impl Into<String>, action: impl Into<String>, argument: Option<&str>) -> BindingConfig {
|
||||
BindingConfig {
|
||||
keys: keys.into(),
|
||||
match_mode: BindingMatch::Keysym,
|
||||
@@ -1909,9 +1786,7 @@ fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String> {
|
||||
let (size, refresh) = value
|
||||
.trim()
|
||||
.split_once('@')
|
||||
.map_or((value.trim(), None), |(size, refresh)| {
|
||||
(size, Some(refresh))
|
||||
});
|
||||
.map_or((value.trim(), None), |(size, refresh)| (size, Some(refresh)));
|
||||
let (width, height) = size
|
||||
.split_once('x')
|
||||
.ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?;
|
||||
@@ -1944,17 +1819,10 @@ fn parse_output_mode(value: &str) -> Result<OutputModeRequest, String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_column_width(
|
||||
value: &ColumnWidthValue,
|
||||
field: &'static str,
|
||||
) -> Result<ColumnWidth, ConfigError> {
|
||||
fn parse_column_width(value: &ColumnWidthValue, field: &'static str) -> Result<ColumnWidth, ConfigError> {
|
||||
match value {
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => {
|
||||
Ok(ColumnWidth::Proportion(*value))
|
||||
}
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => {
|
||||
Ok(ColumnWidth::Full)
|
||||
}
|
||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
||||
value => Err(ConfigError::InvalidColumnWidth {
|
||||
field,
|
||||
value: match value {
|
||||
@@ -1982,15 +1850,9 @@ mod tests {
|
||||
let source = "input {\n touchpad {\n swipe-threshold 96\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\noutput-profile name=\"desk\" {\n output match=\"DP-1\" scale=1.5 {\n position 0 0\n }\n}\ndesktop-widgets {\n clock {\n enabled #true\n outputs \"DP-1\"\n }\n}\nautostart {\n command \"program\" \"argument with space\"\n}\n";
|
||||
let config = Config::parse_source(source).unwrap();
|
||||
config.runtime_config().unwrap();
|
||||
assert_eq!(
|
||||
config.input_settings().unwrap().touchpad.swipe_threshold,
|
||||
96
|
||||
);
|
||||
assert_eq!(config.input_settings().unwrap().touchpad.swipe_threshold, 96);
|
||||
assert_eq!(config.desktop_widgets.clock.outputs, ["DP-1"]);
|
||||
assert_eq!(
|
||||
config.output_profiles().unwrap()[0].outputs[0].position,
|
||||
Some([0, 0])
|
||||
);
|
||||
assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2026,10 +1888,7 @@ mod tests {
|
||||
let config = parse("");
|
||||
|
||||
assert_eq!(config.layout_mode(), LayoutMode::Scrolling);
|
||||
assert_eq!(
|
||||
config.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(0.5)
|
||||
);
|
||||
assert_eq!(config.default_column_width().unwrap(), ColumnWidth::Proportion(0.5));
|
||||
assert_eq!(config.gap_config().unwrap(), GapConfig::default());
|
||||
}
|
||||
|
||||
@@ -2039,10 +1898,7 @@ mod tests {
|
||||
let numeric = parse("scrolling {\n default-column-width 1.0\n}\n");
|
||||
|
||||
assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full);
|
||||
assert_eq!(
|
||||
numeric.default_column_width().unwrap(),
|
||||
ColumnWidth::Proportion(1.0)
|
||||
);
|
||||
assert_eq!(numeric.default_column_width().unwrap(), ColumnWidth::Proportion(1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2066,19 +1922,10 @@ mod tests {
|
||||
let minimal = parse("");
|
||||
let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n");
|
||||
let paged = parse("scrolling {\n focus-strategy \"paged\"\n}\n");
|
||||
assert_eq!(
|
||||
paged.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Paged
|
||||
);
|
||||
assert_eq!(paged.scrolling_focus_strategy(), ViewportFocusStrategy::Paged);
|
||||
|
||||
assert_eq!(
|
||||
minimal.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Minimal
|
||||
);
|
||||
assert_eq!(
|
||||
centered.scrolling_focus_strategy(),
|
||||
ViewportFocusStrategy::Center
|
||||
);
|
||||
assert_eq!(minimal.scrolling_focus_strategy(), ViewportFocusStrategy::Minimal);
|
||||
assert_eq!(centered.scrolling_focus_strategy(), ViewportFocusStrategy::Center);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2120,8 +1967,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_and_validates_layout_gaps() {
|
||||
let configured =
|
||||
parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let configured = parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n");
|
||||
let invalid = parse("layout {\n outer-gap -1.0\n}\n");
|
||||
|
||||
assert_eq!(
|
||||
@@ -2159,11 +2005,9 @@ mod tests {
|
||||
"theme {\n colors {\n border \"#11223344\"\n accent \"#AABBCC\"\n shadow \"#01020380\"\n }\n geometry {\n border-width 1.5\n focus-ring-width 3.0\n window-radius 12.0\n }\n shadow {\n soft {\n offset-y -2.0\n blur 24.0\n opacity 0.4\n }\n }\n material {\n style \"translucent\"\n }\n}\n",
|
||||
);
|
||||
let invalid_color = parse("theme {\n colors {\n accent \"blue\"\n }\n}\n");
|
||||
let invalid_radius =
|
||||
parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity = parse(
|
||||
"theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n",
|
||||
);
|
||||
let invalid_radius = parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n");
|
||||
let invalid_opacity =
|
||||
parse("theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n");
|
||||
|
||||
assert_eq!(
|
||||
configured.theme_settings().unwrap(),
|
||||
@@ -2197,24 +2041,15 @@ mod tests {
|
||||
assert!(invalid_radius.theme_settings().is_err());
|
||||
assert!(invalid_opacity.theme_settings().is_err());
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"mist\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"mist\"\n }\n}\n").is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn materials_default_to_solid_and_reject_removed_style() {
|
||||
assert_eq!(
|
||||
parse("").theme_settings().unwrap().material_style,
|
||||
MaterialStyle::Solid
|
||||
);
|
||||
assert_eq!(parse("").theme_settings().unwrap().material_style, MaterialStyle::Solid);
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n material {\n style \"glass\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
ferese_config::from_str::<Config>("theme {\n material {\n style \"glass\"\n }\n}\n").is_err()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2249,14 +2084,17 @@ mod tests {
|
||||
"from \"#112233\"\nto \"#445566\"\nangle #inf\n",
|
||||
] {
|
||||
assert!(
|
||||
Config::parse_source(&format!(
|
||||
"theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"
|
||||
))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
Config::parse_source(&format!("theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}"))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert!(ferese_config::from_str::<Config>("theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n").is_err());
|
||||
assert!(
|
||||
ferese_config::from_str::<Config>(
|
||||
"theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2336,11 +2174,9 @@ mod tests {
|
||||
("duration_ms", "#inf"),
|
||||
] {
|
||||
assert!(
|
||||
Config::parse_source(&format!(
|
||||
"appearance {{ inactive-dim {{ {key} {value}; }} }}"
|
||||
))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
Config::parse_source(&format!("appearance {{ inactive-dim {{ {key} {value}; }} }}"))
|
||||
.and_then(|config| config.theme_settings())
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2409,11 +2245,9 @@ mod tests {
|
||||
);
|
||||
for threshold in [0, 15, 1001] {
|
||||
assert!(
|
||||
parse(&format!(
|
||||
"input {{ touchpad {{ swipe-threshold {threshold}; }} }}"
|
||||
))
|
||||
.input_settings()
|
||||
.is_err()
|
||||
parse(&format!("input {{ touchpad {{ swipe-threshold {threshold}; }} }}"))
|
||||
.input_settings()
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2435,9 +2269,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn guide_uses_effective_bindings_including_overrides_and_unbindings() {
|
||||
let config = parse(
|
||||
"binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n",
|
||||
);
|
||||
let config =
|
||||
parse("binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n");
|
||||
let input = config.input_settings().unwrap();
|
||||
let map = physical_keymap(&input).unwrap();
|
||||
let entries = config
|
||||
@@ -2446,13 +2279,12 @@ mod tests {
|
||||
.iter()
|
||||
.filter_map(|binding| binding.guide_entry(Some(&map)))
|
||||
.collect::<Vec<_>>();
|
||||
assert!(!entries.iter().any(|entry| entry["description"] == "Close window"));
|
||||
assert!(
|
||||
!entries
|
||||
entries
|
||||
.iter()
|
||||
.any(|entry| entry["description"] == "Close window")
|
||||
.any(|entry| entry["keys"] == "Super + Tab" && entry["description"] == "Toggle floating window")
|
||||
);
|
||||
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Tab"
|
||||
&& entry["description"] == "Toggle floating window"));
|
||||
assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter"));
|
||||
}
|
||||
|
||||
@@ -2472,11 +2304,7 @@ mod tests {
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
|
||||
&& binding.action == action));
|
||||
}
|
||||
assert!(
|
||||
bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Exit)
|
||||
);
|
||||
assert!(bindings.iter().any(|binding| binding.action == BindingAction::Exit));
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.modifiers.logo
|
||||
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
|
||||
@@ -2491,11 +2319,7 @@ mod tests {
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print)
|
||||
&& binding.modifiers == BindingModifiers::default()
|
||||
&& binding.action
|
||||
== BindingAction::Spawn(vec![
|
||||
"ferese-screenshot".to_owned(),
|
||||
"--full".to_owned(),
|
||||
])
|
||||
&& binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()])
|
||||
}));
|
||||
assert!(bindings.iter().any(|binding| {
|
||||
binding.modifiers.logo
|
||||
@@ -2515,29 +2339,18 @@ mod tests {
|
||||
let bindings = replaced.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 48);
|
||||
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 bindings = unbound.bindings(&input).unwrap();
|
||||
assert_eq!(bindings.len(), 47);
|
||||
assert!(
|
||||
!bindings
|
||||
.iter()
|
||||
.any(|binding| binding.action == BindingAction::Close)
|
||||
);
|
||||
assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_physical_xkb_key_names() {
|
||||
let config = parse(
|
||||
"binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n",
|
||||
);
|
||||
let config = parse("binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n");
|
||||
let input = config.input_settings().unwrap();
|
||||
let bindings = config.bindings(&input).unwrap();
|
||||
|
||||
@@ -2549,13 +2362,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_duplicate_or_invalid_user_bindings() {
|
||||
let duplicate = parse(
|
||||
"binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n",
|
||||
);
|
||||
let missing_command =
|
||||
parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument =
|
||||
parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
let duplicate = parse("binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n");
|
||||
let missing_command = parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n");
|
||||
let invalid_argument = parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n");
|
||||
|
||||
for config in [duplicate, missing_command, invalid_argument] {
|
||||
let input = config.input_settings().unwrap();
|
||||
@@ -2569,12 +2378,7 @@ mod tests {
|
||||
"window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 fullscreen=#false\n",
|
||||
);
|
||||
let rules = config.window_rules().unwrap();
|
||||
let result = window_rules::resolve(
|
||||
&rules,
|
||||
Some("org.example.editor.desktop"),
|
||||
Some("Document"),
|
||||
false,
|
||||
);
|
||||
let result = window_rules::resolve(&rules, Some("org.example.editor.desktop"), Some("Document"), false);
|
||||
|
||||
assert_eq!(result.workspace, Some(3));
|
||||
assert_eq!(result.floating, Some(true));
|
||||
@@ -2640,13 +2444,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_output_profiles() {
|
||||
let invalid_scale =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate = parse(
|
||||
"output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n",
|
||||
);
|
||||
let invalid_scale = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n");
|
||||
let invalid_mode = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n");
|
||||
let duplicate =
|
||||
parse("output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n");
|
||||
|
||||
assert!(invalid_scale.output_profiles().is_err());
|
||||
assert!(invalid_mode.output_profiles().is_err());
|
||||
|
||||
+18
-35
@@ -1,11 +1,12 @@
|
||||
use std::{fs, sync::OnceLock};
|
||||
use std::fs;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use smithay::{
|
||||
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||
input::pointer::CursorIcon,
|
||||
utils::{Buffer, Point, Size, Transform},
|
||||
};
|
||||
use xcursor::{CursorTheme, parser::Image};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::input::pointer::CursorIcon;
|
||||
use smithay::utils::{Buffer, Point, Size, Transform};
|
||||
use xcursor::CursorTheme;
|
||||
use xcursor::parser::Image;
|
||||
|
||||
const DEFAULT_CURSOR_SIZE: u32 = 24;
|
||||
|
||||
@@ -58,26 +59,17 @@ fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Im
|
||||
let path = theme.load_icon(name)?;
|
||||
let bytes = fs::read(path).ok()?;
|
||||
|
||||
xcursor::parser::parse_xcursor(&bytes)?
|
||||
.into_iter()
|
||||
.min_by_key(|image| {
|
||||
(
|
||||
image.size.abs_diff(requested_size),
|
||||
image.width.abs_diff(requested_size),
|
||||
)
|
||||
})
|
||||
xcursor::parser::parse_xcursor(&bytes)?.into_iter().min_by_key(|image| {
|
||||
(
|
||||
image.size.abs_diff(requested_size),
|
||||
image.width.abs_diff(requested_size),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn cursor_from_image(image: Image) -> NamedCursor {
|
||||
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&image.pixels_rgba,
|
||||
Fourcc::Abgr8888,
|
||||
size,
|
||||
1,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None);
|
||||
|
||||
NamedCursor {
|
||||
buffer,
|
||||
@@ -122,18 +114,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn supplies_legacy_fallback_names_for_common_cursors() {
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Default),
|
||||
["default", "left_ptr", "arrow"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Pointer),
|
||||
["pointer", "hand2", "left_ptr"]
|
||||
);
|
||||
assert_eq!(
|
||||
cursor_names(CursorIcon::Text),
|
||||
["text", "xterm", "left_ptr"]
|
||||
);
|
||||
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
|
||||
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+10
-24
@@ -1,9 +1,10 @@
|
||||
//! Session-owned services. Never grant shell/effects privileges to daemons.
|
||||
use crate::{Ferese, config::DaemonConfig, private_client::ClientCapabilities};
|
||||
use std::{
|
||||
process::Child,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::process::Child;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::Ferese;
|
||||
use crate::config::DaemonConfig;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
|
||||
struct Service {
|
||||
config: DaemonConfig,
|
||||
@@ -17,15 +18,8 @@ pub(crate) struct Runner(Vec<Service>);
|
||||
impl Runner {
|
||||
pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) {
|
||||
let mut previous = std::mem::take(&mut self.0);
|
||||
for config in configs
|
||||
.iter()
|
||||
.filter(|c| c.enabled && (!nested || c.nested))
|
||||
{
|
||||
if self
|
||||
.0
|
||||
.iter()
|
||||
.any(|service| service.config.command == config.command)
|
||||
{
|
||||
for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) {
|
||||
if self.0.iter().any(|service| service.config.command == config.command) {
|
||||
continue;
|
||||
}
|
||||
if let Some(index) = previous
|
||||
@@ -132,12 +126,7 @@ mod tests {
|
||||
nested: false,
|
||||
};
|
||||
let mut runner = Runner::new(&[config.clone()], false);
|
||||
runner.0[0].child = Some(
|
||||
std::process::Command::new("/bin/sleep")
|
||||
.arg("30")
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
runner.0[0].child = Some(std::process::Command::new("/bin/sleep").arg("30").spawn().unwrap());
|
||||
let pid = runner.0[0].child.as_ref().unwrap().id();
|
||||
config.restart = true;
|
||||
runner.reconcile(&[config.clone()], false);
|
||||
@@ -150,10 +139,7 @@ mod tests {
|
||||
assert!(runner.0.is_empty());
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while unsafe { libc::kill(pid as i32, 0) } == 0 {
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"removed service was not stopped/reaped"
|
||||
);
|
||||
assert!(Instant::now() < deadline, "removed service was not stopped/reaped");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use crate::config::InactiveDimSettings;
|
||||
use ferese_layout::WindowId;
|
||||
use std::time::Duration;
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
use crate::config::InactiveDimSettings;
|
||||
|
||||
pub(crate) fn target(
|
||||
settings: InactiveDimSettings,
|
||||
focused: Option<WindowId>,
|
||||
@@ -60,11 +62,7 @@ impl DimAnimation {
|
||||
pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool {
|
||||
self.advance(
|
||||
target,
|
||||
if self.target != target {
|
||||
Duration::ZERO
|
||||
} else {
|
||||
delta
|
||||
},
|
||||
if self.target != target { Duration::ZERO } else { delta },
|
||||
duration_ms,
|
||||
)
|
||||
}
|
||||
|
||||
+35
-78
@@ -1,27 +1,23 @@
|
||||
use std::sync::{
|
||||
Mutex,
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
use ferese_protocols::effects::v1::server::{
|
||||
ferese_effects_manager_v1::{self, FereseEffectsManagerV1},
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
};
|
||||
use ferese_protocols::material::v1::server::{
|
||||
ferese_material_manager_v1::{self, FereseMaterialManagerV1},
|
||||
ferese_surface_material_v1::{self, FereseSurfaceMaterialV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak,
|
||||
backend::ClientId, protocol::wl_surface::WlSurface,
|
||||
},
|
||||
wayland::compositor::with_states,
|
||||
use ferese_protocols::effects::v1::server::ferese_effects_manager_v1::FereseEffectsManagerV1;
|
||||
use ferese_protocols::effects::v1::server::ferese_surface_effects_v1::FereseSurfaceEffectsV1;
|
||||
use ferese_protocols::effects::v1::server::{ferese_effects_manager_v1, ferese_surface_effects_v1};
|
||||
use ferese_protocols::material::v1::server::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::server::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use ferese_protocols::material::v1::server::{ferese_material_manager_v1, ferese_surface_material_v1};
|
||||
use smithay::reexports::wayland_server::backend::ClientId;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak,
|
||||
};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
|
||||
use crate::{
|
||||
Ferese, config::MaterialStyle, private_client::ClientCapabilities, state::ClientState,
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::MaterialStyle;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum SemanticRole {
|
||||
@@ -41,16 +37,10 @@ pub(crate) struct ResolvedMaterial {
|
||||
pub shadow: [f64; 3],
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_material(
|
||||
role: SemanticRole,
|
||||
style: MaterialStyle,
|
||||
opacity: f32,
|
||||
) -> ResolvedMaterial {
|
||||
pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity: f32) -> ResolvedMaterial {
|
||||
let shadow = match role {
|
||||
SemanticRole::Panel => [1.0, 5.0, 0.04],
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => {
|
||||
[2.0, 8.0, 0.07]
|
||||
}
|
||||
SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => [2.0, 8.0, 0.07],
|
||||
SemanticRole::Hud => [1.0, 4.0, 0.04],
|
||||
SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09],
|
||||
};
|
||||
@@ -58,11 +48,7 @@ pub(crate) fn resolve_material(
|
||||
MaterialStyle::Solid => 1.0,
|
||||
MaterialStyle::Translucent => opacity.clamp(0.0, 1.0),
|
||||
};
|
||||
ResolvedMaterial {
|
||||
style,
|
||||
opacity,
|
||||
shadow,
|
||||
}
|
||||
ResolvedMaterial { style, opacity, shadow }
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -145,9 +131,7 @@ impl Dispatch<FereseMaterialManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => {
|
||||
let attached = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
let effects = states.data_map.get::<SurfaceEffectsState>().unwrap();
|
||||
effects.attached.swap(true, Ordering::AcqRel)
|
||||
});
|
||||
@@ -220,13 +204,9 @@ fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
|
||||
.0
|
||||
.iter()
|
||||
.map(|tuple| {
|
||||
let values: [i32; 5] =
|
||||
std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
||||
(values[2] > 0
|
||||
&& values[3] > 0
|
||||
&& values[4] >= 0
|
||||
&& values.iter().all(|v| v.abs_diff(0) <= 32768))
|
||||
.then_some(values)
|
||||
let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
||||
(values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768))
|
||||
.then_some(values)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -273,9 +253,7 @@ impl Dispatch<FereseEffectsManagerV1, ()> for Ferese {
|
||||
match request {
|
||||
ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => {
|
||||
let already_attached = with_states(&surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new);
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
@@ -346,14 +324,10 @@ impl Dispatch<FereseSurfaceEffectsV1, SurfaceEffectsUserData> for Ferese {
|
||||
ferese_surface_effects_v1::Request::SetRole { role } => {
|
||||
let role = match role {
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => {
|
||||
SemanticRole::PanelElevated
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => SemanticRole::PanelElevated,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Notification) => {
|
||||
SemanticRole::Notification
|
||||
}
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Notification) => SemanticRole::Notification,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud,
|
||||
WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal,
|
||||
WEnum::Unknown(_) | WEnum::Value(_) => return,
|
||||
@@ -437,22 +411,17 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.map_or(1.0, |effects| {
|
||||
*effects.opacity.lock().unwrap() as f32 / 1000.0
|
||||
})
|
||||
.map_or(1.0, |effects| *effects.opacity.lock().unwrap() as f32 / 1000.0)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool {
|
||||
with_states(surface, |states| {
|
||||
states
|
||||
.data_map
|
||||
.get::<SurfaceEffectsState>()
|
||||
.is_some_and(|effects| {
|
||||
effects.presentation_supported.load(Ordering::Acquire)
|
||||
&& effects.attached.load(Ordering::Acquire)
|
||||
&& !effects.dismissing.swap(true, Ordering::AcqRel)
|
||||
})
|
||||
states.data_map.get::<SurfaceEffectsState>().is_some_and(|effects| {
|
||||
effects.presentation_supported.load(Ordering::Acquire)
|
||||
&& effects.attached.load(Ordering::Acquire)
|
||||
&& !effects.dismissing.swap(true, Ordering::AcqRel)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -477,20 +446,8 @@ mod tests {
|
||||
assert_eq!(decode_regions(&[]), Some(vec![]));
|
||||
assert!(decode_regions(&bytes[..19]).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],
|
||||
] {
|
||||
assert!(
|
||||
decode_regions(
|
||||
&invalid
|
||||
.into_iter()
|
||||
.flat_map(i32::to_ne_bytes)
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.is_none()
|
||||
);
|
||||
for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] {
|
||||
assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::<Vec<_>>()).is_none());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
use smithay::{
|
||||
desktop::Window,
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::{Logical, Point, Size},
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Size};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -44,12 +41,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
@@ -57,12 +49,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
frame: AxisFrame,
|
||||
) {
|
||||
fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) {
|
||||
handle.axis(data, frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
use smithay::{
|
||||
desktop::Window,
|
||||
input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||
GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab,
|
||||
PointerInnerHandle, RelativeMotionEvent,
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel,
|
||||
wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
utils::{Logical, Point, Rectangle, Size},
|
||||
wayland::{compositor::with_states, shell::xdg::SurfaceCachedState},
|
||||
use smithay::desktop::Window;
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent,
|
||||
GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData,
|
||||
MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Size};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::SurfaceCachedState;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -31,9 +27,7 @@ impl ResizeEdge {
|
||||
fn left(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Left
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::BottomLeft
|
||||
xdg_toplevel::ResizeEdge::Left | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::BottomLeft
|
||||
)
|
||||
}
|
||||
|
||||
@@ -49,9 +43,7 @@ impl ResizeEdge {
|
||||
fn top(self) -> bool {
|
||||
matches!(
|
||||
self.0,
|
||||
xdg_toplevel::ResizeEdge::Top
|
||||
| xdg_toplevel::ResizeEdge::TopLeft
|
||||
| xdg_toplevel::ResizeEdge::TopRight
|
||||
xdg_toplevel::ResizeEdge::Top | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::TopRight
|
||||
)
|
||||
}
|
||||
|
||||
@@ -82,16 +74,8 @@ impl ResizeSurfaceGrab {
|
||||
initial_rect: Rectangle<i32, Logical>,
|
||||
) -> Self {
|
||||
ResizeState::with(
|
||||
window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel")
|
||||
.wl_surface(),
|
||||
|state| {
|
||||
*state = ResizeState::Resizing {
|
||||
edges,
|
||||
initial_rect,
|
||||
}
|
||||
},
|
||||
window.toplevel().expect("managed window has a toplevel").wl_surface(),
|
||||
|state| *state = ResizeState::Resizing { edges, initial_rect },
|
||||
);
|
||||
Self {
|
||||
start_data,
|
||||
@@ -114,17 +98,13 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
) {
|
||||
handle.motion(data, None, event);
|
||||
let delta = event.location - self.start_data.location;
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
let (minimum, maximum) = with_states(surface.wl_surface(), |states| {
|
||||
let mut cached = states.cached_state.get::<SurfaceCachedState>();
|
||||
let state = cached.current();
|
||||
(state.min_size, state.max_size)
|
||||
});
|
||||
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);
|
||||
data.set_floating_window_geometry(&self.window, rect.loc, rect.size);
|
||||
|
||||
@@ -145,20 +125,12 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
handle.relative_motion(data, focus, event);
|
||||
}
|
||||
|
||||
fn button(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
event: &ButtonEvent,
|
||||
) {
|
||||
fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) {
|
||||
handle.button(data, event);
|
||||
if handle.current_pressed().is_empty() {
|
||||
self.finished = true;
|
||||
handle.unset_grab(self, data, event.serial, event.time, true);
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.last_size);
|
||||
@@ -173,12 +145,7 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
}
|
||||
}
|
||||
|
||||
fn axis(
|
||||
&mut self,
|
||||
data: &mut Ferese,
|
||||
handle: &mut PointerInnerHandle<'_, Ferese>,
|
||||
frame: AxisFrame,
|
||||
) {
|
||||
fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) {
|
||||
handle.axis(data, frame);
|
||||
}
|
||||
|
||||
@@ -267,21 +234,14 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
||||
return;
|
||||
}
|
||||
|
||||
let surface = self
|
||||
.window
|
||||
.toplevel()
|
||||
.expect("managed window has a toplevel");
|
||||
let surface = self.window.toplevel().expect("managed window has a toplevel");
|
||||
surface.with_pending_state(|state| {
|
||||
state.states.unset(xdg_toplevel::State::Resizing);
|
||||
state.size = Some(self.initial_rect.size);
|
||||
});
|
||||
surface.send_pending_configure();
|
||||
ResizeState::with(surface.wl_surface(), |state| *state = ResizeState::Idle);
|
||||
data.set_floating_window_geometry(
|
||||
&self.window,
|
||||
self.initial_rect.loc,
|
||||
self.initial_rect.size,
|
||||
);
|
||||
data.set_floating_window_geometry(&self.window, self.initial_rect.loc, self.initial_rect.size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,14 +276,8 @@ impl ResizeState {
|
||||
fn commit(&mut self) -> Option<(ResizeEdge, Rectangle<i32, Logical>)> {
|
||||
match *self {
|
||||
Self::Idle => None,
|
||||
Self::Resizing {
|
||||
edges,
|
||||
initial_rect,
|
||||
} => Some((edges, initial_rect)),
|
||||
Self::WaitingForFinalCommit {
|
||||
edges,
|
||||
initial_rect,
|
||||
} => {
|
||||
Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)),
|
||||
Self::WaitingForFinalCommit { edges, initial_rect } => {
|
||||
*self = Self::Idle;
|
||||
Some((edges, initial_rect))
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use smithay::{
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::SERIAL_COUNTER,
|
||||
wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
},
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::SERIAL_COUNTER;
|
||||
use smithay::wayland::xdg_activation::{
|
||||
XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
@@ -21,12 +19,7 @@ impl XdgActivationHandler for Ferese {
|
||||
data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE
|
||||
}
|
||||
|
||||
fn request_activation(
|
||||
&mut self,
|
||||
token: XdgActivationToken,
|
||||
data: XdgActivationTokenData,
|
||||
surface: WlSurface,
|
||||
) {
|
||||
fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) {
|
||||
if self.session_lock.active || self.input_capture.captures(1) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
use smithay::{
|
||||
backend::{allocator::dmabuf::Dmabuf, renderer::utils::on_commit_buffer_handler},
|
||||
reexports::wayland_server::{
|
||||
Client,
|
||||
protocol::{wl_buffer, wl_surface::WlSurface},
|
||||
},
|
||||
wayland::{
|
||||
buffer::BufferHandler,
|
||||
compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent,
|
||||
is_sync_subsurface,
|
||||
},
|
||||
dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier},
|
||||
shm::{ShmHandler, ShmState},
|
||||
},
|
||||
use smithay::backend::allocator::dmabuf::Dmabuf;
|
||||
use smithay::backend::renderer::utils::on_commit_buffer_handler;
|
||||
use smithay::reexports::wayland_server::Client;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::buffer::BufferHandler;
|
||||
use smithay::wayland::compositor::{
|
||||
CompositorClientState, CompositorHandler, CompositorState, get_parent, is_sync_subsurface,
|
||||
};
|
||||
use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier};
|
||||
use smithay::wayland::shm::{ShmHandler, ShmState};
|
||||
|
||||
use super::{layer_shell, xdg_shell};
|
||||
use crate::{Ferese, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
|
||||
fn layer_affects_backdrop(layer: smithay::wayland::shell::wlr_layer::Layer) -> bool {
|
||||
matches!(
|
||||
layer,
|
||||
smithay::wayland::shell::wlr_layer::Layer::Background
|
||||
| smithay::wayland::shell::wlr_layer::Layer::Bottom
|
||||
smithay::wayland::shell::wlr_layer::Layer::Background | smithay::wayland::shell::wlr_layer::Layer::Bottom
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,11 +63,7 @@ impl CompositorHandler for Ferese {
|
||||
let window = {
|
||||
self.space
|
||||
.elements()
|
||||
.find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||
})
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root))
|
||||
.cloned()
|
||||
};
|
||||
|
||||
@@ -124,12 +115,7 @@ impl DmabufHandler for Ferese {
|
||||
&mut self.dmabuf_state
|
||||
}
|
||||
|
||||
fn dmabuf_imported(
|
||||
&mut self,
|
||||
_global: &DmabufGlobal,
|
||||
dmabuf: Dmabuf,
|
||||
notifier: ImportNotifier,
|
||||
) {
|
||||
fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
|
||||
self.queue_dmabuf_import(dmabuf, notifier);
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -143,8 +129,9 @@ impl ShmHandler for Ferese {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::wayland::shell::wlr_layer::Layer;
|
||||
|
||||
use super::*;
|
||||
#[test]
|
||||
fn panel_commits_do_not_invalidate_their_own_backdrop() {
|
||||
assert!(!layer_affects_backdrop(Layer::Top));
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
use smithay::{
|
||||
desktop::{PopupKind, WindowSurfaceType, layer_map_for_output},
|
||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||
utils::{Logical, Rectangle},
|
||||
wayland::input_method::{InputMethodHandler, PopupSurface},
|
||||
};
|
||||
use smithay::desktop::{PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
use smithay::wayland::input_method::{InputMethodHandler, PopupSurface};
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
impl InputMethodHandler for Ferese {
|
||||
fn new_popup(&mut self, surface: PopupSurface) {
|
||||
if let Err(error) = self
|
||||
.popups
|
||||
.track_popup(PopupKind::InputMethod(surface.clone()))
|
||||
{
|
||||
if let Err(error) = self.popups.track_popup(PopupKind::InputMethod(surface.clone())) {
|
||||
tracing::warn!(?error, "failed to track input method popup");
|
||||
}
|
||||
crate::backends::direct::render_all(self);
|
||||
@@ -38,10 +33,7 @@ impl InputMethodHandler for Ferese {
|
||||
return geometry;
|
||||
}
|
||||
|
||||
let Some(layer) = self
|
||||
.space
|
||||
.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
let Some(layer) = self.space.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL) else {
|
||||
return Rectangle::default();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
use smithay::{
|
||||
desktop::{
|
||||
LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output,
|
||||
},
|
||||
output::Output,
|
||||
reexports::wayland_server::protocol::{wl_output::WlOutput, wl_surface::WlSurface},
|
||||
wayland::shell::{
|
||||
wlr_layer::{
|
||||
KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
|
||||
},
|
||||
xdg::PopupSurface,
|
||||
},
|
||||
use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::shell::wlr_layer::{
|
||||
KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState,
|
||||
};
|
||||
use smithay::wayland::shell::xdg::PopupSurface;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -19,13 +14,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
&mut self.layer_shell_state
|
||||
}
|
||||
|
||||
fn new_layer_surface(
|
||||
&mut self,
|
||||
surface: LayerSurface,
|
||||
output: Option<WlOutput>,
|
||||
_layer: Layer,
|
||||
namespace: String,
|
||||
) {
|
||||
fn new_layer_surface(&mut self, surface: LayerSurface, output: Option<WlOutput>, _layer: Layer, namespace: String) {
|
||||
let output = output
|
||||
.as_ref()
|
||||
.and_then(Output::from_resource)
|
||||
@@ -42,10 +31,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
}
|
||||
|
||||
tracing::debug!(%namespace, output = %output.name(), "mapped layer surface");
|
||||
super::set_surface_tree_scale(
|
||||
layer.wl_surface(),
|
||||
output.current_scale().fractional_scale(),
|
||||
);
|
||||
super::set_surface_tree_scale(layer.wl_surface(), output.current_scale().fractional_scale());
|
||||
self.relayout();
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
@@ -89,10 +75,7 @@ impl WlrLayerShellHandler for Ferese {
|
||||
}
|
||||
|
||||
pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
let Some(layer) = state
|
||||
.space
|
||||
.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL)
|
||||
else {
|
||||
let Some(layer) = state.space.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) else {
|
||||
return;
|
||||
};
|
||||
let outputs = state.space.outputs().cloned().collect::<Vec<_>>();
|
||||
@@ -112,15 +95,11 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
||||
&& policy.keyboard_interactivity == KeyboardInteractivity::Exclusive
|
||||
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
||||
{
|
||||
state
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.expect("seat has a keyboard")
|
||||
.set_focus(
|
||||
state,
|
||||
Some(layer.wl_surface().clone()),
|
||||
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
state.seat.get_keyboard().expect("seat has a keyboard").set_focus(
|
||||
state,
|
||||
Some(layer.wl_surface().clone()),
|
||||
smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||
);
|
||||
} else if !layer.can_receive_keyboard_focus() && layer_has_keyboard_focus(state, &layer) {
|
||||
state.restore_keyboard_focus();
|
||||
}
|
||||
@@ -136,10 +115,6 @@ fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.current_focus())
|
||||
.and_then(|surface| {
|
||||
state
|
||||
.space
|
||||
.layer_for_surface(&surface, WindowSurfaceType::ALL)
|
||||
})
|
||||
.and_then(|surface| state.space.layer_for_surface(&surface, WindowSurfaceType::ALL))
|
||||
.is_some_and(|focused| focused == *layer)
|
||||
}
|
||||
@@ -8,42 +8,31 @@ pub(crate) mod screenshot_worker;
|
||||
pub(crate) mod window_capture;
|
||||
mod xdg_shell;
|
||||
|
||||
use smithay::{
|
||||
desktop::{PopupManager, layer_map_for_output},
|
||||
input::{
|
||||
Seat, SeatHandler, SeatState,
|
||||
pointer::{CursorImageStatus, PointerHandle},
|
||||
},
|
||||
reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface},
|
||||
utils::Point,
|
||||
wayland::{
|
||||
compositor::{TraversalAction, with_states, with_surface_tree_downward},
|
||||
fractional_scale::{FractionalScaleHandler, with_fractional_scale},
|
||||
idle_inhibit::IdleInhibitHandler,
|
||||
idle_notify::{IdleNotifierHandler, IdleNotifierState},
|
||||
keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState,
|
||||
KeyboardShortcutsInhibitor, KeyboardShortcutsInhibitorSeat,
|
||||
},
|
||||
output::OutputHandler,
|
||||
pointer_constraints::{
|
||||
PointerConstraint, PointerConstraintsHandler, with_pointer_constraint,
|
||||
},
|
||||
selection::{
|
||||
SelectionHandler,
|
||||
data_device::{
|
||||
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler,
|
||||
set_data_device_focus,
|
||||
},
|
||||
primary_selection::{
|
||||
PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
|
||||
},
|
||||
},
|
||||
tablet_manager::TabletSeatHandler,
|
||||
xdg_foreign::{XdgForeignHandler, XdgForeignState},
|
||||
xdg_toplevel_icon::XdgToplevelIconHandler,
|
||||
},
|
||||
use smithay::desktop::{PopupManager, layer_map_for_output};
|
||||
use smithay::input::pointer::{CursorImageStatus, PointerHandle};
|
||||
use smithay::input::{Seat, SeatHandler, SeatState};
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::wayland::compositor::{TraversalAction, with_states, with_surface_tree_downward};
|
||||
use smithay::wayland::fractional_scale::{FractionalScaleHandler, with_fractional_scale};
|
||||
use smithay::wayland::idle_inhibit::IdleInhibitHandler;
|
||||
use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::{
|
||||
KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor,
|
||||
KeyboardShortcutsInhibitorSeat,
|
||||
};
|
||||
use smithay::wayland::output::OutputHandler;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint};
|
||||
use smithay::wayland::selection::SelectionHandler;
|
||||
use smithay::wayland::selection::data_device::{
|
||||
ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, set_data_device_focus,
|
||||
};
|
||||
use smithay::wayland::selection::primary_selection::{
|
||||
PrimarySelectionHandler, PrimarySelectionState, set_primary_focus,
|
||||
};
|
||||
use smithay::wayland::tablet_manager::TabletSeatHandler;
|
||||
use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState};
|
||||
use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconHandler;
|
||||
|
||||
use crate::Ferese;
|
||||
|
||||
@@ -75,9 +64,7 @@ impl SeatHandler for Ferese {
|
||||
{
|
||||
inhibitor.inactivate();
|
||||
}
|
||||
if let Some(inhibitor) =
|
||||
focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface))
|
||||
{
|
||||
if let Some(inhibitor) = focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface)) {
|
||||
inhibitor.activate();
|
||||
self.active_shortcuts_inhibitor = Some(inhibitor);
|
||||
}
|
||||
@@ -137,10 +124,7 @@ impl KeyboardShortcutsInhibitHandler for Ferese {
|
||||
}
|
||||
|
||||
fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) {
|
||||
let focused = self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.and_then(|keyboard| keyboard.current_focus());
|
||||
let focused = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus());
|
||||
|
||||
if focused.as_ref() == Some(inhibitor.wl_surface()) {
|
||||
inhibitor.activate();
|
||||
@@ -199,7 +183,14 @@ pub(crate) fn set_surface_tree_scale(surface: &WlSurface, scale: f64) {
|
||||
}
|
||||
|
||||
impl PointerConstraintsHandler for Ferese {
|
||||
fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle<Self>) {
|
||||
if self
|
||||
.locked_pointer_hint
|
||||
.as_ref()
|
||||
.is_some_and(|hint| hint.surface == *surface)
|
||||
{
|
||||
self.locked_pointer_hint = None;
|
||||
}
|
||||
self.activate_focused_pointer_constraint(pointer);
|
||||
}
|
||||
|
||||
@@ -218,20 +209,10 @@ impl PointerConstraintsHandler for Ferese {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((focused_surface, origin)) = self.surface_under(pointer.current_location()) else {
|
||||
return;
|
||||
};
|
||||
if focused_surface == *surface {
|
||||
let current = pointer.current_location();
|
||||
let (scale_x, scale_y) = self
|
||||
.window_under_visual(current)
|
||||
.and_then(|window| self.visual_scale_for_window(&window))
|
||||
.unwrap_or((1.0, 1.0));
|
||||
let current_surface_location = current - origin;
|
||||
let offset = location - current_surface_location;
|
||||
|
||||
pointer.set_location(current + Point::from((offset.x * scale_x, offset.y * scale_y)));
|
||||
}
|
||||
self.locked_pointer_hint = Some(crate::input::LockedPointerHint {
|
||||
surface: surface.clone(),
|
||||
local: location,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use super::screenshot::{CaptureBuffer, PartOutcome, PartSender};
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{ExportMem, TextureMapping},
|
||||
},
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1},
|
||||
zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1},
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::{Buffer, Logical, Rectangle, Size},
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::{ExportMem, TextureMapping};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{
|
||||
zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1,
|
||||
};
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::utils::{Buffer, Logical, Rectangle, Size};
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
use super::screenshot::{CaptureBuffer, PartOutcome, PartSender};
|
||||
use crate::Ferese;
|
||||
|
||||
const BYTES_PER_PIXEL: usize = 4;
|
||||
@@ -150,8 +144,7 @@ impl PendingScreencopy {
|
||||
|
||||
pub(crate) fn capture_allowed() -> bool {
|
||||
static ALLOWED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ALLOWED
|
||||
.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
*ALLOWED.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref()))
|
||||
}
|
||||
|
||||
fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool {
|
||||
@@ -312,9 +305,7 @@ impl Ferese {
|
||||
let captures = std::mem::take(&mut self.pending_screencopies);
|
||||
|
||||
for capture in captures {
|
||||
if &capture.output != output
|
||||
|| !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
{
|
||||
if &capture.output != output || !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) {
|
||||
remaining.push(capture);
|
||||
continue;
|
||||
}
|
||||
@@ -325,8 +316,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
framebuffer_binding_changed = true;
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err()
|
||||
{
|
||||
if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err() {
|
||||
capture.fail();
|
||||
}
|
||||
}
|
||||
@@ -336,10 +326,9 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool {
|
||||
self.pending_screencopies.iter().any(|capture| {
|
||||
capture.output == *output
|
||||
&& cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)
|
||||
})
|
||||
self.pending_screencopies
|
||||
.iter()
|
||||
.any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,10 +393,7 @@ fn create_frame(
|
||||
}
|
||||
}
|
||||
|
||||
fn capture_region(
|
||||
output: &Output,
|
||||
requested: Option<Rectangle<i32, Logical>>,
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
fn capture_region(output: &Output, requested: Option<Rectangle<i32, Logical>>) -> Option<Rectangle<i32, Buffer>> {
|
||||
let mode = output.current_mode()?;
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let transform = output.current_transform();
|
||||
@@ -434,9 +420,7 @@ pub(crate) fn capture_region_for_geometry(
|
||||
) -> Option<Rectangle<i32, Buffer>> {
|
||||
let logical_size = output_logical_size(mode_size, scale, transform);
|
||||
let output_region = Rectangle::from_size(logical_size);
|
||||
let logical_region = requested
|
||||
.unwrap_or(output_region)
|
||||
.intersection(output_region)?;
|
||||
let logical_region = requested.unwrap_or(output_region).intersection(output_region)?;
|
||||
|
||||
if logical_region.size.w <= 0 || logical_region.size.h <= 0 {
|
||||
return None;
|
||||
@@ -488,9 +472,7 @@ where
|
||||
tracing::warn!(?error, "failed to map output framebuffer copy");
|
||||
})?;
|
||||
let expected = capture.region.size;
|
||||
let stride = (expected.w as usize)
|
||||
.checked_mul(BYTES_PER_PIXEL)
|
||||
.ok_or(())?;
|
||||
let stride = (expected.w as usize).checked_mul(BYTES_PER_PIXEL).ok_or(())?;
|
||||
let bytes = stride.checked_mul(expected.h as usize).ok_or(())?;
|
||||
if source.len() < bytes {
|
||||
return Err(());
|
||||
@@ -548,20 +530,17 @@ where
|
||||
frame.flags(zwlr_screencopy_frame_v1::Flags::empty());
|
||||
|
||||
let seconds = timestamp.as_secs();
|
||||
frame.ready(
|
||||
(seconds >> 32) as u32,
|
||||
seconds as u32,
|
||||
timestamp.subsec_nanos(),
|
||||
);
|
||||
frame.ready((seconds >> 32) as u32, seconds as u32, timestamp.subsec_nanos());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::utils::{Physical, Transform};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn capture_is_off_unless_the_opt_in_is_exactly_one() {
|
||||
use std::ffi::OsStr;
|
||||
@@ -580,8 +559,7 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((-10, 20).into(), (110, 50).into());
|
||||
|
||||
let region =
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
let region = capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap();
|
||||
|
||||
assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into()));
|
||||
}
|
||||
@@ -591,9 +569,7 @@ mod tests {
|
||||
let mode = Size::<i32, Physical>::from((1_920, 1_080));
|
||||
let requested = Rectangle::new((1_000, 600).into(), (100, 100).into());
|
||||
|
||||
assert!(
|
||||
capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none()
|
||||
);
|
||||
assert!(capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -36,9 +36,7 @@ const MAX_CANVAS_BYTES: usize = 128 * 1024 * 1024;
|
||||
|
||||
impl Canvas {
|
||||
fn new(width: u32, height: u32) -> Option<Self> {
|
||||
let length = (width as usize)
|
||||
.checked_mul(height as usize)?
|
||||
.checked_mul(4)?;
|
||||
let length = (width as usize).checked_mul(height as usize)?.checked_mul(4)?;
|
||||
if width == 0 || height == 0 || length > MAX_CANVAS_BYTES {
|
||||
return None;
|
||||
}
|
||||
@@ -81,11 +79,7 @@ impl Canvas {
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_and_convert(
|
||||
frame: &OutputFrame,
|
||||
target_width: usize,
|
||||
target_height: usize,
|
||||
) -> Option<Vec<u8>> {
|
||||
fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: usize) -> Option<Vec<u8>> {
|
||||
let (upright_width, upright_height) = upright_size(frame);
|
||||
if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 {
|
||||
return None;
|
||||
@@ -97,13 +91,8 @@ fn scale_and_convert(
|
||||
if upright_width as usize == target_width && upright_height as usize == target_height {
|
||||
for row in 0..target_height {
|
||||
for column in 0..target_width {
|
||||
let (x, y) = upright_source_pixel(
|
||||
column as i32,
|
||||
row as i32,
|
||||
frame.width,
|
||||
frame.height,
|
||||
frame.transform,
|
||||
);
|
||||
let (x, y) =
|
||||
upright_source_pixel(column as i32, row as i32, frame.width, frame.height, frame.transform);
|
||||
let from = (y as usize)
|
||||
.checked_mul(frame.stride)?
|
||||
.checked_add(x as usize * BYTES_PER_PIXEL)?;
|
||||
@@ -154,13 +143,7 @@ fn scale_and_convert(
|
||||
Some(out)
|
||||
}
|
||||
|
||||
fn upright_source_pixel(
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
transform: Transform,
|
||||
) -> (i32, i32) {
|
||||
fn upright_source_pixel(x: i32, y: i32, width: i32, height: i32, transform: Transform) -> (i32, i32) {
|
||||
match transform {
|
||||
Transform::_90 => (y, height - 1 - x),
|
||||
Transform::_180 => (width - 1 - x, height - 1 - y),
|
||||
@@ -186,10 +169,7 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
}
|
||||
|
||||
let (left, top) = logical_origin(frames).ok_or("Screenshot region is empty")?;
|
||||
let scale = frames
|
||||
.iter()
|
||||
.map(|frame| frame.scale)
|
||||
.fold(0.0_f64, f64::max);
|
||||
let scale = frames.iter().map(|frame| frame.scale).fold(0.0_f64, f64::max);
|
||||
if !(scale.is_finite() && scale > 0.0) {
|
||||
return Err("Screenshot outputs have an invalid scale".into());
|
||||
}
|
||||
@@ -216,22 +196,15 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result<Canvas, String> {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region is empty".into());
|
||||
}
|
||||
let mut canvas = Canvas::new(width as u32, height as u32)
|
||||
.ok_or("Screenshot image is too large to encode")?;
|
||||
let mut canvas = Canvas::new(width as u32, height as u32).ok_or("Screenshot image is too large to encode")?;
|
||||
|
||||
for (frame, (x0, y0, target_width, target_height)) in frames.iter().zip(placement) {
|
||||
if target_width == 0 || target_height == 0 {
|
||||
continue;
|
||||
}
|
||||
let scaled = scale_and_convert(frame, target_width as usize, target_height as usize)
|
||||
.ok_or("Invalid capture size")?;
|
||||
canvas.blit(
|
||||
&scaled,
|
||||
target_width as usize,
|
||||
target_height as usize,
|
||||
x0,
|
||||
y0,
|
||||
);
|
||||
let scaled =
|
||||
scale_and_convert(frame, target_width as usize, target_height as usize).ok_or("Invalid capture size")?;
|
||||
canvas.blit(&scaled, target_width as usize, target_height as usize, x0, y0);
|
||||
}
|
||||
|
||||
Ok(canvas)
|
||||
@@ -292,12 +265,7 @@ fn logical_origin(frames: &[OutputFrame]) -> Option<(i32, i32)> {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Geometry {
|
||||
All,
|
||||
Region {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
},
|
||||
Region { x: i32, y: i32, width: i32, height: i32 },
|
||||
}
|
||||
|
||||
impl Geometry {
|
||||
@@ -307,16 +275,9 @@ impl Geometry {
|
||||
if width <= 0 || height <= 0 {
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
x.checked_add(width)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height)
|
||||
.ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
x.checked_add(width).ok_or("screenshot geometry overflows")?;
|
||||
y.checked_add(height).ok_or("screenshot geometry overflows")?;
|
||||
Ok(Geometry::Region { x, y, width, height })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,10 +355,7 @@ struct Request {
|
||||
|
||||
pub(crate) enum Action {
|
||||
None,
|
||||
Encode {
|
||||
request: u64,
|
||||
frames: Vec<OutputFrame>,
|
||||
},
|
||||
Encode { request: u64, frames: Vec<OutputFrame> },
|
||||
// The path was already sent to the caller; it must not be cleaned up.
|
||||
Delivered(PathBuf),
|
||||
// The request is gone, so this file is ours to remove.
|
||||
@@ -461,10 +419,7 @@ impl Coordinator {
|
||||
response_id,
|
||||
reply,
|
||||
state: RequestState::Collecting,
|
||||
parts: specs
|
||||
.into_iter()
|
||||
.map(|spec| (spec, PartState::Pending))
|
||||
.collect(),
|
||||
parts: specs.into_iter().map(|spec| (spec, PartState::Pending)).collect(),
|
||||
received: 0,
|
||||
deadline: Instant::now() + REQUEST_TIMEOUT,
|
||||
},
|
||||
@@ -485,12 +440,7 @@ impl Coordinator {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn on_part(
|
||||
&mut self,
|
||||
request: u64,
|
||||
part: usize,
|
||||
result: Result<CaptureBuffer, String>,
|
||||
) -> Action {
|
||||
pub(crate) fn on_part(&mut self, request: u64, part: usize, result: Result<CaptureBuffer, String>) -> Action {
|
||||
let Some(entry) = self.requests.get_mut(&request) else {
|
||||
return Action::None;
|
||||
};
|
||||
@@ -595,10 +545,7 @@ impl Coordinator {
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
for id in &expired {
|
||||
self.reject_inner(
|
||||
*id,
|
||||
"Screenshot timed out: an output was not redrawn in time",
|
||||
);
|
||||
self.reject_inner(*id, "Screenshot timed out: an output was not redrawn in time");
|
||||
}
|
||||
expired
|
||||
}
|
||||
@@ -682,10 +629,7 @@ impl PlannedPart {
|
||||
// Turns a requested logical region into the per-output reads that cover it.
|
||||
// The logical-to-buffer conversion is shared with the screencopy path so both
|
||||
// agree on rounding at fractional scales.
|
||||
pub(crate) fn plan(
|
||||
geometry: &Geometry,
|
||||
outputs: &[OutputLayout],
|
||||
) -> Result<Vec<PlannedPart>, String> {
|
||||
pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result<Vec<PlannedPart>, String> {
|
||||
if outputs.is_empty() {
|
||||
return Err("No outputs are enabled".into());
|
||||
}
|
||||
@@ -696,8 +640,7 @@ pub(crate) fn plan(
|
||||
{
|
||||
return Err("Screenshot region must have a positive size".into());
|
||||
}
|
||||
let logical_sizes: Vec<Size<i32, Logical>> =
|
||||
outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
let logical_sizes: Vec<Size<i32, Logical>> = outputs.iter().map(OutputLayout::logical_size).collect();
|
||||
|
||||
let requested = match geometry {
|
||||
Geometry::All => {
|
||||
@@ -709,10 +652,8 @@ pub(crate) fn plan(
|
||||
};
|
||||
union = Some(match union {
|
||||
Some(current) => {
|
||||
let top_left = Point::<i32, Logical>::new(
|
||||
current.loc.x.min(rect.loc.x),
|
||||
current.loc.y.min(rect.loc.y),
|
||||
);
|
||||
let top_left =
|
||||
Point::<i32, Logical>::new(current.loc.x.min(rect.loc.x), current.loc.y.min(rect.loc.y));
|
||||
let bottom_right = Point::<i32, Logical>::new(
|
||||
(current.loc.x + current.size.w).max(rect.loc.x + rect.size.w),
|
||||
(current.loc.y + current.size.h).max(rect.loc.y + rect.size.h),
|
||||
@@ -724,12 +665,7 @@ pub(crate) fn plan(
|
||||
}
|
||||
union.expect("outputs is not empty")
|
||||
}
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
} => Rectangle {
|
||||
Geometry::Region { x, y, width, height } => Rectangle {
|
||||
loc: (*x, *y).into(),
|
||||
size: (*width, *height).into(),
|
||||
},
|
||||
@@ -757,11 +693,7 @@ pub(crate) fn plan(
|
||||
}
|
||||
// capture_region_for_geometry works in output-local logical space.
|
||||
let local = Rectangle {
|
||||
loc: (
|
||||
clipped.loc.x - output.location.x,
|
||||
clipped.loc.y - output.location.y,
|
||||
)
|
||||
.into(),
|
||||
loc: (clipped.loc.x - output.location.x, clipped.loc.y - output.location.y).into(),
|
||||
size: clipped.size,
|
||||
};
|
||||
// The clip above already intersected against this same logical rect, so
|
||||
@@ -808,34 +740,19 @@ mod planning {
|
||||
}
|
||||
|
||||
fn region(x: i32, y: i32, width: i32, height: i32) -> Geometry {
|
||||
Geometry::Region {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Geometry::Region { x, y, width, height }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_crop_crossing_a_seam_splits_into_one_part_per_output() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(1800, 0, 200, 100), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (1800, 120));
|
||||
assert_eq!(
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(1800, 120)
|
||||
);
|
||||
assert_eq!(
|
||||
(
|
||||
parts[1].index,
|
||||
parts[1].logical.loc.x,
|
||||
parts[1].logical.size.w
|
||||
),
|
||||
(parts[1].index, parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(1, 1920, 80)
|
||||
);
|
||||
// The clipped widths must add back up to the request.
|
||||
@@ -868,14 +785,8 @@ mod planning {
|
||||
let outputs = [output(0, 0, 800, 600, 1.0), output(800, 0, 1600, 1200, 1.5)];
|
||||
let parts = plan(®ion(600, 0, 400, 300), &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
assert_eq!(
|
||||
(parts[0].logical.loc.x, parts[0].logical.size.w),
|
||||
(600, 200)
|
||||
);
|
||||
assert_eq!(
|
||||
(parts[1].logical.loc.x, parts[1].logical.size.w),
|
||||
(800, 200)
|
||||
);
|
||||
assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (600, 200));
|
||||
assert_eq!((parts[1].logical.loc.x, parts[1].logical.size.w), (800, 200));
|
||||
// The 1.5 output reads back at 1.5x while still reporting logical 200.
|
||||
let spec = parts[1].spec(&outputs[1]);
|
||||
assert_eq!(spec.logical_width, 200);
|
||||
@@ -885,10 +796,7 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn all_geometry_unions_every_output() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(-1280, -400, 1280, 800, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(-1280, -400, 1280, 800, 1.0)];
|
||||
let parts = plan(&Geometry::All, &outputs).unwrap();
|
||||
assert_eq!(parts.len(), 2);
|
||||
// Neither output is moved into a normalised origin at plan time.
|
||||
@@ -899,10 +807,7 @@ mod planning {
|
||||
|
||||
#[test]
|
||||
fn a_crop_misses_outputs_that_do_not_intersect() {
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(4000, 0, 100, 100), &outputs);
|
||||
assert!(parts.is_err(), "a region off the desktop is reported");
|
||||
}
|
||||
@@ -912,17 +817,11 @@ mod planning {
|
||||
// The second output starts exactly where the region ends. It contributes
|
||||
// nothing, so the capture must still succeed with just the first output
|
||||
// rather than failing on a region it does not overlap.
|
||||
let outputs = [
|
||||
output(0, 0, 1920, 1080, 1.0),
|
||||
output(1920, 0, 1920, 1080, 1.0),
|
||||
];
|
||||
let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)];
|
||||
let parts = plan(®ion(100, 100, 1820, 980), &outputs).expect("captures the overlap");
|
||||
assert_eq!(parts.len(), 1, "only the overlapping output is read");
|
||||
assert_eq!(parts[0].index, 0);
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1820, 980)
|
||||
);
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1820, 980));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -932,16 +831,10 @@ mod planning {
|
||||
// A 90-degree transform turns the 1920x1080 mode into a 1080x1920
|
||||
// logical area, and the readback follows the same rotation.
|
||||
let parts = plan(®ion(0, 0, 1080, 400), std::slice::from_ref(&rotated)).unwrap();
|
||||
assert_eq!(
|
||||
(parts[0].logical.size.w, parts[0].logical.size.h),
|
||||
(1080, 400)
|
||||
);
|
||||
assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1080, 400));
|
||||
let spec = parts[0].spec(&rotated);
|
||||
assert_eq!(spec.transform, Transform::_90);
|
||||
assert!(
|
||||
spec.buffer_height > spec.buffer_width,
|
||||
"buffer axes are swapped"
|
||||
);
|
||||
assert!(spec.buffer_height > spec.buffer_width, "buffer axes are swapped");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -981,10 +874,12 @@ mod planning {
|
||||
|
||||
#[cfg(test)]
|
||||
mod completion {
|
||||
use std::sync::mpsc::sync_channel;
|
||||
|
||||
use smithay::reexports::calloop::channel::channel as loop_channel;
|
||||
|
||||
use super::*;
|
||||
use crate::handlers::screencopy::CaptureSink;
|
||||
use smithay::reexports::calloop::channel::channel as loop_channel;
|
||||
use std::sync::mpsc::sync_channel;
|
||||
|
||||
fn spec(width: i32, height: i32) -> PartSpec {
|
||||
PartSpec {
|
||||
@@ -1021,10 +916,7 @@ mod completion {
|
||||
coordinator
|
||||
.admit(42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect())
|
||||
.expect("admitted");
|
||||
Self {
|
||||
coordinator,
|
||||
received,
|
||||
}
|
||||
Self { coordinator, received }
|
||||
}
|
||||
|
||||
fn response(&self) -> Option<Response> {
|
||||
@@ -1041,21 +933,13 @@ mod completion {
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))),
|
||||
Action::None
|
||||
));
|
||||
assert!(
|
||||
harness.coordinator.is_live(id),
|
||||
"still collecting after part 0"
|
||||
);
|
||||
assert!(harness.coordinator.is_live(id), "still collecting after part 0");
|
||||
|
||||
assert!(matches!(
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("readback failed".into())),
|
||||
harness.coordinator.on_part(id, 1, Err("readback failed".into())),
|
||||
Action::None
|
||||
));
|
||||
assert!(
|
||||
!harness.coordinator.is_live(id),
|
||||
"one failed part ends the request"
|
||||
);
|
||||
assert!(!harness.coordinator.is_live(id), "one failed part ends the request");
|
||||
|
||||
let response = harness.response().expect("caller is always answered");
|
||||
assert_eq!(response.id, 42);
|
||||
@@ -1070,9 +954,7 @@ mod completion {
|
||||
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
// Output 1 is removed from the layout, so its pending capture is failed.
|
||||
harness
|
||||
.coordinator
|
||||
.on_part(id, 1, Err("output was removed".into()));
|
||||
harness.coordinator.on_part(id, 1, Err("output was removed".into()));
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
assert!(harness.response().unwrap().error.is_some());
|
||||
|
||||
@@ -1142,9 +1024,7 @@ mod completion {
|
||||
// Unlocking does not resurrect the request; the file the worker
|
||||
// eventually produces is ours to delete.
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/late.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(ref path) if path.ends_with("late.png")));
|
||||
assert!(harness.response().is_none(), "no late success is delivered");
|
||||
}
|
||||
@@ -1155,9 +1035,7 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png")));
|
||||
let response = harness.response().expect("success is answered");
|
||||
assert_eq!(response.id, 42);
|
||||
assert!(response.error.is_none());
|
||||
@@ -1165,9 +1043,7 @@ mod completion {
|
||||
|
||||
// A duplicate completion for the same request has nowhere to go, and
|
||||
// its file is an orphan rather than a second delivery.
|
||||
let action = harness
|
||||
.coordinator
|
||||
.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/again.png")));
|
||||
assert!(matches!(action, Action::DiscardFile(_)));
|
||||
|
||||
// A delivered path is reported separately so the caller cannot
|
||||
@@ -1190,9 +1066,7 @@ mod completion {
|
||||
let id = harness.coordinator.next_id;
|
||||
harness.coordinator.on_part(id, 0, Ok(buffer(2, 2)));
|
||||
|
||||
harness
|
||||
.coordinator
|
||||
.on_encoded(id, Err("encoding failed".into()));
|
||||
harness.coordinator.on_encoded(id, Err("encoding failed".into()));
|
||||
let response = harness.response().expect("failure is answered");
|
||||
assert_eq!(response.error.unwrap().message, "encoding failed");
|
||||
assert!(!harness.coordinator.is_live(id));
|
||||
@@ -1205,23 +1079,13 @@ mod completion {
|
||||
// client would never be answered.
|
||||
let mut harness = Harness::new(2);
|
||||
let id = harness.coordinator.next_id;
|
||||
harness
|
||||
.coordinator
|
||||
.requests
|
||||
.get_mut(&id)
|
||||
.expect("tracked")
|
||||
.deadline = Instant::now() - Duration::from_secs(1);
|
||||
harness.coordinator.requests.get_mut(&id).expect("tracked").deadline = Instant::now() - Duration::from_secs(1);
|
||||
|
||||
assert_eq!(harness.coordinator.expire(), vec![id]);
|
||||
let response = harness.response().expect("the caller is answered");
|
||||
assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed");
|
||||
assert!(
|
||||
response
|
||||
.error
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.message
|
||||
.contains("timed out"),
|
||||
response.error.as_ref().unwrap().message.contains("timed out"),
|
||||
"the failure explains itself: {:?}",
|
||||
response.error.as_ref().unwrap().message
|
||||
);
|
||||
@@ -1302,11 +1166,7 @@ mod completion {
|
||||
// buffer that was handed off is never replaced.
|
||||
assert!(!sink.publish(buffer(64, 64)));
|
||||
|
||||
let PartOutcome {
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
} = receiver.try_recv().expect("published once");
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("published once");
|
||||
assert_eq!((request, part), (7, 1));
|
||||
assert_eq!(result.unwrap().width, 2);
|
||||
assert!(receiver.try_recv().is_err(), "no second buffer was sent");
|
||||
@@ -1321,11 +1181,7 @@ mod completion {
|
||||
// failure has already been reported.
|
||||
assert!(!sink.publish(buffer(2, 2)));
|
||||
|
||||
let PartOutcome {
|
||||
request,
|
||||
part,
|
||||
result,
|
||||
} = receiver.try_recv().expect("answered");
|
||||
let PartOutcome { request, part, result } = receiver.try_recv().expect("answered");
|
||||
assert_eq!((request, part), (3, 0));
|
||||
assert!(result.is_err());
|
||||
assert!(receiver.try_recv().is_err());
|
||||
@@ -1715,11 +1571,7 @@ mod tests {
|
||||
// 9 logical at scale 1.5.
|
||||
assert_eq!((canvas.width, canvas.height), (14, 5));
|
||||
for column in 0..canvas.width as usize {
|
||||
assert_eq!(
|
||||
canvas.pixels[column * 4 + 3],
|
||||
255,
|
||||
"column {column} has a gap"
|
||||
);
|
||||
assert_eq!(canvas.pixels[column * 4 + 3], 255, "column {column} has a gap");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1751,11 +1603,7 @@ mod tests {
|
||||
|
||||
let canvas = compose(&[left, right]).unwrap();
|
||||
assert_eq!(canvas.width, 5);
|
||||
assert_eq!(
|
||||
&canvas.pixels[2 * 4..3 * 4],
|
||||
&[0, 0, 0, 0],
|
||||
"gap is transparent"
|
||||
);
|
||||
assert_eq!(&canvas.pixels[2 * 4..3 * 4], &[0, 0, 0, 0], "gap is transparent");
|
||||
assert_eq!(canvas.pixels[3], 255);
|
||||
assert_eq!(canvas.pixels[3 * 4 + 3], 255);
|
||||
}
|
||||
@@ -1784,11 +1632,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn geometry_tolerates_surrounding_whitespace_from_slurp() {
|
||||
for raw in [
|
||||
"100,200 800x600\n",
|
||||
" 100,200 800x600 ",
|
||||
"100,200 800x600\r\n",
|
||||
] {
|
||||
for raw in ["100,200 800x600\n", " 100,200 800x600 ", "100,200 800x600\r\n"] {
|
||||
assert_eq!(
|
||||
parse_geometry(raw).unwrap(),
|
||||
Geometry::Region {
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use std::{fs, io, thread};
|
||||
|
||||
use smithay::reexports::calloop::channel;
|
||||
|
||||
@@ -125,14 +123,8 @@ fn encode_png(canvas: &Canvas) -> Result<Vec<u8>, String> {
|
||||
image::codecs::png::CompressionType::Fast,
|
||||
image::codecs::png::FilterType::Adaptive,
|
||||
);
|
||||
image::ImageEncoder::write_image(
|
||||
encoder,
|
||||
image.as_raw(),
|
||||
width,
|
||||
height,
|
||||
image::ExtendedColorType::Rgba8,
|
||||
)
|
||||
.map_err(|error| format!("Could not encode screenshot: {error}"))?;
|
||||
image::ImageEncoder::write_image(encoder, image.as_raw(), width, height, image::ExtendedColorType::Rgba8)
|
||||
.map_err(|error| format!("Could not encode screenshot: {error}"))?;
|
||||
tracing::debug!(encoded = out.len(), "encoded screenshot");
|
||||
Ok(out)
|
||||
}
|
||||
@@ -161,8 +153,8 @@ fn write_private_file(png: &[u8]) -> Result<PathBuf, String> {
|
||||
}
|
||||
|
||||
fn private_directory() -> Result<PathBuf, String> {
|
||||
let runtime = std::env::var_os("XDG_RUNTIME_DIR")
|
||||
.ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let runtime =
|
||||
std::env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?;
|
||||
let root = PathBuf::from(runtime);
|
||||
let directory = root.join("ferese-screenshots");
|
||||
match fs::symlink_metadata(&directory) {
|
||||
@@ -177,9 +169,7 @@ fn private_directory() -> Result<PathBuf, String> {
|
||||
}
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => {
|
||||
fs::create_dir(&directory)
|
||||
.and_then(|()| {
|
||||
fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE))
|
||||
})
|
||||
.and_then(|()| fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE)))
|
||||
.map_err(|error| format!("Could not create screenshot directory: {error}"))?;
|
||||
}
|
||||
Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")),
|
||||
@@ -189,9 +179,10 @@ fn private_directory() -> Result<PathBuf, String> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn frame(width: i32, height: i32) -> OutputFrame {
|
||||
let stride = width as usize * 4;
|
||||
let mut pixels = vec![0u8; stride * height as usize];
|
||||
@@ -237,10 +228,7 @@ mod tests {
|
||||
let path = write_private_file(&png).expect("staged");
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777;
|
||||
assert_eq!(
|
||||
mode, FILE_MODE,
|
||||
"staged screenshots are not group/world readable"
|
||||
);
|
||||
assert_eq!(mode, FILE_MODE, "staged screenshots are not group/world readable");
|
||||
fs::remove_file(&path).unwrap();
|
||||
}
|
||||
|
||||
@@ -254,10 +242,7 @@ mod tests {
|
||||
fs::write(&other, b"not a screenshot").unwrap();
|
||||
|
||||
sweep_stale_files();
|
||||
assert!(
|
||||
fresh.exists(),
|
||||
"a freshly staged screenshot is still in use"
|
||||
);
|
||||
assert!(fresh.exists(), "a freshly staged screenshot is still in use");
|
||||
assert!(other.exists(), "unrelated files are left alone");
|
||||
|
||||
fs::remove_file(&fresh).unwrap();
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
use std::{
|
||||
os::unix::fs::MetadataExt,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::{
|
||||
self as manager, FereseWindowCaptureManagerV1,
|
||||
};
|
||||
use smithay::{
|
||||
reexports::{
|
||||
wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{
|
||||
self as frame, ZwlrScreencopyFrameV1,
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
protocol::{wl_buffer::WlBuffer, wl_shm},
|
||||
},
|
||||
},
|
||||
utils::Rectangle,
|
||||
wayland::shm::with_buffer_contents_mut,
|
||||
};
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1 as manager;
|
||||
use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::FereseWindowCaptureManagerV1;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1 as frame;
|
||||
use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer;
|
||||
use smithay::reexports::wayland_server::protocol::wl_shm;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::utils::Rectangle;
|
||||
use smithay::wayland::shm::with_buffer_contents_mut;
|
||||
|
||||
use super::{screencopy::FrameData, screenshot::CaptureBuffer};
|
||||
use super::screencopy::FrameData;
|
||||
use super::screenshot::CaptureBuffer;
|
||||
use crate::Ferese;
|
||||
|
||||
const MAX_BYTES: usize = 128 * 1024 * 1024;
|
||||
@@ -42,10 +34,7 @@ struct Permit {
|
||||
impl Permit {
|
||||
fn reserve(budget: &Arc<Mutex<Budget>>, bytes: usize) -> Option<Self> {
|
||||
let mut available = budget.lock().unwrap();
|
||||
if bytes == 0
|
||||
|| bytes > MAX_BYTES.saturating_sub(available.bytes)
|
||||
|| available.frames >= MAX_FRAMES
|
||||
{
|
||||
if bytes == 0 || bytes > MAX_BYTES.saturating_sub(available.bytes) || available.frames >= MAX_FRAMES {
|
||||
return None;
|
||||
}
|
||||
available.bytes += bytes;
|
||||
@@ -79,11 +68,7 @@ pub(crate) struct Global {
|
||||
|
||||
pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
use std::os::fd::AsRawFd;
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
||||
let mut length = std::mem::size_of_val(&credentials) as libc::socklen_t;
|
||||
// SAFETY: the connected socket is live and both out pointers cover their
|
||||
// initialized stack allocations. SO_PEERCRED identifies the connecting process.
|
||||
@@ -101,33 +86,23 @@ pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool {
|
||||
}
|
||||
let expected = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| {
|
||||
exe.parent()
|
||||
.map(|dir| dir.join("xdg-desktop-portal-ferese"))
|
||||
})
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join("xdg-desktop-portal-ferese")))
|
||||
.and_then(|exe| std::fs::metadata(exe).ok());
|
||||
let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).ok();
|
||||
matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino())
|
||||
}
|
||||
|
||||
pub(super) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(
|
||||
1,
|
||||
Global {
|
||||
budget: Arc::default(),
|
||||
},
|
||||
);
|
||||
display.create_global::<Ferese, FereseWindowCaptureManagerV1, Global>(1, Global { budget: Arc::default() });
|
||||
}
|
||||
|
||||
impl GlobalDispatch<FereseWindowCaptureManagerV1, Global> for Ferese {
|
||||
fn can_view(client: Client, _: &Global) -> bool {
|
||||
client
|
||||
.get_data::<crate::state::ClientState>()
|
||||
.is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
client.get_data::<crate::state::ClientState>().is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE)
|
||||
})
|
||||
}
|
||||
|
||||
fn bind(
|
||||
@@ -226,8 +201,7 @@ impl Ferese {
|
||||
.clone();
|
||||
let geometry = window.geometry();
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> =
|
||||
geometry.size.to_physical_precise_round(scale);
|
||||
let size: smithay::utils::Size<i32, smithay::utils::Physical> = geometry.size.to_physical_precise_round(scale);
|
||||
let bytes = usize::try_from(size.w)
|
||||
.ok()?
|
||||
.checked_mul(usize::try_from(size.h).ok()?)?
|
||||
@@ -247,8 +221,7 @@ impl Ferese {
|
||||
}
|
||||
let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?;
|
||||
let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64();
|
||||
let (surface, _) = window
|
||||
.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
let (surface, _) = window.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?;
|
||||
if pointer.current_focus().as_ref() != Some(&surface) {
|
||||
return None;
|
||||
}
|
||||
@@ -298,18 +271,10 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn copy_snapshot(
|
||||
state: &Ferese,
|
||||
resource: &ZwlrScreencopyFrameV1,
|
||||
data: &FrameData,
|
||||
buffer: &WlBuffer,
|
||||
) {
|
||||
pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, data: &FrameData, buffer: &WlBuffer) {
|
||||
let mut used = data.used.lock().unwrap();
|
||||
if *used {
|
||||
resource.post_error(
|
||||
frame::Error::AlreadyUsed,
|
||||
"window capture frame has already been used",
|
||||
);
|
||||
resource.post_error(frame::Error::AlreadyUsed, "window capture frame has already been used");
|
||||
return;
|
||||
}
|
||||
*used = true;
|
||||
@@ -342,11 +307,7 @@ pub(super) fn copy_snapshot(
|
||||
// SAFETY: Smithay owns the writable mapping, validated format/stride and
|
||||
// checked offset cover the complete snapshot; the source is separately owned.
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
snapshot.pixels.pixels.as_ptr(),
|
||||
destination.add(offset),
|
||||
bytes,
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(snapshot.pixels.pixels.as_ptr(), destination.add(offset), bytes);
|
||||
}
|
||||
true
|
||||
})
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
use smithay::{
|
||||
desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window,
|
||||
WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords,
|
||||
},
|
||||
input::{
|
||||
Seat,
|
||||
pointer::{Focus, GrabStartData},
|
||||
},
|
||||
reexports::wayland_protocols::xdg::{
|
||||
decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode,
|
||||
shell::server::xdg_toplevel::State as ToplevelState,
|
||||
},
|
||||
reexports::wayland_server::{
|
||||
Resource,
|
||||
protocol::{wl_seat, wl_surface::WlSurface},
|
||||
},
|
||||
utils::Serial,
|
||||
wayland::{
|
||||
compositor::with_states,
|
||||
shell::xdg::decoration::XdgDecorationHandler,
|
||||
shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState,
|
||||
XdgToplevelSurfaceData,
|
||||
},
|
||||
},
|
||||
use smithay::desktop::{
|
||||
PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window, WindowSurfaceType,
|
||||
find_popup_root_surface, get_popup_toplevel_coords,
|
||||
};
|
||||
use smithay::input::Seat;
|
||||
use smithay::input::pointer::{Focus, GrabStartData};
|
||||
use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode;
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as ToplevelState;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_seat;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, Rectangle, Serial};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
|
||||
use smithay::wayland::shell::xdg::{
|
||||
PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit};
|
||||
|
||||
impl XdgShellHandler for Ferese {
|
||||
fn xdg_shell_state(&mut self) -> &mut XdgShellState {
|
||||
@@ -93,12 +81,7 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn reposition_request(
|
||||
&mut self,
|
||||
surface: PopupSurface,
|
||||
positioner: PositionerState,
|
||||
token: u32,
|
||||
) {
|
||||
fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) {
|
||||
surface.with_pending_state(|state| {
|
||||
state.geometry = positioner.get_geometry();
|
||||
state.positioner = positioner;
|
||||
@@ -273,12 +256,7 @@ impl XdgShellHandler for Ferese {
|
||||
};
|
||||
|
||||
if let Some(pointer) = seat.get_pointer() {
|
||||
pointer.set_grab(
|
||||
self,
|
||||
PopupPointerGrab::new(&popup_grab),
|
||||
serial,
|
||||
Focus::Keep,
|
||||
);
|
||||
pointer.set_grab(self, PopupPointerGrab::new(&popup_grab), serial, Focus::Keep);
|
||||
}
|
||||
if let Some(keyboard) = seat.get_keyboard() {
|
||||
keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial);
|
||||
@@ -290,21 +268,14 @@ impl XdgShellHandler for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_grab(
|
||||
seat: &Seat<Ferese>,
|
||||
surface: &WlSurface,
|
||||
serial: Serial,
|
||||
) -> Option<GrabStartData<Ferese>> {
|
||||
fn check_grab(seat: &Seat<Ferese>, surface: &WlSurface, serial: Serial) -> Option<GrabStartData<Ferese>> {
|
||||
let pointer = seat.get_pointer()?;
|
||||
if !pointer.has_grab(serial) {
|
||||
return None;
|
||||
}
|
||||
let start_data = pointer.grab_start_data()?;
|
||||
let (focus, _) = start_data.focus.as_ref()?;
|
||||
focus
|
||||
.id()
|
||||
.same_client_as(&surface.id())
|
||||
.then_some(start_data)
|
||||
focus.id().same_client_as(&surface.id()).then_some(start_data)
|
||||
}
|
||||
|
||||
impl XdgDecorationHandler for Ferese {
|
||||
@@ -381,9 +352,7 @@ pub fn handle_commit(popups: &mut PopupManager, space: &mut Space<Window>, surfa
|
||||
if let Some(PopupKind::Xdg(popup)) = popups.find_popup(surface)
|
||||
&& !popup.is_initial_configure_sent()
|
||||
{
|
||||
popup
|
||||
.send_configure()
|
||||
.expect("initial popup configure is valid");
|
||||
popup.send_configure().expect("initial popup configure is valid");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,40 +413,82 @@ impl Ferese {
|
||||
let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else {
|
||||
return;
|
||||
};
|
||||
let window = self.space.elements().find(|window| {
|
||||
window
|
||||
.toplevel()
|
||||
.is_some_and(|toplevel| toplevel.wl_surface() == &root)
|
||||
});
|
||||
let window = self
|
||||
.space
|
||||
.elements()
|
||||
.find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root));
|
||||
let geometry = (|| {
|
||||
if let Some(window) = window {
|
||||
let output = self.space.outputs().next()?;
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let window_geometry = self.space.element_geometry(window)?;
|
||||
|
||||
Some((output_geometry, window_geometry.loc))
|
||||
} else {
|
||||
let layer = self
|
||||
let id = *self.window_ids.get(window)?;
|
||||
let output = self
|
||||
.space
|
||||
.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
.outputs()
|
||||
.find(|output| self.window_belongs_to_output(id, output))?;
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
let location = (presented.x.round() as i32, presented.y.round() as i32).into();
|
||||
|
||||
Some((output_geometry, location))
|
||||
} else {
|
||||
let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?;
|
||||
|
||||
self.space.outputs().find_map(|output| {
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let layer_geometry =
|
||||
smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
let layer_geometry = smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?;
|
||||
|
||||
Some((output_geometry, output_geometry.loc + layer_geometry.loc))
|
||||
})
|
||||
}
|
||||
})();
|
||||
let Some((mut target, root_location)) = geometry else {
|
||||
let Some((target, root_location)) = geometry else {
|
||||
return;
|
||||
};
|
||||
|
||||
target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone()));
|
||||
target.loc -= root_location;
|
||||
let target = popup_constraint_target(
|
||||
target,
|
||||
root_location,
|
||||
get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())),
|
||||
);
|
||||
popup.with_pending_state(|state| {
|
||||
state.geometry = state.positioner.get_unconstrained_geometry(target);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn popup_constraint_target(
|
||||
mut output: Rectangle<i32, Logical>,
|
||||
root_location: Point<i32, Logical>,
|
||||
parent_offset: Point<i32, Logical>,
|
||||
) -> Rectangle<i32, Logical> {
|
||||
output.loc -= root_location + parent_offset;
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn popup_uses_presented_parent_coordinates_on_an_offset_monitor() {
|
||||
let output = Rectangle::new((1920, -200).into(), (1920, 1080).into());
|
||||
let root = Point::from((2200, -100));
|
||||
let parent = Point::from((40, 30));
|
||||
let target = popup_constraint_target(output, root, parent);
|
||||
assert_eq!(target.loc, Point::from((-320, -130)));
|
||||
let positioner = PositionerState {
|
||||
rect_size: (320, 180).into(),
|
||||
anchor_rect: Rectangle::new((1850, 900).into(), (1, 1).into()),
|
||||
anchor_edges: xdg_positioner::Anchor::BottomRight,
|
||||
gravity: xdg_positioner::Gravity::BottomRight,
|
||||
constraint_adjustment: xdg_positioner::ConstraintAdjustment::SlideX
|
||||
| xdg_positioner::ConstraintAdjustment::SlideY,
|
||||
..Default::default()
|
||||
};
|
||||
let popup = positioner.get_unconstrained_geometry(target);
|
||||
assert_eq!(popup.loc, Point::from((1280, 770)));
|
||||
let global = Rectangle::new(root + parent + popup.loc, popup.size);
|
||||
assert_eq!(global.intersection(output), Some(global));
|
||||
}
|
||||
}
|
||||
+193
-112
@@ -1,53 +1,122 @@
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use std::process::Command;
|
||||
|
||||
use ferese_layout::Direction;
|
||||
use smithay::{
|
||||
backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent,
|
||||
GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState,
|
||||
KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch,
|
||||
SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
},
|
||||
input::{
|
||||
keyboard::{FilterResult, keysyms},
|
||||
pointer::{
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent,
|
||||
GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle,
|
||||
RelativeMotionEvent,
|
||||
},
|
||||
touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent},
|
||||
},
|
||||
reexports::{
|
||||
wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge,
|
||||
wayland_server::protocol::wl_surface::WlSurface,
|
||||
},
|
||||
utils::{Logical, Point, SERIAL_COUNTER, Serial},
|
||||
wayland::{
|
||||
keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat,
|
||||
pointer_constraints::{PointerConstraint, with_pointer_constraint},
|
||||
},
|
||||
use smithay::backend::input::{
|
||||
AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent,
|
||||
GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent,
|
||||
PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, SwitchToggleEvent, TouchEvent,
|
||||
};
|
||||
use smithay::input::keyboard::{FilterResult, keysyms};
|
||||
use smithay::input::pointer::{
|
||||
AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent,
|
||||
GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent,
|
||||
};
|
||||
use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent};
|
||||
use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge;
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER, Serial};
|
||||
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
|
||||
use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint};
|
||||
|
||||
use crate::{
|
||||
Ferese,
|
||||
config::BindingAction,
|
||||
gestures::SwipeDirection,
|
||||
grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab},
|
||||
};
|
||||
use crate::Ferese;
|
||||
use crate::config::BindingAction;
|
||||
use crate::gestures::SwipeDirection;
|
||||
use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab};
|
||||
|
||||
const BTN_LEFT: u32 = 0x110;
|
||||
const BTN_RIGHT: u32 = 0x111;
|
||||
|
||||
pub(crate) struct LockedPointerHint {
|
||||
pub(crate) surface: WlSurface,
|
||||
pub(crate) local: Point<f64, Logical>,
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
pub(crate) fn pointer_hint_position(
|
||||
&self,
|
||||
surface: &WlSurface,
|
||||
hint: Point<f64, Logical>,
|
||||
) -> Option<Point<f64, Logical>> {
|
||||
for window in self.space.elements() {
|
||||
let root = window.toplevel()?.wl_surface();
|
||||
let geometry = window.geometry();
|
||||
let Some(local) = surface_tree_position(root, surface, geometry.loc) else {
|
||||
continue;
|
||||
};
|
||||
let id = self.window_ids.get(window)?;
|
||||
let presented = self.presented_window_rect(*id)?;
|
||||
let (scale_x, scale_y) = self.visual_scale_for_window(window)?;
|
||||
let point = local.to_f64() + hint - geometry.loc.to_f64();
|
||||
return Some(Point::from((
|
||||
presented.x + point.x * scale_x,
|
||||
presented.y + point.y * scale_y,
|
||||
)));
|
||||
}
|
||||
for output in self.space.outputs() {
|
||||
let output_geometry = self.space.output_geometry(output)?;
|
||||
let layers = smithay::desktop::layer_map_for_output(output);
|
||||
for layer in layers.layers() {
|
||||
let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) else {
|
||||
continue;
|
||||
};
|
||||
let geometry = layers.layer_geometry(layer)?;
|
||||
return Some((output_geometry.loc + geometry.loc + local).to_f64() + hint);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn apply_unlocked_pointer_hint(&mut self) {
|
||||
let Some(hint) = self.locked_pointer_hint.take() else {
|
||||
return;
|
||||
};
|
||||
if !hint.surface.is_alive() || self.session_lock.active || self.input_capture.active() {
|
||||
return;
|
||||
}
|
||||
let Some(pointer) = self.seat.get_pointer() else {
|
||||
return;
|
||||
};
|
||||
let locked = with_pointer_constraint(&hint.surface, &pointer, |constraint| {
|
||||
constraint.is_some_and(|constraint| {
|
||||
constraint.is_active() && matches!(&*constraint, PointerConstraint::Locked(_))
|
||||
})
|
||||
});
|
||||
if locked {
|
||||
self.locked_pointer_hint = Some(hint);
|
||||
return;
|
||||
}
|
||||
if pointer.current_focus().is_some_and(|surface| {
|
||||
with_pointer_constraint(&surface, &pointer, |constraint| {
|
||||
constraint.is_some_and(|constraint| constraint.is_active())
|
||||
})
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
let Some(global) = self.pointer_hint_position(&hint.surface, hint.local) else {
|
||||
return;
|
||||
};
|
||||
let location = self.clamp_pointer_position(global);
|
||||
let focus = self.surface_under(location);
|
||||
pointer.motion(
|
||||
self,
|
||||
focus,
|
||||
&MotionEvent {
|
||||
location,
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: self.last_pointer_time,
|
||||
},
|
||||
);
|
||||
pointer.frame(self);
|
||||
self.cursor_redraw_pending = true;
|
||||
}
|
||||
|
||||
pub fn process_input_event<I: InputBackend>(&mut self, event: InputEvent<I>) {
|
||||
let seat = self.seat.clone();
|
||||
self.idle_notifier_state.notify_activity(&seat);
|
||||
if !matches!(
|
||||
&event,
|
||||
InputEvent::DeviceAdded { .. }
|
||||
| InputEvent::DeviceRemoved { .. }
|
||||
| InputEvent::SwitchToggle { .. }
|
||||
InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. }
|
||||
) {
|
||||
self.lock_input_activity();
|
||||
}
|
||||
@@ -117,15 +186,13 @@ impl Ferese {
|
||||
if self.swipe.active() {
|
||||
self.swipe.update(event.delta_x(), event.delta_y());
|
||||
} else {
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_update(
|
||||
self,
|
||||
&GestureSwipeUpdateEvent {
|
||||
time: event.time_msec(),
|
||||
delta: event.delta(),
|
||||
},
|
||||
);
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_update(
|
||||
self,
|
||||
&GestureSwipeUpdateEvent {
|
||||
time: event.time_msec(),
|
||||
delta: event.delta(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
InputEvent::GestureSwipeEnd { event } => {
|
||||
@@ -140,9 +207,7 @@ impl Ferese {
|
||||
.map(|binding| binding.action.clone())
|
||||
{
|
||||
match action {
|
||||
BindingAction::Focus(
|
||||
direction @ (Direction::Left | Direction::Right),
|
||||
) => {
|
||||
BindingAction::Focus(direction @ (Direction::Left | Direction::Right)) => {
|
||||
self.focus_direction_from_swipe(direction);
|
||||
}
|
||||
BindingAction::SwitchRelativeWorkspace(next) => {
|
||||
@@ -152,16 +217,14 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seat.get_pointer()
|
||||
.expect("seat has a pointer")
|
||||
.gesture_swipe_end(
|
||||
self,
|
||||
&GestureSwipeEndEvent {
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: event.time_msec(),
|
||||
cancelled: event.cancelled(),
|
||||
},
|
||||
);
|
||||
seat.get_pointer().expect("seat has a pointer").gesture_swipe_end(
|
||||
self,
|
||||
&GestureSwipeEndEvent {
|
||||
serial: SERIAL_COUNTER.next_serial(),
|
||||
time: event.time_msec(),
|
||||
cancelled: event.cancelled(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => {
|
||||
@@ -301,6 +364,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
InputEvent::PointerMotionAbsolute { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let Some(position) = self.absolute_event_position(&event) else {
|
||||
return;
|
||||
};
|
||||
@@ -325,6 +389,7 @@ impl Ferese {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
InputEvent::PointerMotion { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
let position = pointer.current_location();
|
||||
let focus = self.surface_under(position);
|
||||
@@ -346,8 +411,7 @@ impl Ferese {
|
||||
focus,
|
||||
&RelativeMotionEvent {
|
||||
delta: (delta.x / scale_x, delta.y / scale_y).into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y)
|
||||
.into(),
|
||||
delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y).into(),
|
||||
utime: event.time(),
|
||||
},
|
||||
);
|
||||
@@ -369,6 +433,7 @@ impl Ferese {
|
||||
crate::backends::direct::render_all(self);
|
||||
}
|
||||
InputEvent::PointerButton { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
if self.input_capture.active() {
|
||||
if self.input_capture.captures(2) {
|
||||
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
|
||||
@@ -392,11 +457,8 @@ impl Ferese {
|
||||
&& crate::effects::begin_surface_dismiss(&surface)
|
||||
{
|
||||
let opacity = crate::effects::surface_opacity(&surface);
|
||||
self.dismissing_popups.push((
|
||||
root,
|
||||
popup,
|
||||
crate::dimming::DimAnimation::new(f64::from(opacity)),
|
||||
));
|
||||
self.dismissing_popups
|
||||
.push((root, popup, crate::dimming::DimAnimation::new(f64::from(opacity))));
|
||||
pointer.unset_grab(self, serial, event.time_msec());
|
||||
self.focus_window_at(pointer.current_location(), serial, true);
|
||||
crate::backends::direct::render_all(self);
|
||||
@@ -466,13 +528,14 @@ impl Ferese {
|
||||
pointer.frame(self);
|
||||
}
|
||||
InputEvent::PointerAxis { event, .. } => {
|
||||
self.last_pointer_time = event.time_msec();
|
||||
let source = event.source();
|
||||
let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
||||
});
|
||||
let horizontal = event
|
||||
.amount(Axis::Horizontal)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
let vertical = event
|
||||
.amount(Axis::Vertical)
|
||||
.unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0);
|
||||
if self.input_capture.active() {
|
||||
if self.input_capture.captures(2) {
|
||||
self.input_capture.push(serde_json::json!({"type":"scroll", "x":horizontal, "y":vertical, "v120_x":event.amount_v120(Axis::Horizontal), "v120_y":event.amount_v120(Axis::Vertical), "stop_x":source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0), "stop_y":source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0)}));
|
||||
@@ -482,11 +545,7 @@ impl Ferese {
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
if self.scroll_overview_strip(
|
||||
pointer.current_location(),
|
||||
if horizontal != 0.0 {
|
||||
horizontal
|
||||
} else {
|
||||
vertical
|
||||
},
|
||||
if horizontal != 0.0 { horizontal } else { vertical },
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -577,12 +636,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn start_floating_pointer_grab(
|
||||
&mut self,
|
||||
pointer: &PointerHandle<Self>,
|
||||
button: u32,
|
||||
serial: Serial,
|
||||
) -> bool {
|
||||
fn start_floating_pointer_grab(&mut self, pointer: &PointerHandle<Self>, button: u32, serial: Serial) -> bool {
|
||||
if self.session_lock.active
|
||||
|| pointer.is_grabbed()
|
||||
|| !matches!(button, BTN_LEFT | BTN_RIGHT)
|
||||
@@ -629,10 +683,8 @@ impl Ferese {
|
||||
Focus::Clear,
|
||||
);
|
||||
} else {
|
||||
let horizontal_right =
|
||||
location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0;
|
||||
let vertical_bottom =
|
||||
location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0;
|
||||
let horizontal_right = location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0;
|
||||
let vertical_bottom = location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0;
|
||||
let edge = match (horizontal_right, vertical_bottom) {
|
||||
(false, false) => XdgResizeEdge::TopLeft,
|
||||
(true, false) => XdgResizeEdge::TopRight,
|
||||
@@ -655,9 +707,7 @@ impl Ferese {
|
||||
I: InputBackend,
|
||||
E: AbsolutePositionEvent<I>,
|
||||
{
|
||||
let output = self
|
||||
.focused_output()
|
||||
.or_else(|| self.space.outputs().next())?;
|
||||
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
|
||||
let geometry = self.space.output_geometry(output)?;
|
||||
|
||||
Some(event.position_transformed(geometry.size) + geometry.loc.to_f64())
|
||||
@@ -666,14 +716,8 @@ impl Ferese {
|
||||
fn swipe_navigation_blocked(&self) -> bool {
|
||||
self.session_lock.active
|
||||
|| self.active_shortcuts_inhibitor.is_some()
|
||||
|| self
|
||||
.seat
|
||||
.get_pointer()
|
||||
.is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
|| self.seat.get_pointer().is_some_and(|pointer| pointer.is_grabbed())
|
||||
|| self.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed())
|
||||
}
|
||||
|
||||
fn switch_relative_workspace(&mut self, next: bool, slide: Option<SwipeDirection>) {
|
||||
@@ -689,10 +733,7 @@ impl Ferese {
|
||||
.output_for_workspace(workspace.id)
|
||||
.is_none_or(|owner| owner == output)
|
||||
});
|
||||
let Some(index) = candidates
|
||||
.iter()
|
||||
.position(|workspace| workspace.id == current)
|
||||
else {
|
||||
let Some(index) = candidates.iter().position(|workspace| workspace.id == current) else {
|
||||
return;
|
||||
};
|
||||
let index = if next {
|
||||
@@ -799,11 +840,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_window_under_pointer(
|
||||
&mut self,
|
||||
pointer: &PointerHandle<Self>,
|
||||
position: Point<f64, Logical>,
|
||||
) {
|
||||
fn focus_window_under_pointer(&mut self, pointer: &PointerHandle<Self>, position: Point<f64, Logical>) {
|
||||
if self.session_lock.active {
|
||||
return;
|
||||
}
|
||||
@@ -813,10 +850,7 @@ impl Ferese {
|
||||
}
|
||||
// Keep the explicitly selected window focused until Overview's exit
|
||||
// animation settles. Moving previews must not steal focus on pointer jitter.
|
||||
if self.overview.is_presenting()
|
||||
|| !self.input_settings.focus_follows_mouse
|
||||
|| pointer.is_grabbed()
|
||||
{
|
||||
if self.overview.is_presenting() || !self.input_settings.focus_follows_mouse || pointer.is_grabbed() {
|
||||
return;
|
||||
}
|
||||
if self.layer_under(position).is_some() {
|
||||
@@ -952,9 +986,7 @@ impl Ferese {
|
||||
BindingAction::Focus(direction) => self.focus_direction(direction),
|
||||
BindingAction::Move(direction) => self.move_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),
|
||||
BindingAction::SwitchWorkspace(workspace) => {
|
||||
self.switch_workspace(u32::from(workspace));
|
||||
}
|
||||
@@ -974,6 +1006,55 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_tree_position(
|
||||
root: &WlSurface,
|
||||
target: &WlSurface,
|
||||
geometry_origin: Point<i32, Logical>,
|
||||
) -> Option<Point<i32, Logical>> {
|
||||
use smithay::desktop::PopupManager;
|
||||
|
||||
surface_tree_offset(root, target).or_else(|| {
|
||||
PopupManager::popups_for_surface(root).find_map(|(popup, offset)| {
|
||||
surface_tree_offset(popup.wl_surface(), target)
|
||||
.map(|local| geometry_origin + offset - popup.geometry().loc + local)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn surface_tree_offset(root: &WlSurface, target: &WlSurface) -> Option<Point<i32, Logical>> {
|
||||
use std::cell::Cell;
|
||||
|
||||
use smithay::backend::renderer::utils::RendererSurfaceStateUserData;
|
||||
use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward};
|
||||
|
||||
let found = Cell::new(None);
|
||||
with_surface_tree_downward(
|
||||
root,
|
||||
Point::<i32, Logical>::default(),
|
||||
|_, states, location| {
|
||||
states
|
||||
.data_map
|
||||
.get::<RendererSurfaceStateUserData>()
|
||||
.and_then(|data| data.lock().unwrap().view())
|
||||
.map_or(TraversalAction::SkipChildren, |view| {
|
||||
TraversalAction::DoChildren(*location + view.offset)
|
||||
})
|
||||
},
|
||||
|surface, states, location| {
|
||||
if surface == target
|
||||
&& let Some(view) = states
|
||||
.data_map
|
||||
.get::<RendererSurfaceStateUserData>()
|
||||
.and_then(|data| data.lock().unwrap().view())
|
||||
{
|
||||
found.set(Some(*location + view.offset));
|
||||
}
|
||||
},
|
||||
|_, _, _| found.get().is_none(),
|
||||
);
|
||||
found.get()
|
||||
}
|
||||
|
||||
fn virtual_terminal(symbol: u32, ctrl: bool, alt: bool, direct: bool) -> Option<i32> {
|
||||
if !direct {
|
||||
return None;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
sync::mpsc::SyncSender,
|
||||
};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
|
||||
use ferese_ipc::Response;
|
||||
use serde::Deserialize;
|
||||
@@ -91,24 +89,14 @@ fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
|
||||
}
|
||||
match found {
|
||||
Edge::Top | Edge::Bottom => {
|
||||
let outside = y1
|
||||
+ if matches!(found, Edge::Top) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let outside = y1 + if matches!(found, Edge::Top) { -0.5 } else { 0.5 };
|
||||
outside >= f64::from(other.y)
|
||||
&& outside < other.bottom()
|
||||
&& x1 < other.right()
|
||||
&& x2 + 1.0 > f64::from(other.x)
|
||||
}
|
||||
Edge::Left | Edge::Right => {
|
||||
let outside = x1
|
||||
+ if matches!(found, Edge::Left) {
|
||||
-0.5
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let outside = x1 + if matches!(found, Edge::Left) { -0.5 } else { 0.5 };
|
||||
outside >= f64::from(other.x)
|
||||
&& outside < other.right()
|
||||
&& y1 < other.bottom()
|
||||
@@ -186,10 +174,7 @@ impl InputCapture {
|
||||
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
|
||||
return Err("Unsupported capabilities or input capture session limit reached".into());
|
||||
}
|
||||
self.next_session = self
|
||||
.next_session
|
||||
.checked_add(1)
|
||||
.ok_or("Input capture IDs exhausted")?;
|
||||
self.next_session = self.next_session.checked_add(1).ok_or("Input capture IDs exhausted")?;
|
||||
let id = self.next_session;
|
||||
self.sessions.insert(
|
||||
id,
|
||||
@@ -208,24 +193,15 @@ impl InputCapture {
|
||||
|
||||
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
|
||||
self.session(id)?;
|
||||
Ok(
|
||||
json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}),
|
||||
)
|
||||
Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}))
|
||||
}
|
||||
|
||||
fn session(&self, id: u64) -> Result<&Session, String> {
|
||||
self.sessions
|
||||
.get(&id)
|
||||
.ok_or("Unknown input capture session".into())
|
||||
self.sessions.get(&id).ok_or("Unknown input capture session".into())
|
||||
}
|
||||
|
||||
pub(crate) fn set_barriers(
|
||||
&mut self,
|
||||
id: u64,
|
||||
zone_set: u32,
|
||||
barriers: Vec<Barrier>,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
pub(crate) fn set_barriers(&mut self, id: u64, zone_set: u32, barriers: Vec<Barrier>) -> Result<Vec<u32>, String> {
|
||||
self.session(id)?;
|
||||
if barriers.len() > MAX_BARRIERS {
|
||||
return Err("Too many pointer barriers".into());
|
||||
@@ -250,10 +226,7 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> {
|
||||
let session = self
|
||||
.sessions
|
||||
.get_mut(&id)
|
||||
.ok_or("Unknown input capture session")?;
|
||||
let session = self.sessions.get_mut(&id).ok_or("Unknown input capture session")?;
|
||||
if session.closed {
|
||||
return Err("Input capture session is closed".into());
|
||||
}
|
||||
@@ -279,11 +252,7 @@ impl InputCapture {
|
||||
if self.active() {
|
||||
return true;
|
||||
}
|
||||
if !allowed
|
||||
|| ![from.0, from.1, to.0, to.1]
|
||||
.iter()
|
||||
.all(|value| value.is_finite())
|
||||
{
|
||||
if !allowed || ![from.0, from.1, to.0, to.1].iter().all(|value| value.is_finite()) {
|
||||
return false;
|
||||
}
|
||||
let hit = self
|
||||
@@ -368,10 +337,7 @@ impl InputCapture {
|
||||
self.restore_focus = true;
|
||||
let session = self.sessions.get_mut(&id).unwrap();
|
||||
let activation = session.activation.take();
|
||||
self.enqueue(
|
||||
id,
|
||||
json!({"type":"deactivated", "activation_id":activation}),
|
||||
);
|
||||
self.enqueue(id, json!({"type":"deactivated", "activation_id":activation}));
|
||||
self.flush();
|
||||
}
|
||||
Ok(())
|
||||
@@ -396,18 +362,10 @@ impl InputCapture {
|
||||
}
|
||||
|
||||
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool {
|
||||
point.0.is_finite()
|
||||
&& point.1.is_finite()
|
||||
&& self.zones.iter().any(|zone| zone.contains(point))
|
||||
point.0.is_finite() && point.1.is_finite() && self.zones.iter().any(|zone| zone.contains(point))
|
||||
}
|
||||
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
request: u64,
|
||||
id: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
pub(crate) fn watch(&mut self, owner: u64, request: u64, id: u64, response: SyncSender<Response>) {
|
||||
if !self.sessions.contains_key(&id)
|
||||
|| self.waiters.len() >= MAX_SESSIONS
|
||||
|| self.waiters.values().any(|(_, token, _)| *token == id)
|
||||
@@ -491,10 +449,7 @@ mod tests {
|
||||
[3840, 0, 3840, 1079],
|
||||
[0, 0, 0, 1079],
|
||||
] {
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_some(),
|
||||
"{position:?}"
|
||||
);
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_some(), "{position:?}");
|
||||
}
|
||||
for position in [
|
||||
[1920, 0, 1920, 1079],
|
||||
@@ -503,10 +458,7 @@ mod tests {
|
||||
[50, 50, 50, 200],
|
||||
[0, 5, 0, 4],
|
||||
] {
|
||||
assert!(
|
||||
edge(&Barrier { id: 1, position }, &zones).is_none(),
|
||||
"{position:?}"
|
||||
);
|
||||
assert!(edge(&Barrier { id: 1, position }, &zones).is_none(), "{position:?}");
|
||||
}
|
||||
let barrier = Barrier {
|
||||
id: 1,
|
||||
@@ -595,11 +547,7 @@ mod tests {
|
||||
capture.enable(id).unwrap();
|
||||
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
|
||||
let activation = capture.sessions[&id].activation.unwrap();
|
||||
assert!(
|
||||
capture
|
||||
.release(id, Some(activation.wrapping_add(1)))
|
||||
.is_err()
|
||||
);
|
||||
assert!(capture.release(id, Some(activation.wrapping_add(1))).is_err());
|
||||
capture.release(id, Some(activation)).unwrap();
|
||||
assert!(capture.sessions[&id].enabled);
|
||||
capture.update_zones(vec![zones()[0]]);
|
||||
@@ -667,19 +615,10 @@ impl crate::Ferese {
|
||||
&& !self.overview.is_presenting()
|
||||
&& !pointer.is_grabbed()
|
||||
&& !self.swipe.active()
|
||||
&& self
|
||||
.seat
|
||||
.get_keyboard()
|
||||
.is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self
|
||||
.seat
|
||||
.get_touch()
|
||||
.is_none_or(|touch| !touch.is_grabbed())
|
||||
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
|
||||
&& !self.seat.keyboard_shortcuts_inhibited();
|
||||
if !self
|
||||
.input_capture
|
||||
.motion((from.x, from.y), (to.x, to.y), allowed)
|
||||
{
|
||||
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
|
||||
return false;
|
||||
}
|
||||
let delta = if absolute {
|
||||
|
||||
+137
-251
@@ -1,32 +1,26 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt};
|
||||
use std::os::unix::net::{UnixListener, UnixStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
mpsc::{SyncSender, sync_channel},
|
||||
};
|
||||
use std::thread;
|
||||
use std::sync::mpsc::{SyncSender, sync_channel};
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, io, thread};
|
||||
|
||||
use ferese_core::LayoutMode;
|
||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||
use ferese_layout::Direction;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel, timer};
|
||||
use smithay::utils::Transform;
|
||||
|
||||
use crate::handlers::screencopy;
|
||||
use crate::handlers::screenshot::{
|
||||
Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan,
|
||||
};
|
||||
use crate::handlers::screenshot_worker::{self, Encoded, Job, Worker};
|
||||
use crate::{Ferese, config::OutputTransform};
|
||||
use smithay::output::Output;
|
||||
use crate::Ferese;
|
||||
use crate::config::OutputTransform;
|
||||
use crate::handlers::screenshot::{Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan};
|
||||
use crate::handlers::screenshot_worker::{Encoded, Job, Worker};
|
||||
use crate::handlers::{screencopy, screenshot_worker};
|
||||
|
||||
const REQUEST_QUEUE_CAPACITY: usize = 128;
|
||||
const MAX_CONNECTIONS: usize = 64;
|
||||
@@ -90,18 +84,14 @@ pub(crate) struct IpcSocketGuard {
|
||||
impl Drop for IpcSocketGuard {
|
||||
fn drop(&mut self) {
|
||||
if self.path.symlink_metadata().is_ok_and(|metadata| {
|
||||
metadata.file_type().is_socket()
|
||||
&& metadata.dev() == self.device
|
||||
&& metadata.ino() == self.inode
|
||||
metadata.file_type().is_socket() && metadata.dev() == self.device && metadata.ino() == self.inode
|
||||
}) {
|
||||
let _ = fs::remove_file(&self.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn init(
|
||||
event_loop: &mut EventLoop<'static, Ferese>,
|
||||
) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||
pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<ScreenshotInit, Box<dyn std::error::Error>> {
|
||||
let path = socket_path()?;
|
||||
prepare_parent(&path)?;
|
||||
let listener = bind_listener(&path)?;
|
||||
@@ -115,101 +105,88 @@ pub(crate) fn init(
|
||||
|
||||
let (sender, receiver): (channel::SyncSender<IpcEvent>, channel::Channel<IpcEvent>) =
|
||||
channel::sync_channel(REQUEST_QUEUE_CAPACITY);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
let captured = state.input_capture.active();
|
||||
state.input_capture.remove_owner(owner);
|
||||
if captured && !state.input_capture.active() {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
state.portal_shortcuts.remove(owner);
|
||||
state.portal_session.remove(owner);
|
||||
state.refresh_idle_inhibition();
|
||||
return;
|
||||
event_loop.handle().insert_source(receiver, |event, _, state| {
|
||||
if let channel::Event::Msg(event) = event {
|
||||
let call = match event {
|
||||
IpcEvent::Call(call) => call,
|
||||
IpcEvent::Closed(owner) => {
|
||||
let captured = state.input_capture.active();
|
||||
state.input_capture.remove_owner(owner);
|
||||
if captured && !state.input_capture.active() {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
};
|
||||
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
state.portal_shortcuts.remove(owner);
|
||||
state.portal_session.remove(owner);
|
||||
state.refresh_idle_inhibition();
|
||||
return;
|
||||
}
|
||||
};
|
||||
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
"Input capture is restricted to the native portal",
|
||||
));
|
||||
} else if call.request.command == "input-capture-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.try_send(Response::error(call.request.id, error.code, error.message));
|
||||
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||
state
|
||||
.input_capture
|
||||
.watch(call.owner, call.request.id, id, call.response);
|
||||
} else {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"access_denied",
|
||||
"Input capture is restricted to the native portal",
|
||||
"invalid_argument",
|
||||
"Missing capture session",
|
||||
));
|
||||
} else if call.request.command == "input-capture-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||
state
|
||||
.input_capture
|
||||
.watch(call.owner, call.request.id, id, call.response);
|
||||
} else {
|
||||
let _ = call.response.try_send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing capture session",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
error.code,
|
||||
error.message,
|
||||
));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state.portal_session.watch(
|
||||
call.owner,
|
||||
call.request.id,
|
||||
since,
|
||||
call.response,
|
||||
);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing session revision",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
state.start_window_screenshot(call.request, call.response);
|
||||
} else if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
// once, whenever the request finishes or is terminated.
|
||||
state.start_screenshot(call.request, call.response);
|
||||
} else {
|
||||
let response = state.handle_ipc_request(call.owner, call.request);
|
||||
let _ = call.response.send(response);
|
||||
}
|
||||
} else if call.request.command == "session-watch" {
|
||||
if let Err(error) = validate_request(&call.request) {
|
||||
let _ = call
|
||||
.response
|
||||
.send(Response::error(call.request.id, error.code, error.message));
|
||||
} else if let Some(since) = call.request.args["since"].as_u64() {
|
||||
state
|
||||
.portal_session
|
||||
.watch(call.owner, call.request.id, since, call.response);
|
||||
} else {
|
||||
let _ = call.response.send(Response::error(
|
||||
call.request.id,
|
||||
"invalid_argument",
|
||||
"Missing session revision",
|
||||
));
|
||||
}
|
||||
} else if call.request.command == "screenshot-window" {
|
||||
state.start_window_screenshot(call.request, call.response);
|
||||
} else if call.request.command == "screenshot" {
|
||||
// Deferred: this path answers the caller itself, exactly
|
||||
// once, whenever the request finishes or is terminated.
|
||||
state.start_screenshot(call.request, call.response);
|
||||
} else {
|
||||
let response = state.handle_ipc_request(call.owner, call.request);
|
||||
let _ = call.response.send(response);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
|
||||
// Screenshot readbacks publish parts here, so the loop wakes as soon as a
|
||||
// readback lands, whichever backend produced it.
|
||||
let (parts, part_events) = channel::channel::<PartOutcome>();
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(part_events, |event, _, state| {
|
||||
if let channel::Event::Msg(outcome) = event {
|
||||
state.on_screenshot_part(outcome);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(part_events, |event, _, state| {
|
||||
if let channel::Event::Msg(outcome) = event {
|
||||
state.on_screenshot_part(outcome);
|
||||
}
|
||||
})?;
|
||||
|
||||
let (encoded, encoded_events) = channel::sync_channel::<Encoded>(RESULT_QUEUE_CAPACITY);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(encoded_events, |event, _, state| {
|
||||
if let channel::Event::Msg(result) = event {
|
||||
state.on_screenshot_encoded(result);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(encoded_events, |event, _, state| {
|
||||
if let channel::Event::Msg(result) = event {
|
||||
state.on_screenshot_encoded(result);
|
||||
}
|
||||
})?;
|
||||
let worker = Worker::spawn(encoded);
|
||||
|
||||
let signal = event_loop.get_signal();
|
||||
@@ -231,9 +208,9 @@ pub(crate) fn init(
|
||||
// Readbacks are published by the render path, so a request whose outputs
|
||||
// are not redrawn would otherwise wait forever. This re-arms to the exact
|
||||
// remaining time whenever a request is outstanding, and idles otherwise.
|
||||
event_loop.handle().insert_source(
|
||||
timer::Timer::from_duration(DEADLINE_IDLE),
|
||||
|_, _, state| {
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(timer::Timer::from_duration(DEADLINE_IDLE), |_, _, state| {
|
||||
// Abandoned requests have already been answered, so their
|
||||
// readbacks are simply dropped: they are not failed, because
|
||||
// that would publish into a request that no longer exists.
|
||||
@@ -244,8 +221,7 @@ pub(crate) fn init(
|
||||
}
|
||||
let next = state.screenshot.next_deadline().unwrap_or(DEADLINE_IDLE);
|
||||
timer::TimeoutAction::ToDuration(next)
|
||||
},
|
||||
)?;
|
||||
})?;
|
||||
|
||||
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
|
||||
|
||||
@@ -265,11 +241,7 @@ pub(crate) struct ScreenshotInit {
|
||||
pub(crate) _guard: IpcSocketGuard,
|
||||
}
|
||||
|
||||
fn accept_connections(
|
||||
listener: UnixListener,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
) {
|
||||
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal) {
|
||||
let active_connections = Arc::new(AtomicUsize::new(0));
|
||||
let connection_ids = AtomicU64::new(1);
|
||||
|
||||
@@ -296,10 +268,7 @@ fn accept_connections(
|
||||
}
|
||||
|
||||
let Some(permit) = try_acquire_connection(&active_connections) else {
|
||||
tracing::warn!(
|
||||
limit = MAX_CONNECTIONS,
|
||||
"rejected IPC connection at worker limit"
|
||||
);
|
||||
tracing::warn!(limit = MAX_CONNECTIONS, "rejected IPC connection at worker limit");
|
||||
continue;
|
||||
};
|
||||
let sender = sender.clone();
|
||||
@@ -329,17 +298,10 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
|
||||
}
|
||||
}
|
||||
|
||||
Some(ConnectionPermit {
|
||||
active: active.clone(),
|
||||
})
|
||||
Some(ConnectionPermit { active: active.clone() })
|
||||
}
|
||||
|
||||
fn serve_connection(
|
||||
mut stream: UnixStream,
|
||||
sender: channel::SyncSender<IpcEvent>,
|
||||
signal: LoopSignal,
|
||||
owner: u64,
|
||||
) {
|
||||
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcEvent>, signal: LoopSignal, owner: u64) {
|
||||
let native_portal = crate::handlers::window_capture::is_portal(&stream);
|
||||
let _connection = IpcConnection {
|
||||
owner,
|
||||
@@ -351,9 +313,7 @@ fn serve_connection(
|
||||
Err(ferese_ipc::FrameError::Io(error))
|
||||
if matches!(
|
||||
error.kind(),
|
||||
io::ErrorKind::UnexpectedEof
|
||||
| io::ErrorKind::ConnectionReset
|
||||
| io::ErrorKind::BrokenPipe
|
||||
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe
|
||||
) =>
|
||||
{
|
||||
return;
|
||||
@@ -365,8 +325,7 @@ fn serve_connection(
|
||||
};
|
||||
let (response, receiver) = sync_channel(1);
|
||||
let exit_requested = request.command == "exit";
|
||||
let screenshot_requested =
|
||||
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||
let call = IpcEvent::Call(IpcCall {
|
||||
native_portal,
|
||||
owner,
|
||||
@@ -432,10 +391,7 @@ fn u32_arg(args: &Value, key: &str) -> Result<u32, CommandError> {
|
||||
|
||||
fn discard_undelivered_screenshot(response: &Response) {
|
||||
if response.error.is_none()
|
||||
&& let Some(path) = response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|result| result["path"].as_str())
|
||||
&& let Some(path) = response.result.as_ref().and_then(|result| result["path"].as_str())
|
||||
&& let Err(error) = fs::remove_file(path)
|
||||
&& error.kind() != io::ErrorKind::NotFound
|
||||
{
|
||||
@@ -459,11 +415,7 @@ impl Ferese {
|
||||
| "cancel-session-end"
|
||||
)
|
||||
{
|
||||
return Response::error(
|
||||
request.id,
|
||||
"session_locked",
|
||||
"IPC unavailable while session is locked",
|
||||
);
|
||||
return Response::error(request.id, "session_locked", "IPC unavailable while session is locked");
|
||||
}
|
||||
if let Err(error) = validate_request(&request) {
|
||||
return Response::error(request.id, error.code, error.message);
|
||||
@@ -475,12 +427,7 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_ipc_command(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
command: &str,
|
||||
args: &Value,
|
||||
) -> Result<Value, CommandError> {
|
||||
fn dispatch_ipc_command(&mut self, owner: u64, command: &str, args: &Value) -> Result<Value, CommandError> {
|
||||
match command {
|
||||
"portal-inhibit" => {
|
||||
let inhibition = serde_json::from_value(args.clone())
|
||||
@@ -522,9 +469,9 @@ impl Ferese {
|
||||
.validate_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
@@ -534,17 +481,17 @@ impl Ferese {
|
||||
.commit_end(
|
||||
u32_arg(args, "token")?,
|
||||
u32_arg(args, "inhibitor-revision")?,
|
||||
args["force"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing force confirmation")
|
||||
})?,
|
||||
args["force"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?,
|
||||
)
|
||||
.map_err(|error| CommandError::new("inhibited", error))?;
|
||||
return Ok(json!({}));
|
||||
}
|
||||
"logind-session-ending" => {
|
||||
let ending = args["ending"].as_bool().ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "Missing shutdown state")
|
||||
})?;
|
||||
let ending = args["ending"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "Missing shutdown state"))?;
|
||||
self.portal_session.set_phase(if ending { 3 } else { 1 });
|
||||
return Ok(json!({}));
|
||||
}
|
||||
@@ -573,8 +520,7 @@ impl Ferese {
|
||||
let xkb = state.xkb().lock().unwrap();
|
||||
// SAFETY: the ref-counted keymap does not escape the locked Xkb;
|
||||
// only its owned string is retained for the EIS keyboard.
|
||||
unsafe { xkb.keymap() }
|
||||
.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
unsafe { xkb.keymap() }.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||
});
|
||||
return Ok(json!({"session":id, "keymap":keymap}));
|
||||
}
|
||||
@@ -583,9 +529,9 @@ impl Ferese {
|
||||
return self
|
||||
.input_capture
|
||||
.zones(
|
||||
args["session"].as_u64().ok_or_else(|| {
|
||||
CommandError::new("invalid_capture", "Missing session")
|
||||
})?,
|
||||
args["session"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?,
|
||||
)
|
||||
.map_err(|e| CommandError::new("invalid_capture", e));
|
||||
}
|
||||
@@ -642,12 +588,8 @@ impl Ferese {
|
||||
let result = if command == "input-capture-disable" {
|
||||
self.input_capture.disable(id)
|
||||
} else {
|
||||
self.input_capture.release(
|
||||
id,
|
||||
args["activation_id"]
|
||||
.as_u64()
|
||||
.and_then(|id| u32::try_from(id).ok()),
|
||||
)
|
||||
self.input_capture
|
||||
.release(id, args["activation_id"].as_u64().and_then(|id| u32::try_from(id).ok()))
|
||||
};
|
||||
result.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||
if active && !self.input_capture.active() {
|
||||
@@ -696,9 +638,7 @@ impl Ferese {
|
||||
.get("pid")
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|pid| u32::try_from(pid).ok())
|
||||
.ok_or_else(|| {
|
||||
CommandError::new("invalid_argument", "pid must be an unsigned process ID")
|
||||
})?;
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "pid must be an unsigned process ID"))?;
|
||||
return Ok(json!(self.has_client_surfaces(pid)));
|
||||
}
|
||||
"get-workspaces" => return Ok(self.workspaces_json()),
|
||||
@@ -783,10 +723,7 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
};
|
||||
let specs = planned
|
||||
.iter()
|
||||
.map(|part| part.spec(&layouts[part.index]))
|
||||
.collect();
|
||||
let specs = planned.iter().map(|part| part.spec(&layouts[part.index])).collect();
|
||||
|
||||
// admit stores the reply only on success, so a clone survives the
|
||||
// rejection path and every caller is answered exactly once.
|
||||
@@ -794,8 +731,7 @@ impl Ferese {
|
||||
let id = match self.screenshot.admit(request.id, response, specs) {
|
||||
Ok(id) => id,
|
||||
Err(message) => {
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", message));
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -819,11 +755,7 @@ impl Ferese {
|
||||
|
||||
fn start_window_screenshot(&mut self, request: Request, response: SyncSender<Response>) {
|
||||
let reject = |message: String| {
|
||||
let _ = response.try_send(Response::error(
|
||||
request.id,
|
||||
"window_capture_failed",
|
||||
message,
|
||||
));
|
||||
let _ = response.try_send(Response::error(request.id, "window_capture_failed", message));
|
||||
};
|
||||
if self.session_lock.active || !screencopy::capture_allowed() {
|
||||
reject("Screen capture is unavailable".into());
|
||||
@@ -877,19 +809,13 @@ impl Ferese {
|
||||
let capture = match self.screenshot.admit(request.id, response, vec![spec]) {
|
||||
Ok(capture) => capture,
|
||||
Err(error) => {
|
||||
let _ =
|
||||
fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let result = if let Some(backend) = &self.nested_backend {
|
||||
match backend.try_borrow_mut() {
|
||||
Ok(mut backend) => crate::winit::capture_window_buffer(
|
||||
backend.renderer(),
|
||||
&window,
|
||||
geometry,
|
||||
scale,
|
||||
),
|
||||
Ok(mut backend) => crate::winit::capture_window_buffer(backend.renderer(), &window, geometry, scale),
|
||||
Err(_) => Err("Window renderer is busy".into()),
|
||||
}
|
||||
} else if let Some(backend) = &mut self.direct_backend {
|
||||
@@ -905,9 +831,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) {
|
||||
let action = self
|
||||
.screenshot
|
||||
.on_part(outcome.request, outcome.part, outcome.result);
|
||||
let action = self.screenshot.on_part(outcome.request, outcome.part, outcome.result);
|
||||
self.apply_screenshot_action(action);
|
||||
}
|
||||
|
||||
@@ -925,8 +849,7 @@ impl Ferese {
|
||||
.as_ref()
|
||||
.is_some_and(|worker| worker.submit(Job { request, frames }).is_ok());
|
||||
if !submitted {
|
||||
self.screenshot
|
||||
.reject(request, "Screenshot encoding queue is full");
|
||||
self.screenshot.reject(request, "Screenshot encoding queue is full");
|
||||
}
|
||||
}
|
||||
Action::Delivered(path) => {
|
||||
@@ -955,13 +878,12 @@ impl Ferese {
|
||||
|
||||
fn has_client_surfaces(&self, pid: u32) -> bool {
|
||||
use smithay::reexports::wayland_server::Resource;
|
||||
let belongs_to =
|
||||
|surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
surface
|
||||
.client()
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||
};
|
||||
let belongs_to = |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| {
|
||||
surface
|
||||
.client()
|
||||
.and_then(|client| client.get_credentials(&self.display_handle).ok())
|
||||
.is_some_and(|credentials| credentials.pid as u32 == pid)
|
||||
};
|
||||
self.window_ids
|
||||
.keys()
|
||||
.filter_map(|window| window.toplevel())
|
||||
@@ -974,19 +896,15 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn windows_json(&self) -> Value {
|
||||
use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
let mut windows = self
|
||||
.window_ids
|
||||
.iter()
|
||||
.filter_map(|(window, id)| {
|
||||
let toplevel = window.toplevel()?;
|
||||
let (app_id, title) = 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();
|
||||
(
|
||||
attributes.app_id.clone().unwrap_or_default(),
|
||||
attributes.title.clone().unwrap_or_default(),
|
||||
@@ -1044,10 +962,7 @@ impl Ferese {
|
||||
.connected_outputs
|
||||
.iter()
|
||||
.map(|info| {
|
||||
let mapped = self
|
||||
.space
|
||||
.outputs()
|
||||
.find(|output| output.name() == info.connector);
|
||||
let mapped = self.space.outputs().find(|output| output.name() == info.connector);
|
||||
let id = mapped.and_then(|output| self.output_id(output));
|
||||
let geometry = mapped.and_then(|output| self.space.output_geometry(output));
|
||||
let workspace = id.and_then(|id| self.output_workspaces.active_workspace(id));
|
||||
@@ -1105,9 +1020,7 @@ impl Ferese {
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
outputs.sort_by(|left, right| {
|
||||
left["connector"].as_str().cmp(&right["connector"].as_str())
|
||||
});
|
||||
outputs.sort_by(|left, right| left["connector"].as_str().cmp(&right["connector"].as_str()));
|
||||
return Value::Array(outputs);
|
||||
}
|
||||
|
||||
@@ -1163,9 +1076,7 @@ fn geometry_arg(args: &Value) -> Result<Geometry, String> {
|
||||
match args.get("geometry") {
|
||||
None | Some(Value::Null) => Ok(Geometry::All),
|
||||
Some(value) => {
|
||||
let text = value
|
||||
.as_str()
|
||||
.ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
let text = value.as_str().ok_or("geometry must be a string like \"x,y WxH\"")?;
|
||||
parse_geometry(text)
|
||||
}
|
||||
}
|
||||
@@ -1179,10 +1090,7 @@ fn validate_request(request: &Request) -> Result<(), CommandError> {
|
||||
));
|
||||
}
|
||||
if request.kind != "command" {
|
||||
return Err(CommandError::new(
|
||||
"invalid_type",
|
||||
"expected type \"command\"",
|
||||
));
|
||||
return Err(CommandError::new("invalid_type", "expected type \"command\""));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1247,12 +1155,7 @@ fn workspace_arg(args: &Value) -> Result<u32, CommandError> {
|
||||
.and_then(Value::as_u64)
|
||||
.and_then(|index| u32::try_from(index).ok())
|
||||
.filter(|index| *index > 0)
|
||||
.ok_or_else(|| {
|
||||
CommandError::new(
|
||||
"invalid_argument",
|
||||
"index must be a positive 32-bit integer",
|
||||
)
|
||||
})
|
||||
.ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer"))
|
||||
}
|
||||
|
||||
fn socket_path() -> Result<PathBuf, io::Error> {
|
||||
@@ -1264,9 +1167,7 @@ fn socket_path() -> Result<PathBuf, io::Error> {
|
||||
}
|
||||
|
||||
fn prepare_parent(socket: &Path) -> Result<(), io::Error> {
|
||||
let parent = socket
|
||||
.parent()
|
||||
.expect("the Ferese control socket always has a parent");
|
||||
let parent = socket.parent().expect("the Ferese control socket always has a parent");
|
||||
match fs::symlink_metadata(parent) {
|
||||
Ok(metadata) => {
|
||||
if !metadata.is_dir() || metadata.file_type().is_symlink() {
|
||||
@@ -1311,11 +1212,7 @@ fn bind_listener(path: &Path) -> Result<UnixListener, io::Error> {
|
||||
}
|
||||
|
||||
fn peer_uid(stream: &UnixStream) -> Result<u32, io::Error> {
|
||||
let mut credentials = libc::ucred {
|
||||
pid: 0,
|
||||
uid: 0,
|
||||
gid: 0,
|
||||
};
|
||||
let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 };
|
||||
let mut length = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
|
||||
|
||||
// SAFETY: `credentials` and `length` point to initialized, correctly sized
|
||||
@@ -1361,10 +1258,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_direction_arguments() {
|
||||
assert_eq!(
|
||||
direction_arg(&json!({ "direction": "left" })).unwrap(),
|
||||
Direction::Left
|
||||
);
|
||||
assert_eq!(direction_arg(&json!({ "direction": "left" })).unwrap(), Direction::Left);
|
||||
assert!(direction_arg(&json!({ "direction": "diagonal" })).is_err());
|
||||
}
|
||||
|
||||
@@ -1402,9 +1296,7 @@ mod tests {
|
||||
let worker = thread::spawn(move || serve_connection(server, sender, signal, 1));
|
||||
let invalid = b"not-json";
|
||||
|
||||
client
|
||||
.write_all(&(invalid.len() as u32).to_be_bytes())
|
||||
.unwrap();
|
||||
client.write_all(&(invalid.len() as u32).to_be_bytes()).unwrap();
|
||||
client.write_all(invalid).unwrap();
|
||||
client.shutdown(Shutdown::Write).unwrap();
|
||||
client
|
||||
@@ -1451,10 +1343,7 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
let first = bind_listener(&socket).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
drop(first);
|
||||
let replacement = bind_listener(&socket).unwrap();
|
||||
|
||||
@@ -1470,10 +1359,7 @@ mod tests {
|
||||
let socket = root.join("control.sock");
|
||||
fs::write(&socket, b"keep").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
bind_listener(&socket).unwrap_err().kind(),
|
||||
io::ErrorKind::AddrInUse
|
||||
);
|
||||
assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse);
|
||||
assert_eq!(fs::read(&socket).unwrap(), b"keep");
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
+20
-28
@@ -28,16 +28,14 @@ mod wallpaper;
|
||||
mod window_rules;
|
||||
mod winit;
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io,
|
||||
process::{Child, Command},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::process::{Child, Command};
|
||||
use std::time::Duration;
|
||||
use std::{io, thread};
|
||||
|
||||
use calloop::signals::{Signal, Signals};
|
||||
use smithay::reexports::{calloop::EventLoop, wayland_server::Display};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::wayland_server::Display;
|
||||
pub use state::{Ferese, RuntimeConfig};
|
||||
use tracing::{info, warn};
|
||||
|
||||
@@ -60,8 +58,8 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let launch = LaunchConfig::from_environment()
|
||||
.map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let launch =
|
||||
LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
|
||||
let initial_source = config::config_path().and_then(|path| std::fs::read_to_string(path).ok());
|
||||
let config = match initial_source.as_deref() {
|
||||
Some(source) => Config::parse_source(source)?,
|
||||
@@ -81,15 +79,15 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
|
||||
overview::init_font_loader(&mut event_loop, &mut state)?;
|
||||
backends::init(launch.backend, &mut event_loop, &mut state)?;
|
||||
let mut monitor = config::config_path().and_then(
|
||||
|path| match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) {
|
||||
let mut monitor = config::config_path().and_then(|path| {
|
||||
match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) {
|
||||
Ok(monitor) => Some(monitor),
|
||||
Err(error) => {
|
||||
warn!(%error, "automatic config watching unavailable; use feresectl reload-config");
|
||||
None
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients");
|
||||
let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities);
|
||||
@@ -141,9 +139,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
if state.input_capture.restore_focus {
|
||||
state.restore_input_capture_focus();
|
||||
}
|
||||
state.apply_unlocked_pointer_hint();
|
||||
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry
|
||||
{
|
||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry {
|
||||
backends::direct::render_all(state);
|
||||
}
|
||||
if let Err(error) = state
|
||||
@@ -242,12 +240,11 @@ fn terminate_child(child: &mut Child) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod startup_tests {
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::*;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
os::unix::net::UnixStream,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn deferred_cursor_redraw_runs_after_input_lock_is_released() {
|
||||
@@ -295,15 +292,10 @@ mod startup_tests {
|
||||
}
|
||||
display.dispatch_clients(&mut ()).unwrap();
|
||||
display.handle().flush_clients().unwrap();
|
||||
client
|
||||
.set_read_timeout(Some(Duration::from_secs(1)))
|
||||
.unwrap();
|
||||
client.set_read_timeout(Some(Duration::from_secs(1))).unwrap();
|
||||
let mut reply = [0u8; 12];
|
||||
client.read_exact(&mut reply).unwrap();
|
||||
assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2);
|
||||
assert_eq!(
|
||||
u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff,
|
||||
0
|
||||
);
|
||||
assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::utils::{Physical, Rectangle};
|
||||
|
||||
@@ -94,14 +92,8 @@ impl RenderMetrics {
|
||||
.unwrap_or(0);
|
||||
self.frame_times.make_contiguous().sort_unstable();
|
||||
let percentile = |percent: usize| {
|
||||
let index = (self.frame_times.len() * percent)
|
||||
.div_ceil(100)
|
||||
.saturating_sub(1);
|
||||
self.frame_times
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or_default()
|
||||
.as_micros()
|
||||
let index = (self.frame_times.len() * percent).div_ceil(100).saturating_sub(1);
|
||||
self.frame_times.get(index).copied().unwrap_or_default().as_micros()
|
||||
};
|
||||
tracing::info!(
|
||||
target: "ferese::render",
|
||||
@@ -131,8 +123,7 @@ impl RenderMetrics {
|
||||
}
|
||||
|
||||
fn rectangle_area(rectangle: &Rectangle<i32, Physical>) -> u64 {
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0)
|
||||
* u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
u64::try_from(rectangle.size.w.max(0)).unwrap_or(0) * u64::try_from(rectangle.size.h.max(0)).unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -211,14 +202,8 @@ mod tests {
|
||||
for micros in 0..600 {
|
||||
metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics);
|
||||
}
|
||||
assert_eq!(
|
||||
metrics.frame_times.front(),
|
||||
Some(&Duration::from_micros(88))
|
||||
);
|
||||
assert_eq!(
|
||||
metrics.frame_times.back(),
|
||||
Some(&Duration::from_micros(599))
|
||||
);
|
||||
assert_eq!(metrics.frame_times.front(), Some(&Duration::from_micros(88)));
|
||||
assert_eq!(metrics.frame_times.back(), Some(&Duration::from_micros(599)));
|
||||
assert!(
|
||||
metrics
|
||||
.frame_times
|
||||
|
||||
+53
-158
@@ -1,23 +1,17 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, Mutex, mpsc},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use ab_glyph::{Font, FontArc, FontVec, ScaleFont};
|
||||
use ferese_animation::{AnimatedRect, AnimatedValue, SpringConfig};
|
||||
use ferese_core::{OutputId, WorkspaceId};
|
||||
use ferese_layout::{Direction, Rect, WindowId};
|
||||
use smithay::{
|
||||
output::Output,
|
||||
utils::{Logical, Point},
|
||||
};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Logical, Point, Transform};
|
||||
|
||||
use crate::Ferese;
|
||||
use ab_glyph::{Font, FontArc, FontVec, ScaleFont};
|
||||
use smithay::{
|
||||
backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer},
|
||||
utils::Transform,
|
||||
};
|
||||
|
||||
const OVERVIEW_MARGIN: f64 = 48.0;
|
||||
const OVERVIEW_GAP: f64 = 40.0;
|
||||
@@ -46,16 +40,14 @@ pub(crate) fn init_font_loader(
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
use smithay::reexports::calloop::channel;
|
||||
let (results, source) = channel::sync_channel::<(String, Option<FontArc>)>(1);
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(source, |event, _, state| {
|
||||
if let channel::Event::Msg((family, font)) = event
|
||||
&& state.overview.complete_font_load(&family, font)
|
||||
&& state.overview.is_presenting()
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
}
|
||||
})?;
|
||||
event_loop.handle().insert_source(source, |event, _, state| {
|
||||
if let channel::Event::Msg((family, font)) = event
|
||||
&& state.overview.complete_font_load(&family, font)
|
||||
&& state.overview.is_presenting()
|
||||
{
|
||||
crate::backends::direct::render_all(state);
|
||||
}
|
||||
})?;
|
||||
let latest = Arc::new(Mutex::new(state.overview.font_family.clone()));
|
||||
let worker_latest = latest.clone();
|
||||
let (wake, requests) = mpsc::sync_channel(1);
|
||||
@@ -92,13 +84,8 @@ pub(crate) fn init_font_loader(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type OverviewStateLabel = HashMap<
|
||||
(String, u64, [u32; 4]),
|
||||
(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
),
|
||||
>;
|
||||
type OverviewStateLabel =
|
||||
HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OverviewState {
|
||||
@@ -154,19 +141,11 @@ pub(crate) fn workspace_strip(bounds: Rect) -> Rect {
|
||||
fn window_area(bounds: Rect) -> Rect {
|
||||
let strip = workspace_strip(bounds);
|
||||
let top = strip.y + strip.height + 16.0;
|
||||
Rect::new(
|
||||
bounds.x,
|
||||
top,
|
||||
bounds.width,
|
||||
(bounds.y + bounds.height - top).max(1.0),
|
||||
)
|
||||
Rect::new(bounds.x, top, bounds.width, (bounds.y + bounds.height - top).max(1.0))
|
||||
}
|
||||
|
||||
fn contains(rect: Rect, point: Point<f64, Logical>) -> bool {
|
||||
point.x >= rect.x
|
||||
&& point.x < rect.x + rect.width
|
||||
&& point.y >= rect.y
|
||||
&& point.y < rect.y + rect.height
|
||||
point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height
|
||||
}
|
||||
|
||||
fn strip_capacity(strip: Rect) -> usize {
|
||||
@@ -212,10 +191,7 @@ impl OverviewState {
|
||||
text: &str,
|
||||
scale: f64,
|
||||
color: [f32; 4],
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits));
|
||||
if let Some(buffer) = self.labels.get(&key) {
|
||||
return Some(buffer.clone());
|
||||
@@ -234,10 +210,7 @@ impl OverviewState {
|
||||
let mut pen = 2.0;
|
||||
for ch in text.chars() {
|
||||
let id = font.glyph_id(ch);
|
||||
let glyph = id.with_scale_and_position(
|
||||
scaled.scale(),
|
||||
ab_glyph::point(pen, scaled.ascent() + 1.0),
|
||||
);
|
||||
let glyph = id.with_scale_and_position(scaled.scale(), ab_glyph::point(pen, scaled.ascent() + 1.0));
|
||||
pen += scaled.h_advance(id);
|
||||
if let Some(outline) = font.outline_glyph(glyph) {
|
||||
let bounds = outline.px_bounds();
|
||||
@@ -257,14 +230,8 @@ impl OverviewState {
|
||||
});
|
||||
}
|
||||
}
|
||||
let buffer = MemoryRenderBuffer::from_slice(
|
||||
&pixels,
|
||||
Fourcc::Abgr8888,
|
||||
(width, height),
|
||||
1,
|
||||
Transform::Normal,
|
||||
None,
|
||||
);
|
||||
let buffer =
|
||||
MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None);
|
||||
if self.labels.len() >= 128 {
|
||||
self.labels.clear();
|
||||
}
|
||||
@@ -291,13 +258,7 @@ impl OverviewState {
|
||||
.selected
|
||||
.filter(visible)
|
||||
.or_else(|| preferred.filter(visible))
|
||||
.or_else(|| {
|
||||
windows
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(visible)
|
||||
.min_by_key(|id| id.0)
|
||||
});
|
||||
.or_else(|| windows.iter().copied().filter(visible).min_by_key(|id| id.0));
|
||||
}
|
||||
|
||||
pub(crate) fn selected(&self) -> Option<WindowId> {
|
||||
@@ -319,13 +280,9 @@ impl OverviewState {
|
||||
.filter(|(id, _)| Some(**id) != self.selected)
|
||||
.filter_map(|(id, presentation)| {
|
||||
let center = rect_center(presentation.current);
|
||||
directional_distance(direction, current_center, center)
|
||||
.map(|distance| (*id, distance))
|
||||
})
|
||||
.min_by(|(left_id, left), (right_id, right)| {
|
||||
left.total_cmp(right)
|
||||
.then_with(|| left_id.0.cmp(&right_id.0))
|
||||
directional_distance(direction, current_center, center).map(|distance| (*id, distance))
|
||||
})
|
||||
.min_by(|(left_id, left), (right_id, right)| left.total_cmp(right).then_with(|| left_id.0.cmp(&right_id.0)))
|
||||
.map(|(id, _)| id);
|
||||
let Some(next) = next else {
|
||||
return false;
|
||||
@@ -396,10 +353,7 @@ impl OverviewState {
|
||||
presentation.snap();
|
||||
}
|
||||
}
|
||||
if self
|
||||
.selected
|
||||
.is_none_or(|id| !self.presentations.contains_key(&id))
|
||||
{
|
||||
if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) {
|
||||
self.selected = self.presentations.keys().copied().min_by_key(|id| id.0);
|
||||
}
|
||||
}
|
||||
@@ -429,12 +383,7 @@ impl OverviewState {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
let elapsed = if animations_enabled {
|
||||
elapsed + delta.as_secs_f64()
|
||||
@@ -506,18 +455,12 @@ impl Ferese {
|
||||
window: &smithay::desktop::Window,
|
||||
scale: f64,
|
||||
width: f64,
|
||||
) -> Option<(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
)> {
|
||||
use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData};
|
||||
) -> Option<(MemoryRenderBuffer, smithay::utils::Size<i32, smithay::utils::Buffer>)> {
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
let surface = window.toplevel()?.wl_surface();
|
||||
let title = with_states(surface, |states| {
|
||||
let data = states
|
||||
.data_map
|
||||
.get::<XdgToplevelSurfaceData>()?
|
||||
.lock()
|
||||
.ok()?;
|
||||
let data = states.data_map.get::<XdgToplevelSurfaceData>()?.lock().ok()?;
|
||||
data.title
|
||||
.clone()
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
@@ -539,10 +482,7 @@ impl Ferese {
|
||||
&mut self,
|
||||
workspace: WorkspaceId,
|
||||
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
|
||||
.label(&name, scale, self.theme_settings.text_primary_color.0)
|
||||
@@ -621,8 +561,7 @@ impl Ferese {
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
self.overview
|
||||
.select_workspace_windows(&windows, self.focused_window);
|
||||
self.overview.select_workspace_windows(&windows, self.focused_window);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -637,12 +576,7 @@ impl Ferese {
|
||||
Some(presented)
|
||||
}
|
||||
|
||||
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)?;
|
||||
if !self.overview.is_presenting() {
|
||||
let presented = self.presented_window_rect(id)?;
|
||||
@@ -651,11 +585,7 @@ impl Ferese {
|
||||
let source = geometry.client.committed_size?;
|
||||
let presented = self.overview.presented_rect(id, geometry.visual.current);
|
||||
|
||||
if source.width <= 0
|
||||
|| source.height <= 0
|
||||
|| presented.width <= 0.0
|
||||
|| presented.height <= 0.0
|
||||
{
|
||||
if source.width <= 0 || source.height <= 0 || presented.width <= 0.0 || presented.height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -692,10 +622,9 @@ impl Ferese {
|
||||
.copied()
|
||||
.unwrap_or_else(|| selected_index.saturating_sub(capacity - 1))
|
||||
.min(max_offset);
|
||||
let card_width =
|
||||
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.clamp(1.0, 160.0);
|
||||
let card_width = ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.clamp(1.0, 160.0);
|
||||
let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
|
||||
let start_x = centered_row_start(strip, visible_count, card_width);
|
||||
workspaces
|
||||
@@ -726,9 +655,7 @@ impl Ferese {
|
||||
(rect.width - 12.0).max(1.0),
|
||||
(rect.height - 26.0).max(1.0),
|
||||
);
|
||||
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 {
|
||||
workspace: workspace.id,
|
||||
rect,
|
||||
@@ -796,11 +723,7 @@ impl Ferese {
|
||||
let ordered = self.workspaces.ordered();
|
||||
let current = cards
|
||||
.first()
|
||||
.and_then(|card| {
|
||||
ordered
|
||||
.iter()
|
||||
.position(|workspace| workspace.id == card.workspace)
|
||||
})
|
||||
.and_then(|card| ordered.iter().position(|workspace| workspace.id == card.workspace))
|
||||
.unwrap_or(0);
|
||||
let next = if delta > 0.0 {
|
||||
current.saturating_add(1)
|
||||
@@ -863,12 +786,7 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap<Window
|
||||
preview_layout(bounds, windows, OVERVIEW_MARGIN, OVERVIEW_GAP)
|
||||
}
|
||||
|
||||
fn preview_layout(
|
||||
bounds: Rect,
|
||||
windows: &[(WindowId, Rect)],
|
||||
margin: f64,
|
||||
gap: f64,
|
||||
) -> HashMap<WindowId, Rect> {
|
||||
fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: f64) -> HashMap<WindowId, Rect> {
|
||||
if windows.is_empty() {
|
||||
return HashMap::new();
|
||||
}
|
||||
@@ -881,10 +799,8 @@ fn preview_layout(
|
||||
let cell_size = |columns: usize| {
|
||||
let rows = windows.len().div_ceil(columns);
|
||||
(
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64)
|
||||
.clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64)
|
||||
.clamp(1.0, 420.0),
|
||||
((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).clamp(1.0, 480.0),
|
||||
((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).clamp(1.0, 420.0),
|
||||
)
|
||||
};
|
||||
let score = |columns: usize| {
|
||||
@@ -926,10 +842,8 @@ fn preview_layout(
|
||||
let width = (source.width * scale).max(1.0);
|
||||
let height = (source.height * scale).max(1.0);
|
||||
let row_count = (windows.len() - row * columns).min(columns);
|
||||
let row_width =
|
||||
cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x =
|
||||
bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let row_width = cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64;
|
||||
let cell_x = bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap);
|
||||
let cell_y = origin_y + row as f64 * (cell_height + gap);
|
||||
let target = Rect::new(
|
||||
cell_x + (cell_width - width) / 2.0,
|
||||
@@ -947,11 +861,7 @@ fn rect_center(rect: Rect) -> (f64, f64) {
|
||||
(rect.x + rect.width / 2.0, rect.y + rect.height / 2.0)
|
||||
}
|
||||
|
||||
fn directional_distance(
|
||||
direction: Direction,
|
||||
current: (f64, f64),
|
||||
candidate: (f64, f64),
|
||||
) -> Option<f64> {
|
||||
fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f64, f64)) -> Option<f64> {
|
||||
let delta_x = candidate.0 - current.0;
|
||||
let delta_y = candidate.1 - current.1;
|
||||
let (primary, secondary) = match direction {
|
||||
@@ -1001,10 +911,7 @@ mod tests {
|
||||
requests.request(family);
|
||||
}
|
||||
receiver.try_recv().unwrap();
|
||||
assert!(matches!(
|
||||
receiver.try_recv(),
|
||||
Err(mpsc::TryRecvError::Empty)
|
||||
));
|
||||
assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty)));
|
||||
assert_eq!(*requests.latest.lock().unwrap(), "third");
|
||||
drop(requests);
|
||||
assert!(matches!(receiver.recv(), Err(mpsc::RecvError)));
|
||||
@@ -1024,9 +931,7 @@ mod tests {
|
||||
let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0);
|
||||
for count in [1, 3, 8] {
|
||||
let start = centered_row_start(strip, count, 160.0);
|
||||
let end = start
|
||||
+ count as f64 * 160.0
|
||||
+ count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
let end = start + count as f64 * 160.0 + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP;
|
||||
assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001);
|
||||
}
|
||||
}
|
||||
@@ -1043,11 +948,7 @@ mod tests {
|
||||
assert!(strip.height < bounds.height / 2.0);
|
||||
assert!(area.y > strip.y + strip.height);
|
||||
let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))];
|
||||
assert!(
|
||||
overview_layout(area, &windows)
|
||||
.values()
|
||||
.all(|rect| rect.y >= area.y)
|
||||
);
|
||||
assert!(overview_layout(area, &windows).values().all(|rect| rect.y >= area.y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,10 +1047,7 @@ mod tests {
|
||||
#[test]
|
||||
fn overview_grid_preserves_preview_aspect_ratio() {
|
||||
let source = Rect::new(0.0, 0.0, 1200.0, 800.0);
|
||||
let layout = overview_layout(
|
||||
Rect::new(0.0, 0.0, 1920.0, 1080.0),
|
||||
&[(WindowId(1), source)],
|
||||
);
|
||||
let layout = overview_layout(Rect::new(0.0, 0.0, 1920.0, 1080.0), &[(WindowId(1), source)]);
|
||||
let preview = layout[&WindowId(1)];
|
||||
|
||||
assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001);
|
||||
@@ -1235,10 +1133,7 @@ mod tests {
|
||||
let mut overview = OverviewState::default();
|
||||
|
||||
overview.enter(
|
||||
HashMap::from([
|
||||
(left, (left_rect, left_rect)),
|
||||
(right, (right_rect, right_rect)),
|
||||
]),
|
||||
HashMap::from([(left, (left_rect, left_rect)), (right, (right_rect, right_rect))]),
|
||||
Some(left),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
use std::sync::mpsc::SyncSender;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_ipc::Response;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
sync::mpsc::SyncSender,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -104,31 +103,21 @@ impl PortalSession {
|
||||
|
||||
pub(crate) fn remove(&mut self, owner: u64) {
|
||||
self.waiters.remove(&owner);
|
||||
if self.phase == 2
|
||||
&& self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.owner == Some(owner))
|
||||
{
|
||||
if self.phase == 2 && self.query.as_ref().is_some_and(|query| query.owner == Some(owner)) {
|
||||
self.set_phase(1);
|
||||
}
|
||||
if self.inhibitors.remove(&owner).is_some() {
|
||||
self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1);
|
||||
}
|
||||
let monitor_removed = self.monitors.remove(&owner);
|
||||
let acknowledged = self
|
||||
.query
|
||||
.as_mut()
|
||||
.is_some_and(|query| query.pending.remove(&owner));
|
||||
let acknowledged = self.query.as_mut().is_some_and(|query| query.pending.remove(&owner));
|
||||
if monitor_removed || acknowledged {
|
||||
self.changed();
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_inhibited(&self) -> bool {
|
||||
self.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
self.inhibitors.values().any(|inhibitor| inhibitor.flags & 8 != 0)
|
||||
}
|
||||
|
||||
pub(crate) fn begin_query(&mut self) -> u32 {
|
||||
@@ -152,11 +141,7 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_query(&mut self, token: u32) {
|
||||
if self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
if self.query.as_ref().is_some_and(|query| query.token == token) {
|
||||
self.set_phase(1);
|
||||
}
|
||||
}
|
||||
@@ -169,42 +154,19 @@ impl PortalSession {
|
||||
.is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_end(
|
||||
&self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
if !self.query_ready()
|
||||
|| !self
|
||||
.query
|
||||
.as_ref()
|
||||
.is_some_and(|query| query.token == token)
|
||||
{
|
||||
return Err(
|
||||
"Session-ending confirmation expired or applications are still responding".into(),
|
||||
);
|
||||
pub(crate) fn validate_end(&self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
if !self.query_ready() || !self.query.as_ref().is_some_and(|query| query.token == token) {
|
||||
return Err("Session-ending confirmation expired or applications are still responding".into());
|
||||
}
|
||||
if self.inhibitor_revision != inhibitor_revision
|
||||
|| (!force
|
||||
&& self
|
||||
.inhibitors
|
||||
.values()
|
||||
.any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
|| (!force && self.inhibitors.values().any(|inhibitor| inhibitor.flags & 1 != 0))
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn commit_end(
|
||||
&mut self,
|
||||
token: u32,
|
||||
inhibitor_revision: u32,
|
||||
force: bool,
|
||||
) -> Result<(), String> {
|
||||
pub(crate) fn commit_end(&mut self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> {
|
||||
self.validate_end(token, inhibitor_revision, force)?;
|
||||
self.set_phase(3);
|
||||
Ok(())
|
||||
@@ -247,13 +209,7 @@ impl PortalSession {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn watch(
|
||||
&mut self,
|
||||
owner: u64,
|
||||
id: u64,
|
||||
since: u64,
|
||||
response: SyncSender<Response>,
|
||||
) {
|
||||
pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: SyncSender<Response>) {
|
||||
if since != self.revision {
|
||||
let _ = response.try_send(Response::success(id, self.snapshot()));
|
||||
} else {
|
||||
@@ -262,12 +218,13 @@ impl PortalSession {
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot(&self) -> Value {
|
||||
let inhibitors = self.inhibitors.values().map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})).collect::<Vec<_>>();
|
||||
let inhibitors = self
|
||||
.inhibitors
|
||||
.values()
|
||||
.map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason}))
|
||||
.collect::<Vec<_>>();
|
||||
let remaining = self.query.as_ref().map_or(0, |query| {
|
||||
query
|
||||
.deadline
|
||||
.saturating_duration_since(Instant::now())
|
||||
.as_millis() as u64
|
||||
query.deadline.saturating_duration_since(Instant::now()).as_millis() as u64
|
||||
});
|
||||
json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision,
|
||||
"inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision,
|
||||
@@ -278,24 +235,22 @@ impl PortalSession {
|
||||
|
||||
impl crate::Ferese {
|
||||
pub(crate) fn refresh_idle_inhibition(&mut self) {
|
||||
self.idle_notifier_state.set_is_inhibited(
|
||||
!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited(),
|
||||
);
|
||||
self.idle_notifier_state
|
||||
.set_is_inhibited(!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited());
|
||||
}
|
||||
|
||||
pub(crate) fn end_portal_session(&mut self) {
|
||||
let token = self.portal_session.query.as_ref().map(|query| query.token);
|
||||
let result = self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_millis(200)),
|
||||
move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
},
|
||||
);
|
||||
let result =
|
||||
self.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_millis(200)), move |_, _, state| {
|
||||
if state.portal_session.phase == 3
|
||||
&& state.portal_session.query.as_ref().map(|query| query.token) == token
|
||||
{
|
||||
state.loop_signal.stop();
|
||||
}
|
||||
TimeoutAction::Drop
|
||||
});
|
||||
if let Err(error) = result {
|
||||
tracing::error!(%error, "failed to schedule session ending");
|
||||
self.loop_signal.stop();
|
||||
@@ -354,20 +309,10 @@ mod tests {
|
||||
let approved = session.inhibitor_revision;
|
||||
session.register(1, inhibition(1)).unwrap();
|
||||
assert!(session.commit_end(token, approved, true).is_err());
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, false)
|
||||
.is_err()
|
||||
);
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.unwrap();
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, false).is_err());
|
||||
session.commit_end(token, session.inhibitor_revision, true).unwrap();
|
||||
assert_eq!(session.snapshot()["session-state"], 3);
|
||||
assert!(
|
||||
session
|
||||
.commit_end(token, session.inhibitor_revision, true)
|
||||
.is_err()
|
||||
);
|
||||
assert!(session.commit_end(token, session.inhibitor_revision, true).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -410,10 +355,7 @@ mod tests {
|
||||
let response = receiver.try_recv().unwrap();
|
||||
assert_eq!(response.result.unwrap()["screensaver-active"], true);
|
||||
session.set_locked(false);
|
||||
assert_eq!(
|
||||
session.snapshot()["transitions"].as_array().unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(session.snapshot()["transitions"].as_array().unwrap().len(), 2);
|
||||
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||
session.watch(1, 11, session.revision, sender);
|
||||
session.remove(1);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
time::Instant,
|
||||
};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::time::Instant;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
@@ -28,9 +26,7 @@ struct Registration {
|
||||
|
||||
impl Registration {
|
||||
fn timestamp(&self) -> u64 {
|
||||
self.latest
|
||||
.0
|
||||
.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
self.latest.0.saturating_add(self.latest.1.elapsed().as_millis() as u64)
|
||||
}
|
||||
|
||||
fn release_all(&mut self) {
|
||||
@@ -57,11 +53,7 @@ impl PortalShortcuts {
|
||||
return Err("Shortcut limit reached".into());
|
||||
}
|
||||
let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?;
|
||||
if self
|
||||
.0
|
||||
.get(&owner)
|
||||
.is_some_and(|registration| registration.closed)
|
||||
{
|
||||
if self.0.get(&owner).is_some_and(|registration| registration.closed) {
|
||||
return Err("Shortcut session has ended".into());
|
||||
}
|
||||
let mut parsed: Vec<(Shortcut, Binding)> = Vec::new();
|
||||
@@ -70,17 +62,11 @@ impl PortalShortcuts {
|
||||
.trigger
|
||||
.split('+')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case("num"));
|
||||
if shortcut.id.is_empty()
|
||||
|| shortcut.id.len() > 256
|
||||
|| shortcut.id.chars().any(char::is_control)
|
||||
{
|
||||
if shortcut.id.is_empty() || shortcut.id.len() > 256 || shortcut.id.chars().any(char::is_control) {
|
||||
return Err("Invalid shortcut identifier".into());
|
||||
}
|
||||
let binding =
|
||||
Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured
|
||||
.iter()
|
||||
.any(|other| binding.conflicts(other, &keymap))
|
||||
let binding = Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?;
|
||||
if configured.iter().any(|other| binding.conflicts(other, &keymap))
|
||||
|| parsed
|
||||
.iter()
|
||||
.any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap))
|
||||
@@ -120,9 +106,9 @@ impl PortalShortcuts {
|
||||
return;
|
||||
};
|
||||
for registration in self.0.values_mut() {
|
||||
registration.shortcuts.retain(|(_, binding)| {
|
||||
!configured.iter().any(|key| binding.conflicts(key, &keymap))
|
||||
});
|
||||
registration
|
||||
.shortcuts
|
||||
.retain(|(_, binding)| !configured.iter().any(|key| binding.conflicts(key, &keymap)));
|
||||
let active_ids = registration
|
||||
.shortcuts
|
||||
.iter()
|
||||
@@ -158,13 +144,7 @@ impl PortalShortcuts {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn press(
|
||||
&mut self,
|
||||
key: Keycode,
|
||||
symbols: &[u32],
|
||||
modifiers: &ModifiersState,
|
||||
timestamp: u64,
|
||||
) -> bool {
|
||||
pub(crate) fn press(&mut self, key: Keycode, symbols: &[u32], modifiers: &ModifiersState, timestamp: u64) -> bool {
|
||||
for registration in self.0.values_mut() {
|
||||
let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| {
|
||||
(!shortcut.num_required || modifiers.num_lock)
|
||||
@@ -212,9 +192,10 @@ impl PortalShortcuts {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smithay::input::keyboard::keysyms;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn shortcut(id: &str, trigger: &str) -> Shortcut {
|
||||
Shortcut {
|
||||
id: id.into(),
|
||||
@@ -244,10 +225,7 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Super+q")], &configured, &input)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
shortcuts.poll(1, false)["shortcuts"][0]["trigger"],
|
||||
"Ctrl+Alt+a"
|
||||
);
|
||||
assert_eq!(shortcuts.poll(1, false)["shortcuts"][0]["trigger"], "Ctrl+Alt+a");
|
||||
shortcuts.remove(1);
|
||||
assert_eq!(shortcuts.poll(1, false)["closed"], true);
|
||||
shortcuts
|
||||
@@ -296,12 +274,7 @@ mod tests {
|
||||
.register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input)
|
||||
.unwrap();
|
||||
shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input);
|
||||
assert!(
|
||||
shortcuts.poll(1, false)["shortcuts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(shortcuts.poll(1, false)["shortcuts"].as_array().unwrap().is_empty());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+Escape").is_err());
|
||||
assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err());
|
||||
}
|
||||
@@ -332,10 +305,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() {
|
||||
let config = crate::config::Config::parse_source(
|
||||
"binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
let config =
|
||||
crate::config::Config::parse_source("binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n")
|
||||
.unwrap();
|
||||
let mut input = config.input_settings().unwrap();
|
||||
input.xkb_layout = "us,fr".into();
|
||||
let configured = config.bindings(&input).unwrap();
|
||||
|
||||
+110
-54
@@ -2,21 +2,63 @@
|
||||
//! logical pixels. Content, clips and decorations share those exact edges.
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::{
|
||||
backend::renderer::{
|
||||
element::{Element, Id, Kind, RenderElement},
|
||||
gles::{
|
||||
GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform,
|
||||
element::PixelShaderElement,
|
||||
},
|
||||
utils::{CommitCounter, DamageSet},
|
||||
},
|
||||
utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform},
|
||||
use smithay::backend::renderer::element::{
|
||||
Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates,
|
||||
};
|
||||
use smithay::backend::renderer::gles::element::PixelShaderElement;
|
||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
|
||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||
use smithay::desktop::utils::OutputPresentationFeedback;
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback;
|
||||
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
|
||||
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform};
|
||||
use smithay::wayland::compositor::SurfaceData;
|
||||
|
||||
pub(crate) const HANDOFF: Duration = Duration::from_millis(80);
|
||||
pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024;
|
||||
|
||||
pub(crate) fn take_output_feedback(
|
||||
state: &crate::Ferese,
|
||||
output: &Output,
|
||||
rendered: &RenderElementStates,
|
||||
flags: wp_presentation_feedback::Kind,
|
||||
) -> OutputPresentationFeedback {
|
||||
use smithay::desktop::layer_map_for_output;
|
||||
use smithay::desktop::utils::{OutputPresentationFeedback, surface_presentation_feedback_flags_from_states};
|
||||
|
||||
let mut feedback = OutputPresentationFeedback::new(output);
|
||||
let visible_output =
|
||||
|surface: &WlSurface, _: &SurfaceData| rendered_feedback_output(output, surface.into(), rendered);
|
||||
let surface_flags = |surface: &WlSurface, _: &SurfaceData| {
|
||||
flags | surface_presentation_feedback_flags_from_states(surface, rendered)
|
||||
};
|
||||
|
||||
for window in state.space.elements().filter(|window| {
|
||||
state
|
||||
.window_ids
|
||||
.get(*window)
|
||||
.is_some_and(|id| state.window_belongs_to_output(*id, output))
|
||||
}) {
|
||||
window.take_presentation_feedback(&mut feedback, visible_output, surface_flags);
|
||||
}
|
||||
|
||||
for layer in layer_map_for_output(output).layers() {
|
||||
layer.take_presentation_feedback(&mut feedback, visible_output, surface_flags);
|
||||
}
|
||||
|
||||
feedback
|
||||
}
|
||||
|
||||
fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementStates) -> Option<Output> {
|
||||
rendered
|
||||
.element_render_state(id)
|
||||
.is_some_and(|state| {
|
||||
state.visible_area > 0 && state.presentation_state != RenderElementPresentationState::Skipped
|
||||
})
|
||||
.then(|| output.clone())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CallbackClock {
|
||||
last_sent: Option<Instant>,
|
||||
@@ -24,8 +66,7 @@ pub(crate) struct CallbackClock {
|
||||
|
||||
impl CallbackClock {
|
||||
pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant {
|
||||
self.last_sent
|
||||
.map_or(now, |last| (last + interval).max(now))
|
||||
self.last_sent.map_or(now, |last| (last + interval).max(now))
|
||||
}
|
||||
|
||||
pub(crate) fn sent(&mut self, now: Instant) {
|
||||
@@ -48,6 +89,7 @@ pub(crate) fn physical_rect(
|
||||
let top = ((rect.y - f64::from(origin.y)) * scale).round() as i32;
|
||||
let right = ((rect.x + rect.width - f64::from(origin.x)) * scale).round() as i32;
|
||||
let bottom = ((rect.y + rect.height - f64::from(origin.y)) * scale).round() as i32;
|
||||
|
||||
Rectangle::new(
|
||||
(left, top).into(),
|
||||
((right - left).max(1), (bottom - top).max(1)).into(),
|
||||
@@ -122,11 +164,7 @@ impl Element for PhysicalShaderElement {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.current_commit()) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -187,11 +225,7 @@ impl Element for NativeTextureElement {
|
||||
Kind::Unspecified
|
||||
}
|
||||
|
||||
fn damage_since(
|
||||
&self,
|
||||
_: Scale<f64>,
|
||||
commit: Option<CommitCounter>,
|
||||
) -> DamageSet<i32, Physical> {
|
||||
fn damage_since(&self, _: Scale<f64>, commit: Option<CommitCounter>) -> DamageSet<i32, Physical> {
|
||||
if commit == Some(self.commit) {
|
||||
DamageSet::default()
|
||||
} else {
|
||||
@@ -227,26 +261,59 @@ impl RenderElement<GlesRenderer> for NativeTextureElement {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn presentation_feedback_excludes_missing_and_occluded_surfaces() {
|
||||
use smithay::backend::renderer::element::RenderElementState;
|
||||
use smithay::output::{PhysicalProperties, Subpixel};
|
||||
|
||||
let output = Output::new(
|
||||
"owner".into(),
|
||||
PhysicalProperties {
|
||||
size: (0, 0).into(),
|
||||
subpixel: Subpixel::Unknown,
|
||||
make: "test".into(),
|
||||
model: "test".into(),
|
||||
},
|
||||
);
|
||||
let id = Id::new();
|
||||
let mut states = RenderElementStates::default();
|
||||
assert!(rendered_feedback_output(&output, id.clone(), &states).is_none());
|
||||
for state in [
|
||||
RenderElementState {
|
||||
visible_area: 0,
|
||||
presentation_state: RenderElementPresentationState::Skipped,
|
||||
},
|
||||
RenderElementState {
|
||||
visible_area: 0,
|
||||
presentation_state: RenderElementPresentationState::Rendering { reason: None },
|
||||
},
|
||||
] {
|
||||
states.states.insert(id.clone(), state);
|
||||
assert!(rendered_feedback_output(&output, id.clone(), &states).is_none());
|
||||
}
|
||||
for presentation_state in [
|
||||
RenderElementPresentationState::Rendering { reason: None },
|
||||
RenderElementPresentationState::ZeroCopy,
|
||||
] {
|
||||
states.states.insert(
|
||||
id.clone(),
|
||||
RenderElementState {
|
||||
visible_area: 32,
|
||||
presentation_state,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
rendered_feedback_output(&output, id.clone(), &states),
|
||||
Some(output.clone())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reversing_a_grow_replaces_the_smaller_snapshot() {
|
||||
assert!(!snapshot_covers_source(
|
||||
(500, 600).into(),
|
||||
(1000, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.0
|
||||
));
|
||||
assert!(!snapshot_covers_source(
|
||||
(1000, 600).into(),
|
||||
(500, 600).into(),
|
||||
1.0,
|
||||
1.8
|
||||
));
|
||||
assert!(!snapshot_covers_source((500, 600).into(), (1000, 600).into(), 1.0, 1.0));
|
||||
assert!(snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.0));
|
||||
assert!(!snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -347,25 +414,14 @@ mod tests {
|
||||
fn fractional_motion_does_not_quantize_to_logical_pixels() {
|
||||
let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into());
|
||||
assert_eq!(
|
||||
physical_rect(rect, (0, 0).into(), 1.8).size,
|
||||
(180, 144).into()
|
||||
);
|
||||
assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).size, (180, 144).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adjacent_frames_share_edges_at_all_output_scales() {
|
||||
for scale in [1.0, 1.25, 1.5, 1.8, 2.0] {
|
||||
let left = physical_rect(
|
||||
ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let right = physical_rect(
|
||||
ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0),
|
||||
(0, 0).into(),
|
||||
scale,
|
||||
);
|
||||
let left = physical_rect(ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
let right = physical_rect(ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), (0, 0).into(), scale);
|
||||
assert_eq!(left.loc.x + left.size.w, right.loc.x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::{
|
||||
io,
|
||||
os::{
|
||||
fd::AsRawFd,
|
||||
unix::{net::UnixStream, process::CommandExt},
|
||||
},
|
||||
process::Command,
|
||||
sync::Arc,
|
||||
};
|
||||
use std::io;
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::process::Command;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{Ferese, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::state::ClientState;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct ClientCapabilities(u8);
|
||||
@@ -36,8 +34,8 @@ pub(crate) fn prepare_command(
|
||||
command: &mut Command,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<Vec<UnixStream>> {
|
||||
let split_shell_control = capabilities.contains(ClientCapabilities::EFFECTS)
|
||||
&& capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let split_shell_control =
|
||||
capabilities.contains(ClientCapabilities::EFFECTS) && capabilities.contains(ClientCapabilities::SHELL_CONTROL);
|
||||
let primary_capabilities = if split_shell_control {
|
||||
ClientCapabilities::EFFECTS
|
||||
} else {
|
||||
@@ -56,10 +54,7 @@ pub(crate) fn prepare_command(
|
||||
if split_shell_control {
|
||||
let control = insert_private_client(state, ClientCapabilities::SHELL_CONTROL)?;
|
||||
|
||||
command.env(
|
||||
"FERESE_SHELL_CONTROL_SOCKET",
|
||||
control.as_raw_fd().to_string(),
|
||||
);
|
||||
command.env("FERESE_SHELL_CONTROL_SOCKET", control.as_raw_fd().to_string());
|
||||
clients.push(control);
|
||||
}
|
||||
|
||||
@@ -81,10 +76,7 @@ pub(crate) fn prepare_command(
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
fn insert_private_client(
|
||||
state: &mut Ferese,
|
||||
capabilities: ClientCapabilities,
|
||||
) -> io::Result<UnixStream> {
|
||||
fn insert_private_client(state: &mut Ferese, capabilities: ClientCapabilities) -> io::Result<UnixStream> {
|
||||
let (server, client) = UnixStream::pair()?;
|
||||
|
||||
state.display_handle.insert_client(
|
||||
|
||||
+18
-34
@@ -1,11 +1,10 @@
|
||||
//! Filesystem events wake the loop; only pending edits need a debounce timer.
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use std::{
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
fn read_source(path: &std::path::Path) -> Result<String, String> {
|
||||
let file = std::fs::File::open(path).map_err(|e| e.to_string())?;
|
||||
@@ -40,21 +39,17 @@ impl ConfigMonitor {
|
||||
let (sender, events) = mpsc::sync_channel(1);
|
||||
let target = path.clone();
|
||||
let event_wakeup = wakeup.clone();
|
||||
let mut watcher =
|
||||
notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = event
|
||||
&& !event.kind.is_access()
|
||||
&& event
|
||||
.paths
|
||||
.iter()
|
||||
.any(|p| p == &target || target.starts_with(p))
|
||||
{
|
||||
let _ = sender.try_send(());
|
||||
if let Some(wakeup) = &event_wakeup {
|
||||
wakeup.wakeup();
|
||||
}
|
||||
let mut watcher = notify::recommended_watcher(move |event: notify::Result<notify::Event>| {
|
||||
if let Ok(event) = event
|
||||
&& !event.kind.is_access()
|
||||
&& event.paths.iter().any(|p| p == &target || target.starts_with(p))
|
||||
{
|
||||
let _ = sender.try_send(());
|
||||
if let Some(wakeup) = &event_wakeup {
|
||||
wakeup.wakeup();
|
||||
}
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
// Watch the directory, not the inode: atomic editor saves replace it.
|
||||
let mut directory = path.parent().unwrap_or(std::path::Path::new("."));
|
||||
while !directory.is_dir() {
|
||||
@@ -224,10 +219,7 @@ mod tests {
|
||||
assert_eq!(result.unwrap(), "animations {\n speed 0.75\n}\n");
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"atomic save notification never settled"
|
||||
);
|
||||
assert!(Instant::now() < deadline, "atomic save notification never settled");
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
}
|
||||
@@ -242,17 +234,9 @@ mod tests {
|
||||
let mut monitor = ConfigMonitor::new(path).unwrap();
|
||||
let now = Instant::now();
|
||||
monitor.dirty = Some(now);
|
||||
assert_eq!(
|
||||
monitor.next_deadline(),
|
||||
Some(now + Duration::from_millis(120))
|
||||
);
|
||||
assert_eq!(monitor.next_deadline(), Some(now + Duration::from_millis(120)));
|
||||
assert!(monitor.poll(now + Duration::from_millis(119)).is_none());
|
||||
assert!(
|
||||
monitor
|
||||
.poll(now + Duration::from_millis(121))
|
||||
.unwrap()
|
||||
.is_ok()
|
||||
);
|
||||
assert!(monitor.poll(now + Duration::from_millis(121)).unwrap().is_ok());
|
||||
assert_eq!(monitor.next_deadline(), None);
|
||||
}
|
||||
}
|
||||
@@ -72,8 +72,7 @@ mod tests {
|
||||
#[test]
|
||||
fn shrinking_commit_retains_the_previous_full_frame_geometry() {
|
||||
let source = Rectangle::new((4, 8).into(), (1200, 800).into());
|
||||
let transaction =
|
||||
ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
|
||||
let transaction = ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source);
|
||||
assert_eq!(transaction.source_geometry(), Some(source));
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,21 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use smithay::output::Output;
|
||||
use smithay::reexports::calloop::RegistrationToken;
|
||||
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_surface_v1::ExtSessionLockSurfaceV1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1;
|
||||
use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1;
|
||||
use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource};
|
||||
use smithay::utils::{Logical, Point, SERIAL_COUNTER};
|
||||
use smithay::wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker,
|
||||
};
|
||||
|
||||
use crate::Ferese;
|
||||
use smithay::reexports::calloop::{
|
||||
RegistrationToken,
|
||||
timer::{TimeoutAction, Timer},
|
||||
};
|
||||
use smithay::{
|
||||
output::Output,
|
||||
reexports::{
|
||||
wayland_protocols::ext::session_lock::v1::server::{
|
||||
ext_session_lock_manager_v1::ExtSessionLockManagerV1,
|
||||
ext_session_lock_surface_v1::ExtSessionLockSurfaceV1,
|
||||
ext_session_lock_v1::{self, ExtSessionLockV1},
|
||||
},
|
||||
wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, Resource, protocol::wl_output::WlOutput,
|
||||
},
|
||||
},
|
||||
utils::{Logical, Point, SERIAL_COUNTER},
|
||||
wayland::session_lock::{
|
||||
LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker,
|
||||
},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, serde::Deserialize)]
|
||||
#[serde(default)]
|
||||
@@ -54,10 +46,7 @@ impl IdleSettings {
|
||||
|
||||
fn next_poll(self, elapsed: Duration, animate: bool) -> Duration {
|
||||
let dim_at = Duration::from_secs(self.dim_after_seconds);
|
||||
if animate
|
||||
&& self.dim_after_seconds != 0
|
||||
&& elapsed >= dim_at
|
||||
&& elapsed < dim_at + Duration::from_millis(500)
|
||||
if animate && self.dim_after_seconds != 0 && elapsed >= dim_at && elapsed < dim_at + Duration::from_millis(500)
|
||||
{
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
@@ -72,8 +61,7 @@ impl IdleSettings {
|
||||
}
|
||||
|
||||
fn appearance(self, elapsed: Duration) -> (f32, bool) {
|
||||
let sleeping = self.sleep_after_seconds != 0
|
||||
&& elapsed >= Duration::from_secs(self.sleep_after_seconds);
|
||||
let sleeping = self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds);
|
||||
let dim = if self.dim_after_seconds == 0 {
|
||||
0.0
|
||||
} else {
|
||||
@@ -99,17 +87,13 @@ pub(crate) struct Lock {
|
||||
idle_since: Option<Instant>,
|
||||
pub(crate) idle_opacity: f32,
|
||||
pub(crate) sleeping: bool,
|
||||
pub(crate) idle_overlays:
|
||||
HashMap<Output, smithay::backend::renderer::element::solid::SolidColorBuffer>,
|
||||
pub(crate) idle_overlays: HashMap<Output, smithay::backend::renderer::element::solid::SolidColorBuffer>,
|
||||
idle_timer: Option<RegistrationToken>,
|
||||
}
|
||||
|
||||
impl Lock {
|
||||
fn ready_for_idle<'a>(&self, outputs: impl Iterator<Item = &'a Output>) -> bool {
|
||||
self.active
|
||||
&& outputs
|
||||
.into_iter()
|
||||
.all(|output| self.presented.contains(output))
|
||||
self.active && outputs.into_iter().all(|output| self.presented.contains(output))
|
||||
}
|
||||
|
||||
pub(crate) fn output_added(&mut self, output: &Output) {
|
||||
@@ -146,14 +130,11 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
let since = self.session_lock.idle_since.get_or_insert(now);
|
||||
let (mut opacity, sleeping) = self
|
||||
.lock_idle
|
||||
.appearance(now.saturating_duration_since(*since));
|
||||
let (mut opacity, sleeping) = self.lock_idle.appearance(now.saturating_duration_since(*since));
|
||||
if !self.animations_enabled() && opacity > 0.0 && !sleeping {
|
||||
opacity = 0.65;
|
||||
}
|
||||
let changed =
|
||||
opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping;
|
||||
let changed = opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping;
|
||||
let was_sleeping = self.session_lock.sleeping;
|
||||
self.session_lock.idle_opacity = opacity;
|
||||
self.session_lock.sleeping = sleeping;
|
||||
@@ -190,12 +171,7 @@ impl Ferese {
|
||||
let surface = self
|
||||
.focused_output()
|
||||
.and_then(|output| self.session_lock.surfaces.get(output))
|
||||
.or_else(|| {
|
||||
self.session_lock
|
||||
.surfaces
|
||||
.values()
|
||||
.find(|surface| surface.alive())
|
||||
})
|
||||
.or_else(|| self.session_lock.surfaces.values().find(|surface| surface.alive()))
|
||||
.filter(|surface| surface.alive())
|
||||
.map(|surface| surface.wl_surface().clone());
|
||||
self.seat
|
||||
@@ -234,12 +210,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn lock_frame_callbacks(&self, output: &Output) {
|
||||
if let Some(surface) = self
|
||||
.session_lock
|
||||
.surfaces
|
||||
.get(output)
|
||||
.filter(|surface| surface.alive())
|
||||
{
|
||||
if let Some(surface) = self.session_lock.surfaces.get(output).filter(|surface| surface.alive()) {
|
||||
smithay::desktop::utils::send_frames_surface_tree(
|
||||
surface.wl_surface(),
|
||||
output,
|
||||
@@ -263,9 +234,9 @@ impl SessionLockHandler for Ferese {
|
||||
self.cancel_logout_confirmation();
|
||||
self.session_lock.active = true;
|
||||
self.session_lock.idle_since = Some(Instant::now());
|
||||
match self.loop_handle.insert_source(
|
||||
Timer::from_duration(Duration::from_secs(1)),
|
||||
|_, _, state| {
|
||||
match self
|
||||
.loop_handle
|
||||
.insert_source(Timer::from_duration(Duration::from_secs(1)), |_, _, state| {
|
||||
if !state.session_lock.active {
|
||||
return TimeoutAction::Drop;
|
||||
}
|
||||
@@ -274,13 +245,8 @@ impl SessionLockHandler for Ferese {
|
||||
.session_lock
|
||||
.idle_since
|
||||
.map_or(Duration::ZERO, |since| since.elapsed());
|
||||
TimeoutAction::ToDuration(
|
||||
state
|
||||
.lock_idle
|
||||
.next_poll(elapsed, state.animations_enabled()),
|
||||
)
|
||||
},
|
||||
) {
|
||||
TimeoutAction::ToDuration(state.lock_idle.next_poll(elapsed, state.animations_enabled()))
|
||||
}) {
|
||||
Ok(token) => self.session_lock.idle_timer = Some(token),
|
||||
Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"),
|
||||
}
|
||||
@@ -295,10 +261,7 @@ impl SessionLockHandler for Ferese {
|
||||
// already encoding has no pending capture left, so it is terminated
|
||||
// here and its staged file is discarded when the worker reports back.
|
||||
for request in self.screenshot.terminate_all() {
|
||||
tracing::debug!(
|
||||
request,
|
||||
"cancelled an in-flight screenshot for the session lock"
|
||||
);
|
||||
tracing::debug!(request, "cancelled an in-flight screenshot for the session lock");
|
||||
}
|
||||
self.set_overview_active(false);
|
||||
let serial = SERIAL_COUNTER.next_serial();
|
||||
@@ -345,9 +308,8 @@ impl SessionLockHandler for Ferese {
|
||||
if let Some(output) = Output::from_resource(&output)
|
||||
&& let Some(geometry) = self.space.output_geometry(&output)
|
||||
{
|
||||
surface.with_pending_state(|state| {
|
||||
state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into())
|
||||
});
|
||||
surface
|
||||
.with_pending_state(|state| state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into()));
|
||||
self.session_lock.surfaces.insert(output, surface);
|
||||
self.focus_lock_surface();
|
||||
}
|
||||
@@ -377,10 +339,7 @@ impl Dispatch<ExtSessionLockV1, SessionLockState> for Ferese {
|
||||
return;
|
||||
}
|
||||
if matches!(request, ext_session_lock_v1::Request::GetLockSurface { .. }) && !owner {
|
||||
lock.post_error(
|
||||
ext_session_lock_v1::Error::InvalidUnlock,
|
||||
"not the active lock owner",
|
||||
);
|
||||
lock.post_error(ext_session_lock_v1::Error::InvalidUnlock, "not the active lock owner");
|
||||
return;
|
||||
}
|
||||
<SessionLockManagerState as Dispatch<ExtSessionLockV1, SessionLockState, Ferese>>::request(
|
||||
@@ -413,10 +372,7 @@ mod tests {
|
||||
policy.next_poll(Duration::from_secs(30), true),
|
||||
Duration::from_millis(16)
|
||||
);
|
||||
assert_eq!(
|
||||
policy.next_poll(Duration::from_secs(30), false),
|
||||
Duration::from_secs(1)
|
||||
);
|
||||
assert_eq!(policy.next_poll(Duration::from_secs(30), false), Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -498,9 +454,7 @@ mod tests {
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err()
|
||||
);
|
||||
assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err());
|
||||
assert!(
|
||||
crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }")
|
||||
.unwrap()
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
use ferese_core::{OutputId, WorkspaceId};
|
||||
use ferese_layout::WindowId;
|
||||
use ferese_protocols::shell::v1::server::{
|
||||
ferese_shell_manager_v1::{self, FereseShellManagerV1},
|
||||
ferese_shell_v1::{self, FereseShellV1},
|
||||
};
|
||||
use smithay::{
|
||||
reexports::wayland_server::{
|
||||
Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource,
|
||||
},
|
||||
wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData},
|
||||
};
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellManagerV1;
|
||||
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
|
||||
use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1};
|
||||
use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource};
|
||||
use smithay::wayland::compositor::with_states;
|
||||
use smithay::wayland::shell::xdg::XdgToplevelSurfaceData;
|
||||
|
||||
use crate::{Ferese, private_client::ClientCapabilities, state::ClientState};
|
||||
use crate::Ferese;
|
||||
use crate::private_client::ClientCapabilities;
|
||||
use crate::state::ClientState;
|
||||
|
||||
pub(crate) fn init_global(display: &DisplayHandle) {
|
||||
display.create_global::<Ferese, FereseShellManagerV1, _>(4, ());
|
||||
@@ -52,11 +50,9 @@ impl GlobalDispatch<FereseShellManagerV1, ()> for Ferese {
|
||||
}
|
||||
|
||||
fn can_view(client: Client, _global_data: &()) -> bool {
|
||||
client.get_data::<ClientState>().is_some_and(|state| {
|
||||
state
|
||||
.capabilities
|
||||
.contains(ClientCapabilities::SHELL_CONTROL)
|
||||
})
|
||||
client
|
||||
.get_data::<ClientState>()
|
||||
.is_some_and(|state| state.capabilities.contains(ClientCapabilities::SHELL_CONTROL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,19 +100,13 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
return;
|
||||
}
|
||||
match request {
|
||||
ferese_shell_v1::Request::ActivateWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
ferese_shell_v1::Request::ActivateWindow { window_hi, window_lo } => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
ShellWindowAction::Activate,
|
||||
),
|
||||
ferese_shell_v1::Request::CloseWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => state.handle_shell_window_request(
|
||||
ferese_shell_v1::Request::CloseWindow { window_hi, window_lo } => state.handle_shell_window_request(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::CloseWindow,
|
||||
join_id(window_hi, window_lo),
|
||||
@@ -129,11 +119,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo));
|
||||
|
||||
if !state.activate_managed_workspace(workspace) {
|
||||
send_request_failed(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::ActivateWorkspace,
|
||||
workspace.0,
|
||||
);
|
||||
send_request_failed(shell, ferese_shell_v1::FailedRequest::ActivateWorkspace, workspace.0);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::ConfirmLogout { serial } => {
|
||||
@@ -149,36 +135,21 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
force,
|
||||
} => {
|
||||
if force <= 1 {
|
||||
state.confirm_portal_logout(
|
||||
shell,
|
||||
serial,
|
||||
query_token,
|
||||
inhibitor_revision,
|
||||
force == 1,
|
||||
);
|
||||
state.confirm_portal_logout(shell, serial, query_token, inhibitor_revision, force == 1);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::CancelLogout { serial } => {
|
||||
if state.logout_owner.as_ref() == Some(&shell.id())
|
||||
&& state.pending_logout == Some(serial)
|
||||
{
|
||||
if state.logout_owner.as_ref() == Some(&shell.id()) && state.pending_logout == Some(serial) {
|
||||
state.cancel_logout_confirmation();
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true),
|
||||
ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false),
|
||||
ferese_shell_v1::Request::SelectOverviewWindow {
|
||||
window_hi,
|
||||
window_lo,
|
||||
} => {
|
||||
ferese_shell_v1::Request::SelectOverviewWindow { window_hi, window_lo } => {
|
||||
let id = join_id(window_hi, window_lo);
|
||||
|
||||
if !state.select_overview_window(id) {
|
||||
send_request_failed(
|
||||
shell,
|
||||
ferese_shell_v1::FailedRequest::SelectOverviewWindow,
|
||||
id.0,
|
||||
);
|
||||
send_request_failed(shell, ferese_shell_v1::FailedRequest::SelectOverviewWindow, id.0);
|
||||
}
|
||||
}
|
||||
ferese_shell_v1::Request::Destroy => {
|
||||
@@ -202,12 +173,7 @@ impl Dispatch<FereseShellV1, ()> for Ferese {
|
||||
}
|
||||
}
|
||||
|
||||
fn logout_confirmation_matches(
|
||||
pending: Option<u32>,
|
||||
query: Option<u32>,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
) -> bool {
|
||||
fn logout_confirmation_matches(pending: Option<u32>, query: Option<u32>, serial: u32, token: u32) -> bool {
|
||||
pending == Some(serial) && query == Some(token)
|
||||
}
|
||||
|
||||
@@ -221,14 +187,7 @@ fn consume_logout_confirmation(pending: &mut Option<u32>, serial: u32) -> bool {
|
||||
}
|
||||
|
||||
impl Ferese {
|
||||
fn confirm_portal_logout(
|
||||
&mut self,
|
||||
shell: &FereseShellV1,
|
||||
serial: u32,
|
||||
token: u32,
|
||||
revision: u32,
|
||||
force: bool,
|
||||
) {
|
||||
fn confirm_portal_logout(&mut self, shell: &FereseShellV1, serial: u32, token: u32, revision: u32, force: bool) {
|
||||
if self.logout_owner.as_ref() != Some(&shell.id())
|
||||
|| !logout_confirmation_matches(self.pending_logout, self.logout_query, serial, token)
|
||||
{
|
||||
@@ -269,9 +228,7 @@ impl Ferese {
|
||||
self.logout_query = Some(self.portal_session.begin_query());
|
||||
self.pending_logout = Some(serial);
|
||||
self.logout_owner = Some(shell.id());
|
||||
let output_name = self
|
||||
.focused_output()
|
||||
.map_or_else(String::new, |output| output.name());
|
||||
let output_name = self.focused_output().map_or_else(String::new, |output| output.name());
|
||||
shell.logout_requested(serial, output_name);
|
||||
}
|
||||
|
||||
@@ -280,11 +237,7 @@ impl Ferese {
|
||||
self.portal_session.cancel_query(token);
|
||||
}
|
||||
if let Some(serial) = self.pending_logout.take() {
|
||||
for shell in self
|
||||
.shell_resources
|
||||
.iter()
|
||||
.filter_map(|shell| shell.upgrade().ok())
|
||||
{
|
||||
for shell in self.shell_resources.iter().filter_map(|shell| shell.upgrade().ok()) {
|
||||
if self.logout_owner.as_ref() == Some(&shell.id()) && shell.version() >= 3 {
|
||||
shell.logout_cancelled(serial);
|
||||
}
|
||||
@@ -294,8 +247,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
pub(crate) fn send_shell_snapshots(&mut self) {
|
||||
self.shell_resources
|
||||
.retain(|resource| resource.upgrade().is_ok());
|
||||
self.shell_resources.retain(|resource| resource.upgrade().is_ok());
|
||||
if self.shell_resources.is_empty() {
|
||||
self.last_shell_snapshot = None;
|
||||
return;
|
||||
@@ -325,8 +277,7 @@ impl Ferese {
|
||||
// New subscribers always receive a complete snapshot. Do not update the
|
||||
// broadcast cache here: existing subscribers may still need this state.
|
||||
self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1);
|
||||
self.shell_snapshot()
|
||||
.send(shell, self.shell_snapshot_serial);
|
||||
self.shell_snapshot().send(shell, self.shell_snapshot_serial);
|
||||
}
|
||||
|
||||
fn output_snapshots(&self) -> Vec<OutputSnapshot> {
|
||||
@@ -354,9 +305,8 @@ impl Ferese {
|
||||
.into_iter()
|
||||
.map(|workspace| {
|
||||
let output = self.output_workspaces.output_for_workspace(workspace.id);
|
||||
let active = output.is_some_and(|output| {
|
||||
self.output_workspaces.active_workspace(output) == Some(workspace.id)
|
||||
});
|
||||
let active =
|
||||
output.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace.id));
|
||||
|
||||
WorkspaceSnapshot {
|
||||
id: workspace.id,
|
||||
@@ -479,10 +429,7 @@ impl ShellSnapshot {
|
||||
}
|
||||
for workspace in &self.workspaces {
|
||||
let (workspace_hi, workspace_lo) = split_id(workspace.id.0);
|
||||
let (output_hi, output_lo) = workspace
|
||||
.output
|
||||
.map(|output| split_id(output.0))
|
||||
.unwrap_or_default();
|
||||
let (output_hi, output_lo) = workspace.output.map(|output| split_id(output.0)).unwrap_or_default();
|
||||
|
||||
shell.workspace(
|
||||
workspace_hi,
|
||||
@@ -564,12 +511,7 @@ mod tests {
|
||||
#[test]
|
||||
fn logout_serial_cannot_authorize_a_different_session_query() {
|
||||
assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9));
|
||||
assert!(!super::logout_confirmation_matches(
|
||||
Some(42),
|
||||
Some(9),
|
||||
42,
|
||||
10
|
||||
));
|
||||
assert!(!super::logout_confirmation_matches(Some(42), Some(9), 42, 10));
|
||||
assert!(!super::logout_confirmation_matches(None, Some(9), 42, 9));
|
||||
}
|
||||
|
||||
@@ -633,11 +575,7 @@ mod tests {
|
||||
fn config_chunks_are_bounded_and_preserve_unicode() {
|
||||
let source = "abc🌲é".repeat(9000);
|
||||
let chunks = config_chunks(&source);
|
||||
assert!(
|
||||
chunks
|
||||
.iter()
|
||||
.all(|chunk| !chunk.is_empty() && chunk.len() <= 1024)
|
||||
);
|
||||
assert!(chunks.iter().all(|chunk| !chunk.is_empty() && chunk.len() <= 1024));
|
||||
assert_eq!(chunks.concat(), source);
|
||||
assert!(config_chunks("").is_empty());
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
use ferese_layout::WindowId;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub(crate) fn layer_priority(
|
||||
floating: bool,
|
||||
zooming: bool,
|
||||
fullscreen: bool,
|
||||
above_fullscreen: bool,
|
||||
) -> u8 {
|
||||
use ferese_layout::WindowId;
|
||||
|
||||
pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, above_fullscreen: bool) -> u8 {
|
||||
if above_fullscreen {
|
||||
4
|
||||
} else if fullscreen {
|
||||
@@ -97,18 +93,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn floating_windows_stay_above_tiled_and_maximized_windows() {
|
||||
assert!(
|
||||
layer_priority(true, false, false, false) > layer_priority(false, true, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(false, true, false, false) > layer_priority(false, false, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(false, false, true, false) > layer_priority(true, false, false, false)
|
||||
);
|
||||
assert!(
|
||||
layer_priority(true, false, false, true) > layer_priority(false, false, true, false)
|
||||
);
|
||||
assert!(layer_priority(true, false, false, false) > layer_priority(false, true, false, false));
|
||||
assert!(layer_priority(false, true, false, false) > layer_priority(false, false, false, false));
|
||||
assert!(layer_priority(false, false, true, false) > layer_priority(true, false, false, false));
|
||||
assert!(layer_priority(true, false, false, true) > layer_priority(false, false, true, false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+284
-534
File diff suppressed because it is too large.
Load diff
+36
-84
@@ -1,28 +1,22 @@
|
||||
//! One decoded image and one texture per GPU context, not double-buffered
|
||||
//! full-screen UI surfaces per monitor. Resize changes sampling only.
|
||||
use crate::presentation::NativeTextureElement;
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::sync::mpsc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde::Deserialize;
|
||||
use smithay::{
|
||||
backend::{
|
||||
allocator::Fourcc,
|
||||
renderer::{
|
||||
ErasedContextId, ImportMem, Renderer,
|
||||
element::Id,
|
||||
gles::{GlesRenderer, GlesTexture},
|
||||
utils::CommitCounter,
|
||||
},
|
||||
},
|
||||
output::Output,
|
||||
utils::{Buffer, Physical, Rectangle, Size},
|
||||
};
|
||||
use std::{
|
||||
cell::Cell,
|
||||
collections::HashMap,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::mpsc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use smithay::backend::allocator::Fourcc;
|
||||
use smithay::backend::renderer::element::Id;
|
||||
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
|
||||
use smithay::backend::renderer::utils::CommitCounter;
|
||||
use smithay::backend::renderer::{ErasedContextId, ImportMem, Renderer};
|
||||
use smithay::output::Output;
|
||||
use smithay::utils::{Buffer, Physical, Rectangle, Size};
|
||||
|
||||
use crate::presentation::NativeTextureElement;
|
||||
|
||||
const UPLOAD_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
||||
@@ -80,10 +74,7 @@ impl WallpaperState {
|
||||
Self::with_wakeup(config, None)
|
||||
}
|
||||
|
||||
pub fn with_wakeup(
|
||||
config: WallpaperConfig,
|
||||
wakeup: Option<smithay::reexports::calloop::LoopSignal>,
|
||||
) -> Self {
|
||||
pub fn with_wakeup(config: WallpaperConfig, wakeup: Option<smithay::reexports::calloop::LoopSignal>) -> Self {
|
||||
let retained = config.clone();
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let owned = config.path.as_ref().is_some_and(|path| path.is_file());
|
||||
@@ -92,8 +83,7 @@ impl WallpaperState {
|
||||
let wakeup = wakeup.clone();
|
||||
std::thread::spawn(move || {
|
||||
let load = || -> Result<image::RgbaImage, String> {
|
||||
let (width, height) =
|
||||
image::image_dimensions(&path).map_err(|e| e.to_string())?;
|
||||
let (width, height) = image::image_dimensions(&path).map_err(|e| e.to_string())?;
|
||||
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
||||
return Err("wallpaper exceeds the 256 MiB decode limit".into());
|
||||
}
|
||||
@@ -164,9 +154,7 @@ impl WallpaperState {
|
||||
}
|
||||
|
||||
fn upload_ready(&self, context: &ErasedContextId, now: Instant) -> bool {
|
||||
self.upload_retries
|
||||
.get(context)
|
||||
.is_none_or(|retry| now >= *retry)
|
||||
self.upload_retries.get(context).is_none_or(|retry| now >= *retry)
|
||||
}
|
||||
|
||||
pub(crate) fn take_retry_wakeup(&self) -> bool {
|
||||
@@ -250,11 +238,7 @@ impl WallpaperState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn element(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
output: &Output,
|
||||
) -> Option<NativeTextureElement> {
|
||||
pub fn element(&mut self, renderer: &mut GlesRenderer, output: &Output) -> Option<NativeTextureElement> {
|
||||
let pixels = self.pixels.as_ref()?;
|
||||
let context = renderer.context_id().erased();
|
||||
if !self.textures.contains_key(&context) {
|
||||
@@ -265,32 +249,21 @@ impl WallpaperState {
|
||||
match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) {
|
||||
Ok(texture) => {
|
||||
self.upload_retries.remove(&context);
|
||||
tracing::debug!(
|
||||
bytes = pixels.as_raw().len(),
|
||||
"uploaded shared wallpaper texture"
|
||||
);
|
||||
self.textures.insert(
|
||||
context.clone(),
|
||||
WallpaperTexture {
|
||||
texture,
|
||||
id: Id::new(),
|
||||
},
|
||||
);
|
||||
tracing::debug!(bytes = pixels.as_raw().len(), "uploaded shared wallpaper texture");
|
||||
self.textures
|
||||
.insert(context.clone(), WallpaperTexture { texture, id: Id::new() });
|
||||
}
|
||||
Err(error) => {
|
||||
// Avoid retrying a large failed allocation every frame, but
|
||||
// allow recovery from temporary GPU memory pressure.
|
||||
self.upload_retries
|
||||
.insert(context, Instant::now() + UPLOAD_RETRY_DELAY);
|
||||
self.upload_retries.insert(context, Instant::now() + UPLOAD_RETRY_DELAY);
|
||||
tracing::debug!(%error, "wallpaper texture import failed");
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
let cached = self.textures.get(&context)?;
|
||||
let output_size = output
|
||||
.current_transform()
|
||||
.transform_size(output.current_mode()?.size);
|
||||
let output_size = output.current_transform().transform_size(output.current_mode()?.size);
|
||||
let (geometry, source) = image_geometry(
|
||||
(pixels.width() as i32, pixels.height() as i32).into(),
|
||||
output_size,
|
||||
@@ -324,11 +297,7 @@ fn image_geometry(
|
||||
(
|
||||
Rectangle::from_size(output),
|
||||
Rectangle::new(
|
||||
(
|
||||
(f64::from(image.w) - width) / 2.0,
|
||||
(f64::from(image.h) - height) / 2.0,
|
||||
)
|
||||
.into(),
|
||||
((f64::from(image.w) - width) / 2.0, (f64::from(image.h) - height) / 2.0).into(),
|
||||
(width, height).into(),
|
||||
),
|
||||
)
|
||||
@@ -341,10 +310,7 @@ fn image_geometry(
|
||||
)
|
||||
.into();
|
||||
(
|
||||
Rectangle::new(
|
||||
((output.w - size.w) / 2, (output.h - size.h) / 2).into(),
|
||||
size,
|
||||
),
|
||||
Rectangle::new(((output.w - size.w) / 2, (output.h - size.h) / 2).into(), size),
|
||||
Rectangle::from_size(image.to_f64()),
|
||||
)
|
||||
}
|
||||
@@ -373,9 +339,7 @@ mod tests {
|
||||
state.arm_retry_timer(&event_loop.handle(), now).unwrap();
|
||||
assert!(state.retry_timer_pending.get());
|
||||
assert!(!state.take_retry_wakeup());
|
||||
event_loop
|
||||
.dispatch(Some(Duration::from_secs(1)), &mut ())
|
||||
.unwrap();
|
||||
event_loop.dispatch(Some(Duration::from_secs(1)), &mut ()).unwrap();
|
||||
assert!(!state.retry_timer_pending.get());
|
||||
assert!(
|
||||
state.take_retry_wakeup(),
|
||||
@@ -383,10 +347,7 @@ mod tests {
|
||||
);
|
||||
assert!(!state.take_retry_wakeup());
|
||||
let after = Instant::now();
|
||||
assert_eq!(
|
||||
state.next_retry_deadline(after),
|
||||
Some(after + UPLOAD_RETRY_DELAY)
|
||||
);
|
||||
assert_eq!(state.next_retry_deadline(after), Some(after + UPLOAD_RETRY_DELAY));
|
||||
state.forget_context(&context);
|
||||
assert_eq!(state.next_retry_deadline(after), None);
|
||||
state.retry_wakeup.set(true);
|
||||
@@ -398,7 +359,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn decoder_wakes_an_idle_event_loop() {
|
||||
use smithay::reexports::calloop::{EventLoop, timer::Timer};
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
use smithay::reexports::calloop::timer::Timer;
|
||||
let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap();
|
||||
event_loop
|
||||
.handle()
|
||||
@@ -407,8 +369,7 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
let signal = event_loop.get_signal();
|
||||
let mut state =
|
||||
WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone()));
|
||||
let mut state = WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone()));
|
||||
event_loop
|
||||
.run(None, &mut state, |state| {
|
||||
if state.poll() {
|
||||
@@ -429,18 +390,14 @@ mod tests {
|
||||
mode: WallpaperMode::Fill,
|
||||
});
|
||||
let now = Instant::now();
|
||||
state
|
||||
.upload_retries
|
||||
.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
assert!(!state.upload_ready(&context, now));
|
||||
assert!(state.upload_ready(&other, now));
|
||||
assert!(state.upload_ready(&context, now + UPLOAD_RETRY_DELAY));
|
||||
state.forget_context(&context);
|
||||
assert!(state.upload_ready(&context, now));
|
||||
|
||||
state
|
||||
.upload_retries
|
||||
.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY);
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
state.receiver = Some(receiver);
|
||||
sender.send(Ok(image::RgbaImage::new(2, 2))).unwrap();
|
||||
@@ -489,16 +446,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn fill_crops_and_fit_letterboxes_without_reallocating_image() {
|
||||
let (geometry, source) = image_geometry(
|
||||
(3840, 2160).into(),
|
||||
(1600, 1000).into(),
|
||||
WallpaperMode::Fill,
|
||||
);
|
||||
let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fill);
|
||||
assert_eq!(geometry.size, (1600, 1000).into());
|
||||
assert_eq!(source.size.h, 2160.0);
|
||||
assert!(source.loc.x > 0.0);
|
||||
let (geometry, source) =
|
||||
image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
|
||||
let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit);
|
||||
assert_eq!(geometry.size, (1600, 900).into());
|
||||
assert_eq!(geometry.loc, (0, 50).into());
|
||||
assert_eq!(source.size, (3840.0, 2160.0).into());
|
||||
|
||||
@@ -63,12 +63,7 @@ pub fn validate(configured: &[WindowRuleConfig]) -> Result<Vec<WindowRule>, Stri
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn resolve(
|
||||
rules: &[WindowRule],
|
||||
app_id: Option<&str>,
|
||||
title: Option<&str>,
|
||||
transient: bool,
|
||||
) -> WindowRuleResult {
|
||||
pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, transient: bool) -> WindowRuleResult {
|
||||
let app_id = app_id.map(normalize_app_id);
|
||||
let mut result = WindowRuleResult::default();
|
||||
if matches!(
|
||||
@@ -88,10 +83,7 @@ pub fn resolve(
|
||||
.app_id
|
||||
.as_deref()
|
||||
.is_none_or(|expected| app_id.as_deref() == Some(expected));
|
||||
let title_matches = rule
|
||||
.title
|
||||
.as_deref()
|
||||
.is_none_or(|expected| title == Some(expected));
|
||||
let title_matches = rule.title.as_deref().is_none_or(|expected| title == Some(expected));
|
||||
let transient_matches = rule.transient.is_none_or(|expected| expected == transient);
|
||||
if !app_id_matches || !title_matches || !transient_matches {
|
||||
continue;
|
||||
@@ -109,14 +101,10 @@ pub fn resolve(
|
||||
|
||||
fn validate_rule(index: usize, rule: &WindowRuleConfig) -> Result<WindowRule, String> {
|
||||
if rule.app_id.is_none() && rule.title.is_none() && rule.transient.is_none() {
|
||||
return Err(format!(
|
||||
"window_rules[{index}] must match app_id, title, or transient"
|
||||
));
|
||||
return Err(format!("window_rules[{index}] must match app_id, title, or transient"));
|
||||
}
|
||||
if rule.workspace == Some(0) {
|
||||
return Err(format!(
|
||||
"window_rules[{index}].workspace must be greater than zero"
|
||||
));
|
||||
return Err(format!("window_rules[{index}].workspace must be greater than zero"));
|
||||
}
|
||||
|
||||
let app_id = rule
|
||||
@@ -157,11 +145,7 @@ fn nonempty_matcher(value: &str) -> Result<&str, &'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
fn positive_dimension(
|
||||
value: Option<f64>,
|
||||
index: usize,
|
||||
field: &'static str,
|
||||
) -> Result<Option<f64>, String> {
|
||||
fn positive_dimension(value: Option<f64>, index: usize, field: &'static str) -> Result<Option<f64>, String> {
|
||||
if value.is_none_or(|value| value.is_finite() && value > 0.0) {
|
||||
Ok(value)
|
||||
} else {
|
||||
@@ -193,9 +177,7 @@ mod tests {
|
||||
assert_eq!(removed.floating, Some(false));
|
||||
assert_eq!(removed.fullscreen, Some(false));
|
||||
assert_eq!(
|
||||
super::live_result(old, Default::default(), true)
|
||||
.unwrap()
|
||||
.floating,
|
||||
super::live_result(old, Default::default(), true).unwrap().floating,
|
||||
Some(true)
|
||||
);
|
||||
let new = super::WindowRuleResult {
|
||||
@@ -234,15 +216,9 @@ mod tests {
|
||||
..config(app_id)
|
||||
}])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
resolve(&rules, Some(app_id), None, false).floating,
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(resolve(&rules, Some(app_id), None, false).floating, Some(false));
|
||||
}
|
||||
assert_eq!(
|
||||
resolve(&[], Some("org.example.Editor"), None, false).floating,
|
||||
None
|
||||
);
|
||||
assert_eq!(resolve(&[], Some("org.example.Editor"), None, false).floating, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+208
-501
File diff suppressed because it is too large.
Load diff
@@ -108,13 +108,7 @@ impl AnimatedValue {
|
||||
|
||||
for _ in 0..steps {
|
||||
let previous_error = self.current - self.target;
|
||||
advance_value(
|
||||
&mut self.current,
|
||||
self.target,
|
||||
&mut self.velocity,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(&mut self.current, self.target, &mut self.velocity, step, config);
|
||||
let current_error = self.current - self.target;
|
||||
if previous_error != 0.0 && previous_error.signum() != current_error.signum() {
|
||||
self.snap();
|
||||
@@ -163,20 +157,8 @@ impl AnimatedRect {
|
||||
let step = seconds / steps as f64;
|
||||
|
||||
for _ in 0..steps {
|
||||
advance_value(
|
||||
&mut self.current.x,
|
||||
self.target.x,
|
||||
&mut self.velocity.x,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(
|
||||
&mut self.current.y,
|
||||
self.target.y,
|
||||
&mut self.velocity.y,
|
||||
step,
|
||||
config,
|
||||
);
|
||||
advance_value(&mut self.current.x, self.target.x, &mut self.velocity.x, step, config);
|
||||
advance_value(&mut self.current.y, self.target.y, &mut self.velocity.y, step, config);
|
||||
advance_value(
|
||||
&mut self.current.width,
|
||||
self.target.width,
|
||||
@@ -321,12 +303,7 @@ impl WindowGeometry {
|
||||
self.client.request_size(size, now).then_some(size)
|
||||
}
|
||||
|
||||
pub fn set_presentation_target(
|
||||
&mut self,
|
||||
rect: Rect,
|
||||
fullscreen: bool,
|
||||
now: Duration,
|
||||
) -> Option<ClientSize> {
|
||||
pub fn set_presentation_target(&mut self, rect: Rect, fullscreen: bool, now: Duration) -> Option<ClientSize> {
|
||||
self.set_presentation_mode(
|
||||
rect,
|
||||
if fullscreen {
|
||||
@@ -338,12 +315,7 @@ impl WindowGeometry {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_presentation_mode(
|
||||
&mut self,
|
||||
rect: Rect,
|
||||
mode: PresentationMode,
|
||||
now: Duration,
|
||||
) -> Option<ClientSize> {
|
||||
pub fn set_presentation_mode(&mut self, rect: Rect, mode: PresentationMode, now: Duration) -> Option<ClientSize> {
|
||||
let rect = normalized_rect(rect);
|
||||
if self.mode != mode || (self.zoom.is_some() && self.logical != rect) {
|
||||
let mut progress = AnimatedValue::new(0.0);
|
||||
@@ -359,14 +331,9 @@ impl WindowGeometry {
|
||||
let speed = [velocity.x, velocity.y, velocity.width, velocity.height];
|
||||
let squared_length = distance.iter().map(|value| value * value).sum::<f64>();
|
||||
if squared_length > 0.001 {
|
||||
let projected =
|
||||
distance.iter().zip(speed).map(|(d, v)| d * v).sum::<f64>() / squared_length;
|
||||
let projected = distance.iter().zip(speed).map(|(d, v)| d * v).sum::<f64>() / squared_length;
|
||||
// Keep forward momentum; soften a reversal without jumping position.
|
||||
progress.velocity = if projected < 0.0 {
|
||||
projected * 0.35
|
||||
} else {
|
||||
projected
|
||||
};
|
||||
progress.velocity = if projected < 0.0 { projected * 0.35 } else { projected };
|
||||
}
|
||||
self.zoom = Some(ZoomTransition {
|
||||
from: self.visual.current,
|
||||
@@ -412,24 +379,14 @@ impl WindowGeometry {
|
||||
requested_size
|
||||
}
|
||||
|
||||
pub fn advance(
|
||||
&mut self,
|
||||
delta: Duration,
|
||||
config: SpringConfig,
|
||||
animations_enabled: bool,
|
||||
) -> bool {
|
||||
pub fn advance(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool {
|
||||
let previous = (self.visual.current, self.decorations);
|
||||
let active = self.advance_presentation(delta, config, animations_enabled);
|
||||
self.presentation_changed = previous != (self.visual.current, self.decorations);
|
||||
active
|
||||
}
|
||||
|
||||
fn advance_presentation(
|
||||
&mut self,
|
||||
delta: Duration,
|
||||
config: SpringConfig,
|
||||
animations_enabled: bool,
|
||||
) -> bool {
|
||||
fn advance_presentation(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool {
|
||||
if !animations_enabled {
|
||||
self.visual.snap();
|
||||
self.decorations = if self.is_fullscreen() { 0.0 } else { 1.0 };
|
||||
@@ -506,15 +463,8 @@ impl WindowGeometry {
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_value(
|
||||
current: &mut f64,
|
||||
target: f64,
|
||||
velocity: &mut f64,
|
||||
delta: f64,
|
||||
config: SpringConfig,
|
||||
) {
|
||||
let acceleration =
|
||||
(-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass;
|
||||
fn advance_value(current: &mut f64, target: f64, velocity: &mut f64, delta: f64, config: SpringConfig) {
|
||||
let acceleration = (-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass;
|
||||
*velocity += acceleration * delta;
|
||||
let previous_error = *current - target;
|
||||
*current += *velocity * delta;
|
||||
@@ -641,17 +591,10 @@ mod tests {
|
||||
assert_eq!(geometry.visual.current, rect);
|
||||
assert_eq!(
|
||||
geometry.decorations,
|
||||
if mode == PresentationMode::Fullscreen {
|
||||
0.0
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
if mode == PresentationMode::Fullscreen { 0.0 } else { 1.0 }
|
||||
);
|
||||
assert!(!geometry.is_zooming());
|
||||
assert_eq!(
|
||||
geometry.is_fullscreen(),
|
||||
mode == PresentationMode::Fullscreen
|
||||
);
|
||||
assert_eq!(geometry.is_fullscreen(), mode == PresentationMode::Fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,9 +621,7 @@ mod tests {
|
||||
fn zoom_finishes_without_a_visible_final_edge_jump() {
|
||||
let tiled = Rect::new(12.0, 12.0, 942.0, 1056.0);
|
||||
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||
for (start, target, fullscreen_target) in
|
||||
[(tiled, fullscreen, true), (fullscreen, tiled, false)]
|
||||
{
|
||||
for (start, target, fullscreen_target) in [(tiled, fullscreen, true), (fullscreen, tiled, false)] {
|
||||
let mut geometry = WindowGeometry::new(start, None);
|
||||
if !fullscreen_target {
|
||||
geometry.set_presentation_target(start, true, Duration::ZERO);
|
||||
@@ -690,11 +631,7 @@ mod tests {
|
||||
let mut finished = false;
|
||||
for _ in 0..1000 {
|
||||
let before = geometry.visual.current;
|
||||
if !geometry.advance(
|
||||
Duration::from_secs_f64(1.0 / 144.0),
|
||||
SpringConfig::default(),
|
||||
true,
|
||||
) {
|
||||
if !geometry.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default(), true) {
|
||||
let right_before = before.x + before.width;
|
||||
let right_after = target.x + target.width;
|
||||
assert!(
|
||||
@@ -721,9 +658,7 @@ mod tests {
|
||||
let progress = 1.0 - geometry.decorations;
|
||||
let visual = geometry.visual.current;
|
||||
assert!((visual.x - (start.x + (end.x - start.x) * progress)).abs() < 1e-9);
|
||||
assert!(
|
||||
(visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9
|
||||
);
|
||||
assert!((visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -734,11 +669,7 @@ mod tests {
|
||||
let mut geometry = WindowGeometry::new(tiled, None);
|
||||
for enabled in [true, false, true, false] {
|
||||
let before = (geometry.visual.current, geometry.decorations);
|
||||
geometry.set_presentation_target(
|
||||
if enabled { fullscreen } else { tiled },
|
||||
enabled,
|
||||
Duration::ZERO,
|
||||
);
|
||||
geometry.set_presentation_target(if enabled { fullscreen } else { tiled }, enabled, Duration::ZERO);
|
||||
assert_eq!((geometry.visual.current, geometry.decorations), before);
|
||||
geometry.advance(Duration::from_millis(48), SpringConfig::default(), true);
|
||||
}
|
||||
@@ -782,24 +713,15 @@ mod tests {
|
||||
Some(ClientSize::from_rect(middle))
|
||||
);
|
||||
geometry.advance(Duration::from_millis(64), SpringConfig::default(), true);
|
||||
assert_eq!(geometry.presentation_size_request(Duration::from_millis(64)), None);
|
||||
assert_eq!(
|
||||
geometry.presentation_size_request(Duration::from_millis(64)),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
geometry.set_presentation_mode(
|
||||
end,
|
||||
PresentationMode::Normal,
|
||||
Duration::from_millis(64)
|
||||
),
|
||||
geometry.set_presentation_mode(end, PresentationMode::Normal, Duration::from_millis(64)),
|
||||
Some(ClientSize::from_rect(end))
|
||||
);
|
||||
let mut requests = Vec::new();
|
||||
for frame in 1..240 {
|
||||
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||
if let Some(size) =
|
||||
geometry.presentation_size_request(Duration::from_millis(64 + frame * 8))
|
||||
{
|
||||
if let Some(size) = geometry.presentation_size_request(Duration::from_millis(64 + frame * 8)) {
|
||||
assert_eq!(geometry.visual.current.width, end.width);
|
||||
requests.push(size);
|
||||
}
|
||||
@@ -844,13 +766,8 @@ mod tests {
|
||||
true,
|
||||
);
|
||||
let current = geometry.visual.current;
|
||||
let edges = |rect: Rect| {
|
||||
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
|
||||
};
|
||||
for ((before, after), end) in edges(previous)
|
||||
.into_iter()
|
||||
.zip(edges(current))
|
||||
.zip(edges(target))
|
||||
let edges = |rect: Rect| [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height];
|
||||
for ((before, after), end) in edges(previous).into_iter().zip(edges(current)).zip(edges(target))
|
||||
{
|
||||
assert!((after - end).abs() <= (before - end).abs() + 1e-9);
|
||||
}
|
||||
@@ -1004,11 +921,7 @@ mod tests {
|
||||
let mut geometry = WindowGeometry::new(rect, None);
|
||||
|
||||
assert!(geometry.set_logical_target(rect, Duration::ZERO).is_some());
|
||||
assert!(
|
||||
geometry
|
||||
.set_logical_target(rect, Duration::from_millis(16))
|
||||
.is_none()
|
||||
);
|
||||
assert!(geometry.set_logical_target(rect, Duration::from_millis(16)).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1067,10 +980,7 @@ mod tests {
|
||||
assert!(sixty.current.width <= target.width);
|
||||
}
|
||||
for _ in 0..288 {
|
||||
one_forty_four.advance(
|
||||
Duration::from_secs_f64(1.0 / 144.0),
|
||||
SpringConfig::default(),
|
||||
);
|
||||
one_forty_four.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default());
|
||||
}
|
||||
|
||||
assert!(sixty.is_settled(SpringConfig::default()));
|
||||
|
||||
+36
-119
@@ -1,7 +1,9 @@
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue};
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value};
|
||||
use std::{fmt, path::PathBuf};
|
||||
|
||||
pub const DEFAULT_MATERIAL_OPACITY: f64 = 0.78;
|
||||
|
||||
@@ -36,14 +38,9 @@ pub fn default_wallpaper() -> &'static str {
|
||||
.ok()
|
||||
.and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png")))
|
||||
.filter(|path| path.is_file())
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")
|
||||
});
|
||||
.unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png"));
|
||||
|
||||
path.canonicalize()
|
||||
.unwrap_or(path)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
path.canonicalize().unwrap_or(path).to_string_lossy().into_owned()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -65,10 +62,8 @@ fn field(name: &str, parent: &str) -> String {
|
||||
fn is_records(key: &str, parent: &str) -> bool {
|
||||
matches!(
|
||||
(parent, key),
|
||||
(
|
||||
"",
|
||||
"bindings" | "window_rules" | "output_profiles" | "autostart"
|
||||
) | ("output_profiles", "outputs")
|
||||
("", "bindings" | "window_rules" | "output_profiles" | "autostart")
|
||||
| ("output_profiles", "outputs")
|
||||
| ("desktop_widgets", "notes")
|
||||
)
|
||||
}
|
||||
@@ -77,12 +72,7 @@ fn is_array(key: &str, parent: &str) -> bool {
|
||||
parent == "commands"
|
||||
|| matches!(
|
||||
key,
|
||||
"command"
|
||||
| "position"
|
||||
| "width_presets"
|
||||
| "xkb_options"
|
||||
| "settings_command"
|
||||
| "outputs"
|
||||
"command" | "position" | "width_presets" | "xkb_options" | "settings_command" | "outputs"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -91,9 +81,7 @@ fn scalar(value: &KdlValue) -> Result<Value, Error> {
|
||||
KdlValue::String(s) => Value::String(s.clone()),
|
||||
KdlValue::Bool(v) => Value::Bool(*v),
|
||||
KdlValue::Null => {
|
||||
return Err(Error(
|
||||
"Omit optional settings instead of using #null".into(),
|
||||
));
|
||||
return Err(Error("Omit optional settings instead of using #null".into()));
|
||||
}
|
||||
KdlValue::Integer(n) => {
|
||||
if let Ok(n) = i64::try_from(*n) {
|
||||
@@ -124,9 +112,7 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
|
||||
let key = field(node.name().value(), parent);
|
||||
let value = node_value(node, &key, parent)?;
|
||||
if is_records(&key, parent) {
|
||||
let records = result
|
||||
.entry(key)
|
||||
.or_insert_with(|| Value::Array(Vec::new()));
|
||||
let records = result.entry(key).or_insert_with(|| Value::Array(Vec::new()));
|
||||
records.as_array_mut().unwrap().push(value);
|
||||
} else {
|
||||
insert(&mut result, key, value)?;
|
||||
@@ -138,9 +124,7 @@ fn object(doc: &KdlDocument, parent: &str) -> Result<Map<String, Value>, Error>
|
||||
|
||||
fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
if node.ty().is_some() || node.entries().iter().any(|e| e.ty().is_some()) {
|
||||
return Err(Error(
|
||||
"Type annotations are not supported in configuration".into(),
|
||||
));
|
||||
return Err(Error("Type annotations are not supported in configuration".into()));
|
||||
}
|
||||
|
||||
let args = node
|
||||
@@ -156,11 +140,7 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if is_records(key, parent) {
|
||||
let mut map = node
|
||||
.children()
|
||||
.map(|d| object(d, key))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default();
|
||||
for (name, entry) in props {
|
||||
insert(&mut map, name, scalar(entry.value())?)?;
|
||||
}
|
||||
@@ -195,11 +175,7 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result<Value, Error> {
|
||||
node.name()
|
||||
)));
|
||||
}
|
||||
let mut map = node
|
||||
.children()
|
||||
.map(|d| object(d, key))
|
||||
.transpose()?
|
||||
.unwrap_or_default();
|
||||
let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default();
|
||||
for (name, entry) in props {
|
||||
insert(&mut map, name, scalar(entry.value())?)?;
|
||||
}
|
||||
@@ -224,9 +200,7 @@ pub struct Document {
|
||||
|
||||
impl Document {
|
||||
pub fn parse(source: &str) -> Result<Self, Error> {
|
||||
let doc = source
|
||||
.parse::<KdlDocument>()
|
||||
.map_err(|e| Error(format!("{e:?}")))?;
|
||||
let doc = source.parse::<KdlDocument>().map_err(|e| Error(format!("{e:?}")))?;
|
||||
let value = Value::Object(object(&doc, "")?);
|
||||
|
||||
Ok(Self { doc, value })
|
||||
@@ -267,10 +241,7 @@ impl Document {
|
||||
pub fn add(&mut self, path: &str, fields: Vec<(String, Value)>) -> Result<(), Error> {
|
||||
let mut candidate = self.clone();
|
||||
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||
let parent = parents
|
||||
.rsplit('.')
|
||||
.find(|s| s.parse::<usize>().is_err())
|
||||
.unwrap_or("");
|
||||
let parent = parents.rsplit('.').find(|s| s.parse::<usize>().is_err()).unwrap_or("");
|
||||
|
||||
if !is_records(last, parent) {
|
||||
return Err(Error("Expected a list of records".into()));
|
||||
@@ -278,18 +249,12 @@ impl Document {
|
||||
|
||||
let doc = section_mut(
|
||||
&mut candidate.doc,
|
||||
&parents
|
||||
.split('.')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>(),
|
||||
&parents.split('.').filter(|s| !s.is_empty()).collect::<Vec<_>>(),
|
||||
"",
|
||||
)?;
|
||||
|
||||
doc.nodes_mut().push(value_node(
|
||||
last,
|
||||
&Value::Object(fields.into_iter().collect()),
|
||||
parent,
|
||||
)?);
|
||||
doc.nodes_mut()
|
||||
.push(value_node(last, &Value::Object(fields.into_iter().collect()), parent)?);
|
||||
format_document(&mut candidate.doc);
|
||||
candidate.refresh()?;
|
||||
*self = candidate;
|
||||
@@ -302,19 +267,14 @@ impl Document {
|
||||
let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p));
|
||||
let doc = section_mut(
|
||||
&mut candidate.doc,
|
||||
&parents
|
||||
.split('.')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>(),
|
||||
&parents.split('.').filter(|s| !s.is_empty()).collect::<Vec<_>>(),
|
||||
"",
|
||||
)?;
|
||||
let i = doc
|
||||
.nodes()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, n)| {
|
||||
field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last
|
||||
})
|
||||
.filter(|(_, n)| field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last)
|
||||
.nth(index)
|
||||
.map(|(i, _)| i)
|
||||
.ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?;
|
||||
@@ -346,11 +306,7 @@ type NodeFormat = (Vec<Option<kdl::KdlEntryFormat>>, Option<String>);
|
||||
|
||||
fn collect_node_formats(doc: &KdlDocument, formats: &mut Vec<NodeFormat>) {
|
||||
for node in doc.nodes() {
|
||||
let entries = node
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| entry.format().cloned())
|
||||
.collect();
|
||||
let entries = node.entries().iter().map(|entry| entry.format().cloned()).collect();
|
||||
let terminator = node
|
||||
.format()
|
||||
.map(|format| format.terminator.clone())
|
||||
@@ -391,11 +347,7 @@ fn format_document(doc: &mut KdlDocument) {
|
||||
}
|
||||
|
||||
fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize {
|
||||
if let Some(i) = doc
|
||||
.nodes()
|
||||
.iter()
|
||||
.position(|n| field(n.name().value(), parent) == key)
|
||||
{
|
||||
if let Some(i) = doc.nodes().iter().position(|n| field(n.name().value(), parent) == key) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -403,11 +355,7 @@ fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize {
|
||||
doc.nodes().len() - 1
|
||||
}
|
||||
|
||||
fn section_mut<'a>(
|
||||
doc: &'a mut KdlDocument,
|
||||
parts: &[&str],
|
||||
parent: &str,
|
||||
) -> Result<&'a mut KdlDocument, Error> {
|
||||
fn section_mut<'a>(doc: &'a mut KdlDocument, parts: &[&str], parent: &str) -> Result<&'a mut KdlDocument, Error> {
|
||||
if parts.is_empty() {
|
||||
return Ok(doc);
|
||||
}
|
||||
@@ -500,9 +448,7 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) ->
|
||||
}
|
||||
}
|
||||
|
||||
if key == "autostart"
|
||||
&& rest[0] == "command"
|
||||
&& node.children().is_none_or(|d| d.get("command").is_none())
|
||||
if key == "autostart" && rest[0] == "command" && node.children().is_none_or(|d| d.get("command").is_none())
|
||||
{
|
||||
node.entries_mut().retain(|e| e.name().is_some());
|
||||
for value in value
|
||||
@@ -613,15 +559,9 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result<KdlNode, Error>
|
||||
{
|
||||
node.entries_mut().push(KdlEntry::new(kdl_value(value)?));
|
||||
}
|
||||
} else if is_records(key, parent)
|
||||
&& map.len() <= 4
|
||||
&& !value.is_array()
|
||||
&& !value.is_object()
|
||||
{
|
||||
node.entries_mut().push(KdlEntry::new_prop(
|
||||
name.replace('_', "-"),
|
||||
kdl_value(value)?,
|
||||
));
|
||||
} else if is_records(key, parent) && map.len() <= 4 && !value.is_array() && !value.is_object() {
|
||||
node.entries_mut()
|
||||
.push(KdlEntry::new_prop(name.replace('_', "-"), kdl_value(value)?));
|
||||
} else {
|
||||
append(&mut child, name, value, key)?;
|
||||
}
|
||||
@@ -643,10 +583,7 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result<KdlNode, Error>
|
||||
|
||||
fn append(doc: &mut KdlDocument, key: &str, value: &Value, parent: &str) -> Result<(), Error> {
|
||||
if is_records(key, parent) {
|
||||
for record in value
|
||||
.as_array()
|
||||
.ok_or_else(|| Error("Expected records".into()))?
|
||||
{
|
||||
for record in value.as_array().ok_or_else(|| Error("Expected records".into()))? {
|
||||
doc.nodes_mut().push(value_node(key, record, parent)?);
|
||||
}
|
||||
} else {
|
||||
@@ -677,10 +614,7 @@ desktop-widgets {
|
||||
}
|
||||
"#;
|
||||
let mut doc = Document::parse(source).unwrap();
|
||||
assert_eq!(
|
||||
doc.get("commands.my_command").unwrap(),
|
||||
&serde_json::json!(["program"])
|
||||
);
|
||||
assert_eq!(doc.get("commands.my_command").unwrap(), &serde_json::json!(["program"]));
|
||||
assert_eq!(
|
||||
doc.get("desktop_widgets.clock.outputs").unwrap(),
|
||||
&serde_json::json!(["DP-1"])
|
||||
@@ -691,8 +625,7 @@ desktop-widgets {
|
||||
);
|
||||
doc.set("commands.new_command", serde_json::json!(["two", "words"]))
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.clock.outputs", serde_json::json!([]))
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.clock.outputs", serde_json::json!([])).unwrap();
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(
|
||||
reparsed.get("commands.new_command").unwrap(),
|
||||
@@ -706,8 +639,7 @@ desktop-widgets {
|
||||
|
||||
#[test]
|
||||
fn inline_comments_and_failed_edits_leave_document_intact() {
|
||||
let mut doc =
|
||||
Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap();
|
||||
let mut doc = Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap();
|
||||
doc.set("animations.speed", 0.8.into()).unwrap();
|
||||
assert!(doc.to_string().contains("// keep inline"));
|
||||
assert_eq!(
|
||||
@@ -750,15 +682,11 @@ autostart "program" "two words" enabled=#false
|
||||
assert_eq!(doc.get("bindings.0.action").unwrap(), "toggle-overview");
|
||||
doc.set("bindings.0.action", "none".into()).unwrap();
|
||||
doc.set("input.repeat_rate", 25.into()).unwrap();
|
||||
doc.set("output_profiles.0.outputs.0.scale", 2.0.into())
|
||||
.unwrap();
|
||||
doc.set("output_profiles.0.outputs.0.scale", 2.0.into()).unwrap();
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(reparsed.get("bindings.0.action").unwrap(), "none");
|
||||
assert_eq!(reparsed.get("input.repeat_rate").unwrap(), 25);
|
||||
assert_eq!(
|
||||
reparsed.get("output_profiles.0.outputs.0.scale").unwrap(),
|
||||
2.0
|
||||
);
|
||||
assert_eq!(reparsed.get("output_profiles.0.outputs.0.scale").unwrap(), 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -786,28 +714,17 @@ desktop-widgets {
|
||||
}
|
||||
"#;
|
||||
let mut doc = Document::parse(source).unwrap();
|
||||
doc.set("desktop_widgets.notes.0.text", "one\ntwo".into())
|
||||
.unwrap();
|
||||
doc.set("desktop_widgets.notes.0.text", "one\ntwo".into()).unwrap();
|
||||
assert!(doc.to_string().contains("// personal comment"));
|
||||
let reparsed = Document::parse(&doc.to_string()).unwrap();
|
||||
assert_eq!(
|
||||
reparsed.get("commands.screenshot-full").unwrap(),
|
||||
&serde_json::json!(["ferese-screenshot", "--full"])
|
||||
);
|
||||
assert_eq!(
|
||||
reparsed.get("desktop_widgets.notes.0.text").unwrap(),
|
||||
"one\ntwo"
|
||||
);
|
||||
assert_eq!(reparsed.get("desktop_widgets.notes.0.text").unwrap(), "one\ntwo");
|
||||
doc.add("desktop_widgets.notes", vec![("id".into(), "b".into())])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
doc.get("desktop_widgets.notes")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(doc.get("desktop_widgets.notes").unwrap().as_array().unwrap().len(), 2);
|
||||
doc.remove("desktop_widgets.notes", 0).unwrap();
|
||||
assert_eq!(doc.get("desktop_widgets.notes.0.id").unwrap(), "b");
|
||||
}
|
||||
|
||||
@@ -81,9 +81,7 @@ impl Default for StickyNote {
|
||||
|
||||
impl StickyNote {
|
||||
pub fn on_output(&self, name: Option<&str>) -> bool {
|
||||
self.enabled
|
||||
&& (self.outputs.is_empty()
|
||||
|| name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
|
||||
self.enabled && (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|s| s == name)))
|
||||
}
|
||||
/// Text/style changes repaint in place, never remap the desktop card.
|
||||
pub fn same_surface(&self, other: &Self) -> bool {
|
||||
@@ -99,12 +97,7 @@ impl StickyNote {
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
let invalid = |field: &str| {
|
||||
Err(format!(
|
||||
"desktop_widgets.notes[{}].{field} is invalid",
|
||||
self.id
|
||||
))
|
||||
};
|
||||
let invalid = |field: &str| Err(format!("desktop_widgets.notes[{}].{field} is invalid", self.id));
|
||||
if self.id.is_empty()
|
||||
|| self.id.len() > 64
|
||||
|| !self
|
||||
@@ -138,9 +131,9 @@ impl StickyNote {
|
||||
for color in [&self.color, &self.background].into_iter().flatten() {
|
||||
if !color.is_empty()
|
||||
&& color != "theme"
|
||||
&& !color.strip_prefix('#').is_some_and(|hex| {
|
||||
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
})
|
||||
&& !color
|
||||
.strip_prefix('#')
|
||||
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
{
|
||||
return invalid("color/background");
|
||||
}
|
||||
@@ -253,8 +246,7 @@ impl Default for Clock {
|
||||
impl Clock {
|
||||
pub fn on_output(&self, name: Option<&str>) -> bool {
|
||||
self.enabled
|
||||
&& (self.outputs.is_empty()
|
||||
|| name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
|
||||
&& (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|output| output == name)))
|
||||
}
|
||||
|
||||
pub fn labels(&self, now: &jiff::Zoned) -> Result<(String, String), String> {
|
||||
@@ -264,10 +256,8 @@ impl Clock {
|
||||
}
|
||||
_ => now.clone(),
|
||||
};
|
||||
let time =
|
||||
jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let date =
|
||||
jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let time = jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?;
|
||||
let date = jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?;
|
||||
Ok(if self.lowercase {
|
||||
(time.to_lowercase(), date.to_lowercase())
|
||||
} else {
|
||||
@@ -295,16 +285,14 @@ impl Clock {
|
||||
return invalid(name);
|
||||
}
|
||||
}
|
||||
for color in [&self.color, &self.date_color, &self.background]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
{
|
||||
for color in [&self.color, &self.date_color, &self.background].into_iter().flatten() {
|
||||
if color.is_empty() {
|
||||
continue;
|
||||
}
|
||||
if !color.strip_prefix('#').is_some_and(|hex| {
|
||||
matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
}) {
|
||||
if !color
|
||||
.strip_prefix('#')
|
||||
.is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()))
|
||||
{
|
||||
return invalid("color");
|
||||
}
|
||||
}
|
||||
@@ -414,9 +402,6 @@ mod tests {
|
||||
date_format: "%-d %b".into(),
|
||||
..Clock::default()
|
||||
};
|
||||
assert_eq!(
|
||||
clock.labels(&now).unwrap(),
|
||||
("10:05 pm".into(), "26 sep".into())
|
||||
);
|
||||
assert_eq!(clock.labels(&now).unwrap(), ("10:05 pm".into(), "26 sep".into()));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,6 @@ mod workspace;
|
||||
|
||||
pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch};
|
||||
pub use workspace::{
|
||||
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout,
|
||||
WorkspaceSet, workspace_order_key,
|
||||
LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet,
|
||||
workspace_order_key,
|
||||
};
|
||||
@@ -1,8 +1,6 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
error::Error,
|
||||
fmt,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use crate::WorkspaceId;
|
||||
|
||||
@@ -19,12 +17,7 @@ pub struct OutputGeometry {
|
||||
|
||||
impl OutputGeometry {
|
||||
pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
fn center(self) -> (i64, i64) {
|
||||
@@ -39,10 +32,7 @@ impl OutputGeometry {
|
||||
pub enum OutputError {
|
||||
AlreadyConnected(OutputId),
|
||||
UnknownOutput(OutputId),
|
||||
WorkspaceAlreadyAssigned {
|
||||
workspace: WorkspaceId,
|
||||
output: OutputId,
|
||||
},
|
||||
WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId },
|
||||
}
|
||||
|
||||
impl fmt::Display for OutputError {
|
||||
@@ -51,10 +41,7 @@ impl fmt::Display for OutputError {
|
||||
Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"),
|
||||
Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"),
|
||||
Self::WorkspaceAlreadyAssigned { workspace, output } => {
|
||||
write!(
|
||||
formatter,
|
||||
"workspace {workspace:?} is assigned to {output:?}"
|
||||
)
|
||||
write!(formatter, "workspace {workspace:?} is assigned to {output:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,11 +98,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool {
|
||||
if self
|
||||
.outputs
|
||||
.values()
|
||||
.any(|output| output.active == workspace)
|
||||
{
|
||||
if self.outputs.values().any(|output| output.active == workspace) {
|
||||
return false;
|
||||
}
|
||||
if let Some(owner) = self.assignments.remove(&workspace)
|
||||
@@ -125,9 +108,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
self.revisions.remove(&workspace);
|
||||
self.evacuations.retain(|_, record| {
|
||||
record
|
||||
.workspaces
|
||||
.retain(|entry| entry.workspace != workspace);
|
||||
record.workspaces.retain(|entry| entry.workspace != workspace);
|
||||
if record.active == workspace {
|
||||
if let Some(entry) = record.workspaces.first() {
|
||||
record.active = entry.workspace;
|
||||
@@ -165,8 +146,7 @@ impl OutputWorkspaceMap {
|
||||
let mut reclaimed = Vec::new();
|
||||
if let Some(record) = &record {
|
||||
for evacuated in &record.workspaces {
|
||||
let assignment_matches =
|
||||
self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
|
||||
let assignment_matches = self.assignments.get(&evacuated.workspace).copied() == evacuated.target;
|
||||
let revision_matches = self.revision(evacuated.workspace) == evacuated.revision;
|
||||
|
||||
if assignment_matches && revision_matches {
|
||||
@@ -193,12 +173,7 @@ impl OutputWorkspaceMap {
|
||||
let active = record
|
||||
.as_ref()
|
||||
.and_then(|record| reclaimed.contains(&record.active).then_some(record.active))
|
||||
.or_else(|| {
|
||||
reclaimed
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|workspace| workspace.0)
|
||||
});
|
||||
.or_else(|| reclaimed.iter().copied().min_by_key(|workspace| workspace.0));
|
||||
let active = match active {
|
||||
Some(active) => active,
|
||||
None => {
|
||||
@@ -229,10 +204,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
pub fn disconnect(&mut self, output: OutputId) -> Result<Option<OutputId>, OutputError> {
|
||||
let removed = self
|
||||
.outputs
|
||||
.remove(&output)
|
||||
.ok_or(OutputError::UnknownOutput(output))?;
|
||||
let removed = self.outputs.remove(&output).ok_or(OutputError::UnknownOutput(output))?;
|
||||
let target = self.migration_target(output, removed.geometry);
|
||||
let removed_had_focus = self.focused == Some(output);
|
||||
let mut evacuated = removed.workspaces.into_iter().collect::<Vec<_>>();
|
||||
@@ -309,10 +281,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
let reassigned = self.assignments.insert(workspace, output) != Some(output);
|
||||
let state = self
|
||||
.outputs
|
||||
.get_mut(&output)
|
||||
.expect("output was checked above");
|
||||
let state = self.outputs.get_mut(&output).expect("output was checked above");
|
||||
state.workspaces.insert(workspace);
|
||||
state.active = workspace;
|
||||
self.focused = Some(output);
|
||||
@@ -324,11 +293,7 @@ impl OutputWorkspaceMap {
|
||||
Ok(WorkspaceSwitch::Activated(output))
|
||||
}
|
||||
|
||||
pub fn assign_workspace(
|
||||
&mut self,
|
||||
output: OutputId,
|
||||
workspace: WorkspaceId,
|
||||
) -> Result<OutputId, OutputError> {
|
||||
pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result<OutputId, OutputError> {
|
||||
if !self.outputs.contains_key(&output) {
|
||||
return Err(OutputError::UnknownOutput(output));
|
||||
}
|
||||
@@ -348,11 +313,7 @@ impl OutputWorkspaceMap {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn migration_target(
|
||||
&self,
|
||||
removed: OutputId,
|
||||
removed_geometry: OutputGeometry,
|
||||
) -> Option<OutputId> {
|
||||
fn migration_target(&self, removed: OutputId, removed_geometry: OutputGeometry) -> Option<OutputId> {
|
||||
if let Some(focused) = self.focused
|
||||
&& focused != removed
|
||||
&& self.outputs.contains_key(&focused)
|
||||
@@ -382,10 +343,7 @@ impl OutputWorkspaceMap {
|
||||
}
|
||||
|
||||
fn validate(&self) -> bool {
|
||||
if self
|
||||
.focused
|
||||
.is_some_and(|focused| !self.outputs.contains_key(&focused))
|
||||
{
|
||||
if self.focused.is_some_and(|focused| !self.outputs.contains_key(&focused)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -423,9 +381,7 @@ mod tests {
|
||||
assert!(outputs.forget_workspace(first));
|
||||
assert_eq!(outputs.output_for_workspace(first), None);
|
||||
outputs.disconnect(display).unwrap();
|
||||
outputs
|
||||
.connect(display, geometry, super::WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(display, geometry, super::WorkspaceId(3)).unwrap();
|
||||
assert_eq!(outputs.active_workspace(display), Some(second));
|
||||
assert_eq!(outputs.output_for_workspace(first), None);
|
||||
assert!(outputs.validate());
|
||||
@@ -462,47 +418,28 @@ mod tests {
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_prefers_a_remaining_focused_output_without_replacing_its_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
|
||||
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_the_focused_output_activates_its_workspace_on_the_nearest_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(3), geometry(3_840), WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.connect(OutputId(3), geometry(3_840), WorkspaceId(3)).unwrap();
|
||||
outputs.focus_output(OutputId(2)).unwrap();
|
||||
|
||||
assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1))));
|
||||
@@ -513,12 +450,8 @@ mod tests {
|
||||
#[test]
|
||||
fn reconnect_reclaims_automatically_evacuated_workspaces() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
|
||||
@@ -526,29 +459,20 @@ mod tests {
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconnect_restores_the_migration_targets_remaining_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(2)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2)));
|
||||
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(3))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)).unwrap();
|
||||
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
@@ -557,37 +481,24 @@ mod tests {
|
||||
#[test]
|
||||
fn using_an_evacuated_workspace_does_not_prevent_automatic_return() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
outputs.focus_output(OutputId(1)).unwrap();
|
||||
outputs.disconnect(OutputId(2)).unwrap();
|
||||
outputs
|
||||
.switch_workspace(OutputId(1), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)),
|
||||
Ok(WorkspaceId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap();
|
||||
for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] {
|
||||
let remaining = if removed == OutputId(1) {
|
||||
OutputId(2)
|
||||
@@ -596,14 +507,8 @@ mod tests {
|
||||
};
|
||||
outputs.focus_output(removed).unwrap();
|
||||
outputs.disconnect(removed).unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(remaining)
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(remaining)
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(remaining));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(remaining));
|
||||
// Switching between handed-off workspaces is normal use, not an
|
||||
// explicit change of their home output.
|
||||
outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap();
|
||||
@@ -615,14 +520,8 @@ mod tests {
|
||||
WorkspaceId(3),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(1)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(2))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1)));
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2)));
|
||||
}
|
||||
@@ -631,12 +530,8 @@ mod tests {
|
||||
#[test]
|
||||
fn selecting_a_workspace_owned_by_another_output_focuses_that_output() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs
|
||||
.connect(OutputId(2), geometry(1_920), WorkspaceId(2))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.switch_workspace(OutputId(1), WorkspaceId(2)),
|
||||
@@ -648,18 +543,10 @@ mod tests {
|
||||
#[test]
|
||||
fn assigning_an_unused_workspace_does_not_change_the_active_workspace() {
|
||||
let mut outputs = OutputWorkspaceMap::default();
|
||||
outputs
|
||||
.connect(OutputId(1), geometry(0), WorkspaceId(1))
|
||||
.unwrap();
|
||||
outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outputs.assign_workspace(OutputId(1), WorkspaceId(2)),
|
||||
Ok(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.assign_workspace(OutputId(1), WorkspaceId(2)), Ok(OutputId(1)));
|
||||
assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1)));
|
||||
assert_eq!(
|
||||
outputs.output_for_workspace(WorkspaceId(2)),
|
||||
Some(OutputId(1))
|
||||
);
|
||||
assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1)));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use ferese_layout::{
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect,
|
||||
ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, ScrollingLayout,
|
||||
SizeConstraints, ViewportFocusStrategy, WindowId,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
@@ -44,11 +44,7 @@ impl Default for WorkspaceLayout {
|
||||
}
|
||||
|
||||
impl WorkspaceLayout {
|
||||
pub fn new(
|
||||
mode: LayoutMode,
|
||||
default_column_width: ColumnWidth,
|
||||
focus_strategy: ViewportFocusStrategy,
|
||||
) -> Self {
|
||||
pub fn new(mode: LayoutMode, default_column_width: ColumnWidth, focus_strategy: ViewportFocusStrategy) -> Self {
|
||||
match mode {
|
||||
LayoutMode::Scrolling => {
|
||||
let mut layout = ScrollingLayout::with_default_width(default_column_width);
|
||||
@@ -161,17 +157,11 @@ impl WorkspaceLayout {
|
||||
constraints,
|
||||
focused.filter(|window| layout.contains(*window)),
|
||||
),
|
||||
Self::Tree(layout) => {
|
||||
layout.geometry_with_constraints(bounds, gaps, constraints, focused)
|
||||
}
|
||||
Self::Tree(layout) => layout.geometry_with_constraints(bounds, gaps, constraints, focused),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn automatic_axis(
|
||||
&self,
|
||||
focused: Option<WindowId>,
|
||||
bounds: Rect,
|
||||
) -> Result<Axis, LayoutError> {
|
||||
pub fn automatic_axis(&self, focused: Option<WindowId>, bounds: Rect) -> Result<Axis, LayoutError> {
|
||||
match self {
|
||||
Self::Scrolling(_) => Ok(Axis::Horizontal),
|
||||
Self::Tree(layout) => layout.automatic_axis(focused, bounds),
|
||||
@@ -190,24 +180,14 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result<bool, LayoutError> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.move_window(window, direction),
|
||||
Self::Tree(layout) => layout.move_window(window, direction, bounds),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
amount: f64,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.resize_window(window, direction, amount),
|
||||
Self::Tree(layout) => layout.resize_window(window, direction, amount),
|
||||
@@ -229,11 +209,7 @@ impl WorkspaceLayout {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
|
||||
match self {
|
||||
Self::Scrolling(layout) => layout.cycle_column_width(window, presets),
|
||||
Self::Tree(_) => Ok(false),
|
||||
@@ -280,11 +256,7 @@ impl WorkspaceLayout {
|
||||
|
||||
for column in columns {
|
||||
for (row, window) in column.windows.into_iter().enumerate() {
|
||||
let axis = if row == 0 {
|
||||
Axis::Horizontal
|
||||
} else {
|
||||
Axis::Vertical
|
||||
};
|
||||
let axis = if row == 0 { Axis::Horizontal } else { Axis::Vertical };
|
||||
tree.insert(window, previous, axis, 0.5)?;
|
||||
previous = Some(window);
|
||||
}
|
||||
@@ -444,12 +416,7 @@ impl WorkspaceSet {
|
||||
}
|
||||
|
||||
/// Update policy without moving windows or replacing existing column widths.
|
||||
pub fn reconfigure_defaults(
|
||||
&mut self,
|
||||
mode: LayoutMode,
|
||||
width: ColumnWidth,
|
||||
strategy: ViewportFocusStrategy,
|
||||
) {
|
||||
pub fn reconfigure_defaults(&mut self, mode: LayoutMode, width: ColumnWidth, strategy: ViewportFocusStrategy) {
|
||||
self.default_layout_mode = mode;
|
||||
self.default_column_width = width;
|
||||
self.scrolling_focus_strategy = strategy;
|
||||
@@ -470,11 +437,7 @@ impl WorkspaceSet {
|
||||
let workspace = Workspace {
|
||||
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(),
|
||||
last_focused: None,
|
||||
fullscreen: None,
|
||||
@@ -520,12 +483,8 @@ impl WorkspaceSet {
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
let focused = self.active().last_focused;
|
||||
let visible_floating = focused.filter(|window| {
|
||||
matches!(
|
||||
self.placement(*window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
});
|
||||
let visible_floating =
|
||||
focused.filter(|window| matches!(self.placement(*window), Some(WindowPlacement::Floating { .. })));
|
||||
Ok(visible_floating.or(self.active().fullscreen).or(focused))
|
||||
}
|
||||
|
||||
@@ -543,8 +502,7 @@ impl WorkspaceSet {
|
||||
|
||||
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.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id));
|
||||
workspaces
|
||||
}
|
||||
|
||||
@@ -552,20 +510,12 @@ impl WorkspaceSet {
|
||||
let mut empty = self
|
||||
.workspaces
|
||||
.values()
|
||||
.filter(|workspace| {
|
||||
workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()
|
||||
})
|
||||
.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 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 {
|
||||
@@ -588,32 +538,20 @@ impl WorkspaceSet {
|
||||
self.placements.get(&window).copied()
|
||||
}
|
||||
|
||||
pub fn set_active_layout_mode(
|
||||
&mut self,
|
||||
mode: LayoutMode,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn set_active_layout_mode(&mut self, mode: LayoutMode, bounds: Rect) -> Result<bool, WorkspaceError> {
|
||||
let default_column_width = self.default_column_width;
|
||||
let focus_strategy = self.scrolling_focus_strategy;
|
||||
let workspace = self.active_mut();
|
||||
let focused = tiled_focus(workspace);
|
||||
let changed = workspace.layout.set_mode(
|
||||
mode,
|
||||
bounds,
|
||||
focused,
|
||||
default_column_width,
|
||||
focus_strategy,
|
||||
)?;
|
||||
let changed = workspace
|
||||
.layout
|
||||
.set_mode(mode, bounds, focused, default_column_width, focus_strategy)?;
|
||||
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn set_floating_rect(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
rect: Rect,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn set_floating_rect(&mut self, window: WindowId, rect: Rect) -> Result<(), WorkspaceError> {
|
||||
let placement = self
|
||||
.placements
|
||||
.get_mut(&window)
|
||||
@@ -639,35 +577,23 @@ impl WorkspaceSet {
|
||||
self.focus_window_with_reveal(window, false)
|
||||
}
|
||||
|
||||
fn focus_window_with_reveal(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
reveal: bool,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
fn focus_window_with_reveal(&mut self, window: WindowId, reveal: bool) -> Result<(), WorkspaceError> {
|
||||
if self.workspace_for_window(window) != Some(self.active) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is not on the active workspace",
|
||||
));
|
||||
}
|
||||
if self.active().fullscreen.is_some_and(|fullscreen| {
|
||||
fullscreen != window
|
||||
&& !matches!(
|
||||
self.placement(window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
fullscreen != window && !matches!(self.placement(window), Some(WindowPlacement::Floating { .. }))
|
||||
}) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"));
|
||||
}
|
||||
|
||||
if self.placement(window) == Some(WindowPlacement::Tiled) {
|
||||
if reveal {
|
||||
self.active_mut().layout.activate_window(window)?;
|
||||
} else {
|
||||
self.active_mut()
|
||||
.layout
|
||||
.activate_window_without_reveal(window)?;
|
||||
self.active_mut().layout.activate_window_without_reveal(window)?;
|
||||
}
|
||||
}
|
||||
self.active_mut().last_focused = Some(window);
|
||||
@@ -676,11 +602,7 @@ impl WorkspaceSet {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn stack_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
target: WindowId,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), WorkspaceError> {
|
||||
let workspace = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
@@ -721,11 +643,7 @@ impl WorkspaceSet {
|
||||
Ok(changed)
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, WorkspaceError> {
|
||||
if self.workspace_for_window(window) != Some(self.active)
|
||||
|| self.placement(window) != Some(WindowPlacement::Tiled)
|
||||
{
|
||||
@@ -734,10 +652,7 @@ impl WorkspaceSet {
|
||||
));
|
||||
}
|
||||
|
||||
let changed = self
|
||||
.active_mut()
|
||||
.layout
|
||||
.cycle_column_width(window, presets)?;
|
||||
let changed = self.active_mut().layout.cycle_column_width(window, presets)?;
|
||||
debug_assert!(self.validate().is_ok());
|
||||
Ok(changed)
|
||||
}
|
||||
@@ -804,12 +719,7 @@ impl WorkspaceSet {
|
||||
self.activate(workspace)
|
||||
}
|
||||
|
||||
pub fn insert_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
axis: Axis,
|
||||
ratio: f64,
|
||||
) -> Result<(), WorkspaceError> {
|
||||
pub fn insert_window(&mut self, window: WindowId, axis: Axis, ratio: f64) -> Result<(), WorkspaceError> {
|
||||
if self.window_workspaces.contains_key(&window) {
|
||||
return Err(WorkspaceError::DuplicateWindow(window));
|
||||
}
|
||||
@@ -1004,11 +914,7 @@ impl WorkspaceSet {
|
||||
Ok(enabled)
|
||||
}
|
||||
|
||||
pub fn set_fullscreen(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
enabled: bool,
|
||||
) -> Result<bool, WorkspaceError> {
|
||||
pub fn set_fullscreen(&mut self, window: WindowId, enabled: bool) -> Result<bool, WorkspaceError> {
|
||||
let workspace_id = self
|
||||
.workspace_for_window(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
@@ -1038,18 +944,14 @@ impl WorkspaceSet {
|
||||
return Err(WorkspaceError::InvalidState("active workspace is missing"));
|
||||
}
|
||||
if self.names.len() != self.workspaces.len() {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace name index is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace name index is inconsistent"));
|
||||
}
|
||||
|
||||
let mut seen_windows = HashSet::new();
|
||||
|
||||
for (id, workspace) in &self.workspaces {
|
||||
if workspace.id != *id || self.names.get(&workspace.name) != Some(id) {
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace index is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace index is inconsistent"));
|
||||
}
|
||||
|
||||
workspace.layout.validate()?;
|
||||
@@ -1059,9 +961,7 @@ impl WorkspaceSet {
|
||||
|| self.window_workspaces.get(&window) != Some(id)
|
||||
|| self.placements.get(&window) != Some(&WindowPlacement::Tiled)
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window ownership is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("window ownership is inconsistent"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1071,10 +971,7 @@ impl WorkspaceSet {
|
||||
if !seen_floating.insert(*window)
|
||||
|| !seen_windows.insert(*window)
|
||||
|| self.window_workspaces.get(window) != Some(id)
|
||||
|| !matches!(
|
||||
self.placements.get(window),
|
||||
Some(WindowPlacement::Floating { .. })
|
||||
)
|
||||
|| !matches!(self.placements.get(window), Some(WindowPlacement::Floating { .. }))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"floating window ownership is inconsistent",
|
||||
@@ -1086,9 +983,7 @@ impl WorkspaceSet {
|
||||
.last_focused
|
||||
.is_some_and(|window| self.window_workspaces.get(&window) != Some(id))
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"workspace focus is inconsistent",
|
||||
));
|
||||
return Err(WorkspaceError::InvalidState("workspace focus is inconsistent"));
|
||||
}
|
||||
|
||||
if workspace
|
||||
@@ -1101,12 +996,8 @@ impl WorkspaceSet {
|
||||
}
|
||||
}
|
||||
|
||||
if seen_windows.len() != self.window_workspaces.len()
|
||||
|| self.placements.len() != self.window_workspaces.len()
|
||||
{
|
||||
return Err(WorkspaceError::InvalidState(
|
||||
"window index retains stale entries",
|
||||
));
|
||||
if seen_windows.len() != self.window_workspaces.len() || self.placements.len() != self.window_workspaces.len() {
|
||||
return Err(WorkspaceError::InvalidState("window index retains stale entries"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1114,13 +1005,10 @@ impl WorkspaceSet {
|
||||
}
|
||||
|
||||
fn first_window(workspace: &Workspace) -> Option<WindowId> {
|
||||
workspace.layout.preferred_window().or_else(|| {
|
||||
workspace
|
||||
.floating
|
||||
.iter()
|
||||
.copied()
|
||||
.min_by_key(|window| window.0)
|
||||
})
|
||||
workspace
|
||||
.layout
|
||||
.preferred_window()
|
||||
.or_else(|| workspace.floating.iter().copied().min_by_key(|window| window.0))
|
||||
}
|
||||
|
||||
fn tiled_focus(workspace: &Workspace) -> Option<WindowId> {
|
||||
@@ -1151,10 +1039,7 @@ mod tests {
|
||||
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]
|
||||
);
|
||||
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();
|
||||
@@ -1188,19 +1073,13 @@ mod tests {
|
||||
.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_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_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![first]);
|
||||
assert!(set.validate().is_ok());
|
||||
assert_ne!(set.ensure_numeric(1).unwrap(), first);
|
||||
}
|
||||
@@ -1222,9 +1101,7 @@ mod tests {
|
||||
fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let WorkspaceLayout::Scrolling(layout) = &mut workspaces.active_mut().layout else {
|
||||
panic!()
|
||||
@@ -1263,30 +1140,18 @@ mod tests {
|
||||
#[test]
|
||||
fn reconfiguration_preserves_windows_and_current_widths_but_updates_defaults() {
|
||||
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 workspace = workspaces.active_id();
|
||||
workspaces.reconfigure_defaults(
|
||||
LayoutMode::Tree,
|
||||
ColumnWidth::Full,
|
||||
ViewportFocusStrategy::Minimal,
|
||||
);
|
||||
workspaces.reconfigure_defaults(LayoutMode::Tree, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
|
||||
let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else {
|
||||
panic!("existing layout must survive");
|
||||
};
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5));
|
||||
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();
|
||||
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::*;
|
||||
|
||||
@@ -1304,14 +1169,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn configured_layout_defaults_apply_to_new_workspaces() {
|
||||
let mut workspaces = WorkspaceSet::new(
|
||||
LayoutMode::Scrolling,
|
||||
ColumnWidth::Full,
|
||||
ViewportFocusStrategy::Minimal,
|
||||
);
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let mut workspaces =
|
||||
WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full, ViewportFocusStrategy::Minimal);
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
let first = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
@@ -1323,10 +1183,7 @@ mod tests {
|
||||
)
|
||||
.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();
|
||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling);
|
||||
}
|
||||
@@ -1334,14 +1191,10 @@ mod tests {
|
||||
#[test]
|
||||
fn switching_restores_workspace_focus() {
|
||||
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);
|
||||
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_numeric(2).unwrap(), Some(WindowId(2)));
|
||||
@@ -1351,12 +1204,8 @@ mod tests {
|
||||
#[test]
|
||||
fn moving_a_window_updates_membership_without_switching() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
let first = workspaces.active_id();
|
||||
let second = workspaces
|
||||
.move_window_to_numeric(WindowId(2), 2, Axis::Horizontal, 0.5)
|
||||
@@ -1364,32 +1213,16 @@ mod tests {
|
||||
|
||||
assert_eq!(workspaces.active_id(), first);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(second));
|
||||
assert!(
|
||||
!workspaces
|
||||
.workspace(first)
|
||||
.unwrap()
|
||||
.layout
|
||||
.contains(WindowId(2))
|
||||
);
|
||||
assert!(
|
||||
workspaces
|
||||
.workspace(second)
|
||||
.unwrap()
|
||||
.layout
|
||||
.contains(WindowId(2))
|
||||
);
|
||||
assert!(!workspaces.workspace(first).unwrap().layout.contains(WindowId(2)));
|
||||
assert!(workspaces.workspace(second).unwrap().layout.contains(WindowId(2)));
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_focus_chooses_a_deterministic_remaining_window() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.remove_window(WindowId(1)).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1400,9 +1233,7 @@ mod tests {
|
||||
fn closing_a_scrolling_column_focuses_its_adjacent_column() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
workspaces.focus_window(WindowId(3)).unwrap();
|
||||
|
||||
@@ -1414,9 +1245,7 @@ mod tests {
|
||||
#[test]
|
||||
fn focus_rejects_a_window_on_an_inactive_workspace() {
|
||||
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
|
||||
.move_window_to_numeric(WindowId(1), 2, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
@@ -1433,9 +1262,7 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_toggle_removes_and_restores_tiled_membership() {
|
||||
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 rect = Rect::new(10.0, 20.0, 640.0, 480.0);
|
||||
|
||||
assert_eq!(
|
||||
@@ -1461,21 +1288,14 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_geometry_updates_are_authoritative_and_validated() {
|
||||
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.set_floating_rect(WindowId(1), Rect::default()),
|
||||
Err(WorkspaceError::InvalidState("window is not floating"))
|
||||
);
|
||||
|
||||
workspaces
|
||||
.toggle_floating(
|
||||
WindowId(1),
|
||||
Rect::new(10.0, 20.0, 640.0, 480.0),
|
||||
Axis::Horizontal,
|
||||
0.5,
|
||||
)
|
||||
.toggle_floating(WindowId(1), Rect::new(10.0, 20.0, 640.0, 480.0), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0))
|
||||
@@ -1497,15 +1317,9 @@ mod tests {
|
||||
ViewportFocusStrategy::Center,
|
||||
ViewportFocusStrategy::Paged,
|
||||
] {
|
||||
let mut workspaces = WorkspaceSet::new(
|
||||
LayoutMode::Scrolling,
|
||||
ColumnWidth::Proportion(0.5),
|
||||
strategy,
|
||||
);
|
||||
let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Proportion(0.5), strategy);
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
@@ -1555,20 +1369,13 @@ mod tests {
|
||||
ViewportFocusStrategy::Paged,
|
||||
);
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0);
|
||||
let before = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&HashMap::new(),
|
||||
Some(WindowId(4)),
|
||||
)
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4)))
|
||||
.unwrap();
|
||||
let scroll = workspaces.active().layout.viewport_x();
|
||||
workspaces
|
||||
@@ -1582,12 +1389,7 @@ mod tests {
|
||||
let after = workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(
|
||||
bounds,
|
||||
GapConfig::default(),
|
||||
&HashMap::new(),
|
||||
Some(WindowId(5)),
|
||||
)
|
||||
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5)))
|
||||
.unwrap();
|
||||
assert_eq!(before.geometry, after.geometry);
|
||||
assert_eq!(scroll, workspaces.active().layout.viewport_x());
|
||||
@@ -1598,9 +1400,7 @@ mod tests {
|
||||
#[test]
|
||||
fn floating_window_can_follow_its_parent_workspace_without_focus() {
|
||||
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();
|
||||
workspaces.switch_to_numeric(2).unwrap();
|
||||
workspaces
|
||||
@@ -1612,10 +1412,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
workspaces.workspace_for_window(WindowId(2)),
|
||||
Some(parent_workspace)
|
||||
);
|
||||
assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(parent_workspace));
|
||||
assert_eq!(
|
||||
workspaces.workspace(parent_workspace).unwrap().last_focused,
|
||||
Some(WindowId(1))
|
||||
@@ -1626,9 +1423,7 @@ mod tests {
|
||||
#[test]
|
||||
fn fullscreen_preserves_underlying_placement() {
|
||||
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 before = workspaces.placement(WindowId(1));
|
||||
|
||||
assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap());
|
||||
@@ -1646,13 +1441,9 @@ mod tests {
|
||||
#[test]
|
||||
fn mapping_under_fullscreen_does_not_change_focus() {
|
||||
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
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(workspaces.active().fullscreen, Some(WindowId(1)));
|
||||
@@ -1664,17 +1455,10 @@ mod tests {
|
||||
fn floating_dialog_can_focus_above_fullscreen() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
let workspace = workspaces.active_id();
|
||||
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
|
||||
.insert_floating_window(
|
||||
WindowId(2),
|
||||
workspace,
|
||||
Rect::new(100.0, 100.0, 500.0, 400.0),
|
||||
true,
|
||||
)
|
||||
.insert_floating_window(WindowId(2), workspace, Rect::new(100.0, 100.0, 500.0, 400.0), true)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1688,14 +1472,10 @@ mod tests {
|
||||
#[test]
|
||||
fn switching_to_fullscreen_restores_the_visible_window() {
|
||||
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.switch_to_numeric(2).unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces
|
||||
.move_window_to_numeric(WindowId(2), 1, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
@@ -1704,9 +1484,7 @@ mod tests {
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(1)));
|
||||
assert_eq!(
|
||||
workspaces.focus_window(WindowId(2)),
|
||||
Err(WorkspaceError::InvalidState(
|
||||
"focused window is hidden by fullscreen"
|
||||
))
|
||||
Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen"))
|
||||
);
|
||||
assert!(workspaces.validate().is_ok());
|
||||
}
|
||||
@@ -1714,12 +1492,8 @@ mod tests {
|
||||
#[test]
|
||||
fn grouping_tiled_windows_preserves_workspace_membership() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
workspaces
|
||||
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces
|
||||
.insert_window(WindowId(2), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap();
|
||||
workspaces.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
@@ -1740,26 +1514,16 @@ mod tests {
|
||||
fn layout_mode_conversion_preserves_membership_and_focus() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=4 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
workspaces.focus_window(WindowId(2)).unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
|
||||
assert!(
|
||||
workspaces
|
||||
.set_active_layout_mode(LayoutMode::Tree, bounds)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(workspaces.set_active_layout_mode(LayoutMode::Tree, bounds).unwrap());
|
||||
assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree);
|
||||
assert_eq!(workspaces.active().last_focused, Some(WindowId(2)));
|
||||
assert_eq!(
|
||||
workspaces
|
||||
.active()
|
||||
.layout
|
||||
.window_ids()
|
||||
.collect::<HashSet<_>>(),
|
||||
workspaces.active().layout.window_ids().collect::<HashSet<_>>(),
|
||||
HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)])
|
||||
);
|
||||
|
||||
@@ -1777,19 +1541,14 @@ mod tests {
|
||||
fn scrolling_column_commands_preserve_workspace_state() {
|
||||
let mut workspaces = WorkspaceSet::default();
|
||||
for id in 1..=3 {
|
||||
workspaces
|
||||
.insert_window(WindowId(id), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap();
|
||||
}
|
||||
|
||||
workspaces.stack_window(WindowId(3), WindowId(2)).unwrap();
|
||||
assert!(workspaces.extract_window(WindowId(3)).unwrap());
|
||||
assert!(
|
||||
workspaces
|
||||
.cycle_column_width(
|
||||
WindowId(3),
|
||||
&[ColumnWidth::Proportion(0.5), ColumnWidth::Full],
|
||||
)
|
||||
.cycle_column_width(WindowId(3), &[ColumnWidth::Proportion(0.5), ColumnWidth::Full],)
|
||||
.unwrap()
|
||||
);
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
@@ -1817,9 +1576,7 @@ mod tests {
|
||||
0 if windows.len() < 48 => {
|
||||
let window = WindowId(next_window);
|
||||
next_window += 1;
|
||||
workspaces
|
||||
.insert_window(window, random_axis(random), 0.5)
|
||||
.unwrap();
|
||||
workspaces.insert_window(window, random_axis(random), 0.5).unwrap();
|
||||
windows.push(window);
|
||||
}
|
||||
1 if !windows.is_empty() => {
|
||||
@@ -1866,8 +1623,7 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(window) = candidates.get(random as usize % candidates.len().max(1))
|
||||
{
|
||||
if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) {
|
||||
workspaces.focus_window(*window).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::io::{Read, Write};
|
||||
use std::{fmt, io};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -69,10 +69,7 @@ impl fmt::Display for FrameError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Io(error) => error.fmt(formatter),
|
||||
Self::Oversized(size) => write!(
|
||||
formatter,
|
||||
"IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}"
|
||||
),
|
||||
Self::Oversized(size) => write!(formatter, "IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}"),
|
||||
Self::InvalidJson(error) => write!(formatter, "invalid IPC JSON: {error}"),
|
||||
}
|
||||
}
|
||||
|
||||
+56
-202
@@ -54,12 +54,7 @@ pub struct Rect {
|
||||
|
||||
impl Rect {
|
||||
pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self {
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
Self { x, y, width, height }
|
||||
}
|
||||
|
||||
fn center(self) -> (f64, f64) {
|
||||
@@ -365,11 +360,7 @@ impl LayoutTree {
|
||||
Ok(geometry)
|
||||
}
|
||||
|
||||
pub fn geometry_with_gaps(
|
||||
&self,
|
||||
bounds: Rect,
|
||||
gaps: GapConfig,
|
||||
) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||
pub fn geometry_with_gaps(&self, bounds: Rect, gaps: GapConfig) -> Result<HashMap<WindowId, Rect>, LayoutError> {
|
||||
let mut geometry = self.geometry(bounds)?;
|
||||
apply_gaps(&mut geometry, bounds, gaps);
|
||||
|
||||
@@ -434,11 +425,7 @@ impl LayoutTree {
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn automatic_axis(
|
||||
&self,
|
||||
focused: Option<WindowId>,
|
||||
bounds: Rect,
|
||||
) -> Result<Axis, LayoutError> {
|
||||
pub fn automatic_axis(&self, focused: Option<WindowId>, bounds: Rect) -> Result<Axis, LayoutError> {
|
||||
let Some(root) = self.root else {
|
||||
return Ok(Axis::Horizontal);
|
||||
};
|
||||
@@ -516,12 +503,7 @@ impl LayoutTree {
|
||||
.map(|(candidate, _, _, _)| candidate))
|
||||
}
|
||||
|
||||
pub fn move_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
bounds: Rect,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result<bool, LayoutError> {
|
||||
let Some(neighbor) = self.directional_neighbor(window, direction, bounds)? else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -545,12 +527,7 @@ impl LayoutTree {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn resize_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
amount: f64,
|
||||
) -> Result<bool, LayoutError> {
|
||||
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
|
||||
let mut child = self
|
||||
.windows
|
||||
.get(&window)
|
||||
@@ -651,9 +628,7 @@ impl LayoutTree {
|
||||
match self.nodes.get(&node) {
|
||||
Some(Node::Window(_)) => Ok(node),
|
||||
Some(Node::Split { first, .. }) => self.first_window(*first),
|
||||
Some(Node::Stack {
|
||||
children, active, ..
|
||||
}) => children
|
||||
Some(Node::Stack { children, active, .. }) => children
|
||||
.get(*active)
|
||||
.copied()
|
||||
.ok_or(LayoutError::InvalidTree("stack active index is invalid"))
|
||||
@@ -662,12 +637,7 @@ impl LayoutTree {
|
||||
}
|
||||
}
|
||||
|
||||
fn replace_child(
|
||||
&mut self,
|
||||
parent: NodeId,
|
||||
old: NodeId,
|
||||
new: NodeId,
|
||||
) -> Result<(), LayoutError> {
|
||||
fn replace_child(&mut self, parent: NodeId, old: NodeId, new: NodeId) -> Result<(), LayoutError> {
|
||||
match self.nodes.get_mut(&parent) {
|
||||
Some(Node::Split { first, .. }) if *first == old => *first = new,
|
||||
Some(Node::Split { second, .. }) if *second == old => *second = new,
|
||||
@@ -684,12 +654,7 @@ impl LayoutTree {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collapse_parent(
|
||||
&mut self,
|
||||
parent: NodeId,
|
||||
leaf: NodeId,
|
||||
sibling: NodeId,
|
||||
) -> Result<(), LayoutError> {
|
||||
fn collapse_parent(&mut self, parent: NodeId, leaf: NodeId, sibling: NodeId) -> Result<(), LayoutError> {
|
||||
let window = match self.nodes.get(&leaf) {
|
||||
Some(Node::Window(window)) => *window,
|
||||
_ => return Err(LayoutError::InvalidTree("removed leaf is not a window")),
|
||||
@@ -786,21 +751,11 @@ impl LayoutTree {
|
||||
} else {
|
||||
(available - second_required).max(0.0)
|
||||
};
|
||||
let adjusted_ratio = if available > 0.0 {
|
||||
first_extent / available
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
let adjusted_ratio = if available > 0.0 { first_extent / available } else { 0.5 };
|
||||
let (first_bounds, second_bounds) = split_rect(bounds, *axis, adjusted_ratio);
|
||||
|
||||
self.layout_node_constrained(*first, first_bounds, constraints, focused, geometry)?;
|
||||
self.layout_node_constrained(
|
||||
*second,
|
||||
second_bounds,
|
||||
constraints,
|
||||
focused,
|
||||
geometry,
|
||||
)?;
|
||||
self.layout_node_constrained(*second, second_bounds, constraints, focused, geometry)?;
|
||||
}
|
||||
Some(Node::Stack { children, active }) => {
|
||||
let child = children
|
||||
@@ -826,10 +781,7 @@ impl LayoutTree {
|
||||
Ok((constraint.min_width, constraint.min_height))
|
||||
}
|
||||
Some(Node::Split {
|
||||
axis,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
axis, first, second, ..
|
||||
}) => {
|
||||
let first = self.minimum_size(*first, constraints)?;
|
||||
let second = self.minimum_size(*second, constraints)?;
|
||||
@@ -860,9 +812,7 @@ impl LayoutTree {
|
||||
Some(Node::Split { first, second, .. }) => {
|
||||
self.contains_window(*first, window) || self.contains_window(*second, window)
|
||||
}
|
||||
Some(Node::Stack { children, .. }) => children
|
||||
.iter()
|
||||
.any(|child| self.contains_window(*child, window)),
|
||||
Some(Node::Stack { children, .. }) => children.iter().any(|child| self.contains_window(*child, window)),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -887,15 +837,9 @@ impl LayoutTree {
|
||||
}
|
||||
}
|
||||
Some(Node::Split {
|
||||
ratio,
|
||||
first,
|
||||
second,
|
||||
..
|
||||
ratio, first, second, ..
|
||||
}) => {
|
||||
if first == second
|
||||
|| !ratio.is_finite()
|
||||
|| !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio)
|
||||
{
|
||||
if first == second || !ratio.is_finite() || !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio) {
|
||||
return Err(LayoutError::InvalidTree("split is invalid"));
|
||||
}
|
||||
|
||||
@@ -931,11 +875,7 @@ fn valid_gap(gap: f64) -> f64 {
|
||||
}
|
||||
|
||||
fn valid_resize_amount(amount: f64) -> f64 {
|
||||
if amount.is_finite() {
|
||||
amount.abs()
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if amount.is_finite() { amount.abs() } else { 0.0 }
|
||||
}
|
||||
|
||||
fn normalized_constraints(constraints: SizeConstraints) -> SizeConstraints {
|
||||
@@ -1029,12 +969,7 @@ fn nearly_equal(left: f64, right: f64) -> bool {
|
||||
(left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0)
|
||||
}
|
||||
|
||||
fn intervals_overlap(
|
||||
first_start: f64,
|
||||
first_size: f64,
|
||||
second_start: f64,
|
||||
second_size: f64,
|
||||
) -> bool {
|
||||
fn intervals_overlap(first_start: f64, first_size: f64, second_start: f64, second_size: f64) -> bool {
|
||||
first_start < second_start + second_size && second_start < first_start + first_size
|
||||
}
|
||||
|
||||
@@ -1043,24 +978,14 @@ fn split_rect(bounds: Rect, axis: Axis, ratio: f64) -> (Rect, Rect) {
|
||||
Axis::Horizontal => {
|
||||
let boundary = bounds.x + bounds.width * ratio;
|
||||
let first = Rect::new(bounds.x, bounds.y, boundary - bounds.x, bounds.height);
|
||||
let second = Rect::new(
|
||||
boundary,
|
||||
bounds.y,
|
||||
bounds.x + bounds.width - boundary,
|
||||
bounds.height,
|
||||
);
|
||||
let second = Rect::new(boundary, bounds.y, bounds.x + bounds.width - boundary, bounds.height);
|
||||
|
||||
(first, second)
|
||||
}
|
||||
Axis::Vertical => {
|
||||
let boundary = bounds.y + bounds.height * ratio;
|
||||
let first = Rect::new(bounds.x, bounds.y, bounds.width, boundary - bounds.y);
|
||||
let second = Rect::new(
|
||||
bounds.x,
|
||||
boundary,
|
||||
bounds.width,
|
||||
bounds.y + bounds.height - boundary,
|
||||
);
|
||||
let second = Rect::new(bounds.x, boundary, bounds.width, bounds.y + bounds.height - boundary);
|
||||
|
||||
(first, second)
|
||||
}
|
||||
@@ -1074,43 +999,32 @@ mod tests {
|
||||
#[test]
|
||||
fn insertion_splits_the_focused_leaf() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5)
|
||||
.unwrap();
|
||||
|
||||
assert!(tree.validate().is_ok());
|
||||
assert_eq!(
|
||||
tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0))
|
||||
.unwrap()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0)).unwrap().len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removal_collapses_the_parent_split() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.remove(WindowId(1)).unwrap();
|
||||
|
||||
assert!(tree.validate().is_ok());
|
||||
assert_eq!(
|
||||
tree.node(tree.root().unwrap()),
|
||||
Some(&Node::Window(WindowId(2)))
|
||||
);
|
||||
assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(2))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_split_boundary_is_exact() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 1.0 / 3.0)
|
||||
.unwrap();
|
||||
|
||||
@@ -1125,8 +1039,7 @@ mod tests {
|
||||
#[test]
|
||||
fn rejects_duplicate_and_unknown_windows() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5),
|
||||
@@ -1147,28 +1060,20 @@ mod tests {
|
||||
#[test]
|
||||
fn automatic_axis_follows_focused_tile_shape() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
let wide = Rect::new(0.0, 0.0, 1_920.0, 1_080.0);
|
||||
assert_eq!(
|
||||
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||
Axis::Horizontal
|
||||
);
|
||||
assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Horizontal);
|
||||
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tree.automatic_axis(Some(WindowId(1)), wide).unwrap(),
|
||||
Axis::Vertical
|
||||
);
|
||||
assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Vertical);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gaps_preserve_outer_and_shared_spacing() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
|
||||
@@ -1186,8 +1091,7 @@ mod tests {
|
||||
#[test]
|
||||
fn smart_gaps_remove_outer_gap_for_one_window() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||
let geometry = tree
|
||||
@@ -1206,8 +1110,7 @@ mod tests {
|
||||
#[test]
|
||||
fn default_gaps_keep_outer_spacing_for_one_window() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
let geometry = tree
|
||||
.geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default())
|
||||
@@ -1219,8 +1122,7 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_neighbor_prefers_primary_distance() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(2)), Axis::Vertical, 0.5)
|
||||
@@ -1233,13 +1135,11 @@ mod tests {
|
||||
Some(WindowId(2))
|
||||
);
|
||||
assert_eq!(
|
||||
tree.directional_neighbor(WindowId(2), Direction::Down, bounds)
|
||||
.unwrap(),
|
||||
tree.directional_neighbor(WindowId(2), Direction::Down, bounds).unwrap(),
|
||||
Some(WindowId(3))
|
||||
);
|
||||
assert_eq!(
|
||||
tree.directional_neighbor(WindowId(1), Direction::Left, bounds)
|
||||
.unwrap(),
|
||||
tree.directional_neighbor(WindowId(1), Direction::Left, bounds).unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -1247,16 +1147,12 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_move_swaps_window_leaves() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 80.0);
|
||||
|
||||
assert!(
|
||||
tree.move_window(WindowId(1), Direction::Right, bounds)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.move_window(WindowId(1), Direction::Right, bounds).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(2)].x, 0.0);
|
||||
@@ -1267,16 +1163,11 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_move_at_boundary_is_a_noop() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert!(
|
||||
!tree
|
||||
.move_window(
|
||||
WindowId(1),
|
||||
Direction::Left,
|
||||
Rect::new(0.0, 0.0, 100.0, 80.0),
|
||||
)
|
||||
.move_window(WindowId(1), Direction::Left, Rect::new(0.0, 0.0, 100.0, 80.0),)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
@@ -1284,18 +1175,14 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_changes_the_nearest_matching_split() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
assert!(
|
||||
tree.resize_window(WindowId(1), Direction::Right, 0.1)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.resize_window(WindowId(1), Direction::Right, 0.1).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(1)].width, 30.0);
|
||||
@@ -1307,23 +1194,15 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_walks_to_an_ancestor_and_clamps() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5)
|
||||
.unwrap();
|
||||
let bounds = Rect::new(0.0, 0.0, 100.0, 100.0);
|
||||
|
||||
assert!(
|
||||
tree.resize_window(WindowId(3), Direction::Right, 1.0)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(3), Direction::Right, 1.0)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap());
|
||||
assert!(!tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap());
|
||||
|
||||
let geometry = tree.geometry(bounds).unwrap();
|
||||
assert_eq!(geometry[&WindowId(2)].x, 95.0);
|
||||
@@ -1333,26 +1212,16 @@ mod tests {
|
||||
#[test]
|
||||
fn directional_resize_at_an_outer_edge_is_a_noop() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(1), Direction::Left, 0.05)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(
|
||||
!tree
|
||||
.resize_window(WindowId(1), Direction::Right, f64::NAN)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(!tree.resize_window(WindowId(1), Direction::Left, 0.05).unwrap());
|
||||
assert!(!tree.resize_window(WindowId(1), Direction::Right, f64::NAN).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stacking_windows_shows_only_the_active_child() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1366,8 +1235,7 @@ mod tests {
|
||||
#[test]
|
||||
fn activating_a_hidden_stack_child_changes_visible_geometry() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1382,8 +1250,7 @@ mod tests {
|
||||
#[test]
|
||||
fn removing_from_a_stack_collapses_or_preserves_cardinality() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.stack_window(WindowId(2), WindowId(1)).unwrap();
|
||||
@@ -1393,10 +1260,7 @@ mod tests {
|
||||
tree.remove(WindowId(3)).unwrap();
|
||||
tree.remove(WindowId(2)).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tree.node(tree.root().unwrap()),
|
||||
Some(&Node::Window(WindowId(1)))
|
||||
);
|
||||
assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(1))));
|
||||
assert!(tree.validate().is_ok());
|
||||
}
|
||||
|
||||
@@ -1409,9 +1273,7 @@ mod tests {
|
||||
let bounds = Rect::new(0.0, 0.0, 1_920.0, 1_080.0);
|
||||
|
||||
for _ in 0..2_000 {
|
||||
random = random
|
||||
.wrapping_mul(6_364_136_223_846_793_005)
|
||||
.wrapping_add(1);
|
||||
random = random.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
|
||||
let operation = random % 6;
|
||||
|
||||
match operation {
|
||||
@@ -1425,8 +1287,7 @@ mod tests {
|
||||
Axis::Vertical
|
||||
};
|
||||
|
||||
tree.insert(window, focused, axis, ratio_from_random(random))
|
||||
.unwrap();
|
||||
tree.insert(window, focused, axis, ratio_from_random(random)).unwrap();
|
||||
windows.push(window);
|
||||
}
|
||||
1 if !windows.is_empty() => {
|
||||
@@ -1453,8 +1314,7 @@ mod tests {
|
||||
}
|
||||
5 if !windows.is_empty() => {
|
||||
let window = windows[random as usize % windows.len()];
|
||||
tree.resize_window(window, random_direction(random), 0.03)
|
||||
.unwrap();
|
||||
tree.resize_window(window, random_direction(random), 0.03).unwrap();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1466,8 +1326,7 @@ mod tests {
|
||||
#[test]
|
||||
fn constraints_adjust_split_boundaries_when_space_is_available() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.2)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.2).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.2)
|
||||
.unwrap();
|
||||
let constraints = HashMap::from([
|
||||
@@ -1507,8 +1366,7 @@ mod tests {
|
||||
#[test]
|
||||
fn focused_window_wins_an_impossible_minimum_constraint_conflict() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
let constraints = HashMap::from([
|
||||
@@ -1550,8 +1408,7 @@ mod tests {
|
||||
#[test]
|
||||
fn maximum_constraints_clip_without_overlapping_neighbors() {
|
||||
let mut tree = LayoutTree::default();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5)
|
||||
.unwrap();
|
||||
tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap();
|
||||
let constraints = HashMap::from([(
|
||||
WindowId(1),
|
||||
SizeConstraints {
|
||||
@@ -1569,10 +1426,7 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
result.geometry[&WindowId(1)],
|
||||
Rect::new(4.0, 4.0, 60.0, 40.0)
|
||||
);
|
||||
assert_eq!(result.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 60.0, 40.0));
|
||||
assert_eq!(result.warnings.len(), 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect,
|
||||
SizeConstraints, WindowId, normalized_constraints, record_minimum_warnings,
|
||||
ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect, SizeConstraints,
|
||||
WindowId, normalized_constraints, record_minimum_warnings,
|
||||
};
|
||||
|
||||
const DEFAULT_WIDTH: f64 = 0.5;
|
||||
@@ -145,16 +145,10 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn window_ids(&self) -> impl Iterator<Item = WindowId> + '_ {
|
||||
self.columns
|
||||
.iter()
|
||||
.flat_map(|column| column.windows.iter().copied())
|
||||
self.columns.iter().flat_map(|column| column.windows.iter().copied())
|
||||
}
|
||||
|
||||
pub fn insert(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
focused: Option<WindowId>,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn insert(&mut self, window: WindowId, focused: Option<WindowId>) -> Result<(), LayoutError> {
|
||||
if self.contains(window) {
|
||||
return Err(LayoutError::DuplicateWindow(window));
|
||||
}
|
||||
@@ -163,8 +157,7 @@ impl ScrollingLayout {
|
||||
.and_then(|focused| self.window_location(focused))
|
||||
.map(|(column, _)| column + 1)
|
||||
.unwrap_or(self.columns.len());
|
||||
self.columns
|
||||
.insert(index, Column::new(window, self.default_width));
|
||||
self.columns.insert(index, Column::new(window, self.default_width));
|
||||
self.active_column = Some(index);
|
||||
self.reveal_pending = (self.columns.len() > 1).then_some(window);
|
||||
|
||||
@@ -173,12 +166,9 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column_index, window_index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column_index, window_index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let column = &mut self.columns[column_index];
|
||||
let removed_active_column =
|
||||
self.active_column == Some(column_index) && column.windows.len() == 1;
|
||||
let removed_active_column = self.active_column == Some(column_index) && column.windows.len() == 1;
|
||||
column.windows.remove(window_index);
|
||||
column.heights.remove(window_index);
|
||||
|
||||
@@ -206,9 +196,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn focus(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column, index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let changed_column = self.active_column != Some(column);
|
||||
self.active_column = Some(column);
|
||||
self.columns[column].active = index;
|
||||
@@ -221,9 +209,7 @@ impl ScrollingLayout {
|
||||
/// Hover changes the active window without requesting viewport movement.
|
||||
/// Keep any earlier explicit reveal attached to its original window.
|
||||
pub fn focus_without_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (column, index) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
self.active_column = Some(column);
|
||||
self.columns[column].active = index;
|
||||
Ok(())
|
||||
@@ -237,11 +223,7 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn slide_focus_from(
|
||||
&mut self,
|
||||
previous: WindowId,
|
||||
focused: WindowId,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn slide_focus_from(&mut self, previous: WindowId, focused: WindowId) -> Result<(), LayoutError> {
|
||||
self.window_location(previous)
|
||||
.ok_or(LayoutError::UnknownWindow(previous))?;
|
||||
self.focus_without_reveal(focused)?;
|
||||
@@ -250,16 +232,11 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn move_into_column(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
target: WindowId,
|
||||
) -> Result<(), LayoutError> {
|
||||
pub fn move_into_column(&mut self, window: WindowId, target: WindowId) -> Result<(), LayoutError> {
|
||||
if window == target {
|
||||
return Ok(());
|
||||
}
|
||||
self.window_location(target)
|
||||
.ok_or(LayoutError::UnknownWindow(target))?;
|
||||
self.window_location(target).ok_or(LayoutError::UnknownWindow(target))?;
|
||||
self.remove(window)?;
|
||||
let (target_column, target_index) = self
|
||||
.window_location(target)
|
||||
@@ -278,9 +255,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
|
||||
pub fn extract_to_column(&mut self, window: WindowId) -> Result<(), LayoutError> {
|
||||
let (source, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (source, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
if self.columns[source].windows.len() == 1 {
|
||||
self.active_column = Some(source);
|
||||
return Ok(());
|
||||
@@ -288,8 +263,7 @@ impl ScrollingLayout {
|
||||
|
||||
self.remove(window)?;
|
||||
let insertion = (source + 1).min(self.columns.len());
|
||||
self.columns
|
||||
.insert(insertion, Column::new(window, self.default_width));
|
||||
self.columns.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = self.active_window();
|
||||
|
||||
@@ -297,32 +271,16 @@ impl ScrollingLayout {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
width: ColumnWidth,
|
||||
) -> Result<(), LayoutError> {
|
||||
let (column, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
pub fn set_column_width(&mut self, window: WindowId, width: ColumnWidth) -> Result<(), LayoutError> {
|
||||
let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
self.columns[column].width = normalized_width(width);
|
||||
self.reveal_pending = self.active_window();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn cycle_column_width(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
presets: &[ColumnWidth],
|
||||
) -> Result<bool, LayoutError> {
|
||||
let (column, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let presets = presets
|
||||
.iter()
|
||||
.copied()
|
||||
.map(normalized_width)
|
||||
.collect::<Vec<_>>();
|
||||
pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result<bool, LayoutError> {
|
||||
let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let presets = presets.iter().copied().map(normalized_width).collect::<Vec<_>>();
|
||||
if presets.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -365,35 +323,22 @@ impl ScrollingLayout {
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
) -> Result<Option<WindowId>, LayoutError> {
|
||||
let (column, row) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let neighbor = match direction {
|
||||
Direction::Up => row
|
||||
.checked_sub(1)
|
||||
.map(|row| self.columns[column].windows[row]),
|
||||
Direction::Up => row.checked_sub(1).map(|row| self.columns[column].windows[row]),
|
||||
Direction::Down => self.columns[column].windows.get(row + 1).copied(),
|
||||
Direction::Left => column.checked_sub(1).map(|column| {
|
||||
let column = &self.columns[column];
|
||||
column.windows[column.active]
|
||||
}),
|
||||
Direction::Right => self
|
||||
.columns
|
||||
.get(column + 1)
|
||||
.map(|column| column.windows[column.active]),
|
||||
Direction::Right => self.columns.get(column + 1).map(|column| column.windows[column.active]),
|
||||
};
|
||||
|
||||
Ok(neighbor)
|
||||
}
|
||||
|
||||
pub fn move_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
) -> Result<bool, LayoutError> {
|
||||
let (column, row) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
pub fn move_window(&mut self, window: WindowId, direction: Direction) -> Result<bool, LayoutError> {
|
||||
let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
|
||||
match direction {
|
||||
Direction::Up if row > 0 => {
|
||||
@@ -407,9 +352,7 @@ impl ScrollingLayout {
|
||||
self.columns[column].active = row + 1;
|
||||
}
|
||||
Direction::Left if column > 0 => self.move_horizontally(window, column - 1)?,
|
||||
Direction::Right if column + 1 < self.columns.len() => {
|
||||
self.move_horizontally(window, column + 1)?
|
||||
}
|
||||
Direction::Right if column + 1 < self.columns.len() => self.move_horizontally(window, column + 1)?,
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
@@ -417,20 +360,9 @@ impl ScrollingLayout {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn resize_window(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
direction: Direction,
|
||||
amount: f64,
|
||||
) -> Result<bool, LayoutError> {
|
||||
let (column, row) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let amount = if amount.is_finite() {
|
||||
amount.abs()
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result<bool, LayoutError> {
|
||||
let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
let amount = if amount.is_finite() { amount.abs() } else { 0.0 };
|
||||
if amount == 0.0 {
|
||||
return Ok(false);
|
||||
}
|
||||
@@ -441,18 +373,12 @@ impl ScrollingLayout {
|
||||
ColumnWidth::Proportion(value) => value,
|
||||
ColumnWidth::Fixed(_) | ColumnWidth::Full => DEFAULT_WIDTH,
|
||||
};
|
||||
let adjustment = if direction == Direction::Right {
|
||||
amount
|
||||
} else {
|
||||
-amount
|
||||
};
|
||||
let adjustment = if direction == Direction::Right { amount } else { -amount };
|
||||
let resized = (current + adjustment).clamp(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH);
|
||||
self.columns[column].width = ColumnWidth::Proportion(resized);
|
||||
resized != current
|
||||
}
|
||||
Direction::Up if row > 0 => {
|
||||
resize_rows(&mut self.columns[column], row, row - 1, amount)
|
||||
}
|
||||
Direction::Up if row > 0 => resize_rows(&mut self.columns[column], row, row - 1, amount),
|
||||
Direction::Down if row + 1 < self.columns[column].windows.len() => {
|
||||
resize_rows(&mut self.columns[column], row, row + 1, amount)
|
||||
}
|
||||
@@ -496,9 +422,7 @@ impl ScrollingLayout {
|
||||
|
||||
for column in &self.columns {
|
||||
let requested = match normalized_width(column.width) {
|
||||
ColumnWidth::Proportion(proportion) => {
|
||||
((viewport_width + inner) * proportion - inner).max(1.0)
|
||||
}
|
||||
ColumnWidth::Proportion(proportion) => ((viewport_width + inner) * proportion - inner).max(1.0),
|
||||
ColumnWidth::Fixed(width) => width,
|
||||
ColumnWidth::Full => viewport_width,
|
||||
};
|
||||
@@ -526,18 +450,14 @@ impl ScrollingLayout {
|
||||
self.reveal_pending = None;
|
||||
self.swipe_focus_pending = None;
|
||||
} else if let Some((previous, focused)) = self.swipe_focus_pending.take() {
|
||||
if let (Some((from, _)), Some((to, _))) = (
|
||||
self.window_location(previous),
|
||||
self.window_location(focused),
|
||||
) {
|
||||
if let (Some((from, _)), Some((to, _))) = (self.window_location(previous), self.window_location(focused)) {
|
||||
let last_end = column_positions
|
||||
.last()
|
||||
.map(|(start, width)| start + width)
|
||||
.unwrap_or(0.0);
|
||||
let max_offset = (last_end - viewport_width).max(0.0);
|
||||
self.viewport_x = (self.viewport_x + column_positions[to].0
|
||||
- column_positions[from].0)
|
||||
.clamp(0.0, max_offset);
|
||||
self.viewport_x =
|
||||
(self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset);
|
||||
}
|
||||
self.reveal_pending = None;
|
||||
} else {
|
||||
@@ -554,9 +474,7 @@ impl ScrollingLayout {
|
||||
let mut result = LayoutResult::default();
|
||||
for (column_index, column) in self.columns.iter().enumerate() {
|
||||
let (column_x, column_width) = column_positions[column_index];
|
||||
let available_height = (viewport_height
|
||||
- inner * (column.windows.len().saturating_sub(1) as f64))
|
||||
.max(1.0);
|
||||
let available_height = (viewport_height - inner * (column.windows.len().saturating_sub(1) as f64)).max(1.0);
|
||||
let mut y = bounds.y + outer;
|
||||
|
||||
for (window_index, window) in column.windows.iter().enumerate() {
|
||||
@@ -615,12 +533,7 @@ impl ScrollingLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn paged_viewport(
|
||||
&self,
|
||||
positions: &[(f64, f64)],
|
||||
viewport_width: f64,
|
||||
active: Option<usize>,
|
||||
) -> Option<f64> {
|
||||
fn paged_viewport(&self, positions: &[(f64, f64)], viewport_width: f64, active: Option<usize>) -> Option<f64> {
|
||||
if self.focus_strategy != ViewportFocusStrategy::Paged {
|
||||
return None;
|
||||
}
|
||||
@@ -671,14 +584,8 @@ impl ScrollingLayout {
|
||||
}
|
||||
}
|
||||
|
||||
fn move_horizontally(
|
||||
&mut self,
|
||||
window: WindowId,
|
||||
destination: usize,
|
||||
) -> Result<(), LayoutError> {
|
||||
let (source, _) = self
|
||||
.window_location(window)
|
||||
.ok_or(LayoutError::UnknownWindow(window))?;
|
||||
fn move_horizontally(&mut self, window: WindowId, destination: usize) -> Result<(), LayoutError> {
|
||||
let (source, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
|
||||
if self.columns[source].windows.len() == 1 {
|
||||
self.columns.swap(source, destination);
|
||||
self.active_column = Some(destination);
|
||||
@@ -686,15 +593,10 @@ impl ScrollingLayout {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let insertion = if destination < source {
|
||||
source
|
||||
} else {
|
||||
source + 1
|
||||
};
|
||||
let insertion = if destination < source { source } else { source + 1 };
|
||||
self.remove(window)?;
|
||||
let insertion = insertion.min(self.columns.len());
|
||||
self.columns
|
||||
.insert(insertion, Column::new(window, self.default_width));
|
||||
self.columns.insert(insertion, Column::new(window, self.default_width));
|
||||
self.active_column = Some(insertion);
|
||||
self.reveal_pending = self.active_window();
|
||||
Ok(())
|
||||
@@ -711,31 +613,20 @@ impl ScrollingLayout {
|
||||
|
||||
fn validate_scalars(&self) -> Result<(), LayoutError> {
|
||||
if !self.viewport_x.is_finite() {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling viewport offset is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling viewport offset is invalid"));
|
||||
}
|
||||
if normalized_width(self.default_width) != self.default_width {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling default column width is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling default column width is invalid"));
|
||||
}
|
||||
if self.columns.is_empty() {
|
||||
return if self.active_column.is_none() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(LayoutError::InvalidTree(
|
||||
"empty scrolling layout has an active column",
|
||||
))
|
||||
Err(LayoutError::InvalidTree("empty scrolling layout has an active column"))
|
||||
};
|
||||
}
|
||||
if self
|
||||
.active_column
|
||||
.is_none_or(|active| active >= self.columns.len())
|
||||
{
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling active column is invalid",
|
||||
));
|
||||
if self.active_column.is_none_or(|active| active >= self.columns.len()) {
|
||||
return Err(LayoutError::InvalidTree("scrolling active column is invalid"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -746,24 +637,15 @@ impl ScrollingLayout {
|
||||
let mut windows = HashSet::new();
|
||||
for column in &self.columns {
|
||||
if normalized_width(column.width) != column.width {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling column width is invalid",
|
||||
));
|
||||
return Err(LayoutError::InvalidTree("scrolling column width is invalid"));
|
||||
}
|
||||
if column.windows.is_empty() {
|
||||
return Err(LayoutError::InvalidTree("scrolling column is empty"));
|
||||
}
|
||||
if column.active >= column.windows.len() || column.heights.len() != column.windows.len()
|
||||
{
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling column metadata is invalid",
|
||||
));
|
||||
if column.active >= column.windows.len() || column.heights.len() != column.windows.len() {
|
||||
return Err(LayoutError::InvalidTree("scrolling column metadata is invalid"));
|
||||
}
|
||||
if !column
|
||||
.heights
|
||||
.iter()
|
||||
.all(|height| height.is_finite() && *height > 0.0)
|
||||
{
|
||||
if !column.heights.iter().all(|height| height.is_finite() && *height > 0.0) {
|
||||
return Err(LayoutError::InvalidTree("scrolling height is invalid"));
|
||||
}
|
||||
for window in &column.windows {
|
||||
@@ -790,9 +672,7 @@ fn resize_rows(column: &mut Column, row: usize, neighbor: usize, amount: f64) ->
|
||||
|
||||
fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
||||
match width {
|
||||
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => {
|
||||
ColumnWidth::Proportion(value)
|
||||
}
|
||||
ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => ColumnWidth::Proportion(value),
|
||||
ColumnWidth::Fixed(value) if value.is_finite() && value > 0.0 => ColumnWidth::Fixed(value),
|
||||
ColumnWidth::Full => ColumnWidth::Full,
|
||||
_ => ColumnWidth::default(),
|
||||
@@ -800,11 +680,7 @@ fn normalized_width(width: ColumnWidth) -> ColumnWidth {
|
||||
}
|
||||
|
||||
fn finite_nonnegative(value: f64) -> f64 {
|
||||
if value.is_finite() {
|
||||
value.max(0.0)
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
if value.is_finite() { value.max(0.0) } else { 0.0 }
|
||||
}
|
||||
|
||||
fn apply_maximums(
|
||||
@@ -866,12 +742,7 @@ mod tests {
|
||||
layout.insert(window(2), Some(window(1))).unwrap();
|
||||
|
||||
assert_eq!(layout.default_width(), ColumnWidth::Full);
|
||||
assert!(
|
||||
layout
|
||||
.columns()
|
||||
.iter()
|
||||
.all(|column| column.width == ColumnWidth::Full)
|
||||
);
|
||||
assert!(layout.columns().iter().all(|column| column.width == ColumnWidth::Full));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -920,9 +791,7 @@ mod tests {
|
||||
fn focus_reveal_scrolls_only_as_far_as_needed() {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
for id in 1..=4 {
|
||||
layout
|
||||
.insert(window(id), Some(window(id.saturating_sub(1))))
|
||||
.unwrap();
|
||||
layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
let gaps = GapConfig {
|
||||
@@ -961,14 +830,8 @@ mod tests {
|
||||
|
||||
assert_eq!(layout.viewport_x(), 0.0);
|
||||
assert_eq!(first.geometry, second.geometry);
|
||||
assert_eq!(
|
||||
second.geometry[&window(1)],
|
||||
Rect::new(10.0, 10.0, 485.0, 780.0)
|
||||
);
|
||||
assert_eq!(
|
||||
second.geometry[&window(2)],
|
||||
Rect::new(505.0, 10.0, 485.0, 780.0)
|
||||
);
|
||||
assert_eq!(second.geometry[&window(1)], Rect::new(10.0, 10.0, 485.0, 780.0));
|
||||
assert_eq!(second.geometry[&window(2)], Rect::new(505.0, 10.0, 485.0, 780.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1037,13 +900,9 @@ mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(layout.viewport_x(), 0.0);
|
||||
|
||||
for (previous, focused, expected) in
|
||||
[(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)]
|
||||
{
|
||||
for (previous, focused, expected) in [(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)] {
|
||||
layout.focus_and_reveal(window(focused)).unwrap();
|
||||
layout
|
||||
.slide_focus_from(window(previous), window(focused))
|
||||
.unwrap();
|
||||
layout.slide_focus_from(window(previous), window(focused)).unwrap();
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
|
||||
.unwrap();
|
||||
@@ -1071,9 +930,7 @@ mod tests {
|
||||
#[test]
|
||||
fn paged_focus_preserves_explicit_center_until_focus_changes() {
|
||||
let (mut layout, bounds, gaps) = paged_layout(4);
|
||||
layout
|
||||
.center_window(window(2), bounds, gaps, &HashMap::new())
|
||||
.unwrap();
|
||||
layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap();
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||
.unwrap();
|
||||
@@ -1188,18 +1045,14 @@ mod tests {
|
||||
};
|
||||
|
||||
for id in 1..=3 {
|
||||
layout
|
||||
.insert(window(id), (id > 1).then(|| window(id - 1)))
|
||||
.unwrap();
|
||||
layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id)))
|
||||
.unwrap();
|
||||
}
|
||||
let before = layout.viewport_x();
|
||||
|
||||
layout
|
||||
.resize_window(window(3), Direction::Right, 0.5)
|
||||
.unwrap();
|
||||
layout.resize_window(window(3), Direction::Right, 0.5).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
|
||||
.unwrap();
|
||||
@@ -1345,18 +1198,10 @@ mod tests {
|
||||
layout.insert(window(2), Some(window(1))).unwrap();
|
||||
layout.move_into_column(window(2), window(1)).unwrap();
|
||||
|
||||
assert!(
|
||||
layout
|
||||
.resize_window(window(1), Direction::Right, 0.1)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(layout.resize_window(window(1), Direction::Right, 0.1).unwrap());
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.6));
|
||||
|
||||
assert!(
|
||||
layout
|
||||
.resize_window(window(1), Direction::Down, 0.1)
|
||||
.unwrap()
|
||||
);
|
||||
assert!(layout.resize_window(window(1), Direction::Down, 0.1).unwrap());
|
||||
assert_eq!(layout.columns()[0].heights, vec![0.6, 0.4]);
|
||||
}
|
||||
|
||||
@@ -1371,10 +1216,7 @@ mod tests {
|
||||
];
|
||||
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
assert_eq!(
|
||||
layout.columns()[0].width,
|
||||
ColumnWidth::Proportion(2.0 / 3.0)
|
||||
);
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(2.0 / 3.0));
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
assert_eq!(layout.columns()[0].width, ColumnWidth::Full);
|
||||
assert!(layout.cycle_column_width(window(1), &presets).unwrap());
|
||||
@@ -1459,10 +1301,7 @@ mod tests {
|
||||
let mut layout = ScrollingLayout::default();
|
||||
for id in 1_u64..=4 {
|
||||
layout
|
||||
.insert(
|
||||
window(id),
|
||||
id.checked_sub(1).filter(|id| *id > 0).map(window),
|
||||
)
|
||||
.insert(window(id), id.checked_sub(1).filter(|id| *id > 0).map(window))
|
||||
.unwrap();
|
||||
}
|
||||
let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0);
|
||||
@@ -1475,11 +1314,7 @@ mod tests {
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
layout
|
||||
.center_window(window(2), bounds, gaps, &HashMap::new())
|
||||
.unwrap()
|
||||
);
|
||||
assert!(layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap());
|
||||
let result = layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
|
||||
.unwrap();
|
||||
@@ -1518,9 +1353,7 @@ mod tests {
|
||||
if first == second {
|
||||
second = (second + 1) % windows.len();
|
||||
}
|
||||
layout
|
||||
.move_into_column(windows[first], windows[second])
|
||||
.unwrap();
|
||||
layout.move_into_column(windows[first], windows[second]).unwrap();
|
||||
}
|
||||
3 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
@@ -1534,9 +1367,7 @@ mod tests {
|
||||
5 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
let direction = random_direction(random.rotate_left(23));
|
||||
layout
|
||||
.resize_window(windows[index], direction, 0.05)
|
||||
.unwrap();
|
||||
layout.resize_window(windows[index], direction, 0.05).unwrap();
|
||||
}
|
||||
6 if !windows.is_empty() => {
|
||||
let index = random as usize % windows.len();
|
||||
@@ -1546,10 +1377,7 @@ mod tests {
|
||||
}
|
||||
|
||||
layout.validate().unwrap();
|
||||
assert_eq!(
|
||||
layout.window_ids().collect::<HashSet<_>>().len(),
|
||||
windows.len()
|
||||
);
|
||||
assert_eq!(layout.window_ids().collect::<HashSet<_>>().len(), windows.len());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -195,15 +195,11 @@ pub mod window_capture {
|
||||
use wayland_client::protocol::__interfaces::*;
|
||||
use wayland_protocols_wlr::screencopy::v1::client::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_interfaces!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_client_code!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_client_code!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
@@ -216,15 +212,11 @@ pub mod window_capture {
|
||||
use wayland_protocols_wlr::screencopy::v1::server::__interfaces::*;
|
||||
use wayland_server::protocol::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_interfaces!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
use self::__interfaces::*;
|
||||
|
||||
wayland_scanner::generate_server_code!(
|
||||
"../../protocols/ferese-window-capture-v1.xml"
|
||||
);
|
||||
wayland_scanner::generate_server_code!("../../protocols/ferese-window-capture-v1.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use cosmic::iced::{Background, Color};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::button;
|
||||
|
||||
use crate::{composite, foreground};
|
||||
use cosmic::{
|
||||
iced::{Background, Color},
|
||||
theme,
|
||||
widget::button,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum State {
|
||||
@@ -13,11 +12,7 @@ pub(crate) enum State {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
pub(crate) fn accent_button_style(
|
||||
theme: &cosmic::Theme,
|
||||
state: State,
|
||||
focused: bool,
|
||||
) -> button::Style {
|
||||
pub(crate) fn accent_button_style(theme: &cosmic::Theme, state: State, focused: bool) -> button::Style {
|
||||
let native = theme.cosmic();
|
||||
let component = &native.accent_button;
|
||||
let fill: Color = match state {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
//! Date/time labels and a calendar grid; callers own navigation and popup behavior.
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Alignment, Background, Border, Length},
|
||||
theme,
|
||||
widget::{column, container, row},
|
||||
};
|
||||
use cosmic::iced::{Alignment, Background, Border, Length};
|
||||
use cosmic::widget::{column, container, row};
|
||||
use cosmic::{Element, theme};
|
||||
use jiff::Zoned;
|
||||
|
||||
pub fn format_bar_time(now: &Zoned) -> String {
|
||||
@@ -41,10 +38,7 @@ pub fn stacked_digits(label: &str) -> Option<[String; 2]> {
|
||||
let (hour, minute) = time.split_once(':')?;
|
||||
if !(1..=2).contains(&hour.len())
|
||||
|| minute.len() != 2
|
||||
|| !hour
|
||||
.bytes()
|
||||
.chain(minute.bytes())
|
||||
.all(|digit| digit.is_ascii_digit())
|
||||
|| !hour.bytes().chain(minute.bytes()).all(|digit| digit.is_ascii_digit())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -104,13 +98,9 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
let mut weekdays = row::with_capacity(7).spacing(3);
|
||||
for label in ["M", "T", "W", "T", "F", "S", "S"] {
|
||||
weekdays = weekdays.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.width(30)
|
||||
.center_x(30),
|
||||
container(crate::text(label, font).size(11).class(theme::Text::Color(muted)))
|
||||
.width(30)
|
||||
.center_x(30),
|
||||
);
|
||||
}
|
||||
grid = grid.push(weekdays);
|
||||
@@ -118,26 +108,21 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
let mut dates = row::with_capacity(7).spacing(3);
|
||||
for weekday in 0..7 {
|
||||
let cell = week * 7 + weekday;
|
||||
let day = cell
|
||||
.checked_sub(start)
|
||||
.map(|day| day + 1)
|
||||
.filter(|day| *day <= days);
|
||||
let is_today =
|
||||
year == today.year() && month == today.month() && day == Some(today.day() as usize);
|
||||
let day = cell.checked_sub(start).map(|day| day + 1).filter(|day| *day <= days);
|
||||
let is_today = year == today.year() && month == today.month() && day == Some(today.day() as usize);
|
||||
let label = day.map_or(String::new(), |day| day.to_string());
|
||||
dates = dates.push(
|
||||
container(
|
||||
crate::text(label, font)
|
||||
.size(12)
|
||||
.class(theme::Text::Color(if is_today { primary } else { muted })),
|
||||
)
|
||||
container(crate::text(label, font).size(12).class(theme::Text::Color(if is_today {
|
||||
primary
|
||||
} else {
|
||||
muted
|
||||
})))
|
||||
.width(30)
|
||||
.height(25)
|
||||
.center_x(30)
|
||||
.center_y(25)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: is_today
|
||||
.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))),
|
||||
border: Border {
|
||||
radius: palette.radius.min(12.).into(),
|
||||
..Default::default()
|
||||
@@ -155,14 +140,8 @@ pub fn grid<'a, M: Clone + 'a>(
|
||||
mod tests {
|
||||
#[test]
|
||||
fn pixel_clock_stacks_only_hour_and_minute_formats() {
|
||||
assert_eq!(
|
||||
super::stacked_digits("2:45 pm"),
|
||||
Some(["02".into(), "45".into()])
|
||||
);
|
||||
assert_eq!(
|
||||
super::stacked_digits("14:45"),
|
||||
Some(["14".into(), "45".into()])
|
||||
);
|
||||
assert_eq!(super::stacked_digits("2:45 pm"), Some(["02".into(), "45".into()]));
|
||||
assert_eq!(super::stacked_digits("14:45"), Some(["14".into(), "45".into()]));
|
||||
assert!(super::stacked_digits("14:45:30").is_none());
|
||||
assert!(super::stacked_digits("Today 14:45").is_none());
|
||||
assert!(super::stacked_digits("14:45 pm extra").is_none());
|
||||
@@ -171,18 +150,9 @@ mod tests {
|
||||
#[test]
|
||||
fn clock_uses_lowercase_date_and_twelve_hour_time() {
|
||||
for (stamp, expected) in [
|
||||
(
|
||||
"2026-09-28T21:32:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 9:32 pm",
|
||||
),
|
||||
(
|
||||
"2026-09-28T00:05:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 12:05 am",
|
||||
),
|
||||
(
|
||||
"2026-09-28T12:00:00+01:00[Africa/Lagos]",
|
||||
"28 sept, 12:00 pm",
|
||||
),
|
||||
("2026-09-28T21:32:00+01:00[Africa/Lagos]", "28 sept, 9:32 pm"),
|
||||
("2026-09-28T00:05:00+01:00[Africa/Lagos]", "28 sept, 12:05 am"),
|
||||
("2026-09-28T12:00:00+01:00[Africa/Lagos]", "28 sept, 12:00 pm"),
|
||||
] {
|
||||
assert_eq!(super::format_bar_time(&stamp.parse().unwrap()), expected);
|
||||
}
|
||||
@@ -194,10 +164,7 @@ mod tests {
|
||||
let (first, start, days) = super::month(february, 0);
|
||||
assert_eq!((first.year(), first.month(), start, days), (2024, 2, 3, 29));
|
||||
let (first, start, days) = super::month(february, -2);
|
||||
assert_eq!(
|
||||
(first.year(), first.month(), start, days),
|
||||
(2023, 12, 4, 31)
|
||||
);
|
||||
assert_eq!((first.year(), first.month(), start, days), (2023, 12, 4, 31));
|
||||
let june = jiff::civil::Date::new(2025, 6, 1).unwrap();
|
||||
let (_, start, days) = super::month(june, 0);
|
||||
assert_eq!((start, days), (6, 30));
|
||||
|
||||
@@ -76,27 +76,12 @@ pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) {
|
||||
let surface: Color = native.primary(false).base.into();
|
||||
for component in [&mut native.accent, &mut native.accent_button] {
|
||||
let (base, on) = accent_pair(composite(component.base.into(), surface), preferred);
|
||||
component.hover = rgba(readable_fill(
|
||||
composite(component.hover.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.pressed = rgba(readable_fill(
|
||||
composite(component.pressed.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.selected = rgba(readable_fill(
|
||||
composite(component.selected.into(), surface),
|
||||
base,
|
||||
on,
|
||||
));
|
||||
component.hover = rgba(readable_fill(composite(component.hover.into(), surface), base, on));
|
||||
component.pressed = rgba(readable_fill(composite(component.pressed.into(), surface), base, on));
|
||||
component.selected = rgba(readable_fill(composite(component.selected.into(), surface), base, on));
|
||||
component.base = rgba(base);
|
||||
component.on = rgba(on);
|
||||
component.selected_text = rgba(foreground(
|
||||
composite(component.selected.into(), surface),
|
||||
on,
|
||||
));
|
||||
component.selected_text = rgba(foreground(composite(component.selected.into(), surface), on));
|
||||
component.disabled = rgba(composite(Color { a: 0.5, ..base }, surface));
|
||||
component.on_disabled = rgba(foreground(composite(Color { a: 0.5, ..base }, surface), on));
|
||||
}
|
||||
@@ -125,9 +110,10 @@ fn rgba(c: Color) -> cosmic::cosmic_theme::palette::Srgba {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use cosmic::iced::Background;
|
||||
|
||||
use super::*;
|
||||
use crate::button::{State, accent_button_style};
|
||||
use cosmic::iced::Background;
|
||||
#[test]
|
||||
fn reference_contrast_values_use_linear_srgb() {
|
||||
assert!((contrast(Color::BLACK, Color::WHITE) - 21.).abs() < 0.00001);
|
||||
@@ -139,10 +125,7 @@ mod tests {
|
||||
let preferred = Color::from_rgb8(240, 242, 246);
|
||||
assert_eq!(foreground(Color::BLACK, preferred), preferred);
|
||||
assert_eq!(foreground(Color::WHITE, preferred), Color::BLACK);
|
||||
assert_eq!(
|
||||
foreground(Color::from_rgb8(61, 123, 230), preferred),
|
||||
Color::BLACK
|
||||
);
|
||||
assert_eq!(foreground(Color::from_rgb8(61, 123, 230), preferred), Color::BLACK);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -170,12 +153,7 @@ mod tests {
|
||||
.build();
|
||||
apply(&mut native, preferred);
|
||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||
for state in [
|
||||
State::Active,
|
||||
State::Hovered,
|
||||
State::Pressed,
|
||||
State::Disabled,
|
||||
] {
|
||||
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
|
||||
let actual = accent_button_style(&theme, state, false);
|
||||
assert_eq!(actual.text_color, Some(preferred));
|
||||
assert_eq!(actual.icon_color, Some(preferred));
|
||||
@@ -223,20 +201,11 @@ mod tests {
|
||||
for fill in [component.base, component.hover, component.pressed] {
|
||||
assert!(contrast(fill.into(), component.on.into()) >= 4.5);
|
||||
}
|
||||
assert!(
|
||||
contrast(component.selected.into(), component.selected_text.into()) >= 4.5
|
||||
);
|
||||
assert!(
|
||||
contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5
|
||||
);
|
||||
assert!(contrast(component.selected.into(), component.selected_text.into()) >= 4.5);
|
||||
assert!(contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5);
|
||||
}
|
||||
let theme = cosmic::Theme::custom(std::sync::Arc::new(native));
|
||||
for state in [
|
||||
State::Active,
|
||||
State::Hovered,
|
||||
State::Pressed,
|
||||
State::Disabled,
|
||||
] {
|
||||
for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] {
|
||||
let style = accent_button_style(&theme, state, false);
|
||||
let Some(Background::Color(fill)) = style.background else {
|
||||
panic!("No fill")
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use cosmic::iced::{Background, Border, Color, Vector};
|
||||
use cosmic::widget::{button, container};
|
||||
use cosmic::{theme, widget};
|
||||
|
||||
use crate::{Palette, mix};
|
||||
use cosmic::{
|
||||
iced::{Background, Border, Color, Vector},
|
||||
theme,
|
||||
widget::{self, button, container},
|
||||
};
|
||||
|
||||
pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
|
||||
theme::Container::custom(move |_| surface_appearance(background, radius))
|
||||
@@ -85,11 +84,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button
|
||||
icon_color: Some(on),
|
||||
border_radius: p.radius.min(9.).into(),
|
||||
border_width: if selected { 1. } else { 0. },
|
||||
border_color: if selected {
|
||||
p.accent
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
},
|
||||
border_color: if selected { p.accent } else { Color::TRANSPARENT },
|
||||
outline_width: 0.,
|
||||
outline_color: Color::TRANSPARENT,
|
||||
overlay: None,
|
||||
@@ -136,11 +131,7 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lock_input(
|
||||
radius: f32,
|
||||
accent: cosmic::iced::Color,
|
||||
surface: cosmic::iced::Color,
|
||||
) -> theme::TextInput {
|
||||
pub fn lock_input(radius: f32, accent: cosmic::iced::Color, surface: cosmic::iced::Color) -> theme::TextInput {
|
||||
let appearance = move |focused: bool| widget::text_input::Appearance {
|
||||
background: cosmic::iced::Color::from_rgba(
|
||||
(surface.r + 1.) * 0.5,
|
||||
@@ -190,9 +181,7 @@ pub fn shell_button(
|
||||
a: if pressed {
|
||||
0.20 * opacity
|
||||
} else {
|
||||
((if selected { 0.14 } else { 0.0 })
|
||||
+ progress * if selected { 0.02 } else { 0.08 })
|
||||
* opacity
|
||||
((if selected { 0.14 } else { 0.0 }) + progress * if selected { 0.02 } else { 0.08 }) * opacity
|
||||
},
|
||||
..foreground
|
||||
})),
|
||||
@@ -211,12 +200,7 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notification_button(
|
||||
foreground: Color,
|
||||
hover: Color,
|
||||
radius: f32,
|
||||
filled: bool,
|
||||
) -> theme::Button {
|
||||
pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button {
|
||||
let style = move |active: bool| button::Style {
|
||||
text_color: Some(foreground),
|
||||
icon_color: Some(foreground),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Canonical SVG assets, sizing and semantic tint rules for native Ferese UIs.
|
||||
use cosmic::{
|
||||
iced::{Color, ContentFit, Length},
|
||||
theme,
|
||||
widget::icon,
|
||||
};
|
||||
use cosmic::iced::{Color, ContentFit, Length};
|
||||
use cosmic::theme;
|
||||
use cosmic::widget::icon;
|
||||
|
||||
pub const ARROW: &[u8] = include_bytes!("../assets/icons/arrow.svg");
|
||||
pub const BATTERY: &[u8] = include_bytes!("../assets/icons/battery.svg");
|
||||
@@ -18,8 +16,7 @@ pub const BATTERY_75: &[u8] = include_bytes!("../assets/icons/status/battery-75.
|
||||
pub const BATTERY_CHARGING: &[u8] = include_bytes!("../assets/icons/status/battery-charging.svg");
|
||||
pub const BATTERY_EMPTY: &[u8] = include_bytes!("../assets/icons/status/battery-empty.svg");
|
||||
pub const BATTERY_FULL: &[u8] = include_bytes!("../assets/icons/status/battery-full.svg");
|
||||
pub const BLUETOOTH_CONNECTED: &[u8] =
|
||||
include_bytes!("../assets/icons/status/bluetooth-connected.svg");
|
||||
pub const BLUETOOTH_CONNECTED: &[u8] = include_bytes!("../assets/icons/status/bluetooth-connected.svg");
|
||||
pub const BLUETOOTH_OFF: &[u8] = include_bytes!("../assets/icons/status/bluetooth-off.svg");
|
||||
pub const BLUETOOTH_ON: &[u8] = include_bytes!("../assets/icons/status/bluetooth-on.svg");
|
||||
pub const BRIGHTNESS: &[u8] = include_bytes!("../assets/icons/status/brightness.svg");
|
||||
@@ -67,10 +64,7 @@ pub fn tinted(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon
|
||||
}
|
||||
|
||||
pub fn symbolic(source: &'static [u8], size: u16) -> icon::Icon {
|
||||
icon::from_svg_bytes(source)
|
||||
.symbolic(true)
|
||||
.icon()
|
||||
.size(size)
|
||||
icon::from_svg_bytes(source).symbolic(true).icon().size(size)
|
||||
}
|
||||
|
||||
pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
|
||||
@@ -88,8 +82,8 @@ pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Col
|
||||
.size(size)
|
||||
.width(Length::Fixed(f32::from(size) * aspect))
|
||||
.content_fit(ContentFit::Contain)
|
||||
.class(theme::Svg::custom(move |_| {
|
||||
cosmic::iced::widget::svg::Style { color: None }
|
||||
.class(theme::Svg::custom(move |_| cosmic::iced::widget::svg::Style {
|
||||
color: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ferese_protocols::material::v1::client::{
|
||||
ferese_material_manager_v1::FereseMaterialManagerV1,
|
||||
ferese_surface_material_v1::FereseSurfaceMaterialV1,
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::{wl_registry, wl_surface},
|
||||
};
|
||||
use ferese_protocols::material::v1::client::ferese_material_manager_v1::FereseMaterialManagerV1;
|
||||
use ferese_protocols::material::v1::client::ferese_surface_material_v1::FereseSurfaceMaterialV1;
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::{wl_registry, wl_surface};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
||||
|
||||
/// Keeps a compositor-owned dialog material attached for the window lifetime.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -37,9 +33,8 @@ impl ModalMaterial {
|
||||
return Err("Dialog materials require Wayland".into());
|
||||
};
|
||||
// Iced lends live native handles; this backend borrows its connection.
|
||||
let backend = unsafe {
|
||||
wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast())
|
||||
};
|
||||
let backend =
|
||||
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
|
||||
let connection = Connection::from_backend(backend);
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
@@ -48,8 +43,7 @@ impl ModalMaterial {
|
||||
)
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let surface =
|
||||
wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let (globals, mut queue) =
|
||||
registry_queue_init::<MaterialState>(&connection).map_err(|error| error.to_string())?;
|
||||
let qh = queue.handle();
|
||||
@@ -63,9 +57,7 @@ impl ModalMaterial {
|
||||
material,
|
||||
};
|
||||
let mut state = MaterialState::default();
|
||||
queue
|
||||
.roundtrip(&mut state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
|
||||
if !state.ready {
|
||||
return Err("Dialog material attachment was not acknowledged".into());
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
//! Shared menu layouts and paint; callers own actions and animation timing.
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Alignment, Color, Length},
|
||||
widget::{button, container, row as iced_row},
|
||||
};
|
||||
use cosmic::Element;
|
||||
use cosmic::iced::{Alignment, Color, Length};
|
||||
use cosmic::widget::{button, container, row as iced_row};
|
||||
|
||||
pub fn button<'a, M: Clone + 'a>(
|
||||
label: impl Into<std::borrow::Cow<'a, str>> + 'a,
|
||||
@@ -20,12 +18,9 @@ pub fn row<'a, M: 'a>(
|
||||
trailing: Element<'a, M>,
|
||||
font: cosmic::font::Font,
|
||||
) -> Element<'a, M> {
|
||||
iced_row![
|
||||
crate::text(label, font).width(Length::Fill).size(14),
|
||||
trailing
|
||||
]
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
iced_row![crate::text(label, font).width(Length::Fill).size(14), trailing]
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn heading<'a, M: 'a>(
|
||||
@@ -38,12 +33,7 @@ pub fn heading<'a, M: 'a>(
|
||||
.align_y(Alignment::Center)
|
||||
}
|
||||
|
||||
pub fn badge<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
accent: Color,
|
||||
opacity: f32,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn badge<'a, M: 'a>(content: Element<'a, M>, accent: Color, opacity: f32, radius: f32) -> Element<'a, M> {
|
||||
container(content)
|
||||
.width(36)
|
||||
.height(36)
|
||||
@@ -56,12 +46,7 @@ pub fn badge<'a, M: 'a>(
|
||||
.into()
|
||||
}
|
||||
|
||||
pub fn section<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
foreground: Color,
|
||||
opacity: f32,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn section<'a, M: 'a>(content: Element<'a, M>, foreground: Color, opacity: f32, radius: f32) -> Element<'a, M> {
|
||||
container(content)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
@@ -87,10 +72,7 @@ pub fn section_label<'a>(
|
||||
|
||||
pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> {
|
||||
container(cosmic::widget::Space::new().width(Length::Fill).height(1))
|
||||
.class(crate::controls::surface(
|
||||
foreground.scale_alpha(0.12 * opacity),
|
||||
0.,
|
||||
))
|
||||
.class(crate::controls::surface(foreground.scale_alpha(0.12 * opacity), 0.))
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -112,10 +94,7 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) -
|
||||
.width(16)
|
||||
.height(16)
|
||||
.class(crate::controls::surface(
|
||||
Color {
|
||||
a: opacity,
|
||||
..thumb
|
||||
},
|
||||
Color { a: opacity, ..thumb },
|
||||
palette.radius.min(8.),
|
||||
));
|
||||
container(knob)
|
||||
|
||||
@@ -102,11 +102,7 @@ pub fn parse_color(value: &str) -> Option<Color> {
|
||||
}
|
||||
|
||||
pub fn mix(a: Color, b: Color, t: f32) -> Color {
|
||||
Color::from_rgb(
|
||||
a.r + (b.r - a.r) * t,
|
||||
a.g + (b.g - a.g) * t,
|
||||
a.b + (b.b - a.b) * t,
|
||||
)
|
||||
Color::from_rgb(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t)
|
||||
}
|
||||
|
||||
pub fn surface_shade(base: Color) -> Color {
|
||||
@@ -179,10 +175,8 @@ mod tests {
|
||||
("translucent", 2., 1.),
|
||||
("translucent", -1., 0.),
|
||||
] {
|
||||
let document = Document::parse(&format!(
|
||||
"theme {{ material {{ style {style}; opacity {opacity}; }} }}"
|
||||
))
|
||||
.unwrap();
|
||||
let document =
|
||||
Document::parse(&format!("theme {{ material {{ style {style}; opacity {opacity}; }} }}")).unwrap();
|
||||
assert_eq!(material_opacity(Some(&document)), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Mutex, OnceLock},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
pub fn font(family: Option<&str>) -> cosmic::font::Font {
|
||||
let Some(family) = family else {
|
||||
|
||||
+11
-24
@@ -1,11 +1,9 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, fs,
|
||||
io::Read,
|
||||
os::unix::process::CommandExt,
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::Read;
|
||||
use std::os::unix::process::CommandExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{env, fs};
|
||||
|
||||
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
for key in ["WAYLAND_SOCKET", "FERESE_SHELL_CONTROL_SOCKET"] {
|
||||
@@ -35,20 +33,14 @@ pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
if system_directories.is_empty() {
|
||||
system_directories.push(PathBuf::from("/etc/xdg"));
|
||||
}
|
||||
let directories = std::iter::once(home)
|
||||
.chain(system_directories)
|
||||
.collect::<Vec<_>>();
|
||||
let directories = std::iter::once(home).chain(system_directories).collect::<Vec<_>>();
|
||||
let mut entries = BTreeMap::new();
|
||||
for directory in directories {
|
||||
let Ok(files) = fs::read_dir(directory.join("autostart")) else {
|
||||
continue;
|
||||
};
|
||||
for file in files.flatten() {
|
||||
if file
|
||||
.path()
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "desktop")
|
||||
{
|
||||
if file.path().extension().is_some_and(|extension| extension == "desktop") {
|
||||
entries.entry(file.file_name()).or_insert(file.path());
|
||||
}
|
||||
}
|
||||
@@ -108,9 +100,7 @@ fn read_entry(path: &Path) -> Option<BTreeMap<String, String>> {
|
||||
return None;
|
||||
}
|
||||
let mut source = String::new();
|
||||
file.take(1024 * 1024 + 1)
|
||||
.read_to_string(&mut source)
|
||||
.ok()?;
|
||||
file.take(1024 * 1024 + 1).read_to_string(&mut source).ok()?;
|
||||
if source.len() > 1024 * 1024 {
|
||||
return None;
|
||||
}
|
||||
@@ -152,15 +142,12 @@ fn enabled(entry: &BTreeMap<String, String>, desktop: &str) -> bool {
|
||||
fn available(program: &str) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let executable = |path: &Path| {
|
||||
fs::metadata(path)
|
||||
.is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
|
||||
fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
|
||||
};
|
||||
if program.contains('/') {
|
||||
executable(Path::new(program))
|
||||
} else {
|
||||
env::var_os("PATH").is_some_and(|paths| {
|
||||
env::split_paths(&paths).any(|path| executable(&path.join(program)))
|
||||
})
|
||||
env::var_os("PATH").is_some_and(|paths| env::split_paths(&paths).any(|path| executable(&path.join(program))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-17
@@ -1,11 +1,10 @@
|
||||
mod autostart;
|
||||
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::{self, Write};
|
||||
use std::io::Write;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::{env, fs, io};
|
||||
|
||||
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
|
||||
use serde_json::{Value, json};
|
||||
@@ -101,10 +100,9 @@ fn parse_args(args: impl IntoIterator<Item = String>) -> Result<(String, Value),
|
||||
));
|
||||
}
|
||||
},
|
||||
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout"
|
||||
| "toggle-overview" | "cycle-column-width" | "center-column" | "consume" | "expel"
|
||||
| "close" | "get-focused-window" | "get-windows" | "get-workspaces" | "get-outputs"
|
||||
| "reload-config" | "exit" | "request-logout" => {
|
||||
"toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout" | "toggle-overview"
|
||||
| "cycle-column-width" | "center-column" | "consume" | "expel" | "close" | "get-focused-window"
|
||||
| "get-windows" | "get-workspaces" | "get-outputs" | "reload-config" | "exit" | "request-logout" => {
|
||||
if !positional.is_empty() {
|
||||
return Err(format!("{command} does not accept arguments"));
|
||||
}
|
||||
@@ -152,15 +150,11 @@ mod tests {
|
||||
#[test]
|
||||
fn parses_direction_and_workspace_commands() {
|
||||
assert_eq!(
|
||||
parse_args(["focus".to_owned(), "left".to_owned()])
|
||||
.unwrap()
|
||||
.1,
|
||||
parse_args(["focus".to_owned(), "left".to_owned()]).unwrap().1,
|
||||
json!({ "direction": "left" })
|
||||
);
|
||||
assert_eq!(
|
||||
parse_args(["workspace".to_owned(), "7".to_owned()])
|
||||
.unwrap()
|
||||
.1,
|
||||
parse_args(["workspace".to_owned(), "7".to_owned()]).unwrap().1,
|
||||
json!({ "index": 7 })
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -203,10 +197,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_or_missing_arguments() {
|
||||
assert_eq!(
|
||||
parse_args(["exit".to_owned()]).unwrap(),
|
||||
("exit".to_owned(), json!({}))
|
||||
);
|
||||
assert_eq!(parse_args(["exit".to_owned()]).unwrap(), ("exit".to_owned(), json!({})));
|
||||
assert!(parse_args(["exit".to_owned(), "extra".to_owned()]).is_err());
|
||||
assert!(parse_args(["focus".to_owned()]).is_err());
|
||||
assert!(parse_args(["reload-config".to_owned()]).is_ok());
|
||||
|
||||
+58
-154
@@ -1,26 +1,22 @@
|
||||
//! ScreenCast portal backend. Only the portal frontend may create sessions;
|
||||
//! saved monitor consent is restored only through the frontend permission store.
|
||||
use crate::{
|
||||
capture::{Capture, Source},
|
||||
picker::Prompt,
|
||||
stream::Ready,
|
||||
};
|
||||
use std::sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
use std::{collections::HashMap, process::Stdio, time::Duration};
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
|
||||
process::{Child, Command},
|
||||
sync::{Mutex, Notify},
|
||||
};
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedObjectPath, OwnedValue, Value},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::process::{Child, Command};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
|
||||
|
||||
use crate::capture::{Capture, Source};
|
||||
use crate::picker::Prompt;
|
||||
use crate::stream::Ready;
|
||||
|
||||
pub(crate) type Options = HashMap<String, OwnedValue>;
|
||||
pub(crate) static CONFIG_TRANSACTION: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
@@ -105,18 +101,12 @@ impl RecorderControl {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let sender = header
|
||||
.sender()
|
||||
.ok_or_else(|| error("Missing recorder caller"))?;
|
||||
let sender = header.sender().ok_or_else(|| error("Missing recorder caller"))?;
|
||||
if !owns_session(sender.as_str(), session_handle.as_str()) {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Not your recording session".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::AccessDenied("Not your recording session".into()));
|
||||
}
|
||||
let dbus = zbus::fdo::DBusProxy::new(connection).await?;
|
||||
let pid = dbus
|
||||
.get_connection_unix_process_id(sender.clone().into())
|
||||
.await?;
|
||||
let pid = dbus.get_connection_unix_process_id(sender.clone().into()).await?;
|
||||
if !shell_recorder(pid) {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"The shell must own the recording controls".into(),
|
||||
@@ -155,18 +145,14 @@ fn shell_recorder(pid: u32) -> bool {
|
||||
let parent = std::fs::read_to_string(format!("/proc/{pid}/status"))
|
||||
.ok()
|
||||
.and_then(|status| {
|
||||
status.lines().find_map(|line| {
|
||||
line.strip_prefix("PPid:")
|
||||
.and_then(|s| s.trim().parse::<u32>().ok())
|
||||
})
|
||||
status
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("PPid:").and_then(|s| s.trim().parse::<u32>().ok()))
|
||||
});
|
||||
parent.is_some_and(|pid| matches_binary(pid, "ferese-shell") == Some(true))
|
||||
}
|
||||
|
||||
pub(crate) async fn authorize(
|
||||
connection: &Connection,
|
||||
header: &Header<'_>,
|
||||
) -> zbus::fdo::Result<String> {
|
||||
pub(crate) async fn authorize(connection: &Connection, header: &Header<'_>) -> zbus::fdo::Result<String> {
|
||||
let sender = header
|
||||
.sender()
|
||||
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing caller".into()))?;
|
||||
@@ -176,9 +162,7 @@ pub(crate) async fn authorize(
|
||||
.await
|
||||
.map_err(|_| zbus::fdo::Error::AccessDenied("Desktop portal is unavailable".into()))?;
|
||||
if sender.as_str() != owner.as_str() {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Use the desktop portal".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::AccessDenied("Use the desktop portal".into()));
|
||||
}
|
||||
|
||||
Ok(sender.to_string())
|
||||
@@ -227,10 +211,7 @@ impl Backend {
|
||||
let conn = connection.clone();
|
||||
let path = path.to_owned();
|
||||
tokio::spawn(async move {
|
||||
let _ = conn
|
||||
.object_server()
|
||||
.remove::<SessionObject, _>(path.as_str())
|
||||
.await;
|
||||
let _ = conn.object_server().remove::<SessionObject, _>(path.as_str()).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -319,21 +300,14 @@ impl Backend {
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
let settings = source_options(&options).and_then(|(multiple, cursor, types)| {
|
||||
Ok((
|
||||
multiple,
|
||||
cursor,
|
||||
types,
|
||||
crate::restore::persist_mode(&options)?,
|
||||
))
|
||||
Ok((multiple, cursor, types, crate::restore::persist_mode(&options)?))
|
||||
});
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
let session = sessions
|
||||
.get_mut(session_handle.as_str())
|
||||
.ok_or_else(|| error("Unknown session"))?;
|
||||
|
||||
if session.owner != owner
|
||||
|| session.app != app_id
|
||||
|| !matches!(session.phase, Phase::Created | Phase::Selected)
|
||||
if session.owner != owner || session.app != app_id || !matches!(session.phase, Phase::Created | Phase::Selected)
|
||||
{
|
||||
return Err(error("Invalid session state"));
|
||||
}
|
||||
@@ -369,10 +343,7 @@ impl Backend {
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
|
||||
if !handle
|
||||
.as_str()
|
||||
.starts_with("/org/freedesktop/portal/desktop/request/")
|
||||
{
|
||||
if !handle.as_str().starts_with("/org/freedesktop/portal/desktop/request/") {
|
||||
return Err(error("Invalid request path"));
|
||||
}
|
||||
|
||||
@@ -435,10 +406,7 @@ impl Backend {
|
||||
result.unwrap_or_else(|_| Err(StartError::Failed("Sharing request timed out".into())))
|
||||
}
|
||||
};
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<Request, _>(handle)
|
||||
.await;
|
||||
let _ = connection.object_server().remove::<Request, _>(handle).await;
|
||||
|
||||
match result {
|
||||
Ok((children, streams, mut reply)) if !cancel.stopped.load(Ordering::SeqCst) => {
|
||||
@@ -455,8 +423,7 @@ impl Backend {
|
||||
});
|
||||
reply.insert(
|
||||
"streams".into(),
|
||||
OwnedValue::try_from(Value::from(streams))
|
||||
.map_err(|e| error(&e.to_string()))?,
|
||||
OwnedValue::try_from(Value::from(streams)).map_err(|e| error(&e.to_string()))?,
|
||||
);
|
||||
|
||||
Ok((0, reply))
|
||||
@@ -593,8 +560,7 @@ async fn start_streams(
|
||||
let rememberable = sources
|
||||
.iter()
|
||||
.filter(|source| {
|
||||
crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1)
|
||||
.is_some()
|
||||
crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1).is_some()
|
||||
})
|
||||
.map(|source| source.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
@@ -602,10 +568,7 @@ async fn start_streams(
|
||||
let selected = restore.resolve(app, cursor, multiple, &sources, &outputs)?;
|
||||
let selected = validate_selection(
|
||||
&sources,
|
||||
&selected
|
||||
.iter()
|
||||
.map(|source| source.name.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
&selected.iter().map(|source| source.name.clone()).collect::<Vec<_>>(),
|
||||
multiple,
|
||||
)
|
||||
.ok()?;
|
||||
@@ -707,15 +670,11 @@ async fn start_streams(
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut line = String::new();
|
||||
let output = child.stdout.take().unwrap();
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
BufReader::new(output).read_line(&mut line),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "PipeWire stream startup timed out")?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let ready: Ready =
|
||||
serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?;
|
||||
tokio::time::timeout(Duration::from_secs(10), BufReader::new(output).read_line(&mut line))
|
||||
.await
|
||||
.map_err(|_| "PipeWire stream startup timed out")?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let ready: Ready = serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?;
|
||||
|
||||
if ready.node == u32::MAX {
|
||||
return Err("Invalid PipeWire node".into());
|
||||
@@ -734,10 +693,7 @@ async fn start_streams(
|
||||
return Err("A selected display changed during stream startup".into());
|
||||
}
|
||||
|
||||
if children
|
||||
.iter_mut()
|
||||
.any(|child| !matches!(child.try_wait(), Ok(None)))
|
||||
{
|
||||
if children.iter_mut().any(|child| !matches!(child.try_wait(), Ok(None))) {
|
||||
return Err("A selected stream ended during startup".into());
|
||||
}
|
||||
|
||||
@@ -751,11 +707,10 @@ async fn start_streams(
|
||||
.find(|source| source.name == name)
|
||||
.ok_or("Unknown window stream")?;
|
||||
let id = source.window_id().ok_or("Invalid selected window")?;
|
||||
if !final_windows.as_array().is_some_and(|windows| {
|
||||
windows
|
||||
.iter()
|
||||
.any(|window| window["id"].as_u64() == Some(id))
|
||||
}) {
|
||||
if !final_windows
|
||||
.as_array()
|
||||
.is_some_and(|windows| windows.iter().any(|window| window["id"].as_u64() == Some(id)))
|
||||
{
|
||||
return Err("A selected window closed during startup".to_owned());
|
||||
}
|
||||
window_metadata(source, &ready)
|
||||
@@ -773,9 +728,7 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
|
||||
if source.window_id().is_none() || ready.width == 0 || ready.height == 0 {
|
||||
return Err("Invalid window stream".into());
|
||||
}
|
||||
let (width, height) = ready
|
||||
.logical_size
|
||||
.ok_or("Missing captured window geometry")?;
|
||||
let (width, height) = ready.logical_size.ok_or("Missing captured window geometry")?;
|
||||
let size = (
|
||||
i32::try_from(width).map_err(|_| "Invalid window width")?,
|
||||
i32::try_from(height).map_err(|_| "Invalid window height")?,
|
||||
@@ -787,25 +740,15 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
|
||||
("source_type".into(), 2u32.into()),
|
||||
(
|
||||
"size".into(),
|
||||
Value::from(size)
|
||||
.try_to_owned()
|
||||
.map_err(|e| e.to_string())?,
|
||||
Value::from(size).try_to_owned().map_err(|e| e.to_string())?,
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
fn monitor_metadata(
|
||||
outputs: &serde_json::Value,
|
||||
name: &str,
|
||||
ready: &Ready,
|
||||
) -> Result<Options, String> {
|
||||
fn monitor_metadata(outputs: &serde_json::Value, name: &str, ready: &Ready) -> Result<Options, String> {
|
||||
let output = outputs
|
||||
.as_array()
|
||||
.and_then(|outputs| {
|
||||
outputs
|
||||
.iter()
|
||||
.find(|output| output["name"].as_str() == Some(name))
|
||||
})
|
||||
.and_then(|outputs| outputs.iter().find(|output| output["name"].as_str() == Some(name)))
|
||||
.ok_or("Selected monitor is unavailable")?;
|
||||
let integer = |key: &str| {
|
||||
output[key]
|
||||
@@ -851,23 +794,13 @@ fn monitor_metadata(
|
||||
),
|
||||
(
|
||||
"size".into(),
|
||||
Value::from(size)
|
||||
.try_to_owned()
|
||||
.map_err(|error| error.to_string())?,
|
||||
Value::from(size).try_to_owned().map_err(|error| error.to_string())?,
|
||||
),
|
||||
]))
|
||||
}
|
||||
|
||||
fn validate_selection(
|
||||
sources: &[Source],
|
||||
names: &[String],
|
||||
multiple: bool,
|
||||
) -> Result<Vec<Source>, String> {
|
||||
if names.is_empty()
|
||||
|| names.len() > 8
|
||||
|| names.len() > sources.len()
|
||||
|| (!multiple && names.len() != 1)
|
||||
{
|
||||
fn validate_selection(sources: &[Source], names: &[String], multiple: bool) -> Result<Vec<Source>, String> {
|
||||
if names.is_empty() || names.len() > 8 || names.len() > sources.len() || (!multiple && names.len() != 1) {
|
||||
return Err("Invalid display selection".into());
|
||||
}
|
||||
|
||||
@@ -890,11 +823,7 @@ fn validate_selection(
|
||||
|
||||
async fn supervise(mut children: Vec<Child>, cancel: Arc<Cancel>) {
|
||||
loop {
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| children
|
||||
.iter_mut()
|
||||
.any(|c| !matches!(c.try_wait(), Ok(None)))
|
||||
{
|
||||
if cancel.stopped.load(Ordering::SeqCst) || children.iter_mut().any(|c| !matches!(c.try_wait(), Ok(None))) {
|
||||
break;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
@@ -928,10 +857,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.serve_at(PATH, shortcuts.clone())?
|
||||
.serve_at(PATH, inhibit.clone())?
|
||||
.serve_at(PATH, input_capture.clone())?
|
||||
.serve_at(
|
||||
"/org/ferese/ScreenRecorder",
|
||||
RecorderControl(backend.clone()),
|
||||
)?
|
||||
.serve_at("/org/ferese/ScreenRecorder", RecorderControl(backend.clone()))?
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
@@ -955,9 +881,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
requests.revoke_stale(owner.as_deref()).await;
|
||||
shortcuts.revoke_stale(&connection, owner.as_deref()).await;
|
||||
inhibit.revoke_stale(&connection, owner.as_deref()).await;
|
||||
input_capture
|
||||
.revoke_stale(&connection, owner.as_deref())
|
||||
.await;
|
||||
input_capture.revoke_stale(&connection, owner.as_deref()).await;
|
||||
|
||||
let stale: Vec<_> = backend
|
||||
.sessions
|
||||
@@ -1057,10 +981,7 @@ mod tests {
|
||||
scale: 1,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let names = sources
|
||||
.iter()
|
||||
.map(|source| source.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let names = sources.iter().map(|source| source.name.clone()).collect::<Vec<_>>();
|
||||
assert!(validate_selection(&sources, &names[..8], true).is_ok());
|
||||
assert!(validate_selection(&sources, &names, true).is_err());
|
||||
assert!(validate_selection(&sources, &names[..2], false).is_err());
|
||||
@@ -1117,10 +1038,7 @@ mod tests {
|
||||
scale: 1,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let names = sources
|
||||
.iter()
|
||||
.map(|source| source.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let names = sources.iter().map(|source| source.name.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(validate_selection(&sources, &names, true).unwrap().len(), 6);
|
||||
assert!(validate_selection(&sources, &names, false).is_err());
|
||||
}
|
||||
@@ -1137,22 +1055,8 @@ mod tests {
|
||||
scale: 1,
|
||||
};
|
||||
assert!(validate_selection(std::slice::from_ref(&source), &[], false).is_err());
|
||||
assert!(
|
||||
validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err()
|
||||
);
|
||||
assert!(
|
||||
validate_selection(
|
||||
std::slice::from_ref(&source),
|
||||
&["test".into(), "test".into()],
|
||||
true
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(
|
||||
validate_selection(&[source], &["test".into()], false)
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert!(validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err());
|
||||
assert!(validate_selection(std::slice::from_ref(&source), &["test".into(), "test".into()], true).is_err());
|
||||
assert_eq!(validate_selection(&[source], &["test".into()], false).unwrap().len(), 1);
|
||||
}
|
||||
}
|
||||
+8
-17
@@ -1,5 +1,8 @@
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::Value as Json;
|
||||
use std::{os::unix::net::UnixStream, sync::Arc, time::Duration};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Bridge(Arc<BridgeInner>);
|
||||
@@ -41,17 +44,9 @@ impl Bridge {
|
||||
async fn request(&self, command: &'static str, args: Json, wait: bool) -> Result<Json, String> {
|
||||
let bridge = self.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let mut stream = bridge
|
||||
.0
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Compositor connection failed")?;
|
||||
let mut stream = bridge.0.stream.lock().map_err(|_| "Compositor connection failed")?;
|
||||
stream
|
||||
.set_read_timeout(if wait {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(2))
|
||||
})
|
||||
.set_read_timeout(if wait { None } else { Some(Duration::from_secs(2)) })
|
||||
.map_err(|error| error.to_string())?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
@@ -73,12 +68,8 @@ impl Bridge {
|
||||
}
|
||||
|
||||
pub(crate) async fn capture_watch(&self, session: u64) -> Result<Json, String> {
|
||||
self.request(
|
||||
"input-capture-watch",
|
||||
serde_json::json!({"session":session}),
|
||||
true,
|
||||
)
|
||||
.await
|
||||
self.request("input-capture-watch", serde_json::json!({"session":session}), true)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn closed(&self) {
|
||||
|
||||
+28
-88
@@ -1,20 +1,15 @@
|
||||
//! Bounded, cancellable Wayland capture. No screenshots or external capture tools.
|
||||
use std::fs::File;
|
||||
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
|
||||
use memmap2::MmapMut;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fs::File,
|
||||
os::fd::{AsFd, AsRawFd, FromRawFd},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, QueueHandle, WEnum,
|
||||
protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool},
|
||||
};
|
||||
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool};
|
||||
use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum};
|
||||
use wayland_protocols_wlr::screencopy::v1::client::{
|
||||
zwlr_screencopy_frame_v1 as frame, zwlr_screencopy_manager_v1 as manager,
|
||||
};
|
||||
@@ -104,9 +99,7 @@ impl Capture {
|
||||
// uninterruptible roundtrip on a stalled compositor.
|
||||
this.sync(stop)?;
|
||||
this.sync(stop)?;
|
||||
if (this.state.manager.is_none() && this.state.window_manager.is_none())
|
||||
|| this.state.shm.is_none()
|
||||
{
|
||||
if (this.state.manager.is_none() && this.state.window_manager.is_none()) || this.state.shm.is_none() {
|
||||
return Err("Ferese screen capture is unavailable in this session".into());
|
||||
}
|
||||
Ok(this)
|
||||
@@ -114,9 +107,7 @@ impl Capture {
|
||||
|
||||
fn sync(&mut self, stop: &AtomicBool) -> Result<(), String> {
|
||||
let done = Arc::new(AtomicBool::new(false));
|
||||
self.connection
|
||||
.display()
|
||||
.sync(&self.queue.handle(), done.clone());
|
||||
self.connection.display().sync(&self.queue.handle(), done.clone());
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
while !done.load(Ordering::Relaxed) {
|
||||
self.dispatch(stop, deadline)?;
|
||||
@@ -168,34 +159,19 @@ impl Capture {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn frame(
|
||||
&mut self,
|
||||
name: &str,
|
||||
cursor: bool,
|
||||
stop: &AtomicBool,
|
||||
mut pixels: Vec<u8>,
|
||||
) -> Result<Frame, String> {
|
||||
pub fn frame(&mut self, name: &str, cursor: bool, stop: &AtomicBool, mut pixels: Vec<u8>) -> Result<Frame, String> {
|
||||
self.state.result = None;
|
||||
self.state.flipped = false;
|
||||
self.state.logical_size = None;
|
||||
self.state.busy = false;
|
||||
let (frame, transform) = if let Some(id) = name
|
||||
.strip_prefix("window:")
|
||||
.and_then(|id| id.parse::<u64>().ok())
|
||||
{
|
||||
let (frame, transform) = if let Some(id) = name.strip_prefix("window:").and_then(|id| id.parse::<u64>().ok()) {
|
||||
let manager = self
|
||||
.state
|
||||
.window_manager
|
||||
.as_ref()
|
||||
.ok_or("Window capture is unavailable")?;
|
||||
(
|
||||
manager.capture_window(
|
||||
(id >> 32) as u32,
|
||||
id as u32,
|
||||
cursor as u32,
|
||||
&self.queue.handle(),
|
||||
(),
|
||||
),
|
||||
manager.capture_window((id >> 32) as u32, id as u32, cursor as u32, &self.queue.handle(), ()),
|
||||
wl_output::Transform::Normal,
|
||||
)
|
||||
} else {
|
||||
@@ -205,18 +181,11 @@ impl Capture {
|
||||
.iter()
|
||||
.find(|o| o.source.name == name)
|
||||
.ok_or("Shared monitor disconnected")?;
|
||||
if self
|
||||
.selected_output
|
||||
.is_some_and(|selected| selected != output.global)
|
||||
{
|
||||
if self.selected_output.is_some_and(|selected| selected != output.global) {
|
||||
return Err("Shared monitor was replaced".into());
|
||||
}
|
||||
self.selected_output = Some(output.global);
|
||||
let manager = self
|
||||
.state
|
||||
.manager
|
||||
.as_ref()
|
||||
.ok_or("Monitor capture is unavailable")?;
|
||||
let manager = self.state.manager.as_ref().ok_or("Monitor capture is unavailable")?;
|
||||
(
|
||||
manager.capture_output(cursor as i32, &output.proxy, &self.queue.handle(), ()),
|
||||
output.transform,
|
||||
@@ -234,11 +203,7 @@ impl Capture {
|
||||
frame.destroy();
|
||||
self.state.result.take().unwrap()?;
|
||||
|
||||
let buffer = self
|
||||
.state
|
||||
.buffer
|
||||
.as_ref()
|
||||
.ok_or("Capture supplied no buffer")?;
|
||||
let buffer = self.state.buffer.as_ref().ok_or("Capture supplied no buffer")?;
|
||||
let (width, height) = copy_oriented_pixels(
|
||||
&buffer.map,
|
||||
buffer.width,
|
||||
@@ -283,9 +248,7 @@ impl Capture {
|
||||
|
||||
if ready > 0 {
|
||||
read.read().map_err(|e| e.to_string())?;
|
||||
} else if ready < 0
|
||||
&& std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted
|
||||
{
|
||||
} else if ready < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
|
||||
return Err(std::io::Error::last_os_error().to_string());
|
||||
}
|
||||
}
|
||||
@@ -327,13 +290,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
|
||||
scale: 1,
|
||||
},
|
||||
}),
|
||||
"ferese_window_capture_manager_v1" => {
|
||||
state.window_manager = Some(registry.bind(name, 1, qh, ()))
|
||||
}
|
||||
"ferese_window_capture_manager_v1" => state.window_manager = Some(registry.bind(name, 1, qh, ())),
|
||||
"wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
|
||||
"zwlr_screencopy_manager_v1" => {
|
||||
state.manager = Some(registry.bind(name, version.min(3), qh, ()))
|
||||
}
|
||||
"zwlr_screencopy_manager_v1" => state.manager = Some(registry.bind(name, version.min(3), qh, ())),
|
||||
_ => (),
|
||||
},
|
||||
wl_registry::Event::GlobalRemove { name } => state.outputs.retain(|o| o.global != name),
|
||||
@@ -358,9 +317,7 @@ impl Dispatch<wl_output::WlOutput, u32> for State {
|
||||
match event {
|
||||
wl_output::Event::Name { name } => output.source.name = name,
|
||||
wl_output::Event::Description { description } => output.source.label = description,
|
||||
wl_output::Event::Geometry {
|
||||
x, y, transform, ..
|
||||
} => {
|
||||
wl_output::Event::Geometry { x, y, transform, .. } => {
|
||||
if let WEnum::Value(transform) = transform {
|
||||
output.transform = transform;
|
||||
}
|
||||
@@ -412,9 +369,7 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
|
||||
return Ok(());
|
||||
}
|
||||
// SAFETY: fixed NUL-terminated name and a newly owned memfd.
|
||||
let fd = unsafe {
|
||||
libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC)
|
||||
};
|
||||
let fd = unsafe { libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC) };
|
||||
|
||||
if fd < 0 {
|
||||
return Err(std::io::Error::last_os_error().to_string());
|
||||
@@ -424,20 +379,13 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
|
||||
file.set_len(bytes as u64).map_err(|e| e.to_string())?;
|
||||
// SAFETY: the file has the checked mapping length and lives with the mapping.
|
||||
let map = unsafe { MmapMut::map_mut(&file) }.map_err(|e| e.to_string())?;
|
||||
let pool = state
|
||||
.shm
|
||||
.as_ref()
|
||||
.ok_or("Missing shared memory global")?
|
||||
.create_pool(file.as_fd(), bytes as i32, qh, ());
|
||||
let proxy = pool.create_buffer(
|
||||
0,
|
||||
width as i32,
|
||||
height as i32,
|
||||
stride as i32,
|
||||
format,
|
||||
let pool = state.shm.as_ref().ok_or("Missing shared memory global")?.create_pool(
|
||||
file.as_fd(),
|
||||
bytes as i32,
|
||||
qh,
|
||||
(),
|
||||
);
|
||||
let proxy = pool.create_buffer(0, width as i32, height as i32, stride as i32, format, qh, ());
|
||||
|
||||
pool.destroy();
|
||||
frame.copy(&proxy);
|
||||
@@ -540,9 +488,7 @@ fn copy_oriented_pixels(
|
||||
|
||||
fn buffer_size(width: u32, height: u32, stride: u32) -> Result<usize, String> {
|
||||
let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?;
|
||||
let bytes = stride
|
||||
.checked_mul(height)
|
||||
.ok_or("Capture dimensions overflow")?;
|
||||
let bytes = stride.checked_mul(height).ok_or("Capture dimensions overflow")?;
|
||||
|
||||
if width == 0 || height == 0 || stride < row || bytes > 128 * 1024 * 1024 {
|
||||
return Err("Unsupported capture dimensions".into());
|
||||
@@ -578,9 +524,7 @@ impl Dispatch<window_manager::FereseWindowCaptureManagerV1, ()> for State {
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
match event {
|
||||
window_manager::Event::Geometry { width, height, .. } => {
|
||||
state.logical_size = Some((width, height))
|
||||
}
|
||||
window_manager::Event::Geometry { width, height, .. } => state.logical_size = Some((width, height)),
|
||||
window_manager::Event::Busy { .. } => state.busy = true,
|
||||
_ => (),
|
||||
}
|
||||
@@ -609,11 +553,7 @@ mod tests {
|
||||
for (transform, size, expected) in cases {
|
||||
for y_invert in [false, true] {
|
||||
let mut source = Vec::new();
|
||||
let rows = if y_invert {
|
||||
[b"def", b"abc"]
|
||||
} else {
|
||||
[b"abc", b"def"]
|
||||
};
|
||||
let rows = if y_invert { [b"def", b"abc"] } else { [b"abc", b"def"] };
|
||||
for row in rows {
|
||||
for &value in row {
|
||||
source.extend_from_slice(&pixel(value));
|
||||
|
||||
+11
-23
@@ -1,14 +1,14 @@
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Length},
|
||||
widget::{button, column, container, image, row, scrollable, text_input},
|
||||
};
|
||||
use ferese_theme::{Palette, accent_button, controls, material::ModalMaterial, text};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use cosmic::Element;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, column, container, image, row, scrollable, text_input};
|
||||
use ferese_theme::material::ModalMaterial;
|
||||
use ferese_theme::{Palette, accent_button, controls, text};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct Prompt {
|
||||
pub title: String,
|
||||
@@ -30,15 +30,7 @@ pub(crate) struct ShortcutField {
|
||||
#[derive(Clone, Debug)]
|
||||
enum Message {
|
||||
Opened(cosmic::iced::window::Id),
|
||||
Attached(
|
||||
Result<
|
||||
(
|
||||
Option<Arc<crate::parent::Parent>>,
|
||||
Result<ModalMaterial, String>,
|
||||
),
|
||||
String,
|
||||
>,
|
||||
),
|
||||
Attached(Result<(Option<Arc<crate::parent::Parent>>, Result<ModalMaterial, String>), String>),
|
||||
Accept,
|
||||
Cancel,
|
||||
Shortcut(usize, String),
|
||||
@@ -56,9 +48,7 @@ struct Consent {
|
||||
|
||||
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.take(256 * 1024 + 1)
|
||||
.read_to_string(&mut input)?;
|
||||
std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?;
|
||||
|
||||
if input.len() > 256 * 1024 {
|
||||
return Err("Consent request is too large".into());
|
||||
@@ -124,9 +114,7 @@ impl cosmic::Application for Consent {
|
||||
|
||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||
Some(Message::Opened(id))
|
||||
}
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
|
||||
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
|
||||
..
|
||||
|
||||
+55
-125
@@ -1,22 +1,21 @@
|
||||
use crate::{
|
||||
backend::{Cancel, Options, Request, authorize},
|
||||
consent::Prompt,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
io::Read,
|
||||
os::unix::{fs::PermissionsExt, net::UnixStream},
|
||||
path::{Path, PathBuf},
|
||||
process::Stdio,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{io::AsyncWriteExt, sync::Mutex};
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
zvariant::{OwnedObjectPath, Value},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Mutex;
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::zvariant::{OwnedObjectPath, Value};
|
||||
|
||||
use crate::backend::{Cancel, Options, Request, authorize};
|
||||
use crate::consent::Prompt;
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024;
|
||||
@@ -56,9 +55,7 @@ impl Requests {
|
||||
}
|
||||
let mut pending = self.0.lock().await;
|
||||
if pending.len() >= self.1 || pending.contains_key(handle.as_str()) {
|
||||
return Err(zbus::fdo::Error::LimitsExceeded(
|
||||
"Request limit reached".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::LimitsExceeded("Request limit reached".into()));
|
||||
}
|
||||
let cancel = Arc::new(Cancel::default());
|
||||
if !connection
|
||||
@@ -72,9 +69,7 @@ impl Requests {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Request already exists".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Request already exists".into()));
|
||||
}
|
||||
pending.insert(handle.to_string(), (owner, cancel.clone()));
|
||||
Ok(cancel)
|
||||
@@ -82,10 +77,7 @@ impl Requests {
|
||||
|
||||
pub(crate) async fn end(&self, connection: &Connection, handle: &OwnedObjectPath) {
|
||||
self.0.lock().await.remove(handle.as_str());
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<Request, _>(handle)
|
||||
.await;
|
||||
let _ = connection.object_server().remove::<Request, _>(handle).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,10 +223,7 @@ pub(crate) async fn consent(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn ipc(
|
||||
command: &'static str,
|
||||
args: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
pub(crate) async fn ipc(command: &'static str, args: serde_json::Value) -> Result<serde_json::Value, String> {
|
||||
tokio::task::spawn_blocking(move || ipc_sync(command, args))
|
||||
.await
|
||||
.map_err(|error| error.to_string())?
|
||||
@@ -260,8 +249,7 @@ fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result<serde_json
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut stream).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
@@ -313,10 +301,7 @@ async fn capture(geometry: Option<String>) -> Result<Vec<u8>, String> {
|
||||
capture_command("screenshot", serde_json::json!({"geometry": geometry})).await
|
||||
}
|
||||
|
||||
async fn capture_command(
|
||||
command: &'static str,
|
||||
args: serde_json::Value,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
async fn capture_command(command: &'static str, args: serde_json::Value) -> Result<Vec<u8>, String> {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let result = ipc_sync(command, args)?;
|
||||
let path = result["path"].as_str().ok_or("Missing screenshot path")?;
|
||||
@@ -462,20 +447,14 @@ fn window_sources(windows: &serde_json::Value) -> Result<Vec<crate::capture::Sou
|
||||
Ok(sources)
|
||||
}
|
||||
|
||||
pub(crate) fn sharing_window_sources(
|
||||
windows: &serde_json::Value,
|
||||
) -> Result<Vec<crate::capture::Source>, String> {
|
||||
pub(crate) fn sharing_window_sources(windows: &serde_json::Value) -> Result<Vec<crate::capture::Source>, String> {
|
||||
let windows = windows.as_array().ok_or("Invalid window list")?;
|
||||
let eligible = windows
|
||||
.iter()
|
||||
.filter(|window| {
|
||||
window["workspace"].as_u64().is_some()
|
||||
&& window["capture_width"]
|
||||
.as_i64()
|
||||
.is_some_and(|width| width > 0)
|
||||
&& window["capture_height"]
|
||||
.as_i64()
|
||||
.is_some_and(|height| height > 0)
|
||||
&& window["capture_width"].as_i64().is_some_and(|width| width > 0)
|
||||
&& window["capture_height"].as_i64().is_some_and(|height| height > 0)
|
||||
&& window["app_id"].as_str() != Some("dev.ferese.ScreenShare")
|
||||
})
|
||||
.take(128)
|
||||
@@ -530,8 +509,7 @@ async fn select_window(app: &str, parent: &str) -> Result<Option<u64>, String> {
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
let names: Vec<String> =
|
||||
serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?;
|
||||
let names: Vec<String> = serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?;
|
||||
if names.len() != 1 || !prompt.sources.iter().any(|source| source.name == names[0]) {
|
||||
return Err("Unknown selected window".into());
|
||||
}
|
||||
@@ -562,9 +540,7 @@ async fn pick_color(app: &str, parent: &str) -> Result<Option<Options>, String>
|
||||
let color = png_color(&bytes)?;
|
||||
Ok(Some(HashMap::from([(
|
||||
"color".into(),
|
||||
Value::from(color)
|
||||
.try_to_owned()
|
||||
.map_err(|error| error.to_string())?,
|
||||
Value::from(color).try_to_owned().map_err(|error| error.to_string())?,
|
||||
)])))
|
||||
}
|
||||
|
||||
@@ -577,9 +553,7 @@ fn png_color(bytes: &[u8]) -> Result<(f64, f64, f64), String> {
|
||||
.filter(|size| *size <= 16 * 1024)
|
||||
.ok_or("Unexpected pixel dimensions")?;
|
||||
let mut pixels = vec![0; size];
|
||||
let info = reader
|
||||
.next_frame(&mut pixels)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let info = reader.next_frame(&mut pixels).map_err(|error| error.to_string())?;
|
||||
if info.width == 0 || info.height == 0 {
|
||||
return Err("Empty image".into());
|
||||
}
|
||||
@@ -599,8 +573,7 @@ fn private_directory(name: &str) -> Result<PathBuf, String> {
|
||||
.join("ferese/portal")
|
||||
.join(name);
|
||||
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
|
||||
std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| error.to_string())?;
|
||||
std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700)).map_err(|error| error.to_string())?;
|
||||
Ok(directory)
|
||||
}
|
||||
|
||||
@@ -668,11 +641,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
|
||||
.custom_flags(libc::O_NONBLOCK)
|
||||
.open(input)
|
||||
.map_err(|error| error.to_string())?;
|
||||
if !input
|
||||
.metadata()
|
||||
.map_err(|error| error.to_string())?
|
||||
.is_file()
|
||||
{
|
||||
if !input.metadata().map_err(|error| error.to_string())?.is_file() {
|
||||
return Err("Wallpaper must be a regular file".into());
|
||||
}
|
||||
let directory = private_directory("wallpapers")?;
|
||||
@@ -695,8 +664,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
|
||||
if size > MAX_IMAGE_BYTES {
|
||||
return Err("Wallpaper is too large".into());
|
||||
}
|
||||
std::io::Write::write_all(&mut file, &buffer[..bytes])
|
||||
.map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut file, &buffer[..bytes]).map_err(|error| error.to_string())?;
|
||||
}
|
||||
if size == 0 {
|
||||
return Err("Wallpaper is empty".into());
|
||||
@@ -705,9 +673,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
|
||||
.map_err(|error| error.to_string())?
|
||||
.with_guessed_format()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let (width, height) = reader
|
||||
.into_dimensions()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let (width, height) = reader.into_dimensions().map_err(|error| error.to_string())?;
|
||||
if width == 0
|
||||
|| height == 0
|
||||
|| width > 16384
|
||||
@@ -741,8 +707,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
|
||||
Err(error) => return Err(error.to_string()),
|
||||
};
|
||||
let mut document =
|
||||
ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
|
||||
let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
|
||||
if target == "background" && document.get("theme.background.lock_path").is_none() {
|
||||
let old = document
|
||||
.get("theme.background.path")
|
||||
@@ -755,10 +720,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
|
||||
}
|
||||
if target != "lockscreen" {
|
||||
document
|
||||
.set(
|
||||
"theme.background.path",
|
||||
image.to_string_lossy().into_owned().into(),
|
||||
)
|
||||
.set("theme.background.path", image.to_string_lossy().into_owned().into())
|
||||
.map_err(|error| error.to_string())?;
|
||||
}
|
||||
if target != "background" {
|
||||
@@ -772,11 +734,8 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
|
||||
let parent = path.parent().ok_or("Invalid config path")?;
|
||||
std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||
let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut temp, document.to_string().as_bytes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
temp.as_file()
|
||||
.sync_all()
|
||||
.map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut temp, document.to_string().as_bytes()).map_err(|error| error.to_string())?;
|
||||
temp.as_file().sync_all().map_err(|error| error.to_string())?;
|
||||
if std::fs::read_to_string(&path).unwrap_or_default() != source {
|
||||
return Err("Configuration changed; please try again".into());
|
||||
}
|
||||
@@ -806,11 +765,7 @@ mod tests {
|
||||
fn local_uris_preserve_special_characters_and_reject_network_paths() {
|
||||
let path = Path::new("/tmp/wallpaper #1_日本.png");
|
||||
assert_eq!(local_path(&file_uri(path).unwrap()).unwrap(), path);
|
||||
for uri in [
|
||||
"https://example.com/a.png",
|
||||
"file://example.com/a.png",
|
||||
"not a URI",
|
||||
] {
|
||||
for uri in ["https://example.com/a.png", "file://example.com/a.png", "not a URI"] {
|
||||
assert!(local_path(uri).is_err());
|
||||
}
|
||||
}
|
||||
@@ -862,46 +817,25 @@ mod tests {
|
||||
fn wallpaper_targets_preserve_independent_paths_and_other_settings() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("config.kdl");
|
||||
std::fs::write(&path, "// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n").unwrap();
|
||||
std::fs::write(
|
||||
&path,
|
||||
"// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n",
|
||||
)
|
||||
.unwrap();
|
||||
edit_wallpaper(&path, Path::new("/desktop.png"), "background").unwrap();
|
||||
let document =
|
||||
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
document.get("theme.background.path").unwrap(),
|
||||
"/desktop.png"
|
||||
);
|
||||
assert_eq!(
|
||||
document.get("theme.background.lock_path").unwrap(),
|
||||
"/old.png"
|
||||
);
|
||||
assert!(
|
||||
std::fs::read_to_string(&path)
|
||||
.unwrap()
|
||||
.contains("Keep my settings")
|
||||
);
|
||||
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png");
|
||||
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/old.png");
|
||||
assert!(std::fs::read_to_string(&path).unwrap().contains("Keep my settings"));
|
||||
edit_wallpaper(&path, Path::new("/lock.png"), "lockscreen").unwrap();
|
||||
let document =
|
||||
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
document.get("theme.background.path").unwrap(),
|
||||
"/desktop.png"
|
||||
);
|
||||
assert_eq!(
|
||||
document.get("theme.background.lock_path").unwrap(),
|
||||
"/lock.png"
|
||||
);
|
||||
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png");
|
||||
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/lock.png");
|
||||
edit_wallpaper(&path, Path::new("/both.png"), "both").unwrap();
|
||||
let document =
|
||||
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(document.get("theme.background.path").unwrap(), "/both.png");
|
||||
assert_eq!(
|
||||
document.get("theme.background.lock_path").unwrap(),
|
||||
"/both.png"
|
||||
);
|
||||
assert_eq!(
|
||||
document.get("commands.terminal").unwrap(),
|
||||
&serde_json::json!(["foot"])
|
||||
);
|
||||
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/both.png");
|
||||
assert_eq!(document.get("commands.terminal").unwrap(), &serde_json::json!(["foot"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -912,10 +846,6 @@ mod tests {
|
||||
std::fs::write(&path, [0u8; 32]).unwrap();
|
||||
let cancel = Cancel::default();
|
||||
cancel.stop();
|
||||
assert!(
|
||||
prepare_wallpaper(&path, &cancel)
|
||||
.unwrap_err()
|
||||
.contains("cancelled")
|
||||
);
|
||||
assert!(prepare_wallpaper(&path, &cancel).unwrap_err().contains("cancelled"));
|
||||
}
|
||||
}
|
||||
+47
-101
@@ -1,23 +1,15 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Write,
|
||||
os::{
|
||||
fd::{AsFd, OwnedFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, OwnedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use calloop::{EventLoop, LoopSignal, PostAction, channel};
|
||||
use reis::{
|
||||
calloop::{EisRequestSource, EisRequestSourceEvent},
|
||||
eis,
|
||||
enumflags2::BitFlags,
|
||||
request::{Connection, Device, DeviceCapability, EisRequest},
|
||||
};
|
||||
use reis::calloop::{EisRequestSource, EisRequestSourceEvent};
|
||||
use reis::eis;
|
||||
use reis::enumflags2::BitFlags;
|
||||
use reis::request::{Connection, Device, DeviceCapability, EisRequest};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::backend::Cancel;
|
||||
@@ -29,11 +21,7 @@ pub(crate) struct Worker {
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
pub(crate) fn start(
|
||||
capabilities: u32,
|
||||
keymap: &str,
|
||||
cancel: Arc<Cancel>,
|
||||
) -> Result<(Self, OwnedFd), String> {
|
||||
pub(crate) fn start(capabilities: u32, keymap: &str, cancel: Arc<Cancel>) -> Result<(Self, OwnedFd), String> {
|
||||
if keymap.is_empty() || keymap.len() > 256 * 1024 {
|
||||
return Err("Invalid captured keyboard map".into());
|
||||
}
|
||||
@@ -59,8 +47,7 @@ impl Worker {
|
||||
let _finish = Finish(cancel);
|
||||
let run = || {
|
||||
let context = eis::Context::new(server).map_err(|e| e.to_string())?;
|
||||
let mut event_loop =
|
||||
EventLoop::<State>::try_new().map_err(|e| e.to_string())?;
|
||||
let mut event_loop = EventLoop::<State>::try_new().map_err(|e| e.to_string())?;
|
||||
let signal = event_loop.get_signal();
|
||||
let (sender, receiver) = channel::sync_channel(4);
|
||||
event_loop
|
||||
@@ -72,22 +59,19 @@ impl Worker {
|
||||
.map_err(|e| e.to_string())?;
|
||||
event_loop
|
||||
.handle()
|
||||
.insert_source(
|
||||
EisRequestSource::new(context, 1),
|
||||
|event, connection, state| {
|
||||
let action = match event {
|
||||
Ok(event) => state.request(connection, event),
|
||||
Err(_) => {
|
||||
state.signal.stop();
|
||||
PostAction::Remove
|
||||
}
|
||||
};
|
||||
if connection.flush().is_err() {
|
||||
.insert_source(EisRequestSource::new(context, 1), |event, connection, state| {
|
||||
let action = match event {
|
||||
Ok(event) => state.request(connection, event),
|
||||
Err(_) => {
|
||||
state.signal.stop();
|
||||
PostAction::Remove
|
||||
}
|
||||
Ok(action)
|
||||
},
|
||||
)
|
||||
};
|
||||
if connection.flush().is_err() {
|
||||
state.signal.stop();
|
||||
}
|
||||
Ok(action)
|
||||
})
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut state = State {
|
||||
signal: signal.clone(),
|
||||
@@ -107,8 +91,7 @@ impl Worker {
|
||||
.map_err(|_| "Input transport startup cancelled".to_owned())?;
|
||||
let _ = event_loop.run(None, &mut state, |_| {});
|
||||
if let Some(connection) = state.connection.take() {
|
||||
connection
|
||||
.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
||||
connection.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
||||
}
|
||||
state.ready.store(false, Ordering::SeqCst);
|
||||
Ok::<(), String>(())
|
||||
@@ -121,14 +104,7 @@ impl Worker {
|
||||
let (sender, signal) = started
|
||||
.recv_timeout(std::time::Duration::from_secs(2))
|
||||
.map_err(|_| "Input transport startup timed out".to_owned())??;
|
||||
Ok((
|
||||
Self {
|
||||
sender,
|
||||
signal,
|
||||
ready,
|
||||
},
|
||||
client.into(),
|
||||
))
|
||||
Ok((Self { sender, signal, ready }, client.into()))
|
||||
}
|
||||
|
||||
pub(crate) fn send(&self, events: Vec<Value>) -> Result<(), String> {
|
||||
@@ -180,18 +156,14 @@ impl State {
|
||||
capabilities |= DeviceCapability::Keyboard;
|
||||
}
|
||||
if self.capabilities & 2 != 0 {
|
||||
capabilities |= DeviceCapability::Pointer
|
||||
| DeviceCapability::Button
|
||||
| DeviceCapability::Scroll;
|
||||
capabilities |= DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll;
|
||||
}
|
||||
let _ = connection.add_seat(Some("Ferese input capture"), capabilities);
|
||||
self.connection = Some(connection.clone());
|
||||
}
|
||||
EisRequestSourceEvent::Request(EisRequest::Bind(request)) => {
|
||||
let keyboard = request.capabilities.contains(DeviceCapability::Keyboard)
|
||||
&& self.capabilities & 1 != 0;
|
||||
let pointer = request.capabilities.contains(DeviceCapability::Pointer)
|
||||
&& self.capabilities & 2 != 0;
|
||||
let keyboard = request.capabilities.contains(DeviceCapability::Keyboard) && self.capabilities & 1 != 0;
|
||||
let pointer = request.capabilities.contains(DeviceCapability::Pointer) && self.capabilities & 2 != 0;
|
||||
if !keyboard && let Some(device) = self.keyboard.take() {
|
||||
device.remove();
|
||||
self.keyboard_ready = false;
|
||||
@@ -207,11 +179,7 @@ impl State {
|
||||
DeviceCapability::Keyboard.into(),
|
||||
|device| {
|
||||
if let Some(keyboard) = device.interface::<eis::Keyboard>() {
|
||||
keyboard.keymap(
|
||||
eis::keyboard::KeymapType::Xkb,
|
||||
self.keymap_size,
|
||||
self.keymap.as_fd(),
|
||||
);
|
||||
keyboard.keymap(eis::keyboard::KeymapType::Xkb, self.keymap_size, self.keymap.as_fd());
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -221,15 +189,11 @@ impl State {
|
||||
}
|
||||
if pointer && self.pointer.is_none() {
|
||||
let caps = request.capabilities
|
||||
& (DeviceCapability::Pointer
|
||||
| DeviceCapability::Button
|
||||
| DeviceCapability::Scroll);
|
||||
let device = request.seat.add_device(
|
||||
Some("Ferese pointer"),
|
||||
eis::device::DeviceType::Virtual,
|
||||
caps,
|
||||
|_| {},
|
||||
);
|
||||
& (DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll);
|
||||
let device =
|
||||
request
|
||||
.seat
|
||||
.add_device(Some("Ferese pointer"), eis::device::DeviceType::Virtual, caps, |_| {});
|
||||
self.pointer_ready = device.device().version() < 3;
|
||||
device.resumed();
|
||||
self.pointer = Some(device);
|
||||
@@ -263,14 +227,11 @@ impl State {
|
||||
return PostAction::Remove;
|
||||
}
|
||||
self.ready.store(
|
||||
(self.keyboard.is_none() || self.keyboard_ready)
|
||||
&& (self.pointer.is_none() || self.pointer_ready),
|
||||
(self.keyboard.is_none() || self.keyboard_ready) && (self.pointer.is_none() || self.pointer_ready),
|
||||
Ordering::SeqCst,
|
||||
);
|
||||
}
|
||||
EisRequestSourceEvent::Request(
|
||||
EisRequest::Disconnect | EisRequest::DeviceClosed(_),
|
||||
) => {
|
||||
EisRequestSourceEvent::Request(EisRequest::Disconnect | EisRequest::DeviceClosed(_)) => {
|
||||
self.signal.stop();
|
||||
return PostAction::Remove;
|
||||
}
|
||||
@@ -307,12 +268,7 @@ impl State {
|
||||
if let Some(device) = &self.keyboard
|
||||
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
||||
{
|
||||
for key in event["keys"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(Value::as_u64)
|
||||
{
|
||||
for key in event["keys"].as_array().into_iter().flatten().filter_map(Value::as_u64) {
|
||||
keyboard.key(key as u32, eis::keyboard::KeyState::Press);
|
||||
}
|
||||
device.frame(now());
|
||||
@@ -392,10 +348,7 @@ impl State {
|
||||
}
|
||||
|
||||
fn now() -> u64 {
|
||||
let mut time = libc::timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
};
|
||||
let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 };
|
||||
// SAFETY: the initialized output pointer covers a timespec allocation.
|
||||
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } == 0 {
|
||||
time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000
|
||||
@@ -406,12 +359,12 @@ fn now() -> u64 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::json;
|
||||
use std::{
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[ignore = "requires Python and the system libei receiver library"]
|
||||
@@ -419,8 +372,8 @@ mod tests {
|
||||
let cancel = Arc::new(Cancel::default());
|
||||
let map = "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; xkb_types { include \"complete\" }; xkb_compatibility { include \"complete\" }; xkb_symbols { include \"pc+us+inet(evdev)\" }; };";
|
||||
let (worker, fd) = Worker::start(3, map, cancel.clone()).unwrap();
|
||||
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../scripts/tests/fixtures/eis-receiver.py");
|
||||
let fixture =
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fixtures/eis-receiver.py");
|
||||
let mut client = Command::new("python3")
|
||||
.arg(fixture)
|
||||
.stdin(Stdio::from(fd))
|
||||
@@ -435,10 +388,7 @@ mod tests {
|
||||
if !worker.ready.load(Ordering::SeqCst) {
|
||||
let _ = client.kill();
|
||||
let output = client.wait_with_output().unwrap();
|
||||
panic!(
|
||||
"Receiver did not bind: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
panic!("Receiver did not bind: {}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
worker
|
||||
.send(vec![
|
||||
@@ -453,11 +403,7 @@ mod tests {
|
||||
])
|
||||
.unwrap();
|
||||
let output = client.wait_with_output().unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr));
|
||||
assert!(String::from_utf8_lossy(&output.stdout).contains("\"keymap\": true"));
|
||||
let deadline = Instant::now() + Duration::from_secs(2);
|
||||
while !cancel.stopped.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||
|
||||
+74
-92
@@ -1,21 +1,17 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedFd, OwnedObjectPath, Value},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
desktop::Requests,
|
||||
};
|
||||
use tokio::sync::{Mutex, Notify};
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value};
|
||||
|
||||
use crate::backend::{Cancel, Options, authorize};
|
||||
use crate::bridge::Bridge;
|
||||
use crate::desktop::Requests;
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
@@ -68,10 +64,7 @@ impl Inhibit {
|
||||
if let Ok(emitter) = SignalEmitter::new(connection, path) {
|
||||
let _ = MonitorObject::closed(&emitter).await;
|
||||
}
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<MonitorObject, _>(path)
|
||||
.await;
|
||||
let _ = connection.object_server().remove::<MonitorObject, _>(path).await;
|
||||
self.changed.notify_one();
|
||||
}
|
||||
}
|
||||
@@ -114,9 +107,7 @@ impl Inhibit {
|
||||
since = state["revision"].as_u64();
|
||||
for (path, monitor) in monitors {
|
||||
if !state["monitor-owners"].as_array().is_some_and(|owners| {
|
||||
owners
|
||||
.iter()
|
||||
.any(|owner| owner.as_u64() == Some(monitor.native_owner))
|
||||
owners.iter().any(|owner| owner.as_u64() == Some(monitor.native_owner))
|
||||
}) {
|
||||
self.end(&connection, &path).await;
|
||||
continue;
|
||||
@@ -128,8 +119,7 @@ impl Inhibit {
|
||||
let transitions = match *last {
|
||||
None => vec![state.clone()],
|
||||
Some((_, _, revision, _)) => {
|
||||
let events =
|
||||
state["transitions"].as_array().cloned().unwrap_or_default();
|
||||
let events = state["transitions"].as_array().cloned().unwrap_or_default();
|
||||
if events
|
||||
.first()
|
||||
.and_then(|event| event["revision"].as_u64())
|
||||
@@ -141,11 +131,7 @@ impl Inhibit {
|
||||
}
|
||||
events
|
||||
.into_iter()
|
||||
.filter(|event| {
|
||||
event["revision"]
|
||||
.as_u64()
|
||||
.is_some_and(|next| next > revision)
|
||||
})
|
||||
.filter(|event| event["revision"].as_u64().is_some_and(|next| next > revision))
|
||||
.collect()
|
||||
}
|
||||
};
|
||||
@@ -154,19 +140,13 @@ impl Inhibit {
|
||||
let phase = event["session-state"].as_u64().unwrap_or(1) as u32;
|
||||
let revision = event["revision"].as_u64().unwrap_or(0);
|
||||
let token = event["query-token"].as_u64().unwrap_or(0) as u32;
|
||||
let changed = last.as_ref().is_none_or(|old| {
|
||||
old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token)
|
||||
});
|
||||
let changed = last
|
||||
.as_ref()
|
||||
.is_none_or(|old| old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token));
|
||||
if changed {
|
||||
let values = HashMap::from([
|
||||
(
|
||||
"screensaver-active".into(),
|
||||
Value::from(locked).try_into().unwrap(),
|
||||
),
|
||||
(
|
||||
"session-state".into(),
|
||||
Value::from(phase).try_into().unwrap(),
|
||||
),
|
||||
("screensaver-active".into(), Value::from(locked).try_into().unwrap()),
|
||||
("session-state".into(), Value::from(phase).try_into().unwrap()),
|
||||
]);
|
||||
if let Ok(emitter) = SignalEmitter::new(&connection, PATH) {
|
||||
if Self::state_changed(
|
||||
@@ -203,9 +183,7 @@ impl Inhibit {
|
||||
use futures_util::StreamExt;
|
||||
loop {
|
||||
let result: Result<(), String> = async {
|
||||
let connection = Connection::system()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let connection = Connection::system().await.map_err(|error| error.to_string())?;
|
||||
let proxy = zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.login1",
|
||||
@@ -219,16 +197,10 @@ impl Inhibit {
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
while let Some(message) = signals.next().await {
|
||||
let (ending,): (bool,) = message
|
||||
.body()
|
||||
.deserialize()
|
||||
.map_err(|error| error.to_string())?;
|
||||
let (ending,): (bool,) = message.body().deserialize().map_err(|error| error.to_string())?;
|
||||
if let Ok(bridge) = Bridge::connect() {
|
||||
let _ = bridge
|
||||
.call(
|
||||
"logind-session-ending",
|
||||
serde_json::json!({"ending": ending}),
|
||||
)
|
||||
.call("logind-session-ending", serde_json::json!({"ending": ending}))
|
||||
.await;
|
||||
bridge.close();
|
||||
}
|
||||
@@ -246,9 +218,7 @@ impl Inhibit {
|
||||
|
||||
fn flags_what(flags: u32) -> zbus::fdo::Result<String> {
|
||||
if flags == 0 || flags & !15 != 0 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid inhibition flags".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Invalid inhibition flags".into()));
|
||||
}
|
||||
let mut what = Vec::new();
|
||||
if flags & 1 != 0 {
|
||||
@@ -268,9 +238,7 @@ async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result<Option<O
|
||||
return Ok(None);
|
||||
}
|
||||
tokio::time::timeout(Duration::from_secs(5), async {
|
||||
let connection = Connection::system()
|
||||
.await
|
||||
.map_err(|error| error.to_string())?;
|
||||
let connection = Connection::system().await.map_err(|error| error.to_string())?;
|
||||
let proxy = zbus::Proxy::new(
|
||||
&connection,
|
||||
"org.freedesktop.login1",
|
||||
@@ -309,13 +277,8 @@ impl Inhibit {
|
||||
.transpose()
|
||||
.map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid inhibition reason".into()))?
|
||||
.unwrap_or_else(|| "Application requested session inhibition".into());
|
||||
if app_id.len() > 512
|
||||
|| reason.len() > 1024
|
||||
|| app_id.chars().chain(reason.chars()).any(char::is_control)
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid application or reason".into(),
|
||||
));
|
||||
if app_id.len() > 512 || reason.len() > 1024 || app_id.chars().chain(reason.chars()).any(char::is_control) {
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Invalid application or reason".into()));
|
||||
}
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let bridge = match Bridge::connect() {
|
||||
@@ -351,9 +314,7 @@ impl Inhibit {
|
||||
bridge.close();
|
||||
self.requests.end(connection, &handle).await;
|
||||
Err(zbus::fdo::Error::Failed(
|
||||
result
|
||||
.err()
|
||||
.unwrap_or_else(|| "Inhibition was cancelled".into()),
|
||||
result.err().unwrap_or_else(|| "Inhibition was cancelled".into()),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -369,15 +330,11 @@ impl Inhibit {
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<u32> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
if !session_handle
|
||||
.as_str()
|
||||
.starts_with(&format!("{PATH}/session/"))
|
||||
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|
||||
|| session_handle.as_str().len() > 512
|
||||
|| app_id.len() > 512
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid session monitor".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Invalid session monitor".into()));
|
||||
}
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let mut monitors = self.monitors.lock().await;
|
||||
@@ -394,30 +351,59 @@ impl Inhibit {
|
||||
_ = cancel.wait() => Err("Monitor creation was cancelled".into()),
|
||||
result = bridge.call("portal-monitor-register", serde_json::json!({})) => result,
|
||||
};
|
||||
let native = match result { Ok(native) => native, Err(error) => { bridge.close(); return Err(zbus::fdo::Error::Failed(error)); } };
|
||||
let native = match result {
|
||||
Ok(native) => native,
|
||||
Err(error) => {
|
||||
bridge.close();
|
||||
return Err(zbus::fdo::Error::Failed(error));
|
||||
}
|
||||
};
|
||||
let Some(native_owner) = native["monitor-owner"].as_u64() else {
|
||||
bridge.close();
|
||||
return Err(zbus::fdo::Error::Failed("Missing monitor owner".into()));
|
||||
};
|
||||
let exported = connection.object_server().at(&session_handle, MonitorObject {
|
||||
backend: self.clone(), path: session_handle.to_string(), owner: owner.clone(), cancel: cancel.clone(),
|
||||
}).await;
|
||||
let exported = connection
|
||||
.object_server()
|
||||
.at(
|
||||
&session_handle,
|
||||
MonitorObject {
|
||||
backend: self.clone(),
|
||||
path: session_handle.to_string(),
|
||||
owner: owner.clone(),
|
||||
cancel: cancel.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
match exported {
|
||||
Ok(true) if !cancel.stopped.load(Ordering::SeqCst) => {
|
||||
monitors.insert(session_handle.to_string(), Arc::new(Monitor {
|
||||
owner, cancel: cancel.clone(), last: Mutex::new(None), bridge,
|
||||
native_owner,
|
||||
}));
|
||||
monitors.insert(
|
||||
session_handle.to_string(),
|
||||
Arc::new(Monitor {
|
||||
owner,
|
||||
cancel: cancel.clone(),
|
||||
last: Mutex::new(None),
|
||||
bridge,
|
||||
native_owner,
|
||||
}),
|
||||
);
|
||||
self.changed.notify_one();
|
||||
Ok(0)
|
||||
}
|
||||
result => {
|
||||
bridge.close();
|
||||
if matches!(result, Ok(true)) { let _ = connection.object_server().remove::<MonitorObject, _>(&session_handle).await; }
|
||||
result.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 }).map_err(Into::into)
|
||||
if matches!(result, Ok(true)) {
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<MonitorObject, _>(&session_handle)
|
||||
.await;
|
||||
}
|
||||
result
|
||||
.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 })
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}.await;
|
||||
}
|
||||
.await;
|
||||
attempt
|
||||
};
|
||||
drop(monitors);
|
||||
@@ -442,9 +428,7 @@ impl Inhibit {
|
||||
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Not your session monitor".into()))?;
|
||||
let last = monitor.last.lock().await;
|
||||
if monitor.cancel.stopped.load(Ordering::SeqCst) {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Session monitor closed".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::AccessDenied("Session monitor closed".into()));
|
||||
}
|
||||
if let Some((_, 2, _, token)) = *last {
|
||||
monitor
|
||||
@@ -484,9 +468,7 @@ impl MonitorObject {
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
if authorize(connection, &header).await? != self.owner {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Not your session monitor".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::AccessDenied("Not your session monitor".into()));
|
||||
}
|
||||
self.cancel.stop();
|
||||
self.backend.end(connection, &self.path).await;
|
||||
|
||||
+47
-108
@@ -1,24 +1,19 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{Arc, atomic::Ordering},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedFd, OwnedObjectPath, Value},
|
||||
};
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value};
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
desktop::Requests,
|
||||
eis::Worker,
|
||||
};
|
||||
use crate::backend::{Cancel, Options, authorize};
|
||||
use crate::bridge::Bridge;
|
||||
use crate::desktop::Requests;
|
||||
use crate::eis::Worker;
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
@@ -92,10 +87,7 @@ impl InputCapture {
|
||||
let connection = connection.clone();
|
||||
let path = path.to_owned();
|
||||
tokio::spawn(async move {
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<SessionObject, _>(path)
|
||||
.await;
|
||||
let _ = connection.object_server().remove::<SessionObject, _>(path).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -106,9 +98,7 @@ impl InputCapture {
|
||||
.lock()
|
||||
.await
|
||||
.iter()
|
||||
.filter_map(|(path, session)| {
|
||||
(Some(session.owner.as_str()) != owner).then_some(path.clone())
|
||||
})
|
||||
.filter_map(|(path, session)| (Some(session.owner.as_str()) != owner).then_some(path.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
for path in stale {
|
||||
self.end(connection, &path).await;
|
||||
@@ -152,10 +142,7 @@ impl InputCapture {
|
||||
if session.cancel.stopped.load(Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
let transport = state
|
||||
.worker
|
||||
.as_ref()
|
||||
.map(|worker| worker.send(events.clone()));
|
||||
let transport = state.worker.as_ref().map(|worker| worker.send(events.clone()));
|
||||
if transport.is_some_and(|result| result.is_err()) {
|
||||
break;
|
||||
}
|
||||
@@ -179,11 +166,9 @@ impl InputCapture {
|
||||
.as_array()
|
||||
.filter(|position| position.len() == 2)
|
||||
{
|
||||
if let Ok(position) = Value::from((
|
||||
position[0].as_f64().unwrap_or(0.0),
|
||||
position[1].as_f64().unwrap_or(0.0),
|
||||
))
|
||||
.try_to_owned()
|
||||
if let Ok(position) =
|
||||
Value::from((position[0].as_f64().unwrap_or(0.0), position[1].as_f64().unwrap_or(0.0)))
|
||||
.try_to_owned()
|
||||
{
|
||||
options.insert("cursor_position".into(), position);
|
||||
}
|
||||
@@ -201,12 +186,8 @@ impl InputCapture {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Some("disabled") => {
|
||||
Self::disabled(&emitter, path.clone(), Options::new()).await
|
||||
}
|
||||
Some("zones") => {
|
||||
Self::zones_changed(&emitter, path.clone(), Options::new()).await
|
||||
}
|
||||
Some("disabled") => Self::disabled(&emitter, path.clone(), Options::new()).await,
|
||||
Some("zones") => Self::zones_changed(&emitter, path.clone(), Options::new()).await,
|
||||
Some("closed") => {
|
||||
closed = true;
|
||||
Ok(())
|
||||
@@ -238,8 +219,7 @@ fn capabilities(options: &Options) -> zbus::fdo::Result<u32> {
|
||||
let requested = options
|
||||
.get("capabilities")
|
||||
.ok_or_else(|| invalid("Missing requested capabilities"))?;
|
||||
let requested =
|
||||
u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?;
|
||||
let requested = u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?;
|
||||
if requested == 0 || requested & 3 == 0 {
|
||||
return Err(invalid("No supported capabilities requested"));
|
||||
}
|
||||
@@ -267,9 +247,7 @@ impl InputCapture {
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<Options> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
if !session_handle
|
||||
.as_str()
|
||||
.starts_with(&format!("{PATH}/session/"))
|
||||
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|
||||
|| session_handle.as_str().len() > 512
|
||||
|| app_id.len() > 512
|
||||
{
|
||||
@@ -365,9 +343,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let caps = capabilities(&options)?;
|
||||
crate::restore::persist_mode(&options)?;
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
@@ -406,18 +382,11 @@ impl InputCapture {
|
||||
let reply = bridge
|
||||
.call("input-capture-register", json!({"capabilities":caps}))
|
||||
.await?;
|
||||
let id = reply["session"]
|
||||
.as_u64()
|
||||
.ok_or("Missing capture session ID")?;
|
||||
let keymap = reply["keymap"]
|
||||
.as_str()
|
||||
.ok_or("Missing keyboard map")?
|
||||
.to_owned();
|
||||
let id = reply["session"].as_u64().ok_or("Missing capture session ID")?;
|
||||
let keymap = reply["keymap"].as_str().ok_or("Missing keyboard map")?.to_owned();
|
||||
let watch = Bridge::connect()?;
|
||||
let mut state = session.state.lock().await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
{
|
||||
if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) {
|
||||
bridge.close();
|
||||
return Err("Input capture approval was cancelled".to_owned());
|
||||
}
|
||||
@@ -473,9 +442,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let result = tokio::select! {
|
||||
@@ -485,23 +452,18 @@ impl InputCapture {
|
||||
result = bridge.call("input-capture-zones", json!({"session":id})) => Some(result),
|
||||
};
|
||||
self.requests.end(connection, &handle).await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| result.is_none()
|
||||
{
|
||||
if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) || result.is_none() {
|
||||
return Ok((1, Options::new()));
|
||||
}
|
||||
let result = result.unwrap().map_err(failed)?;
|
||||
let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone())
|
||||
.map_err(|_| invalid("Invalid native capture zones"))?;
|
||||
let zones: Vec<(u32, u32, i32, i32)> =
|
||||
serde_json::from_value(result["zones"].clone()).map_err(|_| invalid("Invalid native capture zones"))?;
|
||||
Ok((
|
||||
0,
|
||||
Options::from([
|
||||
(
|
||||
"zones".into(),
|
||||
Value::from(zones)
|
||||
.try_to_owned()
|
||||
.map_err(|e| failed(e.to_string()))?,
|
||||
Value::from(zones).try_to_owned().map_err(|e| failed(e.to_string()))?,
|
||||
),
|
||||
(
|
||||
"zone_set".into(),
|
||||
@@ -523,9 +485,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
if barriers.len() > 64 {
|
||||
return Err(invalid("Too many pointer barriers"));
|
||||
@@ -533,20 +493,15 @@ impl InputCapture {
|
||||
let barriers = barriers
|
||||
.into_iter()
|
||||
.map(|barrier| {
|
||||
let id = u32::try_from(
|
||||
barrier
|
||||
.get("barrier_id")
|
||||
.ok_or_else(|| invalid("Missing barrier ID"))?,
|
||||
)
|
||||
.map_err(|_| invalid("Invalid barrier ID"))?;
|
||||
let id = u32::try_from(barrier.get("barrier_id").ok_or_else(|| invalid("Missing barrier ID"))?)
|
||||
.map_err(|_| invalid("Invalid barrier ID"))?;
|
||||
let position = barrier
|
||||
.get("position")
|
||||
.ok_or_else(|| invalid("Missing barrier position"))?
|
||||
.try_clone()
|
||||
.map_err(|_| invalid("Invalid position"))?;
|
||||
let position: (i32, i32, i32, i32) = position
|
||||
.try_into()
|
||||
.map_err(|_| invalid("Invalid barrier position"))?;
|
||||
let position: (i32, i32, i32, i32) =
|
||||
position.try_into().map_err(|_| invalid("Invalid barrier position"))?;
|
||||
Ok(json!({"id":id,"position":[position.0,position.1,position.2,position.3]}))
|
||||
})
|
||||
.collect::<zbus::fdo::Result<Vec<_>>>()?;
|
||||
@@ -558,10 +513,7 @@ impl InputCapture {
|
||||
result = bridge.call("input-capture-barriers", json!({"session":id,"zone_set":zone_set,"barriers":barriers})) => Some(result),
|
||||
};
|
||||
self.requests.end(connection, &handle).await;
|
||||
if cancel.stopped.load(Ordering::SeqCst)
|
||||
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| result.is_none()
|
||||
{
|
||||
if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) || result.is_none() {
|
||||
self.end(connection, session_handle.as_str()).await;
|
||||
return Ok((1, Options::new()));
|
||||
}
|
||||
@@ -588,9 +540,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<OwnedFd> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
Self::native(&session).await?;
|
||||
let mut state = session.state.lock().await;
|
||||
if state.worker.is_some() {
|
||||
@@ -599,11 +549,10 @@ impl InputCapture {
|
||||
let capabilities = state.capabilities;
|
||||
let keymap = state.keymap.clone();
|
||||
let cancel = session.cancel.clone();
|
||||
let (worker, fd) =
|
||||
tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel))
|
||||
.await
|
||||
.map_err(|e| failed(e.to_string()))?
|
||||
.map_err(failed)?;
|
||||
let (worker, fd) = tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel))
|
||||
.await
|
||||
.map_err(|e| failed(e.to_string()))?
|
||||
.map_err(failed)?;
|
||||
state.worker = Some(worker);
|
||||
Ok(fd.into())
|
||||
}
|
||||
@@ -616,9 +565,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let ready = session
|
||||
.state
|
||||
@@ -630,9 +577,7 @@ impl InputCapture {
|
||||
.ok_or_else(|| invalid("Connect to EIS before enabling input capture"))?;
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||
while !ready.load(Ordering::SeqCst) {
|
||||
if session.cancel.stopped.load(Ordering::SeqCst)
|
||||
|| tokio::time::Instant::now() >= deadline
|
||||
{
|
||||
if session.cancel.stopped.load(Ordering::SeqCst) || tokio::time::Instant::now() >= deadline {
|
||||
return Err(invalid("EIS receiver has not bound an input device"));
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
@@ -652,9 +597,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
bridge
|
||||
.call("input-capture-disable", json!({"session":id}))
|
||||
@@ -671,9 +614,7 @@ impl InputCapture {
|
||||
#[zbus(connection)] connection: &Connection,
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let session = self
|
||||
.session(connection, &header, &session_handle, &app_id)
|
||||
.await?;
|
||||
let session = self.session(connection, &header, &session_handle, &app_id).await?;
|
||||
let (bridge, id) = Self::native(&session).await?;
|
||||
let activation = options
|
||||
.get("activation_id")
|
||||
@@ -770,9 +711,7 @@ mod tests {
|
||||
fn requested_capabilities_are_required_and_supported_subset_is_returned() {
|
||||
assert!(capabilities(&Options::new()).is_err());
|
||||
for requested in [0u32, 4, 8] {
|
||||
assert!(
|
||||
capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err()
|
||||
);
|
||||
assert!(capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err());
|
||||
}
|
||||
for (requested, supported) in [(1u32, 1), (2, 2), (3, 3), (7, 3), (9, 1)] {
|
||||
assert_eq!(
|
||||
|
||||
+14
-14
@@ -1,6 +1,13 @@
|
||||
use std::{borrow::Cow, collections::HashMap, sync::Arc, time::Duration};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use zbus::{Connection, message::Header, object_server::SignalEmitter, zvariant::Value};
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::Value;
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
const KEYS: [&str; 7] = [
|
||||
@@ -55,8 +62,7 @@ impl Lockdown {
|
||||
connection: &Connection,
|
||||
header: Option<Header<'_>>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let header =
|
||||
header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?;
|
||||
let header = header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?;
|
||||
crate::backend::authorize(connection, &header).await?;
|
||||
let mut current = self.0.write().await;
|
||||
store(index, value).map_err(zbus::fdo::Error::Failed)?;
|
||||
@@ -209,8 +215,7 @@ fn store(index: usize, value: bool) -> Result<(), String> {
|
||||
Err(error) => return Err(error.to_string()),
|
||||
_ => return Err("Configuration is too large".into()),
|
||||
};
|
||||
let mut document =
|
||||
ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
|
||||
let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
|
||||
if values(&document).is_none() {
|
||||
return Err("Invalid lockdown policy".into());
|
||||
}
|
||||
@@ -223,11 +228,8 @@ fn store(index: usize, value: bool) -> Result<(), String> {
|
||||
let directory = path.parent().ok_or("Invalid config path")?;
|
||||
std::fs::create_dir_all(directory).map_err(|error| error.to_string())?;
|
||||
let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut file, document.to_string().as_bytes())
|
||||
.map_err(|error| error.to_string())?;
|
||||
file.as_file()
|
||||
.sync_all()
|
||||
.map_err(|error| error.to_string())?;
|
||||
std::io::Write::write_all(&mut file, document.to_string().as_bytes()).map_err(|error| error.to_string())?;
|
||||
file.as_file().sync_all().map_err(|error| error.to_string())?;
|
||||
if std::fs::read_to_string(&path).unwrap_or_default() != source {
|
||||
return Err("Configuration changed; please try again".into());
|
||||
}
|
||||
@@ -250,9 +252,7 @@ mod tests {
|
||||
let values = values(&enabled).unwrap();
|
||||
assert!(values[1] && values[4]);
|
||||
assert!(!values[0]);
|
||||
let invalid =
|
||||
ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }")
|
||||
.unwrap();
|
||||
let invalid = ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }").unwrap();
|
||||
assert!(super::values(&invalid).is_none());
|
||||
for source in ["portals #false", "portals { lockdown \"bad\"; }"] {
|
||||
let invalid = ferese_config::Document::parse(source).unwrap();
|
||||
|
||||
+4
-8
@@ -19,18 +19,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args: Vec<_> = std::env::args().skip(1).collect();
|
||||
|
||||
match args.as_slice() {
|
||||
[flag, name, cursor] if flag == "--stream" => {
|
||||
stream::run(name.clone(), cursor == "embedded", None)
|
||||
}
|
||||
[flag, name, cursor] if flag == "--stream" => stream::run(name.clone(), cursor == "embedded", None),
|
||||
[flag, id, cursor] if flag == "--stream-window" => {
|
||||
let id: u64 = id.parse()?;
|
||||
stream::run(format!("window:{id}"), cursor == "embedded", None)
|
||||
}
|
||||
[flag, name, cursor, generation] if flag == "--stream" => stream::run(
|
||||
name.clone(),
|
||||
cursor == "embedded",
|
||||
Some(generation.parse()?),
|
||||
),
|
||||
[flag, name, cursor, generation] if flag == "--stream" => {
|
||||
stream::run(name.clone(), cursor == "embedded", Some(generation.parse()?))
|
||||
}
|
||||
[flag] if flag == "--sources" => {
|
||||
let capture = capture::Capture::connect(&std::sync::atomic::AtomicBool::new(false))?;
|
||||
println!("{}", serde_json::to_string(&capture.sources())?);
|
||||
|
||||
+12
-23
@@ -1,12 +1,9 @@
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::{wl_registry, wl_surface},
|
||||
};
|
||||
use wayland_protocols::xdg::foreign::zv2::client::{
|
||||
zxdg_imported_v2::{self, ZxdgImportedV2},
|
||||
zxdg_importer_v2::ZxdgImporterV2,
|
||||
};
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::{wl_registry, wl_surface};
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
||||
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2;
|
||||
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2::ZxdgImportedV2;
|
||||
use wayland_protocols::xdg::foreign::zv2::client::zxdg_importer_v2::ZxdgImporterV2;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Parent {
|
||||
@@ -16,10 +13,7 @@ pub(crate) struct Parent {
|
||||
}
|
||||
|
||||
impl Parent {
|
||||
pub(crate) fn attach(
|
||||
window: &dyn cosmic::iced::window::Window,
|
||||
parent: &str,
|
||||
) -> Result<Option<Self>, String> {
|
||||
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<Option<Self>, String> {
|
||||
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
|
||||
if parent.is_empty() {
|
||||
return Ok(None);
|
||||
@@ -36,9 +30,8 @@ impl Parent {
|
||||
return Err("Portal parenting requires Wayland".into());
|
||||
};
|
||||
// The window lends live handles; the borrowed backend never owns the display.
|
||||
let backend = unsafe {
|
||||
wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast())
|
||||
};
|
||||
let backend =
|
||||
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
|
||||
let connection = Connection::from_backend(backend);
|
||||
let id = unsafe {
|
||||
wayland_client::backend::ObjectId::from_ptr(
|
||||
@@ -47,19 +40,15 @@ impl Parent {
|
||||
)
|
||||
}
|
||||
.map_err(|error| error.to_string())?;
|
||||
let surface =
|
||||
wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let (globals, mut queue) =
|
||||
registry_queue_init::<State>(&connection).map_err(|error| error.to_string())?;
|
||||
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
|
||||
let (globals, mut queue) = registry_queue_init::<State>(&connection).map_err(|error| error.to_string())?;
|
||||
let qh = queue.handle();
|
||||
let importer = globals
|
||||
.bind::<ZxdgImporterV2, _, _>(&qh, 1..=1, ())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let imported = importer.import_toplevel(handle.into(), &qh, ());
|
||||
let mut state = State::default();
|
||||
queue
|
||||
.roundtrip(&mut state)
|
||||
.map_err(|error| error.to_string())?;
|
||||
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
|
||||
if state.invalid {
|
||||
imported.destroy();
|
||||
importer.destroy();
|
||||
|
||||
+23
-65
@@ -1,20 +1,16 @@
|
||||
use crate::{
|
||||
backend::{Options, authorize},
|
||||
desktop::{Requests, consent, ipc, reply},
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedObjectPath, OwnedValue, Value},
|
||||
};
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
|
||||
|
||||
use crate::backend::{Options, authorize};
|
||||
use crate::desktop::{Requests, consent, ipc, reply};
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
|
||||
@@ -56,19 +52,12 @@ fn usb_label(info: &Options, id: &str) -> String {
|
||||
.and_then(|value| HashMap::<String, OwnedValue>::try_from(value).ok())
|
||||
.unwrap_or_default();
|
||||
let property = |keys: &[&str]| {
|
||||
keys.iter().find_map(|key| {
|
||||
properties
|
||||
.get(*key)
|
||||
.and_then(|value| <&str>::try_from(value).ok())
|
||||
})
|
||||
keys.iter()
|
||||
.find_map(|key| properties.get(*key).and_then(|value| <&str>::try_from(value).ok()))
|
||||
};
|
||||
let vendor = property(&["ID_VENDOR_FROM_DATABASE", "ID_VENDOR", "ID_VENDOR_ID"]);
|
||||
let model = property(&["ID_MODEL_FROM_DATABASE", "ID_MODEL", "ID_MODEL_ID"]);
|
||||
let label = [vendor, model]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let label = [vendor, model].into_iter().flatten().collect::<Vec<_>>().join(" ");
|
||||
let label = if label.is_empty() { id } else { &label };
|
||||
label
|
||||
.chars()
|
||||
@@ -102,27 +91,11 @@ async fn usb_consent(
|
||||
description.push_str(&format!(
|
||||
"\n{} — {}",
|
||||
label,
|
||||
if writable {
|
||||
"read and write"
|
||||
} else {
|
||||
"read only"
|
||||
}
|
||||
));
|
||||
selected.push((
|
||||
id,
|
||||
HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]),
|
||||
if writable { "read and write" } else { "read only" }
|
||||
));
|
||||
selected.push((id, HashMap::from([("writable".to_owned(), OwnedValue::from(writable))])));
|
||||
}
|
||||
if !consent(
|
||||
app,
|
||||
parent,
|
||||
"USB device access",
|
||||
&description,
|
||||
"Allow access",
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
if !consent(app, parent, "USB device access", &description, "Allow access", None).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(HashMap::from([(
|
||||
@@ -212,8 +185,7 @@ impl Background {
|
||||
let directory = dirs::config_dir()
|
||||
.ok_or_else(|| zbus::fdo::Error::Failed("Missing config directory".into()))?
|
||||
.join("autostart");
|
||||
set_autostart(&directory, &app_id, enable, &commandline, flags)
|
||||
.map_err(zbus::fdo::Error::InvalidArgs)
|
||||
set_autostart(&directory, &app_id, enable, &commandline, flags).map_err(zbus::fdo::Error::InvalidArgs)
|
||||
}
|
||||
|
||||
#[zbus(signal)]
|
||||
@@ -254,13 +226,7 @@ fn app_states(windows: &serde_json::Value) -> HashMap<String, u32> {
|
||||
apps
|
||||
}
|
||||
|
||||
fn set_autostart(
|
||||
directory: &Path,
|
||||
app: &str,
|
||||
enable: bool,
|
||||
argv: &[String],
|
||||
flags: u32,
|
||||
) -> Result<bool, String> {
|
||||
fn set_autostart(directory: &Path, app: &str, enable: bool, argv: &[String], flags: u32) -> Result<bool, String> {
|
||||
if app.is_empty()
|
||||
|| app.len() > 255
|
||||
|| !app
|
||||
@@ -287,9 +253,7 @@ fn set_autostart(
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
if argv.len() > 128
|
||||
|| (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0)
|
||||
{
|
||||
if argv.len() > 128 || (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0) {
|
||||
return Err("Invalid autostart command".into());
|
||||
}
|
||||
let exec = argv
|
||||
@@ -377,15 +341,9 @@ mod tests {
|
||||
"ID_VENDOR_FROM_DATABASE".to_owned(),
|
||||
Value::from("Example Vendor").try_to_owned().unwrap(),
|
||||
),
|
||||
(
|
||||
"ID_MODEL".to_owned(),
|
||||
Value::from("Gamepad").try_to_owned().unwrap(),
|
||||
),
|
||||
("ID_MODEL".to_owned(), Value::from("Gamepad").try_to_owned().unwrap()),
|
||||
]);
|
||||
let info = HashMap::from([(
|
||||
"properties".into(),
|
||||
Value::from(properties).try_to_owned().unwrap(),
|
||||
)]);
|
||||
let info = HashMap::from([("properties".into(), Value::from(properties).try_to_owned().unwrap())]);
|
||||
assert_eq!(usb_label(&info, "opaque-id"), "Example Vendor Gamepad");
|
||||
assert_eq!(usb_label(&Options::new(), "fallback-id"), "fallback-id");
|
||||
}
|
||||
|
||||
+30
-66
@@ -1,13 +1,13 @@
|
||||
use crate::capture::Source;
|
||||
use cosmic::{
|
||||
ApplicationExt, Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Length},
|
||||
widget::{button, column, container, row, scrollable},
|
||||
};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, column, container, row, scrollable};
|
||||
use cosmic::{ApplicationExt, Element};
|
||||
use ferese_theme::icons::symbolic as glyph;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use crate::capture::Source;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct Prompt {
|
||||
@@ -82,9 +82,7 @@ struct Picker {
|
||||
|
||||
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut input = String::new();
|
||||
std::io::stdin()
|
||||
.take(256 * 1024 + 1)
|
||||
.read_to_string(&mut input)?;
|
||||
std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?;
|
||||
if input.len() > 256 * 1024 {
|
||||
return Err("Picker request is too large".into());
|
||||
}
|
||||
@@ -95,8 +93,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
|
||||
let (theme, font, background, palette) = appearance();
|
||||
let height =
|
||||
180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. };
|
||||
let height = 180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. };
|
||||
cosmic::app::run::<Picker>(
|
||||
Settings::default()
|
||||
.size(cosmic::iced::Size::new(400., height))
|
||||
@@ -114,12 +111,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
impl cosmic::Application for Picker {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = (
|
||||
Prompt,
|
||||
cosmic::iced::Color,
|
||||
ferese_theme::Palette,
|
||||
cosmic::font::Font,
|
||||
);
|
||||
type Flags = (Prompt, cosmic::iced::Color, ferese_theme::Palette, cosmic::font::Font);
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "dev.ferese.ScreenShare";
|
||||
|
||||
@@ -131,10 +123,7 @@ impl cosmic::Application for Picker {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(
|
||||
mut core: Core,
|
||||
(prompt, background, palette, font): Self::Flags,
|
||||
) -> (Self, Task<Message>) {
|
||||
fn init(mut core: Core, (prompt, background, palette, font): Self::Flags) -> (Self, Task<Message>) {
|
||||
core.window.show_headerbar = false;
|
||||
core.window.border_padding = Some(0);
|
||||
core.window.content_container = false;
|
||||
@@ -171,9 +160,7 @@ impl cosmic::Application for Picker {
|
||||
|
||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||
Some(Message::WindowOpened(id))
|
||||
}
|
||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::WindowOpened(id)),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
@@ -183,12 +170,8 @@ impl cosmic::Application for Picker {
|
||||
Message::WindowOpened(id) => {
|
||||
let parent = self.prompt.parent.clone();
|
||||
return cosmic::iced::window::run(id, move |window| {
|
||||
let parent =
|
||||
crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
|
||||
Ok((
|
||||
parent,
|
||||
ferese_theme::material::ModalMaterial::attach(window),
|
||||
))
|
||||
let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
|
||||
Ok((parent, ferese_theme::material::ModalMaterial::attach(window)))
|
||||
})
|
||||
.map(|result| cosmic::Action::App(Message::Attached(result)));
|
||||
}
|
||||
@@ -216,11 +199,7 @@ impl cosmic::Application for Picker {
|
||||
}
|
||||
Message::Remember if self.can_remember() => self.remember = !self.remember,
|
||||
Message::Share if !self.selected.is_empty() => {
|
||||
let names: Vec<_> = self
|
||||
.selected
|
||||
.iter()
|
||||
.map(|i| &self.prompt.sources[*i].name)
|
||||
.collect();
|
||||
let names: Vec<_> = self.selected.iter().map(|i| &self.prompt.sources[*i].name).collect();
|
||||
if self.prompt.window_capture {
|
||||
println!("{}", serde_json::to_string(&names).unwrap());
|
||||
} else {
|
||||
@@ -288,26 +267,20 @@ impl cosmic::Application for Picker {
|
||||
for (index, source) in self.prompt.sources.iter().enumerate() {
|
||||
let is_window = self.prompt.window_capture || source.window_id().is_some();
|
||||
if !self.prompt.window_capture && previous_kind != Some(is_window) {
|
||||
sources = sources.push(
|
||||
self.text(if is_window { "Windows" } else { "Displays" })
|
||||
.size(12),
|
||||
);
|
||||
sources = sources.push(self.text(if is_window { "Windows" } else { "Displays" }).size(12));
|
||||
previous_kind = Some(is_window);
|
||||
}
|
||||
let selected = self.selected.contains(&index);
|
||||
let mut entry = row![
|
||||
glyph(ferese_theme::icons::DISPLAY, 24),
|
||||
column![
|
||||
self.text(
|
||||
if self.prompt.window_capture || source.window_id().is_some() {
|
||||
&source.label
|
||||
} else {
|
||||
&source.name
|
||||
}
|
||||
)
|
||||
self.text(if self.prompt.window_capture || source.window_id().is_some() {
|
||||
&source.label
|
||||
} else {
|
||||
&source.name
|
||||
})
|
||||
.size(14),
|
||||
self.text(format!("{} × {}", source.width, source.height))
|
||||
.size(12)
|
||||
self.text(format!("{} × {}", source.width, source.height)).size(12)
|
||||
]
|
||||
.spacing(3)
|
||||
.width(Length::Fill),
|
||||
@@ -329,8 +302,7 @@ impl cosmic::Application for Picker {
|
||||
.on_press(Message::Select(index)),
|
||||
);
|
||||
}
|
||||
content = content
|
||||
.push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height())));
|
||||
content = content.push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height())));
|
||||
content = content.push(
|
||||
self.text(if self.prompt.window_capture {
|
||||
"Only the selected window will be captured."
|
||||
@@ -354,21 +326,14 @@ impl cosmic::Application for Picker {
|
||||
ferese_theme::menus::switch(self.remember, self.palette, 1.),
|
||||
self.font,
|
||||
))
|
||||
.class(ferese_theme::controls::navigation_style(
|
||||
self.palette,
|
||||
false,
|
||||
))
|
||||
.class(ferese_theme::controls::navigation_style(self.palette, false))
|
||||
.width(Length::Fill)
|
||||
.on_press_maybe(self.can_remember().then_some(Message::Remember)),
|
||||
);
|
||||
}
|
||||
let mut share = button::custom(
|
||||
row![
|
||||
self.text(if self.prompt.window_capture {
|
||||
"Capture"
|
||||
} else {
|
||||
"Share"
|
||||
}),
|
||||
self.text(if self.prompt.window_capture { "Capture" } else { "Share" }),
|
||||
glyph(ferese_theme::icons::ARROW, 16)
|
||||
]
|
||||
.spacing(8)
|
||||
@@ -396,11 +361,10 @@ impl Picker {
|
||||
fn can_remember(&self) -> bool {
|
||||
!self.selected.is_empty()
|
||||
&& self.selected.len() <= 16
|
||||
&& self.selected.iter().all(|index| {
|
||||
self.prompt
|
||||
.rememberable
|
||||
.contains(&self.prompt.sources[*index].name)
|
||||
})
|
||||
&& self
|
||||
.selected
|
||||
.iter()
|
||||
.all(|index| self.prompt.rememberable.contains(&self.prompt.sources[*index].name))
|
||||
}
|
||||
|
||||
fn text<'a>(
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
mod recorder;
|
||||
|
||||
fn main() {
|
||||
if let Err(error) = tokio::runtime::Runtime::new().and_then(|runtime| {
|
||||
runtime
|
||||
.block_on(recorder::run())
|
||||
.map_err(std::io::Error::other)
|
||||
}) {
|
||||
if let Err(error) = tokio::runtime::Runtime::new()
|
||||
.and_then(|runtime| runtime.block_on(recorder::run()).map_err(std::io::Error::other))
|
||||
{
|
||||
recorder::event("error", Some(&error.to_string()), None);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
+39
-101
@@ -1,22 +1,19 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{Read, Write};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use gstreamer::{self as gst, prelude::*};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::{File, OpenOptions},
|
||||
io::{Read, Write},
|
||||
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use gstreamer as gst;
|
||||
use gstreamer::prelude::*;
|
||||
use tokio::sync::watch;
|
||||
use zbus::{
|
||||
Connection, Proxy,
|
||||
zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value},
|
||||
};
|
||||
use zbus::zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value};
|
||||
use zbus::{Connection, Proxy};
|
||||
|
||||
type Options = HashMap<String, OwnedValue>;
|
||||
const DESKTOP: &str = "org.freedesktop.portal.Desktop";
|
||||
@@ -38,32 +35,15 @@ async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>(
|
||||
body: &B,
|
||||
stop: &mut watch::Receiver<bool>,
|
||||
) -> Result<Option<Options>, String> {
|
||||
let sender = connection
|
||||
.unique_name()
|
||||
.ok_or("No D-Bus identity")?
|
||||
.as_str()[1..]
|
||||
.replace('.', "_");
|
||||
let sender = connection.unique_name().ok_or("No D-Bus identity")?.as_str()[1..].replace('.', "_");
|
||||
let path = format!("{DESKTOP_PATH}/request/{sender}/{token}");
|
||||
let request = Proxy::new(
|
||||
connection,
|
||||
DESKTOP,
|
||||
path.as_str(),
|
||||
"org.freedesktop.portal.Request",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut responses = request
|
||||
.receive_signal("Response")
|
||||
let request = Proxy::new(connection, DESKTOP, path.as_str(), "org.freedesktop.portal.Request")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut responses = request.receive_signal("Response").await.map_err(|e| e.to_string())?;
|
||||
let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let portal = Proxy::new(
|
||||
connection,
|
||||
DESKTOP,
|
||||
DESKTOP_PATH,
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let returned: OwnedObjectPath = portal
|
||||
.call(method, body)
|
||||
.await
|
||||
@@ -90,14 +70,7 @@ async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>(
|
||||
|
||||
pub async fn run() -> Result<(), String> {
|
||||
gst::init().map_err(|e| e.to_string())?;
|
||||
for factory in [
|
||||
"pipewiresrc",
|
||||
"queue",
|
||||
"videoconvert",
|
||||
"vp8enc",
|
||||
"webmmux",
|
||||
"fdsink",
|
||||
] {
|
||||
for factory in ["pipewiresrc", "queue", "videoconvert", "vp8enc", "webmmux", "fdsink"] {
|
||||
if gst::ElementFactory::find(factory).is_none() {
|
||||
return Err(format!(
|
||||
"Recording needs the GStreamer {factory} plugin. See the recording installation guide."
|
||||
@@ -127,13 +100,10 @@ pub async fn run() -> Result<(), String> {
|
||||
event("cancelled", None, None);
|
||||
return Ok(());
|
||||
};
|
||||
let handle = created
|
||||
.get("session_handle")
|
||||
.ok_or("Portal returned no session")?;
|
||||
let handle = created.get("session_handle").ok_or("Portal returned no session")?;
|
||||
// The public portal historically returns this path as a D-Bus string.
|
||||
let handle = OwnedObjectPath::try_from(
|
||||
String::try_from(handle.try_clone().map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?,
|
||||
String::try_from(handle.try_clone().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
// Older/foreign backends keep their normal sharing indicator. Only Ferese
|
||||
@@ -146,19 +116,12 @@ pub async fn run() -> Result<(), String> {
|
||||
"org.ferese.ScreenRecorder",
|
||||
)
|
||||
.await?;
|
||||
control
|
||||
.call::<_, _, ()>("UseBarControls", &(handle.clone(),))
|
||||
.await
|
||||
control.call::<_, _, ()>("UseBarControls", &(handle.clone(),)).await
|
||||
})
|
||||
.await;
|
||||
let session = Proxy::new(
|
||||
&connection,
|
||||
DESKTOP,
|
||||
handle.as_str(),
|
||||
"org.freedesktop.portal.Session",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let session = Proxy::new(&connection, DESKTOP, handle.as_str(), "org.freedesktop.portal.Session")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let result = record_session(&connection, &session, &handle, &token, &output, &mut stop).await;
|
||||
let _: Result<(), _> = session.call("Close", &()).await;
|
||||
match result {
|
||||
@@ -176,10 +139,7 @@ async fn record_session(
|
||||
output: &Path,
|
||||
stop: &mut watch::Receiver<bool>,
|
||||
) -> Result<Option<PathBuf>, String> {
|
||||
let mut closed = session
|
||||
.receive_signal("Closed")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let mut closed = session.receive_signal("Closed").await.map_err(|e| e.to_string())?;
|
||||
if request(
|
||||
connection,
|
||||
"SelectSources",
|
||||
@@ -205,11 +165,7 @@ async fn record_session(
|
||||
connection,
|
||||
"Start",
|
||||
&token(2),
|
||||
&(
|
||||
handle,
|
||||
"",
|
||||
HashMap::from([("handle_token", option_string(token(2)))]),
|
||||
),
|
||||
&(handle, "", HashMap::from([("handle_token", option_string(token(2)))])),
|
||||
stop,
|
||||
)
|
||||
.await?
|
||||
@@ -226,14 +182,9 @@ async fn record_session(
|
||||
if streams.len() != 1 {
|
||||
return Err("Recording requires exactly one display".into());
|
||||
}
|
||||
let portal = Proxy::new(
|
||||
connection,
|
||||
DESKTOP,
|
||||
DESKTOP_PATH,
|
||||
"org.freedesktop.portal.ScreenCast",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast")
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
let remote: OwnedFd = portal
|
||||
.call("OpenPipeWireRemote", &(handle, Options::new()))
|
||||
.await
|
||||
@@ -278,8 +229,7 @@ fn output_directory() -> Result<PathBuf, String> {
|
||||
}
|
||||
}
|
||||
fn recording_file(directory: &Path) -> Result<(File, PathBuf, PathBuf), String> {
|
||||
std::fs::create_dir_all(directory)
|
||||
.map_err(|e| format!("Cannot create recording folder: {e}"))?;
|
||||
std::fs::create_dir_all(directory).map_err(|e| format!("Cannot create recording folder: {e}"))?;
|
||||
let name = format!(
|
||||
"Recording-{}-{}",
|
||||
jiff::Zoned::now().strftime("%Y-%m-%d_%H-%M-%S"),
|
||||
@@ -338,24 +288,19 @@ fn encode(source: gst::Element, output: &Path, stop: Arc<AtomicBool>) -> Result<
|
||||
pipeline
|
||||
.add_many([&source, &queue, &convert, &encoder, &mux, &sink])
|
||||
.map_err(|e| e.to_string())?;
|
||||
gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink])
|
||||
.map_err(|e| e.to_string())?;
|
||||
gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink]).map_err(|e| e.to_string())?;
|
||||
let announced = Arc::new(AtomicBool::new(false));
|
||||
let notice = announced.clone();
|
||||
source
|
||||
.static_pad("src")
|
||||
.ok_or("Capture has no video output")?
|
||||
.add_probe(gst::PadProbeType::BUFFER, move |_, info| {
|
||||
if info.buffer().is_some_and(|b| b.size() > 0)
|
||||
&& !notice.swap(true, Ordering::Relaxed)
|
||||
{
|
||||
if info.buffer().is_some_and(|b| b.size() > 0) && !notice.swap(true, Ordering::Relaxed) {
|
||||
event("recording", None, None);
|
||||
}
|
||||
gst::PadProbeReturn::Ok
|
||||
});
|
||||
pipeline
|
||||
.set_state(gst::State::Playing)
|
||||
.map_err(|e| e.to_string())?;
|
||||
pipeline.set_state(gst::State::Playing).map_err(|e| e.to_string())?;
|
||||
let bus = pipeline.bus().ok_or("Encoder has no message bus")?;
|
||||
let mut finishing = None;
|
||||
let started = Instant::now();
|
||||
@@ -404,8 +349,7 @@ fn encode(source: gst::Element, output: &Path, stop: Arc<AtomicBool>) -> Result<
|
||||
result.map_err(|e| format!("{e}. Incomplete file: {}", partial.display()))?;
|
||||
file.sync_all()
|
||||
.map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
|
||||
std::fs::rename(&partial, &destination)
|
||||
.map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
|
||||
std::fs::rename(&partial, &destination).map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
|
||||
Ok(destination)
|
||||
}
|
||||
|
||||
@@ -419,10 +363,7 @@ mod tests {
|
||||
let (_, second, _) = recording_file(directory.path()).unwrap();
|
||||
assert_ne!(first, second);
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
assert_eq!(
|
||||
std::fs::metadata(first).unwrap().permissions().mode() & 0o777,
|
||||
0o600
|
||||
);
|
||||
assert_eq!(std::fs::metadata(first).unwrap().permissions().mode() & 0o777, 0o600);
|
||||
}
|
||||
#[test]
|
||||
#[ignore = "requires an explicitly provided temporary PipeWire test node"]
|
||||
@@ -493,10 +434,7 @@ mod tests {
|
||||
"Invalid saved duration: {duration:?}"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
message.as_ref().map(|m| m.view()),
|
||||
Some(gst::MessageView::Eos(_))
|
||||
),
|
||||
matches!(message.as_ref().map(|m| m.view()), Some(gst::MessageView::Eos(_))),
|
||||
"Saved video did not decode: {message:?}"
|
||||
);
|
||||
}
|
||||
|
||||
+23
-58
@@ -1,8 +1,10 @@
|
||||
use crate::{backend::Options, capture::Source};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value as Json;
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
use crate::backend::Options;
|
||||
use crate::capture::Source;
|
||||
|
||||
const VENDOR: &str = "Ferese";
|
||||
const VERSION: u32 = 1;
|
||||
const MAX_DATA: usize = 16 * 1024;
|
||||
@@ -31,9 +33,7 @@ pub(crate) fn persist_mode(options: &Options) -> zbus::fdo::Result<u32> {
|
||||
.map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid persistence option".into()))?
|
||||
.unwrap_or(0);
|
||||
if mode > 2 {
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Unsupported persistence mode".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Unsupported persistence mode".into()));
|
||||
}
|
||||
Ok(mode)
|
||||
}
|
||||
@@ -43,9 +43,7 @@ fn identity<'a>(outputs: &'a Json, name: &str) -> Option<&'a str> {
|
||||
(output["name"].as_str() == Some(name) && output["enabled"] == true)
|
||||
.then(|| output["identity"].as_str())
|
||||
.flatten()
|
||||
.filter(|identity| {
|
||||
!identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:")
|
||||
})
|
||||
.filter(|identity| !identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:"))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,14 +52,15 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso
|
||||
outputs
|
||||
.as_array()
|
||||
.and_then(|outputs| {
|
||||
outputs.iter().find(|output| {
|
||||
output["name"].as_str() == Some(name) && output["enabled"] == true
|
||||
})
|
||||
outputs
|
||||
.iter()
|
||||
.find(|output| output["name"].as_str() == Some(name) && output["enabled"] == true)
|
||||
})
|
||||
.map(|output| (output["identity"].clone(), output["id"].clone()))
|
||||
};
|
||||
sources.iter().all(
|
||||
|source| match (find(before, &source.name), find(after, &source.name)) {
|
||||
sources
|
||||
.iter()
|
||||
.all(|source| match (find(before, &source.name), find(after, &source.name)) {
|
||||
(Some((identity, id)), Some((current_identity, current_id))) => {
|
||||
if identity.is_null() {
|
||||
id.as_u64().is_some() && id == current_id && current_identity.is_null()
|
||||
@@ -70,8 +69,7 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl Restore {
|
||||
@@ -125,29 +123,13 @@ impl Restore {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
selected.push(
|
||||
sources
|
||||
.iter()
|
||||
.find(|source| source.name == monitor.name)?
|
||||
.clone(),
|
||||
);
|
||||
selected.push(sources.iter().find(|source| source.name == monitor.name)?.clone());
|
||||
}
|
||||
Some(selected)
|
||||
}
|
||||
|
||||
pub(crate) fn create(
|
||||
app: &str,
|
||||
cursor: bool,
|
||||
sources: &[Source],
|
||||
outputs: &Json,
|
||||
mode: u32,
|
||||
) -> Option<Self> {
|
||||
if app.is_empty()
|
||||
|| app.len() > 512
|
||||
|| sources.is_empty()
|
||||
|| sources.len() > 16
|
||||
|| !(1..=2).contains(&mode)
|
||||
{
|
||||
pub(crate) fn create(app: &str, cursor: bool, sources: &[Source], outputs: &Json, mode: u32) -> Option<Self> {
|
||||
if app.is_empty() || app.len() > 512 || sources.is_empty() || sources.len() > 16 || !(1..=2).contains(&mode) {
|
||||
return None;
|
||||
}
|
||||
let monitors = sources
|
||||
@@ -196,8 +178,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn restore_is_bound_to_app_capture_options_and_display_identity() {
|
||||
let outputs =
|
||||
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
|
||||
let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
|
||||
let sources = [source()];
|
||||
let saved = Restore::create("org.test.App", true, &sources, &outputs, 2).unwrap();
|
||||
let options = Options::from([("restore_data".into(), saved.value().unwrap())]);
|
||||
@@ -217,20 +198,14 @@ mod tests {
|
||||
.resolve("org.test.App", false, false, &sources, &outputs)
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
restored
|
||||
.resolve("org.test.App", true, false, &[], &outputs)
|
||||
.is_none()
|
||||
);
|
||||
let changed =
|
||||
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]);
|
||||
assert!(restored.resolve("org.test.App", true, false, &[], &outputs).is_none());
|
||||
let changed = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]);
|
||||
assert!(
|
||||
restored
|
||||
.resolve("org.test.App", true, false, &sources, &changed)
|
||||
.is_none()
|
||||
);
|
||||
let disabled =
|
||||
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]);
|
||||
let disabled = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]);
|
||||
assert!(
|
||||
restored
|
||||
.resolve("org.test.App", true, false, &sources, &disabled)
|
||||
@@ -240,14 +215,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn transient_permission_cannot_be_upgraded_without_new_consent() {
|
||||
let outputs =
|
||||
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
|
||||
let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
|
||||
let saved = Restore::create("org.test.App", false, &[source()], &outputs, 1).unwrap();
|
||||
let restored = Restore::read(&Options::from([(
|
||||
"restore_data".into(),
|
||||
saved.value().unwrap(),
|
||||
)]))
|
||||
.unwrap();
|
||||
let restored = Restore::read(&Options::from([("restore_data".into(), saved.value().unwrap())])).unwrap();
|
||||
assert_eq!(restored.effective_mode(2), 1);
|
||||
assert_eq!(restored.effective_mode(0), 0);
|
||||
let weak = serde_json::json!([{"name":"DP-1", "identity":"drm:DP-1", "enabled":true}]);
|
||||
@@ -259,8 +229,7 @@ mod tests {
|
||||
fn anonymous_nested_selection_has_lifetime_checks_without_saved_grants() {
|
||||
let outputs = serde_json::json!([{"name":"DP-1", "id":1, "identity":null, "enabled":true}]);
|
||||
assert!(selection_unchanged(&[source()], &outputs, &outputs));
|
||||
let replacement =
|
||||
serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]);
|
||||
let replacement = serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]);
|
||||
assert!(!selection_unchanged(&[source()], &outputs, &replacement));
|
||||
assert!(Restore::create("org.test.App", false, &[source()], &outputs, 2).is_none());
|
||||
}
|
||||
@@ -288,11 +257,7 @@ mod tests {
|
||||
.is_none()
|
||||
);
|
||||
saved.monitors[1] = saved.monitors[0].clone();
|
||||
assert!(
|
||||
saved
|
||||
.resolve("org.test.App", false, true, &sources, &outputs)
|
||||
.is_none()
|
||||
);
|
||||
assert!(saved.resolve("org.test.App", false, true, &sources, &outputs).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+13
-17
@@ -1,12 +1,13 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use ferese_config::Document;
|
||||
use ferese_theme::Palette;
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use tokio::sync::RwLock;
|
||||
use zbus::{
|
||||
Connection,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedValue, Value},
|
||||
};
|
||||
use zbus::Connection;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const APPEARANCE: &str = "org.freedesktop.appearance";
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
@@ -50,10 +51,7 @@ impl Appearance {
|
||||
fn values(self) -> HashMap<String, OwnedValue> {
|
||||
HashMap::from([
|
||||
("color-scheme".into(), self.scheme.into()),
|
||||
(
|
||||
"accent-color".into(),
|
||||
Value::from(self.accent).try_to_owned().unwrap(),
|
||||
),
|
||||
("accent-color".into(), Value::from(self.accent).try_to_owned().unwrap()),
|
||||
("contrast".into(), 0u32.into()),
|
||||
("reduced-motion".into(), self.reduced_motion.into()),
|
||||
])
|
||||
@@ -156,12 +154,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn namespaces_support_only_trailing_section_wildcards() {
|
||||
for filter in [
|
||||
"",
|
||||
"org.freedesktop.appearance",
|
||||
"org.freedesktop.*",
|
||||
"org.*",
|
||||
] {
|
||||
for filter in ["", "org.freedesktop.appearance", "org.freedesktop.*", "org.*"] {
|
||||
assert!(matches_namespace(&[filter.into()], APPEARANCE));
|
||||
}
|
||||
for filter in [
|
||||
@@ -177,7 +170,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn reports_ferese_theme_and_reduced_motion() {
|
||||
let doc = Document::parse("theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }").unwrap();
|
||||
let doc = Document::parse(
|
||||
"theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }",
|
||||
)
|
||||
.unwrap();
|
||||
let appearance = Appearance::from_document(Some(&doc));
|
||||
assert_eq!(appearance.scheme, 2);
|
||||
assert_eq!(appearance.reduced_motion, 1);
|
||||
|
||||
+31
-76
@@ -1,20 +1,21 @@
|
||||
use std::{collections::HashMap, io::Read, process::Stdio, sync::Arc, time::Duration};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::process::Stdio;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::{io::AsyncWriteExt, sync::Mutex};
|
||||
use zbus::{
|
||||
Connection,
|
||||
message::Header,
|
||||
object_server::SignalEmitter,
|
||||
zvariant::{OwnedObjectPath, OwnedValue, Value},
|
||||
};
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::Mutex;
|
||||
use zbus::Connection;
|
||||
use zbus::message::Header;
|
||||
use zbus::object_server::SignalEmitter;
|
||||
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
|
||||
|
||||
use crate::{
|
||||
backend::{Cancel, Options, authorize},
|
||||
bridge::Bridge,
|
||||
consent::{Prompt, ShortcutField},
|
||||
desktop::Requests,
|
||||
};
|
||||
use crate::backend::{Cancel, Options, authorize};
|
||||
use crate::bridge::Bridge;
|
||||
use crate::consent::{Prompt, ShortcutField};
|
||||
use crate::desktop::Requests;
|
||||
|
||||
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||
type Shortcuts = Vec<(String, Options)>;
|
||||
@@ -88,10 +89,7 @@ impl GlobalShortcuts {
|
||||
if let Ok(emitter) = SignalEmitter::new(connection, path) {
|
||||
let _ = SessionObject::closed(&emitter).await;
|
||||
}
|
||||
let _ = connection
|
||||
.object_server()
|
||||
.remove::<SessionObject, _>(path)
|
||||
.await;
|
||||
let _ = connection.object_server().remove::<SessionObject, _>(path).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,25 +132,14 @@ impl GlobalShortcuts {
|
||||
.filter(|field| !field.trigger.trim().is_empty())
|
||||
.map(|field| json!({"id":field.id,"trigger":field.trigger.trim()}))
|
||||
.collect::<Vec<_>>();
|
||||
match bridge
|
||||
.call("portal-shortcuts-register", json!(registration))
|
||||
.await
|
||||
{
|
||||
match bridge.call("portal-shortcuts-register", json!(registration)).await {
|
||||
Ok(_) => {
|
||||
let mut state = session.state.lock().await;
|
||||
|
||||
if session
|
||||
.cancel
|
||||
.stopped
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst)
|
||||
|| cancel.stopped.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
if first
|
||||
|| session
|
||||
.cancel
|
||||
.stopped
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
if first || session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
bridge.close();
|
||||
} else {
|
||||
let previous = previous
|
||||
@@ -161,11 +148,7 @@ impl GlobalShortcuts {
|
||||
.map(|field| json!({"id":field.id,"trigger":field.trigger}))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if bridge
|
||||
.call("portal-shortcuts-register", json!(previous))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if bridge.call("portal-shortcuts-register", json!(previous)).await.is_err() {
|
||||
bridge.close();
|
||||
}
|
||||
}
|
||||
@@ -202,13 +185,7 @@ impl GlobalShortcuts {
|
||||
}
|
||||
}
|
||||
|
||||
async fn watch(
|
||||
&self,
|
||||
connection: Connection,
|
||||
path: OwnedObjectPath,
|
||||
session: Arc<Session>,
|
||||
bridge: Bridge,
|
||||
) {
|
||||
async fn watch(&self, connection: Connection, path: OwnedObjectPath, session: Arc<Session>, bridge: Bridge) {
|
||||
let emitter = SignalEmitter::new(&connection, PATH).unwrap();
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -221,11 +198,7 @@ impl GlobalShortcuts {
|
||||
_ => break,
|
||||
};
|
||||
let mut state = session.state.lock().await;
|
||||
if session
|
||||
.cancel
|
||||
.stopped
|
||||
.load(std::sync::atomic::Ordering::SeqCst)
|
||||
{
|
||||
if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
break;
|
||||
}
|
||||
if let Some(active) = value["shortcuts"]
|
||||
@@ -234,16 +207,13 @@ impl GlobalShortcuts {
|
||||
{
|
||||
let mut changed = false;
|
||||
for field in &mut state.fields {
|
||||
if !field.trigger.is_empty()
|
||||
&& !active.iter().any(|item| item["id"] == field.id)
|
||||
{
|
||||
if !field.trigger.is_empty() && !active.iter().any(|item| item["id"] == field.id) {
|
||||
field.trigger.clear();
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields))
|
||||
.await;
|
||||
let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields)).await;
|
||||
}
|
||||
}
|
||||
for event in value["events"].as_array().into_iter().flatten() {
|
||||
@@ -252,12 +222,9 @@ impl GlobalShortcuts {
|
||||
};
|
||||
let timestamp = event["timestamp"].as_u64().unwrap_or(0);
|
||||
if event["active"] == true {
|
||||
let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new())
|
||||
.await;
|
||||
let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new()).await;
|
||||
} else {
|
||||
let _ =
|
||||
Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new())
|
||||
.await;
|
||||
let _ = Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new()).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,15 +249,11 @@ impl GlobalShortcuts {
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<(u32, Options)> {
|
||||
let owner = authorize(connection, &header).await?;
|
||||
if !session_handle
|
||||
.as_str()
|
||||
.starts_with(&format!("{PATH}/session/"))
|
||||
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|
||||
|| session_handle.as_str().len() > 512
|
||||
|| app_id.len() > 512
|
||||
{
|
||||
return Err(zbus::fdo::Error::InvalidArgs(
|
||||
"Invalid shortcut session".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::InvalidArgs("Invalid shortcut session".into()));
|
||||
}
|
||||
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||
let mut sessions = self.sessions.lock().await;
|
||||
@@ -500,9 +463,7 @@ impl SessionObject {
|
||||
#[zbus(header)] header: Header<'_>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
if header.sender().map(|sender| sender.as_str()) != Some(self.owner.as_str()) {
|
||||
return Err(zbus::fdo::Error::AccessDenied(
|
||||
"Not your shortcut session".into(),
|
||||
));
|
||||
return Err(zbus::fdo::Error::AccessDenied("Not your shortcut session".into()));
|
||||
}
|
||||
self.backend.end(connection, &self.path).await;
|
||||
Ok(())
|
||||
@@ -547,10 +508,7 @@ fn fields(shortcuts: Shortcuts) -> Result<Vec<ShortcutField>, String> {
|
||||
Ok(fields)
|
||||
}
|
||||
|
||||
fn validate_selection(
|
||||
requested: &[ShortcutField],
|
||||
selected: &[ShortcutField],
|
||||
) -> Result<(), String> {
|
||||
fn validate_selection(requested: &[ShortcutField], selected: &[ShortcutField]) -> Result<(), String> {
|
||||
if selected.len() != requested.len()
|
||||
|| selected.iter().zip(requested).any(|(chosen, original)| {
|
||||
chosen.id != original.id
|
||||
@@ -590,10 +548,7 @@ fn reply(response: Result<Option<Shortcuts>, String>) -> (u32, Options) {
|
||||
match response {
|
||||
Ok(Some(shortcuts)) => (
|
||||
0,
|
||||
HashMap::from([(
|
||||
"shortcuts".into(),
|
||||
Value::from(shortcuts).try_into().unwrap(),
|
||||
)]),
|
||||
HashMap::from([("shortcuts".into(), Value::from(shortcuts).try_into().unwrap())]),
|
||||
),
|
||||
Ok(None) => (1, Options::new()),
|
||||
Err(error) => {
|
||||
|
||||
+24
-49
@@ -1,17 +1,17 @@
|
||||
//! PipeWire runs outside the compositor. Keep one frame, drop old frames under
|
||||
//! backpressure, and destroy the node when the portal's control pipe closes.
|
||||
use crate::capture::{Capture, Frame};
|
||||
use pipewire::{self as pw, properties::properties, spa};
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use pipewire as pw;
|
||||
use pipewire::properties::properties;
|
||||
use pipewire::spa;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use spa::pod::Pod;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use crate::capture::{Capture, Frame};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Ready {
|
||||
@@ -70,10 +70,7 @@ impl<'a> OutputBuffer<'a> {
|
||||
)
|
||||
.cast::<spa::sys::spa_meta_header>();
|
||||
if let Some(header) = header.as_mut() {
|
||||
let mut now = libc::timespec {
|
||||
tv_sec: 0,
|
||||
tv_nsec: 0,
|
||||
};
|
||||
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
|
||||
header.pts = if libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) == 0 {
|
||||
now.tv_sec * 1_000_000_000 + now.tv_nsec
|
||||
} else {
|
||||
@@ -104,13 +101,10 @@ fn int_property(key: u32, value: i32) -> spa::pod::Property {
|
||||
}
|
||||
}
|
||||
fn pod(object: spa::pod::Object) -> Vec<u8> {
|
||||
spa::pod::serialize::PodSerializer::serialize(
|
||||
std::io::Cursor::new(Vec::new()),
|
||||
&spa::pod::Value::Object(object),
|
||||
)
|
||||
.unwrap()
|
||||
.0
|
||||
.into_inner()
|
||||
spa::pod::serialize::PodSerializer::serialize(std::io::Cursor::new(Vec::new()), &spa::pod::Value::Object(object))
|
||||
.unwrap()
|
||||
.0
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
fn video_format(width: u32, height: u32) -> Vec<u8> {
|
||||
@@ -145,11 +139,7 @@ fn video_format(width: u32, height: u32) -> Vec<u8> {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
name: String,
|
||||
cursor: bool,
|
||||
generation: Option<u32>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let pipe_stop = stop.clone();
|
||||
std::thread::spawn(move || {
|
||||
@@ -213,14 +203,10 @@ pub fn run(
|
||||
.add_local_listener_with_user_data(data)
|
||||
.state_changed(move |stream, data, _, state| {
|
||||
data.fresh.store(false, Ordering::SeqCst);
|
||||
data.streaming.store(
|
||||
matches!(state, pw::stream::StreamState::Streaming),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
data.streaming
|
||||
.store(matches!(state, pw::stream::StreamState::Streaming), Ordering::Relaxed);
|
||||
match state {
|
||||
pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming
|
||||
if !data.announced =>
|
||||
{
|
||||
pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming if !data.announced => {
|
||||
let node = stream.node_id();
|
||||
if node != u32::MAX {
|
||||
data.announced = true;
|
||||
@@ -257,8 +243,7 @@ pub fn run(
|
||||
|| format.size().width == 0
|
||||
|| format.size().height == 0
|
||||
|| (!resizable && (format.size().width, format.size().height) != (width, height))
|
||||
|| u64::from(format.size().width) * u64::from(format.size().height) * 4
|
||||
> 128 * 1024 * 1024
|
||||
|| u64::from(format.size().width) * u64::from(format.size().height) * 4 > 128 * 1024 * 1024
|
||||
|| format.format() != spa::param::video::VideoFormat::BGRx
|
||||
{
|
||||
data.stop.store(true, Ordering::Relaxed);
|
||||
@@ -271,10 +256,7 @@ pub fn run(
|
||||
spa::param::ParamType::Buffers,
|
||||
int_property(spa::sys::SPA_PARAM_BUFFERS_buffers, 4),
|
||||
int_property(spa::sys::SPA_PARAM_BUFFERS_blocks, 1),
|
||||
int_property(
|
||||
spa::sys::SPA_PARAM_BUFFERS_size,
|
||||
(width * height * 4) as i32
|
||||
),
|
||||
int_property(spa::sys::SPA_PARAM_BUFFERS_size, (width * height * 4) as i32),
|
||||
int_property(spa::sys::SPA_PARAM_BUFFERS_stride, (width * 4) as i32),
|
||||
int_property(spa::sys::SPA_PARAM_BUFFERS_align, 16)
|
||||
));
|
||||
@@ -292,10 +274,7 @@ pub fn run(
|
||||
)
|
||||
));
|
||||
if stream
|
||||
.update_params(&mut [
|
||||
Pod::from_bytes(&buffers).unwrap(),
|
||||
Pod::from_bytes(&header).unwrap(),
|
||||
])
|
||||
.update_params(&mut [Pod::from_bytes(&buffers).unwrap(), Pod::from_bytes(&header).unwrap()])
|
||||
.is_err()
|
||||
{
|
||||
data.stop.store(true, Ordering::Relaxed);
|
||||
@@ -358,9 +337,7 @@ pub fn run(
|
||||
while !capture_stop.load(Ordering::Relaxed) {
|
||||
let start = Instant::now();
|
||||
|
||||
if capture_streaming.load(Ordering::Relaxed)
|
||||
|| checked.elapsed() >= Duration::from_secs(1)
|
||||
{
|
||||
if capture_streaming.load(Ordering::Relaxed) || checked.elapsed() >= Duration::from_secs(1) {
|
||||
checked = Instant::now();
|
||||
match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) {
|
||||
Ok(frame) if resizable || (frame.width == width && frame.height == height) => {
|
||||
@@ -407,9 +384,7 @@ pub fn run(
|
||||
} else {
|
||||
Duration::from_millis(10)
|
||||
};
|
||||
mainloop
|
||||
.loop_()
|
||||
.iterate(pw::loop_::Timeout::Finite(timeout));
|
||||
mainloop.loop_().iterate(pw::loop_::Timeout::Finite(timeout));
|
||||
if streaming.load(Ordering::Relaxed) && Instant::now() >= next_frame {
|
||||
let _ = stream.trigger_process();
|
||||
next_frame = Instant::now() + interval;
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
max_width = 120
|
||||
imports_granularity = "Module"
|
||||
group_imports = "StdExternalCrate"
|
||||
reorder_imports = true
|
||||
@@ -1,4 +1,5 @@
|
||||
use cosmic::{iced::Color, widget::image};
|
||||
use cosmic::iced::Color;
|
||||
use cosmic::widget::image;
|
||||
use ferese_config::Document;
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -89,11 +90,7 @@ impl Appearance {
|
||||
}
|
||||
|
||||
pub fn clock_format(&self) -> &'static str {
|
||||
if self.twelve_hour {
|
||||
"%I:%M %p"
|
||||
} else {
|
||||
"%H:%M"
|
||||
}
|
||||
if self.twelve_hour { "%I:%M %p" } else { "%H:%M" }
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> cosmic::Theme {
|
||||
@@ -112,14 +109,12 @@ impl Appearance {
|
||||
}
|
||||
|
||||
fn account_picture(user: &str) -> Option<image::Handle> {
|
||||
let mut candidates =
|
||||
vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)];
|
||||
let mut candidates = vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)];
|
||||
if let Some(home) = std::env::var_os("HOME") {
|
||||
candidates.push(std::path::PathBuf::from(home).join(".face"));
|
||||
}
|
||||
candidates.into_iter().find_map(|path| {
|
||||
let pixels =
|
||||
cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?;
|
||||
let pixels = cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?;
|
||||
let side = pixels.width().min(pixels.height());
|
||||
if side == 0 {
|
||||
return None;
|
||||
@@ -132,13 +127,11 @@ fn account_picture(user: &str) -> Option<image::Handle> {
|
||||
side,
|
||||
)
|
||||
.to_image();
|
||||
let mut pixels =
|
||||
::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3);
|
||||
let mut pixels = ::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3);
|
||||
// Clip the decoded thumbnail, so the avatar stays circular even on
|
||||
// renderers that do not support rounded image clipping.
|
||||
for (x, y, pixel) in pixels.enumerate_pixels_mut() {
|
||||
let distance =
|
||||
((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt();
|
||||
let distance = ((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt();
|
||||
let coverage = (128. - distance).clamp(0., 1.);
|
||||
pixel[3] = (pixel[3] as f32 * coverage).round() as u8;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! PAM stays on a worker thread. No password is placed in argv, logs or IPC.
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[repr(C)]
|
||||
@@ -14,8 +15,7 @@ struct Response {
|
||||
}
|
||||
#[repr(C)]
|
||||
struct Conversation {
|
||||
callback:
|
||||
unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int,
|
||||
callback: unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int,
|
||||
data: *mut c_void,
|
||||
}
|
||||
struct Credentials {
|
||||
@@ -39,8 +39,7 @@ unsafe extern "C" fn converse(
|
||||
unsafe {
|
||||
*out = std::ptr::null_mut();
|
||||
let credentials = &mut *(data as *mut Credentials);
|
||||
let responses =
|
||||
libc::calloc(count as usize, std::mem::size_of::<Response>()) as *mut Response;
|
||||
let responses = libc::calloc(count as usize, std::mem::size_of::<Response>()) as *mut Response;
|
||||
if responses.is_null() {
|
||||
return CONV_ERR;
|
||||
}
|
||||
@@ -51,13 +50,11 @@ unsafe extern "C" fn converse(
|
||||
} else {
|
||||
match (*msg).style {
|
||||
1 => {
|
||||
credentials.password_prompts =
|
||||
credentials.password_prompts.saturating_add(1);
|
||||
(credentials.password_prompts == 1)
|
||||
.then_some(credentials.password.as_slice())
|
||||
credentials.password_prompts = credentials.password_prompts.saturating_add(1);
|
||||
(credentials.password_prompts == 1).then_some(credentials.password.as_slice())
|
||||
} // Never send the password as a second-factor response.
|
||||
2 => Some(credentials.user.as_bytes_with_nul()), // PAM_PROMPT_ECHO_ON
|
||||
3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO
|
||||
3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
@@ -65,11 +62,7 @@ unsafe extern "C" fn converse(
|
||||
if !bytes.is_empty() {
|
||||
let allocation = libc::malloc(bytes.len()) as *mut c_char;
|
||||
if !allocation.is_null() {
|
||||
std::ptr::copy_nonoverlapping(
|
||||
bytes.as_ptr(),
|
||||
allocation.cast(),
|
||||
bytes.len(),
|
||||
);
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), allocation.cast(), bytes.len());
|
||||
(*responses.add(i)).text = allocation;
|
||||
continue;
|
||||
}
|
||||
@@ -139,12 +132,7 @@ fn authenticate_with(user: &str, password: &str, config: Option<&CStr>) -> bool
|
||||
let Ok(library) = libloading::Library::new("libpam.so.0") else {
|
||||
return false;
|
||||
};
|
||||
type Start = unsafe extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const Conversation,
|
||||
*mut *mut c_void,
|
||||
) -> c_int;
|
||||
type Start = unsafe extern "C" fn(*const c_char, *const c_char, *const Conversation, *mut *mut c_void) -> c_int;
|
||||
type StartConf = unsafe extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
|
||||
@@ -4,22 +4,15 @@ mod appearance;
|
||||
mod auth;
|
||||
mod runtime;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::event::{PlatformSpecific, wayland};
|
||||
use cosmic::iced::platform_specific::shell::commands::session_lock;
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{
|
||||
self, Event, Length, Subscription,
|
||||
event::{self, PlatformSpecific, wayland},
|
||||
window,
|
||||
},
|
||||
theme,
|
||||
widget::{self, column, container, image, row},
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use cosmic::iced::{Event, Length, Subscription, event, window};
|
||||
use cosmic::widget::{column, container, image, row};
|
||||
use cosmic::{Element, iced, theme, widget};
|
||||
use wayland_client::protocol::wl_output::WlOutput;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
@@ -31,8 +24,7 @@ fn main() -> iced::Result {
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let preview = args == ["--preview"]
|
||||
|| (args.len() == 3 && args[0] == "--preview" && args[1] == "--config");
|
||||
let preview = args == ["--preview"] || (args.len() == 3 && args[0] == "--preview" && args[1] == "--config");
|
||||
let config = if preview && args.len() == 3 {
|
||||
Some(std::path::PathBuf::from(&args[2]))
|
||||
} else {
|
||||
@@ -201,9 +193,7 @@ impl cosmic::Application for Locker {
|
||||
wayland::Event::Output(..) | wayland::Event::SessionLock(..),
|
||||
))
|
||||
| Event::Window(window::Event::Opened { .. })
|
||||
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => {
|
||||
Some(Message::Event(Box::new(event)))
|
||||
}
|
||||
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => Some(Message::Event(Box::new(event))),
|
||||
_ => None,
|
||||
}),
|
||||
iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick),
|
||||
@@ -212,9 +202,7 @@ impl cosmic::Application for Locker {
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
))) => {
|
||||
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers))) => {
|
||||
self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK);
|
||||
}
|
||||
Message::Tick => {
|
||||
@@ -222,8 +210,7 @@ impl cosmic::Application for Locker {
|
||||
self.clock = now.strftime(self.appearance.clock_format()).to_string();
|
||||
self.date = now.strftime("%A, %B %-d").to_string();
|
||||
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2))
|
||||
{
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) {
|
||||
self.state = AuthState::Ready;
|
||||
}
|
||||
}
|
||||
@@ -242,10 +229,7 @@ impl cosmic::Application for Locker {
|
||||
return focus();
|
||||
}
|
||||
|
||||
if !self.confirmation.confirmed()
|
||||
|| !self.state.may_submit()
|
||||
|| self.password.is_empty()
|
||||
{
|
||||
if !self.confirmation.confirmed() || !self.state.may_submit() || self.password.is_empty() {
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
@@ -269,9 +253,10 @@ impl cosmic::Application for Locker {
|
||||
self.status = "Password not accepted. Try again.";
|
||||
return focus();
|
||||
}
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Output(event, output),
|
||||
))) if !self.preview => {
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(
|
||||
event,
|
||||
output,
|
||||
)))) if !self.preview => {
|
||||
if matches!(event, wayland::OutputEvent::Removed) {
|
||||
if let Some(id) = self.outputs.remove(&output) {
|
||||
return session_lock::destroy_lock_surface(id);
|
||||
@@ -284,9 +269,9 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::SessionLock(event),
|
||||
))) if !self.preview => match event {
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::SessionLock(
|
||||
event,
|
||||
)))) if !self.preview => match event {
|
||||
wayland::SessionLockEvent::Locked => {
|
||||
if self.confirmation.observe() {
|
||||
use std::io::Write;
|
||||
@@ -296,9 +281,11 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
if !self.started {
|
||||
self.started = true;
|
||||
return Task::batch(self.outputs.iter().map(|(output, id)| {
|
||||
session_lock::get_lock_surface(*id, output.clone())
|
||||
}));
|
||||
return Task::batch(
|
||||
self.outputs
|
||||
.iter()
|
||||
.map(|(output, id)| session_lock::get_lock_surface(*id, output.clone())),
|
||||
);
|
||||
}
|
||||
}
|
||||
wayland::SessionLockEvent::Focused(..) => return focus(),
|
||||
@@ -353,14 +340,7 @@ impl Locker {
|
||||
fn screen_content(&self, size: iced::Size) -> Element<'_, Message> {
|
||||
let avatar_size = (size.height * 0.08).clamp(56., 88.);
|
||||
let clock_size = (size.height * 0.165)
|
||||
.min(
|
||||
size.width
|
||||
* if self.appearance.twelve_hour {
|
||||
0.075
|
||||
} else {
|
||||
0.12
|
||||
},
|
||||
)
|
||||
.min(size.width * if self.appearance.twelve_hour { 0.075 } else { 0.12 })
|
||||
.clamp(40., 180.) as u16;
|
||||
let margin = if size.width < 640. { 24. } else { 36. };
|
||||
let top = (size.height * 0.09).max(24.);
|
||||
@@ -368,9 +348,7 @@ impl Locker {
|
||||
let mut input = widget::text_input("Password", self.password.as_str())
|
||||
.password()
|
||||
.padding([6, 12])
|
||||
.style(ferese_theme::controls::lock_input(
|
||||
a.radius, a.accent, a.panel,
|
||||
))
|
||||
.style(ferese_theme::controls::lock_input(a.radius, a.accent, a.panel))
|
||||
.font(a.font)
|
||||
.id(widget::Id::new("password"));
|
||||
|
||||
@@ -422,15 +400,8 @@ impl Locker {
|
||||
self.label(&self.user, 16).class(iced::Color::WHITE),
|
||||
widget::Space::new().height(6),
|
||||
input,
|
||||
self.label(
|
||||
if self.caps_lock {
|
||||
"Caps Lock is on"
|
||||
} else {
|
||||
self.status
|
||||
},
|
||||
12
|
||||
)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.68)),
|
||||
self.label(if self.caps_lock { "Caps Lock is on" } else { self.status }, 12)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.68)),
|
||||
]
|
||||
.spacing(12)
|
||||
.align_x(iced::Alignment::Center)
|
||||
@@ -453,17 +424,10 @@ impl Locker {
|
||||
}
|
||||
|
||||
if a.show_date {
|
||||
clock = clock.push(
|
||||
self.label(&self.date, 18)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.72)),
|
||||
);
|
||||
clock = clock.push(self.label(&self.date, 18).class(iced::Color::WHITE.scale_alpha(0.72)));
|
||||
}
|
||||
|
||||
let clock_height = if a.show_clock {
|
||||
clock_size as f32 * 1.3
|
||||
} else {
|
||||
0.
|
||||
} + if a.show_date { 24. } else { 0. };
|
||||
let clock_height = if a.show_clock { clock_size as f32 * 1.3 } else { 0. } + if a.show_date { 24. } else { 0. };
|
||||
let gap = (size.height * 0.44 - top - clock_height).max(24.);
|
||||
let foreground = container(
|
||||
column![
|
||||
@@ -497,14 +461,12 @@ impl Locker {
|
||||
})
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
widget::container::Style {
|
||||
text_color: Some(iced::Color::WHITE.scale_alpha(0.65)),
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
0.025, 0.035, 0.06, a.dim,
|
||||
))),
|
||||
..Default::default()
|
||||
}
|
||||
.class(theme::Container::custom(move |_| widget::container::Style {
|
||||
text_color: Some(iced::Color::WHITE.scale_alpha(0.65)),
|
||||
background: Some(iced::Background::Color(iced::Color::from_rgba(
|
||||
0.025, 0.035, 0.06, a.dim,
|
||||
))),
|
||||
..Default::default()
|
||||
}));
|
||||
let background: Element<'_, Message> = image(a.wallpaper.clone())
|
||||
.width(Length::Fill)
|
||||
@@ -524,10 +486,7 @@ fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
|
||||
|
||||
fn ferese_symbol() -> widget::icon::Icon {
|
||||
widget::icon(
|
||||
widget::icon::from_svg_bytes(
|
||||
include_bytes!("../../../packaging/icons/ferese.svg").as_slice(),
|
||||
)
|
||||
.symbolic(true),
|
||||
widget::icon::from_svg_bytes(include_bytes!("../../../packaging/icons/ferese.svg").as_slice()).symbolic(true),
|
||||
)
|
||||
.size(30)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
//! Startup handshake: the invoking command exits successfully only after the
|
||||
//! compositor's confirmation. The UI child remains alive for authentication.
|
||||
use std::{
|
||||
io::{BufRead, BufReader},
|
||||
process::{Command, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, QueueHandle,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::wl_registry,
|
||||
};
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::Duration;
|
||||
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::wl_registry;
|
||||
use wayland_client::{Connection, Dispatch, QueueHandle};
|
||||
struct Registry;
|
||||
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for Registry {
|
||||
fn event(
|
||||
@@ -23,10 +20,8 @@ impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for Registry {
|
||||
}
|
||||
}
|
||||
pub fn check_protocol() -> Result<(), String> {
|
||||
let connection =
|
||||
Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?;
|
||||
let (globals, _) = registry_queue_init::<Registry>(&connection)
|
||||
.map_err(|e| format!("Wayland registry: {e}"))?;
|
||||
let connection = Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?;
|
||||
let (globals, _) = registry_queue_init::<Registry>(&connection).map_err(|e| format!("Wayland registry: {e}"))?;
|
||||
if globals
|
||||
.contents()
|
||||
.clone_list()
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
mod prompt;
|
||||
mod session;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
};
|
||||
use zbus::zvariant::{OwnedValue, Value};
|
||||
|
||||
const AGENT_PATH: &str = "/dev/ferese/PolkitAgent";
|
||||
@@ -45,9 +42,8 @@ impl Agent {
|
||||
cookie: String,
|
||||
identities: Vec<Identity>,
|
||||
) -> zbus::fdo::Result<()> {
|
||||
let uid = choose_identity(&identities).ok_or_else(|| {
|
||||
zbus::fdo::Error::Failed("No supported authentication identity".into())
|
||||
})?;
|
||||
let uid = choose_identity(&identities)
|
||||
.ok_or_else(|| zbus::fdo::Error::Failed("No supported authentication identity".into()))?;
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
|
||||
{
|
||||
@@ -60,9 +56,7 @@ impl Agent {
|
||||
|
||||
let result = tokio::task::spawn_blocking({
|
||||
let cookie = cookie.clone();
|
||||
move || {
|
||||
session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
|
||||
}
|
||||
move || session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?;
|
||||
@@ -116,10 +110,7 @@ fn session_subject() -> Result<(String, HashMap<String, Value<'static>>), String
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut details = HashMap::new();
|
||||
details.insert("pid".to_owned(), Value::from(std::process::id()));
|
||||
details.insert(
|
||||
"uid".to_owned(),
|
||||
Value::from(unsafe { libc::geteuid() } as i32),
|
||||
);
|
||||
details.insert("uid".to_owned(), Value::from(unsafe { libc::geteuid() } as i32));
|
||||
details.insert("start-time".to_owned(), Value::from(started));
|
||||
Ok(("unix-process".into(), details))
|
||||
}
|
||||
@@ -130,15 +121,11 @@ async fn run_agent() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.serve_at(AGENT_PATH, Agent::default())?
|
||||
.build()
|
||||
.await?;
|
||||
let authority =
|
||||
zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?;
|
||||
let authority = zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?;
|
||||
let locale = std::env::var("LANG").unwrap_or_default();
|
||||
|
||||
authority
|
||||
.call::<_, _, ()>(
|
||||
"RegisterAuthenticationAgent",
|
||||
&(subject, locale, AGENT_PATH),
|
||||
)
|
||||
.call::<_, _, ()>("RegisterAuthenticationAgent", &(subject, locale, AGENT_PATH))
|
||||
.await?;
|
||||
std::future::pending::<()>().await;
|
||||
|
||||
@@ -174,9 +161,6 @@ mod tests {
|
||||
HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]),
|
||||
)
|
||||
};
|
||||
assert_eq!(
|
||||
choose_identity(&[identity(0), identity(current)]),
|
||||
Some(current)
|
||||
);
|
||||
assert_eq!(choose_identity(&[identity(0), identity(current)]), Some(current));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,16 @@
|
||||
use crate::PromptEvent;
|
||||
use cosmic::{
|
||||
Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Color, Length, Size, Subscription, window},
|
||||
theme,
|
||||
widget::{self, button, column, container, row},
|
||||
};
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
io::{BufRead, BufReader, Write},
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Color, Length, Size, Subscription, window};
|
||||
use cosmic::widget::{button, column, container, row};
|
||||
use cosmic::{Element, theme, widget};
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use crate::PromptEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Appearance {
|
||||
surface: Color,
|
||||
@@ -103,9 +100,7 @@ fn content_height(description: &str, status: Option<&str>) -> f32 {
|
||||
.lines()
|
||||
.map(|line| line.chars().count().div_ceil(48).max(1))
|
||||
.sum();
|
||||
let status_height = status.map_or(0, |message| {
|
||||
14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3)
|
||||
});
|
||||
let status_height = status.map_or(0, |message| 14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3));
|
||||
(214 + 18 * lines.saturating_sub(1) + status_height).min(360) as f32
|
||||
}
|
||||
|
||||
@@ -163,10 +158,7 @@ fn focus() -> Task<Message> {
|
||||
|
||||
impl Prompt {
|
||||
fn resize_to_content(&mut self) -> Task<Message> {
|
||||
let height = content_height(
|
||||
&self.description,
|
||||
self.error.as_deref().or(self.info.as_deref()),
|
||||
);
|
||||
let height = content_height(&self.description, self.error.as_deref().or(self.info.as_deref()));
|
||||
if (height - self.height).abs() < 1.0 {
|
||||
return Task::none();
|
||||
}
|
||||
@@ -180,12 +172,7 @@ impl Prompt {
|
||||
|
||||
impl cosmic::Application for Prompt {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = (
|
||||
Arc<Mutex<VecDeque<PromptEvent>>>,
|
||||
String,
|
||||
String,
|
||||
Appearance,
|
||||
);
|
||||
type Flags = (Arc<Mutex<VecDeque<PromptEvent>>>, String, String, Appearance);
|
||||
type Message = Message;
|
||||
const APP_ID: &'static str = "dev.ferese.Authentication";
|
||||
|
||||
@@ -198,21 +185,14 @@ impl cosmic::Application for Prompt {
|
||||
}
|
||||
|
||||
fn style(&self) -> Option<cosmic::iced::theme::Style> {
|
||||
Some(
|
||||
self.appearance
|
||||
.palette()
|
||||
.application_style(if self.material.is_some() {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
} else {
|
||||
self.appearance.surface
|
||||
}),
|
||||
)
|
||||
Some(self.appearance.palette().application_style(if self.material.is_some() {
|
||||
cosmic::iced::Color::TRANSPARENT
|
||||
} else {
|
||||
self.appearance.surface
|
||||
}))
|
||||
}
|
||||
|
||||
fn init(
|
||||
mut core: Core,
|
||||
(incoming, description, user, appearance): Self::Flags,
|
||||
) -> (Self, Task<Message>) {
|
||||
fn init(mut core: Core, (incoming, description, user, appearance): Self::Flags) -> (Self, Task<Message>) {
|
||||
core.window.show_headerbar = false;
|
||||
core.window.border_padding = Some(0);
|
||||
core.window.content_container = false;
|
||||
@@ -257,11 +237,8 @@ impl cosmic::Application for Prompt {
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::WindowOpened(id) => {
|
||||
return cosmic::iced::window::run(
|
||||
id,
|
||||
ferese_theme::material::ModalMaterial::attach,
|
||||
)
|
||||
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
||||
return cosmic::iced::window::run(id, ferese_theme::material::ModalMaterial::attach)
|
||||
.map(|result| cosmic::Action::App(Message::MaterialAttached(result)));
|
||||
}
|
||||
Message::MaterialAttached(result) => {
|
||||
self.material = result.ok();
|
||||
@@ -323,9 +300,7 @@ impl cosmic::Application for Prompt {
|
||||
.id(widget::Id::new("auth-response"))
|
||||
.font(palette.font)
|
||||
.padding([7, 12])
|
||||
.style(ferese_theme::controls::authentication_input(
|
||||
palette.palette(),
|
||||
));
|
||||
.style(ferese_theme::controls::authentication_input(palette.palette()));
|
||||
if !self.echo {
|
||||
input = input.password();
|
||||
}
|
||||
@@ -347,10 +322,7 @@ impl cosmic::Application for Prompt {
|
||||
.height(40)
|
||||
.center_x(40)
|
||||
.center_y(40)
|
||||
.class(ferese_theme::controls::surface(
|
||||
palette.accent.scale_alpha(0.13),
|
||||
12.,
|
||||
)),
|
||||
.class(ferese_theme::controls::surface(palette.accent.scale_alpha(0.13), 12.,)),
|
||||
column![
|
||||
ferese_theme::text("Authentication required", palette.font).size(18),
|
||||
ferese_theme::text(format!("Confirm as {}", self.user), palette.font)
|
||||
@@ -384,10 +356,7 @@ impl cosmic::Application for Prompt {
|
||||
body = body.push(
|
||||
row![
|
||||
button::custom(ferese_theme::text("Cancel", palette.font).size(13))
|
||||
.class(ferese_theme::controls::button_style(
|
||||
palette.palette(),
|
||||
false
|
||||
))
|
||||
.class(ferese_theme::controls::button_style(palette.palette(), false))
|
||||
.padding([8, 14])
|
||||
.on_press(Message::Cancel),
|
||||
action
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
use crate::PromptEvent;
|
||||
use std::ffi::{CStr, CString, c_char, c_int, c_void};
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
use std::process::{Child, ChildStdin, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{Arc, mpsc};
|
||||
use std::time::Duration;
|
||||
|
||||
use glib::gobject_ffi;
|
||||
use libloading::Library;
|
||||
use std::{
|
||||
ffi::{CStr, CString, c_char, c_int, c_void},
|
||||
io::{BufRead, BufReader, BufWriter, Write},
|
||||
process::{Child, ChildStdin, Command, Stdio},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
mpsc::{self, Sender},
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use crate::PromptEvent;
|
||||
|
||||
enum SessionEvent {
|
||||
Request(String, bool),
|
||||
Info(String),
|
||||
@@ -38,21 +36,15 @@ impl Api {
|
||||
unsafe {
|
||||
let agent = Library::new("libpolkit-agent-1.so.0").map_err(|e| e.to_string())?;
|
||||
let polkit = Library::new("libpolkit-gobject-1.so.0").map_err(|e| e.to_string())?;
|
||||
let user_new = *polkit
|
||||
.get(b"polkit_unix_user_new\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let session_new = *agent
|
||||
.get(b"polkit_agent_session_new\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let user_new = *polkit.get(b"polkit_unix_user_new\0").map_err(|e| e.to_string())?;
|
||||
let session_new = *agent.get(b"polkit_agent_session_new\0").map_err(|e| e.to_string())?;
|
||||
let initiate = *agent
|
||||
.get(b"polkit_agent_session_initiate\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let response = *agent
|
||||
.get(b"polkit_agent_session_response\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let cancel = *agent
|
||||
.get(b"polkit_agent_session_cancel\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let cancel = *agent.get(b"polkit_agent_session_cancel\0").map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Self {
|
||||
_agent: agent,
|
||||
@@ -72,9 +64,7 @@ pub fn check() -> Result<(), String> {
|
||||
}
|
||||
|
||||
fn username(uid: u32) -> String {
|
||||
let output = Command::new("getent")
|
||||
.args(["passwd", &uid.to_string()])
|
||||
.output();
|
||||
let output = Command::new("getent").args(["passwd", &uid.to_string()]).output();
|
||||
output
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
@@ -122,32 +112,21 @@ fn prompt_process(
|
||||
Ok((child, writer, rx))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn request(
|
||||
_session: Object,
|
||||
prompt: *const c_char,
|
||||
echo: c_int,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
unsafe extern "C" fn request(_session: Object, prompt: *const c_char, echo: c_int, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let prompt = unsafe { CStr::from_ptr(prompt) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let prompt = unsafe { CStr::from_ptr(prompt) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Request(prompt, echo != 0));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn info(_session: Object, text: *const c_char, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let text = unsafe { CStr::from_ptr(text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Info(text));
|
||||
}
|
||||
|
||||
unsafe extern "C" fn error(_session: Object, text: *const c_char, data: *mut c_void) {
|
||||
let tx = unsafe { &*(data as *const Sender<SessionEvent>) };
|
||||
let text = unsafe { CStr::from_ptr(text) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned();
|
||||
let _ = tx.send(SessionEvent::Error(text));
|
||||
}
|
||||
|
||||
@@ -156,12 +135,7 @@ unsafe extern "C" fn completed(_session: Object, success: c_int, data: *mut c_vo
|
||||
let _ = tx.send(SessionEvent::Completed(success != 0));
|
||||
}
|
||||
|
||||
unsafe fn connect(
|
||||
session: Object,
|
||||
name: &'static CStr,
|
||||
callback: gobject_ffi::GCallback,
|
||||
data: *mut c_void,
|
||||
) {
|
||||
unsafe fn connect(session: Object, name: &'static CStr, callback: gobject_ffi::GCallback, data: *mut c_void) {
|
||||
unsafe {
|
||||
gobject_ffi::g_signal_connect_data(
|
||||
session.cast(),
|
||||
@@ -215,7 +189,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -226,7 +200,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -237,7 +211,7 @@ fn run_session(
|
||||
unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
completed as unsafe extern "C" fn(Object, c_int, *mut c_void)
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
@@ -276,10 +250,7 @@ fn run_session(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if !stopped
|
||||
&& (cancelled.load(Ordering::Acquire)
|
||||
|| child.try_wait().map_err(|e| e.to_string())?.is_some())
|
||||
{
|
||||
if !stopped && (cancelled.load(Ordering::Acquire) || child.try_wait().map_err(|e| e.to_string())?.is_some()) {
|
||||
unsafe { (api.cancel)(session) };
|
||||
stopped = true;
|
||||
}
|
||||
@@ -293,12 +264,7 @@ fn run_session(
|
||||
Ok(outcome.filter(|_| !stopped))
|
||||
}
|
||||
|
||||
pub fn authenticate(
|
||||
uid: u32,
|
||||
message: String,
|
||||
cookie: String,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> Result<bool, String> {
|
||||
pub fn authenticate(uid: u32, message: String, cookie: String, cancelled: Arc<AtomicBool>) -> Result<bool, String> {
|
||||
let api = Api::load()?;
|
||||
let cookie = CString::new(cookie).map_err(|_| "Invalid authentication cookie")?;
|
||||
let context = glib::MainContext::new();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::{process::Command, sync::OnceLock};
|
||||
use std::process::Command;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub fn families() -> &'static [String] {
|
||||
static FAMILIES: OnceLock<Vec<String>> = OnceLock::new();
|
||||
|
||||
+123
-192
@@ -4,22 +4,20 @@ mod store;
|
||||
mod visuals;
|
||||
mod watch;
|
||||
|
||||
use cosmic::{
|
||||
ApplicationExt, Element,
|
||||
app::{Core, Settings, Task},
|
||||
iced::{Alignment, Length},
|
||||
widget::{self, button, column, container, row, scrollable, slider, text_input, toggler},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::{Alignment, Length};
|
||||
use cosmic::widget::{button, column, container, row, scrollable, slider, text_input, toggler};
|
||||
use cosmic::{ApplicationExt, Element, widget};
|
||||
use schema::{Field, Kind, Page};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use store::{Edit, Snapshot, set};
|
||||
|
||||
fn main() -> cosmic::iced::Result {
|
||||
let mut args = std::env::args_os().skip(1);
|
||||
let path = match args.next() {
|
||||
Some(arg) if arg == "--config" => {
|
||||
PathBuf::from(args.next().expect("--config requires a path"))
|
||||
}
|
||||
Some(arg) if arg == "--config" => PathBuf::from(args.next().expect("--config requires a path")),
|
||||
Some(_) => {
|
||||
eprintln!("Usage: ferese-settings [--config PATH]");
|
||||
return Ok(());
|
||||
@@ -195,9 +193,7 @@ impl cosmic::Application for App {
|
||||
return self.change(set(&format!("desktop_widgets.notes.{index}.text"), value));
|
||||
}
|
||||
}
|
||||
Message::AddNote
|
||||
if !self.saving && self.draft.records("desktop_widgets.notes") < 32 =>
|
||||
{
|
||||
Message::AddNote if !self.saving && self.draft.records("desktop_widgets.notes") < 32 => {
|
||||
let id = format!(
|
||||
"note-{}",
|
||||
std::time::SystemTime::now()
|
||||
@@ -223,9 +219,7 @@ impl cosmic::Application for App {
|
||||
}
|
||||
Message::DragWindow => return self.core.drag(None),
|
||||
Message::ExternalConfig(result) => match result {
|
||||
Ok(snapshot)
|
||||
if snapshot.source == self.current.source
|
||||
|| snapshot.source == self.draft.source => {}
|
||||
Ok(snapshot) if snapshot.source == self.current.source || snapshot.source == self.draft.source => {}
|
||||
Ok(snapshot) => {
|
||||
if self.saving
|
||||
|| self.note_editors.values().any(|editor| editor.dirty)
|
||||
@@ -233,9 +227,7 @@ impl cosmic::Application for App {
|
||||
|| !self.ranges.is_empty()
|
||||
|| !self.pending.is_empty()
|
||||
{
|
||||
self.status =
|
||||
"Config changed externally · Reload when your edits are finished"
|
||||
.into();
|
||||
self.status = "Config changed externally · Reload when your edits are finished".into();
|
||||
} else {
|
||||
self.current = snapshot.clone();
|
||||
self.draft = snapshot;
|
||||
@@ -307,9 +299,7 @@ impl cosmic::Application for App {
|
||||
match shlex::split(&value).filter(|v| !v.is_empty()) {
|
||||
Some(args) => set(
|
||||
&field.path,
|
||||
args.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect::<Vec<_>>(),
|
||||
args.into_iter().map(serde_json::Value::from).collect::<Vec<_>>(),
|
||||
),
|
||||
None => {
|
||||
self.error = Some("Enter a program and arguments with balanced quotes. Commands are not run through a shell.".into());
|
||||
@@ -348,11 +338,7 @@ impl cosmic::Application for App {
|
||||
}
|
||||
.into();
|
||||
|
||||
return Task::batch([
|
||||
self.update_theme(),
|
||||
self.flush(),
|
||||
self.load_thumbnail(),
|
||||
]);
|
||||
return Task::batch([self.update_theme(), self.flush(), self.load_thumbnail()]);
|
||||
}
|
||||
Err(error) => {
|
||||
self.error = Some(error);
|
||||
@@ -410,13 +396,20 @@ impl cosmic::Application for App {
|
||||
"--file-filter=All files | *",
|
||||
])
|
||||
.output()
|
||||
.map_err(|error| format!("Could not open the file chooser: {error}. Install zenity or enter an image path."))
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"Could not open the file chooser: {error}. Install zenity or enter an image path."
|
||||
)
|
||||
})
|
||||
.and_then(|output| {
|
||||
if output.status.code() == Some(1) {
|
||||
return Ok(None); // User cancelled.
|
||||
}
|
||||
if !output.status.success() {
|
||||
return Err(format!("File chooser failed: {}", String::from_utf8_lossy(&output.stderr).trim()));
|
||||
return Err(format!(
|
||||
"File chooser failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
let path = String::from_utf8(output.stdout)
|
||||
.map_err(|_| "The image path is not valid UTF-8.".to_owned())?;
|
||||
@@ -458,31 +451,23 @@ impl cosmic::Application for App {
|
||||
return self.load_thumbnail();
|
||||
}
|
||||
Message::NewCommand(value) => self.new_command = value,
|
||||
Message::AddCommand if !self.saving => {
|
||||
match shlex::split(&self.new_command).filter(|a| !a.is_empty()) {
|
||||
Some(args) => {
|
||||
self.new_command.clear();
|
||||
return self.change(Edit::Add(
|
||||
"autostart".into(),
|
||||
vec![
|
||||
(
|
||||
"command".into(),
|
||||
args.into_iter()
|
||||
.map(serde_json::Value::from)
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
),
|
||||
("enabled".into(), true.into()),
|
||||
("restart".into(), true.into()),
|
||||
],
|
||||
));
|
||||
}
|
||||
None => {
|
||||
self.error =
|
||||
Some("Enter a program and arguments with balanced quotes.".into())
|
||||
}
|
||||
Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) {
|
||||
Some(args) => {
|
||||
self.new_command.clear();
|
||||
return self.change(Edit::Add(
|
||||
"autostart".into(),
|
||||
vec![
|
||||
(
|
||||
"command".into(),
|
||||
args.into_iter().map(serde_json::Value::from).collect::<Vec<_>>().into(),
|
||||
),
|
||||
("enabled".into(), true.into()),
|
||||
("restart".into(), true.into()),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
None => self.error = Some("Enter a program and arguments with balanced quotes.".into()),
|
||||
},
|
||||
Message::AddSwipe(keys) if !self.saving => {
|
||||
let (action, argument) = match keys {
|
||||
"Swipe3Up" => ("workspace-next", None),
|
||||
@@ -490,10 +475,7 @@ impl cosmic::Application for App {
|
||||
"Swipe3Left" => ("focus", Some("right")),
|
||||
_ => ("focus", Some("left")),
|
||||
};
|
||||
let mut fields = vec![
|
||||
("keys".into(), keys.into()),
|
||||
("action".into(), action.into()),
|
||||
];
|
||||
let mut fields = vec![("keys".into(), keys.into()), ("action".into(), action.into())];
|
||||
|
||||
if let Some(argument) = argument {
|
||||
fields.push(("argument".into(), argument.into()));
|
||||
@@ -596,8 +578,7 @@ impl cosmic::Application for App {
|
||||
|
||||
if !self.search.is_empty() {
|
||||
if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) {
|
||||
body = body
|
||||
.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||
body = body.push(self.note("No matching settings. Try wallpaper, keyboard, or motion."));
|
||||
}
|
||||
for page in Page::ALL.into_iter().filter(|p| p.matches(&self.search)) {
|
||||
body = body.push(
|
||||
@@ -682,8 +663,7 @@ impl cosmic::Application for App {
|
||||
.unwrap_or("Choose an image to preview your wallpaper.")
|
||||
}));
|
||||
}
|
||||
body =
|
||||
body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||
body = body.push(button::standard("Choose image…").on_press(Message::PickWallpaper));
|
||||
}
|
||||
let fields = schema::fields(self.page);
|
||||
|
||||
@@ -692,30 +672,21 @@ impl cosmic::Application for App {
|
||||
for field in fields {
|
||||
group = group.push(self.field(field));
|
||||
}
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
|
||||
match self.page {
|
||||
Page::LockScreen => {
|
||||
body = body.push(self.note("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens."));
|
||||
body = body.push(
|
||||
button::standard("Preview lock screen").on_press(Message::PreviewLock),
|
||||
);
|
||||
body = body.push(button::standard("Preview lock screen").on_press(Message::PreviewLock));
|
||||
body = body.push(self.note("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items."));
|
||||
}
|
||||
Page::Desktop => {
|
||||
let can_change_list =
|
||||
!self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||||
let can_change_list = !self.saving && !self.note_editors.values().any(|e| e.dirty);
|
||||
body = body.push(self.label("Sticky notes", 16.));
|
||||
body = body.push(self.note("Edit here; notes save after you pause typing. Desktop cards stay behind windows and are click-through."));
|
||||
for index in 0..self.draft.records("desktop_widgets.notes") {
|
||||
let id = self
|
||||
.draft
|
||||
.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||
let id = self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note");
|
||||
let mut group = column([]).spacing(8);
|
||||
|
||||
for field in schema::note_fields(index) {
|
||||
@@ -729,23 +700,13 @@ impl cosmic::Application for App {
|
||||
.height(160)
|
||||
.font(self.font)
|
||||
.size(14.)
|
||||
.on_action(move |action| {
|
||||
Message::NoteAction(id.clone(), action)
|
||||
}),
|
||||
.on_action(move |action| Message::NoteAction(id.clone(), action)),
|
||||
);
|
||||
}
|
||||
group =
|
||||
group.push(button::destructive("Remove note").on_press_maybe(
|
||||
can_change_list.then_some(Message::Remove(
|
||||
"desktop_widgets.notes".into(),
|
||||
index,
|
||||
)),
|
||||
));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(12)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
group = group.push(button::destructive("Remove note").on_press_maybe(
|
||||
can_change_list.then_some(Message::Remove("desktop_widgets.notes".into(), index)),
|
||||
));
|
||||
body = body.push(container(group).padding(12).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
body = body.push(
|
||||
button::standard("Add note").on_press_maybe(
|
||||
@@ -783,19 +744,12 @@ impl cosmic::Application for App {
|
||||
Kind::Toggle(true),
|
||||
)))
|
||||
.push(
|
||||
container(
|
||||
button::destructive("Remove item").on_press_maybe(
|
||||
(!self.saving)
|
||||
.then_some(Message::Remove("autostart".into(), index)),
|
||||
),
|
||||
)
|
||||
container(button::destructive("Remove item").on_press_maybe(
|
||||
(!self.saving).then_some(Message::Remove("autostart".into(), index)),
|
||||
))
|
||||
.padding(12),
|
||||
);
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
body = body.push(
|
||||
row([])
|
||||
@@ -804,12 +758,9 @@ impl cosmic::Application for App {
|
||||
text_input("Program and arguments", self.new_command.clone())
|
||||
.on_input(Message::NewCommand),
|
||||
)
|
||||
.push(
|
||||
button::standard("Add login item").on_press_maybe(
|
||||
(!self.saving && !self.new_command.trim().is_empty())
|
||||
.then_some(Message::AddCommand),
|
||||
),
|
||||
),
|
||||
.push(button::standard("Add login item").on_press_maybe(
|
||||
(!self.saving && !self.new_command.trim().is_empty()).then_some(Message::AddCommand),
|
||||
)),
|
||||
);
|
||||
}
|
||||
Page::Shortcuts => {
|
||||
@@ -828,10 +779,10 @@ impl cosmic::Application for App {
|
||||
});
|
||||
|
||||
if !exists {
|
||||
gestures =
|
||||
gestures.push(button::standard(label).on_press_maybe(
|
||||
(!self.saving).then_some(Message::AddSwipe(keys)),
|
||||
));
|
||||
gestures = gestures.push(
|
||||
button::standard(label)
|
||||
.on_press_maybe((!self.saving).then_some(Message::AddSwipe(keys))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -859,11 +810,7 @@ impl cosmic::Application for App {
|
||||
"Command name, direction, or action argument.",
|
||||
"",
|
||||
)));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
if self.draft.records("bindings") == 0 {
|
||||
body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload."));
|
||||
@@ -873,29 +820,44 @@ impl cosmic::Application for App {
|
||||
body = body.push(self.note("Display profiles update connected outputs live. Keep your config open for adding profiles or changing display positions."));
|
||||
for profile in 0..self.draft.records("output_profiles") {
|
||||
let prefix = format!("output_profiles.{profile}");
|
||||
body = body.push(
|
||||
self.label(
|
||||
self.draft
|
||||
.string(&format!("{prefix}.name"), "Display profile"),
|
||||
17.,
|
||||
),
|
||||
);
|
||||
body =
|
||||
body.push(self.label(self.draft.string(&format!("{prefix}.name"), "Display profile"), 17.));
|
||||
|
||||
for output in 0..self.draft.records(&format!("{prefix}.outputs")) {
|
||||
let prefix = format!("{prefix}.outputs.{output}");
|
||||
let group = column([]).spacing(1)
|
||||
.push(self.field(schema::text(format!("{prefix}.match"), "Display", "Output name or matching pattern.", "")))
|
||||
.push(self.field(schema::text(format!("{prefix}.mode"), "Resolution", "For example: 2560x1440@60. Leave unchanged to retain automatic selection.", "")))
|
||||
.push(self.field(schema::range(format!("{prefix}.scale"), "Scale", "Logical size of text and controls.", 1., 0.75, 3., 0.25, "×", false)));
|
||||
body = body.push(
|
||||
container(group)
|
||||
.padding(8)
|
||||
.class(visuals::surface(palette.card, 14.)),
|
||||
);
|
||||
let group = column([])
|
||||
.spacing(1)
|
||||
.push(self.field(schema::text(
|
||||
format!("{prefix}.match"),
|
||||
"Display",
|
||||
"Output name or matching pattern.",
|
||||
"",
|
||||
)))
|
||||
.push(self.field(schema::text(
|
||||
format!("{prefix}.mode"),
|
||||
"Resolution",
|
||||
"For example: 2560x1440@60. Leave unchanged to retain automatic selection.",
|
||||
"",
|
||||
)))
|
||||
.push(self.field(schema::range(
|
||||
format!("{prefix}.scale"),
|
||||
"Scale",
|
||||
"Logical size of text and controls.",
|
||||
1.,
|
||||
0.75,
|
||||
3.,
|
||||
0.25,
|
||||
"×",
|
||||
false,
|
||||
)));
|
||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||
}
|
||||
}
|
||||
if self.draft.records("output_profiles") == 0 {
|
||||
body = body.push(self.note("No saved profiles. Ferese currently configures connected displays automatically."));
|
||||
body =
|
||||
body.push(self.note(
|
||||
"No saved profiles. Ferese currently configures connected displays automatically.",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
@@ -947,14 +909,7 @@ impl cosmic::Application for App {
|
||||
},
|
||||
palette.muted,
|
||||
),
|
||||
self.label(
|
||||
if self.saving {
|
||||
"Saving…"
|
||||
} else {
|
||||
&self.status
|
||||
},
|
||||
11.,
|
||||
),
|
||||
self.label(if self.saving { "Saving…" } else { &self.status }, 11.),
|
||||
widget::tooltip::Position::Top,
|
||||
))
|
||||
.push(widget::Space::new().width(Length::Fill))
|
||||
@@ -986,21 +941,16 @@ impl cosmic::Application for App {
|
||||
.push(sidebar)
|
||||
.push(
|
||||
container(
|
||||
container(
|
||||
column([])
|
||||
.push(content.height(Length::Fill))
|
||||
.push(footer)
|
||||
.spacing(6),
|
||||
)
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: 20.,
|
||||
right: 20.,
|
||||
bottom: 8.,
|
||||
left: 20.,
|
||||
})
|
||||
.max_width(840)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
container(column([]).push(content.height(Length::Fill)).push(footer).spacing(6))
|
||||
.padding(cosmic::iced::Padding {
|
||||
top: 20.,
|
||||
right: 20.,
|
||||
bottom: 8.,
|
||||
left: 20.,
|
||||
})
|
||||
.max_width(840)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.center_x(Length::Fill)
|
||||
@@ -1022,11 +972,8 @@ impl App {
|
||||
}
|
||||
|
||||
fn note_index(&self, id: &str) -> Option<usize> {
|
||||
(0..self.draft.records("desktop_widgets.notes")).find(|index| {
|
||||
self.draft
|
||||
.string(&format!("desktop_widgets.notes.{index}.id"), "note")
|
||||
== id
|
||||
})
|
||||
(0..self.draft.records("desktop_widgets.notes"))
|
||||
.find(|index| self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note") == id)
|
||||
}
|
||||
|
||||
fn sync_notes(&mut self) {
|
||||
@@ -1036,14 +983,11 @@ impl App {
|
||||
let prefix = format!("desktop_widgets.notes.{index}");
|
||||
let id = self.draft.string(&format!("{prefix}.id"), "note");
|
||||
let text = self.draft.string(&format!("{prefix}.text"), "");
|
||||
let editor = self
|
||||
.note_editors
|
||||
.entry(id.clone())
|
||||
.or_insert_with(|| NoteEditor {
|
||||
content: widget::text_editor::Content::with_text(&text),
|
||||
revision: 0,
|
||||
dirty: false,
|
||||
});
|
||||
let editor = self.note_editors.entry(id.clone()).or_insert_with(|| NoteEditor {
|
||||
content: widget::text_editor::Content::with_text(&text),
|
||||
revision: 0,
|
||||
dirty: false,
|
||||
});
|
||||
if !editor.dirty && editor.content.text() != text {
|
||||
editor.content = widget::text_editor::Content::with_text(&text);
|
||||
}
|
||||
@@ -1121,9 +1065,7 @@ impl App {
|
||||
|
||||
fn field(&self, field: Field) -> Element<'static, Message> {
|
||||
let palette = visuals::Palette::from_document(Some(&self.draft.doc));
|
||||
let mut labels = column([])
|
||||
.spacing(3)
|
||||
.push(self.label(field.label.clone(), 13.));
|
||||
let mut labels = column([]).spacing(3).push(self.label(field.label.clone(), 13.));
|
||||
if !field.description.is_empty() {
|
||||
labels = labels.push(
|
||||
self.label(field.description.clone(), 11.)
|
||||
@@ -1205,12 +1147,10 @@ impl App {
|
||||
.align_y(Alignment::Center)
|
||||
.spacing(14)
|
||||
.push(
|
||||
slider(min..=max, value, move |value| {
|
||||
Message::Range(field.clone(), value)
|
||||
})
|
||||
.step(step)
|
||||
.on_release(Message::Release(release))
|
||||
.width(145),
|
||||
slider(min..=max, value, move |value| Message::Range(field.clone(), value))
|
||||
.step(step)
|
||||
.on_release(Message::Release(release))
|
||||
.width(145),
|
||||
)
|
||||
.push(self.label(display, 12.).width(65))
|
||||
.into()
|
||||
@@ -1254,10 +1194,7 @@ impl App {
|
||||
row([])
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.push(
|
||||
container(widget::Space::new().width(24).height(24))
|
||||
.class(visuals::surface(swatch, 6.)),
|
||||
)
|
||||
.push(container(widget::Space::new().width(24).height(24)).class(visuals::surface(swatch, 6.)))
|
||||
.push(input)
|
||||
.into()
|
||||
} else {
|
||||
@@ -1342,8 +1279,9 @@ impl App {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::Application;
|
||||
|
||||
use super::*;
|
||||
fn app() -> App {
|
||||
App::init(
|
||||
Core::default(),
|
||||
@@ -1361,10 +1299,7 @@ mod tests {
|
||||
app.draft
|
||||
.edit(&Edit::Add(
|
||||
"desktop_widgets.notes".into(),
|
||||
vec![
|
||||
("id".into(), "test".into()),
|
||||
("text".into(), "first\nsecond".into()),
|
||||
],
|
||||
vec![("id".into(), "test".into()), ("text".into(), "first\nsecond".into())],
|
||||
))
|
||||
.unwrap();
|
||||
app.sync_notes();
|
||||
@@ -1377,10 +1312,7 @@ mod tests {
|
||||
let _ = app.update(Message::SaveNote("test".into(), 2));
|
||||
assert!(app.saving);
|
||||
assert!(!app.note_editors["test"].dirty);
|
||||
assert_eq!(
|
||||
app.draft.string("desktop_widgets.notes.0.text", ""),
|
||||
"first\nsecond"
|
||||
);
|
||||
assert_eq!(app.draft.string("desktop_widgets.notes.0.text", ""), "first\nsecond");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1424,8 +1356,7 @@ mod tests {
|
||||
let _ = app.update(Message::ExternalConfig(Ok(external.clone())));
|
||||
assert_eq!(app.current.source, external.source);
|
||||
assert_eq!(app.draft.number("animations.speed", 0.), 0.5);
|
||||
app.inputs
|
||||
.insert("theme.colors.accent".into(), "#ffffff".into());
|
||||
app.inputs.insert("theme.colors.accent".into(), "#ffffff".into());
|
||||
let newer = Snapshot::parse("animations {\n speed 0.8\n}\n".into()).unwrap();
|
||||
let _ = app.update(Message::ExternalConfig(Ok(newer)));
|
||||
assert_eq!(app.current.source, external.source);
|
||||
|
||||
@@ -129,12 +129,7 @@ pub struct Field {
|
||||
}
|
||||
|
||||
impl Field {
|
||||
pub fn new(
|
||||
path: impl Into<String>,
|
||||
label: impl Into<String>,
|
||||
description: impl Into<String>,
|
||||
kind: Kind,
|
||||
) -> Self {
|
||||
pub fn new(path: impl Into<String>, label: impl Into<String>, description: impl Into<String>, kind: Kind) -> Self {
|
||||
Self {
|
||||
path: path.into(),
|
||||
label: label.into(),
|
||||
@@ -180,15 +175,7 @@ pub fn text(
|
||||
description: impl Into<String>,
|
||||
default: &'static str,
|
||||
) -> Field {
|
||||
Field::new(
|
||||
path,
|
||||
label,
|
||||
description,
|
||||
Kind::Text {
|
||||
default,
|
||||
argv: false,
|
||||
},
|
||||
)
|
||||
Field::new(path, label, description, Kind::Text { default, argv: false })
|
||||
}
|
||||
|
||||
fn choice(
|
||||
@@ -400,18 +387,8 @@ pub fn note_fields(index: usize) -> Vec<Field> {
|
||||
"Drag the title and edit directly. Off makes the card click-through.",
|
||||
Kind::Toggle(true),
|
||||
),
|
||||
Field::new(
|
||||
format!("{prefix}.enabled"),
|
||||
"Show note",
|
||||
"",
|
||||
Kind::Toggle(true),
|
||||
),
|
||||
text(
|
||||
format!("{prefix}.title"),
|
||||
"Title",
|
||||
"Empty hides the title.",
|
||||
"Note",
|
||||
),
|
||||
Field::new(format!("{prefix}.enabled"), "Show note", "", Kind::Toggle(true)),
|
||||
text(format!("{prefix}.title"), "Title", "Empty hides the title.", "Note"),
|
||||
text(
|
||||
format!("{prefix}.anchor"),
|
||||
"Position",
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use std::fs;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_config::Document;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub const LIMIT: usize = 60 * 1024;
|
||||
|
||||
@@ -45,10 +44,7 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
pub fn string(&self, path: &str, fallback: &str) -> String {
|
||||
self.item(path)
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or(fallback)
|
||||
.to_owned()
|
||||
self.item(path).and_then(Value::as_str).unwrap_or(fallback).to_owned()
|
||||
}
|
||||
|
||||
pub fn number(&self, path: &str, fallback: f64) -> f64 {
|
||||
@@ -60,9 +56,7 @@ impl Snapshot {
|
||||
}
|
||||
|
||||
pub fn records(&self, path: &str) -> usize {
|
||||
self.item(path)
|
||||
.and_then(Value::as_array)
|
||||
.map_or(0, |a| a.len())
|
||||
self.item(path).and_then(Value::as_array).map_or(0, |a| a.len())
|
||||
}
|
||||
|
||||
pub fn argv(&self, path: &str) -> String {
|
||||
@@ -172,9 +166,7 @@ fn save_with(
|
||||
let snapshot = Snapshot::parse(desired.to_owned())?;
|
||||
let unchanged = || -> Result<(), String> {
|
||||
if Snapshot::read(path)?.source != expected {
|
||||
return Err(
|
||||
"Your config changed in another app. Reload settings before saving.".into(),
|
||||
);
|
||||
return Err("Your config changed in another app. Reload settings before saving.".into());
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
@@ -188,17 +180,13 @@ fn save_with(
|
||||
.set_permissions(metadata.permissions())
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
staged
|
||||
.write_all(desired.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
staged.write_all(desired.as_bytes()).map_err(|e| e.to_string())?;
|
||||
staged.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
validate(staged.path())?;
|
||||
unchanged()?;
|
||||
// Keep the last working source, including comments and unknown keys.
|
||||
let mut backup = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?;
|
||||
backup
|
||||
.write_all(expected.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
backup.write_all(expected.as_bytes()).map_err(|e| e.to_string())?;
|
||||
backup.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
backup
|
||||
.persist(path.with_extension("kdl.settings-backup"))
|
||||
@@ -212,8 +200,7 @@ pub fn reload_running() -> bool {
|
||||
let Some(runtime) = std::env::var_os("XDG_RUNTIME_DIR") else {
|
||||
return false;
|
||||
};
|
||||
let Ok(mut socket) =
|
||||
std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock"))
|
||||
let Ok(mut socket) = std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock"))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
@@ -293,17 +280,11 @@ window-rule app-id="mine" floating=#true
|
||||
|
||||
#[test]
|
||||
fn nested_array_edits_and_login_items_roundtrip() {
|
||||
let mut snapshot = Snapshot::parse(
|
||||
"output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into(),
|
||||
)
|
||||
.unwrap();
|
||||
snapshot
|
||||
.edit(&set("output_profiles.0.outputs.0.scale", 1.75))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
snapshot.number("output_profiles.0.outputs.0.scale", 1.),
|
||||
1.75
|
||||
);
|
||||
let mut snapshot =
|
||||
Snapshot::parse("output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into())
|
||||
.unwrap();
|
||||
snapshot.edit(&set("output_profiles.0.outputs.0.scale", 1.75)).unwrap();
|
||||
assert_eq!(snapshot.number("output_profiles.0.outputs.0.scale", 1.), 1.75);
|
||||
snapshot
|
||||
.edit(&Edit::Add(
|
||||
"autostart".into(),
|
||||
@@ -331,28 +312,18 @@ window-rule app-id="mine" floating=#true
|
||||
|
||||
#[test]
|
||||
fn nested_note_lists_keep_existing_clock_settings() {
|
||||
let mut snapshot = Snapshot::parse(
|
||||
"desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut snapshot =
|
||||
Snapshot::parse("desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into()).unwrap();
|
||||
snapshot
|
||||
.edit(&Edit::Add(
|
||||
"desktop_widgets.notes".into(),
|
||||
vec![
|
||||
("id".into(), "first".into()),
|
||||
("text".into(), "one\ntwo".into()),
|
||||
],
|
||||
vec![("id".into(), "first".into()), ("text".into(), "one\ntwo".into())],
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.records("desktop_widgets.notes"), 1);
|
||||
assert!(snapshot.boolean("desktop_widgets.clock.enabled", false));
|
||||
assert_eq!(
|
||||
snapshot.string("desktop_widgets.notes.0.text", ""),
|
||||
"one\ntwo"
|
||||
);
|
||||
snapshot
|
||||
.edit(&Edit::Remove("desktop_widgets.notes".into(), 0))
|
||||
.unwrap();
|
||||
assert_eq!(snapshot.string("desktop_widgets.notes.0.text", ""), "one\ntwo");
|
||||
snapshot.edit(&Edit::Remove("desktop_widgets.notes".into(), 0)).unwrap();
|
||||
assert_eq!(snapshot.records("desktop_widgets.notes"), 0);
|
||||
}
|
||||
|
||||
@@ -364,11 +335,7 @@ window-rule app-id="mine" floating=#true
|
||||
let path = directory.path().join("config.kdl");
|
||||
let validate = |snapshot: &Snapshot| {
|
||||
fs::write(&path, &snapshot.source).unwrap();
|
||||
let result = Command::new(&binary)
|
||||
.arg("--check-config")
|
||||
.arg(&path)
|
||||
.output()
|
||||
.unwrap();
|
||||
let result = Command::new(&binary).arg("--check-config").arg(&path).output().unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}\n{}",
|
||||
@@ -386,9 +353,7 @@ window-rule app-id="mine" floating=#true
|
||||
let values: Vec<Value> = match field.kind {
|
||||
Kind::Font => vec!["".into(), "sans-serif".into()],
|
||||
Kind::Toggle(_) => vec![true.into(), false.into()],
|
||||
Kind::Range {
|
||||
min, max, integer, ..
|
||||
} => {
|
||||
Kind::Range { min, max, integer, .. } => {
|
||||
if integer {
|
||||
vec![(min as i64).into(), (max as i64).into()]
|
||||
} else {
|
||||
@@ -396,9 +361,7 @@ window-rule app-id="mine" floating=#true
|
||||
}
|
||||
}
|
||||
Kind::Text { default, .. } => vec![default.into()],
|
||||
Kind::Choice { choices, .. } => {
|
||||
choices.iter().map(|(v, _)| (*v).into()).collect()
|
||||
}
|
||||
Kind::Choice { choices, .. } => choices.iter().map(|(v, _)| (*v).into()).collect(),
|
||||
};
|
||||
for value in values {
|
||||
let mut snapshot = Snapshot::parse(String::new()).unwrap();
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
use crate::{
|
||||
Message,
|
||||
schema::Page,
|
||||
store::{Edit, Snapshot, set},
|
||||
};
|
||||
use cosmic::{
|
||||
Element,
|
||||
iced::{Color, Length},
|
||||
widget::icon as svg_icon,
|
||||
};
|
||||
|
||||
pub use ferese_theme::controls::{
|
||||
button_style, navigation_style, settings_input as input_style, surface,
|
||||
};
|
||||
use cosmic::Element;
|
||||
use cosmic::iced::{Color, Length};
|
||||
use cosmic::widget::icon as svg_icon;
|
||||
pub use ferese_theme::controls::{button_style, navigation_style, settings_input as input_style, surface};
|
||||
pub use ferese_theme::{Palette, mix, surface_shade};
|
||||
|
||||
use crate::Message;
|
||||
use crate::schema::Page;
|
||||
use crate::store::{Edit, Snapshot, set};
|
||||
|
||||
pub fn color(value: &str, fallback: Color) -> Color {
|
||||
ferese_theme::parse_color(value).unwrap_or(fallback)
|
||||
}
|
||||
@@ -32,9 +26,7 @@ pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme {
|
||||
}
|
||||
|
||||
pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font {
|
||||
ferese_theme::font(Some(
|
||||
&snapshot.string("theme.typography.font_family", "Inter"),
|
||||
))
|
||||
ferese_theme::font(Some(&snapshot.string("theme.typography.font_family", "Inter")))
|
||||
}
|
||||
|
||||
pub fn icon(page: Page, tint: Color) -> svg_icon::Icon {
|
||||
@@ -91,20 +83,14 @@ pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool {
|
||||
("theme.surface.bar.text_primary", preset.text, "#F0F3FA"),
|
||||
("theme.surface.bar.text_muted", preset.muted, "#AAB4C7"),
|
||||
("theme.focus_ring.gradient.from", preset.accent, "#3D7BE6"),
|
||||
(
|
||||
"theme.focus_ring.gradient.to",
|
||||
preset.gradient_end,
|
||||
"#3D7BE6",
|
||||
),
|
||||
("theme.focus_ring.gradient.to", preset.gradient_end, "#3D7BE6"),
|
||||
]
|
||||
.into_iter()
|
||||
.all(|(path, expected, fallback)| {
|
||||
if index == 0 && snapshot.item(path).is_none() {
|
||||
return true;
|
||||
}
|
||||
snapshot
|
||||
.string(path, fallback)
|
||||
.eq_ignore_ascii_case(expected)
|
||||
snapshot.string(path, fallback).eq_ignore_ascii_case(expected)
|
||||
}) && snapshot.number("theme.focus_ring.gradient.angle", 0.) == 0.
|
||||
}
|
||||
|
||||
@@ -164,19 +150,14 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
||||
let accent = hex(p.accent);
|
||||
let muted = hex(p.muted);
|
||||
let gap = snapshot.number("layout.inner_gap", 8.).clamp(0., 32.);
|
||||
let radius = snapshot
|
||||
.number("theme.geometry.window_radius", 14.)
|
||||
.clamp(0., 28.);
|
||||
let radius = snapshot.number("theme.geometry.window_radius", 14.).clamp(0., 28.);
|
||||
let bar_y = snapshot.number("theme.geometry.top_bar_margin_top", 0.) * 0.5 + 12.;
|
||||
let bar_margin = snapshot.number("theme.geometry.top_bar_margin_horizontal", 0.) * 0.5 + 14.;
|
||||
let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65;
|
||||
let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65;
|
||||
let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" {
|
||||
snapshot
|
||||
.number(
|
||||
"theme.material.opacity",
|
||||
ferese_config::DEFAULT_MATERIAL_OPACITY,
|
||||
)
|
||||
.number("theme.material.opacity", ferese_config::DEFAULT_MATERIAL_OPACITY)
|
||||
.clamp(0., 1.)
|
||||
} else {
|
||||
1.
|
||||
@@ -235,16 +216,8 @@ mod tests {
|
||||
let snapshot = configured_preset(index);
|
||||
let palette = Palette::from_document(Some(&snapshot.doc));
|
||||
for background in [palette.background, palette.sidebar, palette.card] {
|
||||
assert!(
|
||||
contrast(palette.text, background) >= 4.5,
|
||||
"{} text",
|
||||
preset.name
|
||||
);
|
||||
assert!(
|
||||
contrast(palette.muted, background) >= 4.5,
|
||||
"{} muted text",
|
||||
preset.name
|
||||
);
|
||||
assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name);
|
||||
assert!(contrast(palette.muted, background) >= 4.5, "{} muted text", preset.name);
|
||||
}
|
||||
for other in PRESETS.iter().skip(index + 1) {
|
||||
assert_ne!(preset.accent, other.accent);
|
||||
@@ -263,17 +236,12 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(!preset_selected(&snapshot, index));
|
||||
let mut snapshot = configured_preset(index);
|
||||
snapshot
|
||||
.edit(&set("theme.focus_ring.gradient.to", "#123456"))
|
||||
.unwrap();
|
||||
snapshot.edit(&set("theme.focus_ring.gradient.to", "#123456")).unwrap();
|
||||
assert!(!preset_selected(&snapshot, index));
|
||||
}
|
||||
assert!(preset_selected(&Snapshot::parse(String::new()).unwrap(), 0));
|
||||
assert!(preset(usize::MAX).is_empty());
|
||||
assert!(!preset_selected(
|
||||
&Snapshot::parse(String::new()).unwrap(),
|
||||
usize::MAX
|
||||
));
|
||||
assert!(!preset_selected(&Snapshot::parse(String::new()).unwrap(), usize::MAX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -283,10 +251,7 @@ mod tests {
|
||||
|
||||
for (index, item) in PRESETS.iter().enumerate() {
|
||||
let snapshot = configured_preset(index);
|
||||
assert_eq!(
|
||||
native_theme(Some(&snapshot)).cosmic().is_dark,
|
||||
item.name != "Ayu Light"
|
||||
);
|
||||
assert_eq!(native_theme(Some(&snapshot)).cosmic().is_dark, item.name != "Ayu Light");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
//! Watch the parent directory so atomic editor saves keep working.
|
||||
use crate::{Message, store::Snapshot};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use cosmic::iced::futures::{SinkExt, Stream};
|
||||
use notify::{RecursiveMode, Watcher};
|
||||
use std::{path::PathBuf, time::Duration};
|
||||
|
||||
use crate::Message;
|
||||
use crate::store::Snapshot;
|
||||
|
||||
pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let path = path.clone();
|
||||
@@ -21,9 +25,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let mut watcher = match watcher {
|
||||
Ok(watcher) => watcher,
|
||||
Err(error) => {
|
||||
let _ = output
|
||||
.send(Message::ExternalConfig(Err(error.to_string())))
|
||||
.await;
|
||||
let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -32,9 +34,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
path.parent().unwrap_or(std::path::Path::new(".")),
|
||||
RecursiveMode::NonRecursive,
|
||||
) {
|
||||
let _ = output
|
||||
.send(Message::ExternalConfig(Err(error.to_string())))
|
||||
.await;
|
||||
let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -44,11 +44,7 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
let snapshot = tokio::task::spawn_blocking(move || Snapshot::read(&file))
|
||||
.await
|
||||
.unwrap_or_else(|e| Err(e.to_string()));
|
||||
if output
|
||||
.send(Message::ExternalConfig(snapshot))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
if output.send(Message::ExternalConfig(snapshot)).await.is_err() {
|
||||
return;
|
||||
}
|
||||
if events.recv().await.is_none() {
|
||||
@@ -64,9 +60,10 @@ pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use cosmic::iced::futures::{StreamExt, pin_mut};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn watches_atomic_replacement_and_retains_subscription_after_bad_kdl() {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
@@ -79,10 +76,7 @@ mod tests {
|
||||
std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap();
|
||||
let stream = changes(&path);
|
||||
pin_mut!(stream);
|
||||
assert!(matches!(
|
||||
stream.next().await,
|
||||
Some(Message::ExternalConfig(Ok(_)))
|
||||
));
|
||||
assert!(matches!(stream.next().await, Some(Message::ExternalConfig(Ok(_)))));
|
||||
for source in ["bad [", "animations {\n speed 0.75\n}\n"] {
|
||||
let temporary = directory.path().join("replacement");
|
||||
std::fs::write(&temporary, source).unwrap();
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
os::unix::net::UnixStream,
|
||||
path::PathBuf,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub(super) fn call(command: &str) -> Result<Value, String> {
|
||||
request(command, json!({}))
|
||||
@@ -33,10 +32,7 @@ impl SessionConnection {
|
||||
}
|
||||
|
||||
fn request(&self, command: &str, args: Value) -> Result<Value, String> {
|
||||
let mut stream = self
|
||||
.stream
|
||||
.lock()
|
||||
.map_err(|_| "Compositor connection failed")?;
|
||||
let mut stream = self.stream.lock().map_err(|_| "Compositor connection failed")?;
|
||||
let request = ferese_ipc::Request {
|
||||
version: ferese_ipc::VERSION,
|
||||
id: 1,
|
||||
@@ -45,8 +41,7 @@ impl SessionConnection {
|
||||
args,
|
||||
};
|
||||
ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response =
|
||||
ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?;
|
||||
if let Some(error) = response.error {
|
||||
return Err(error.message);
|
||||
}
|
||||
@@ -89,24 +84,21 @@ impl Approval {
|
||||
}
|
||||
|
||||
pub fn prepare(&self) -> Result<(), String> {
|
||||
if matches!(self.mode, Mode::Shutdown | Mode::Suspend)
|
||||
&& login_inhibitors(self.mode)? != self.external
|
||||
{
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
if matches!(self.mode, Mode::Shutdown | Mode::Suspend) && login_inhibitors(self.mode)? != self.external {
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
if self.mode == Mode::Shutdown {
|
||||
self.connection.as_ref().ok_or("Missing session-ending lease")?.request(
|
||||
"validate-session-end",
|
||||
json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}),
|
||||
)?;
|
||||
self.connection
|
||||
.as_ref()
|
||||
.ok_or("Missing session-ending lease")?
|
||||
.request(
|
||||
"validate-session-end",
|
||||
json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}),
|
||||
)?;
|
||||
} else if self.mode == Mode::Suspend {
|
||||
let state = call("get-session-state")?;
|
||||
if state["inhibitor-revision"].as_u64() != Some(u64::from(self.revision)) {
|
||||
return Err(
|
||||
"Applications changed their inhibitors; review the confirmation again".into(),
|
||||
);
|
||||
return Err("Applications changed their inhibitors; review the confirmation again".into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -129,19 +121,12 @@ fn login_inhibitors(mode: Mode) -> Result<Vec<String>, String> {
|
||||
"org.freedesktop.login1.Manager",
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let inhibitors: Vec<(String, String, String, String, u32, u32)> = proxy
|
||||
.call("ListInhibitors", &())
|
||||
.map_err(|error| error.to_string())?;
|
||||
let what = if mode == Mode::Suspend {
|
||||
"sleep"
|
||||
} else {
|
||||
"shutdown"
|
||||
};
|
||||
let inhibitors: Vec<(String, String, String, String, u32, u32)> =
|
||||
proxy.call("ListInhibitors", &()).map_err(|error| error.to_string())?;
|
||||
let what = if mode == Mode::Suspend { "sleep" } else { "shutdown" };
|
||||
let mut reasons = inhibitors
|
||||
.into_iter()
|
||||
.filter(|(flags, _, _, inhibition, _, _)| {
|
||||
inhibition == "block" && flags.split(':').any(|flag| flag == what)
|
||||
})
|
||||
.filter(|(flags, _, _, inhibition, _, _)| inhibition == "block" && flags.split(':').any(|flag| flag == what))
|
||||
.map(|(_, app, reason, _, _, _)| format!("{app}: {reason}"))
|
||||
.collect::<Vec<_>>();
|
||||
reasons.sort();
|
||||
@@ -181,9 +166,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result<Approval, String> {
|
||||
}
|
||||
}
|
||||
let flags = if mode == Mode::Suspend { 4 } else { 1 };
|
||||
let entries = state["inhibitors"]
|
||||
.as_array()
|
||||
.ok_or("Invalid inhibitor list")?;
|
||||
let entries = state["inhibitors"].as_array().ok_or("Invalid inhibitor list")?;
|
||||
let mut reasons = entries
|
||||
.iter()
|
||||
.filter(|entry| entry["flags"].as_u64().unwrap_or(0) & flags != 0)
|
||||
@@ -192,9 +175,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result<Approval, String> {
|
||||
.as_str()
|
||||
.filter(|app| !app.is_empty())
|
||||
.unwrap_or("Application");
|
||||
let reason = entry["reason"]
|
||||
.as_str()
|
||||
.unwrap_or("Requested session inhibition");
|
||||
let reason = entry["reason"].as_str().unwrap_or("Requested session inhibition");
|
||||
format!("{app}: {reason}")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -93,8 +93,7 @@ impl Default for ShellTheme {
|
||||
|
||||
impl ShellTheme {
|
||||
pub(crate) fn palette(self) -> ferese_theme::Palette {
|
||||
let color =
|
||||
|[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.);
|
||||
let color = |[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.);
|
||||
let surface = color(self.surface_base);
|
||||
ferese_theme::Palette {
|
||||
background: surface,
|
||||
@@ -330,14 +329,8 @@ pub(crate) fn load() -> ShellConfig {
|
||||
pub(crate) fn parse_source(source: &str) -> Result<ShellConfig, ferese_config::Error> {
|
||||
match ferese_config::from_str::<FereseConfig>(source) {
|
||||
Ok(config) => {
|
||||
config
|
||||
.notifications
|
||||
.validate()
|
||||
.map_err(ferese_config::Error::from)?;
|
||||
config
|
||||
.desktop_widgets
|
||||
.validate()
|
||||
.map_err(ferese_config::Error::from)?;
|
||||
config.notifications.validate().map_err(ferese_config::Error::from)?;
|
||||
config.desktop_widgets.validate().map_err(ferese_config::Error::from)?;
|
||||
let mut theme = shell_theme(&config.theme);
|
||||
theme.material_radius = nonnegative_or(
|
||||
config
|
||||
@@ -387,19 +380,15 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
|
||||
ShellTheme {
|
||||
material_radius: defaults.material_radius,
|
||||
bar_background: {
|
||||
let mut background =
|
||||
parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background);
|
||||
let mut background = parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background);
|
||||
background[3] = alpha;
|
||||
background
|
||||
},
|
||||
bar_text_primary: parse_color(&theme.surface.bar.text_primary)
|
||||
.unwrap_or(defaults.bar_text_primary),
|
||||
bar_text_muted: parse_color(&theme.surface.bar.text_muted)
|
||||
.unwrap_or(defaults.bar_text_muted),
|
||||
bar_text_primary: parse_color(&theme.surface.bar.text_primary).unwrap_or(defaults.bar_text_primary),
|
||||
bar_text_muted: parse_color(&theme.surface.bar.text_muted).unwrap_or(defaults.bar_text_muted),
|
||||
surface_base: parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base),
|
||||
surface_popover: {
|
||||
let mut color =
|
||||
parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base);
|
||||
let mut color = parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base);
|
||||
color[3] = alpha;
|
||||
color
|
||||
},
|
||||
@@ -424,8 +413,7 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme {
|
||||
control_gap: nonnegative_or(theme.geometry.control_gap, defaults.control_gap),
|
||||
shadow_offset_y: finite_or(theme.shadow.soft.offset_y, defaults.shadow_offset_y),
|
||||
shadow_blur: nonnegative_or(theme.shadow.soft.blur, defaults.shadow_blur),
|
||||
shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity)
|
||||
.clamp(0.0, 1.0),
|
||||
shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,16 +544,8 @@ mod tests {
|
||||
.clock;
|
||||
assert!(clock.enabled);
|
||||
assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight);
|
||||
assert!(
|
||||
super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n")
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
super::parse_source(
|
||||
"desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n").is_err());
|
||||
assert!(super::parse_source("desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n").is_err());
|
||||
}
|
||||
use super::*;
|
||||
|
||||
@@ -603,10 +583,7 @@ theme {
|
||||
config.theme.typography.font_family.as_deref(),
|
||||
Some("JetBrainsMono Nerd Font")
|
||||
);
|
||||
assert_eq!(
|
||||
config.theme.background.path,
|
||||
Some(PathBuf::from("/tmp/wallpaper.png"))
|
||||
);
|
||||
assert_eq!(config.theme.background.path, Some(PathBuf::from("/tmp/wallpaper.png")));
|
||||
assert_eq!(config.theme.background.mode, WallpaperMode::Fit);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
os::{
|
||||
fd::{FromRawFd, RawFd},
|
||||
unix::net::UnixStream,
|
||||
},
|
||||
sync::mpsc::{self, Receiver, Sender, TryRecvError},
|
||||
thread,
|
||||
};
|
||||
use std::error::Error;
|
||||
use std::os::fd::{FromRawFd, RawFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::mpsc;
|
||||
use std::sync::mpsc::{Receiver, Sender, TryRecvError};
|
||||
use std::thread;
|
||||
|
||||
use ferese_protocols::shell::v1::client::{
|
||||
ferese_shell_manager_v1::FereseShellManagerV1,
|
||||
ferese_shell_v1::{self, FereseShellV1},
|
||||
};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
protocol::wl_registry,
|
||||
};
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_manager_v1::FereseShellManagerV1;
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_v1;
|
||||
use ferese_protocols::shell::v1::client::ferese_shell_v1::FereseShellV1;
|
||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||
use wayland_client::protocol::wl_registry;
|
||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub(crate) struct ShellSnapshot {
|
||||
@@ -116,11 +110,7 @@ impl ShellControl {
|
||||
match self.updates.try_recv() {
|
||||
Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)),
|
||||
Ok(ControlUpdate::LogoutCancelled(serial)) => {
|
||||
if poll
|
||||
.logout
|
||||
.as_ref()
|
||||
.is_some_and(|(pending, _)| *pending == serial)
|
||||
{
|
||||
if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) {
|
||||
poll.logout = None;
|
||||
}
|
||||
poll.logout_cancelled.push(serial);
|
||||
@@ -174,8 +164,8 @@ impl ShellControl {
|
||||
}
|
||||
|
||||
fn control_connection() -> Result<Connection, Box<dyn Error>> {
|
||||
let raw_fd = std::env::var_os("FERESE_SHELL_CONTROL_SOCKET")
|
||||
.ok_or("Ferese did not provide a shell-control connection")?;
|
||||
let raw_fd =
|
||||
std::env::var_os("FERESE_SHELL_CONTROL_SOCKET").ok_or("Ferese did not provide a shell-control connection")?;
|
||||
let fd = raw_fd
|
||||
.to_str()
|
||||
.ok_or("FERESE_SHELL_CONTROL_SOCKET is not valid UTF-8")?
|
||||
@@ -299,26 +289,14 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
||||
fullscreen,
|
||||
});
|
||||
}
|
||||
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),
|
||||
)
|
||||
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,
|
||||
} => {
|
||||
let _ = state
|
||||
.sender
|
||||
.send(ControlUpdate::Logout(serial, output_name));
|
||||
ferese_shell_v1::Event::LogoutRequested { serial, output_name } => {
|
||||
let _ = state.sender.send(ControlUpdate::Logout(serial, output_name));
|
||||
}
|
||||
ferese_shell_v1::Event::LogoutCancelled { serial } => {
|
||||
let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial));
|
||||
|
||||
@@ -6,8 +6,7 @@ pub(super) struct Entry {
|
||||
|
||||
pub(super) fn load() -> Result<Vec<Entry>, String> {
|
||||
let entries: Vec<Entry> =
|
||||
serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?)
|
||||
.map_err(|error| error.to_string())?;
|
||||
serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?).map_err(|error| error.to_string())?;
|
||||
if entries.len() > 256
|
||||
|| entries
|
||||
.iter()
|
||||
|
||||
+179
-518
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,5 @@
|
||||
//! Draw-only popup transform; layout stays stable while its presentation animates.
|
||||
use cosmic::iced::advanced::Renderer as _;
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget, layout, mouse, renderer, widget};
|
||||
use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector};
|
||||
use cosmic::{Element, Theme};
|
||||
|
||||
@@ -42,8 +41,7 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
static SETTINGS: std::sync::OnceLock<std::sync::RwLock<Settings>> = std::sync::OnceLock::new();
|
||||
static SHELL_RADIUS: std::sync::atomic::AtomicU32 =
|
||||
std::sync::atomic::AtomicU32::new(14.0_f32.to_bits());
|
||||
static SHELL_RADIUS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(14.0_f32.to_bits());
|
||||
|
||||
/// All shell shapes share one radius, capped to fit smaller controls.
|
||||
pub(crate) fn radius(limit: f32) -> f32 {
|
||||
@@ -97,17 +95,14 @@ impl PopupMotion {
|
||||
}
|
||||
|
||||
pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 {
|
||||
let duration = self
|
||||
.settings
|
||||
.duration(if self.target == 1.0 { 200.0 } else { 140.0 });
|
||||
let duration = self.settings.duration(if self.target == 1.0 { 200.0 } else { 140.0 });
|
||||
if duration.is_zero() {
|
||||
return self.target;
|
||||
}
|
||||
let Some(started) = self.started else {
|
||||
return self.start;
|
||||
};
|
||||
let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32())
|
||||
.min(1.0);
|
||||
let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32()).min(1.0);
|
||||
if t == 1.0 {
|
||||
return self.target;
|
||||
}
|
||||
@@ -145,14 +140,7 @@ pub(crate) fn button<'a, M: Clone + 'a>(
|
||||
let active = progress.clone();
|
||||
let hovered = progress.clone();
|
||||
let paint = move |progress: f32, pressed: bool| {
|
||||
ferese_theme::controls::shell_button(
|
||||
foreground,
|
||||
selected,
|
||||
opacity,
|
||||
radius(14.),
|
||||
progress,
|
||||
pressed,
|
||||
)
|
||||
ferese_theme::controls::shell_button(foreground, selected, opacity, radius(14.), progress, pressed)
|
||||
};
|
||||
let content = button
|
||||
.class(cosmic::theme::Button::Custom {
|
||||
@@ -181,12 +169,7 @@ struct HoverState {
|
||||
}
|
||||
|
||||
impl HoverState {
|
||||
fn advance(
|
||||
&mut self,
|
||||
target: f32,
|
||||
now: std::time::Instant,
|
||||
duration: std::time::Duration,
|
||||
) -> bool {
|
||||
fn advance(&mut self, target: f32, now: std::time::Instant, duration: std::time::Duration) -> bool {
|
||||
if self.target != target {
|
||||
self.start = self.current;
|
||||
self.target = target;
|
||||
@@ -196,8 +179,7 @@ impl HoverState {
|
||||
1.0
|
||||
} else {
|
||||
self.started.map_or(1.0, |start| {
|
||||
(now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32())
|
||||
.min(1.0)
|
||||
(now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32()).min(1.0)
|
||||
})
|
||||
};
|
||||
self.current = self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t);
|
||||
@@ -222,9 +204,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
tree: &widget::Tree,
|
||||
cursor: mouse::Cursor,
|
||||
) -> iced_accessibility::A11yTree {
|
||||
self.content
|
||||
.as_widget()
|
||||
.a11y_nodes(layout, &tree.children[0], cursor)
|
||||
self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor)
|
||||
}
|
||||
|
||||
fn tag(&self) -> widget::tree::Tag {
|
||||
@@ -298,17 +278,10 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
self.progress
|
||||
.set(tree.state.downcast_ref::<HoverState>().current);
|
||||
self.content.as_widget().draw(
|
||||
&tree.children[0],
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
);
|
||||
self.progress.set(tree.state.downcast_ref::<HoverState>().current);
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport);
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
@@ -319,13 +292,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
&tree.children[0],
|
||||
layout,
|
||||
cursor,
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
@@ -348,22 +317,13 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Hover<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
translation: Vector,
|
||||
) -> Option<cosmic::iced::advanced::overlay::Element<'a, M, Theme, cosmic::Renderer>> {
|
||||
self.content.as_widget_mut().overlay(
|
||||
&mut tree.children[0],
|
||||
layout,
|
||||
renderer,
|
||||
viewport,
|
||||
translation,
|
||||
)
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.overlay(&mut tree.children[0], layout, renderer, viewport, translation)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn animated<'a, M: 'a>(
|
||||
content: Element<'a, M>,
|
||||
progress: f32,
|
||||
regions: Regions,
|
||||
radius: f32,
|
||||
) -> Element<'a, M> {
|
||||
pub fn animated<'a, M: 'a>(content: Element<'a, M>, progress: f32, regions: Regions, radius: f32) -> Element<'a, M> {
|
||||
Element::new(Motion {
|
||||
content,
|
||||
progress,
|
||||
@@ -445,9 +405,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
}
|
||||
*self.regions.lock().unwrap() = collector.into_regions(self.translation(), self.radius);
|
||||
let cursor = self.cursor(cursor);
|
||||
self.content.as_widget_mut().update(
|
||||
tree, event, layout, cursor, renderer, clipboard, shell, viewport,
|
||||
);
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.update(tree, event, layout, cursor, renderer, clipboard, shell, viewport);
|
||||
}
|
||||
|
||||
fn draw(
|
||||
@@ -463,15 +423,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
let v = self.translation();
|
||||
let transform = Transformation::translate(v.x, v.y);
|
||||
renderer.with_transformation(transform, |renderer| {
|
||||
self.content.as_widget().draw(
|
||||
tree,
|
||||
renderer,
|
||||
theme,
|
||||
style,
|
||||
layout,
|
||||
self.cursor(cursor),
|
||||
viewport,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.draw(tree, renderer, theme, style, layout, self.cursor(cursor), viewport)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -483,13 +437,9 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
viewport: &Rectangle,
|
||||
renderer: &cosmic::Renderer,
|
||||
) -> mouse::Interaction {
|
||||
self.content.as_widget().mouse_interaction(
|
||||
tree,
|
||||
layout,
|
||||
self.cursor(cursor),
|
||||
viewport,
|
||||
renderer,
|
||||
)
|
||||
self.content
|
||||
.as_widget()
|
||||
.mouse_interaction(tree, layout, self.cursor(cursor), viewport, renderer)
|
||||
}
|
||||
|
||||
fn operate(
|
||||
@@ -499,9 +449,7 @@ impl<M> Widget<M, Theme, cosmic::Renderer> for Motion<'_, M> {
|
||||
renderer: &cosmic::Renderer,
|
||||
operation: &mut dyn widget::Operation,
|
||||
) {
|
||||
self.content
|
||||
.as_widget_mut()
|
||||
.operate(tree, layout, renderer, operation);
|
||||
self.content.as_widget_mut().operate(tree, layout, renderer, operation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -557,11 +505,7 @@ mod tests {
|
||||
fn centered_modal_material_tracks_surface_position_and_animation() {
|
||||
let mut collector = CollectRegions(Vec::new());
|
||||
let card = Rectangle::new((380.0, 420.0).into(), (440.0, 220.0).into());
|
||||
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!(
|
||||
collector.into_regions(Vector::new(0.0, -2.0), 14.0),
|
||||
vec![[380, 418, 440, 220, 14]]
|
||||
@@ -572,29 +516,16 @@ mod tests {
|
||||
fn popup_waits_for_configuration_then_opens_and_closes_faster() {
|
||||
let now = std::time::Instant::now();
|
||||
let mut motion = PopupMotion::new(Settings::default());
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_secs(30)),
|
||||
0.0
|
||||
);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_secs(30)), 0.0);
|
||||
motion.begin(now);
|
||||
// Later configure/focus notifications must not restart the clock.
|
||||
motion.begin(now + std::time::Duration::from_millis(90));
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(200)),
|
||||
1.0
|
||||
);
|
||||
assert!((motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(200)), 1.0);
|
||||
motion.retarget(0.0, now + std::time::Duration::from_millis(200));
|
||||
assert!(motion.closing());
|
||||
assert!(
|
||||
(motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001
|
||||
);
|
||||
assert_eq!(
|
||||
motion.progress_at(now + std::time::Duration::from_millis(340)),
|
||||
0.0
|
||||
);
|
||||
assert!((motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001);
|
||||
assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(340)), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -613,10 +544,7 @@ mod tests {
|
||||
let before = motion.progress_at(reopen);
|
||||
motion.retarget(1.0, reopen);
|
||||
assert_eq!(motion.progress_at(reopen), before);
|
||||
assert_eq!(
|
||||
motion.progress_at(reopen + std::time::Duration::from_secs(1)),
|
||||
1.0
|
||||
);
|
||||
assert_eq!(motion.progress_at(reopen + std::time::Duration::from_secs(1)), 1.0);
|
||||
for settings in [
|
||||
Settings {
|
||||
reduced_motion: true,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Atomic, conflict-checked note edits; always called from a worker thread.
|
||||
use std::{fs, io::Write, path::Path};
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Edit {
|
||||
@@ -63,13 +65,10 @@ pub fn save(path: &Path, edits: &[Edit]) -> Result<(), String> {
|
||||
file.as_file()
|
||||
.set_permissions(fs::metadata(path).map_err(|e| e.to_string())?.permissions())
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.write_all(updated.as_bytes())
|
||||
.map_err(|e| e.to_string())?;
|
||||
file.write_all(updated.as_bytes()).map_err(|e| e.to_string())?;
|
||||
file.as_file().sync_all().map_err(|e| e.to_string())?;
|
||||
if fs::read_to_string(path).map_err(|e| e.to_string())? != source {
|
||||
return Err(
|
||||
"Config changed while saving; your note remains open. Edit again to retry.".into(),
|
||||
);
|
||||
return Err("Config changed while saving; your note remains open. Edit again to retry.".into());
|
||||
}
|
||||
file.persist(path).map_err(|e| e.to_string())?;
|
||||
fs::File::open(parent)
|
||||
@@ -131,10 +130,7 @@ desktop-widgets {
|
||||
let source = fs::read_to_string(&path).unwrap();
|
||||
assert!(source.contains("// keep this"));
|
||||
assert!(source.contains("#123456"));
|
||||
let notes = crate::config::parse_source(&source)
|
||||
.unwrap()
|
||||
.desktop_widgets
|
||||
.notes;
|
||||
let notes = crate::config::parse_source(&source).unwrap().desktop_widgets.notes;
|
||||
assert_eq!(notes[0].text, "one\ntwo");
|
||||
assert_eq!((notes[0].margin_x, notes[0].margin_y), (50, 60));
|
||||
assert_eq!(notes[0].anchor, ferese_core::desktop::Anchor::TopLeft);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
use super::*;
|
||||
use cosmic::iced::widget::scrollable::{Direction, Scrollbar};
|
||||
use cosmic::widget::{column, scrollable};
|
||||
|
||||
use super::*;
|
||||
|
||||
const POPUP_WIDTH: u32 = 368;
|
||||
|
||||
fn popup_height(notice: ¬ifications::Notice, count: usize) -> u32 {
|
||||
let body = if notice.body.is_empty() && count == 1 {
|
||||
0
|
||||
} else {
|
||||
22
|
||||
};
|
||||
let body = if notice.body.is_empty() && count == 1 { 0 } else { 22 };
|
||||
let actions = if notice.live && notice.actions.iter().any(|(key, _)| key != "default") {
|
||||
39
|
||||
} else {
|
||||
@@ -33,18 +30,13 @@ fn history_height(center: ¬ifications::Center) -> u32 {
|
||||
} else {
|
||||
1
|
||||
};
|
||||
popup_height(notice, count)
|
||||
+ wrapped_lines.saturating_sub(1) as u32 * 17
|
||||
+ if count > 1 { 12 } else { 0 }
|
||||
popup_height(notice, count) + wrapped_lines.saturating_sub(1) as u32 * 17 + if count > 1 { 12 } else { 0 }
|
||||
};
|
||||
let cards: u32 = groups
|
||||
.iter()
|
||||
.map(|group| {
|
||||
if group.len() > 1 && center.expanded_apps.contains(&group[0].app) {
|
||||
32 + group
|
||||
.iter()
|
||||
.map(|notice| card_height(notice, 1))
|
||||
.sum::<u32>()
|
||||
32 + group.iter().map(|notice| card_height(notice, 1)).sum::<u32>()
|
||||
+ group.len().saturating_sub(1) as u32 * 8
|
||||
} else {
|
||||
card_height(group[0], group.len())
|
||||
@@ -188,9 +180,10 @@ impl FereseShell {
|
||||
.outputs
|
||||
.iter()
|
||||
.find(|entry| {
|
||||
self.snapshot.outputs.iter().any(|output| {
|
||||
output.focused && Some(output.name.as_str()) == entry.name.as_deref()
|
||||
})
|
||||
self.snapshot
|
||||
.outputs
|
||||
.iter()
|
||||
.any(|output| output.focused && Some(output.name.as_str()) == entry.name.as_deref())
|
||||
})
|
||||
.or_else(|| self.outputs.first())
|
||||
.map(|entry| entry.output.clone());
|
||||
@@ -213,9 +206,7 @@ impl FereseShell {
|
||||
.notifications
|
||||
.popup_groups()
|
||||
.iter()
|
||||
.map(|(notice, _, count)| {
|
||||
popup_height(notice, *count) + if *count > 1 { 12 } else { 0 }
|
||||
})
|
||||
.map(|(notice, _, count)| popup_height(notice, *count) + if *count > 1 { 12 } else { 0 })
|
||||
.sum::<u32>()
|
||||
+ count.saturating_sub(1) as u32 * 8
|
||||
+ 8;
|
||||
@@ -299,8 +290,7 @@ impl FereseShell {
|
||||
let id = notice.id;
|
||||
let mut bold = *SHELL_FONT.get().unwrap().read().unwrap();
|
||||
bold.weight = cosmic::iced::font::Weight::Bold;
|
||||
let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty()
|
||||
{
|
||||
let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty() {
|
||||
accented_icon(ferese_theme::icons::FERESE, 18, accent, accent)
|
||||
} else if notice.icon.starts_with('/') {
|
||||
icon::icon(icon::from_path(notice.icon.clone().into())).size(18)
|
||||
@@ -308,18 +298,17 @@ impl FereseShell {
|
||||
icon::from_name(notice.icon.as_str()).size(18).icon()
|
||||
}
|
||||
.opacity(opacity);
|
||||
let icon_well =
|
||||
container(app_icon)
|
||||
.center_x(30)
|
||||
.center_y(30)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(15.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
let icon_well = container(app_icon)
|
||||
.center_x(30)
|
||||
.center_y(30)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(15.0).into(),
|
||||
..Default::default()
|
||||
}));
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let icon: Element<'_, cosmic::Action<Message>> = if count > 1 {
|
||||
let badge_fill = ferese_theme::composite(
|
||||
color(palette.accent),
|
||||
@@ -329,8 +318,7 @@ impl FereseShell {
|
||||
palette.surface_popover
|
||||
}),
|
||||
);
|
||||
let (badge_fill, badge_text) =
|
||||
ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
||||
let (badge_fill, badge_text) = ferese_theme::accent_pair(badge_fill, color(palette.text_primary));
|
||||
let badge = container(
|
||||
text(count.to_string())
|
||||
.size(10)
|
||||
@@ -375,15 +363,10 @@ impl FereseShell {
|
||||
.align_y(alignment::Vertical::Center)
|
||||
.push(icon)
|
||||
.push(
|
||||
container(
|
||||
text(app)
|
||||
.size(11)
|
||||
.font(bold)
|
||||
.class(theme::Text::Color(muted)),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
.clip(true),
|
||||
container(text(app).size(11).font(bold).class(theme::Text::Color(muted)))
|
||||
.width(Length::Fill)
|
||||
.height(16)
|
||||
.clip(true),
|
||||
)
|
||||
.push(
|
||||
text(age_label(notice.received_at.elapsed()))
|
||||
@@ -446,13 +429,13 @@ impl FereseShell {
|
||||
let mut actions = row([]);
|
||||
for (index, (key, label)) in available.into_iter().enumerate() {
|
||||
if index > 0 {
|
||||
actions = actions.push(
|
||||
container(cosmic::iced::widget::Space::new().width(1).height(38))
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
actions =
|
||||
actions.push(container(cosmic::iced::widget::Space::new().width(1).height(38)).class(
|
||||
theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(divider)),
|
||||
..Default::default()
|
||||
})),
|
||||
);
|
||||
}),
|
||||
));
|
||||
}
|
||||
actions = actions.push(card_button(
|
||||
container(
|
||||
@@ -472,17 +455,12 @@ impl FereseShell {
|
||||
}
|
||||
face = face
|
||||
.push(
|
||||
container(
|
||||
cosmic::iced::widget::Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(1),
|
||||
)
|
||||
.class(theme::Container::custom(move |_| {
|
||||
container::Style {
|
||||
container(cosmic::iced::widget::Space::new().width(Length::Fill).height(1)).class(
|
||||
theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(divider)),
|
||||
..Default::default()
|
||||
}
|
||||
})),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.push(actions);
|
||||
}
|
||||
@@ -518,23 +496,19 @@ impl FereseShell {
|
||||
// bottom edges, after the card's measured height.
|
||||
let back = move |inset: f32| {
|
||||
container(
|
||||
container(
|
||||
cosmic::iced::widget::Space::new()
|
||||
.width(Length::Fill)
|
||||
.height(6),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.045 * opacity,
|
||||
..foreground
|
||||
})),
|
||||
border: Border {
|
||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||
container(cosmic::iced::widget::Space::new().width(Length::Fill).height(6))
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(Color {
|
||||
a: 0.045 * opacity,
|
||||
..foreground
|
||||
})),
|
||||
border: Border {
|
||||
radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
})),
|
||||
)
|
||||
.padding([0.0, inset])
|
||||
};
|
||||
@@ -552,10 +526,7 @@ impl FereseShell {
|
||||
Message::ToggleNotificationHistory
|
||||
}));
|
||||
} else if notice.live && notice.actions.iter().any(|(key, _)| key == "default") {
|
||||
area = area.on_press(cosmic::Action::App(Message::InvokeNotification(
|
||||
id,
|
||||
"default".into(),
|
||||
)));
|
||||
area = area.on_press(cosmic::Action::App(Message::InvokeNotification(id, "default".into())));
|
||||
}
|
||||
area.into()
|
||||
}
|
||||
@@ -589,11 +560,7 @@ impl FereseShell {
|
||||
} else {
|
||||
format!(
|
||||
"{count} {} · {} {}",
|
||||
if count == 1 {
|
||||
"notification"
|
||||
} else {
|
||||
"notifications"
|
||||
},
|
||||
if count == 1 { "notification" } else { "notifications" },
|
||||
groups.len(),
|
||||
if groups.len() == 1 { "app" } else { "apps" }
|
||||
)
|
||||
@@ -619,11 +586,7 @@ impl FereseShell {
|
||||
well,
|
||||
28,
|
||||
));
|
||||
let dnd_color = if self.notifications.dnd {
|
||||
accent
|
||||
} else {
|
||||
muted
|
||||
};
|
||||
let dnd_color = if self.notifications.dnd { accent } else { muted };
|
||||
let dnd_icon = if self.notifications.dnd {
|
||||
ferese_theme::icons::NOTIFICATIONS_OFF
|
||||
} else {
|
||||
@@ -674,40 +637,34 @@ impl FereseShell {
|
||||
self.notifications.dnd,
|
||||
1.0,
|
||||
));
|
||||
let controls =
|
||||
container(dnd)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(base)),
|
||||
border: Border {
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
let controls = container(dnd)
|
||||
.padding(10)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(base)),
|
||||
border: Border {
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
}));
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
let mut content = column([]).spacing(10).push(header).push(controls);
|
||||
if count == 0 {
|
||||
let empty = column([])
|
||||
.spacing(10)
|
||||
.align_x(alignment::Horizontal::Center)
|
||||
.push(
|
||||
container(accented_icon(
|
||||
ferese_theme::icons::NOTIFICATIONS,
|
||||
26,
|
||||
muted,
|
||||
accent,
|
||||
))
|
||||
.center_x(56)
|
||||
.center_y(56)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(28.0).into(),
|
||||
container(accented_icon(ferese_theme::icons::NOTIFICATIONS, 26, muted, accent))
|
||||
.center_x(56)
|
||||
.center_y(56)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: Some(Background::Color(well)),
|
||||
border: Border {
|
||||
radius: motion::radius(28.0).into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
})),
|
||||
})),
|
||||
)
|
||||
.push(
|
||||
text("You're all caught up")
|
||||
@@ -720,11 +677,7 @@ impl FereseShell {
|
||||
.size(11)
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
content = content.push(
|
||||
container(empty)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill),
|
||||
);
|
||||
content = content.push(container(empty).center_x(Length::Fill).center_y(Length::Fill));
|
||||
} else {
|
||||
content = content.push(
|
||||
row([])
|
||||
@@ -786,12 +739,7 @@ impl FereseShell {
|
||||
}
|
||||
entries = entries.push(cards);
|
||||
} else {
|
||||
entries = entries.push(self.notification_card(
|
||||
latest,
|
||||
opacity,
|
||||
true,
|
||||
group.len(),
|
||||
));
|
||||
entries = entries.push(self.notification_card(latest, opacity, true, group.len()));
|
||||
}
|
||||
}
|
||||
content = content.push(
|
||||
@@ -806,8 +754,7 @@ impl FereseShell {
|
||||
.height(history_height(&self.notifications) as f32)
|
||||
.width(Length::Fill)
|
||||
.class(theme::Container::custom(move |_| container::Style {
|
||||
background: (!compositor_material)
|
||||
.then_some(Background::Color(color(palette.surface_popover))),
|
||||
background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))),
|
||||
border: Border {
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: 1.0,
|
||||
@@ -819,9 +766,7 @@ impl FereseShell {
|
||||
motion::animated(
|
||||
panel,
|
||||
progress,
|
||||
surface
|
||||
.map(|surface| surface.regions.clone())
|
||||
.unwrap_or_default(),
|
||||
surface.map(|surface| surface.regions.clone()).unwrap_or_default(),
|
||||
palette.material_radius,
|
||||
)
|
||||
} else {
|
||||
|
||||
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user