use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::thread::{self, JoinHandle};
use tracing::{debug, error, trace};
use windows::Win32::Foundation::POINT;
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use crate::ecs::PhysicalPoint;
const POLL_INTERVAL: std::time::Duration = std::time::Duration::from_millis(12);
#[inline]
fn pack(x: i32, y: i32) -> i64 {
((x as i64) << 32) | (y as u32 as i64)
}
#[inline]
fn unpack(v: i64) -> (i32, i32) {
let x = (v >> 32) as i32;
let y = (v & 0xFFFF_FFFF) as u32 as i32;
(x, y)
}
pub(crate) struct CursorMonitorBridge {
cursor_event: Arc<event_listener::Event>,
latest_pos: Arc<AtomicI64>,
stop_flag: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl CursorMonitorBridge {
pub(crate) fn spawn(cursor_event: Arc<event_listener::Event>) -> Self {
let latest_pos = Arc::new(AtomicI64::new(0));
let stop_flag = Arc::new(AtomicBool::new(false));
let thread_event = Arc::clone(&cursor_event);
let thread_latest = Arc::clone(&latest_pos);
let thread_stop = Arc::clone(&stop_flag);
let handle = thread::Builder::new()
.name("wintf-cursor-monitor".to_owned())
.spawn(move || cursor_loop(thread_event, thread_latest, thread_stop))
.expect("failed to spawn wintf-cursor-monitor thread");
debug!("CursorMonitorBridge started (wintf-cursor-monitor thread spawned)");
Self {
cursor_event,
latest_pos,
stop_flag,
handle: Some(handle),
}
}
pub(crate) fn latest_cursor(&self) -> PhysicalPoint {
let (x, y) = unpack(self.latest_pos.load(Ordering::Acquire));
PhysicalPoint::new(x, y)
}
pub(crate) fn event(&self) -> &Arc<event_listener::Event> {
&self.cursor_event
}
fn stop(&mut self) {
if let Some(handle) = self.handle.take() {
self.stop_flag.store(true, Ordering::Release);
if handle.join().is_err() {
trace!("wintf-cursor-monitor thread join returned Err (thread panicked)");
}
debug!("CursorMonitorBridge stopped (wintf-cursor-monitor thread joined)");
}
}
}
impl Drop for CursorMonitorBridge {
fn drop(&mut self) {
self.stop();
}
}
fn cursor_loop(event: Arc<event_listener::Event>, latest: Arc<AtomicI64>, stop: Arc<AtomicBool>) {
if let Err(e) = crate::ecs::world::thread_registry::register_current_thread(
crate::ecs::world::thread_registry::ROLE_CURSOR_MONITOR,
) {
error!(error = %e, "[cursor_loop] スレッド名簿への登録に失敗した(カーソル監視は継続)");
}
let mut prev: Option<(i32, i32)> = None;
while !stop.load(Ordering::Acquire) {
let mut pt = POINT::default();
let got = unsafe { GetCursorPos(&mut pt) };
match got {
Ok(()) => {
let cur = (pt.x, pt.y);
if prev != Some(cur) {
latest.store(pack(cur.0, cur.1), Ordering::Release);
event.notify(usize::MAX);
prev = Some(cur);
}
}
Err(_) => {
trace!("GetCursorPos failed; skipping this iteration");
}
}
thread::sleep(POLL_INTERVAL);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::world::thread_registry::{self, ROLE_CURSOR_MONITOR, ThreadEntry};
use event_listener::{Event, Listener};
use std::time::Duration;
use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
fn wait_for_role(role: &str, limit: Duration) -> Option<ThreadEntry> {
let deadline = std::time::Instant::now() + limit;
loop {
let found = thread_registry::snapshot()
.into_iter()
.find(|entry| entry.role == role);
if found.is_some() {
return found;
}
if std::time::Instant::now() >= deadline {
return None;
}
thread::sleep(Duration::from_millis(5));
}
}
#[test]
fn cursor_monitor_thread_registers_itself_with_the_cursor_monitor_role() {
let bridge = CursorMonitorBridge::spawn(Arc::new(Event::new()));
let found = wait_for_role(ROLE_CURSOR_MONITOR, Duration::from_secs(5))
.expect("wintf-cursor-monitor スレッドは走り始めに名簿へ載るはず");
assert_eq!(
found.name.as_deref(),
Some("wintf-cursor-monitor"),
"名簿の OS 名は生成時に付けたスレッド名であるはず"
);
assert!(
thread_registry::is_known_role(ROLE_CURSOR_MONITOR),
"登録した役割名は固定語彙に含まれるはず"
);
drop(bridge);
}
#[test]
fn pack_unpack_roundtrip() {
for (x, y) in [
(0, 0),
(100, 200),
(-1, -1),
(-1920, 1080),
(i32::MIN, i32::MAX),
] {
assert_eq!(
unpack(pack(x, y)),
(x, y),
"pack/unpack roundtrip for ({x},{y})"
);
}
}
#[test]
fn spawn_then_joins_on_drop() {
let bridge = CursorMonitorBridge::spawn(Arc::new(Event::new()));
drop(bridge);
}
#[test]
fn latest_cursor_reads_stored_position() {
let bridge = CursorMonitorBridge::spawn(Arc::new(Event::new()));
thread::sleep(Duration::from_millis(60));
let raw = bridge.latest_pos.load(Ordering::Acquire);
let (ex, ey) = unpack(raw);
let p = bridge.latest_cursor();
assert_eq!(
(p.x, p.y),
(ex, ey),
"latest_cursor は latest_pos の unpack と一致"
);
drop(bridge);
}
#[test]
fn notify_on_move_and_ui_reads_latest() {
let event = Arc::new(Event::new());
let bridge = CursorMonitorBridge::spawn(Arc::clone(&event));
let listener = event.listen();
let target = (137, 149);
let moved = unsafe { SetCursorPos(target.0, target.1) };
if moved.is_err() {
trace!("SetCursorPos rejected (headless/non-interactive); asserting RAII only");
drop(bridge);
return;
}
let got = listener.wait_timeout(Duration::from_millis(500));
if got.is_some() {
let p = bridge.latest_cursor();
assert_ne!(
(p.x, p.y),
(0, 0),
"移動 notify 後は latest_cursor が更新済みであるべき"
);
} else {
trace!("no move notify observed within timeout (environment-dependent)");
}
drop(bridge);
}
}