use core::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::path::PathBuf;
use crate::AspectRatio;
use super::events::{RawEvent, enqueue};
use super::ffi::{Handle, Rect};
use super::geometry::{Frame, Track};
#[cfg(target_os = "windows")]
use super::dnd;
#[cfg(target_os = "windows")]
use super::ffi::{self, CreateStructW, Lparam, Lresult, MinMaxInfo, Point, Wparam, msg, value};
#[cfg(target_os = "windows")]
use super::geometry;
#[cfg(target_os = "windows")]
use super::input::{self, RawReport};
#[cfg(target_os = "windows")]
use super::{keys, pointer};
#[cfg(target_os = "windows")]
use crate::PhysicalSize;
#[cfg(target_os = "windows")]
use core::ptr;
#[cfg(target_os = "windows")]
use crcbl_core::input::ButtonState;
pub(super) fn key(hwnd: Handle) -> isize {
hwnd as isize
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Limits {
pub hwnd: isize,
pub track: Track,
pub aspect: Option<AspectRatio>,
pub frame: Frame,
}
#[derive(Clone, Copy, Debug)]
pub(super) struct Shape {
pub hwnd: isize,
pub cursor: Handle,
}
#[derive(Clone, Debug)]
pub(super) struct FileDrop {
pub hwnd: isize,
pub paths: Vec<PathBuf>,
pub x: i32,
pub y: i32,
}
#[derive(Clone, Debug)]
pub(super) struct Preedit {
pub hwnd: isize,
pub units: Vec<u16>,
pub cursor: Option<usize>,
}
#[derive(Debug, Default)]
pub(super) struct Shared {
events: RefCell<Vec<RawEvent>>,
limits: RefCell<Vec<Limits>>,
shapes: RefCell<Vec<Shape>>,
drops: RefCell<VecDeque<FileDrop>>,
preedits: RefCell<VecDeque<Preedit>>,
input_areas: RefCell<Vec<(isize, Rect)>>,
passthrough: RefCell<Vec<isize>>,
appbars: RefCell<Vec<isize>>,
tracked: Cell<isize>,
capture: Cell<(isize, u32)>,
clipped: Cell<isize>,
keyboard: Cell<isize>,
focused: Cell<isize>,
#[cfg(all(target_os = "windows", feature = "accesskit"))]
access: super::access::Adapters,
}
impl Shared {
fn push(&self, event: RawEvent) {
enqueue(&mut self.events.borrow_mut(), event);
}
#[cfg(target_os = "windows")]
pub(super) fn has_events(&self) -> bool {
!self.events.borrow().is_empty()
}
pub(super) fn take_events(&self) -> Vec<RawEvent> {
core::mem::take(&mut self.events.borrow_mut())
}
fn push_drop(&self, drop: FileDrop) {
self.drops.borrow_mut().push_back(drop);
}
pub(super) fn take_drop(&self, hwnd: isize) -> Option<FileDrop> {
let mut drops = self.drops.borrow_mut();
let index = drops.iter().position(|drop| drop.hwnd == hwnd)?;
drops.remove(index)
}
pub(super) fn set_limits(&self, limits: Limits) {
let mut table = self.limits.borrow_mut();
match table.iter_mut().find(|known| known.hwnd == limits.hwnd) {
Some(known) => *known = limits,
None => table.push(limits),
}
}
pub(super) fn forget(&self, hwnd: isize) {
self.limits.borrow_mut().retain(|known| known.hwnd != hwnd);
self.shapes.borrow_mut().retain(|known| known.hwnd != hwnd);
self.drops.borrow_mut().retain(|drop| drop.hwnd != hwnd);
self.preedits
.borrow_mut()
.retain(|preedit| preedit.hwnd != hwnd);
self.input_areas
.borrow_mut()
.retain(|&(known, _)| known != hwnd);
self.passthrough.borrow_mut().retain(|&known| known != hwnd);
self.appbars.borrow_mut().retain(|&known| known != hwnd);
if self.tracked.get() == hwnd {
self.tracked.set(0);
}
if self.capture.get().0 == hwnd {
self.capture.set((0, 0));
}
if self.clipped.get() == hwnd {
self.clipped.set(0);
}
if self.keyboard.get() == hwnd {
self.keyboard.set(0);
}
if self.focused.get() == hwnd {
self.focused.set(0);
}
#[cfg(all(target_os = "windows", feature = "accesskit"))]
self.access.close(hwnd);
}
#[cfg(all(target_os = "windows", feature = "accesskit"))]
pub(super) fn access(&self) -> &super::access::Adapters {
&self.access
}
pub(super) fn set_passthrough(&self, hwnd: isize) {
self.passthrough.borrow_mut().push(hwnd);
}
#[cfg(any(target_os = "windows", test))]
pub(super) fn set_appbar(&self, hwnd: isize, registered: bool) {
let mut appbars = self.appbars.borrow_mut();
appbars.retain(|&known| known != hwnd);
if registered {
appbars.push(hwnd);
}
}
#[cfg(any(target_os = "windows", test))]
fn is_appbar(&self, hwnd: isize) -> bool {
self.appbars.borrow().contains(&hwnd)
}
fn limits_for(&self, hwnd: isize) -> Option<Limits> {
self.limits
.borrow()
.iter()
.find(|known| known.hwnd == hwnd)
.copied()
}
pub(super) fn set_shape(&self, shape: Shape) {
let mut table = self.shapes.borrow_mut();
match table.iter_mut().find(|known| known.hwnd == shape.hwnd) {
Some(known) => *known = shape,
None => table.push(shape),
}
}
fn push_preedit(&self, preedit: Preedit) {
self.preedits.borrow_mut().push_back(preedit);
}
pub(super) fn take_preedit(&self, hwnd: isize) -> Option<Preedit> {
let mut preedits = self.preedits.borrow_mut();
let index = preedits.iter().position(|preedit| preedit.hwnd == hwnd)?;
preedits.remove(index)
}
pub(super) fn set_input_area(&self, hwnd: isize, area: Option<Rect>) {
let mut table = self.input_areas.borrow_mut();
table.retain(|&(known, _)| known != hwnd);
if let Some(area) = area {
table.push((hwnd, area));
}
}
fn input_area_for(&self, hwnd: isize) -> Option<Rect> {
self.input_areas
.borrow()
.iter()
.find(|&&(known, _)| known == hwnd)
.map(|&(_, area)| area)
}
fn shape_for(&self, hwnd: isize) -> Option<Handle> {
self.shapes
.borrow()
.iter()
.find(|known| known.hwnd == hwnd)
.map(|known| known.cursor)
}
fn enter(&self, hwnd: isize, x: i32, y: i32, millis: u32) -> bool {
let left = self.tracked.replace(hwnd);
if left == hwnd {
return false;
}
if left != 0 {
self.push(RawEvent::PointerFocus {
hwnd: left,
entered: false,
x: 0,
y: 0,
millis,
});
}
self.push(RawEvent::PointerFocus {
hwnd,
entered: true,
x,
y,
millis,
});
true
}
fn leave(&self, hwnd: isize) -> bool {
if self.tracked.get() != hwnd {
return false;
}
self.tracked.set(0);
true
}
fn press(&self, hwnd: isize, bit: u32) -> bool {
let (holder, held) = self.capture.get();
if holder != hwnd {
self.capture.set((hwnd, bit));
return true;
}
self.capture.set((hwnd, held | bit));
held == 0
}
fn release(&self, hwnd: isize, bit: u32) -> bool {
let (holder, held) = self.capture.get();
if holder != hwnd {
return false;
}
let held = held & !bit;
if held == 0 {
self.capture.set((0, 0));
return true;
}
self.capture.set((hwnd, held));
false
}
fn capture_lost(&self) {
self.capture.set((0, 0));
}
pub(super) fn set_clipped(&self, hwnd: isize) {
self.clipped.set(hwnd);
}
pub(super) fn clipped(&self) -> isize {
self.clipped.get()
}
pub(super) fn sync_focus(&self, foreground: isize) -> (Option<isize>, Option<isize>) {
let (now, lost, gained) = focus_change(self.keyboard.get(), foreground, self.focused.get());
self.focused.set(now);
for (hwnd, focused) in [(lost, false), (gained, true)] {
let Some(hwnd) = hwnd else { continue };
self.push(RawEvent::Focus { hwnd, focused });
#[cfg(all(target_os = "windows", feature = "accesskit"))]
self.access.focus(hwnd, focused);
}
(lost, gained)
}
}
const fn focus_change(
keyboard: isize,
foreground: isize,
reported: isize,
) -> (isize, Option<isize>, Option<isize>) {
let now = if keyboard != 0 && keyboard == foreground {
keyboard
} else {
0
};
if now == reported {
return (now, None, None);
}
let lost = if reported == 0 { None } else { Some(reported) };
let gained = if now == 0 { None } else { Some(now) };
(now, lost, gained)
}
#[cfg(target_os = "windows")]
pub(super) fn foreground() -> isize {
key(unsafe { ffi::GetForegroundWindow() })
}
const fn low_word(value: usize) -> u32 {
(value & 0xFFFF) as u32
}
const fn high_word(value: usize) -> u32 {
((value >> 16) & 0xFFFF) as u32
}
#[cfg(target_os = "windows")]
pub(super) unsafe extern "system" fn window_proc(
hwnd: Handle,
message: u32,
w_param: Wparam,
l_param: Lparam,
) -> Lresult {
let default = || unsafe { ffi::DefWindowProcW(hwnd, message, w_param, l_param) };
if message == msg::NC_CREATE {
let shared = unsafe { (*(l_param as *const CreateStructW)).lp_create_params };
unsafe { ffi::SetWindowLongPtrW(hwnd, value::GWLP_USERDATA, shared as isize) };
return default();
}
let shared = unsafe { ffi::GetWindowLongPtrW(hwnd, value::GWLP_USERDATA) } as *const Shared;
let Some(shared) = (unsafe { shared.as_ref() }) else {
return default();
};
let window = key(hwnd);
let millis = unsafe { ffi::GetMessageTime() } as u32;
if let Some(notice) = super::panel::owed_to_shell(message)
&& shared.is_appbar(window)
{
super::panel::tell_shell(hwnd, notice);
}
match message {
msg::SIZE => {
if w_param == value::SIZE_MINIMIZED {
shared.push(RawEvent::Minimized { hwnd: window });
if shared.clipped() == window {
input::release_clip();
}
} else {
shared.push(RawEvent::Resized {
hwnd: window,
size: PhysicalSize::new(
low_word(l_param as usize),
high_word(l_param as usize),
),
});
input::reclip(shared, hwnd, window);
}
0
}
msg::MOVE => {
input::reclip(shared, hwnd, window);
0
}
msg::EXIT_SIZE_MOVE => {
input::reclip(shared, hwnd, window);
default()
}
msg::CLOSE => {
shared.push(RawEvent::CloseRequested { hwnd: window });
0
}
msg::SET_FOCUS | msg::KILL_FOCUS | msg::ACTIVATE | msg::ACTIVATE_APP => {
match message {
msg::SET_FOCUS => shared.keyboard.set(window),
msg::KILL_FOCUS if shared.keyboard.get() == window => shared.keyboard.set(0),
_ => {}
}
let (lost, gained) = shared.sync_focus(foreground());
let clipped = shared.clipped();
if clipped != 0 && lost == Some(clipped) {
input::release_clip();
}
if clipped == window && gained == Some(window) {
input::apply_clip(hwnd);
}
if matches!(message, msg::ACTIVATE | msg::ACTIVATE_APP) {
return default();
}
0
}
#[cfg(feature = "accesskit")]
msg::GET_OBJECT => {
let answer = shared.access().get_object(window, w_param, l_param);
if answer.activated {
shared.push(RawEvent::AccessibilityActivated { hwnd: window });
}
answer.lresult.unwrap_or_else(default)
}
msg::KEY_DOWN | msg::KEY_UP | msg::SYS_KEY_DOWN | msg::SYS_KEY_UP => {
let pressed = message == msg::KEY_DOWN || message == msg::SYS_KEY_DOWN;
shared.push(RawEvent::Key {
hwnd: window,
scancode: keys::scancode(l_param),
virtual_key: low_word(w_param) as u16,
state: ButtonState::from_pressed(pressed),
repeat: pressed && keys::was_already_down(l_param),
modifiers: input::modifiers_now(),
millis,
});
if message == msg::SYS_KEY_DOWN || message == msg::SYS_KEY_UP {
default()
} else {
0
}
}
msg::SYS_COMMAND => {
if w_param & value::SC_COMMAND_MASK == value::SC_KEY_MENU && l_param == 0 {
return 0;
}
default()
}
msg::CHAR => {
shared.push(RawEvent::Char {
hwnd: window,
unit: low_word(w_param) as u16,
millis,
});
0
}
msg::IME_SET_CONTEXT => {
let l_param = keys::without_composition_window(l_param);
unsafe { ffi::DefWindowProcW(hwnd, message, w_param, l_param) }
}
msg::IME_START_COMPOSITION => {
if let Some(area) = shared.input_area_for(window) {
input::place_ime(hwnd, Some(area));
}
0
}
msg::IME_COMPOSITION => {
if keys::composition_committed(l_param) {
for unit in input::composition_result(hwnd) {
shared.push(RawEvent::Char {
hwnd: window,
unit,
millis,
});
}
}
if keys::composition_changed(l_param) {
let (units, cursor) = input::composition(hwnd);
shared.push_preedit(Preedit {
hwnd: window,
units,
cursor,
});
shared.push(RawEvent::Preedit {
hwnd: window,
millis,
});
}
0
}
msg::IME_END_COMPOSITION => {
shared.push_preedit(Preedit {
hwnd: window,
units: Vec::new(),
cursor: None,
});
shared.push(RawEvent::Preedit {
hwnd: window,
millis,
});
default()
}
msg::MOUSE_MOVE => {
let (x, y) = pointer::point(l_param);
if shared.enter(window, x, y, millis) {
crcbl_core::log::trace!("win32: pointer arrived in {window:#x} at ({x}, {y})");
input::track_leave(hwnd);
}
shared.push(RawEvent::PointerMotion {
hwnd: window,
x,
y,
millis,
});
0
}
msg::MOUSE_LEAVE => {
let current = shared.leave(window);
crcbl_core::log::trace!(
"win32: WM_MOUSELEAVE for {window:#x} ({})",
if current {
"reported"
} else {
"stale, dropped"
}
);
if current {
shared.push(RawEvent::PointerFocus {
hwnd: window,
entered: false,
x: 0,
y: 0,
millis,
});
}
0
}
msg::L_BUTTON_DOWN
| msg::L_BUTTON_UP
| msg::R_BUTTON_DOWN
| msg::R_BUTTON_UP
| msg::M_BUTTON_DOWN
| msg::M_BUTTON_UP
| msg::X_BUTTON_DOWN
| msg::X_BUTTON_UP => {
let Some((button, state)) = pointer::button(message, w_param) else {
return default();
};
let (x, y) = pointer::point(l_param);
let bit = pointer::button_bit(button);
if state.is_pressed() {
if shared.press(window, bit) {
unsafe { ffi::SetCapture(hwnd) };
}
} else if shared.release(window, bit) {
unsafe { ffi::ReleaseCapture() };
}
shared.push(RawEvent::Button {
hwnd: window,
button,
state,
x,
y,
modifiers: input::modifiers_now(),
millis,
});
if message == msg::X_BUTTON_DOWN || message == msg::X_BUTTON_UP {
1
} else {
0
}
}
msg::CAPTURE_CHANGED => {
if l_param != hwnd as Lparam {
shared.capture_lost();
}
default()
}
msg::MOUSE_WHEEL | msg::MOUSE_H_WHEEL => {
let (x, y) = pointer::point(l_param);
let mut position = Point { x, y };
unsafe { ffi::ScreenToClient(hwnd, &raw mut position) };
shared.push(RawEvent::Wheel {
hwnd: window,
horizontal: message == msg::MOUSE_H_WHEEL,
ticks: high_word(w_param) as u16 as i16,
x: position.x,
y: position.y,
modifiers: input::modifiers_now(),
millis,
});
0
}
msg::POINTER_DOWN
| msg::POINTER_UPDATE
| msg::POINTER_UP
| msg::POINTER_CAPTURE_CHANGED => {
let Some((pointer_id, phase)) = pointer::touch(message, w_param) else {
return default();
};
let position = if message == msg::POINTER_CAPTURE_CHANGED {
None
} else {
let mut kind = 0;
let known = unsafe { ffi::GetPointerType(pointer_id, &raw mut kind) } != 0;
if !known || kind != value::PT_TOUCH {
return default();
}
let (x, y) = pointer::point(l_param);
let mut position = Point { x, y };
unsafe { ffi::ScreenToClient(hwnd, &raw mut position) };
Some((position.x, position.y))
};
shared.push(RawEvent::Touch {
hwnd: window,
pointer_id,
phase,
position,
millis,
});
if position.is_some() && !pointer::is_primary(w_param) {
return 0;
}
default()
}
msg::INPUT => {
match unsafe { input::read_raw(l_param) } {
Some(RawReport::Mouse { device, mouse }) => shared.push(RawEvent::RawMotion {
hwnd: window,
device: device as isize,
flags: mouse.us_flags,
buttons: low_word(mouse.ul_buttons as usize) as u16,
x: mouse.l_last_x,
y: mouse.l_last_y,
millis,
}),
Some(RawReport::Keyboard { device, keyboard }) => {
shared.push(RawEvent::RawKey {
hwnd: window,
device: device as isize,
make_code: keyboard.make_code,
flags: keyboard.flags,
millis,
});
}
None => {}
}
default()
}
msg::INPUT_DEVICE_CHANGE => {
if w_param == value::GIDC_REMOVAL {
shared.push(RawEvent::DeviceRemoved { device: l_param });
}
default()
}
msg::SET_CURSOR => {
if low_word(l_param as usize) != value::HT_CLIENT {
return default();
}
let Some(cursor) = shared.shape_for(window) else {
return default();
};
unsafe { ffi::SetCursor(cursor) };
1
}
msg::DPI_CHANGED => {
let suggested = unsafe { *(l_param as *const Rect) };
unsafe {
ffi::SetWindowPos(
hwnd,
ptr::null_mut(),
suggested.left,
suggested.top,
suggested.width(),
suggested.height(),
value::SWP_NO_Z_ORDER | value::SWP_NO_ACTIVATE,
);
}
shared.push(RawEvent::DpiChanged {
hwnd: window,
dpi: low_word(w_param),
});
0
}
msg::GET_MIN_MAX_INFO => {
let Some(limits) = shared.limits_for(window) else {
return default();
};
let info = unsafe { &mut *(l_param as *mut MinMaxInfo) };
if let Some(min) = limits.track.min {
info.pt_min_track_size = min;
}
if let Some(max) = limits.track.max {
info.pt_max_track_size = max;
}
0
}
msg::SIZING => {
let Some(limits) = shared.limits_for(window) else {
return default();
};
let Some(aspect) = limits.aspect else {
return default();
};
let rect = unsafe { &mut *(l_param as *mut Rect) };
*rect = geometry::adjust_sizing_rect(*rect, w_param, aspect, limits.frame);
1
}
msg::DROP_FILES => {
let (paths, point) = unsafe { dnd::take_drop(w_param as Handle) };
shared.push_drop(FileDrop {
hwnd: window,
paths,
x: point.x,
y: point.y,
});
shared.push(RawEvent::FilesDropped {
hwnd: window,
millis,
});
0
}
msg::DESTROY => {
shared.push(RawEvent::Destroyed { hwnd: window });
0
}
msg::NC_DESTROY => {
if shared.clipped() == window {
input::release_clip();
}
shared.forget(window);
unsafe { ffi::SetWindowLongPtrW(hwnd, value::GWLP_USERDATA, 0) };
default()
}
msg::DISPLAY_CHANGE => {
shared.push(RawEvent::MonitorsChanged);
default()
}
msg::NC_HIT_TEST if shared.passthrough.borrow().contains(&window) => value::HT_TRANSPARENT,
msg::SETTING_CHANGE => {
if w_param == value::SPI_SET_WORK_AREA {
shared.push(RawEvent::MonitorsChanged);
}
default()
}
msg::APP_BAR => {
shared.push(RawEvent::AppBar {
hwnd: window,
notification: w_param as u32,
param: l_param,
});
0
}
_ => default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
const PANEL: isize = 0x10;
const OTHER: isize = 0x20;
const ELSEWHERE: isize = 0x99;
#[test]
fn thread_focus_without_the_foreground_is_not_the_keyboard() {
assert_eq!(focus_change(PANEL, ELSEWHERE, 0), (0, None, None));
assert_eq!(focus_change(PANEL, PANEL, 0), (PANEL, None, Some(PANEL)));
assert_eq!(focus_change(PANEL, PANEL, PANEL), (PANEL, None, None));
assert_eq!(
focus_change(PANEL, ELSEWHERE, PANEL),
(0, Some(PANEL), None)
);
assert_eq!(
focus_change(OTHER, OTHER, PANEL),
(OTHER, Some(PANEL), Some(OTHER))
);
assert_eq!(focus_change(0, 0, 0), (0, None, None));
}
#[test]
fn sync_focus_reports_each_change_once() {
let shared = Shared::default();
let focus = |hwnd, focused| RawEvent::Focus { hwnd, focused };
shared.keyboard.set(PANEL);
assert_eq!(shared.sync_focus(ELSEWHERE), (None, None));
assert!(shared.take_events().is_empty(), "refused: nothing to say");
assert_eq!(shared.sync_focus(PANEL), (None, Some(PANEL)));
assert_eq!(shared.sync_focus(PANEL), (None, None));
assert_eq!(shared.take_events(), [focus(PANEL, true)]);
shared.keyboard.set(OTHER);
assert_eq!(shared.sync_focus(OTHER), (Some(PANEL), Some(OTHER)));
assert_eq!(
shared.take_events(),
[focus(PANEL, false), focus(OTHER, true)]
);
shared.forget(OTHER);
assert_eq!(shared.sync_focus(OTHER), (None, None));
assert!(
shared.take_events().is_empty(),
"a dead window loses nothing"
);
}
#[test]
fn an_appbar_is_known_until_removed_or_forgotten() {
let shared = Shared::default();
shared.set_appbar(0x10, true);
shared.set_appbar(0x20, true);
shared.set_appbar(0x20, true);
assert!(shared.is_appbar(0x10) && shared.is_appbar(0x20));
shared.set_appbar(0x20, false);
assert!(!shared.is_appbar(0x20), "a removed appbar owes nothing");
shared.forget(0x10);
assert!(!shared.is_appbar(0x10), "nor does a dead one");
shared.set_appbar(0x10, true);
assert_eq!(*shared.appbars.borrow(), [0x10], "registered once each");
}
#[test]
fn a_passthrough_popup_is_forgotten_with_its_window() {
let shared = Shared::default();
shared.set_passthrough(0x10);
shared.set_passthrough(0x20);
assert!(shared.passthrough.borrow().contains(&0x10));
shared.forget(0x10);
assert_eq!(*shared.passthrough.borrow(), [0x20]);
}
use crate::{LogicalSize, SizeConstraints};
const FRAME: Frame = Frame {
width: 16,
height: 39,
};
fn limits(hwnd: isize) -> Limits {
Limits {
hwnd,
track: super::super::geometry::track_sizes(
SizeConstraints::min(LogicalSize::new(320.0, 240.0)),
1.0,
FRAME,
),
aspect: None,
frame: FRAME,
}
}
#[test]
fn a_window_is_keyed_by_its_handle_and_a_null_one_is_zero() {
assert_eq!(key(core::ptr::null_mut()), 0);
assert_eq!(key(0x1234 as Handle), 0x1234);
}
#[test]
fn a_words_halves_are_the_client_width_and_height() {
let packed = (720usize << 16) | 1280usize;
assert_eq!(low_word(packed), 1280);
assert_eq!(high_word(packed), 720);
assert_eq!(low_word(3840), 3840);
assert_eq!(high_word(0xFFFF_FFFF), 0xFFFF);
}
#[test]
fn limits_are_replaced_per_window_and_forgotten_on_the_way_out() {
let shared = Shared::default();
shared.set_limits(limits(1));
shared.set_limits(limits(2));
assert_eq!(shared.limits_for(1).map(|found| found.hwnd), Some(1));
let mut updated = limits(1);
updated.aspect = Some(AspectRatio::WIDESCREEN);
shared.set_limits(updated);
assert_eq!(shared.limits.borrow().len(), 2);
assert_eq!(
shared.limits_for(1).and_then(|found| found.aspect),
Some(AspectRatio::WIDESCREEN)
);
let found = shared.limits_for(2).expect("still there");
assert_eq!(found.frame, FRAME);
assert_eq!(
found.track.min,
Some(super::super::ffi::Point {
x: 320 + FRAME.width,
y: 240 + FRAME.height
})
);
shared.forget(1);
assert!(shared.limits_for(1).is_none());
assert!(shared.limits_for(2).is_some(), "one window, not the table");
}
#[test]
fn the_pointer_enters_once_per_arrival_and_a_stale_leave_is_ignored() {
let shared = Shared::default();
let focus = |hwnd, entered, x, y| RawEvent::PointerFocus {
hwnd,
entered,
x,
y,
millis: 0,
};
assert!(shared.enter(1, 5, 6, 0), "the first movement is an arrival");
assert!(
!shared.enter(1, 7, 8, 0),
"and the next two hundred are not"
);
assert!(!shared.enter(1, 9, 9, 0));
assert_eq!(
shared.take_events(),
[focus(1, true, 5, 6)],
"one entry, where it arrived, and no leave: it came from nowhere"
);
assert!(shared.enter(2, 3, 4, 0));
assert_eq!(
shared.take_events(),
[focus(1, false, 0, 0), focus(2, true, 3, 4)],
"the first window's leave, then the second's entry"
);
assert!(
!shared.leave(1),
"a leave for a window we stopped tracking is stale"
);
assert!(shared.leave(2));
assert!(!shared.leave(2), "and it only fires once");
assert!(shared.enter(2, 1, 1, 0));
assert_eq!(shared.take_events(), [focus(2, true, 1, 1)]);
shared.forget(2);
assert!(shared.enter(1, 2, 2, 0));
assert_eq!(shared.take_events(), [focus(1, true, 2, 2)]);
}
#[test]
fn the_capture_is_taken_on_the_first_button_and_given_back_on_the_last() {
let shared = Shared::default();
const LEFT: u32 = 1;
const RIGHT: u32 = 2;
assert!(shared.press(1, LEFT), "the first press takes it");
assert!(!shared.press(1, RIGHT), "the second does not take it twice");
assert!(!shared.release(1, LEFT), "one button is still down");
assert!(shared.release(1, RIGHT), "the last one gives it back");
assert!(!shared.release(1, RIGHT), "and not a second time");
assert!(!shared.release(1, LEFT));
assert!(shared.press(1, LEFT), "which must still work afterwards");
assert!(shared.press(2, RIGHT));
assert!(shared.release(2, RIGHT));
assert!(shared.press(1, LEFT));
shared.capture_lost();
assert!(
shared.press(1, RIGHT),
"the next press takes the capture again"
);
}
#[test]
fn a_cursor_shape_is_replaced_per_window_and_read_back_for_wm_setcursor() {
let shared = Shared::default();
let arrow = 0x1234 as Handle;
let beam = 0x5678 as Handle;
assert_eq!(shared.shape_for(1), None, "nothing asked for yet");
shared.set_shape(Shape {
hwnd: 1,
cursor: arrow,
});
shared.set_shape(Shape {
hwnd: 2,
cursor: beam,
});
assert_eq!(shared.shape_for(1), Some(arrow));
assert_eq!(shared.shape_for(2), Some(beam));
shared.set_shape(Shape {
hwnd: 1,
cursor: beam,
});
assert_eq!(shared.shape_for(1), Some(beam));
assert_eq!(shared.shapes.borrow().len(), 2);
}
#[test]
fn a_destroyed_window_leaves_nothing_behind_that_names_it() {
let shared = Shared::default();
shared.set_limits(limits(1));
shared.set_shape(Shape {
hwnd: 1,
cursor: 0x1234 as Handle,
});
assert!(shared.enter(1, 0, 0, 0));
assert!(shared.press(1, 1));
shared.set_clipped(1);
shared.forget(1);
assert!(shared.limits_for(1).is_none());
assert_eq!(shared.shape_for(1), None);
assert_eq!(shared.clipped(), 0);
assert!(
shared.enter(1, 0, 0, 0),
"the reused handle is a fresh arrival"
);
assert!(shared.press(1, 1), "and a fresh capture");
shared.set_clipped(2);
shared.set_shape(Shape {
hwnd: 2,
cursor: 0x5678 as Handle,
});
shared.forget(1);
assert_eq!(shared.clipped(), 2);
assert!(shared.shape_for(2).is_some());
}
#[test]
fn each_drop_marker_claims_its_own_payload_in_arrival_order() {
let shared = Shared::default();
let drop = |hwnd: isize, name: &str| FileDrop {
hwnd,
paths: vec![PathBuf::from(name)],
x: 4,
y: 5,
};
shared.push_drop(drop(1, "first"));
shared.push_drop(drop(2, "other window"));
shared.push_drop(drop(1, "second"));
let first = shared.take_drop(1).expect("queued");
assert_eq!(
first.paths,
[PathBuf::from("first")],
"oldest first, per window"
);
assert_eq!(
(first.x, first.y),
(4, 5),
"the drop point travels with its paths — `DroppedFile` carries both"
);
assert_eq!(
shared.take_drop(1).expect("queued").paths,
[PathBuf::from("second")]
);
assert!(shared.take_drop(1).is_none(), "and no third");
assert_eq!(
shared.take_drop(2).expect("untouched").paths,
[PathBuf::from("other window")],
"another window's drop was never in the running"
);
}
#[test]
fn a_drop_whose_window_died_before_the_pump_is_discarded_with_it() {
let shared = Shared::default();
shared.push_drop(FileDrop {
hwnd: 1,
paths: vec![PathBuf::from("doomed")],
x: 0,
y: 0,
});
shared.push_drop(FileDrop {
hwnd: 2,
paths: vec![PathBuf::from("survivor")],
x: 0,
y: 0,
});
shared.forget(1);
assert!(shared.take_drop(1).is_none());
assert_eq!(
shared.take_drop(2).expect("still there").paths,
[PathBuf::from("survivor")]
);
}
#[test]
fn a_composition_is_claimed_by_its_own_window_and_dies_with_it() {
let shared = Shared::default();
let preedit = |hwnd, text: &str| Preedit {
hwnd,
units: text.encode_utf16().collect(),
cursor: Some(1),
};
shared.push_preedit(preedit(1, "か"));
shared.push_preedit(preedit(2, "な"));
shared.push_preedit(preedit(1, "かな"));
let first = shared.take_preedit(1).expect("queued");
assert_eq!(String::from_utf16_lossy(&first.units), "か");
assert_eq!(first.cursor, Some(1));
shared.forget(1);
assert!(shared.take_preedit(1).is_none(), "went with its window");
assert_eq!(
String::from_utf16_lossy(&shared.take_preedit(2).expect("untouched").units),
"な"
);
}
#[test]
fn a_text_input_area_is_replaced_cleared_and_forgotten_per_window() {
let shared = Shared::default();
let area = |left| Rect {
left,
top: 10,
right: left + 5,
bottom: 30,
};
shared.set_input_area(1, Some(area(0)));
shared.set_input_area(2, Some(area(100)));
shared.set_input_area(1, Some(area(50)));
assert_eq!(shared.input_area_for(1).map(|area| area.left), Some(50));
shared.set_input_area(1, None);
assert!(
shared.input_area_for(1).is_none(),
"None hands placement back"
);
shared.forget(2);
assert!(shared.input_area_for(2).is_none(), "went with its window");
}
#[test]
fn taking_the_queue_empties_it_and_holds_no_borrow_afterwards() {
let shared = Shared::default();
shared.push(RawEvent::MonitorsChanged);
shared.push(RawEvent::CloseRequested { hwnd: 7 });
let taken = shared.take_events();
assert_eq!(taken.len(), 2);
assert!(shared.take_events().is_empty());
shared.push(RawEvent::Destroyed { hwnd: 7 });
assert_eq!(shared.take_events(), [RawEvent::Destroyed { hwnd: 7 }]);
}
}