use crate::{ShellError, WindowId};
use super::ffi::{self, value};
const HOLD: u32 = value::ES_CONTINUOUS | value::ES_DISPLAY_REQUIRED | value::ES_SYSTEM_REQUIRED;
const RELEASE: u32 = value::ES_CONTINUOUS;
#[derive(Debug, Default)]
pub(super) struct IdleInhibit {
windows: Vec<WindowId>,
held: bool,
}
impl IdleInhibit {
pub(super) fn set(&mut self, window: WindowId, inhibit: bool) -> Result<(), ShellError> {
let asking = self.windows.contains(&window);
if asking == inhibit {
return Ok(());
}
if inhibit {
self.windows.push(window);
} else {
self.windows.retain(|&other| other != window);
}
self.sync().inspect_err(|_| {
if inhibit {
self.windows.retain(|&other| other != window);
} else {
self.windows.push(window);
}
})
}
pub(super) fn forget(&mut self, window: WindowId) -> Result<(), ShellError> {
self.windows.retain(|&other| other != window);
self.sync()
}
fn sync(&mut self) -> Result<(), ShellError> {
let want = !self.windows.is_empty();
if want != self.held {
set_thread_state(if want { HOLD } else { RELEASE })?;
self.held = want;
}
Ok(())
}
}
impl Drop for IdleInhibit {
fn drop(&mut self) {
self.windows.clear();
if let Err(error) = self.sync() {
crcbl_core::log::warn!("releasing the display on dropping the shell: {error}");
}
}
}
fn set_thread_state(flags: u32) -> Result<u32, ShellError> {
let previous = unsafe { ffi::SetThreadExecutionState(flags) };
if previous == 0 {
return Err(ShellError::Backend(format!(
"SetThreadExecutionState refused {flags:#010x}"
)));
}
Ok(previous)
}
#[cfg(test)]
mod tests {
use super::*;
use crcbl_core::Pool;
fn held_now() -> u32 {
let previous = set_thread_state(RELEASE).expect("reading the state");
set_thread_state(previous).expect("putting the state back");
previous & (value::ES_DISPLAY_REQUIRED | value::ES_SYSTEM_REQUIRED)
}
const BOTH: u32 = value::ES_DISPLAY_REQUIRED | value::ES_SYSTEM_REQUIRED;
fn on_fresh_thread(test: impl FnOnce() + Send + 'static) {
std::thread::spawn(test).join().expect("the test thread");
}
fn two_windows() -> (WindowId, WindowId) {
let mut pool = Pool::new();
(pool.insert(()).cast(), pool.insert(()).cast())
}
#[test]
fn the_display_is_held_while_any_window_asks() {
on_fresh_thread(|| {
let (a, b) = two_windows();
let mut idle = IdleInhibit::default();
assert_eq!(held_now(), 0, "a fresh thread holds nothing");
idle.set(a, true).expect("a asks");
assert_eq!(held_now(), BOTH);
idle.set(a, true).expect("asking twice is asking once");
idle.set(b, true).expect("b asks");
idle.set(a, false).expect("a stops");
assert_eq!(held_now(), BOTH, "b is still asking");
idle.set(a, false).expect("stopping twice is stopping once");
idle.set(b, false).expect("b stops");
assert_eq!(held_now(), 0, "nobody asks");
});
}
#[test]
fn destroying_the_last_window_asking_releases_the_display() {
on_fresh_thread(|| {
let (a, b) = two_windows();
let mut idle = IdleInhibit::default();
idle.set(a, true).expect("a asks");
idle.forget(b).expect("b never asked");
assert_eq!(held_now(), BOTH);
idle.forget(a).expect("a is destroyed");
assert_eq!(held_now(), 0);
idle.set(b, true).expect("b asks");
assert_eq!(held_now(), BOTH);
idle.set(b, false).expect("b stops");
});
}
#[test]
fn dropping_the_shell_releases_the_display() {
on_fresh_thread(|| {
let (a, _) = two_windows();
let mut idle = IdleInhibit::default();
idle.set(a, true).expect("a asks");
assert_eq!(held_now(), BOTH);
drop(idle);
assert_eq!(held_now(), 0);
});
}
#[test]
fn the_state_is_the_threads_own() {
on_fresh_thread(|| {
let (a, _) = two_windows();
let mut idle = IdleInhibit::default();
idle.set(a, true).expect("a asks");
on_fresh_thread(|| assert_eq!(held_now(), 0, "another thread holds nothing"));
assert_eq!(held_now(), BOTH);
});
}
}