#[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::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};
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 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 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
}
}
#[cfg(windows)]
fn inject_parent(d: rfd::FileDialog) -> rfd::FileDialog {
use raw_window_handle::{
HandleError, HasWindowHandle, RawWindowHandle, Win32WindowHandle, WindowHandle,
};
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)))
})
}
}
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 {
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()
}
}