#[cfg(windows)]
pub mod win32;
#[cfg(windows)]
pub use win32::clipboard::WinClipboard as Clipboard;
#[cfg(windows)]
pub(crate) use win32::run;
#[cfg(windows)]
pub use win32::{open_url, Tray, TrayCtx, TrayMenuItem};
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(target_os = "macos")]
pub use macos::clipboard::MacClipboard as Clipboard;
#[cfg(target_os = "macos")]
pub(crate) use macos::run;
#[cfg(target_os = "macos")]
pub use macos::{open_url, Tray, TrayCtx, TrayMenuItem};
#[cfg(not(any(windows, target_os = "macos")))]
compile_error!("windui 目前仅支持 Windows 与 macOS 平台");
use std::cell::Cell;
use std::path::Path;
use std::path::PathBuf;
use tiny_skia::Pixmap;
use crate::event::{CursorShape, KeyEvent, MouseButton, PointerEvent, PointerKind, WindowOp};
use crate::geometry::{Color, Point, Size};
thread_local! {
static IN_EVENT_DISPATCH: Cell<bool> = const { Cell::new(false) };
}
pub(crate) struct EventDispatchGuard(());
impl EventDispatchGuard {
pub(crate) fn enter() -> Self {
IN_EVENT_DISPATCH.with(|f| f.set(true));
Self(())
}
}
impl Drop for EventDispatchGuard {
fn drop(&mut self) {
IN_EVENT_DISPATCH.with(|f| f.set(false));
}
}
fn in_event_dispatch() -> bool {
IN_EVENT_DISPATCH.with(|f| f.get())
}
pub(crate) fn to_skia_color(c: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
}
pub(crate) fn run_offscreen(cfg: &WindowConfig, handler: &mut Box<dyn AppHandler>, path: &Path) {
let s = cfg.screenshot_scale.max(0.1);
let pw = (cfg.width as f32 * s).round().max(1.0) as i32;
let ph = (cfg.height as f32 * s).round().max(1.0) as i32;
let size = Size::new(pw, ph);
let mut pixmap = Pixmap::new(pw as u32, ph as u32).expect("分配 pixmap 失败");
pixmap.fill(to_skia_color(cfg.bg));
handler.set_scale(s);
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
if let Some((lx, ly)) = cfg.screenshot_rclick {
let pos = Point::new(
(lx as f32 * s).round() as i32,
(ly as f32 * s).round() as i32,
);
handler.on_pointer(PointerEvent::single(
PointerKind::Down,
pos,
MouseButton::Right,
));
pixmap.fill(to_skia_color(cfg.bg));
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
}
if let Some((lx, ly)) = cfg.screenshot_click {
let pos = Point::new(
(lx as f32 * s).round() as i32,
(ly as f32 * s).round() as i32,
);
handler.on_pointer(PointerEvent::single(
PointerKind::Down,
pos,
MouseButton::Left,
));
handler.on_pointer(PointerEvent::single(
PointerKind::Up,
pos,
MouseButton::Left,
));
pixmap.fill(to_skia_color(cfg.bg));
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
}
if let Some((lx, ly)) = cfg.screenshot_hover {
let pos = Point::new(
(lx as f32 * s).round() as i32,
(ly as f32 * s).round() as i32,
);
handler.on_pointer(PointerEvent::single(
PointerKind::Move,
pos,
MouseButton::Left,
));
std::thread::sleep(std::time::Duration::from_millis(650));
pixmap.fill(to_skia_color(cfg.bg));
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
}
for _ in 0..4 {
if !handler.wants_animation() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(300));
pixmap.fill(to_skia_color(cfg.bg));
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
}
if let Ok(spec) = std::env::var("WINDUI_BENCH") {
let n: u32 = spec.parse().unwrap_or(30);
let mut total = 0.0f32;
for i in 0..n {
let t = std::time::Instant::now();
pixmap.fill(to_skia_color(cfg.bg));
let mut tgt = crate::render::PixmapTarget {
pixmap: &mut pixmap,
};
handler.render(&mut tgt, size);
let ms = t.elapsed().as_secs_f32() * 1000.0;
total += ms;
eprintln!("[windui] bench frame {i}: {ms:.2} ms");
}
eprintln!(
"[windui] bench 平均: {:.2} ms / 帧({} 帧,全窗重绘)",
total / n.max(1) as f32,
n
);
}
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
pixmap.save_png(path).expect("保存 PNG 失败");
eprintln!("[windui] 截屏已保存: {}", path.display());
}
pub struct WindowConfig {
pub title: String,
pub width: i32,
pub height: i32,
pub bg: Color,
pub centered: bool,
pub resizable: bool,
pub screenshot: Option<PathBuf>,
pub screenshot_scale: f32,
pub screenshot_rclick: Option<(i32, i32)>,
pub screenshot_click: Option<(i32, i32)>,
pub screenshot_hover: Option<(i32, i32)>,
pub tray: Option<Tray>,
pub frameless: bool,
pub animations: Option<bool>,
pub accelerated: bool,
pub min_width: i32,
pub min_height: i32,
}
impl Default for WindowConfig {
fn default() -> Self {
Self {
title: "windui".into(),
width: 800,
height: 600,
bg: Color::hex(0xF3F3F3),
centered: false,
resizable: true,
screenshot: None,
screenshot_scale: 1.0,
screenshot_rclick: None,
screenshot_click: None,
screenshot_hover: None,
tray: None,
frameless: false,
animations: None,
accelerated: false,
min_width: 0,
min_height: 0,
}
}
}
pub trait AppHandler {
fn render(&mut self, target: &mut dyn crate::render::RenderTarget, size: Size);
fn on_pointer(&mut self, _ev: PointerEvent) -> bool {
false
}
fn on_key(&mut self, _ev: KeyEvent) -> bool {
false
}
fn wants_close(&self) -> bool {
false
}
fn on_close_request(&mut self) -> bool {
true
}
fn capture_active(&self) -> bool {
false
}
fn on_capture_lost(&mut self) -> bool {
false
}
fn set_scale(&mut self, _scale: f32) {}
fn ime_caret(&self) -> Option<(i32, i32, i32)> {
None
}
fn set_ime_composing(&mut self, _composing: bool) -> bool {
false
}
fn wants_animation(&self) -> bool {
false
}
fn intervals(&self) -> Vec<std::time::Duration> {
Vec::new()
}
fn on_interval_fired(&mut self, _idx: usize) -> bool {
false
}
fn cursor(&self) -> CursorShape {
CursorShape::Arrow
}
fn on_pan(&mut self, _pos: Point, _dy: i32) -> bool {
false
}
fn start_fling(&mut self, _pos: Point, _vy: f32) -> bool {
false
}
fn cancel_fling(&mut self) -> bool {
false
}
fn on_drop_files(&mut self, _pos: Point, _paths: Vec<std::path::PathBuf>) -> bool {
false
}
fn window_drag_at(&self, _pos: Point) -> bool {
false
}
fn interactive_at(&self, _pos: Point) -> bool {
false
}
fn take_window_op(&mut self) -> Option<WindowOp> {
None
}
fn take_dialog_request(&mut self) -> Option<DialogRequest> {
None
}
}
#[cfg(windows)]
fn inject_parent(d: rfd::FileDialog) -> rfd::FileDialog {
use raw_window_handle::{
DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, RawDisplayHandle,
RawWindowHandle, Win32WindowHandle, WindowHandle, WindowsDisplayHandle,
};
use std::num::NonZeroIsize;
let hwnd_val = win32::active_hwnd();
let Some(nz) = NonZeroIsize::new(hwnd_val) else {
return d;
};
struct W(NonZeroIsize);
impl HasWindowHandle for W {
fn window_handle(&self) -> Result<WindowHandle<'_>, HandleError> {
Ok(unsafe {
WindowHandle::borrow_raw(RawWindowHandle::Win32(Win32WindowHandle::new(self.0)))
})
}
}
impl HasDisplayHandle for W {
fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
Ok(unsafe {
DisplayHandle::borrow_raw(RawDisplayHandle::Windows(WindowsDisplayHandle::new()))
})
}
}
d.set_parent(&W(nz))
}
#[cfg(target_os = "macos")]
fn inject_parent(d: rfd::FileDialog) -> rfd::FileDialog {
d
}
pub struct PickDialog(rfd::FileDialog);
impl Default for PickDialog {
fn default() -> Self {
Self::new()
}
}
impl PickDialog {
pub fn new() -> Self {
Self(rfd::FileDialog::new())
}
pub fn title(mut self, title: impl AsRef<str>) -> Self {
self.0 = self.0.set_title(title.as_ref());
self
}
pub fn filter(mut self, name: impl AsRef<str>, extensions: &[impl AsRef<str>]) -> Self {
let exts: Vec<&str> = extensions.iter().map(|s| s.as_ref()).collect();
self.0 = self.0.add_filter(name.as_ref(), &exts);
self
}
pub fn directory(mut self, path: impl AsRef<Path>) -> Self {
self.0 = self.0.set_directory(path.as_ref());
self
}
pub fn file_name(mut self, name: impl AsRef<str>) -> Self {
self.0 = self.0.set_file_name(name.as_ref());
self
}
fn into_dialog(self) -> rfd::FileDialog {
debug_assert!(
!in_event_dispatch(),
"PickDialog::pick_file()/pick_files()/pick_folder()/pick_folders()/save_file() \
不能在控件事件回调(on_click/on_event)里直接调用——此时 OS 鼠标捕获尚未同步,\
会与对话框自身的模态消息泵抢鼠标输入,反复开关几次就会让鼠标彻底失灵。回调里请改用 \
EventCtx::request_pick_file()/request_pick_files()/request_pick_folder()/\
request_pick_folders()/request_save_file(),多步流程用 EventCtx::defer_blocking()。"
);
inject_parent(self.0)
}
pub fn pick_file(self) -> Option<PathBuf> {
self.into_dialog().pick_file()
}
pub fn pick_files(self) -> Option<Vec<PathBuf>> {
self.into_dialog().pick_files()
}
pub fn pick_folder(self) -> Option<PathBuf> {
self.into_dialog().pick_folder()
}
pub fn pick_folders(self) -> Option<Vec<PathBuf>> {
self.into_dialog().pick_folders()
}
pub fn save_file(self) -> Option<PathBuf> {
self.into_dialog().save_file()
}
}
pub enum DialogRequest {
PickFile(PickDialog, Box<dyn FnOnce(Option<PathBuf>)>),
PickFiles(PickDialog, Box<dyn FnOnce(Option<Vec<PathBuf>>)>),
PickFolder(PickDialog, Box<dyn FnOnce(Option<PathBuf>)>),
PickFolders(PickDialog, Box<dyn FnOnce(Option<Vec<PathBuf>>)>),
SaveFile(PickDialog, Box<dyn FnOnce(Option<PathBuf>)>),
Custom(Box<dyn FnOnce()>),
}
impl DialogRequest {
pub fn run(self) {
match self {
DialogRequest::PickFile(d, cb) => cb(d.pick_file()),
DialogRequest::PickFiles(d, cb) => cb(d.pick_files()),
DialogRequest::PickFolder(d, cb) => cb(d.pick_folder()),
DialogRequest::PickFolders(d, cb) => cb(d.pick_folders()),
DialogRequest::SaveFile(d, cb) => cb(d.save_file()),
DialogRequest::Custom(f) => f(),
}
}
}
#[cfg(test)]
mod dispatch_guard_tests {
use super::*;
#[test]
fn event_dispatch_guard_tracks_state_and_clears_on_drop() {
assert!(!in_event_dispatch());
let guard = EventDispatchGuard::enter();
assert!(in_event_dispatch());
drop(guard);
assert!(!in_event_dispatch());
}
#[test]
#[cfg(debug_assertions)]
#[should_panic(expected = "不能在控件事件回调")]
fn pick_dialog_panics_when_called_inside_event_dispatch() {
let _guard = EventDispatchGuard::enter();
let _ = PickDialog::new().pick_folder();
}
}