use crate::backend::Backend;
use crate::event::{Point, Viewport};
use crate::primitives::image::ImageSource;
use crate::runner::AppLogic;
pub(crate) use crate::runtime::EventOutcome;
pub use crate::runner::StepOutcome;
use crate::win::backend::WinBackend;
use crate::{ButtonMask, Modifiers, MouseButton, UiEvent};
#[allow(dead_code)]
pub(crate) fn dispatch_event<A: AppLogic>(
event: UiEvent,
backend: &mut WinBackend,
app: &mut A,
) -> EventOutcome {
if matches!(event, UiEvent::KeyPressed { .. } | UiEvent::CharTyped(_)) {
backend.note_caret_activity(std::time::Instant::now());
}
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
crate::runtime::preprocess_event(event, backend, app)
})) {
Ok(outcome) => outcome,
Err(payload) => {
crate::desktop::report_caught_panic_once(
"win::run::dispatch_event (app.handle)",
payload.as_ref(),
);
EventOutcome::Continue
}
}
}
#[allow(dead_code)]
pub(crate) fn route_mouse_down<A: AppLogic>(
backend: &mut WinBackend,
app: &mut A,
position: Point,
button: MouseButton,
modifiers: Modifiers,
) -> EventOutcome {
let events = {
let stack_rc = backend.modal_stack_handle();
let drag_rc = backend.drag_state_handle();
let stack = stack_rc.borrow();
let mut drag = drag_rc.borrow_mut();
crate::dispatch::route_pointer(
&stack,
&mut drag,
backend.text_selection_state_mut(),
UiEvent::MouseDown {
widget: None,
button,
position,
modifiers,
},
)
};
let mut outcome = EventOutcome::Continue;
for ev in events {
let step = dispatch_event(ev, backend, app);
if matches!(step, EventOutcome::Exit) {
return EventOutcome::Exit;
}
outcome = outcome.merge(step);
}
outcome
}
#[allow(dead_code)]
pub(crate) fn route_mouse_move<A: AppLogic>(
backend: &mut WinBackend,
app: &mut A,
position: Point,
buttons: ButtonMask,
) -> EventOutcome {
let events = {
let stack_rc = backend.modal_stack_handle();
let drag_rc = backend.drag_state_handle();
let stack = stack_rc.borrow();
let mut drag = drag_rc.borrow_mut();
crate::dispatch::route_pointer(
&stack,
&mut drag,
backend.text_selection_state_mut(),
UiEvent::MouseMoved { position, buttons },
)
};
let mut outcome = EventOutcome::Continue;
for ev in events {
let step = dispatch_event(ev, backend, app);
if matches!(step, EventOutcome::Exit) {
return EventOutcome::Exit;
}
outcome = outcome.merge(step);
}
outcome
}
#[allow(dead_code)]
pub(crate) fn route_mouse_up<A: AppLogic>(
backend: &mut WinBackend,
app: &mut A,
position: Point,
button: MouseButton,
) -> EventOutcome {
let events = {
let stack_rc = backend.modal_stack_handle();
let drag_rc = backend.drag_state_handle();
let stack = stack_rc.borrow();
let mut drag = drag_rc.borrow_mut();
crate::dispatch::route_pointer(
&stack,
&mut drag,
backend.text_selection_state_mut(),
UiEvent::MouseUp {
widget: None,
button,
position,
},
)
};
let mut outcome = EventOutcome::Continue;
for ev in events {
let step = dispatch_event(ev, backend, app);
if matches!(step, EventOutcome::Exit) {
return EventOutcome::Exit;
}
outcome = outcome.merge(step);
}
outcome
}
#[allow(dead_code)]
pub(crate) fn render_frame<A: AppLogic>(backend: &mut WinBackend, app: &A, viewport: Viewport) {
backend.begin_frame(viewport);
let rendered = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
app.render(&mut *backend, Default::default());
}));
if let Err(payload) = rendered {
crate::desktop::report_caught_panic_once(
"win::run::render_frame (app.render)",
payload.as_ref(),
);
backend.end_frame();
return;
}
let focus_ring_rect =
crate::runtime::focused_stop_rect(backend, app, <A as AppLogic>::AreaId::default());
if let Some(rect) = focus_ring_rect {
backend.draw_focus_ring(rect);
}
backend.apply_selection_highlight();
backend.end_frame();
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RunConfig {
pub title: String,
pub single_instance: bool,
pub app_icon: Option<ImageSource>,
}
impl RunConfig {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
single_instance: false,
app_icon: None,
}
}
pub fn with_single_instance(mut self, single_instance: bool) -> Self {
self.single_instance = single_instance;
self
}
pub fn with_app_icon(mut self, icon: ImageSource) -> Self {
self.app_icon = Some(icon);
self
}
}
impl Default for RunConfig {
fn default() -> Self {
Self {
title: "quadraui".to_string(),
single_instance: false,
app_icon: None,
}
}
}
#[allow(dead_code)]
fn encode_copydata_argv(args: &[String]) -> String {
args.join("\0")
}
#[allow(dead_code)]
fn decode_copydata_argv(payload: &str) -> Vec<String> {
payload
.trim_end_matches('\0')
.split('\0')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}
const CLASS_NAME_PREFIX: &str = "QuadrauiWin32WindowClass";
fn instance_identity(title: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
title.hash(&mut hasher);
hasher.finish()
}
#[allow(dead_code)]
fn window_class_name_string(title: &str) -> String {
format!("{CLASS_NAME_PREFIX}-{:016x}", instance_identity(title))
}
#[cfg(any(target_os = "windows", test))]
use crate::desktop::{ModalPumpDepth, ModalPumpGuard};
#[cfg(any(target_os = "windows", test))]
use std::cell::RefCell;
#[cfg(any(target_os = "windows", test))]
fn guarded_call<T, R>(
state: &RefCell<T>,
pump_depth: &ModalPumpDepth,
f: impl FnOnce(&mut T) -> R,
) -> Option<R> {
if pump_depth.is_pumping() {
return None;
}
let _guard = ModalPumpGuard::new(pump_depth);
Some(f(&mut state.borrow_mut()))
}
#[cfg(target_os = "windows")]
mod win32 {
use std::cell::{Cell, RefCell};
use std::ffi::c_void;
use std::mem::size_of;
use std::rc::Rc;
use std::time::{Duration, Instant};
use windows::core::{Error as WinError, PCWSTR};
use windows::Win32::Foundation::{
GetLastError, ERROR_ALREADY_EXISTS, HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM,
};
use windows::Win32::Graphics::Gdi::{
InvalidateRect, ScreenToClient, UpdateWindow, ValidateRect,
};
use windows::Win32::System::DataExchange::COPYDATASTRUCT;
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::System::Threading::CreateMutexW;
use windows::Win32::UI::HiDpi::{
SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
};
use windows::Win32::UI::Input::KeyboardAndMouse::{
GetKeyState, VIRTUAL_KEY, VK_CONTROL, VK_LWIN, VK_MENU, VK_RWIN, VK_SHIFT,
};
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, FindWindowW,
GetClientRect, GetWindowLongPtrW, KillTimer, LoadCursorW, MsgWaitForMultipleObjects,
PeekMessageW, PostQuitMessage, RegisterClassExW, SendMessageW, SetForegroundWindow,
SetTimer, SetWindowLongPtrW, SetWindowPos, ShowWindow, TranslateMessage, CREATESTRUCTW,
CS_HREDRAW, CS_VREDRAW, CW_USEDEFAULT, GWLP_USERDATA, HTCAPTION, HTCLIENT, ICON_BIG,
ICON_SMALL, IDC_ARROW, MSG, NCCALCSIZE_PARAMS, PM_REMOVE, QS_ALLINPUT, SWP_NOACTIVATE,
SWP_NOZORDER, SW_RESTORE, SW_SHOW, WINDOW_EX_STYLE, WM_CHAR, WM_CLOSE, WM_COPYDATA,
WM_DESTROY, WM_DPICHANGED, WM_DROPFILES, WM_KEYDOWN, WM_KILLFOCUS, WM_LBUTTONDOWN,
WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL,
WM_NCCALCSIZE, WM_NCCREATE, WM_NCHITTEST, WM_PAINT, WM_QUIT, WM_RBUTTONDOWN, WM_RBUTTONUP,
WM_SETCURSOR, WM_SETFOCUS, WM_SETICON, WM_SIZE, WM_SYSCHAR, WM_SYSKEYDOWN, WM_TIMER,
WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_OVERLAPPEDWINDOW,
};
use windows::Win32::UI::Shell::{
DragAcceptFiles, DragFinish, DragQueryFileW, DragQueryPoint, HDROP,
};
use crate::backend::Backend;
use crate::desktop::{
smoke_clipboard_round_trip_ok, smoke_size_ok, ModalPumpDepth, ModalPumpGuard, SmokeConfig,
};
use crate::event::{classify_open_args, UiEvent};
use crate::primitives::image::ImageSource;
use crate::runner::{AppLogic, Reaction, StepOutcome};
use crate::win::backend::WinBackend;
use crate::win::tray::decode_hicon;
use crate::win::events::{
win_button_down, win_button_up, win_files_dropped, win_focus_to_uievent, win_modifiers,
win_mouse_button_for_message, win_mouse_moved, win_wheel_to_uievent, wm_char_to_uievent,
wm_keydown_to_uievent,
};
use crate::runtime::{ResizeDebouncer, RESIZE_SETTLE};
use crate::win::msg::{
dpi_scale_from_wparam, is_repeat_from_lparam, point_from_lparam, size_from_lparam,
wheel_delta_from_wparam,
};
use crate::{ButtonMask, Key, Modifiers};
use super::RunConfig;
fn window_class_name(title: &str) -> Vec<u16> {
wide(&format!("{}\0", super::window_class_name_string(title)))
}
const ARGV_FORWARD_TAG: usize = 0x957;
const DEFAULT_WIDTH: i32 = 800;
const DEFAULT_HEIGHT: i32 = 600;
const SMOKE_MS_VAR: &str = "QUADRAUI_WIN_SMOKE_MS";
const SMOKE_PASTE_VAR: &str = "QUADRAUI_WIN_SMOKE_PASTE";
const SMOKE_MIN_WIDTH: i32 = 200;
const SMOKE_MIN_HEIGHT: i32 = 150;
const SMOKE_TIMER_ID: usize = 1;
const RESIZE_TIMER_ID: usize = 2;
const IDLE_POLL_TIMER_ID: usize = 4;
const CARET_BLINK_TIMER_ID: usize = 5;
struct RunState<A: AppLogic> {
app: A,
backend: WinBackend,
}
struct WindowState<A: AppLogic> {
state: RefCell<RunState<A>>,
pump_depth: ModalPumpDepth,
smoke: Option<SmokeConfig>,
smoke_failed: Rc<Cell<bool>>,
resize_debouncer: RefCell<ResizeDebouncer>,
pending_resize_resync: Cell<bool>,
}
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().collect()
}
const MK_LBUTTON: usize = 0x0001;
const MK_RBUTTON: usize = 0x0002;
const MK_MBUTTON: usize = 0x0010;
use crate::win::backend::{FRAME_TIMER_ID, WM_QUADRAUI_TRAY_CALLBACK, WM_QUADRAUI_USER_EVENT};
fn win_key_modifiers() -> Modifiers {
win_modifiers(
key_is_down(VK_CONTROL),
key_is_down(VK_SHIFT),
key_is_down(VK_MENU),
key_is_down(VK_LWIN) || key_is_down(VK_RWIN),
)
}
fn key_is_down(vk: VIRTUAL_KEY) -> bool {
unsafe { GetKeyState(vk.0 as i32) < 0 }
}
fn is_primary_instance(title: &str) -> bool {
let name = wide(&format!(
"Local\\quadraui-single-instance-{:016x}\0",
super::instance_identity(title)
));
match unsafe { CreateMutexW(None, false, PCWSTR(name.as_ptr())) } {
Ok(_handle) => (unsafe { GetLastError() }) != ERROR_ALREADY_EXISTS,
Err(_) => true,
}
}
fn forward_argv_to_existing_instance(title: &str) {
let class_name = window_class_name(title);
let Ok(hwnd) = (unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) }) else {
return;
};
let payload = super::encode_copydata_argv(&std::env::args().skip(1).collect::<Vec<_>>());
let payload_wide = wide(&format!("{payload}\0"));
let cds = COPYDATASTRUCT {
dwData: ARGV_FORWARD_TAG,
cbData: (payload_wide.len() * size_of::<u16>()) as u32,
lpData: payload_wide.as_ptr() as *mut c_void,
};
unsafe {
SendMessageW(
hwnd,
WM_COPYDATA,
Some(WPARAM(0)),
Some(LPARAM(std::ptr::addr_of!(cds) as isize)),
);
let _ = SetForegroundWindow(hwnd);
let _ = ShowWindow(hwnd, SW_RESTORE);
}
}
pub(super) fn run<A: AppLogic + 'static>(
app: A,
config: super::RunConfig,
) -> std::process::ExitCode {
unsafe {
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
if config.single_instance && !is_primary_instance(&config.title) {
forward_argv_to_existing_instance(&config.title);
return std::process::ExitCode::SUCCESS;
}
match unsafe { run_inner(app, &config) } {
Ok(smoke_ok) => {
if smoke_ok {
std::process::ExitCode::SUCCESS
} else {
std::process::ExitCode::FAILURE
}
}
Err(_) => {
std::process::ExitCode::FAILURE
}
}
}
type CreatedWindow<A> = (HWND, *mut WindowState<A>, Rc<Cell<bool>>);
unsafe fn create_window<A: AppLogic + 'static>(
mut app: A,
title: &str,
app_icon: Option<&ImageSource>,
) -> windows::core::Result<CreatedWindow<A>> {
let hinstance: HINSTANCE = unsafe { GetModuleHandleW(PCWSTR::null())?.into() };
let class_name = window_class_name(title);
let title_nul = format!("{title}\0");
let window_title = wide(&title_nul);
let wc = WNDCLASSEXW {
cbSize: size_of::<WNDCLASSEXW>() as u32,
style: CS_HREDRAW | CS_VREDRAW,
lpfnWndProc: Some(wndproc::<A>),
hInstance: hinstance,
hCursor: unsafe { LoadCursorW(None, IDC_ARROW)? },
lpszClassName: PCWSTR(class_name.as_ptr()),
..Default::default()
};
if unsafe { RegisterClassExW(&wc) } == 0 {
return Err(WinError::from_thread());
}
let mut backend = WinBackend::new();
app.setup(&mut backend);
let smoke = SmokeConfig::from_env(SMOKE_MS_VAR, SMOKE_PASTE_VAR);
let smoke_failed = Rc::new(Cell::new(false));
let state_ptr: *mut WindowState<A> = Box::into_raw(Box::new(WindowState {
state: RefCell::new(RunState { app, backend }),
pump_depth: ModalPumpDepth::new(),
smoke,
smoke_failed: smoke_failed.clone(),
resize_debouncer: RefCell::new(ResizeDebouncer::new()),
pending_resize_resync: Cell::new(false),
}));
let hwnd = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE::default(),
PCWSTR(class_name.as_ptr()),
PCWSTR(window_title.as_ptr()),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
DEFAULT_WIDTH,
DEFAULT_HEIGHT,
None,
None,
Some(hinstance),
Some(state_ptr as *const c_void),
)
};
let hwnd = match hwnd {
Ok(hwnd) => hwnd,
Err(e) => {
drop(unsafe { Box::from_raw(state_ptr) });
return Err(e);
}
};
let attach_result = {
let ws: &WindowState<A> = unsafe { &*state_ptr };
let _guard = ModalPumpGuard::new(&ws.pump_depth);
ws.state.borrow_mut().backend.attach_surface(hwnd)
};
if let Err(e) = attach_result {
let _ = unsafe { DestroyWindow(hwnd) };
drop(unsafe { Box::from_raw(state_ptr) });
return Err(e);
}
unsafe {
DragAcceptFiles(hwnd, true);
}
if let Some(source) = app_icon {
if let Some(hicon) = decode_hicon(source) {
unsafe {
SendMessageW(
hwnd,
WM_SETICON,
Some(WPARAM(ICON_BIG as usize)),
Some(LPARAM(hicon.0 as isize)),
);
SendMessageW(
hwnd,
WM_SETICON,
Some(WPARAM(ICON_SMALL as usize)),
Some(LPARAM(hicon.0 as isize)),
);
}
}
}
{
let ws: &WindowState<A> = unsafe { &*state_ptr };
let _ = super::guarded_call(&ws.state, &ws.pump_depth, |run_state| {
let viewport = run_state.backend.viewport();
let RunState { app, backend } = run_state;
super::render_frame(backend, app, viewport);
if matches!(
Backend::last_error(backend),
Some(crate::backend::BackendError::SurfaceLost)
) && backend.ensure_surface().is_ok()
{
let viewport = backend.viewport();
super::render_frame(backend, app, viewport);
}
});
}
let _ = unsafe { ShowWindow(hwnd, SW_SHOW) };
let _ = unsafe { UpdateWindow(hwnd) };
unsafe {
SetTimer(
Some(hwnd),
IDLE_POLL_TIMER_ID,
crate::runtime::IDLE_POLL_CEILING
.as_millis()
.min(u32::MAX as u128) as u32,
None,
);
}
{
let ws: &WindowState<A> = unsafe { &*state_ptr };
let period_ms = ws
.state
.borrow()
.backend
.caret_blink_period()
.as_millis()
.min(u32::MAX as u128) as u32;
unsafe {
SetTimer(Some(hwnd), CARET_BLINK_TIMER_ID, period_ms, None);
}
}
{
let ws: &WindowState<A> = unsafe { &*state_ptr };
if let Some(cfg) = &ws.smoke {
unsafe {
SetTimer(Some(hwnd), SMOKE_TIMER_ID, cfg.after_ms as u32, None);
}
}
}
{
let args: Vec<String> = std::env::args().skip(1).collect();
if !args.is_empty() {
let ws: &WindowState<A> = unsafe { &*state_ptr };
dispatch(ws, hwnd, classify_open_args(args));
}
}
Ok((hwnd, state_ptr, smoke_failed))
}
pub struct WinRunner<A: AppLogic + 'static> {
hwnd: HWND,
state_ptr: *mut WindowState<A>,
smoke_failed: Rc<Cell<bool>>,
finished: bool,
}
impl<A: AppLogic + 'static> WinRunner<A> {
pub unsafe fn new(app: A) -> windows::core::Result<Self> {
unsafe { Self::new_with(app, &RunConfig::default()) }
}
pub unsafe fn new_with(app: A, config: &RunConfig) -> windows::core::Result<Self> {
let created = unsafe { create_window(app, &config.title, config.app_icon.as_ref())? };
let (hwnd, state_ptr, smoke_failed) = created;
Ok(Self {
hwnd,
state_ptr,
smoke_failed,
finished: false,
})
}
pub fn step(&mut self) -> windows::core::Result<StepOutcome> {
self.run_one(None)
}
pub fn pump(&mut self, timeout: Duration) -> windows::core::Result<StepOutcome> {
self.run_one(Some(timeout))
}
fn run_one(&mut self, block_for: Option<Duration>) -> windows::core::Result<StepOutcome> {
if self.finished {
return Ok(StepOutcome::Exited);
}
if let Some(timeout) = block_for {
let timeout_ms = timeout.as_millis().min(u32::MAX as u128) as u32;
unsafe {
let _ = MsgWaitForMultipleObjects(None, false, timeout_ms, QS_ALLINPUT);
}
}
let mut msg = MSG::default();
loop {
let peeked = unsafe { PeekMessageW(&mut msg, None, 0, 0, PM_REMOVE) };
let has_message = peeked.as_bool();
if !has_message {
return Ok(StepOutcome::Continue);
}
if msg.message == WM_QUIT {
self.finish();
return Ok(StepOutcome::Exited);
}
unsafe {
let _ = TranslateMessage(&msg);
DispatchMessageW(&msg);
}
}
}
fn finish(&mut self) {
if self.finished {
return;
}
self.finished = true;
drop(unsafe { Box::from_raw(self.state_ptr) });
}
}
impl<A: AppLogic + 'static> Drop for WinRunner<A> {
fn drop(&mut self) {
if !self.finished {
unsafe {
let _ = DestroyWindow(self.hwnd);
}
}
self.finish();
}
}
unsafe fn run_inner<A: AppLogic + 'static>(
app: A,
config: &super::RunConfig,
) -> windows::core::Result<bool> {
let mut runner = unsafe { WinRunner::new_with(app, config)? };
while let StepOutcome::Continue = runner.pump(crate::runtime::IDLE_POLL_CEILING)? {}
Ok(!runner.smoke_failed.get())
}
fn dispatch<A: AppLogic>(ws: &WindowState<A>, hwnd: HWND, event: UiEvent) -> Reaction {
dispatch_with(ws, hwnd, |backend, app| {
super::dispatch_event(event, backend, app)
})
}
fn dispatch_with<A: AppLogic>(
ws: &WindowState<A>,
hwnd: HWND,
f: impl FnOnce(&mut WinBackend, &mut A) -> super::EventOutcome,
) -> Reaction {
let outcome = super::guarded_call(&ws.state, &ws.pump_depth, |run_state| {
let RunState { app, backend } = run_state;
f(backend, app)
});
let Some(outcome) = outcome else {
return Reaction::Continue;
};
let reaction = match outcome {
super::EventOutcome::Continue => Reaction::Continue,
super::EventOutcome::Redraw => Reaction::Redraw,
super::EventOutcome::RedrawAfter(delay) => {
ws.state.borrow().backend.request_frame_in(delay);
Reaction::Continue
}
super::EventOutcome::Exit => Reaction::Exit,
};
if reaction == Reaction::Redraw {
unsafe {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
if reaction == Reaction::Exit {
unsafe {
let _ = DestroyWindow(hwnd);
}
}
reaction
}
fn tick<A: AppLogic>(ws: &WindowState<A>, hwnd: HWND) -> Reaction {
let reaction = super::guarded_call(&ws.state, &ws.pump_depth, |run_state| {
let RunState { app, backend } = run_state;
app.tick(backend)
});
let Some(reaction) = reaction else {
return Reaction::Continue;
};
match reaction {
Reaction::Redraw => unsafe {
let _ = InvalidateRect(Some(hwnd), None, false);
},
Reaction::RedrawAfter(delay) => {
ws.state.borrow().backend.request_frame_in(delay);
}
Reaction::Exit => {
unsafe {
let _ = DestroyWindow(hwnd);
}
}
Reaction::Continue => {}
}
reaction
}
#[allow(clippy::print_stderr)]
fn run_smoke_check<A: AppLogic>(ws: &WindowState<A>, hwnd: HWND) {
let Some(cfg) = ws.smoke.as_ref() else {
return;
};
let mut rect = RECT::default();
let (width, height) = unsafe {
let _ = GetClientRect(hwnd, &mut rect);
(rect.right - rect.left, rect.bottom - rect.top)
};
if !smoke_size_ok(width, height, SMOKE_MIN_WIDTH, SMOKE_MIN_HEIGHT) {
eprintln!(
"quadraui smoke: client area size looks broken ({width}x{height}px, expected \
at least {SMOKE_MIN_WIDTH}x{SMOKE_MIN_HEIGHT}px) — this is the quadraui#437 \
tiny-window regression class"
);
ws.smoke_failed.set(true);
}
if let Some(text) = &cfg.paste_text {
let read_back = {
let state = ws.state.borrow();
let clipboard = state.backend.services().clipboard();
clipboard.write_text(text);
clipboard.read_text()
};
if !smoke_clipboard_round_trip_ok(text, read_back.as_deref()) {
eprintln!(
"quadraui smoke: OS clipboard round-trip failed — wrote {text:?}, read back \
{read_back:?}"
);
ws.smoke_failed.set(true);
} else {
dispatch(
ws,
hwnd,
UiEvent::KeyPressed {
key: Key::Char('v'),
modifiers: Modifiers {
ctrl: true,
shift: false,
alt: false,
cmd: false,
},
repeat: false,
},
);
}
}
}
fn paths_from_dropfiles(hdrop: HDROP) -> (Vec<std::path::PathBuf>, i16, i16) {
let mut pt = POINT::default();
unsafe {
let _ = DragQueryPoint(hdrop, &mut pt);
}
let count = unsafe { DragQueryFileW(hdrop, u32::MAX, None) };
let mut paths = Vec::with_capacity(count as usize);
for i in 0..count {
let len = unsafe { DragQueryFileW(hdrop, i, None) };
let mut buf = vec![0u16; len as usize + 1];
let written = unsafe { DragQueryFileW(hdrop, i, Some(&mut buf[..])) };
buf.truncate(written as usize);
paths.push(std::path::PathBuf::from(String::from_utf16_lossy(&buf)));
}
unsafe {
DragFinish(hdrop);
}
(paths, pt.x as i16, pt.y as i16)
}
fn apply_resize<A: AppLogic>(ws: &WindowState<A>, hwnd: HWND, width: u32, height: u32) {
let viewport = {
let mut s = ws.state.borrow_mut();
let _ = s.backend.ensure_surface();
let _ = s.backend.resize_surface(width, height);
s.backend.viewport()
};
ws.resize_debouncer.borrow_mut().note(viewport);
unsafe {
SetTimer(
Some(hwnd),
RESIZE_TIMER_ID,
RESIZE_SETTLE.as_millis() as u32,
None,
);
}
unsafe {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
fn resync_pending_resize<A: AppLogic>(ws: &WindowState<A>, hwnd: HWND) {
if !ws.pending_resize_resync.replace(false) {
return;
}
let mut rect = RECT::default();
let (width, height) = unsafe {
let _ = GetClientRect(hwnd, &mut rect);
(
(rect.right - rect.left) as u32,
(rect.bottom - rect.top) as u32,
)
};
apply_resize(ws, hwnd, width, height);
}
unsafe extern "system" fn wndproc<A: AppLogic + 'static>(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
if msg == WM_NCCREATE {
let cs = lparam.0 as *const CREATESTRUCTW;
if !cs.is_null() {
let create_params = unsafe { (*cs).lpCreateParams };
unsafe {
SetWindowLongPtrW(hwnd, GWLP_USERDATA, create_params as isize);
}
}
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
}
let state_ptr = unsafe { GetWindowLongPtrW(hwnd, GWLP_USERDATA) } as *const WindowState<A>;
if state_ptr.is_null() {
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
}
let ws: &WindowState<A> = unsafe { &*state_ptr };
if ws.pump_depth.is_pumping() {
if msg == WM_SIZE || msg == WM_DPICHANGED {
ws.pending_resize_resync.set(true);
}
return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
}
resync_pending_resize(ws, hwnd);
match msg {
WM_SIZE => {
let (width, height) = size_from_lparam(lparam.0);
apply_resize(ws, hwnd, width, height);
LRESULT(0)
}
WM_DPICHANGED => {
let scale = dpi_scale_from_wparam(wparam.0);
{
let mut s = ws.state.borrow_mut();
s.backend.set_dpi_scale(scale);
}
let suggested = lparam.0 as *const RECT;
if !suggested.is_null() {
let r = unsafe { *suggested };
unsafe {
let _ = SetWindowPos(
hwnd,
None,
r.left,
r.top,
r.right - r.left,
r.bottom - r.top,
SWP_NOZORDER | SWP_NOACTIVATE,
);
}
}
dispatch(ws, hwnd, UiEvent::DpiChanged(scale));
LRESULT(0)
}
WM_LBUTTONDOWN | WM_RBUTTONDOWN | WM_MBUTTONDOWN | WM_XBUTTONDOWN => {
if let Some(button) = win_mouse_button_for_message(msg, wparam.0) {
let (x, y) = point_from_lparam(lparam.0);
let scale = ws.state.borrow().backend.viewport().scale;
let modifiers = win_key_modifiers();
let UiEvent::MouseDown { position, .. } =
win_button_down(button, x, y, scale, modifiers)
else {
unreachable!("win_button_down always returns UiEvent::MouseDown")
};
dispatch_with(ws, hwnd, |backend, app| {
super::route_mouse_down(backend, app, position, button, modifiers)
});
}
if msg == WM_XBUTTONDOWN {
LRESULT(1)
} else {
LRESULT(0)
}
}
WM_LBUTTONUP | WM_RBUTTONUP | WM_MBUTTONUP | WM_XBUTTONUP => {
if let Some(button) = win_mouse_button_for_message(msg, wparam.0) {
let (x, y) = point_from_lparam(lparam.0);
let scale = ws.state.borrow().backend.viewport().scale;
let UiEvent::MouseUp { position, .. } = win_button_up(button, x, y, scale)
else {
unreachable!("win_button_up always returns UiEvent::MouseUp")
};
dispatch_with(ws, hwnd, |backend, app| {
super::route_mouse_up(backend, app, position, button)
});
}
if msg == WM_XBUTTONUP {
LRESULT(1)
} else {
LRESULT(0)
}
}
WM_MOUSEMOVE => {
let (x, y) = point_from_lparam(lparam.0);
let scale = ws.state.borrow().backend.viewport().scale;
let buttons = ButtonMask {
left: wparam.0 & MK_LBUTTON != 0,
right: wparam.0 & MK_RBUTTON != 0,
middle: wparam.0 & MK_MBUTTON != 0,
};
let UiEvent::MouseMoved { position, .. } = win_mouse_moved(x, y, scale, buttons)
else {
unreachable!("win_mouse_moved always returns UiEvent::MouseMoved")
};
dispatch_with(ws, hwnd, |backend, app| {
super::route_mouse_move(backend, app, position, buttons)
});
LRESULT(0)
}
WM_MOUSEWHEEL | WM_MOUSEHWHEEL => {
let raw_delta = wheel_delta_from_wparam(wparam.0);
let horizontal = msg == WM_MOUSEHWHEEL;
let (screen_x, screen_y) = point_from_lparam(lparam.0);
let mut pt = POINT {
x: screen_x as i32,
y: screen_y as i32,
};
unsafe {
let _ = ScreenToClient(hwnd, &mut pt);
}
let scale = ws.state.borrow().backend.viewport().scale;
let event =
win_wheel_to_uievent(raw_delta, pt.x as i16, pt.y as i16, scale, horizontal);
dispatch(ws, hwnd, event);
LRESULT(0)
}
WM_KEYDOWN => {
let vk = (wparam.0 & 0xFF) as u32;
let repeat = is_repeat_from_lparam(lparam.0);
let modifiers = win_key_modifiers();
if let Some(event) = wm_keydown_to_uievent(vk, modifiers, repeat) {
dispatch(ws, hwnd, event);
}
LRESULT(0)
}
WM_CHAR => {
let repeat = is_repeat_from_lparam(lparam.0);
if let Some(c) = char::from_u32((wparam.0 & 0xFFFF) as u32) {
let modifiers = win_key_modifiers();
if let Some(event) = wm_char_to_uievent(c, modifiers, repeat) {
dispatch(ws, hwnd, event);
}
}
LRESULT(0)
}
WM_SYSKEYDOWN => {
let vk = (wparam.0 & 0xFF) as u32;
let repeat = is_repeat_from_lparam(lparam.0);
let modifiers = win_key_modifiers();
if let Some(event) = wm_keydown_to_uievent(vk, modifiers, repeat) {
dispatch(ws, hwnd, event);
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
WM_SYSCHAR => {
let repeat = is_repeat_from_lparam(lparam.0);
if let Some(c) = char::from_u32((wparam.0 & 0xFFFF) as u32) {
let modifiers = win_key_modifiers();
if let Some(event) = wm_char_to_uievent(c, modifiers, repeat) {
dispatch(ws, hwnd, event);
}
}
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
WM_SETFOCUS => {
dispatch(ws, hwnd, win_focus_to_uievent(true));
LRESULT(0)
}
WM_KILLFOCUS => {
dispatch(ws, hwnd, win_focus_to_uievent(false));
LRESULT(0)
}
WM_PAINT => {
let _ = super::guarded_call(&ws.state, &ws.pump_depth, |run_state| {
let _ = run_state.backend.ensure_surface();
let viewport = run_state.backend.viewport();
let RunState { app, backend } = run_state;
super::render_frame(backend, app, viewport);
});
unsafe {
let _ = ValidateRect(Some(hwnd), None);
}
LRESULT(0)
}
WM_NCCALCSIZE => {
if wparam.0 != 0 {
let params = unsafe { &mut *(lparam.0 as *mut NCCALCSIZE_PARAMS) };
let outer_top = params.rgrc[0].top;
unsafe {
DefWindowProcW(hwnd, msg, wparam, lparam);
}
params.rgrc[0].top = outer_top;
LRESULT(0)
} else {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
}
WM_NCHITTEST => {
let default = unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
if default.0 as u32 != HTCLIENT {
return default;
}
let (screen_x, screen_y) = point_from_lparam(lparam.0);
let mut pt = POINT {
x: screen_x as i32,
y: screen_y as i32,
};
unsafe {
let _ = ScreenToClient(hwnd, &mut pt);
}
let scale = ws.state.borrow().backend.viewport().scale;
let (dip_x, dip_y) = if scale == 0.0 {
(pt.x as f32, pt.y as f32)
} else {
(pt.x as f32 / scale, pt.y as f32 / scale)
};
match ws.state.borrow().backend.nc_hit_test(dip_x, dip_y) {
Some(true) => LRESULT(HTCAPTION as isize),
Some(false) | None => default,
}
}
WM_SETCURSOR => {
let hit_test = (lparam.0 as usize) & 0xFFFF;
if hit_test == HTCLIENT as usize {
ws.state.borrow().backend.apply_current_cursor();
LRESULT(1)
} else {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
}
WM_TIMER => {
if wparam.0 == SMOKE_TIMER_ID {
unsafe {
let _ = KillTimer(Some(hwnd), SMOKE_TIMER_ID);
}
run_smoke_check(ws, hwnd);
unsafe {
let _ = DestroyWindow(hwnd);
}
LRESULT(0)
} else if wparam.0 == RESIZE_TIMER_ID {
unsafe {
let _ = KillTimer(Some(hwnd), RESIZE_TIMER_ID);
}
if let Some(viewport) = ws.resize_debouncer.borrow_mut().take() {
dispatch(ws, hwnd, UiEvent::WindowResized { viewport });
}
LRESULT(0)
} else if wparam.0 == FRAME_TIMER_ID {
unsafe {
let _ = KillTimer(Some(hwnd), FRAME_TIMER_ID);
}
tick(ws, hwnd);
LRESULT(0)
} else if wparam.0 == IDLE_POLL_TIMER_ID {
tick(ws, hwnd);
LRESULT(0)
} else if wparam.0 == CARET_BLINK_TIMER_ID {
let redraw = ws
.state
.borrow_mut()
.backend
.tick_caret_blink(Instant::now());
if redraw {
unsafe {
let _ = InvalidateRect(Some(hwnd), None, false);
}
}
LRESULT(0)
} else {
unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
}
}
WM_QUADRAUI_USER_EVENT => {
let events = ws.state.borrow_mut().backend.drain_user_events();
for event in events {
if dispatch(ws, hwnd, event) == Reaction::Exit {
break;
}
}
LRESULT(0)
}
WM_QUADRAUI_TRAY_CALLBACK => {
let Some(button) = crate::win::tray::tray_click_button(lparam.0 as u32) else {
return LRESULT(0);
};
let menu = ws.state.borrow().backend.tray_menu();
if let Some(menu) = menu {
if let Some(id) = crate::win::tray::track_menu(hwnd, &menu) {
if dispatch(ws, hwnd, UiEvent::ContextMenuItemActivated(id))
== Reaction::Exit
{
return LRESULT(0);
}
}
dispatch(ws, hwnd, UiEvent::ContextMenuDismissed);
} else {
dispatch(ws, hwnd, crate::win::tray::plain_click_event(button));
}
LRESULT(0)
}
WM_DROPFILES => {
let hdrop = HDROP(wparam.0 as *mut c_void);
let (paths, x, y) = paths_from_dropfiles(hdrop);
if !paths.is_empty() {
let scale = ws.state.borrow().backend.viewport().scale;
dispatch(ws, hwnd, win_files_dropped(paths, x, y, scale));
}
LRESULT(0)
}
WM_COPYDATA => {
let cds = unsafe { &*(lparam.0 as *const COPYDATASTRUCT) };
if cds.dwData != ARGV_FORWARD_TAG {
return LRESULT(0);
}
let code_units = cds.cbData as usize / size_of::<u16>();
let units =
unsafe { std::slice::from_raw_parts(cds.lpData.cast::<u16>(), code_units) };
let payload = String::from_utf16_lossy(units);
let args = super::decode_copydata_argv(&payload);
if !args.is_empty() {
dispatch(ws, hwnd, classify_open_args(args));
}
unsafe {
let _ = SetForegroundWindow(hwnd);
let _ = ShowWindow(hwnd, SW_RESTORE);
}
LRESULT(1)
}
WM_CLOSE => {
dispatch(ws, hwnd, UiEvent::WindowClose);
LRESULT(0)
}
WM_DESTROY => {
unsafe {
PostQuitMessage(0);
}
LRESULT(0)
}
_ => unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) },
}
}
#[cfg(test)]
mod live_window_nchittest_tests {
use super::*;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use windows::Win32::Graphics::Gdi::ClientToScreen;
#[derive(Clone, Copy, Debug, Default)]
struct ProbeTargets {
file_item: Option<(f32, f32)>,
close_button: Option<(f32, f32)>,
search_box: Option<(f32, f32)>,
empty_band: (f32, f32),
scale: f32,
}
struct LiveWindowApp {
targets: Arc<Mutex<ProbeTargets>>,
}
impl AppLogic for LiveWindowApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
let viewport = backend.viewport();
let band_height = 32.0_f32;
let band = crate::event::Rect::new(0.0, 0.0, viewport.width, band_height);
Backend::register_zone(
backend,
crate::types::WidgetId::new(WinBackend::TITLE_BAR_DRAG_ZONE),
band,
);
let bar = crate::MenuBar {
id: crate::types::WidgetId::new("probe:bar"),
items: vec![crate::MenuBarItem {
id: crate::types::WidgetId::new("probe:bar:file"),
label: "&File".into(),
disabled: false,
submenu: None,
}],
open_item: None,
focused_item: None,
};
let menu_layout = backend
.draw_menu_bar(crate::event::Rect::new(0.0, 0.0, 100.0, band_height), &bar);
let status = crate::StatusBar {
id: crate::types::WidgetId::new("probe:status"),
left_segments: vec![],
right_segments: vec![crate::StatusBarSegment {
text: "X".into(),
fg: crate::types::Color::rgb(255, 255, 255),
bg: crate::types::Color::rgb(0, 0, 0),
bold: false,
action_id: Some(crate::types::WidgetId::new("probe:close")),
}],
};
let status_rect =
crate::event::Rect::new(viewport.width - 40.0, 0.0, 40.0, band_height);
let status_layout = backend.draw_status_bar_interactive(
status_rect,
&status,
&crate::interaction::InteractionState::new(),
);
let cc = crate::CommandCenter {
id: crate::types::WidgetId::new("probe:cc"),
back_enabled: true,
forward_enabled: true,
search_label: "probe".into(),
};
let cc_rect = crate::event::Rect::new(160.0, 0.0, 240.0, band_height);
let cc_layout = backend.draw_command_center(cc_rect, &cc);
let file_item = menu_layout
.visible_items
.iter()
.find(|v| v.clickable)
.map(|v| {
(
v.bounds.x + v.bounds.width / 2.0,
v.bounds.y + v.bounds.height / 2.0,
)
});
let close_button = status_layout.hit_regions.iter().find_map(|(r, hit)| {
matches!(hit, crate::primitives::status_bar::StatusBarHit::Segment(_)).then(
|| {
(
status_rect.x + r.x + r.width / 2.0,
status_rect.y + r.y + r.height / 2.0,
)
},
)
});
let search_box = cc_layout
.search_bounds
.map(|r| (r.x + r.width / 2.0, r.y + r.height / 2.0));
let mut targets = self.targets.lock().unwrap();
targets.file_item = file_item;
targets.close_button = close_button;
targets.search_box = search_box;
targets.empty_band = (130.0, band_height / 2.0);
targets.scale = viewport.scale;
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::WindowClose => Reaction::Exit,
_ => Reaction::Continue,
}
}
}
fn dip_to_screen_lparam(hwnd: HWND, dip: (f32, f32), scale: f32) -> LPARAM {
let mut pt = POINT {
x: (dip.0 * scale).round() as i32,
y: (dip.1 * scale).round() as i32,
};
unsafe {
let _ = ClientToScreen(hwnd, &mut pt);
}
LPARAM((((pt.y as u16 as u32) << 16) | (pt.x as u16 as u32)) as i32 as isize)
}
#[test]
#[allow(clippy::print_stderr)]
fn real_wm_nchittest_excludes_band_widgets_and_caps_the_empty_strip() {
let targets = Arc::new(Mutex::new(ProbeTargets::default()));
let app = LiveWindowApp {
targets: targets.clone(),
};
let title = format!(
"quadraui-nchittest-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let config = crate::win::run::RunConfig::new(title.clone());
let runner = std::thread::spawn(move || run(app, config));
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping \
real_wm_nchittest_excludes_band_widgets_and_caps_the_empty_strip — \
the test window never became findable within 10s (no interactive \
window station on this host?)"
);
return;
};
let ready = loop {
let snapshot = *targets.lock().unwrap();
if snapshot.file_item.is_some()
&& snapshot.close_button.is_some()
&& snapshot.search_box.is_some()
{
break Some(snapshot);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(snapshot) = ready else {
eprintln!(
"quadraui: skipping \
real_wm_nchittest_excludes_band_widgets_and_caps_the_empty_strip — \
the window never painted a full frame within 10s"
);
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
return;
};
let assert_hit = |label: &str, dip: (f32, f32), expect_caption: bool| {
let lparam = dip_to_screen_lparam(hwnd, dip, snapshot.scale);
let result = {
unsafe { SendMessageW(hwnd, WM_NCHITTEST, Some(WPARAM(0)), Some(lparam)) }
};
let is_caption = result.0 as u32 == HTCAPTION;
assert_eq!(
is_caption,
expect_caption,
"{label}: expected {} but WM_NCHITTEST returned {:#x}",
if expect_caption {
"HTCAPTION"
} else {
"not HTCAPTION (HTCLIENT)"
},
result.0
);
};
assert_hit("File menu item", snapshot.file_item.unwrap(), false);
assert_hit(
"status-bar close segment",
snapshot.close_button.unwrap(),
false,
);
assert_hit(
"command-centre search box",
snapshot.search_box.unwrap(),
false,
);
assert_hit("empty band strip", snapshot.empty_band, true);
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
}
struct ShellProbeTargets {
file_item: Option<(f32, f32)>,
scale: f32,
}
struct LiveShellMenuApp {
menu_system: crate::MenuSystem,
targets: Arc<Mutex<ShellProbeTargets>>,
}
impl crate::ShellApp for LiveShellMenuApp {
fn render_content(&self, backend: &mut dyn Backend, layout: &crate::AppShellLayout) {
let Some(bar_rect) = layout.title_bar_bounds else {
return;
};
self.menu_system.render(backend, bar_rect);
let bar = self.menu_system.menu_bar();
let menu_layout = backend.menu_bar_layout(bar_rect, &bar);
let file_item = menu_layout
.visible_items
.iter()
.find(|v| v.clickable)
.map(|v| {
(
v.bounds.x + v.bounds.width / 2.0,
v.bounds.y + v.bounds.height / 2.0,
)
});
let mut targets = self.targets.lock().unwrap();
targets.file_item = file_item;
targets.scale = backend.viewport().scale;
}
fn handle(
&mut self,
event: UiEvent,
backend: &mut dyn Backend,
ctx: &crate::ShellContext,
) -> Reaction {
if let UiEvent::WindowClose = event {
return Reaction::Exit;
}
let Some(bar_rect) = ctx.title_bar_bounds() else {
return Reaction::Continue;
};
match self.menu_system.handle(&event, backend, bar_rect) {
crate::MenuEvent::Ignored => Reaction::Continue,
_ => Reaction::Redraw,
}
}
}
#[test]
#[allow(clippy::print_stderr)]
fn real_wm_nchittest_reaches_the_file_menu_through_a_composed_shell() {
let targets = Arc::new(Mutex::new(ShellProbeTargets {
file_item: None,
scale: 1.0,
}));
let app = LiveShellMenuApp {
menu_system: crate::MenuSystem::new(vec![crate::MenuDef {
id: crate::types::WidgetId::new("probe:file"),
label: "&File".into(),
disabled: false,
items: vec![crate::ContextMenuItem {
id: Some(crate::types::WidgetId::new("probe:new")),
label: crate::StyledText::plain("New File"),
..Default::default()
}],
}]),
targets: targets.clone(),
};
let title = format!(
"quadraui-shell-nchittest-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let config = crate::ShellConfig::new(
title.clone(),
vec![crate::PanelDefinition {
id: crate::types::WidgetId::new("probe:panel"),
icon: "E".into(),
tooltip: "Explorer".into(),
title: "EXPLORER".into(),
}],
)
.with_title_bar(1.0);
let runner =
std::thread::spawn(move || crate::win::shell_runner::run_with_shell(app, config));
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping \
real_wm_nchittest_reaches_the_file_menu_through_a_composed_shell — \
the test window never became findable within 10s (no interactive \
window station on this host?)"
);
return;
};
let ready = loop {
let snapshot_file = targets.lock().unwrap().file_item;
if let Some(file_item) = snapshot_file {
break Some(file_item);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(file_item) = ready else {
eprintln!(
"quadraui: skipping \
real_wm_nchittest_reaches_the_file_menu_through_a_composed_shell — \
the window never painted a full frame within 10s"
);
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
return;
};
let scale = targets.lock().unwrap().scale;
let lparam = dip_to_screen_lparam(hwnd, file_item, scale);
let result = {
unsafe { SendMessageW(hwnd, WM_NCHITTEST, Some(WPARAM(0)), Some(lparam)) }
};
let is_caption = result.0 as u32 == HTCAPTION;
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
assert!(
!is_caption,
"File menu item (through AppShell + MenuSystem): expected \
not HTCAPTION (HTCLIENT) but WM_NCHITTEST returned {:#x} — \
a real click here would be swallowed as a title-bar drag \
instead of opening the dropdown (quadraui#1307)",
result.0
);
}
}
#[cfg(test)]
mod idle_poll_fallback_tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
struct IdlePollApp {
tick_count: Arc<AtomicUsize>,
}
impl AppLogic for IdlePollApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::WindowClose => Reaction::Exit,
_ => Reaction::Continue,
}
}
fn tick(&mut self, _backend: &mut dyn Backend) -> Reaction {
self.tick_count.fetch_add(1, Ordering::SeqCst);
Reaction::Continue
}
}
#[test]
#[allow(clippy::print_stderr)]
fn idle_poll_ceiling_fires_tick_with_no_explicit_rearm() {
let tick_count = Arc::new(AtomicUsize::new(0));
let app = IdlePollApp {
tick_count: tick_count.clone(),
};
let title = format!(
"quadraui-idle-poll-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let config = crate::win::run::RunConfig::new(title.clone());
let runner = std::thread::spawn(move || run(app, config));
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping \
idle_poll_ceiling_fires_tick_with_no_explicit_rearm — \
the test window never became findable within 10s (no \
interactive window station on this host?)"
);
return;
};
let tick_deadline = Instant::now() + Duration::from_secs(5);
let fired = loop {
if tick_count.load(Ordering::SeqCst) > 0 {
break true;
}
if Instant::now() >= tick_deadline {
break false;
}
std::thread::sleep(Duration::from_millis(20));
};
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
assert!(
fired,
"AppLogic::tick was never called within 5s of the window \
appearing, with no explicit RedrawAfter/request_frame_in — \
the IDLE_POLL_TIMER_ID safety net (quadraui#1265) isn't \
firing"
);
}
}
#[cfg(test)]
mod mouse_and_key_exit_tests {
use super::*;
use std::time::{Duration, Instant};
struct ExitOnInputApp;
impl AppLogic for ExitOnInputApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::MouseUp { .. } | UiEvent::KeyPressed { .. } | UiEvent::WindowClose => {
Reaction::Exit
}
_ => Reaction::Continue,
}
}
}
#[allow(clippy::print_stderr)]
fn assert_exit_reaction_destroys_window(test_name: &str, trigger: impl FnOnce(HWND)) {
let title = format!(
"quadraui-exit-reaction-probe-{test_name}-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let config = crate::win::run::RunConfig::new(title.clone());
let runner = std::thread::spawn(move || run(ExitOnInputApp, config));
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping {test_name} — the test window never \
became findable within 10s (no interactive window \
station on this host?)"
);
return;
};
trigger(hwnd);
let exit_deadline = Instant::now() + Duration::from_secs(5);
let destroyed = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
let still_there = found.is_ok();
if !still_there {
break true;
}
if Instant::now() >= exit_deadline {
break false;
}
std::thread::sleep(Duration::from_millis(20));
};
if destroyed {
let _ = runner.join();
} else {
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
}
assert!(
destroyed,
"{test_name}: window was never destroyed within 5s of an \
app `handle` returning Reaction::Exit — the reaction was \
dropped (quadraui#1278)"
);
}
#[test]
fn mouse_up_exit_reaction_destroys_window() {
assert_exit_reaction_destroys_window(
"mouse_up_exit_reaction_destroys_window",
|hwnd| {
let lparam = LPARAM((20i32 << 16 | 20i32) as isize);
unsafe {
SendMessageW(hwnd, WM_LBUTTONDOWN, Some(WPARAM(0)), Some(lparam));
SendMessageW(hwnd, WM_LBUTTONUP, Some(WPARAM(0)), Some(lparam));
}
},
);
}
#[test]
fn key_down_exit_reaction_destroys_window() {
assert_exit_reaction_destroys_window(
"key_down_exit_reaction_destroys_window",
|hwnd| {
const VK_ESCAPE: usize = 0x1B;
unsafe {
SendMessageW(hwnd, WM_KEYDOWN, Some(WPARAM(VK_ESCAPE)), Some(LPARAM(0)));
}
},
);
}
}
#[cfg(test)]
mod first_paint_before_show_tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use windows::Win32::UI::WindowsAndMessaging::IsWindowVisible;
struct PaintFlagApp {
painted: Arc<AtomicBool>,
}
impl AppLogic for PaintFlagApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {
self.painted.store(true, Ordering::SeqCst);
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::WindowClose => Reaction::Exit,
_ => Reaction::Continue,
}
}
}
#[test]
#[allow(clippy::print_stderr)]
fn window_is_never_visible_before_its_first_frame_has_painted() {
let painted = Arc::new(AtomicBool::new(false));
let app = PaintFlagApp {
painted: painted.clone(),
};
let title = format!(
"quadraui-first-paint-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let config = crate::win::run::RunConfig::new(title.clone());
let runner = std::thread::spawn(move || run(app, config));
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let mut observed: Option<(HWND, bool)> = None;
loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
if unsafe { IsWindowVisible(hwnd) }.as_bool() {
observed = Some((hwnd, painted.load(Ordering::SeqCst)));
break;
}
}
if Instant::now() >= deadline {
break;
}
}
let Some((hwnd, painted_before_visible)) = observed else {
eprintln!(
"quadraui: skipping \
window_is_never_visible_before_its_first_frame_has_painted — \
the test window never became visible within 10s (no \
interactive window station on this host?)"
);
return;
};
assert!(
painted_before_visible,
"the window reported `IsWindowVisible` before \
`AppLogic::render` ever ran — the swap chain's backbuffer \
had nothing real painted into it the instant DWM could \
first composite it, reopening issue #1263's white-flash \
gap"
);
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner.join();
}
}
#[cfg(test)]
mod winrunner_step_pump_tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
struct StepPumpApp {
painted: Arc<AtomicBool>,
closed: Arc<AtomicBool>,
}
impl AppLogic for StepPumpApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {
self.painted.store(true, Ordering::SeqCst);
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
if matches!(event, UiEvent::WindowClose) {
self.closed.store(true, Ordering::SeqCst);
return Reaction::Exit;
}
Reaction::Continue
}
}
#[test]
#[allow(clippy::print_stderr)]
fn step_then_pump_drive_app_without_run() {
let painted = Arc::new(AtomicBool::new(false));
let closed = Arc::new(AtomicBool::new(false));
let first_step_elapsed_ms = Arc::new(AtomicU64::new(u64::MAX));
let title = format!(
"quadraui-step-pump-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let painted_for_thread = painted.clone();
let closed_for_thread = closed.clone();
let first_step_elapsed_for_thread = first_step_elapsed_ms.clone();
let title_for_thread = title.clone();
let runner_thread = std::thread::spawn(move || {
let config = RunConfig::new(title_for_thread);
let app = StepPumpApp {
painted: painted_for_thread,
closed: closed_for_thread,
};
let mut runner = unsafe { WinRunner::new_with(app, &config) }
.expect("WinRunner::new_with failed");
let step_start = Instant::now();
let outcome = runner.step().expect("step() failed");
first_step_elapsed_for_thread
.store(step_start.elapsed().as_millis() as u64, Ordering::SeqCst);
debug_assert!(matches!(
outcome,
StepOutcome::Continue | StepOutcome::Exited
));
loop {
match runner
.pump(Duration::from_millis(50))
.expect("pump() failed")
{
StepOutcome::Continue => {}
StepOutcome::Exited => break,
}
}
});
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping step_then_pump_drive_app_without_run — \
the test window never became findable within 10s (no \
interactive window station on this host?)"
);
return;
};
let paint_deadline = Instant::now() + Duration::from_secs(5);
let saw_paint = loop {
if painted.load(Ordering::SeqCst) {
break true;
}
if Instant::now() >= paint_deadline {
break false;
}
std::thread::sleep(Duration::from_millis(20));
};
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner_thread.join();
assert!(
saw_paint,
"AppLogic::render never ran within 5s of a WinRunner stepped \
directly (not via run()) becoming findable"
);
assert!(
closed.load(Ordering::SeqCst),
"WM_CLOSE never reached AppLogic::handle as a dispatched \
UiEvent::WindowClose through a directly-stepped WinRunner"
);
let first_step_elapsed = first_step_elapsed_ms.load(Ordering::SeqCst);
assert!(
first_step_elapsed < 1000,
"WinRunner::step() took {first_step_elapsed}ms on its very \
first call with nothing queued — it must never block"
);
}
}
#[cfg(test)]
mod winrunner_waker_tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct Ping(u32);
struct WakerApp {
waker_slot: Arc<Mutex<Option<Arc<dyn Fn(crate::UserPayload) + Send + Sync>>>>,
got_ping: Arc<AtomicBool>,
}
impl AppLogic for WakerApp {
type AreaId = ();
fn setup(&mut self, backend: &mut dyn Backend) {
*self.waker_slot.lock().unwrap() = Some(backend.waker());
}
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::User(payload) => {
if let Some(ping) = payload.downcast_ref::<Ping>() {
if ping.0 == 42 {
self.got_ping.store(true, Ordering::SeqCst);
}
}
Reaction::Continue
}
UiEvent::WindowClose => Reaction::Exit,
_ => Reaction::Continue,
}
}
}
#[test]
#[allow(clippy::print_stderr)]
fn waker_wakes_a_blocked_pump_without_waiting_out_the_timeout() {
let waker_slot: Arc<Mutex<Option<Arc<dyn Fn(crate::UserPayload) + Send + Sync>>>> =
Arc::new(Mutex::new(None));
let got_ping = Arc::new(AtomicBool::new(false));
let title = format!(
"quadraui-waker-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let waker_slot_for_thread = waker_slot.clone();
let got_ping_for_thread = got_ping.clone();
let title_for_thread = title.clone();
let runner_thread = std::thread::spawn(move || {
let config = RunConfig::new(title_for_thread);
let app = WakerApp {
waker_slot: waker_slot_for_thread,
got_ping: got_ping_for_thread,
};
let mut runner = unsafe { WinRunner::new_with(app, &config) }
.expect("WinRunner::new_with failed");
loop {
match runner.pump(Duration::from_secs(5)).expect("pump() failed") {
StepOutcome::Continue => {}
StepOutcome::Exited => break,
}
}
});
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping \
waker_wakes_a_blocked_pump_without_waiting_out_the_timeout — \
the test window never became findable within 10s (no \
interactive window station on this host?)"
);
return;
};
let waker_deadline = Instant::now() + Duration::from_secs(5);
let waker = loop {
if let Some(w) = waker_slot.lock().unwrap().clone() {
break Some(w);
}
if Instant::now() >= waker_deadline {
break None;
}
std::thread::sleep(Duration::from_millis(10));
};
let Some(waker) = waker else {
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner_thread.join();
panic!(
"AppLogic::setup never captured backend.waker() within 5s \
of the window becoming findable"
);
};
(waker)(crate::UserPayload::new(Ping(42)));
let ping_deadline = Instant::now() + Duration::from_secs(3);
let saw_ping = loop {
if got_ping.load(Ordering::SeqCst) {
break true;
}
if Instant::now() >= ping_deadline {
break false;
}
std::thread::sleep(Duration::from_millis(10));
};
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner_thread.join();
assert!(
saw_ping,
"UiEvent::User(Ping(42)) from Backend::waker() never reached \
AppLogic::handle within 3s of the waker being called from \
another thread — the waker's wake round trip isn't waking a \
blocked WinRunner::pump()"
);
}
}
#[cfg(test)]
mod winrunner_redraw_after_tests {
use super::*;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
struct RedrawAfterApp {
delay: Duration,
armed: Arc<AtomicBool>,
arm_instant: Arc<Mutex<Option<Instant>>>,
tick_instants: Arc<Mutex<Vec<Instant>>>,
}
impl AppLogic for RedrawAfterApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
match event {
UiEvent::WindowClose => Reaction::Exit,
UiEvent::MouseMoved { .. } => {
if self.armed.swap(true, Ordering::SeqCst) {
Reaction::Continue
} else {
*self.arm_instant.lock().unwrap() = Some(Instant::now());
Reaction::RedrawAfter(self.delay)
}
}
_ => Reaction::Continue,
}
}
fn tick(&mut self, _backend: &mut dyn Backend) -> Reaction {
self.tick_instants.lock().unwrap().push(Instant::now());
Reaction::Continue
}
}
#[test]
#[allow(clippy::print_stderr)]
fn redraw_after_wakes_exactly_once_near_its_deadline() {
let delay = Duration::from_millis(60);
let armed = Arc::new(AtomicBool::new(false));
let arm_instant: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
let tick_instants: Arc<Mutex<Vec<Instant>>> = Arc::new(Mutex::new(Vec::new()));
let title = format!(
"quadraui-redraw-after-probe-{}-{:?}",
std::process::id(),
std::thread::current().id()
);
let armed_for_thread = armed.clone();
let arm_instant_for_thread = arm_instant.clone();
let tick_instants_for_thread = tick_instants.clone();
let title_for_thread = title.clone();
let runner_thread = std::thread::spawn(move || {
let config = RunConfig::new(title_for_thread);
let app = RedrawAfterApp {
delay,
armed: armed_for_thread,
arm_instant: arm_instant_for_thread,
tick_instants: tick_instants_for_thread,
};
let mut runner = unsafe { WinRunner::new_with(app, &config) }
.expect("WinRunner::new_with failed");
loop {
match runner
.pump(Duration::from_millis(50))
.expect("pump() failed")
{
StepOutcome::Continue => {}
StepOutcome::Exited => break,
}
}
});
let deadline = Instant::now() + Duration::from_secs(10);
let class_name = window_class_name(&title);
let hwnd = loop {
let found = unsafe { FindWindowW(PCWSTR(class_name.as_ptr()), PCWSTR::null()) };
if let Ok(hwnd) = found {
break Some(hwnd);
}
if Instant::now() >= deadline {
break None;
}
std::thread::sleep(Duration::from_millis(20));
};
let Some(hwnd) = hwnd else {
eprintln!(
"quadraui: skipping \
redraw_after_wakes_exactly_once_near_its_deadline — the \
test window never became findable within 10s (no \
interactive window station on this host?)"
);
return;
};
let lparam = LPARAM((30i32 << 16 | 30i32) as isize);
unsafe {
SendMessageW(hwnd, WM_MOUSEMOVE, Some(WPARAM(0)), Some(lparam));
}
std::thread::sleep(Duration::from_millis(180));
unsafe {
SendMessageW(hwnd, WM_CLOSE, Some(WPARAM(0)), Some(LPARAM(0)));
}
let _ = runner_thread.join();
let armed_at = arm_instant
.lock()
.unwrap()
.expect("RedrawAfterApp::handle never saw the WM_MOUSEMOVE as MouseMoved");
let ticks = tick_instants.lock().unwrap();
let near_deadline: Vec<Duration> = ticks
.iter()
.filter(|t| **t >= armed_at)
.map(|t| t.duration_since(armed_at))
.filter(|elapsed| *elapsed < Duration::from_millis(230))
.collect();
assert_eq!(
near_deadline.len(),
1,
"expected exactly one AppLogic::tick within 230ms of arming \
Reaction::RedrawAfter({delay:?}), saw {near_deadline:?} — \
RedrawAfter is either dropping the wake entirely or firing \
it more than once"
);
assert!(
near_deadline[0] >= delay.saturating_sub(Duration::from_millis(20)),
"RedrawAfter's wake fired after only {:?}, well before its \
{delay:?} deadline",
near_deadline[0]
);
}
}
}
#[cfg(target_os = "windows")]
pub use win32::WinRunner;
#[cfg(target_os = "windows")]
pub fn run<A: AppLogic + 'static>(app: A) -> std::process::ExitCode {
win32::run(app, RunConfig::default())
}
#[cfg(target_os = "windows")]
pub fn run_with<A: AppLogic + 'static>(app: A, config: RunConfig) -> std::process::ExitCode {
win32::run(app, config)
}
#[cfg(not(target_os = "windows"))]
pub fn run<A: AppLogic + 'static>(_app: A) -> std::process::ExitCode {
todo!(
"Win32 message loop: RegisterClassEx, CreateWindowEx, \
Direct2D render target, translate WM_* → UiEvent, \
dispatch to app.handle(), redraw via app.render()"
)
}
#[cfg(not(target_os = "windows"))]
pub fn run_with<A: AppLogic + 'static>(_app: A, _config: RunConfig) -> std::process::ExitCode {
todo!(
"Win32 message loop: RegisterClassEx, CreateWindowEx, \
Direct2D render target, translate WM_* → UiEvent, \
dispatch to app.handle(), redraw via app.render() — with a \
custom window title from RunConfig"
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
#[test]
fn every_non_test_todo_in_this_file_is_a_cfg_not_windows_stand_in() {
let src = include_str!("run.rs");
let test_mod_start = src
.find("\n#[cfg(test)]\nmod tests {")
.expect("this file must still have its `mod tests` block");
let non_test_src = &src[..test_mod_start];
let lines: Vec<&str> = non_test_src.lines().collect();
let mut todo_count = 0;
for (idx, line) in lines.iter().enumerate() {
if !line.trim_start().starts_with("todo!(") {
continue;
}
todo_count += 1;
let guard = lines[..idx]
.iter()
.rev()
.find(|l| l.trim_start().starts_with("#[cfg("))
.unwrap_or_else(|| {
panic!(
"todo!() at non-test line {} has no preceding #[cfg(...)] guard at all",
idx + 1
)
});
assert_eq!(
guard.trim(),
r#"#[cfg(not(target_os = "windows"))]"#,
"todo!() at non-test line {} must be guarded by \
#[cfg(not(target_os = \"windows\"))], found {:?} instead — \
a todo!() reachable on a real Windows build must become a \
real BackendError::Unsupported instead (#1131)",
idx + 1,
guard.trim()
);
}
assert_eq!(
todo_count, 2,
"expected exactly 2 non-test todo!() call sites in win/run.rs \
(run/run_with's non-Windows stand-ins) — if this changed, \
re-verify #1131's claim and update this test's expectation"
);
}
#[test]
fn new_sets_the_title() {
let config = RunConfig::new("kubeui");
assert_eq!(config.title, "kubeui");
assert!(
!config.single_instance,
"single-instance forwarding defaults off — see RunConfig::single_instance's doc"
);
}
#[test]
fn new_accepts_owned_and_borrowed_strings() {
assert_eq!(RunConfig::new("borrowed").title, "borrowed");
assert_eq!(RunConfig::new(String::from("owned")).title, "owned");
}
#[test]
fn default_matches_runs_previously_hardcoded_title() {
assert_eq!(RunConfig::default().title, "quadraui");
assert!(!RunConfig::default().single_instance);
}
#[test]
fn with_single_instance_overrides_the_default() {
let config = RunConfig::new("kubeui").with_single_instance(true);
assert!(config.single_instance);
}
#[test]
fn new_and_default_set_no_app_icon() {
assert_eq!(RunConfig::new("kubeui").app_icon, None);
assert_eq!(RunConfig::default().app_icon, None);
}
#[test]
fn with_app_icon_sets_the_source() {
let config = RunConfig::new("kubeui").with_app_icon(ImageSource::Bytes(vec![0xde, 0xad]));
assert_eq!(config.app_icon, Some(ImageSource::Bytes(vec![0xde, 0xad])));
}
#[test]
fn copydata_argv_round_trips() {
let args = vec!["file.txt".to_string(), "--flag".to_string()];
let payload = encode_copydata_argv(&args);
assert_eq!(decode_copydata_argv(&payload), args);
}
#[test]
fn copydata_argv_survives_embedded_newline() {
let args = vec!["weird\npath.txt".to_string(), "second.txt".to_string()];
let payload = encode_copydata_argv(&args);
assert_eq!(decode_copydata_argv(&payload), args);
}
#[test]
fn decode_copydata_argv_ignores_trailing_terminator() {
let payload = format!("{}\0", encode_copydata_argv(&["a.txt".to_string()]));
assert_eq!(decode_copydata_argv(&payload), vec!["a.txt".to_string()]);
}
#[test]
fn decode_copydata_argv_of_empty_payload_is_empty() {
assert!(decode_copydata_argv("").is_empty());
assert!(decode_copydata_argv("\0").is_empty());
}
#[test]
fn instance_identity_differs_across_titles() {
assert_ne!(instance_identity("kubeui"), instance_identity("vimcode"));
}
#[test]
fn instance_identity_is_deterministic() {
assert_eq!(instance_identity("kubeui"), instance_identity("kubeui"));
}
#[test]
fn window_class_name_string_embeds_instance_identity() {
let title = "kubeui";
let expected = format!("{CLASS_NAME_PREFIX}-{:016x}", instance_identity(title));
assert_eq!(window_class_name_string(title), expected);
}
#[test]
fn window_class_name_string_differs_across_titles() {
assert_ne!(
window_class_name_string("kubeui"),
window_class_name_string("vimcode")
);
}
#[test]
fn reentrant_call_is_skipped_not_double_borrowed() {
let state = RefCell::new(0i32);
let depth = ModalPumpDepth::new();
let outer = guarded_call(&state, &depth, |v| {
*v += 1;
let inner = guarded_call(&state, &depth, |v2| {
*v2 += 100;
*v2
});
assert_eq!(
inner, None,
"a reentrant guarded_call must be skipped, not run"
);
*v
});
assert_eq!(
outer,
Some(1),
"the outer call must still complete normally"
);
assert_eq!(
*state.borrow(),
1,
"the skipped reentrant call must not have mutated state"
);
}
#[test]
fn reentrant_message_during_a_direct_borrow_is_deferred_not_double_borrowed() {
let state = RefCell::new(0i32);
let depth = ModalPumpDepth::new();
let fake_wndproc_arm = |state: &RefCell<i32>, depth: &ModalPumpDepth| -> Option<i32> {
if depth.is_pumping() {
return None;
}
let mut v = state.borrow_mut();
*v += 100;
Some(*v)
};
let outer_result = {
let _guard = ModalPumpGuard::new(&depth);
let mut s = state.borrow_mut();
*s += 1;
let nested = fake_wndproc_arm(&state, &depth);
assert_eq!(
nested, None,
"a nested message arriving while the outer direct borrow is \
live must be deferred, never touch `state` a second time"
);
*s
};
assert_eq!(
outer_result, 1,
"the outer direct-borrow call must still complete normally"
);
assert!(
!depth.is_pumping(),
"depth must return to 0 once the guard around the outer call drops"
);
assert_eq!(fake_wndproc_arm(&state, &depth), Some(101));
}
#[test]
fn ceded_resize_is_replayed_once_the_guard_releases() {
let depth = ModalPumpDepth::new();
let pending_resize_resync = Cell::new(false);
let surface_size = Cell::new((800, 600));
let resized_dispatched = Cell::new(false);
let fake_wndproc = |is_size: bool, current_size: (i32, i32)| {
if depth.is_pumping() {
if is_size {
pending_resize_resync.set(true);
}
return; }
if pending_resize_resync.replace(false) {
surface_size.set(current_size);
resized_dispatched.set(true);
}
if is_size {
surface_size.set(current_size);
resized_dispatched.set(true);
}
};
{
let _guard = ModalPumpGuard::new(&depth);
fake_wndproc(true, (1920, 1080));
assert_eq!(
surface_size.get(),
(800, 600),
"a WM_SIZE arriving while the guard is held must not touch \
the surface yet — only the next non-reentrant call may"
);
assert!(
!resized_dispatched.get(),
"must not dispatch WindowResized while the guard is held"
);
}
assert!(
!depth.is_pumping(),
"depth must return to 0 once the guard around the click handler drops"
);
fake_wndproc(false, (1920, 1080));
assert_eq!(
surface_size.get(),
(1920, 1080),
"the resize ceded during the guarded call must be replayed, \
not dropped, once the guard releases"
);
assert!(
resized_dispatched.get(),
"the app must still learn the window was resized"
);
}
#[test]
fn sequential_non_reentrant_calls_both_run() {
let state = RefCell::new(0i32);
let depth = ModalPumpDepth::new();
assert_eq!(
guarded_call(&state, &depth, |v| {
*v += 1;
*v
}),
Some(1)
);
assert_eq!(
guarded_call(&state, &depth, |v| {
*v += 1;
*v
}),
Some(2)
);
assert!(
!depth.is_pumping(),
"depth must return to 0 once each guarded call completes"
);
}
}
#[cfg(test)]
mod paste_dispatch_tests {
use super::*;
use crate::runner::Reaction;
use crate::{Key, Modifiers, UiEvent};
#[cfg(target_os = "windows")]
const PASTE_PAYLOAD: &str = "quadraui#728 ctrl-v round trip";
#[derive(Default)]
struct RecordingApp {
events: Vec<UiEvent>,
}
impl AppLogic for RecordingApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.events.push(event);
Reaction::Continue
}
}
fn ctrl_v() -> UiEvent {
UiEvent::KeyPressed {
key: Key::Char('v'),
modifiers: Modifiers {
ctrl: true,
shift: false,
alt: false,
cmd: false,
},
repeat: false,
}
}
#[test]
fn ctrl_v_is_never_forwarded_to_the_app_as_a_raw_keypress() {
let _clipboard = crate::win::lock_real_clipboard();
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let _ = dispatch_event(ctrl_v(), &mut backend, &mut app);
assert!(
app.events
.iter()
.all(|e| matches!(e, UiEvent::ClipboardPaste(_))),
"once is_paste_keypress recognises the chord, the raw Ctrl-V \
KeyPressed must never reach the app — the only thing that may \
is a ClipboardPaste; got {:?}",
app.events
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn ctrl_v_with_nothing_to_paste_is_consumed_not_forwarded() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let outcome = dispatch_event(ctrl_v(), &mut backend, &mut app);
assert!(
matches!(outcome, EventOutcome::Continue),
"an intercepted paste keypress with an empty clipboard must \
resolve to Continue, not fall through to app.handle"
);
assert!(
app.events.is_empty(),
"the raw Ctrl-V KeyPressed must never reach the app once \
is_paste_keypress recognises it — got {:?}",
app.events
);
}
#[cfg(target_os = "windows")]
#[test]
#[allow(clippy::print_stderr)]
fn ctrl_v_delivers_real_clipboard_text_as_clipboard_paste() {
let _clipboard = crate::win::lock_real_clipboard();
let mut backend = WinBackend::new();
backend.services().clipboard().write_text(PASTE_PAYLOAD);
let seeded = backend.services().clipboard().read_text();
if seeded.as_deref() != Some(PASTE_PAYLOAD) {
eprintln!(
"quadraui: skipping ctrl_v_delivers_real_clipboard_text_as_clipboard_paste — \
this host's clipboard is not usable from the test process (wrote \
{PASTE_PAYLOAD:?}, read back {seeded:?})"
);
return;
}
let mut app = RecordingApp::default();
let _ = dispatch_event(ctrl_v(), &mut backend, &mut app);
assert_eq!(
app.events,
vec![UiEvent::ClipboardPaste(PASTE_PAYLOAD.to_string())],
"Ctrl-V with text on the clipboard must deliver exactly one \
ClipboardPaste carrying that text, and no raw KeyPressed"
);
}
#[test]
fn non_paste_keypress_still_reaches_the_app() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let ev = UiEvent::KeyPressed {
key: Key::Char('x'),
modifiers: Modifiers::default(),
repeat: false,
};
let _ = dispatch_event(ev.clone(), &mut backend, &mut app);
assert_eq!(
app.events,
vec![ev],
"a non-paste keypress must fall through to app.handle unchanged"
);
}
}
#[cfg(test)]
mod ctrl_accelerator_dispatch_tests_1674 {
use super::*;
use crate::runner::Reaction;
use crate::{Accelerator, AcceleratorId, AcceleratorScope, KeyBinding, Modifiers, UiEvent};
#[derive(Default)]
struct RecordingApp {
events: Vec<UiEvent>,
}
impl AppLogic for RecordingApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.events.push(event);
Reaction::Continue
}
}
fn global_accelerator(id: &str, binding: &str) -> Accelerator {
Accelerator {
id: AcceleratorId::new(id),
binding: KeyBinding::Literal(binding.to_string()),
scope: AcceleratorScope::Global,
label: None,
}
}
#[test]
fn ctrl_b_accelerator_reaches_the_app_via_dispatch_event() {
let mut backend = WinBackend::new();
backend.register_accelerator(&global_accelerator("toggle-sidebar", "<C-b>"));
let modifiers = Modifiers {
ctrl: true,
..Default::default()
};
let event = crate::win::events::wm_char_to_uievent('\u{0002}', modifiers, false)
.expect("Ctrl+B's WM_CHAR payload must translate to a KeyPressed event");
let mut app = RecordingApp::default();
let _ = dispatch_event(event, &mut backend, &mut app);
assert_eq!(
app.events,
vec![UiEvent::Accelerator(
AcceleratorId::new("toggle-sidebar"),
modifiers
)],
"a registered Ctrl+B accelerator must reach the app as \
UiEvent::Accelerator once WM_CHAR's '\\x02' payload is \
translated — got {:?}",
app.events
);
}
#[test]
fn ctrl_backtick_accelerator_reaches_the_app_via_dispatch_event() {
let mut backend = WinBackend::new();
backend.register_accelerator(&global_accelerator("toggle-terminal", "<C-`>"));
let modifiers = Modifiers {
ctrl: true,
..Default::default()
};
let event = crate::win::events::wm_char_to_uievent('`', modifiers, false)
.expect("Ctrl+`'s WM_CHAR payload must translate to a KeyPressed event");
let mut app = RecordingApp::default();
let _ = dispatch_event(event, &mut backend, &mut app);
assert_eq!(
app.events,
vec![UiEvent::Accelerator(
AcceleratorId::new("toggle-terminal"),
modifiers
)],
"a registered Ctrl+` accelerator must reach the app as \
UiEvent::Accelerator once WM_CHAR's literal backtick payload \
is translated with ctrl held — got {:?}",
app.events
);
}
}
#[cfg(test)]
mod text_selection_dispatch_tests {
use super::*;
use crate::dispatch::TextRegion;
use crate::runner::Reaction;
use crate::types::WidgetId;
use crate::{ButtonMask, Key, Modifiers, MouseButton, UiEvent};
#[derive(Default)]
struct RecordingApp {
events: Vec<UiEvent>,
}
impl AppLogic for RecordingApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.events.push(event);
Reaction::Continue
}
}
fn region(id: &str, x: f32, y: f32, w: f32, h: f32, lines: &[&str]) -> TextRegion {
TextRegion {
id: WidgetId::new(id),
bounds: crate::event::Rect::new(x, y, w, h),
lines: lines.iter().map(|s| s.to_string()).collect(),
}
}
fn ctrl_char(c: char) -> UiEvent {
UiEvent::KeyPressed {
key: Key::Char(c),
modifiers: Modifiers {
ctrl: true,
shift: false,
alt: false,
cmd: false,
},
repeat: false,
}
}
#[test]
fn drag_select_then_ctrl_c_copies_the_selection() {
let _clipboard = crate::win::lock_real_clipboard();
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
backend.register_text_region(region("body", 0.0, 0.0, 88.0, 16.0, &["hello world"]));
let outcome = route_mouse_down(
&mut backend,
&mut app,
crate::event::Point::new(0.0, 4.0),
MouseButton::Left,
Modifiers::default(),
);
assert!(matches!(
outcome,
EventOutcome::Continue | EventOutcome::Redraw
));
let outcome = route_mouse_move(
&mut backend,
&mut app,
crate::event::Point::new(88.0, 4.0),
ButtonMask {
left: true,
..ButtonMask::default()
},
);
assert!(
matches!(outcome, EventOutcome::Redraw),
"a TextSelectionChanged event must force a redraw"
);
assert!(
backend.active_text_selection().is_some(),
"dragging across a registered TextRegion must produce an active selection"
);
let _ = route_mouse_up(
&mut backend,
&mut app,
crate::event::Point::new(88.0, 4.0),
MouseButton::Left,
);
assert!(
backend.active_text_selection().is_some(),
"MouseUp must not clear the finalised selection"
);
let outcome = dispatch_event(ctrl_char('c'), &mut backend, &mut app);
assert!(matches!(
outcome,
EventOutcome::Redraw | EventOutcome::Continue
));
assert!(
backend.active_text_selection().is_none(),
"Ctrl-C must clear the selection after copying it"
);
assert!(
app.events
.iter()
.any(|e| matches!(e, UiEvent::TextCopied(text) if text == "hello world")),
"Ctrl-C over a full-region selection must deliver TextCopied(\"hello world\"); \
got {:?}",
app.events
);
}
#[test]
fn ctrl_c_without_a_selection_falls_through_to_the_app() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let ev = ctrl_char('c');
let _ = dispatch_event(ev.clone(), &mut backend, &mut app);
assert_eq!(
app.events,
vec![ev],
"Ctrl-C with no active selection must fall through to app.handle unchanged, \
matching gtk::run::dispatch_event/TuiBackend::apply_dispatch"
);
}
#[test]
fn ctrl_a_selects_the_sole_registered_region() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
backend.register_text_region(region("body", 0.0, 0.0, 40.0, 16.0, &["hi"]));
let outcome = dispatch_event(ctrl_char('a'), &mut backend, &mut app);
assert!(matches!(outcome, EventOutcome::Redraw));
assert!(
backend.active_text_selection().is_some(),
"Ctrl-A must select the sole registered TextRegion"
);
assert!(
app.events.is_empty(),
"a recognised Ctrl-A must not also forward the raw KeyPressed to the app"
);
}
#[test]
fn ctrl_a_with_no_regions_falls_through_to_the_app() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let ev = ctrl_char('a');
let _ = dispatch_event(ev.clone(), &mut backend, &mut app);
assert_eq!(
app.events,
vec![ev],
"Ctrl-A with no registered TextRegion must fall through to app.handle unchanged"
);
}
#[test]
fn mouse_down_clears_the_displayed_selection() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
backend.set_active_text_selection(
WidgetId::new("r"),
crate::event::Point::new(0.0, 0.0),
crate::event::Point::new(10.0, 0.0),
);
assert!(backend.active_text_selection().is_some());
let ev = UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: crate::event::Point::new(0.0, 0.0),
modifiers: Modifiers::default(),
};
let _ = dispatch_event(ev, &mut backend, &mut app);
assert!(
backend.active_text_selection().is_none(),
"a MouseDown reaching dispatch_event must clear the displayed selection"
);
}
#[test]
fn text_selection_changed_updates_active_selection_and_forces_redraw() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
backend.register_text_region(region("body", 0.0, 0.0, 40.0, 16.0, &["hi there"]));
let ev = UiEvent::TextSelectionChanged {
region: WidgetId::new("body"),
anchor: crate::event::Point::new(0.0, 0.0),
focus: crate::event::Point::new(40.0, 0.0),
};
let outcome = dispatch_event(ev, &mut backend, &mut app);
assert!(matches!(outcome, EventOutcome::Redraw));
assert!(backend.active_text_selection().is_some());
}
#[test]
fn middle_click_falls_through_to_the_app_no_primary_selection_on_windows() {
let mut backend = WinBackend::new();
let mut app = RecordingApp::default();
let ev = UiEvent::MouseDown {
widget: None,
button: MouseButton::Middle,
position: crate::event::Point::new(5.0, 5.0),
modifiers: Modifiers::default(),
};
let _ = dispatch_event(ev.clone(), &mut backend, &mut app);
assert_eq!(
app.events,
vec![ev],
"middle-click must fall through to app.handle unchanged when there is no \
PRIMARY selection to paste, matching gtk::run's identical fallthrough branch"
);
}
}
#[cfg(test)]
mod panic_guard_tests {
use super::*;
use crate::runner::Reaction;
use crate::{Key, Modifiers, UiEvent};
struct PanicsOnHandle;
impl AppLogic for PanicsOnHandle {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
panic!("PanicsOnHandle: deliberate panic for #922 coverage");
}
}
fn some_key() -> UiEvent {
UiEvent::KeyPressed {
key: Key::Char('z'),
modifiers: Modifiers::default(),
repeat: false,
}
}
#[test]
fn a_panicking_handle_does_not_unwind_past_dispatch_event() {
let mut backend = WinBackend::new();
let mut app = PanicsOnHandle;
let outcome = dispatch_event(some_key(), &mut backend, &mut app);
assert!(
matches!(outcome, EventOutcome::Continue),
"a caught panic in app.handle must resolve to Continue (no \
redraw, no exit), matching wndproc's fallthrough for every \
message it doesn't otherwise handle"
);
}
#[test]
fn a_panicking_handle_keeps_working_across_repeated_frames() {
let mut backend = WinBackend::new();
let mut app = PanicsOnHandle;
for _ in 0..5 {
let outcome = dispatch_event(some_key(), &mut backend, &mut app);
assert!(matches!(outcome, EventOutcome::Continue));
}
}
#[test]
fn a_non_panicking_handle_is_unaffected_by_the_guard() {
struct RecordingApp {
seen: bool,
}
impl AppLogic for RecordingApp {
type AreaId = ();
fn render(&self, _backend: &mut dyn Backend, _area: ()) {}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
self.seen = true;
Reaction::Continue
}
}
let mut backend = WinBackend::new();
let mut app = RecordingApp { seen: false };
let _ = dispatch_event(some_key(), &mut backend, &mut app);
assert!(
app.seen,
"catch_unwind must not swallow or alter a non-panicking call"
);
}
}