Reduce Settings page and config reload overhead

Mount nearby shortcut and theme rows, preserve gallery keyboard navigation, reset page scrolling, and reuse native themes during palette transitions. Prepare and deduplicate live config reloads on a worker and apply only changed runtime sections.

Align pointer focus and overview selection on overlapping outputs, add regression coverage, and clean up workspace Clippy warnings.
This commit is contained in:
Samuel Onoja committed 2026-10-02 23:10:01 +01:00
1 parent 629414e0fb
commit 647eaf4ad9
23 files changed
+1098 -220

No files matched your search

+2 -1
View File
@@ -25,7 +25,8 @@ fn visible(states: &RenderElementStates) -> HashSet<Id> {
states
.states
.iter()
.filter_map(|(id, state)| is_visible(*state).then(|| id.clone()))
.filter(|&(_id, state)| is_visible(*state))
.map(|(id, _state)| id.clone())
.collect()
}
+14
View File
@@ -176,6 +176,20 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result<Screen
"Missing session revision",
));
}
} else if call.request.command == "reload-config" {
if let Err(error) = validate_request(&call.request) {
let _ = call
.response
.try_send(Response::error(call.request.id, error.code, error.message));
} else if state.session_lock.active() {
let _ = call.response.try_send(Response::error(
call.request.id,
"session_locked",
"IPC unavailable while session is locked",
));
} else if let Err(error) = state.queue_config_reload(false, Some((call.request.id, call.response))) {
tracing::warn!(%error, "cannot queue configuration reload");
}
} else if call.request.command == "screenshot-window" {
state.start_window_screenshot(call.request, call.response);
} else if call.request.command == "screenshot" {
+11
View File
@@ -96,6 +96,17 @@ fn main() -> Result<(), Box<dyn Error>> {
let display = Display::new()?;
let mut state = Ferese::new(&mut event_loop, display, runtime)?;
state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024);
state.config_sections = state
.config_source
.as_deref()
.and_then(|source| ferese_config::Document::parse(source).ok())
.map(|document| {
let mut value = document.value().clone();
if let Some(object) = value.as_object_mut() {
object.remove("theme");
}
value
});
theme::init(&mut event_loop, &mut state, candidate)?;
resume::init(&mut event_loop)?;
overview::init_font_loader(&mut event_loop, &mut state)?;
+4 -7
View File
@@ -758,13 +758,10 @@ impl Ferese {
if !self.overview.is_active() {
return None;
}
self.space
.outputs()
.find(|output| {
self.output_bounds_for(output)
.is_some_and(|bounds| contains(workspace_strip(bounds), point))
})
.cloned()
let output = self.space.output_under(point).next()?;
self.output_bounds_for(output)
.is_some_and(|bounds| contains(workspace_strip(bounds), point))
.then(|| output.clone())
}
pub(crate) fn click_overview_workspace(&mut self, point: Point<f64, Logical>) -> bool {
+1 -1
View File
@@ -155,7 +155,7 @@ impl PortalSession {
}
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) {
if !self.query_ready() || self.query.as_ref().is_none_or(|query| query.token != token) {
return Err("Session-ending confirmation expired or applications are still responding".into());
}
if self.inhibitor_revision != inhibitor_revision
+1 -1
View File
@@ -49,7 +49,7 @@ impl Ferese {
surfaces
.into_iter()
.filter(|surface| mapped_tree(surface))
.filter(mapped_tree)
.filter_map(|surface| {
let id = Id::from(&surface);
let output = self
+284 -21
View File
@@ -117,38 +117,225 @@ impl ConfigMonitor {
}
}
impl crate::Ferese {
pub(crate) fn reload_config_source(&mut self, source: String) -> Result<(), String> {
#[derive(Clone)]
struct Prepared {
source: String,
sections: serde_json::Value,
runtime: crate::RuntimeConfig,
candidate: ferese_config::theme::Candidate,
}
impl Prepared {
fn new(source: String, directory: &std::path::Path) -> Result<Self, String> {
Self::with_watch(source, directory, &mut Vec::new())
}
fn with_watch(source: String, directory: &std::path::Path, watched: &mut Vec<PathBuf>) -> Result<Self, String> {
if source.len() > 60 * 1024 {
return Err("configuration exceeds the 60 KiB live-reload limit".into());
}
// Validate every runtime field before mutating any scene/input state.
let directory = crate::config::config_path()
.and_then(|path| path.parent().map(ToOwned::to_owned))
.unwrap_or_else(|| PathBuf::from("."));
let (_, mut runtime, candidate) = match crate::theme::prepare(&source, &directory) {
Ok(prepared) => prepared,
Err(error) => {
self.theme_engine.reject(error.clone());
return Err(error);
let document = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?;
if let Some(value) = document.get("theme")
&& let Ok(policy) = serde_json::from_value::<ferese_config::theme::Policy>(value.clone())
{
watched.extend(
[&policy.file, &policy.light.file, &policy.dark.file]
.into_iter()
.flatten()
.chain(policy.custom_themes.values().map(|theme| &theme.file))
.filter(|path| !path.as_os_str().is_empty())
.map(|path| ferese_config::theme::theme_path(directory, path)),
);
}
let (_, runtime, candidate) = crate::theme::prepare_document(&document, directory)?;
let mut sections = document.value().clone();
if let Some(object) = sections.as_object_mut() {
object.remove("theme");
}
Ok(Self {
source,
sections,
runtime,
candidate,
})
}
}
fn file_stamps(files: &[PathBuf]) -> Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)> {
files
.iter()
.map(|path| {
let stamp = path
.metadata()
.ok()
.and_then(|m| m.modified().ok().map(|time| (time, m.len())));
(path.clone(), stamp)
})
.collect()
}
type Reply = (u64, std::sync::mpsc::SyncSender<ferese_ipc::Response>);
struct Request {
force: bool,
fallback: Option<String>,
reply: Option<Reply>,
}
pub(crate) struct Worker {
requests: std::sync::mpsc::SyncSender<Request>,
}
impl Worker {
pub(crate) fn new(
event_loop: &mut calloop::EventLoop<'static, crate::Ferese>,
) -> Result<Self, Box<dyn std::error::Error>> {
let (requests, receiver) = std::sync::mpsc::sync_channel::<Request>(8);
let (results, events) = calloop::channel::channel::<(Result<Prepared, String>, Vec<PathBuf>, Option<Reply>)>();
event_loop.handle().insert_source(events, |event, _, state| {
if let calloop::channel::Event::Msg((result, watched, reply)) = event {
state.theme_engine.watch_files(&watched);
let result = result.and_then(|prepared| state.apply_prepared_config(prepared));
if let Err(error) = &result {
state.theme_engine.reject(error.clone());
}
if let Some((id, reply)) = reply {
let response = match result {
Ok(()) => ferese_ipc::Response::success(id, serde_json::json!({})),
Err(error) => ferese_ipc::Response::error(id, "invalid_config", error),
};
let _ = reply.try_send(response);
}
}
})?;
std::thread::Builder::new()
.name("ferese-config".into())
.spawn(move || {
let mut cached: Option<Prepared> = None;
let mut stamps = Vec::new();
for request in receiver {
let started = std::time::Instant::now();
let path = crate::config::config_path();
let directory = path
.as_ref()
.and_then(|p| p.parent())
.unwrap_or(std::path::Path::new("."));
let mut watched = Vec::new();
let result = path
.as_ref()
.map_or_else(
|| request.fallback.ok_or_else(|| "No configuration available".into()),
|path| {
if path.exists() || !request.force {
crate::theme::read_source(path)
} else {
Ok(String::new())
}
},
)
.and_then(|source| {
// Watcher and explicit reloads share the same cache.
// Auto mode still resolves time and system appearance.
if let Some(previous) = &cached
&& previous.source == source
&& previous.candidate.policy.mode != ferese_config::theme::Mode::Auto
&& file_stamps(&previous.candidate.files) == stamps
{
return Ok(previous.clone());
}
let prepared = Prepared::with_watch(source, directory, &mut watched)?;
stamps = file_stamps(&prepared.candidate.files);
cached = Some(prepared.clone());
Ok(prepared)
});
tracing::debug!(
elapsed_us = started.elapsed().as_micros() as u64,
"configuration prepared on worker"
);
if results.send((result, watched, request.reply)).is_err() {
break;
}
}
})?;
Ok(Self { requests })
}
}
impl crate::Ferese {
pub(crate) fn queue_config_reload(&self, force: bool, reply: Option<Reply>) -> Result<(), String> {
let request = Request {
force,
fallback: self.config_source.clone(),
reply,
};
let worker = self.config_worker.as_ref().ok_or("configuration worker unavailable")?;
if let Err(error) = worker.requests.try_send(request) {
let (std::sync::mpsc::TrySendError::Full(request) | std::sync::mpsc::TrySendError::Disconnected(request)) =
error;
if let Some((id, reply)) = request.reply {
let _ = reply.try_send(ferese_ipc::Response::error(
id,
"reload_busy",
"Configuration reload queue is full",
));
}
return Err("Configuration reload queue is full".into());
}
Ok(())
}
fn apply_prepared_config(&mut self, prepared: Prepared) -> Result<(), String> {
let Prepared {
source,
sections,
mut runtime,
candidate,
} = prepared;
// Identical watcher/IPC reloads do not restart a theme transition.
if self.config_source.as_ref() == Some(&source)
&& candidate.theme == self.theme_engine.snapshot.theme
&& candidate.families == self.theme_engine.snapshot.families
&& candidate.policy.mode == self.theme_engine.snapshot.mode
&& candidate.warnings == self.theme_engine.snapshot.warnings
&& candidate.fallback_note == self.theme_engine.snapshot.fallback_note
&& self.theme_engine.matches_files(&candidate.files)
&& self.theme_engine.snapshot.error.is_none()
{
if let Err(error) = self.theme_engine.arm_clock(candidate.next_transition) {
tracing::warn!(%error, "cannot arm appearance schedule");
}
return Ok(());
}
let wallpaper = runtime.wallpaper.clone();
runtime.wallpaper = self.wallpaper.configuration().clone();
let previous_theme = self.theme_settings;
let non_theme = |source: &str| -> Option<serde_json::Value> {
let document = ferese_config::Document::parse(source).ok()?;
let mut value = document.value().clone();
value.as_object_mut()?.remove("theme");
Some(value)
};
if self.config_source.as_deref().and_then(non_theme) != non_theme(&source) {
self.apply_runtime_config(runtime)?;
if self.config_sections.is_none() {
self.config_sections = self
.config_source
.as_deref()
.and_then(|s| ferese_config::Document::parse(s).ok())
.map(|document| {
let mut value = document.value().clone();
if let Some(object) = value.as_object_mut() {
object.remove("theme");
}
value
});
}
if self.config_sections.as_ref() != Some(&sections) {
self.apply_runtime_config(runtime, &sections)?;
}
self.overview
.set_font_family(candidate.theme.tokens.typography.font_family.clone());
self.theme_settings = previous_theme;
self.accept_theme(candidate, wallpaper);
self.config_sections = Some(sections);
self.config_source = Some(source.clone());
self.shell_resources.retain(|resource| {
if let Ok(shell) = resource.upgrade() {
@@ -164,10 +351,17 @@ impl crate::Ferese {
Ok(())
}
pub(crate) fn reload_config_source(&mut self, source: String) -> Result<(), String> {
let directory = crate::config::config_path()
.and_then(|p| p.parent().map(ToOwned::to_owned))
.unwrap_or_else(|| PathBuf::from("."));
let prepared = Prepared::new(source, &directory)?;
self.apply_prepared_config(prepared)
}
pub(crate) fn reload_config(&mut self) -> Result<(), String> {
let path = crate::config::config_path().ok_or("config directory is unavailable")?;
let source = crate::theme::read_source(&path)?;
self.reload_config_source(source)
self.reload_config_source(crate::theme::read_source(&path)?)
}
}
@@ -194,6 +388,75 @@ mod tests {
}
}
#[test]
fn worker_reload_applies_once_and_rejects_invalid_configuration() {
const CHILD: &str = "FERESE_WORKER_RELOAD_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
use std::os::unix::fs::PermissionsExt;
let directory = Directory::new();
std::fs::set_permissions(&directory.0, std::fs::Permissions::from_mode(0o700)).unwrap();
let result = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"reload::tests::worker_reload_applies_once_and_rejects_invalid_configuration",
"--nocapture",
])
.env(CHILD, "1")
.env("XDG_RUNTIME_DIR", &directory.0)
.env("XDG_CONFIG_HOME", &directory.0)
.env_remove("FERESE_SOCKET")
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
return;
}
let (_, runtime, _) = crate::theme::prepare("", std::path::Path::new(".")).unwrap();
let mut event_loop = calloop::EventLoop::try_new().unwrap();
let display = smithay::reexports::wayland_server::Display::new().unwrap();
let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap();
let path = crate::config::config_path().unwrap();
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let valid = "animations { speed 0.5; }";
std::fs::write(&path, valid).unwrap();
for (index, source) in [Some(valid), Some(valid), Some("animations { speed -1.0; }"), None]
.into_iter()
.enumerate()
{
if let Some(source) = source {
std::fs::write(&path, source).unwrap();
} else {
std::fs::remove_file(&path).unwrap();
}
let before = state.theme_engine.snapshot.revision;
let (reply, received) = std::sync::mpsc::sync_channel(1);
state.queue_config_reload(index == 1, Some((42, reply))).unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
let response = loop {
event_loop.dispatch(Duration::from_millis(10), &mut state).unwrap();
if let Ok(response) = received.try_recv() {
break response;
}
assert!(Instant::now() < deadline, "worker did not reply");
};
assert_eq!(response.id, 42);
assert_eq!(response.error.is_some(), source != Some(valid));
assert_eq!(
state.animation_duration(Duration::from_millis(100)),
Duration::from_millis(200)
);
assert_eq!(state.config_source.as_deref(), Some(valid));
if source == Some(valid) && before > 0 {
assert_eq!(state.theme_engine.snapshot.revision, before);
}
}
}
#[test]
fn live_reload_keeps_dimming_policy_independent_of_theme_transitions() {
if std::env::var_os("FERESE_DIM_RELOAD_TEST_CHILD").is_none() {
+70 -34
View File
@@ -317,6 +317,8 @@ pub struct Ferese {
pub(crate) session_lock: Lock,
pub(crate) lock_idle: IdleSettings,
pub(crate) config_source: Option<String>,
pub(crate) config_sections: Option<serde_json::Value>,
pub(crate) config_worker: Option<crate::reload::Worker>,
pub(crate) theme_engine: crate::theme::Engine,
pub start_time: Instant,
pub socket_name: OsString,
@@ -434,6 +436,7 @@ pub struct Ferese {
pub xdg_toplevel_icon_manager: XdgToplevelIconManager,
}
#[derive(Clone)]
pub struct RuntimeConfig {
pub(crate) lock_idle: IdleSettings,
pub(crate) autostart: Vec<DaemonConfig>,
@@ -542,6 +545,8 @@ impl Ferese {
session_lock: crate::session_lock::Lock::default(),
lock_idle: config.lock_idle,
config_source: None,
config_sections: None,
config_worker: None,
theme_engine: Default::default(),
start_time,
socket_name,
@@ -659,6 +664,7 @@ impl Ferese {
xdg_shell_state,
xdg_toplevel_icon_manager,
};
state.config_worker = Some(crate::reload::Worker::new(event_loop)?);
let screenshot = crate::ipc::init(event_loop)?;
state.screenshot_parts = Some(screenshot.parts);
state.screenshot_worker = Some(screenshot.worker);
@@ -667,7 +673,23 @@ impl Ferese {
Ok(state)
}
pub(crate) fn apply_runtime_config(&mut self, config: RuntimeConfig) -> Result<(), String> {
pub(crate) fn apply_runtime_config(
&mut self,
config: RuntimeConfig,
sections: &serde_json::Value,
) -> Result<(), String> {
let changed = |keys: &[&str]| {
self.config_sections
.as_ref()
.is_none_or(|previous| keys.iter().any(|key| previous.get(*key) != sections.get(*key)))
};
let layout_changed = changed(&["layout", "scrolling", "workspaces"]);
let bindings_changed = changed(&["bindings", "commands"]);
let input_changed = changed(&["input"]);
let daemons_changed = changed(&["autostart"]);
let lock_changed = changed(&["lock_screen"]);
let scene_changed = layout_changed || changed(&["animations", "appearance", "window_rules", "output_profiles"]);
if self.output_profiles != config.output_profiles {
crate::backends::direct::validate_live_outputs(self, &config.output_profiles)?;
}
@@ -676,7 +698,7 @@ impl Ferese {
|| self.input_settings.xkb_variant != config.input_settings.xkb_variant
|| self.input_settings.xkb_options != config.input_settings.xkb_options;
if let Some(keyboard) = self.seat.get_keyboard() {
if input_changed && let Some(keyboard) = self.seat.get_keyboard() {
if keyboard_changed {
self.input_capture.close_all();
if self.input_capture.restore_focus {
@@ -700,41 +722,51 @@ impl Ferese {
keyboard.change_repeat_info(config.input_settings.repeat_rate, config.input_settings.repeat_delay_ms);
}
self.lock_idle = config.lock_idle;
self.refresh_lock_idle_policy();
self.advance_animations(Instant::now());
if lock_changed {
self.lock_idle = config.lock_idle;
self.refresh_lock_idle_policy();
}
if scene_changed {
self.advance_animations(Instant::now());
}
let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad;
let outputs_changed = self.output_profiles != config.output_profiles;
let bounds = self
.workspaces
.iter()
.map(|workspace| {
let output = self.output_workspaces.output_for_workspace(workspace.id);
let rect = self
.output_ids
.iter()
.find(|(_, id)| Some(**id) == output)
.and_then(|(output, _)| self.output_bounds_for(output))
.or_else(|| self.output_bounds())
.unwrap_or(Rect::new(0., 0., 1920., 1080.));
(workspace.id, rect)
})
.collect();
if layout_changed {
let bounds = self
.workspaces
.iter()
.map(|workspace| {
let output = self.output_workspaces.output_for_workspace(workspace.id);
let rect = self
.output_ids
.iter()
.find(|(_, id)| Some(**id) == output)
.and_then(|(output, _)| self.output_bounds_for(output))
.or_else(|| self.output_bounds())
.unwrap_or(Rect::new(0., 0., 1920., 1080.));
(workspace.id, rect)
})
.collect();
self.workspaces
.reconfigure_live(
config.layout_mode,
config.default_column_width,
config.scrolling_focus_strategy,
&bounds,
)
.map_err(|error| error.to_string())?;
self.workspaces
.reconfigure_live(
config.layout_mode,
config.default_column_width,
config.scrolling_focus_strategy,
&bounds,
)
.map_err(|error| error.to_string())?;
}
self.gap_config = config.gap_config;
self.input_settings = config.input_settings;
self.bindings = config.bindings.into();
if bindings_changed {
self.bindings = config.bindings.into();
}
self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth;
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
if bindings_changed || input_changed {
self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings);
}
let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules);
self.theme_settings = config.theme_settings;
self.inactive_dim = config.inactive_dim;
@@ -749,7 +781,7 @@ impl Ferese {
self.viewport_spring_config = config.viewport_spring_config;
self.output_profiles = config.output_profiles;
self.autostart = config.autostart;
if let Some(runner) = self.daemons.clone() {
if daemons_changed && let Some(runner) = self.daemons.clone() {
crate::daemon::Runner::refresh(&runner, self);
}
@@ -766,10 +798,14 @@ impl Ferese {
}
self.overview.set_font_family(config.overview_font_family);
self.wallpaper.reload(config.wallpaper);
if self.wallpaper.configuration() != &config.wallpaper {
self.wallpaper.reload(config.wallpaper);
}
self.relayout();
crate::backends::direct::render_all(self);
if scene_changed {
self.relayout();
crate::backends::direct::render_all(self);
}
Ok(())
}
+2 -2
View File
@@ -309,10 +309,10 @@ impl Ferese {
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty() || !resume_pauses.is_empty();
for (workspace, viewport) in &mut self.viewport_animations {
if !self
if self
.output_workspaces
.output_for_workspace(*workspace)
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace))
.is_none_or(|output| self.output_workspaces.active_workspace(output) != Some(*workspace))
{
continue;
}
+1 -1
View File
@@ -11,7 +11,7 @@ impl Ferese {
&& self.window_content_ready(window)
&& self.windows.geometry(id).is_some()
&& self.output_workspaces.focused_output().is_some()
&& !self.windows.record(*id).is_some_and(|record| record.closing.is_some()))
&& self.windows.record(*id).is_none_or(|record| record.closing.is_none()))
.then_some(*id)
})
.collect()
+5 -1
View File
@@ -17,6 +17,10 @@ impl Ferese {
.map(|(_, surface, origin)| (surface, origin))
.or_else(|| self.window_surface_under(position))
.or_else(|| {
if self.overview.is_active() {
return None;
}
self.layer_surface_under(position, &[Layer::Bottom, Layer::Background])
.map(|(_, surface, origin)| (surface, origin))
})
@@ -32,7 +36,7 @@ impl Ferese {
if let Some(layer) = self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) {
return Some(layer);
}
if self.window_surface_under(position).is_some() {
if self.overview.is_active() || self.window_surface_under(position).is_some() {
return None;
}
+180 -9
View File
@@ -235,15 +235,7 @@ impl Ferese {
}
pub(crate) fn focus_output_at(&mut self, position: Point<f64, Logical>) {
let output = self.space.outputs().find_map(|output| {
let geometry = self.space.output_geometry(output)?;
(position.x >= f64::from(geometry.loc.x)
&& position.y >= f64::from(geometry.loc.y)
&& position.x < f64::from(geometry.loc.x + geometry.size.w)
&& position.y < f64::from(geometry.loc.y + geometry.size.h))
.then(|| output.clone())
});
let Some(output) = output else {
let Some(output) = self.space.output_under(position).next().cloned() else {
return;
};
let Some(output_id) = self.output_ids.get(&output).copied() else {
@@ -260,3 +252,182 @@ impl Ferese {
self.relayout();
}
}
#[cfg(test)]
mod tests {
use super::*;
use smithay::backend::input::{ButtonState, Event, InputBackend, InputEvent, PointerButtonEvent, UnusedEvent};
use smithay::backend::winit::WinitVirtualDevice;
use smithay::input::pointer::MotionEvent;
use smithay::utils::SERIAL_COUNTER;
#[derive(Debug)]
struct TestInput;
impl InputBackend for TestInput {
type Device = WinitVirtualDevice;
type PointerButtonEvent = TestButton;
type KeyboardKeyEvent = UnusedEvent;
type PointerAxisEvent = UnusedEvent;
type PointerMotionEvent = UnusedEvent;
type PointerMotionAbsoluteEvent = UnusedEvent;
type GestureSwipeBeginEvent = UnusedEvent;
type GestureSwipeUpdateEvent = UnusedEvent;
type GestureSwipeEndEvent = UnusedEvent;
type GesturePinchBeginEvent = UnusedEvent;
type GesturePinchUpdateEvent = UnusedEvent;
type GesturePinchEndEvent = UnusedEvent;
type GestureHoldBeginEvent = UnusedEvent;
type GestureHoldEndEvent = UnusedEvent;
type TouchDownEvent = UnusedEvent;
type TouchUpEvent = UnusedEvent;
type TouchMotionEvent = UnusedEvent;
type TouchCancelEvent = UnusedEvent;
type TouchFrameEvent = UnusedEvent;
type TabletToolAxisEvent = UnusedEvent;
type TabletToolProximityEvent = UnusedEvent;
type TabletToolTipEvent = UnusedEvent;
type TabletToolButtonEvent = UnusedEvent;
type SwitchToggleEvent = UnusedEvent;
type SpecialEvent = UnusedEvent;
}
#[derive(Debug)]
struct TestButton(ButtonState);
impl Event<TestInput> for TestButton {
fn time(&self) -> u64 {
1_000
}
fn device(&self) -> WinitVirtualDevice {
WinitVirtualDevice
}
}
impl PointerButtonEvent<TestInput> for TestButton {
fn button_code(&self) -> u32 {
0x110
}
fn state(&self) -> ButtonState {
self.0
}
}
fn click(state: &mut Ferese, position: Point<f64, Logical>) {
let pointer = state.seat.get_pointer().unwrap();
pointer.motion(
state,
state.surface_under(position),
&MotionEvent {
location: position,
serial: SERIAL_COUNTER.next_serial(),
time: 1,
},
);
pointer.frame(state);
for button in [ButtonState::Pressed, ButtonState::Released] {
state.process_input_event::<TestInput>(InputEvent::PointerButton {
event: TestButton(button),
});
}
}
#[test]
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
fn overlapping_outputs_agree_on_pointer_focus_and_overview_selection() {
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
assert!(runtime.starts_with(std::env::temp_dir()));
let mut event_loop = EventLoop::try_new().unwrap();
let config = crate::config::Config::default().runtime_config().unwrap();
let mut state = Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap();
let mut outputs = Vec::new();
for name in ["first", "second"] {
let output = Output::new(
name.into(),
smithay::output::PhysicalProperties {
size: (0, 0).into(),
subpixel: smithay::output::Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(smithay::output::Mode {
size: (1920, 1080).into(),
refresh: 60_000,
}),
Some(smithay::utils::Transform::Normal),
Some(smithay::output::Scale::Fractional(1.75)),
Some((0, 0).into()),
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, name.into());
outputs.push(output);
}
let position = Point::from((300.0, 300.0));
let expected_output = state.space.output_under(position).next().unwrap().clone();
assert_eq!(expected_output, outputs[1]);
let expected_workspace = state.workspace_under_pointer(position).unwrap();
state.focus_output_at(position);
assert_eq!(state.workspaces.active_id(), expected_workspace);
assert_eq!(
state.output_workspaces.focused_output(),
state.output_id(&expected_output)
);
state
.workspaces
.insert_window(WindowId(1), Axis::Horizontal, 0.5)
.unwrap();
state.relayout();
state.set_overview_active(true);
let card = state
.overview_workspace_cards(&expected_output)
.into_iter()
.find(|card| card.workspace != expected_workspace)
.expect("occupied workspace has an empty successor");
let position = Point::from((
card.rect.x + card.rect.width / 2.0,
card.rect.y + card.rect.height / 2.0,
));
assert_eq!(state.overview_strip_at(position), Some(expected_output.clone()));
click(&mut state, position);
assert_eq!(state.workspaces.active_id(), card.workspace);
state.windows.records.insert(
WindowId(1),
super::super::window_registry::WindowRecord {
geometry: Some(WindowGeometry::new(Rect::new(0.0, 0.0, 800.0, 600.0), None)),
..Default::default()
},
);
let occupied = state
.overview_workspace_cards(&expected_output)
.into_iter()
.find(|card| card.workspace == expected_workspace)
.unwrap();
let preview = occupied.windows[0].1;
let image_point = Point::from((preview.x + preview.width / 2.0, preview.y + preview.height / 2.0));
let label_point = Point::from((
occupied.rect.x + occupied.rect.width / 2.0,
occupied.rect.y + occupied.rect.height - 8.0,
));
for position in [image_point, label_point] {
state.activate_managed_workspace(card.workspace);
click(&mut state, position);
assert_eq!(state.workspaces.active_id(), expected_workspace);
assert!(state.overview.is_active());
}
assert_eq!(
state.output_workspaces.focused_output(),
state.output_id(&expected_output)
);
}
}
+1 -1
View File
@@ -664,7 +664,7 @@ impl Ferese {
}
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
if !self.windows.window(id).is_some() {
if self.windows.window(id).is_none() {
return false;
}
+15 -44
View File
@@ -47,7 +47,14 @@ pub(crate) fn read_source(path: &Path) -> Result<String, String> {
pub(crate) fn prepare(source: &str, directory: &Path) -> Result<(Config, RuntimeConfig, Candidate), String> {
let document = Document::parse(source).map_err(|e| e.to_string())?;
let candidate = resolve_with_context(&document, directory, Timestamp::now(), &auto_context(), read_source)?;
prepare_document(&document, directory)
}
pub(crate) fn prepare_document(
document: &Document,
directory: &Path,
) -> Result<(Config, RuntimeConfig, Candidate), String> {
let candidate = resolve_with_context(document, directory, Timestamp::now(), &auto_context(), read_source)?;
let config: Config =
serde_json::from_value(document.with_theme(&candidate.theme).value().clone()).map_err(|e| e.to_string())?;
let runtime = config.runtime_config().map_err(|e| e.to_string())?;
@@ -131,6 +138,10 @@ pub(crate) struct Engine {
}
impl Engine {
pub(crate) fn matches_files(&self, files: &[PathBuf]) -> bool {
self.active_files == files
}
pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: mpsc::SyncSender<Response>) {
if since != self.snapshot.revision {
let _ = response.try_send(Response::success(id, self.value()));
@@ -163,7 +174,7 @@ impl Engine {
}
}
fn watch_files(&mut self, files: &[PathBuf]) {
pub(crate) fn watch_files(&mut self, files: &[PathBuf]) {
let mut targets = HashSet::from([PathBuf::from("/etc/localtime")]);
if let Some(path) = crate::config::config_path() {
targets.insert(path);
@@ -213,7 +224,7 @@ impl Engine {
self.watched_directories = directories;
}
fn arm_clock(&self, next: Option<Timestamp>) -> io::Result<()> {
pub(crate) fn arm_clock(&self, next: Option<Timestamp>) -> io::Result<()> {
let Some(fd) = &self.clock else { return Ok(()) };
let mut timer: libc::itimerspec = unsafe { std::mem::zeroed() };
if let Some(next) = next {
@@ -342,47 +353,7 @@ pub(crate) fn init(
impl Ferese {
pub(crate) fn refresh_theme(&mut self) {
let result = crate::config::config_path()
.map_or_else(
|| {
self.config_source
.clone()
.ok_or_else(|| "No configuration available".into())
},
|path| {
if path.exists() {
read_source(&path)
} else {
Ok(String::new())
}
},
)
.and_then(|source| {
if let Ok(document) = Document::parse(&source)
&& let Some(value) = document.get("theme")
&& let Ok(policy) = serde_json::from_value::<ferese_config::theme::Policy>(value.clone())
{
let directory = crate::config::config_path()
.and_then(|path| path.parent().map(ToOwned::to_owned))
.unwrap_or_else(|| PathBuf::from("."));
let files: Vec<_> = [&policy.file, &policy.light.file, &policy.dark.file]
.into_iter()
.flatten()
.filter(|path| !path.as_os_str().is_empty())
.map(|path| ferese_config::theme::theme_path(&directory, path))
.collect();
let mut files = files;
files.extend(
policy
.custom_themes
.values()
.map(|theme| ferese_config::theme::theme_path(&directory, &theme.file)),
);
self.theme_engine.watch_files(&files);
}
self.reload_config_source(source)
});
if let Err(error) = result {
if let Err(error) = self.queue_config_reload(true, None) {
self.theme_engine.reject(error);
}
}