├── Kick.toml ├── .gitignore ├── examples ├── tokio.ico ├── copy_data.rs ├── registry.rs ├── named_mutex.rs ├── clipboard.rs ├── multiple.rs └── showcase.rs ├── graphics └── showcase.png ├── src ├── window │ ├── mod.rs │ └── window.rs ├── windows │ ├── real.rs │ └── fake.rs ├── window_loop │ ├── messages.rs │ ├── window_class_handle.rs │ ├── area_handle.rs │ ├── mod.rs │ ├── icon_handle.rs │ ├── menu_manager.rs │ ├── window_handle.rs │ ├── popup_menu_handle.rs │ ├── clipboard_manager.rs │ └── window_loop.rs ├── notification_id.rs ├── area_id.rs ├── icon │ ├── mod.rs │ └── stock_icon.rs ├── modify_menu_item.rs ├── modify_area.rs ├── icon_buffer.rs ├── tools.rs ├── icons.rs ├── convert.rs ├── menu_item.rs ├── area.rs ├── named_mutex.rs ├── notification.rs ├── item_id.rs ├── autostart.rs ├── event.rs ├── popup_menu.rs ├── clipboard │ ├── mod.rs │ └── clipboard_format.rs ├── event_loop.rs ├── registry.rs ├── lib.rs ├── error.rs ├── create_window.rs └── sender.rs ├── Cargo.toml ├── .github └── workflows │ └── ci.yml └── README.md /Kick.toml: -------------------------------------------------------------------------------- 1 | os = ["windows"] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | /Cargo.lock 3 | /*.png 4 | -------------------------------------------------------------------------------- /examples/tokio.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udoprog/winctx/HEAD/examples/tokio.ico -------------------------------------------------------------------------------- /graphics/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udoprog/winctx/HEAD/graphics/showcase.png -------------------------------------------------------------------------------- /src/window/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types related to finding and manipulating windows. 2 | 3 | pub use self::window::{FindWindow, Window}; 4 | mod window; 5 | -------------------------------------------------------------------------------- /src/windows/real.rs: -------------------------------------------------------------------------------- 1 | pub(crate) use std::os::windows::ffi::{OsStrExt, OsStringExt}; 2 | pub(crate) use std::os::windows::io::{FromRawHandle, OwnedHandle}; 3 | -------------------------------------------------------------------------------- /src/window_loop/messages.rs: -------------------------------------------------------------------------------- 1 | use windows_sys::Win32::UI::WindowsAndMessaging::WM_USER; 2 | 3 | // Icon message. 4 | pub(super) const ICON_ID: u32 = WM_USER + 1; 5 | // Transfer bytes payload. 6 | pub(super) const BYTES_ID: u32 = WM_USER + 2; 7 | -------------------------------------------------------------------------------- /src/notification_id.rs: -------------------------------------------------------------------------------- 1 | /// An identifier for a notification. 2 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 3 | pub struct NotificationId(u32); 4 | 5 | impl NotificationId { 6 | #[inline] 7 | pub(crate) fn new(id: u32) -> Self { 8 | Self(id) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/copy_data.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use winctx::window::FindWindow; 3 | 4 | pub fn main() -> Result<()> { 5 | let Some(window) = FindWindow::new().class("se.tedro.Example").find()? else { 6 | println!("Could not find window"); 7 | return Ok(()); 8 | }; 9 | 10 | window.copy_data(42, b"foobar")?; 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /examples/registry.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use anyhow::Result; 4 | 5 | fn main() -> Result<()> { 6 | let key = winctx::OpenRegistryKey::local_machine().open("SOFTWARE\\Tesseract-OCR")?; 7 | let value = key.get_string("Path")?; 8 | let path = PathBuf::from(value); 9 | let dll = path.join("libtesseract-5.dll"); 10 | dbg!(&dll); 11 | Ok(()) 12 | } 13 | -------------------------------------------------------------------------------- /src/window_loop/window_class_handle.rs: -------------------------------------------------------------------------------- 1 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 2 | 3 | pub(super) struct WindowClassHandle { 4 | pub(super) class_name: Vec, 5 | } 6 | 7 | impl Drop for WindowClassHandle { 8 | fn drop(&mut self) { 9 | unsafe { 10 | winuser::UnregisterClassW(self.class_name.as_ptr(), 0); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/named_mutex.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use tokio::signal::ctrl_c; 3 | use winctx::NamedMutex; 4 | 5 | const NAME: &str = "se.tedro.Example"; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<()> { 9 | let Some(_m) = NamedMutex::create_acquired(NAME)? else { 10 | println!("Mutex '{NAME}' already acquired"); 11 | return Ok(()); 12 | }; 13 | 14 | ctrl_c().await?; 15 | Ok(()) 16 | } 17 | -------------------------------------------------------------------------------- /src/area_id.rs: -------------------------------------------------------------------------------- 1 | /// The identifier for an [`Area`]. 2 | /// 3 | /// [`Area`]: crate::area::Area 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 5 | #[repr(transparent)] 6 | pub struct AreaId(u32); 7 | 8 | impl AreaId { 9 | /// Construct a new menu id. 10 | pub(crate) const fn new(id: u32) -> Self { 11 | Self(id) 12 | } 13 | 14 | /// Get the menu id. 15 | pub(crate) const fn id(&self) -> u32 { 16 | self.0 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/icon/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types related to icons. 2 | 3 | #[doc(inline)] 4 | pub use self::stock_icon::StockIcon; 5 | mod stock_icon; 6 | 7 | /// A reference to an icon. 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 9 | pub struct IconId(u32); 10 | 11 | impl IconId { 12 | #[inline] 13 | pub(crate) fn new(id: u32) -> Self { 14 | Self(id) 15 | } 16 | 17 | #[inline] 18 | pub(crate) fn as_usize(self) -> usize { 19 | self.0 as usize 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/window_loop/area_handle.rs: -------------------------------------------------------------------------------- 1 | use crate::AreaId; 2 | 3 | use super::PopupMenuHandle; 4 | 5 | #[repr(C)] 6 | pub(crate) struct AreaHandle { 7 | pub(crate) area_id: AreaId, 8 | pub(crate) popup_menu: Option, 9 | } 10 | 11 | impl AreaHandle { 12 | /// Construct a new menu handle. 13 | pub(crate) fn new(area_id: AreaId, popup_menu: Option) -> Self { 14 | Self { 15 | area_id, 16 | popup_menu, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/modify_menu_item.rs: -------------------------------------------------------------------------------- 1 | /// Parameters to modify a menu item. 2 | #[derive(Default, Debug)] 3 | pub(super) struct ModifyMenuItem { 4 | pub(super) checked: Option, 5 | pub(super) highlight: Option, 6 | } 7 | 8 | impl ModifyMenuItem { 9 | /// Set the checked state of the menu item. 10 | pub(super) fn checked(&mut self, checked: bool) { 11 | self.checked = Some(checked); 12 | } 13 | 14 | /// Set that the menu item should be highlighted. 15 | pub(super) fn highlight(&mut self, highlight: bool) { 16 | self.highlight = Some(highlight); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/window_loop/mod.rs: -------------------------------------------------------------------------------- 1 | mod messages; 2 | 3 | pub(super) use self::window_loop::{WindowEvent, WindowLoop}; 4 | mod window_loop; 5 | 6 | pub(super) use self::icon_handle::IconHandle; 7 | mod icon_handle; 8 | 9 | use self::clipboard_manager::ClipboardManager; 10 | mod clipboard_manager; 11 | 12 | use self::menu_manager::MenuManager; 13 | mod menu_manager; 14 | 15 | use self::window_handle::WindowHandle; 16 | mod window_handle; 17 | 18 | use self::window_class_handle::WindowClassHandle; 19 | mod window_class_handle; 20 | 21 | pub(super) use self::area_handle::AreaHandle; 22 | mod area_handle; 23 | 24 | pub(super) use self::popup_menu_handle::PopupMenuHandle; 25 | mod popup_menu_handle; 26 | -------------------------------------------------------------------------------- /src/modify_area.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::IconId; 4 | 5 | /// A message sent to modify a notification area. 6 | #[derive(Default, Debug)] 7 | pub(crate) struct ModifyArea { 8 | pub(super) icon: Option, 9 | pub(super) tooltip: Option>, 10 | } 11 | 12 | impl ModifyArea { 13 | /// Set the icon of the notification area. 14 | pub(crate) fn icon(&mut self, icon: IconId) { 15 | self.icon = Some(icon); 16 | } 17 | 18 | /// Set the tooltip of the notification area. 19 | pub(crate) fn tooltip(&mut self, tooltip: T) 20 | where 21 | T: fmt::Display, 22 | { 23 | self.tooltip = Some(tooltip.to_string().into()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/icon_buffer.rs: -------------------------------------------------------------------------------- 1 | /// The buffer for an image. 2 | pub(crate) struct IconBuffer { 3 | buffer: Box<[u8]>, 4 | width: u32, 5 | height: u32, 6 | } 7 | 8 | impl IconBuffer { 9 | /// Construct an icon from a raw buffer. 10 | pub(crate) fn from_buffer(buffer: T, width: u32, height: u32) -> Self 11 | where 12 | T: AsRef<[u8]>, 13 | { 14 | Self { 15 | buffer: buffer.as_ref().into(), 16 | width, 17 | height, 18 | } 19 | } 20 | 21 | pub(crate) fn as_bytes(&self) -> &[u8] { 22 | self.buffer.as_ref() 23 | } 24 | 25 | pub(crate) fn width(&self) -> u32 { 26 | self.width 27 | } 28 | 29 | pub(crate) fn height(&self) -> u32 { 30 | self.height 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/tools.rs: -------------------------------------------------------------------------------- 1 | //! Minor tools made available for convenience. 2 | 3 | use std::ffi::OsStr; 4 | use std::io; 5 | use std::ptr; 6 | 7 | use windows_sys::Win32::UI::Shell::ShellExecuteW; 8 | use windows_sys::Win32::UI::WindowsAndMessaging::SW_SHOW; 9 | 10 | use crate::convert::ToWide; 11 | 12 | /// Open the given directory using the default file manager, which on windows 13 | /// would most likely be Explorer. 14 | /// 15 | /// # Examples 16 | /// 17 | /// ``` 18 | /// use winctx::tools; 19 | /// 20 | /// tools::open_dir("D:\\Files")?; 21 | /// # Ok::<_, std::io::Error>(()) 22 | /// ``` 23 | pub fn open_dir

(path: P) -> io::Result 24 | where 25 | P: AsRef, 26 | { 27 | let path = path.to_wide_null(); 28 | let operation = "open".to_wide_null(); 29 | 30 | let result = unsafe { 31 | ShellExecuteW( 32 | 0, 33 | operation.as_ptr(), 34 | path.as_ptr(), 35 | ptr::null(), 36 | ptr::null(), 37 | SW_SHOW, 38 | ) 39 | }; 40 | 41 | Ok(result as usize > 32) 42 | } 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "winctx" 3 | version = "0.0.19" 4 | authors = ["John-John Tedro "] 5 | edition = "2021" 6 | rust-version = "1.70" 7 | description = """ 8 | A minimal window context for Rust on Windows. 9 | """ 10 | documentation = "https://docs.rs/winctx" 11 | readme = "README.md" 12 | homepage = "https://github.com/udoprog/winctx" 13 | repository = "https://github.com/udoprog/winctx" 14 | license = "MIT OR Apache-2.0" 15 | keywords = ["async", "windows"] 16 | categories = ["asynchronous"] 17 | 18 | [dependencies] 19 | tokio = { version = "1.34.0", features = ["sync", "macros"] } 20 | windows-core = "0.52.0" 21 | 22 | [dependencies.windows-sys] 23 | version = "0.52.0" 24 | features = [ 25 | "Win32_System_Threading", 26 | "Win32_Foundation", 27 | "Win32_Security", 28 | "Win32_UI_WindowsAndMessaging", 29 | "Win32_Graphics_Gdi", 30 | "Win32_UI_Shell", 31 | "Win32_System_Registry", 32 | "Win32_System_DataExchange", 33 | "Win32_System_Ole", 34 | "Win32_System_Memory", 35 | ] 36 | 37 | [dev-dependencies] 38 | anyhow = "1.0.75" 39 | image = "0.24.7" 40 | tokio = { version = "1.34.0", features = ["full"] } 41 | -------------------------------------------------------------------------------- /src/windows/fake.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | ffi::{OsStr, OsString}, 3 | marker::PhantomData, 4 | }; 5 | 6 | pub(crate) type RawHandle = *mut (); 7 | 8 | pub(crate) trait FromRawHandle { 9 | fn from_raw_handle(handle: RawHandle) -> Self; 10 | } 11 | 12 | pub(crate) struct OwnedHandle; 13 | pub(crate) struct EncodeWide<'a>(PhantomData<&'a ()>); 14 | 15 | impl Iterator for EncodeWide<'_> { 16 | type Item = u16; 17 | 18 | #[inline] 19 | fn next(&mut self) -> Option { 20 | unimplemented!("not implemented on this platform") 21 | } 22 | } 23 | 24 | impl FromRawHandle for OwnedHandle { 25 | #[inline] 26 | fn from_raw_handle(_: RawHandle) -> Self { 27 | unimplemented!("not implemented on this platform") 28 | } 29 | } 30 | 31 | pub(crate) trait OsStringExt { 32 | fn from_wide(wide: &[u16]) -> Self; 33 | } 34 | 35 | pub(crate) trait OsStrExt { 36 | fn encode_wide(&self) -> EncodeWide<'_> { 37 | unimplemented!("not implemented on this platform") 38 | } 39 | } 40 | 41 | impl OsStrExt for OsStr { 42 | #[inline] 43 | fn encode_wide(&self) -> EncodeWide<'_> { 44 | EncodeWide(PhantomData) 45 | } 46 | } 47 | 48 | impl OsStringExt for OsString { 49 | #[inline] 50 | fn from_wide(_: &[u16]) -> Self { 51 | unimplemented!("not implemented on this platform") 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/icons.rs: -------------------------------------------------------------------------------- 1 | //! Type used to interact with an icons collection. 2 | 3 | use crate::{IconBuffer, IconId}; 4 | 5 | /// A collection of notification icons. 6 | /// 7 | /// This defines the various icons that an application using winctx can use. 8 | /// 9 | /// This is returned by [`CreateWindow::icons`]. 10 | /// 11 | /// [`CreateWindow::icons`]: crate::CreateWindow::icons 12 | #[derive(Default)] 13 | pub struct Icons { 14 | pub(super) icons: Vec, 15 | } 16 | 17 | impl Icons { 18 | /// Construct a new empty collection of notification icons. 19 | #[inline] 20 | pub fn new() -> Self { 21 | Self::default() 22 | } 23 | 24 | /// Push an icon from a buffer and return a handle to it. 25 | /// 26 | /// # Examples 27 | /// 28 | /// ``` 29 | /// use winctx::CreateWindow; 30 | /// 31 | /// # macro_rules! include_bytes { ($path:literal) => { &[] } } 32 | /// const ICON: &[u8] = include_bytes!("tokio.ico"); 33 | /// 34 | /// let mut window = CreateWindow::new("se.tedro.Example"); 35 | /// let icon = window.icons().insert_buffer(ICON, 22, 22); 36 | /// ``` 37 | pub fn insert_buffer(&mut self, buffer: T, width: u32, height: u32) -> IconId 38 | where 39 | T: AsRef<[u8]>, 40 | { 41 | let icon = IconId::new(self.icons.len() as u32); 42 | self.icons 43 | .push(IconBuffer::from_buffer(buffer, width, height)); 44 | icon 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | schedule: 9 | - cron: '48 0 * * 0' 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | msrv: 17 | runs-on: windows-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: dtolnay/rust-toolchain@1.70 21 | - run: cargo build --workspace --lib 22 | 23 | test: 24 | runs-on: windows-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - uses: dtolnay/rust-toolchain@stable 28 | - run: cargo test --workspace --all-targets 29 | - run: cargo test --workspace --doc 30 | 31 | clippy: 32 | runs-on: windows-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: dtolnay/rust-toolchain@stable 36 | with: 37 | components: clippy 38 | - run: cargo clippy --workspace --all-features --all-targets -- -D warnings 39 | 40 | rustfmt: 41 | runs-on: windows-latest 42 | steps: 43 | - uses: actions/checkout@v4 44 | - uses: dtolnay/rust-toolchain@stable 45 | with: 46 | components: rustfmt 47 | - run: cargo fmt --check --all 48 | 49 | docs: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: dtolnay/rust-toolchain@stable 54 | - uses: Swatinem/rust-cache@v2 55 | - run: cargo doc --all-features 56 | env: 57 | RUSTFLAGS: --cfg winctx_docsrs 58 | RUSTDOCFLAGS: --cfg winctx_docsrs 59 | -------------------------------------------------------------------------------- /src/window_loop/icon_handle.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use windows_sys::Win32::Foundation::TRUE; 4 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 5 | use windows_sys::Win32::UI::WindowsAndMessaging::{DestroyIcon, HICON}; 6 | 7 | #[derive(Clone)] 8 | pub(crate) struct IconHandle { 9 | pub(super) hicon: HICON, 10 | } 11 | 12 | impl IconHandle { 13 | pub(crate) fn from_buffer(buffer: &[u8], width: u32, height: u32) -> io::Result { 14 | let offset = unsafe { 15 | winuser::LookupIconIdFromDirectoryEx( 16 | buffer.as_ptr(), 17 | TRUE, 18 | width as i32, 19 | height as i32, 20 | winuser::LR_DEFAULTCOLOR, 21 | ) 22 | }; 23 | 24 | if offset == 0 { 25 | return Err(io::Error::last_os_error()); 26 | } 27 | 28 | let icon_data = &buffer[offset as usize..]; 29 | 30 | let hicon = unsafe { 31 | winuser::CreateIconFromResourceEx( 32 | icon_data.as_ptr(), 33 | icon_data.len() as u32, 34 | TRUE, 35 | 0x30000, 36 | width as i32, 37 | height as i32, 38 | winuser::LR_DEFAULTCOLOR, 39 | ) 40 | }; 41 | 42 | if hicon == 0 { 43 | return Err(io::Error::last_os_error()); 44 | } 45 | 46 | Ok(Self { hicon }) 47 | } 48 | } 49 | 50 | impl Drop for IconHandle { 51 | fn drop(&mut self) { 52 | // SAFETY: icon handle is owned by this struct. 53 | unsafe { 54 | DestroyIcon(self.hicon); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /examples/clipboard.rs: -------------------------------------------------------------------------------- 1 | use std::io::Cursor; 2 | use std::pin::pin; 3 | 4 | use anyhow::Result; 5 | use tokio::signal::ctrl_c; 6 | use winctx::event::ClipboardEvent; 7 | use winctx::{CreateWindow, Event}; 8 | 9 | const ICON: &[u8] = include_bytes!("tokio.ico"); 10 | 11 | #[tokio::main] 12 | async fn main() -> Result<()> { 13 | let mut window = CreateWindow::new("se.tedro.Example").clipboard_events(true); 14 | 15 | let default_icon = window.icons().insert_buffer(ICON, 22, 22); 16 | 17 | window.new_area().icon(default_icon); 18 | 19 | let (sender, mut event_loop) = window.build().await?; 20 | 21 | let mut ctrl_c = pin!(ctrl_c()); 22 | let mut shutdown = false; 23 | 24 | loop { 25 | let event = tokio::select! { 26 | _ = ctrl_c.as_mut(), if !shutdown => { 27 | sender.shutdown(); 28 | shutdown = true; 29 | continue; 30 | } 31 | event = event_loop.tick() => { 32 | event? 33 | } 34 | }; 35 | 36 | match event { 37 | Event::Clipboard { event } => match event { 38 | ClipboardEvent::BitMap(bitmap) => { 39 | let decoder = image::codecs::bmp::BmpDecoder::new_without_file_header( 40 | Cursor::new(&bitmap[..]), 41 | )?; 42 | let image = image::DynamicImage::from_decoder(decoder)?; 43 | image.save("clipboard.png")?; 44 | println!("Saved clipboard image to clipboard.png"); 45 | } 46 | ClipboardEvent::Text(text) => { 47 | println!("Clipboard text: {text:?}"); 48 | } 49 | _ => {} 50 | }, 51 | Event::Shutdown { .. } => { 52 | println!("Window shut down"); 53 | break; 54 | } 55 | _ => {} 56 | } 57 | } 58 | 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /src/convert.rs: -------------------------------------------------------------------------------- 1 | use std::char::decode_utf16; 2 | use std::char::DecodeUtf16Error; 3 | use std::ffi::{OsStr, OsString}; 4 | 5 | use crate::windows::{OsStrExt, OsStringExt}; 6 | use crate::Result; 7 | 8 | /// Copy a wide string from a source to a destination, truncating if necessary. 9 | pub(crate) fn copy_wstring_lossy(dest: &mut [u16], source: &str) { 10 | let mut n = 0; 11 | 12 | for c in source.encode_utf16().take(dest.len()) { 13 | dest[n] = c; 14 | n += 1; 15 | } 16 | 17 | if dest.len() > n { 18 | dest[n] = 0; 19 | } else { 20 | dest[n - 1] = 0; 21 | } 22 | } 23 | 24 | pub(crate) trait ToWide { 25 | /// Encode into a null-terminated wide string. 26 | fn to_wide_null(&self) -> Vec; 27 | } 28 | 29 | impl ToWide for T 30 | where 31 | T: AsRef, 32 | { 33 | #[inline] 34 | fn to_wide_null(&self) -> Vec { 35 | self.as_ref().encode_wide().chain(Some(0)).collect() 36 | } 37 | } 38 | 39 | pub(crate) trait FromWide 40 | where 41 | Self: Sized, 42 | { 43 | fn from_wide(wide: &[u16]) -> Self; 44 | } 45 | 46 | impl FromWide for std::ffi::OsString { 47 | fn from_wide(wide: &[u16]) -> OsString { 48 | OsStringExt::from_wide(wide) 49 | } 50 | } 51 | 52 | pub(super) fn encode_escaped_os_str( 53 | out: &mut String, 54 | input: &OsStr, 55 | ) -> Result<(), DecodeUtf16Error> { 56 | let mut escape = false; 57 | 58 | for c in input.encode_wide() { 59 | // ' ' 60 | if c == 0x00000020 { 61 | escape = true; 62 | break; 63 | } 64 | } 65 | 66 | if escape { 67 | out.push('"'); 68 | 69 | for c in decode_utf16(input.encode_wide()) { 70 | out.push(c?); 71 | } 72 | 73 | out.push('"'); 74 | } else { 75 | // No escaping needed. 76 | for c in decode_utf16(input.encode_wide()) { 77 | out.push(c?); 78 | } 79 | } 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /src/menu_item.rs: -------------------------------------------------------------------------------- 1 | //! Types related to menu construction. 2 | 3 | use crate::{ItemId, ModifyMenuItem}; 4 | 5 | pub(super) enum MenuItemKind { 6 | Separator, 7 | String { text: String }, 8 | } 9 | 10 | /// A menu item in the context menu. 11 | /// 12 | /// This is constructed through: 13 | /// * [`MenuItem::separator`]. 14 | /// * [`MenuItem::entry`]. 15 | pub struct MenuItem { 16 | pub(crate) item_id: ItemId, 17 | pub(crate) kind: MenuItemKind, 18 | pub(crate) initial: ModifyMenuItem, 19 | } 20 | 21 | impl MenuItem { 22 | pub(super) fn new(item_id: ItemId, kind: MenuItemKind) -> Self { 23 | Self { 24 | item_id, 25 | kind, 26 | initial: ModifyMenuItem::default(), 27 | } 28 | } 29 | 30 | /// Get the identifier of the menu item. 31 | pub fn id(&self) -> ItemId { 32 | self.item_id 33 | } 34 | 35 | /// Set the checked state of the menu item. 36 | /// 37 | /// # Examples 38 | /// 39 | /// ```no_run 40 | /// use winctx::CreateWindow; 41 | /// 42 | /// let mut window = CreateWindow::new("se.tedro.Example");; 43 | /// let area = window.new_area(); 44 | /// 45 | /// let mut menu = area.popup_menu(); 46 | /// menu.push_entry("Example Application").checked(true); 47 | /// ``` 48 | pub fn checked(&mut self, checked: bool) -> &mut Self { 49 | self.initial.checked(checked); 50 | self 51 | } 52 | 53 | /// Set that the menu item should be highlighted. 54 | /// 55 | /// # Examples 56 | /// 57 | /// ```no_run 58 | /// use winctx::CreateWindow; 59 | /// 60 | /// let mut window = CreateWindow::new("se.tedro.Example");; 61 | /// let area = window.new_area(); 62 | /// 63 | /// let mut menu = area.popup_menu(); 64 | /// menu.push_entry("Example Application").checked(true); 65 | /// ``` 66 | pub fn highlight(&mut self, highlight: bool) -> &mut Self { 67 | self.initial.highlight(highlight); 68 | self 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/area.rs: -------------------------------------------------------------------------------- 1 | //! Types related to defining the notification area. 2 | 3 | use std::fmt; 4 | 5 | use crate::{AreaId, IconId, ModifyArea, PopupMenu}; 6 | 7 | /// A notification area. 8 | /// 9 | /// This is what occupies space in the notification area. 10 | /// 11 | /// It can have an icon, a tooltip, and a popup menu associated with it. 12 | /// 13 | /// Note that if an area doesn't have the icon, it will still be added to the 14 | /// notification tray but with an empty space. 15 | pub struct Area { 16 | pub(super) id: AreaId, 17 | pub(super) popup_menu: Option, 18 | pub(super) initial: ModifyArea, 19 | } 20 | 21 | impl Area { 22 | /// Construct a new empty notification area. 23 | /// 24 | /// Without any configuration this will just occupy a blank space in the 25 | /// notification area. 26 | /// 27 | /// To set an icon or a popup menu, use the relevant builder methods. 28 | pub(super) fn new(area_id: AreaId) -> Self { 29 | Self { 30 | id: area_id, 31 | popup_menu: None, 32 | initial: ModifyArea::default(), 33 | } 34 | } 35 | 36 | /// Get the identifier for this area. 37 | pub fn id(&self) -> AreaId { 38 | self.id 39 | } 40 | 41 | /// Set the icon of the notification area. 42 | #[inline] 43 | pub fn icon(&mut self, icon: IconId) -> &mut Self { 44 | self.initial.icon(icon); 45 | self 46 | } 47 | 48 | /// Set the tooltip of the notification area. 49 | #[inline] 50 | pub fn tooltip(&mut self, tooltip: T) -> &mut Self 51 | where 52 | T: fmt::Display, 53 | { 54 | self.initial.tooltip(tooltip); 55 | self 56 | } 57 | 58 | /// Set that a popup menu should be used and return a handle to populate it. 59 | #[inline] 60 | pub fn popup_menu(&mut self) -> &mut PopupMenu { 61 | if self.popup_menu.is_none() { 62 | self.popup_menu = Some(PopupMenu::new(self.id)); 63 | } 64 | 65 | self.popup_menu.as_mut().unwrap() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/multiple.rs: -------------------------------------------------------------------------------- 1 | use std::pin::pin; 2 | 3 | use anyhow::Result; 4 | use tokio::signal::ctrl_c; 5 | use winctx::{CreateWindow, Event}; 6 | 7 | const ICON: &[u8] = include_bytes!("tokio.ico"); 8 | 9 | #[tokio::main] 10 | async fn main() -> Result<()> { 11 | let mut window = CreateWindow::new("se.tedro.Example").clipboard_events(true); 12 | 13 | let default_icon = window.icons().insert_buffer(ICON, 22, 22); 14 | 15 | let area1 = window.new_area().icon(default_icon); 16 | 17 | let menu1 = area1.popup_menu(); 18 | let first = menu1.push_entry("Menu 1").id(); 19 | menu1.set_default(first); 20 | 21 | let area2 = window.new_area().icon(default_icon); 22 | 23 | let menu2 = area2.popup_menu(); 24 | let second = menu2.push_entry("Menu 2").id(); 25 | menu2.set_default(first); 26 | 27 | let (sender, mut event_loop) = window.build().await?; 28 | 29 | let mut ctrl_c = pin!(ctrl_c()); 30 | let mut shutdown = false; 31 | 32 | loop { 33 | let event = tokio::select! { 34 | _ = ctrl_c.as_mut(), if !shutdown => { 35 | sender.shutdown(); 36 | shutdown = true; 37 | continue; 38 | } 39 | event = event_loop.tick() => { 40 | event? 41 | } 42 | }; 43 | 44 | match event { 45 | Event::IconClicked { area_id, .. } => { 46 | println!("Icon clicked: {area_id:?}"); 47 | } 48 | Event::MenuItemClicked { item_id, .. } => { 49 | println!("Menu entry clicked: {item_id:?}"); 50 | 51 | if item_id == first { 52 | println!("Menu 1 clicked"); 53 | } 54 | 55 | if item_id == second { 56 | println!("Menu 2 clicked"); 57 | } 58 | } 59 | Event::Shutdown { .. } => { 60 | println!("Window shut down"); 61 | break; 62 | } 63 | _ => {} 64 | } 65 | } 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /src/named_mutex.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | use std::io; 3 | use std::ptr; 4 | 5 | use windows_core::PCWSTR; 6 | use windows_sys::Win32::Foundation::{GetLastError, ERROR_ALREADY_EXISTS, TRUE}; 7 | use windows_sys::Win32::System::Threading::CreateMutexW; 8 | 9 | use crate::convert::ToWide; 10 | use crate::error::ErrorKind::*; 11 | use crate::windows::{FromRawHandle, OwnedHandle}; 12 | use crate::Result; 13 | 14 | /// A named exclusive mutex that can be used to ensure that only one instance of 15 | /// an application is running. 16 | /// 17 | /// # Examples 18 | /// 19 | /// ```no_run 20 | /// use winctx::NamedMutex; 21 | /// 22 | /// if let Some(_m) = NamedMutex::create_acquired("se.tedro.Example")? { 23 | /// // The only one holding the mutex. 24 | /// } 25 | /// # Ok::<_, winctx::Error>(()) 26 | /// ``` 27 | pub struct NamedMutex { 28 | _handle: OwnedHandle, 29 | } 30 | 31 | impl NamedMutex { 32 | /// Create a named mutex with the given name that is already acquired. 33 | /// 34 | /// Returns `None` if the mutex could not be acquired. 35 | /// 36 | /// # Errors 37 | /// 38 | /// Errors in case the named mutex could not be created. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ```no_run 43 | /// use winctx::NamedMutex; 44 | /// 45 | /// if let Some(_m) = NamedMutex::create_acquired("se.tedro.Example")? { 46 | /// // The only one holding the mutex. 47 | /// } 48 | /// # Ok::<_, winctx::Error>(()) 49 | /// ``` 50 | pub fn create_acquired(name: N) -> Result> 51 | where 52 | N: fmt::Display, 53 | { 54 | let name = name.to_string(); 55 | let name = name.to_wide_null(); 56 | let name = PCWSTR::from_raw(name.as_ptr()); 57 | 58 | unsafe { 59 | let handle = CreateMutexW(ptr::null(), TRUE, name.as_ptr()); 60 | 61 | if handle == 0 { 62 | return Err(CreateMutex(io::Error::last_os_error()).into()); 63 | } 64 | 65 | let handle = OwnedHandle::from_raw_handle(handle as *mut _); 66 | 67 | if GetLastError() == ERROR_ALREADY_EXISTS { 68 | return Ok(None); 69 | } 70 | 71 | Ok(Some(NamedMutex { _handle: handle })) 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/notification.rs: -------------------------------------------------------------------------------- 1 | //! Types related to notifications. 2 | 3 | use std::fmt; 4 | use std::time::Duration; 5 | 6 | use windows_sys::Win32::UI::Shell::{self, NIIF_LARGE_ICON, NIIF_NOSOUND, NIIF_RESPECT_QUIET_TIME}; 7 | 8 | use crate::icon::StockIcon; 9 | 10 | /// Indicates the [standard icon] that Windows should use for the notification. 11 | /// 12 | /// [standard icon]: https://learn.microsoft.com/en-us/windows/win32/uxguide/vis-std-icons 13 | #[derive(Debug)] 14 | #[non_exhaustive] 15 | pub(super) enum NotificationIcon { 16 | /// An information icon. 17 | Info, 18 | /// A warning icon. 19 | Warning, 20 | /// An error icon. 21 | Error, 22 | /// A stock icon icon. 23 | StockIcon(StockIcon), 24 | } 25 | 26 | /// A single notification. 27 | #[derive(Debug)] 28 | pub(super) struct Notification { 29 | pub(super) title: Option, 30 | pub(super) message: Option, 31 | pub(super) icon: Option, 32 | pub(super) timeout: Option, 33 | pub(super) options: u32, 34 | pub(super) stock_icon_opts: u32, 35 | } 36 | 37 | impl Notification { 38 | /// Create a new notification. 39 | pub(super) fn new() -> Self { 40 | Self { 41 | message: None, 42 | title: None, 43 | icon: None, 44 | timeout: Some(Duration::from_secs(1)), 45 | options: 0, 46 | stock_icon_opts: 0, 47 | } 48 | } 49 | 50 | pub(super) fn message(&mut self, message: M) 51 | where 52 | M: fmt::Display, 53 | { 54 | self.message = Some(message.to_string()); 55 | } 56 | 57 | pub(super) fn title(&mut self, title: M) 58 | where 59 | M: fmt::Display, 60 | { 61 | self.title = Some(title.to_string()); 62 | } 63 | 64 | pub(super) fn icon(&mut self, icon: NotificationIcon) { 65 | self.icon = Some(icon); 66 | } 67 | 68 | pub(super) fn no_sound(&mut self) { 69 | self.options |= NIIF_NOSOUND; 70 | } 71 | 72 | pub(super) fn large_icon(&mut self) { 73 | self.options |= NIIF_LARGE_ICON; 74 | } 75 | 76 | pub(super) fn respect_quiet_time(&mut self) { 77 | self.options |= NIIF_RESPECT_QUIET_TIME; 78 | } 79 | 80 | pub(crate) fn icon_selected(&mut self) { 81 | self.stock_icon_opts |= Shell::SHGSI_SELECTED; 82 | } 83 | 84 | pub(crate) fn icon_link_overlay(&mut self) { 85 | self.stock_icon_opts |= Shell::SHGSI_LINKOVERLAY; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/item_id.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::AreaId; 4 | 5 | /// Helper macro to build a match pattern over an item id. 6 | /// 7 | /// If an item is pushed to a popup menu in a given area it will always have a 8 | /// consistent identifier, so this can be used to improve pattern matching over 9 | /// which item was clicked. 10 | /// 11 | /// # Examples 12 | /// 13 | /// ```no_run 14 | /// use std::pin::pin; 15 | /// 16 | /// use tokio::signal::ctrl_c; 17 | /// use winctx::{Event, CreateWindow}; 18 | /// 19 | /// # macro_rules! include_bytes { ($path:literal) => { &[] } } 20 | /// const ICON: &[u8] = include_bytes!("tokio.ico"); 21 | /// 22 | /// # async fn test() -> winctx::Result<()> { 23 | /// let mut window = CreateWindow::new("se.tedro.Example") 24 | /// .window_name("Example Application"); 25 | /// 26 | /// let icon = window.icons().insert_buffer(ICON, 22, 22); 27 | /// 28 | /// let area = window.new_area().icon(icon); 29 | /// 30 | /// let menu = area.popup_menu(); 31 | /// let first = menu.push_entry("Example Application").id(); 32 | /// menu.push_entry("Quit"); 33 | /// menu.set_default(first); 34 | /// 35 | /// let area2 = window.new_area().icon(icon); 36 | /// let menu = area2.popup_menu(); 37 | /// menu.push_entry("Other area"); 38 | /// 39 | /// let (sender, mut event_loop) = window 40 | /// .build() 41 | /// .await?; 42 | /// 43 | /// let mut ctrl_c = pin!(ctrl_c()); 44 | /// let mut shutdown = false; 45 | /// 46 | /// loop { 47 | /// let event = tokio::select! { 48 | /// _ = ctrl_c.as_mut(), if !shutdown => { 49 | /// sender.shutdown(); 50 | /// shutdown = true; 51 | /// continue; 52 | /// } 53 | /// event = event_loop.tick() => { 54 | /// event? 55 | /// } 56 | /// }; 57 | /// 58 | /// match event { 59 | /// Event::MenuItemClicked { item_id, .. } => { 60 | /// match item_id { 61 | /// winctx::item_id!(0, 0) => { 62 | /// println!("first item clicked"); 63 | /// assert_eq!(item_id, first); 64 | /// } 65 | /// winctx::item_id!(0, 1) => { 66 | /// sender.shutdown(); 67 | /// } 68 | /// winctx::item_id!(1, 0) => { 69 | /// println!("Item clicked in second area"); 70 | /// } 71 | /// _ => {} 72 | /// } 73 | /// } 74 | /// Event::Shutdown { .. } => { 75 | /// println!("Window shut down"); 76 | /// break; 77 | /// } 78 | /// _ => {} 79 | /// } 80 | /// } 81 | /// # Ok(()) } 82 | /// ``` 83 | #[macro_export] 84 | macro_rules! item_id { 85 | ($area_id:pat, $id:pat) => { 86 | $crate::ItemId { 87 | area_id: $area_id, 88 | id: $id, 89 | } 90 | }; 91 | } 92 | 93 | /// An identifier for a menu item. 94 | #[derive(Clone, Copy, PartialEq, Eq, Hash)] 95 | pub struct ItemId { 96 | #[doc(hidden)] 97 | pub area_id: u32, 98 | #[doc(hidden)] 99 | pub id: u32, 100 | } 101 | 102 | impl ItemId { 103 | #[inline] 104 | pub(crate) fn new(area_id: u32, id: u32) -> Self { 105 | Self { area_id, id } 106 | } 107 | 108 | #[inline] 109 | pub(crate) const fn area_id(&self) -> AreaId { 110 | AreaId::new(self.area_id) 111 | } 112 | 113 | #[inline] 114 | pub(crate) const fn id(&self) -> u32 { 115 | self.id 116 | } 117 | } 118 | 119 | impl fmt::Debug for ItemId { 120 | #[inline] 121 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 122 | f.debug_tuple("ItemId") 123 | .field(&self.area_id) 124 | .field(&self.id) 125 | .finish() 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/autostart.rs: -------------------------------------------------------------------------------- 1 | use std::env::current_exe; 2 | use std::ffi::{OsStr, OsString}; 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use crate::convert::encode_escaped_os_str; 7 | use crate::error::Error; 8 | use crate::error::ErrorKind::*; 9 | use crate::registry::OpenRegistryKey; 10 | use crate::Result; 11 | 12 | /// Helper to register and qeury for a binary to autostart. 13 | #[non_exhaustive] 14 | pub struct AutoStart { 15 | name: Box, 16 | executable: Box, 17 | arguments: Vec, 18 | } 19 | 20 | impl AutoStart { 21 | /// Helper to make the current executable automatically start. 22 | pub fn current_exe(name: N) -> Result 23 | where 24 | N: AsRef, 25 | { 26 | let executable = current_exe().map_err(CurrentExecutable)?; 27 | Ok(Self::new(name, executable)) 28 | } 29 | 30 | /// Construct a new auto start helper. 31 | /// 32 | /// The name should be something suitable for a registry key, like 33 | /// `OxidizeBot`. Note that in the registry it is case-insensitive. 34 | #[inline] 35 | pub fn new(name: N, executable: E) -> Self 36 | where 37 | N: AsRef, 38 | E: AsRef, 39 | { 40 | Self { 41 | name: name.as_ref().into(), 42 | executable: executable.as_ref().into(), 43 | arguments: Vec::new(), 44 | } 45 | } 46 | 47 | /// Append arguments to the executable when autostarting. 48 | pub fn arguments(&mut self, arguments: A) 49 | where 50 | A: IntoIterator, 51 | A::Item: AsRef, 52 | { 53 | self.arguments = arguments 54 | .into_iter() 55 | .map(|a| a.as_ref().to_os_string()) 56 | .collect(); 57 | } 58 | } 59 | 60 | impl AutoStart { 61 | /// Entry for automatic startup. 62 | fn registry_entry(&self) -> Result { 63 | let mut entry = String::new(); 64 | 65 | encode_escaped_os_str(&mut entry, self.executable.as_os_str()) 66 | .map_err(BadAutoStartExecutable)?; 67 | 68 | for argument in &self.arguments { 69 | entry.push(' '); 70 | encode_escaped_os_str(&mut entry, argument).map_err(BadAutoStartArgument)?; 71 | } 72 | 73 | Ok(entry) 74 | } 75 | 76 | /// If the program is installed to run at startup. 77 | pub fn is_installed(&self) -> Result { 78 | let key = OpenRegistryKey::current_user() 79 | .open("Software\\Microsoft\\Windows\\CurrentVersion\\Run") 80 | .map_err(OpenRegistryKey)?; 81 | 82 | let path = match key.get_string(&self.name) { 83 | Ok(path) => path, 84 | Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false), 85 | Err(e) => return Err(Error::new(GetRegistryValue(e))), 86 | }; 87 | 88 | Ok(self.registry_entry()?.as_str() == path) 89 | } 90 | 91 | /// Install the current executable to be automatically started. 92 | pub fn install(&self) -> Result<()> { 93 | let key = OpenRegistryKey::current_user() 94 | .set_value() 95 | .open("Software\\Microsoft\\Windows\\CurrentVersion\\Run") 96 | .map_err(OpenRegistryKey)?; 97 | key.set(&self.name, self.registry_entry()?) 98 | .map_err(SetRegistryKey)?; 99 | Ok(()) 100 | } 101 | 102 | /// Remove the program from automatic startup. 103 | pub fn uninstall(&self) -> Result<()> { 104 | let key = OpenRegistryKey::current_user() 105 | .set_value() 106 | .open("Software\\Microsoft\\Windows\\CurrentVersion\\Run") 107 | .map_err(OpenRegistryKey)?; 108 | key.delete(&self.name).map_err(DeleteRegistryKey)?; 109 | Ok(()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | //! Types related to events produced by this library. 2 | 3 | use crate::{AreaId, Error, ItemId, NotificationId}; 4 | 5 | /// A mouse button. 6 | #[derive(Debug, Clone, Copy)] 7 | #[non_exhaustive] 8 | #[repr(u32)] 9 | pub enum MouseButton { 10 | /// Left mouse button. 11 | Left = 0x1, 12 | /// Right mouse button. 13 | Right = 0x2, 14 | } 15 | 16 | /// A collection of mouse buttons. 17 | #[derive(Debug)] 18 | pub struct MouseButtons(u32); 19 | 20 | impl MouseButtons { 21 | /// A set containing only the right mouse button. 22 | pub(super) const RIGHT: Self = Self(MouseButton::Right as u32); 23 | 24 | /// Copy the buttons collection. 25 | /// 26 | /// Note that we don't want to implement [`Copy`] for this type as it would 27 | /// be too leaky. 28 | pub(super) const fn copy_data(&self) -> Self { 29 | Self(self.0) 30 | } 31 | 32 | /// Create a new empty collection of mouse buttons. 33 | pub(super) const fn empty() -> Self { 34 | Self(0) 35 | } 36 | 37 | /// Create a new collection of mouse buttons. 38 | pub(super) fn from_iter(iter: I) -> Self 39 | where 40 | I: IntoIterator, 41 | { 42 | let mut buttons = 0; 43 | 44 | for button in iter { 45 | buttons |= button as u32; 46 | } 47 | 48 | Self(buttons) 49 | } 50 | 51 | /// Test if the given mouse button is active. 52 | pub fn test(&self, button: MouseButton) -> bool { 53 | self.0 & button as u32 != 0 54 | } 55 | } 56 | 57 | /// An event generated by a mouse click. 58 | #[derive(Debug)] 59 | #[non_exhaustive] 60 | pub struct MouseEvent { 61 | /// Mouse button responsible for the event. 62 | pub buttons: MouseButtons, 63 | } 64 | 65 | /// A clipbaord event. 66 | #[derive(Debug)] 67 | #[non_exhaustive] 68 | pub enum ClipboardEvent { 69 | /// A bitmap has been copied. 70 | BitMap(Vec), 71 | /// A string has been copied. 72 | Text(String), 73 | } 74 | 75 | /// An event emitted by the event loop. 76 | #[derive(Debug)] 77 | #[non_exhaustive] 78 | pub enum Event { 79 | /// Window has been shut down. 80 | Shutdown {}, 81 | /// The menu item identified by [`ItemId`] has been clicked. 82 | MenuItemClicked { 83 | /// The item that was clicked. 84 | item_id: ItemId, 85 | /// The generated event. 86 | event: MouseEvent, 87 | }, 88 | /// An icon has been clicked. 89 | IconClicked { 90 | /// The area that was clicked. 91 | area_id: AreaId, 92 | /// The generated event. 93 | event: MouseEvent, 94 | }, 95 | /// Indicates that the notification with the associated token has been clicked. 96 | NotificationClicked { 97 | /// The area the notification belonged to. 98 | area_id: AreaId, 99 | /// The identifier of the notification. 100 | id: NotificationId, 101 | /// The generated event. 102 | event: MouseEvent, 103 | }, 104 | /// The notification associated with the given token either timed out or was dismissed. 105 | NotificationDismissed { 106 | /// The area from which the dismissed notification originated. 107 | area_id: AreaId, 108 | /// The identifier of the dismissed notification. 109 | id: NotificationId, 110 | }, 111 | /// The system clipboard has been modified. 112 | Clipboard { 113 | /// The generated clipboard event. 114 | event: ClipboardEvent, 115 | }, 116 | /// Data was copied to the current process remotely using 117 | /// [`Window::copy_data`]. 118 | /// 119 | /// [`Window::copy_data`]: crate::Window::copy_data 120 | CopyData { 121 | /// The type parameter passed when sending the data. 122 | ty: usize, 123 | /// The data. 124 | data: Vec, 125 | }, 126 | /// A non-fatal error has been reported. 127 | Error { 128 | /// The reported error. 129 | error: Error, 130 | }, 131 | } 132 | -------------------------------------------------------------------------------- /src/popup_menu.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::event::{MouseButton, MouseButtons}; 4 | use crate::menu_item::MenuItemKind; 5 | use crate::{AreaId, ItemId, MenuItem}; 6 | 7 | /// The structure of a popup menu. 8 | pub struct PopupMenu { 9 | area_id: AreaId, 10 | pub(super) menu: Vec, 11 | /// The default item in the menu. 12 | pub(super) default: Option, 13 | /// Mouse buttons which will be accepted to open the menu. 14 | pub(super) open_menu: MouseButtons, 15 | } 16 | 17 | impl PopupMenu { 18 | /// Construct a new empt popup menu. 19 | pub(super) fn new(area_id: AreaId) -> Self { 20 | Self { 21 | area_id, 22 | menu: Vec::new(), 23 | default: None, 24 | open_menu: MouseButtons::RIGHT, 25 | } 26 | } 27 | 28 | /// Specify a collection of mouse buttons which will be accepted to open the 29 | /// context menu. 30 | /// 31 | /// By default this is [`MouseButton::Right`]. 32 | /// 33 | /// # Examples 34 | /// 35 | /// ``` 36 | /// use winctx::CreateWindow; 37 | /// use winctx::event::MouseButton; 38 | /// 39 | /// let mut window = CreateWindow::new("se.tedro.Example");; 40 | /// let area = window.new_area(); 41 | /// 42 | /// let menu = area.popup_menu().open_menu([MouseButton::Left, MouseButton::Right]); 43 | /// menu.push_entry("Example Application"); 44 | /// menu.push_separator(); 45 | /// menu.push_entry("Exit..."); 46 | /// ``` 47 | pub fn open_menu(&mut self, buttons: I) -> &mut Self 48 | where 49 | I: IntoIterator, 50 | { 51 | self.open_menu = MouseButtons::from_iter(buttons); 52 | self 53 | } 54 | 55 | /// Construct a menu entry. 56 | /// 57 | /// The `default` parameter indicates whether the entry shoudl be 58 | /// highlighted. 59 | /// 60 | /// This returns a token which can be matched against the token returned in 61 | /// [`Event::MenuItemClicked`]. 62 | /// 63 | /// [`Event::MenuItemClicked`]: crate::Event::MenuItemClicked 64 | /// 65 | /// # Examples 66 | /// 67 | /// ``` 68 | /// use winctx::CreateWindow; 69 | /// 70 | /// let mut window = CreateWindow::new("se.tedro.Example");; 71 | /// let area = window.new_area(); 72 | /// 73 | /// let menu = area.popup_menu(); 74 | /// menu.push_entry("Example Application"); 75 | /// menu.push_separator(); 76 | /// menu.push_entry("Exit..."); 77 | /// ``` 78 | pub fn push_entry(&mut self, text: T) -> &mut MenuItem 79 | where 80 | T: fmt::Display, 81 | { 82 | let menu_id = ItemId::new(self.area_id.id(), self.menu.len() as u32); 83 | self.menu.push(MenuItem::new( 84 | menu_id, 85 | MenuItemKind::String { 86 | text: text.to_string(), 87 | }, 88 | )); 89 | self.menu.last_mut().unwrap() 90 | } 91 | 92 | /// Construct a menu separator. 93 | /// 94 | /// # Examples 95 | /// 96 | /// ```no_run 97 | /// use winctx::CreateWindow; 98 | /// 99 | /// let mut window = CreateWindow::new("se.tedro.Example");; 100 | /// let area = window.new_area(); 101 | /// 102 | /// let menu = area.popup_menu(); 103 | /// menu.push_separator(); 104 | /// ``` 105 | pub fn push_separator(&mut self) -> &mut MenuItem { 106 | let menu_id = ItemId::new(self.area_id.id(), self.menu.len() as u32); 107 | self.menu 108 | .push(MenuItem::new(menu_id, MenuItemKind::Separator)); 109 | self.menu.last_mut().unwrap() 110 | } 111 | 112 | /// Set the default item in the menu. 113 | /// 114 | /// # Examples 115 | /// 116 | /// ```no_run 117 | /// use winctx::CreateWindow; 118 | /// 119 | /// let mut window = CreateWindow::new("se.tedro.Example"); 120 | /// let area = window.new_area(); 121 | /// 122 | /// let menu = area.popup_menu(); 123 | /// let first = menu.push_entry("Example Application").id(); 124 | /// menu.push_separator(); 125 | /// menu.push_entry("Exit..."); 126 | /// menu.set_default(first); 127 | /// ``` 128 | pub fn set_default(&mut self, menu_item_id: ItemId) { 129 | if self.area_id == menu_item_id.area_id() { 130 | self.default = Some(menu_item_id.id()); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/clipboard/mod.rs: -------------------------------------------------------------------------------- 1 | pub(super) use self::clipboard_format::ClipboardFormat; 2 | mod clipboard_format; 3 | 4 | use std::ffi::c_void; 5 | use std::io; 6 | use std::marker::PhantomData; 7 | use std::ops::Range; 8 | use std::slice; 9 | 10 | use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; 11 | use windows_sys::Win32::Foundation::{FALSE, HANDLE, HWND}; 12 | use windows_sys::Win32::System::DataExchange::GetUpdatedClipboardFormats; 13 | use windows_sys::Win32::System::DataExchange::{CloseClipboard, GetClipboardData, OpenClipboard}; 14 | use windows_sys::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock}; 15 | 16 | /// An open clipboard handle. 17 | pub(crate) struct Clipboard; 18 | 19 | impl Clipboard { 20 | /// Construct a new clipboard around the given window. 21 | /// 22 | /// # Safety 23 | /// 24 | /// The window handle must be valid and no other component must've acquired the clipboard. 25 | pub(super) unsafe fn new(handle: HWND) -> io::Result { 26 | if OpenClipboard(handle) == FALSE { 27 | return Err(io::Error::last_os_error()); 28 | } 29 | 30 | Ok(Self) 31 | } 32 | 33 | /// Enumerate available clipboard formats. 34 | pub(super) fn updated_formats() -> UpdatedFormats { 35 | unsafe { 36 | let mut formats = [0u32; N]; 37 | let mut actual = 0; 38 | GetUpdatedClipboardFormats(formats.as_mut_ptr(), 16, &mut actual); 39 | 40 | UpdatedFormats { 41 | formats, 42 | range: 0..actual as usize, 43 | } 44 | } 45 | } 46 | 47 | /// Acquire data with the specified format. 48 | pub(crate) fn data(&self, format: ClipboardFormat) -> io::Result> { 49 | // SAFETY: This is safe as long as construction is correct. 50 | unsafe { 51 | let handle = GetClipboardData(format.as_u16() as u32); 52 | 53 | if handle == 0 || handle == INVALID_HANDLE_VALUE { 54 | return Err(io::Error::last_os_error()); 55 | } 56 | 57 | Ok(Data { 58 | handle, 59 | _marker: PhantomData, 60 | }) 61 | } 62 | } 63 | } 64 | 65 | impl Drop for Clipboard { 66 | fn drop(&mut self) { 67 | unsafe { 68 | _ = CloseClipboard(); 69 | } 70 | } 71 | } 72 | 73 | /// A clipboard data handle. 74 | pub(super) struct Data<'a> { 75 | handle: HANDLE, 76 | _marker: PhantomData<&'a Clipboard>, 77 | } 78 | 79 | impl Data<'_> { 80 | pub(super) fn lock(&self) -> io::Result> { 81 | // SAFETY: Construction of Clipboard ensures that this is used 82 | // correctly. 83 | unsafe { 84 | let handle = GlobalLock(self.handle as *mut _); 85 | 86 | if handle.is_null() { 87 | return Err(io::Error::last_os_error()); 88 | } 89 | 90 | Ok(Lock { 91 | handle, 92 | _marker: PhantomData, 93 | }) 94 | } 95 | } 96 | } 97 | 98 | pub(super) struct Lock<'a> { 99 | handle: *mut c_void, 100 | _marker: PhantomData<&'a ()>, 101 | } 102 | 103 | impl Lock<'_> { 104 | /// Coerce locked data into a byte slice. 105 | pub(super) fn as_slice(&self) -> &[u8] { 106 | // SAFETY: Lock has been correctly acquired. 107 | unsafe { 108 | let len = GlobalSize(self.handle) as usize; 109 | slice::from_raw_parts(self.handle.cast(), len) 110 | } 111 | } 112 | 113 | /// Coerce locked data into a wide slice. 114 | pub(super) fn as_wide_slice(&self) -> &[u16] { 115 | // SAFETY: Lock has been correctly acquired. 116 | unsafe { 117 | let len = GlobalSize(self.handle) as usize; 118 | debug_assert!(len % 2 == 0, "a wide slice must be a multiple of two"); 119 | slice::from_raw_parts(self.handle.cast(), len / 2) 120 | } 121 | } 122 | } 123 | 124 | impl Drop for Lock<'_> { 125 | fn drop(&mut self) { 126 | // SAFETY: Lock has been correctly acquired. 127 | unsafe { 128 | _ = GlobalUnlock(self.handle); 129 | } 130 | } 131 | } 132 | 133 | /// An iterator over clipboard formats. 134 | pub(super) struct UpdatedFormats { 135 | formats: [u32; N], 136 | range: Range, 137 | } 138 | 139 | impl Iterator for UpdatedFormats { 140 | type Item = ClipboardFormat; 141 | 142 | #[inline] 143 | fn next(&mut self) -> Option { 144 | let index = self.range.next()?; 145 | let format = self.formats[index]; 146 | Some(ClipboardFormat::new(format as u16)) 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/window_loop/menu_manager.rs: -------------------------------------------------------------------------------- 1 | use std::mem::MaybeUninit; 2 | use std::ptr; 3 | 4 | use tokio::sync::mpsc::UnboundedSender; 5 | use windows_sys::Win32::Foundation::FALSE; 6 | use windows_sys::Win32::UI::Shell as shellapi; 7 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 8 | use windows_sys::Win32::UI::WindowsAndMessaging::{HMENU, MSG}; 9 | 10 | use crate::event::MouseButton; 11 | use crate::event::MouseButtons; 12 | use crate::event::MouseEvent; 13 | use crate::AreaId; 14 | 15 | use super::messages; 16 | use super::WindowEvent; 17 | 18 | /// Helper to manager clipboard polling state. 19 | pub(super) struct MenuManager<'a> { 20 | events_tx: &'a UnboundedSender, 21 | menus: &'a [Option<(winuser::HMENU, MouseButtons)>], 22 | } 23 | 24 | impl<'a> MenuManager<'a> { 25 | pub(super) fn new( 26 | events_tx: &'a UnboundedSender, 27 | menus: &'a [Option<(winuser::HMENU, MouseButtons)>], 28 | ) -> Self { 29 | Self { events_tx, menus } 30 | } 31 | 32 | pub(super) unsafe fn dispatch(&mut self, msg: &MSG) -> bool { 33 | match msg.message { 34 | messages::ICON_ID => { 35 | let area_id = AreaId::new(msg.wParam as u32); 36 | 37 | match msg.lParam as u32 { 38 | // Balloon clicked. 39 | shellapi::NIN_BALLOONUSERCLICK => { 40 | let event = MouseEvent { 41 | buttons: MouseButtons::empty(), 42 | }; 43 | 44 | _ = self 45 | .events_tx 46 | .send(WindowEvent::NotificationClicked(area_id, event)); 47 | return true; 48 | } 49 | // Balloon timed out. 50 | shellapi::NIN_BALLOONTIMEOUT => { 51 | _ = self 52 | .events_tx 53 | .send(WindowEvent::NotificationDismissed(area_id)); 54 | return true; 55 | } 56 | winuser::WM_LBUTTONUP | winuser::WM_RBUTTONUP => { 57 | let button = match msg.lParam as u32 { 58 | winuser::WM_LBUTTONUP => MouseButton::Left, 59 | winuser::WM_RBUTTONUP => MouseButton::Right, 60 | _ => return true, 61 | }; 62 | 63 | _ = self.events_tx.send(WindowEvent::IconClicked( 64 | area_id, 65 | MouseEvent { 66 | buttons: MouseButtons::from_iter([button]), 67 | }, 68 | )); 69 | 70 | let Some(Some((hmenu, open_menu))) = self.menus.get(area_id.id() as usize) 71 | else { 72 | return true; 73 | }; 74 | 75 | if !open_menu.test(button) { 76 | return true; 77 | } 78 | 79 | let mut p = MaybeUninit::zeroed(); 80 | 81 | if winuser::GetCursorPos(p.as_mut_ptr()) == FALSE { 82 | return true; 83 | } 84 | 85 | let p = p.assume_init(); 86 | 87 | winuser::SetForegroundWindow(msg.hwnd); 88 | 89 | winuser::TrackPopupMenu( 90 | *hmenu, 91 | 0, 92 | p.x, 93 | p.y, 94 | (winuser::TPM_BOTTOMALIGN | winuser::TPM_LEFTALIGN) as i32, 95 | msg.hwnd, 96 | ptr::null_mut(), 97 | ); 98 | 99 | return true; 100 | } 101 | _ => (), 102 | } 103 | } 104 | winuser::WM_MENUCOMMAND => { 105 | let hmenu = msg.lParam as HMENU; 106 | 107 | let Some(area_id) = self 108 | .menus 109 | .iter() 110 | .position(|el| el.as_ref().map(|(h, _)| *h) == Some(hmenu)) 111 | else { 112 | return true; 113 | }; 114 | 115 | let event = MouseEvent { 116 | buttons: MouseButtons::empty(), 117 | }; 118 | 119 | _ = self.events_tx.send(WindowEvent::MenuItemClicked( 120 | AreaId::new(area_id as u32), 121 | msg.wParam as u32, 122 | event, 123 | )); 124 | 125 | return true; 126 | } 127 | _ => {} 128 | } 129 | 130 | false 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/window/window.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fmt; 3 | use std::io; 4 | use std::ptr; 5 | 6 | use windows_sys::Win32::Foundation::GetLastError; 7 | use windows_sys::Win32::Foundation::HWND; 8 | use windows_sys::Win32::System::DataExchange::COPYDATASTRUCT; 9 | use windows_sys::Win32::UI::WindowsAndMessaging::FindWindowExW; 10 | use windows_sys::Win32::UI::WindowsAndMessaging::SendMessageW; 11 | use windows_sys::Win32::UI::WindowsAndMessaging::WM_COPYDATA; 12 | 13 | use crate::convert::ToWide; 14 | 15 | /// Helper to find windows by title or class. 16 | #[derive(Default)] 17 | pub struct FindWindow { 18 | class: Option>, 19 | title: Option>, 20 | } 21 | 22 | impl FindWindow { 23 | /// Creates a blank find window query. 24 | /// 25 | /// # Examples 26 | /// 27 | /// ```no_run 28 | /// use winctx::window::FindWindow; 29 | /// 30 | /// let mut options = FindWindow::new().class("se.tedro.Example"); 31 | /// ``` 32 | #[inline] 33 | pub fn new() -> Self { 34 | Self::default() 35 | } 36 | 37 | /// Find window by the specified class name. 38 | /// 39 | /// The `class` argument matches what has been provided to 40 | /// [`CreateWindow::new`]. 41 | /// 42 | /// [`CreateWindow::new`]: crate::CreateWindow::new 43 | /// 44 | /// # Examples 45 | /// 46 | /// ```no_run 47 | /// use winctx::window::FindWindow; 48 | /// 49 | /// let window = FindWindow::new().class("se.tedro.Example").find()?; 50 | /// # Ok::<_, std::io::Error>(()) 51 | /// ``` 52 | pub fn class(&mut self, class: C) -> &mut Self 53 | where 54 | C: AsRef, 55 | { 56 | self.class = Some(class.as_ref().to_wide_null()); 57 | self 58 | } 59 | 60 | /// Find window by the specified class name. 61 | /// 62 | /// # Examples 63 | /// 64 | /// ```no_run 65 | /// use winctx::window::FindWindow; 66 | /// 67 | /// let window = FindWindow::new().title("Example Application").find()?; 68 | /// # Ok::<_, std::io::Error>(()) 69 | /// ``` 70 | pub fn title(&mut self, title: T) -> &mut Self 71 | where 72 | T: AsRef, 73 | { 74 | self.title = Some(title.as_ref().to_wide_null()); 75 | self 76 | } 77 | 78 | /// Construct an iterator over all matching windows. 79 | /// 80 | /// # Examples 81 | /// 82 | /// ```no_run 83 | /// use winctx::window::FindWindow; 84 | /// 85 | /// let window = FindWindow::new().class("se.tedro.Example").find()?; 86 | /// # Ok::<_, std::io::Error>(()) 87 | /// ``` 88 | pub fn find(&self) -> io::Result> { 89 | // SAFETY: All arguments are correctly popuplated by this builder. 90 | unsafe { 91 | let hwnd = FindWindowExW( 92 | 0, 93 | 0, 94 | self.class.as_ref().map_or(ptr::null(), |c| c.as_ptr()), 95 | self.title.as_ref().map_or(ptr::null(), |c| c.as_ptr()), 96 | ); 97 | 98 | if hwnd == 0 { 99 | let code = GetLastError(); 100 | 101 | if code == 0 { 102 | return Ok(None); 103 | } 104 | 105 | return Err(io::Error::from_raw_os_error(code as i32)); 106 | } 107 | 108 | Ok(Some(Window { hwnd })) 109 | } 110 | } 111 | } 112 | 113 | /// Handle to a window on the system. 114 | pub struct Window { 115 | hwnd: HWND, 116 | } 117 | 118 | impl Window { 119 | /// Copy bytes to the given process. 120 | /// 121 | /// Data is received as an [`Event::CopyData`] event. 122 | /// 123 | /// [`Event::CopyData`]: crate::Event::CopyData 124 | /// 125 | /// # Examples 126 | /// 127 | /// ```no_run 128 | /// use winctx::window::FindWindow; 129 | /// 130 | /// let Some(window) = FindWindow::new().class("se.tedro.Example").find()? else { 131 | /// println!("Could not find window"); 132 | /// return Ok(()); 133 | /// }; 134 | /// 135 | /// window.copy_data(42, b"foobar")?; 136 | /// # Ok::<_, std::io::Error>(()) 137 | /// ``` 138 | pub fn copy_data(&self, ty: usize, bytes: &[u8]) -> io::Result<()> { 139 | // SAFETY: All arguments are correctly popuplated by this builder. 140 | unsafe { 141 | let data = COPYDATASTRUCT { 142 | dwData: ty, 143 | cbData: bytes.len() as u32, 144 | lpData: (bytes.as_ptr() as *mut u8).cast(), 145 | }; 146 | 147 | SendMessageW(self.hwnd, WM_COPYDATA, 0, &data as *const _ as isize); 148 | Ok(()) 149 | } 150 | } 151 | } 152 | 153 | impl fmt::Debug for Window { 154 | #[inline] 155 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 156 | f.debug_struct("Window").field("hwnd", &self.hwnd).finish() 157 | } 158 | } 159 | 160 | unsafe impl Send for Window {} 161 | unsafe impl Sync for Window {} 162 | -------------------------------------------------------------------------------- /src/window_loop/window_handle.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::mem::{size_of, MaybeUninit}; 3 | 4 | use windows_sys::Win32::Foundation::{FALSE, HWND}; 5 | use windows_sys::Win32::UI::Shell::{self as shellapi, SHGetStockIconInfo}; 6 | 7 | use crate::convert::copy_wstring_lossy; 8 | use crate::notification::NotificationIcon; 9 | use crate::{AreaId, Notification}; 10 | 11 | use super::{messages, IconHandle}; 12 | 13 | pub(crate) struct WindowHandle { 14 | pub(super) hwnd: HWND, 15 | } 16 | 17 | impl WindowHandle { 18 | fn new_nid(&self, area_id: AreaId) -> shellapi::NOTIFYICONDATAW { 19 | let mut nid: shellapi::NOTIFYICONDATAW = unsafe { MaybeUninit::zeroed().assume_init() }; 20 | nid.cbSize = size_of::() as u32; 21 | nid.hWnd = self.hwnd; 22 | nid.uID = area_id.id(); 23 | nid 24 | } 25 | 26 | pub(crate) fn add_notification(&mut self, area_id: AreaId) -> io::Result<()> { 27 | let mut nid = self.new_nid(area_id); 28 | nid.uFlags = shellapi::NIF_MESSAGE; 29 | nid.uCallbackMessage = messages::ICON_ID; 30 | 31 | let result = unsafe { shellapi::Shell_NotifyIconW(shellapi::NIM_ADD, &nid) }; 32 | 33 | if result == FALSE { 34 | return Err(io::Error::last_os_error()); 35 | } 36 | 37 | Ok(()) 38 | } 39 | 40 | pub(crate) fn delete_notification(&mut self, area_id: AreaId) -> io::Result<()> { 41 | let result = unsafe { 42 | let mut nid = self.new_nid(area_id); 43 | nid.uFlags = shellapi::NIF_ICON; 44 | shellapi::Shell_NotifyIconW(shellapi::NIM_DELETE, &nid) 45 | }; 46 | 47 | if result == FALSE { 48 | return Err(io::Error::last_os_error()); 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | /// Clear out tooltip. 55 | pub(crate) fn modify_notification( 56 | &self, 57 | area_id: AreaId, 58 | icon: Option<&IconHandle>, 59 | tooltip: Option<&str>, 60 | ) -> io::Result<()> { 61 | let mut nid = self.new_nid(area_id); 62 | 63 | if let Some(icon) = icon { 64 | nid.uFlags |= shellapi::NIF_ICON; 65 | nid.hIcon = icon.hicon; 66 | } 67 | 68 | if let Some(tooltip) = tooltip { 69 | nid.uFlags |= shellapi::NIF_TIP | shellapi::NIF_SHOWTIP; 70 | copy_wstring_lossy(&mut nid.szTip, tooltip); 71 | } 72 | 73 | let result = unsafe { shellapi::Shell_NotifyIconW(shellapi::NIM_MODIFY, &nid) }; 74 | 75 | if result == FALSE { 76 | return Err(io::Error::last_os_error()); 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | /// Send a notification. 83 | pub(crate) fn send_notification(&self, area_id: AreaId, n: Notification) -> io::Result<()> { 84 | let mut nid = self.new_nid(area_id); 85 | nid.uFlags = shellapi::NIF_INFO; 86 | 87 | if let Some(title) = n.title { 88 | copy_wstring_lossy(&mut nid.szInfoTitle, title.as_str()); 89 | } 90 | 91 | if let Some(message) = n.message { 92 | copy_wstring_lossy(&mut nid.szInfo, message.as_str()); 93 | } 94 | 95 | if let Some(timeout) = n.timeout { 96 | nid.Anonymous.uTimeout = timeout.as_millis() as u32; 97 | } 98 | 99 | nid.dwInfoFlags = n.options; 100 | 101 | if let Some(icon) = n.icon { 102 | match icon { 103 | NotificationIcon::Info => { 104 | nid.dwInfoFlags |= shellapi::NIIF_INFO; 105 | } 106 | NotificationIcon::Warning => { 107 | nid.dwInfoFlags |= shellapi::NIIF_WARNING; 108 | } 109 | NotificationIcon::Error => { 110 | nid.dwInfoFlags |= shellapi::NIIF_ERROR; 111 | } 112 | NotificationIcon::StockIcon(stock) => unsafe { 113 | let mut sii: shellapi::SHSTOCKICONINFO = MaybeUninit::zeroed().assume_init(); 114 | sii.cbSize = size_of::() as u32; 115 | 116 | let mut opts = shellapi::SHGSI_ICON | n.stock_icon_opts; 117 | 118 | if nid.dwInfoFlags & shellapi::NIIF_LARGE_ICON != 0 { 119 | opts |= shellapi::SHGSI_LARGEICON; 120 | } else { 121 | opts |= shellapi::SHGSI_SMALLICON; 122 | } 123 | 124 | if SHGetStockIconInfo(stock.as_id(), opts, &mut sii) == 0 { 125 | nid.hBalloonIcon = sii.hIcon; 126 | nid.dwInfoFlags |= shellapi::NIIF_USER; 127 | } 128 | }, 129 | }; 130 | } 131 | 132 | let result = unsafe { shellapi::Shell_NotifyIconW(shellapi::NIM_MODIFY, &nid) }; 133 | 134 | if result == FALSE { 135 | return Err(io::Error::last_os_error()); 136 | } 137 | 138 | Ok(()) 139 | } 140 | } 141 | 142 | unsafe impl Send for WindowHandle {} 143 | unsafe impl Sync for WindowHandle {} 144 | -------------------------------------------------------------------------------- /examples/showcase.rs: -------------------------------------------------------------------------------- 1 | use std::pin::pin; 2 | 3 | use tokio::signal::ctrl_c; 4 | use winctx::icon::StockIcon; 5 | use winctx::{CreateWindow, Event}; 6 | 7 | const ICON: &[u8] = include_bytes!("tokio.ico"); 8 | 9 | #[tokio::main] 10 | async fn main() -> winctx::Result<()> { 11 | let mut has_tooltip = true; 12 | let mut is_checked = true; 13 | let mut is_highlighted = true; 14 | 15 | let mut window = CreateWindow::new("se.tedro.Example").window_name("Example Application"); 16 | 17 | let initial_icon = window.icons().insert_buffer(ICON, 22, 22); 18 | 19 | let area = window.new_area().icon(initial_icon); 20 | 21 | if has_tooltip { 22 | area.tooltip("Example Application"); 23 | } 24 | 25 | let menu = area.popup_menu(); 26 | 27 | let title = menu.push_entry("Hello World").id(); 28 | menu.push_entry("Show notification"); 29 | menu.push_entry("Show multiple notifications"); 30 | 31 | menu.push_entry("Toggle tooltip").checked(has_tooltip); 32 | menu.push_entry("Toggle checked").checked(is_checked); 33 | 34 | menu.push_entry("Toggle highlighted") 35 | .checked(is_highlighted) 36 | .highlight(is_highlighted); 37 | 38 | menu.push_separator(); 39 | 40 | let quit = menu.push_entry("Quit").id(); 41 | 42 | menu.set_default(title); 43 | 44 | let area_id = area.id(); 45 | 46 | let (sender, mut event_loop) = window.build().await?; 47 | 48 | let mut ctrl_c = pin!(ctrl_c()); 49 | let mut shutdown = false; 50 | 51 | loop { 52 | let event = tokio::select! { 53 | _ = ctrl_c.as_mut(), if !shutdown => { 54 | sender.shutdown(); 55 | shutdown = true; 56 | continue; 57 | } 58 | event = event_loop.tick() => { 59 | event? 60 | } 61 | }; 62 | 63 | match event { 64 | Event::IconClicked { area_id, event, .. } => { 65 | println!("Icon clicked: {area_id:?}: {event:?}"); 66 | } 67 | Event::MenuItemClicked { item_id, .. } => { 68 | println!("Menu entry clicked: {item_id:?}"); 69 | 70 | match item_id { 71 | winctx::item_id!(0, 1) => { 72 | sender 73 | .notification(area_id) 74 | .title("This is a title") 75 | .message("This is a body") 76 | .large_icon() 77 | .stock_icon(StockIcon::AUDIOFILES) 78 | .icon_link_overlay() 79 | .send(); 80 | } 81 | winctx::item_id!(0, 2) => { 82 | sender.notification(area_id).message("First").send(); 83 | sender.notification(area_id).message("Second").send(); 84 | } 85 | winctx::item_id!(0, 3) => { 86 | if has_tooltip { 87 | sender.modify_area(area_id).tooltip("").send(); 88 | } else { 89 | sender 90 | .modify_area(area_id) 91 | .tooltip("This is a tooltip!") 92 | .send(); 93 | } 94 | 95 | has_tooltip = !has_tooltip; 96 | sender.modify_menu_item(item_id).checked(has_tooltip).send(); 97 | } 98 | winctx::item_id!(0, 4) => { 99 | is_checked = !is_checked; 100 | sender.modify_menu_item(item_id).checked(is_checked).send(); 101 | } 102 | winctx::item_id!(0, 5) => { 103 | is_highlighted = !is_highlighted; 104 | sender 105 | .modify_menu_item(item_id) 106 | .checked(is_highlighted) 107 | .highlight(is_highlighted) 108 | .send(); 109 | } 110 | _ => { 111 | println!("Unhandled: {item_id:?}"); 112 | } 113 | } 114 | 115 | if item_id == quit { 116 | sender.shutdown(); 117 | } 118 | } 119 | Event::NotificationClicked { area_id, id, .. } => { 120 | println!("Balloon clicked: {area_id:?}: {id:?}"); 121 | } 122 | Event::NotificationDismissed { area_id, id, .. } => { 123 | println!("Notification dismissed: {area_id:?}: {id:?}"); 124 | } 125 | Event::CopyData { ty, data, .. } => { 126 | println!("Data of type {ty} copied to process: {data:?}"); 127 | } 128 | Event::Shutdown { .. } => { 129 | println!("Window shut down"); 130 | break; 131 | } 132 | _ => {} 133 | } 134 | } 135 | 136 | Ok(()) 137 | } 138 | -------------------------------------------------------------------------------- /src/window_loop/popup_menu_handle.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::mem::{size_of, MaybeUninit}; 3 | use std::str; 4 | 5 | use windows_sys::Win32::Foundation::{FALSE, TRUE}; 6 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 7 | 8 | use crate::convert::ToWide; 9 | use crate::event::MouseButtons; 10 | use crate::ModifyMenuItem; 11 | 12 | #[repr(C)] 13 | pub(crate) struct PopupMenuHandle { 14 | pub(crate) hmenu: winuser::HMENU, 15 | pub(crate) open_menu: MouseButtons, 16 | } 17 | 18 | impl PopupMenuHandle { 19 | /// Construct a new menu handle. 20 | pub(crate) fn new(open_menu: MouseButtons) -> io::Result { 21 | unsafe { 22 | // Setup menu 23 | let hmenu = winuser::CreatePopupMenu(); 24 | 25 | if hmenu == 0 { 26 | return Err(io::Error::last_os_error()); 27 | } 28 | 29 | let menu = Self { hmenu, open_menu }; 30 | 31 | let m = winuser::MENUINFO { 32 | cbSize: size_of::() as u32, 33 | fMask: winuser::MIM_APPLYTOSUBMENUS | winuser::MIM_STYLE, 34 | dwStyle: winuser::MNS_NOTIFYBYPOS, 35 | cyMax: 0, 36 | hbrBack: 0, 37 | dwContextHelpID: 0, 38 | dwMenuData: 0, 39 | }; 40 | 41 | if winuser::SetMenuInfo(hmenu, &m) == FALSE { 42 | return Err(io::Error::last_os_error()); 43 | } 44 | 45 | Ok(menu) 46 | } 47 | } 48 | 49 | /// Add a menu entry. 50 | pub(crate) fn add_menu_entry( 51 | &self, 52 | menu_item_id: u32, 53 | string: &str, 54 | default: bool, 55 | modify: &ModifyMenuItem, 56 | ) -> io::Result<()> { 57 | let mut item = new_menuitem(); 58 | item.fMask = winuser::MIIM_FTYPE | winuser::MIIM_ID; 59 | item.fType = winuser::MFT_STRING; 60 | item.wID = menu_item_id; 61 | 62 | let string = string.to_wide_null(); 63 | 64 | modify_string(&mut item, Some(&string[..])); 65 | modify_default(&mut item, default); 66 | apply(&mut item, modify); 67 | 68 | let result = unsafe { winuser::InsertMenuItemW(self.hmenu, menu_item_id, TRUE, &item) }; 69 | 70 | if result == FALSE { 71 | return Err(io::Error::last_os_error()); 72 | } 73 | 74 | Ok(()) 75 | } 76 | 77 | /// Add a menu separator at the given index. 78 | pub(crate) fn add_menu_separator( 79 | &self, 80 | menu_item_id: u32, 81 | default: bool, 82 | modify: &ModifyMenuItem, 83 | ) -> io::Result<()> { 84 | let mut item = new_menuitem(); 85 | item.fMask = winuser::MIIM_FTYPE | winuser::MIIM_ID; 86 | item.fType = winuser::MFT_SEPARATOR; 87 | item.wID = menu_item_id; 88 | 89 | apply(&mut item, modify); 90 | modify_default(&mut item, default); 91 | 92 | let result = unsafe { winuser::InsertMenuItemW(self.hmenu, menu_item_id, 1, &item) }; 93 | 94 | if result == FALSE { 95 | return Err(io::Error::last_os_error()); 96 | } 97 | 98 | Ok(()) 99 | } 100 | 101 | /// Set the checked state of the specified menu item. 102 | pub(crate) fn modify_menu_item( 103 | &self, 104 | item_idx: u32, 105 | modify: &ModifyMenuItem, 106 | ) -> io::Result<()> { 107 | let mut item = new_menuitem(); 108 | apply(&mut item, modify); 109 | 110 | let result = unsafe { winuser::SetMenuItemInfoW(self.hmenu, item_idx, 1, &item) }; 111 | 112 | if result == FALSE { 113 | return Err(io::Error::last_os_error()); 114 | } 115 | 116 | Ok(()) 117 | } 118 | } 119 | 120 | fn modify_string(item: &mut winuser::MENUITEMINFOW, string: Option<&[u16]>) { 121 | if let Some(string) = string { 122 | item.fMask |= winuser::MIIM_STRING; 123 | item.dwTypeData = string.as_ptr() as *mut _; 124 | item.cch = string.len().saturating_sub(1) as u32 * 2; 125 | } 126 | } 127 | 128 | fn modify_default(item: &mut winuser::MENUITEMINFOW, default: bool) { 129 | if default { 130 | item.fMask |= winuser::MIIM_STATE; 131 | item.fState |= winuser::MFS_DEFAULT; 132 | } 133 | } 134 | 135 | fn apply(item: &mut winuser::MENUITEMINFOW, modify: &ModifyMenuItem) { 136 | modify_checked(item, modify.checked); 137 | modify_highlight(item, modify.highlight); 138 | } 139 | 140 | fn modify_checked(item: &mut winuser::MENUITEMINFOW, checked: Option) { 141 | if let Some(checked) = checked { 142 | item.fMask |= winuser::MIIM_STATE; 143 | 144 | item.fState |= if checked { 145 | winuser::MFS_CHECKED 146 | } else { 147 | winuser::MFS_UNCHECKED 148 | }; 149 | } 150 | } 151 | 152 | fn modify_highlight(item: &mut winuser::MENUITEMINFOW, highlight: Option) { 153 | if let Some(highlight) = highlight { 154 | item.fMask |= winuser::MIIM_STATE; 155 | 156 | item.fState |= if highlight { 157 | winuser::MFS_HILITE 158 | } else { 159 | winuser::MFS_UNHILITE 160 | }; 161 | } 162 | } 163 | 164 | impl Drop for PopupMenuHandle { 165 | fn drop(&mut self) { 166 | unsafe { 167 | _ = winuser::DestroyMenu(self.hmenu); 168 | } 169 | } 170 | } 171 | 172 | fn new_menuitem() -> winuser::MENUITEMINFOW { 173 | let mut info: winuser::MENUITEMINFOW = unsafe { MaybeUninit::zeroed().assume_init() }; 174 | info.cbSize = size_of::() as u32; 175 | info 176 | } 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # winctx 2 | 3 | [github](https://github.com/udoprog/winctx) 4 | [crates.io](https://crates.io/crates/winctx) 5 | [docs.rs](https://docs.rs/winctx) 6 | [build status](https://github.com/udoprog/winctx/actions?query=branch%3Amain) 7 | 8 | A minimal window context for Rust on Windows. 9 | 10 | *I read msdn so you don't have to*. 11 | 12 | ![The showcase popup menu](https://github.com/udoprog/winctx/blob/main/graphics/showcase.png?raw=true) 13 | 14 | This crate provides a minimalistic method for setting up and running a 15 | [*window*][window]. A window on windows is more like a generic application 16 | framework and doesn't actually need to have any visible elements, but is 17 | necessary to do many of the productive things you might want to do on 18 | Windows. 19 | 20 | Some example of this are: 21 | 22 | * [Register and use a tray icon with a popup menu][showcase], or the 23 | "clickable icons" you see in the bottom right for running applications. 24 | * [Send desktop notifcations][showcase], or "balloons" as they are sometimes 25 | called. 26 | * Interact with the clipboard and [monitor it for changes][clipboard]. 27 | * [Copy data to a remote process][copy-data], allowing for very simple 28 | unidirection IPC. 29 | 30 | There are a few additional APIs provided by this crate because they are also 31 | useful: 32 | 33 | * [Basic safe registry access][registry] allowing for example of the 34 | registration of an application that should be [started automatically] when 35 | the user logs in. 36 | 37 | This crate is an amalgamation and cleanup of code I've copied back and forth 38 | between my projects, so it is fairly opinionated to things I personally find 39 | useful. Not everything will be possible, but if there is something you're 40 | missing and ~~hate being happy~~ enjoy Windows programming feel free to open 41 | an issue or a pull request. 42 | 43 |
44 | 45 | ## Example 46 | 47 | The primary purpose of this crate is to: 48 | * Define a window and its capabilities. I.e. if it should have a context 49 | menu or receive clipboard events. 50 | * Handle incoming [Events][Event] from the window. 51 | 52 | The basic loop looks like this: 53 | 54 | ```rust 55 | use std::pin::pin; 56 | 57 | use tokio::signal::ctrl_c; 58 | use winctx::{Event, CreateWindow}; 59 | 60 | const ICON: &[u8] = include_bytes!("tokio.ico"); 61 | 62 | let mut window = CreateWindow::new("se.tedro.Example") 63 | .window_name("Example Application"); 64 | 65 | let icon = window.icons().insert_buffer(ICON, 22, 22); 66 | 67 | let area = window.new_area().icon(icon); 68 | 69 | let menu = area.popup_menu(); 70 | 71 | let first = menu.push_entry("Example Application").id(); 72 | menu.push_separator(); 73 | let quit = menu.push_entry("Quit").id(); 74 | menu.set_default(first); 75 | 76 | let (sender, mut event_loop) = window 77 | .build() 78 | .await?; 79 | 80 | let mut ctrl_c = pin!(ctrl_c()); 81 | let mut shutdown = false; 82 | 83 | loop { 84 | let event = tokio::select! { 85 | _ = ctrl_c.as_mut(), if !shutdown => { 86 | sender.shutdown(); 87 | shutdown = true; 88 | continue; 89 | } 90 | event = event_loop.tick() => { 91 | event? 92 | } 93 | }; 94 | 95 | match event { 96 | Event::MenuItemClicked { item_id, .. } => { 97 | println!("Menu entry clicked: {item_id:?}"); 98 | 99 | if item_id == quit { 100 | sender.shutdown(); 101 | } 102 | } 103 | Event::Shutdown { .. } => { 104 | println!("Window shut down"); 105 | break; 106 | } 107 | _ => {} 108 | } 109 | } 110 | ``` 111 | 112 | [window]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window 113 | [Event]: https://docs.rs/winctx/latest/winctx/enum.Event.html 114 | [clipboard]: https://github.com/udoprog/winctx/blob/main/examples/clipboard.rs 115 | [copy-data]: https://github.com/udoprog/winctx/blob/main/examples/copy_data.rs 116 | [registry]: https://github.com/udoprog/winctx/blob/main/examples/registry.rs 117 | [showcase]: https://github.com/udoprog/winctx/blob/main/examples/showcase.rs 118 | [started automatically]: https://docs.rs/winctx/latest/winctx/struct.AutoStart.html 119 | -------------------------------------------------------------------------------- /src/event_loop.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use tokio::sync::mpsc; 4 | 5 | use crate::error::Error; 6 | use crate::error::ErrorKind::*; 7 | use crate::item_id::ItemId; 8 | use crate::window_loop::IconHandle; 9 | use crate::window_loop::{WindowEvent, WindowLoop}; 10 | use crate::NotificationId; 11 | use crate::{AreaId, Event, InputEvent, Notification, Result}; 12 | 13 | /// The event loop being run. 14 | #[repr(C)] 15 | pub struct EventLoop { 16 | events_rx: mpsc::UnboundedReceiver, 17 | window_loop: WindowLoop, 18 | icons: Vec, 19 | visible: Option<(AreaId, NotificationId)>, 20 | pending: VecDeque<(AreaId, NotificationId, Notification)>, 21 | } 22 | 23 | impl EventLoop { 24 | pub(crate) fn new( 25 | events_rx: mpsc::UnboundedReceiver, 26 | window_loop: WindowLoop, 27 | icons: Vec, 28 | ) -> Self { 29 | Self { 30 | events_rx, 31 | window_loop, 32 | icons, 33 | visible: None, 34 | pending: VecDeque::new(), 35 | } 36 | } 37 | 38 | fn take_notification(&mut self) -> Result<(AreaId, NotificationId)> { 39 | let (area_id, id) = self.visible.take().ok_or(MissingNotification)?; 40 | 41 | if let Some((area_id, id, n)) = self.pending.pop_front() { 42 | self.visible = Some((area_id, id)); 43 | self.window_loop 44 | .window 45 | .send_notification(area_id, n) 46 | .map_err(SendNotification)?; 47 | } 48 | 49 | Ok((area_id, id)) 50 | } 51 | 52 | /// Tick the event loop. 53 | pub async fn tick(&mut self) -> Result { 54 | if self.window_loop.is_closed() { 55 | return Err(Error::new(WindowClosed)); 56 | }; 57 | 58 | loop { 59 | tokio::select! { 60 | Some(event) = self.events_rx.recv() => { 61 | match event { 62 | InputEvent::ModifyArea { area_id, modify } => { 63 | let icon = modify.icon.and_then(|icon| self.icons.get(icon.as_usize())); 64 | self.window_loop.window.modify_notification(area_id, icon, modify.tooltip.as_deref()).map_err(ModifyNotification)?; 65 | } 66 | InputEvent::ModifyMenuItem { item_id, modify } => { 67 | let Some(menu) = self.window_loop.areas.get(item_id.area_id().id() as usize) else { 68 | continue; 69 | }; 70 | 71 | let Some(popup_menu) = &menu.popup_menu else { 72 | continue; 73 | }; 74 | 75 | popup_menu.modify_menu_item(item_id.id(), &modify).map_err(ModifyMenuItem)?; 76 | } 77 | InputEvent::Notification { area_id, notification_id, notification } => { 78 | if self.visible.is_some() { 79 | self.pending.push_back((area_id, notification_id, notification)); 80 | } else { 81 | self.visible = Some((area_id, notification_id)); 82 | self.window_loop.window.send_notification(area_id, notification).map_err(SendNotification)?; 83 | } 84 | } 85 | InputEvent::Shutdown => { 86 | self.window_loop.join()?; 87 | return Ok(Event::Shutdown {}); 88 | } 89 | } 90 | } 91 | e = self.window_loop.tick() => { 92 | match e { 93 | WindowEvent::MenuItemClicked(area_id, idx, event) => { 94 | return Ok(Event::MenuItemClicked { 95 | item_id: ItemId::new(area_id.id(), idx), 96 | event, 97 | }); 98 | }, 99 | WindowEvent::Clipboard(event) => { 100 | return Ok(Event::Clipboard { event }); 101 | } 102 | WindowEvent::IconClicked(area_id, event) => { 103 | return Ok(Event::IconClicked { area_id, event }); 104 | } 105 | WindowEvent::NotificationClicked(actual_menu_id, event) => { 106 | let (area_id, id) = self.take_notification()?; 107 | debug_assert_eq!(actual_menu_id, area_id); 108 | return Ok(Event::NotificationClicked { 109 | area_id, 110 | id, 111 | event, 112 | }); 113 | } 114 | WindowEvent::NotificationDismissed(actual_menu_id) => { 115 | let (area_id, id) = self.take_notification()?; 116 | debug_assert_eq!(actual_menu_id, area_id); 117 | return Ok(Event::NotificationDismissed { area_id, id }); 118 | } 119 | WindowEvent::CopyData(ty, data) => { 120 | return Ok(Event::CopyData { ty, data }); 121 | } 122 | WindowEvent::Error(error) => { 123 | return Ok(Event::Error { error }); 124 | } 125 | WindowEvent::Shutdown => { 126 | self.window_loop.join()?; 127 | return Ok(Event::Shutdown {}); 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | } 135 | 136 | impl Drop for EventLoop { 137 | fn drop(&mut self) { 138 | _ = self.window_loop.join(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/registry.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{OsStr, OsString}; 2 | use std::io; 3 | use std::mem::MaybeUninit; 4 | use std::ptr; 5 | 6 | use windows_sys::Win32::Foundation::ERROR_SUCCESS; 7 | use windows_sys::Win32::System::Registry::{self as winreg, HKEY}; 8 | 9 | use crate::convert::{FromWide, ToWide}; 10 | 11 | /// An open registry key. 12 | /// 13 | /// This is constructed using [`OpenRegistryKey`]. 14 | pub struct RegistryKey(winreg::HKEY); 15 | 16 | unsafe impl Sync for RegistryKey {} 17 | unsafe impl Send for RegistryKey {} 18 | 19 | /// Helper to open a registry key with the ability to specify desired 20 | /// permissions. 21 | pub struct OpenRegistryKey { 22 | key: HKEY, 23 | desired: u32, 24 | } 25 | 26 | impl OpenRegistryKey { 27 | /// Open the given key in the `HKEY_CURRENT_USER` registry. 28 | pub fn current_user() -> Self { 29 | Self { 30 | key: winreg::HKEY_CURRENT_USER, 31 | desired: winreg::KEY_READ | winreg::KEY_WOW64_64KEY, 32 | } 33 | } 34 | 35 | /// Open the given key in the `HKEY_LOCAL_MACHINE` registry. 36 | pub fn local_machine() -> Self { 37 | Self { 38 | key: winreg::HKEY_LOCAL_MACHINE, 39 | desired: winreg::KEY_READ, 40 | } 41 | } 42 | 43 | /// Enable the `KEY_SET_VALUE` desired access mode. 44 | pub fn set_value(mut self) -> Self { 45 | self.desired |= winreg::KEY_SET_VALUE; 46 | self 47 | } 48 | 49 | /// Internal open implementation. 50 | pub fn open(self, key: K) -> io::Result 51 | where 52 | K: AsRef, 53 | { 54 | let key = key.to_wide_null(); 55 | self.open_inner(&key) 56 | } 57 | 58 | fn open_inner(&self, key: &[u16]) -> io::Result { 59 | unsafe { 60 | let mut hkey = MaybeUninit::uninit(); 61 | 62 | let status = 63 | winreg::RegOpenKeyExW(self.key, key.as_ptr(), 0, self.desired, hkey.as_mut_ptr()); 64 | 65 | if status != ERROR_SUCCESS { 66 | return Err(io::Error::from_raw_os_error(status as i32)); 67 | } 68 | 69 | Ok(RegistryKey(hkey.assume_init())) 70 | } 71 | } 72 | } 73 | 74 | impl RegistryKey { 75 | /// Open the given key in the HKEY_CURRENT_USER scope. 76 | #[inline] 77 | pub fn current_user(key: K) -> io::Result 78 | where 79 | K: AsRef, 80 | { 81 | OpenRegistryKey::current_user().open(key) 82 | } 83 | 84 | /// Open the given key in the HKEY_LOCAL_MACHINE scope. 85 | pub fn local_machine(key: K) -> io::Result 86 | where 87 | K: AsRef, 88 | { 89 | OpenRegistryKey::local_machine().open(key) 90 | } 91 | 92 | /// Get the given value as a string. 93 | pub fn get_string(&self, name: N) -> io::Result 94 | where 95 | N: AsRef, 96 | { 97 | let name = name.to_wide_null(); 98 | let bytes = self.get_wide(&name, winreg::RRF_RT_REG_SZ)?; 99 | // Skip the terminating null. 100 | Ok(OsString::from_wide(&bytes[..bytes.len().saturating_sub(1)])) 101 | } 102 | 103 | fn get_wide(&self, name: &[u16], flags: u32) -> io::Result> { 104 | let mut len = 0; 105 | 106 | unsafe { 107 | let status = winreg::RegGetValueW( 108 | self.0, 109 | ptr::null_mut(), 110 | name.as_ptr(), 111 | flags, 112 | ptr::null_mut(), 113 | ptr::null_mut(), 114 | &mut len, 115 | ); 116 | 117 | if status != ERROR_SUCCESS { 118 | return Err(io::Error::from_raw_os_error(status as i32)); 119 | } 120 | 121 | debug_assert!(len % 2 == 0); 122 | let mut value = vec![0u16; (len / 2) as usize]; 123 | 124 | let status = winreg::RegGetValueW( 125 | self.0, 126 | ptr::null_mut(), 127 | name.as_ptr(), 128 | flags, 129 | ptr::null_mut(), 130 | value.as_mut_ptr().cast(), 131 | &mut len, 132 | ); 133 | 134 | if status != ERROR_SUCCESS { 135 | return Err(io::Error::from_raw_os_error(status as i32)); 136 | } 137 | 138 | debug_assert!(len % 2 == 0); 139 | 140 | // Length reported *including* wide null terminator. 141 | value.truncate((len / 2) as usize); 142 | Ok(value) 143 | } 144 | } 145 | 146 | /// Set the given value. 147 | pub fn set(&self, name: N, value: impl AsRef) -> io::Result<()> 148 | where 149 | N: AsRef, 150 | { 151 | let name = name.to_wide_null(); 152 | let value = value.to_wide_null(); 153 | self.set_inner(&name, &value) 154 | } 155 | 156 | fn set_inner(&self, name: &[u16], value: &[u16]) -> io::Result<()> { 157 | let value_len = value 158 | .len() 159 | .checked_mul(2) 160 | .and_then(|n| u32::try_from(n).ok()) 161 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Value size overflow"))?; 162 | 163 | let status = unsafe { 164 | winreg::RegSetValueExW( 165 | self.0, 166 | name.as_ptr(), 167 | 0, 168 | winreg::REG_SZ, 169 | value.as_ptr().cast(), 170 | value_len, 171 | ) 172 | }; 173 | 174 | if status != 0 { 175 | return Err(io::Error::last_os_error()); 176 | } 177 | 178 | Ok(()) 179 | } 180 | 181 | /// Delete the given registry key. 182 | pub fn delete(&self, name: N) -> io::Result<()> 183 | where 184 | N: AsRef, 185 | { 186 | let name = name.to_wide_null(); 187 | self.delete_inner(&name) 188 | } 189 | 190 | fn delete_inner(&self, name: &[u16]) -> io::Result<()> { 191 | let status = unsafe { winreg::RegDeleteKeyValueW(self.0, ptr::null_mut(), name.as_ptr()) }; 192 | 193 | if status != 0 { 194 | return Err(io::Error::last_os_error()); 195 | } 196 | 197 | Ok(()) 198 | } 199 | } 200 | 201 | impl Drop for RegistryKey { 202 | fn drop(&mut self) { 203 | unsafe { 204 | winreg::RegCloseKey(self.0); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/window_loop/clipboard_manager.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | 3 | use tokio::sync::mpsc::UnboundedSender; 4 | use windows_sys::Win32::Foundation::HWND; 5 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 6 | use windows_sys::Win32::UI::WindowsAndMessaging::MSG; 7 | 8 | use crate::clipboard::{Clipboard, ClipboardFormat}; 9 | use crate::error::{ErrorKind, WindowError}; 10 | use crate::event::ClipboardEvent; 11 | use crate::Error; 12 | 13 | use super::WindowEvent; 14 | 15 | const CLIPBOARD_RETRY_TIMER: usize = 1000; 16 | const RETRY_MILLIS: u32 = 25; 17 | const RETRY_MAX_ATTEMPTS: usize = 10; 18 | 19 | /// A timer used to debounce reacting to clipboard updates. 20 | /// 21 | /// We will only process updates again after this timer has been fired. 22 | const CLIPBOARD_DEBOUNCE_TIMER: usize = 1001; 23 | const DEBOUNCE_MILLIS: u32 = 25; 24 | 25 | /// Helper to manager clipboard polling state. 26 | pub(super) struct ClipboardManager<'a> { 27 | events_tx: &'a UnboundedSender, 28 | attempts: usize, 29 | supported: Option, 30 | } 31 | 32 | impl<'a> ClipboardManager<'a> { 33 | pub(super) fn new(events_tx: &'a UnboundedSender) -> Self { 34 | Self { 35 | events_tx, 36 | attempts: 0, 37 | supported: None, 38 | } 39 | } 40 | 41 | pub(super) unsafe fn dispatch(&mut self, msg: &MSG) -> bool { 42 | match msg.message { 43 | winuser::WM_CLIPBOARDUPDATE => { 44 | // Debounce incoming events. 45 | winuser::SetTimer(msg.hwnd, CLIPBOARD_DEBOUNCE_TIMER, DEBOUNCE_MILLIS, None); 46 | true 47 | } 48 | winuser::WM_TIMER => match msg.wParam { 49 | CLIPBOARD_RETRY_TIMER => { 50 | self.handle_timer(msg.hwnd); 51 | true 52 | } 53 | CLIPBOARD_DEBOUNCE_TIMER => { 54 | winuser::KillTimer(msg.hwnd, CLIPBOARD_DEBOUNCE_TIMER); 55 | self.populate_formats(); 56 | 57 | // We need to incorporate a little delay to avoid "clobbering" 58 | // the clipboard, since it might still be in use by the 59 | // application that just updated it. Including the resources 60 | // that were apart of the update. 61 | // 62 | // Note that there are two distinct states we might clobber: 63 | // * The clipboard itself may only be open by one process at a 64 | // time. 65 | // * Any resources sent over the clipboard may only be locked by 66 | // one process at a time (GlobalLock / GlobalUnlock). 67 | // 68 | // If these overlap in the sending process, it might result in 69 | // it ironically enough failing to send the clipboard data. 70 | // 71 | // So as a best effort, we impose a minor timeout of 72 | // INITIAL_MILLIS to hopefully avoid this. 73 | let Ok(result) = self.poll_clipboard(msg.hwnd) else { 74 | winuser::SetTimer(msg.hwnd, CLIPBOARD_RETRY_TIMER, RETRY_MILLIS, None); 75 | self.attempts = 1; 76 | return true; 77 | }; 78 | 79 | if let Some(clipboard_event) = result { 80 | _ = self.events_tx.send(WindowEvent::Clipboard(clipboard_event)); 81 | } 82 | 83 | true 84 | } 85 | _ => false, 86 | }, 87 | _ => false, 88 | } 89 | } 90 | 91 | fn populate_formats(&mut self) { 92 | self.supported = 'out: { 93 | for format in Clipboard::updated_formats::<16>() { 94 | if matches!( 95 | format, 96 | ClipboardFormat::DIBV5 | ClipboardFormat::TEXT | ClipboardFormat::UNICODETEXT 97 | ) { 98 | break 'out Some(format); 99 | } 100 | } 101 | 102 | None 103 | }; 104 | } 105 | 106 | unsafe fn handle_timer(&mut self, hwnd: HWND) { 107 | let result = match self.poll_clipboard(hwnd) { 108 | Ok(result) => result, 109 | Err(error) => { 110 | if self.attempts >= RETRY_MAX_ATTEMPTS { 111 | winuser::KillTimer(hwnd, CLIPBOARD_RETRY_TIMER); 112 | self.attempts = 0; 113 | _ = self.events_tx.send(WindowEvent::Error(Error::new( 114 | ErrorKind::ClipboardPoll(error), 115 | ))); 116 | } else { 117 | if self.attempts == 0 { 118 | winuser::SetTimer(hwnd, CLIPBOARD_RETRY_TIMER, RETRY_MILLIS, None); 119 | } 120 | 121 | self.attempts += 1; 122 | } 123 | 124 | return; 125 | } 126 | }; 127 | 128 | winuser::KillTimer(hwnd, CLIPBOARD_RETRY_TIMER); 129 | self.attempts = 0; 130 | 131 | if let Some(clipboard_event) = result { 132 | _ = self.events_tx.send(WindowEvent::Clipboard(clipboard_event)); 133 | } 134 | } 135 | 136 | pub(super) unsafe fn poll_clipboard( 137 | &mut self, 138 | hwnd: HWND, 139 | ) -> Result, WindowError> { 140 | let clipboard = Clipboard::new(hwnd).map_err(WindowError::OpenClipboard)?; 141 | 142 | let Some(format) = self.supported else { 143 | return Ok(None); 144 | }; 145 | 146 | let data = clipboard 147 | .data(format) 148 | .map_err(WindowError::GetClipboardData)?; 149 | let data = data.lock().map_err(WindowError::LockClipboardData)?; 150 | 151 | // We've successfully locked the data, so take it from here. 152 | self.supported = None; 153 | 154 | let clipboard_event = match format { 155 | ClipboardFormat::DIBV5 => ClipboardEvent::BitMap(data.as_slice().to_vec()), 156 | ClipboardFormat::TEXT => { 157 | let data = data.as_slice(); 158 | 159 | let data = match data { 160 | [head @ .., 0] => head, 161 | rest => rest, 162 | }; 163 | 164 | let Ok(string) = str::from_utf8(data) else { 165 | return Ok(None); 166 | }; 167 | 168 | ClipboardEvent::Text(string.to_owned()) 169 | } 170 | ClipboardFormat::UNICODETEXT => { 171 | let data = data.as_wide_slice(); 172 | 173 | let data = match data { 174 | [head @ .., 0] => head, 175 | rest => rest, 176 | }; 177 | 178 | let Ok(string) = String::from_utf16(data) else { 179 | return Ok(None); 180 | }; 181 | 182 | ClipboardEvent::Text(string.to_owned()) 183 | } 184 | _ => { 185 | return Ok(None); 186 | } 187 | }; 188 | 189 | Ok(Some(clipboard_event)) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [github](https://github.com/udoprog/winctx) 2 | //! [crates.io](https://crates.io/crates/winctx) 3 | //! [docs.rs](https://docs.rs/winctx) 4 | //! 5 | //! A minimal window context for Rust on Windows. 6 | //! 7 | //! *I read msdn so you don't have to*. 8 | //! 9 | //! ![The showcase popup menu](https://github.com/udoprog/winctx/blob/main/graphics/showcase.png?raw=true) 10 | //! 11 | //! This crate provides a minimalistic method for setting up and running a 12 | //! [*window*][window]. A window on windows is more like a generic application 13 | //! framework and doesn't actually need to have any visible elements, but is 14 | //! necessary to do many of the productive things you might want to do on 15 | //! Windows. 16 | //! 17 | //! Some example of this are: 18 | //! 19 | //! * [Register and use a tray icon with a popup menu][showcase], or the 20 | //! "clickable icons" you see in the bottom right for running applications. 21 | //! * [Send desktop notifcations][showcase], or "balloons" as they are sometimes 22 | //! called. 23 | //! * Interact with the clipboard and [monitor it for changes][clipboard]. 24 | //! * [Copy data to a remote process][copy-data], allowing for very simple 25 | //! unidirection IPC. 26 | //! 27 | //! There are a few additional APIs provided by this crate because they are also 28 | //! useful: 29 | //! 30 | //! * [Basic safe registry access][registry] allowing for example of the 31 | //! registration of an application that should be [started automatically] when 32 | //! the user logs in. 33 | //! 34 | //! This crate is an amalgamation and cleanup of code I've copied back and forth 35 | //! between my projects, so it is fairly opinionated to things I personally find 36 | //! useful. Not everything will be possible, but if there is something you're 37 | //! missing and ~~hate being happy~~ enjoy Windows programming feel free to open 38 | //! an issue or a pull request. 39 | //! 40 | //!
41 | //! 42 | //! ## Example 43 | //! 44 | //! The primary purpose of this crate is to: 45 | //! * Define a window and its capabilities. I.e. if it should have a context 46 | //! menu or receive clipboard events. 47 | //! * Handle incoming [Events][Event] from the window. 48 | //! 49 | //! The basic loop looks like this: 50 | //! 51 | //! ```no_run 52 | //! use std::pin::pin; 53 | //! 54 | //! use tokio::signal::ctrl_c; 55 | //! use winctx::{Event, CreateWindow}; 56 | //! 57 | //! # macro_rules! include_bytes { ($path:literal) => { &[] } } 58 | //! const ICON: &[u8] = include_bytes!("tokio.ico"); 59 | //! 60 | //! # #[tokio::main] async fn main() -> winctx::Result<()> { 61 | //! let mut window = CreateWindow::new("se.tedro.Example") 62 | //! .window_name("Example Application"); 63 | //! 64 | //! let icon = window.icons().insert_buffer(ICON, 22, 22); 65 | //! 66 | //! let area = window.new_area().icon(icon); 67 | //! 68 | //! let menu = area.popup_menu(); 69 | //! 70 | //! let first = menu.push_entry("Example Application").id(); 71 | //! menu.push_separator(); 72 | //! let quit = menu.push_entry("Quit").id(); 73 | //! menu.set_default(first); 74 | //! 75 | //! let (sender, mut event_loop) = window 76 | //! .build() 77 | //! .await?; 78 | //! 79 | //! let mut ctrl_c = pin!(ctrl_c()); 80 | //! let mut shutdown = false; 81 | //! 82 | //! loop { 83 | //! let event = tokio::select! { 84 | //! _ = ctrl_c.as_mut(), if !shutdown => { 85 | //! sender.shutdown(); 86 | //! shutdown = true; 87 | //! continue; 88 | //! } 89 | //! event = event_loop.tick() => { 90 | //! event? 91 | //! } 92 | //! }; 93 | //! 94 | //! match event { 95 | //! Event::MenuItemClicked { item_id, .. } => { 96 | //! println!("Menu entry clicked: {item_id:?}"); 97 | //! 98 | //! if item_id == quit { 99 | //! sender.shutdown(); 100 | //! } 101 | //! } 102 | //! Event::Shutdown { .. } => { 103 | //! println!("Window shut down"); 104 | //! break; 105 | //! } 106 | //! _ => {} 107 | //! } 108 | //! } 109 | //! # Ok(()) } 110 | //! ``` 111 | //! 112 | //! [window]: https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window 113 | //! [Event]: https://docs.rs/winctx/latest/winctx/enum.Event.html 114 | //! [clipboard]: https://github.com/udoprog/winctx/blob/main/examples/clipboard.rs 115 | //! [copy-data]: https://github.com/udoprog/winctx/blob/main/examples/copy_data.rs 116 | //! [registry]: https://github.com/udoprog/winctx/blob/main/examples/registry.rs 117 | //! [showcase]: https://github.com/udoprog/winctx/blob/main/examples/showcase.rs 118 | //! [started automatically]: https://docs.rs/winctx/latest/winctx/struct.AutoStart.html 119 | 120 | #![allow(clippy::module_inception)] 121 | #![deny(missing_docs)] 122 | 123 | /// Convenient result alias for this crate. 124 | pub type Result = core::result::Result; 125 | 126 | mod clipboard; 127 | mod convert; 128 | 129 | #[doc(inline)] 130 | pub use self::registry::{OpenRegistryKey, RegistryKey}; 131 | mod registry; 132 | 133 | #[doc(inline)] 134 | pub use self::window::Window; 135 | pub mod window; 136 | 137 | mod window_loop; 138 | 139 | #[doc(inline)] 140 | use self::notification::Notification; 141 | mod notification; 142 | 143 | #[doc(inline)] 144 | pub use self::error::Error; 145 | mod error; 146 | 147 | #[doc(inline)] 148 | pub use self::item_id::ItemId; 149 | mod item_id; 150 | 151 | #[doc(inline)] 152 | pub use self::notification_id::NotificationId; 153 | mod notification_id; 154 | 155 | #[doc(inline)] 156 | pub use self::area_id::AreaId; 157 | mod area_id; 158 | 159 | #[doc(inline)] 160 | pub use self::event_loop::EventLoop; 161 | mod event_loop; 162 | 163 | #[doc(inline)] 164 | pub use self::event::Event; 165 | pub mod event; 166 | 167 | #[doc(inline)] 168 | pub use self::create_window::CreateWindow; 169 | mod create_window; 170 | 171 | pub mod area; 172 | pub mod icons; 173 | 174 | #[doc(inline)] 175 | pub use self::popup_menu::PopupMenu; 176 | mod popup_menu; 177 | 178 | #[doc(inline)] 179 | use self::icon_buffer::IconBuffer; 180 | mod icon_buffer; 181 | 182 | #[doc(inline)] 183 | pub use self::autostart::AutoStart; 184 | mod autostart; 185 | 186 | pub mod tools; 187 | 188 | #[doc(inline)] 189 | pub use self::named_mutex::NamedMutex; 190 | mod named_mutex; 191 | 192 | #[doc(inline)] 193 | use self::menu_item::MenuItem; 194 | pub(crate) mod menu_item; 195 | 196 | #[doc(inline)] 197 | pub use self::icon::IconId; 198 | pub mod icon; 199 | 200 | #[doc(inline)] 201 | use self::modify_area::ModifyArea; 202 | mod modify_area; 203 | 204 | #[doc(inline)] 205 | use self::modify_menu_item::ModifyMenuItem; 206 | mod modify_menu_item; 207 | 208 | use self::sender::InputEvent; 209 | #[doc(inline)] 210 | pub use self::sender::Sender; 211 | pub mod sender; 212 | 213 | #[cfg_attr(windows, path = "windows/real.rs")] 214 | #[cfg_attr(not(windows), path = "windows/fake.rs")] 215 | mod windows; 216 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::char::DecodeUtf16Error; 2 | use std::fmt; 3 | use std::io; 4 | 5 | /// The error raised by this library. 6 | #[derive(Debug)] 7 | pub struct Error { 8 | kind: ErrorKind, 9 | } 10 | 11 | impl Error { 12 | /// Construct a new error. 13 | pub(super) fn new(kind: K) -> Self 14 | where 15 | ErrorKind: From, 16 | { 17 | Self { kind: kind.into() } 18 | } 19 | } 20 | 21 | impl From for Error { 22 | #[inline] 23 | fn from(kind: ErrorKind) -> Self { 24 | Self { kind } 25 | } 26 | } 27 | 28 | impl fmt::Display for Error { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | match &self.kind { 31 | ErrorKind::WindowSetup(..) => write!(f, "Failed to set up window"), 32 | ErrorKind::ThreadError(..) => write!(f, "Error in window thread"), 33 | ErrorKind::ClipboardPoll(..) => write!(f, "Failed to poll clipboard"), 34 | ErrorKind::DeleteRegistryKey(..) => write!(f, "Failed to delete registry key"), 35 | ErrorKind::GetRegistryValue(..) => write!(f, "Failed to get registry value"), 36 | ErrorKind::SetRegistryKey(..) => write!(f, "Failed to set registry key"), 37 | ErrorKind::CurrentExecutable(..) => write!(f, "Could not get current executable"), 38 | ErrorKind::BuildPopupMenu(..) => write!(f, "Failed to build popup menu"), 39 | ErrorKind::SetupIcons(..) => write!(f, "Failed to setup icons"), 40 | ErrorKind::SetupMenu(..) => write!(f, "Failed to setup menu"), 41 | ErrorKind::ModifyMenuItem(..) => write!(f, "Failed to modify menu item"), 42 | ErrorKind::AddNotification(..) => write!(f, "Failed to add notification area"), 43 | ErrorKind::ModifyNotification(..) => write!(f, "Failed to modify notification area"), 44 | ErrorKind::SendNotification(..) => write!(f, "Failed to send notification"), 45 | ErrorKind::CreateMutex(..) => write!(f, "Failed to construct mutex"), 46 | ErrorKind::OpenRegistryKey(..) => write!(f, "Failed to open registry key"), 47 | ErrorKind::MissingNotification => write!(f, "Missing notification state"), 48 | ErrorKind::BadAutoStartExecutable(..) => write!(f, "Bad autostart executable"), 49 | ErrorKind::BadAutoStartArgument(..) => write!(f, "Bad autostart argument"), 50 | ErrorKind::WindowClosed => write!(f, "Window has been closed"), 51 | ErrorKind::PostMessageDestroy => write!(f, "Failed to post destroy window message"), 52 | } 53 | } 54 | } 55 | 56 | impl std::error::Error for Error { 57 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 58 | match &self.kind { 59 | ErrorKind::WindowSetup(error) => Some(error), 60 | ErrorKind::ThreadError(error) => Some(error), 61 | ErrorKind::ClipboardPoll(error) => Some(error), 62 | ErrorKind::DeleteRegistryKey(error) => Some(error), 63 | ErrorKind::GetRegistryValue(error) => Some(error), 64 | ErrorKind::SetRegistryKey(error) => Some(error), 65 | ErrorKind::CurrentExecutable(error) => Some(error), 66 | ErrorKind::BuildPopupMenu(error) => Some(error), 67 | ErrorKind::SetupIcons(error) => Some(error), 68 | ErrorKind::SetupMenu(error) => Some(error), 69 | ErrorKind::ModifyMenuItem(error) => Some(error), 70 | ErrorKind::AddNotification(error) => Some(error), 71 | ErrorKind::ModifyNotification(error) => Some(error), 72 | ErrorKind::SendNotification(error) => Some(error), 73 | ErrorKind::CreateMutex(error) => Some(error), 74 | ErrorKind::OpenRegistryKey(error) => Some(error), 75 | ErrorKind::BadAutoStartExecutable(error) => Some(error), 76 | ErrorKind::BadAutoStartArgument(error) => Some(error), 77 | _ => None, 78 | } 79 | } 80 | } 81 | 82 | #[derive(Debug)] 83 | pub(super) enum WindowError { 84 | Init(io::Error), 85 | AddClipboardFormatListener(io::Error), 86 | OpenClipboard(io::Error), 87 | GetClipboardData(io::Error), 88 | LockClipboardData(io::Error), 89 | ClassNameTooLong(usize), 90 | ThreadPanicked, 91 | ThreadExited, 92 | } 93 | 94 | impl fmt::Display for WindowError { 95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 96 | match self { 97 | WindowError::Init(..) => write!(f, "Failed to initialize window"), 98 | WindowError::AddClipboardFormatListener(..) => { 99 | write!(f, "Failed to add clipboard format listener") 100 | } 101 | WindowError::OpenClipboard(..) => write!(f, "Failed to open clipboard"), 102 | WindowError::GetClipboardData(..) => write!(f, "Failed to get clipboard data"), 103 | WindowError::LockClipboardData(..) => write!(f, "Failed to lock clipboard data"), 104 | WindowError::ClassNameTooLong(len) => write!( 105 | f, 106 | "Class name of length {len} is longer than maximum of 256 bytes" 107 | ), 108 | WindowError::ThreadPanicked => write!(f, "Window thread panicked"), 109 | WindowError::ThreadExited => write!(f, "Window thread unexpectedly exited"), 110 | } 111 | } 112 | } 113 | 114 | impl std::error::Error for WindowError { 115 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 116 | match self { 117 | WindowError::Init(error) => Some(error), 118 | WindowError::AddClipboardFormatListener(error) => Some(error), 119 | WindowError::OpenClipboard(error) => Some(error), 120 | WindowError::GetClipboardData(error) => Some(error), 121 | WindowError::LockClipboardData(error) => Some(error), 122 | WindowError::ClassNameTooLong(..) => None, 123 | WindowError::ThreadPanicked => None, 124 | WindowError::ThreadExited => None, 125 | } 126 | } 127 | } 128 | 129 | #[derive(Debug)] 130 | pub(super) enum ErrorKind { 131 | WindowSetup(WindowError), 132 | ThreadError(WindowError), 133 | ClipboardPoll(WindowError), 134 | DeleteRegistryKey(io::Error), 135 | GetRegistryValue(io::Error), 136 | SetRegistryKey(io::Error), 137 | CurrentExecutable(io::Error), 138 | BuildPopupMenu(io::Error), 139 | SetupIcons(SetupIconsError), 140 | SetupMenu(SetupMenuError), 141 | ModifyMenuItem(io::Error), 142 | AddNotification(io::Error), 143 | ModifyNotification(io::Error), 144 | SendNotification(io::Error), 145 | CreateMutex(io::Error), 146 | OpenRegistryKey(io::Error), 147 | MissingNotification, 148 | BadAutoStartExecutable(DecodeUtf16Error), 149 | BadAutoStartArgument(DecodeUtf16Error), 150 | WindowClosed, 151 | PostMessageDestroy, 152 | } 153 | 154 | #[derive(Debug)] 155 | pub(super) enum SetupIconsError { 156 | BuildIcon(io::Error), 157 | } 158 | 159 | impl fmt::Display for SetupIconsError { 160 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 161 | match self { 162 | Self::BuildIcon(..) => write!(f, "Failed to construct icon"), 163 | } 164 | } 165 | } 166 | 167 | impl std::error::Error for SetupIconsError { 168 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 169 | match self { 170 | Self::BuildIcon(error) => Some(error), 171 | } 172 | } 173 | } 174 | 175 | #[derive(Debug)] 176 | pub(super) enum SetupMenuError { 177 | AddMenuEntry(usize, io::Error), 178 | AddMenuSeparator(usize, io::Error), 179 | } 180 | 181 | impl fmt::Display for SetupMenuError { 182 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 183 | match self { 184 | Self::AddMenuEntry(index, ..) => { 185 | write!(f, "Failed to add menu entry {index}") 186 | } 187 | Self::AddMenuSeparator(index, ..) => { 188 | write!(f, "Failed to add menu separator {index}") 189 | } 190 | } 191 | } 192 | } 193 | 194 | impl std::error::Error for SetupMenuError { 195 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 196 | match self { 197 | Self::AddMenuEntry(_, error) => Some(error), 198 | Self::AddMenuSeparator(_, error) => Some(error), 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/create_window.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::ffi::OsString; 3 | 4 | use tokio::sync::mpsc; 5 | 6 | use crate::area::Area; 7 | use crate::error::ErrorKind::*; 8 | use crate::error::{SetupIconsError, SetupMenuError}; 9 | use crate::icons::Icons; 10 | use crate::menu_item::{MenuItem, MenuItemKind}; 11 | use crate::window_loop::PopupMenuHandle; 12 | use crate::window_loop::{AreaHandle, IconHandle, WindowLoop}; 13 | use crate::{AreaId, EventLoop, Result, Sender}; 14 | 15 | /// Construct a window. 16 | /// 17 | /// This is a builder type which allows the customization of which capabilities 18 | /// the constructed window should have. 19 | /// 20 | /// The general functionality is that accessor methods are used to mutate the 21 | /// builder, which then returns a mutable reference to the type being built. 22 | /// While sometimes a little awkward, this greatly reduces the number of types a 23 | /// typical use-case has to import and ensures that there is a strong 24 | /// correlation between the identifiers returned (such as [`IconId`] and 25 | /// [`AreaId`]) and the builder with which they are associated. 26 | /// 27 | /// [`IconId`]: crate::IconId 28 | /// [`AreaId`]: crate::AreaId 29 | /// 30 | /// # Examples 31 | /// 32 | /// ```no_run 33 | /// use winctx::CreateWindow; 34 | /// 35 | /// # macro_rules! include_bytes { ($path:literal) => { &[] } } 36 | /// const ICON: &[u8] = include_bytes!("tokio.ico"); 37 | /// 38 | /// let mut window = CreateWindow::new("se.tedro.Example") 39 | /// .window_name("Example Application"); 40 | /// 41 | /// let icon = window.icons().insert_buffer(ICON, 22, 22); 42 | /// 43 | /// let area = window.new_area().icon(icon); 44 | /// 45 | /// let menu = area.popup_menu(); 46 | /// let first = menu.push_entry("Example Application").id(); 47 | /// menu.push_entry("Quit"); 48 | /// menu.set_default(first); 49 | /// ``` 50 | pub struct CreateWindow { 51 | class_name: OsString, 52 | window_name: Option, 53 | areas: Vec, 54 | clipboard_events: bool, 55 | icons: Icons, 56 | } 57 | 58 | impl CreateWindow { 59 | /// Construct a new event loop where the window has the specified class 60 | /// name. 61 | /// 62 | /// # Examples 63 | /// 64 | /// ``` 65 | /// use winctx::CreateWindow; 66 | /// 67 | /// let mut builder = CreateWindow::new("se.tedro.Example"); 68 | /// ``` 69 | pub fn new(class_name: N) -> Self 70 | where 71 | N: AsRef, 72 | { 73 | Self { 74 | class_name: class_name.as_ref().to_owned(), 75 | window_name: None, 76 | areas: Vec::new(), 77 | clipboard_events: false, 78 | icons: Icons::default(), 79 | } 80 | } 81 | 82 | /// Indicates whether we should monitor the system clipboard for changes. 83 | /// 84 | /// # Examples 85 | /// 86 | /// ``` 87 | /// use winctx::CreateWindow; 88 | /// 89 | /// let mut builder = CreateWindow::new("se.tedro.Example") 90 | /// .clipboard_events(true); 91 | /// ``` 92 | pub fn clipboard_events(self, clipboard_events: bool) -> Self { 93 | Self { 94 | clipboard_events, 95 | ..self 96 | } 97 | } 98 | 99 | /// Modify the window name for use in the application. 100 | /// 101 | /// # Examples 102 | /// 103 | /// ``` 104 | /// use winctx::CreateWindow; 105 | /// 106 | /// let mut builder = CreateWindow::new("se.tedro.Example") 107 | /// .window_name("Example Application"); 108 | /// ``` 109 | pub fn window_name(self, window_name: N) -> Self 110 | where 111 | N: AsRef, 112 | { 113 | Self { 114 | window_name: Some(window_name.as_ref().to_owned()), 115 | ..self 116 | } 117 | } 118 | 119 | /// Push a notification area onto the window and return its id. 120 | /// 121 | /// # Examples 122 | /// 123 | /// ``` 124 | /// use winctx::CreateWindow; 125 | /// 126 | /// # macro_rules! include_bytes { ($path:literal) => { &[] } } 127 | /// const ICON: &[u8] = include_bytes!("tokio.ico"); 128 | /// 129 | /// let mut window = CreateWindow::new("se.tedro.Example"); 130 | /// let icon = window.icons().insert_buffer(ICON, 22, 22); 131 | /// window.new_area().icon(icon); 132 | /// ``` 133 | pub fn new_area(&mut self) -> &mut Area { 134 | let id = AreaId::new(self.areas.len() as u32); 135 | self.areas.push(Area::new(id)); 136 | self.areas.last_mut().unwrap() 137 | } 138 | 139 | /// Associate custom icons with the window. 140 | /// 141 | /// If [`IconId`] handles are used their associated icons lexicon has to be 142 | /// installed here. 143 | /// 144 | /// [`IconId`]: crate::IconId 145 | /// 146 | /// # Examples 147 | /// 148 | /// ``` 149 | /// use winctx::CreateWindow; 150 | /// 151 | /// # macro_rules! include_bytes { ($path:literal) => { &[] } } 152 | /// const ICON: &[u8] = include_bytes!("tokio.ico"); 153 | /// 154 | /// 155 | /// let mut window = CreateWindow::new("se.tedro.Example") 156 | /// .window_name("Example Application"); 157 | /// 158 | /// let icon = window.icons().insert_buffer(ICON, 22, 22); 159 | /// 160 | /// let area = window.new_area().icon(icon).tooltip("Example Application"); 161 | /// ``` 162 | pub fn icons(&mut self) -> &mut Icons { 163 | &mut self.icons 164 | } 165 | 166 | /// Construct a new event loop and system integration. 167 | pub async fn build(self) -> Result<(Sender, EventLoop)> { 168 | let (events_tx, events_rx) = mpsc::unbounded_channel(); 169 | 170 | let icons = self.setup_icons(&self.icons).map_err(SetupIcons)?; 171 | let mut menus = Vec::with_capacity(self.areas.len()); 172 | let mut initial = Vec::new(); 173 | 174 | for (id, m) in self.areas.into_iter().enumerate() { 175 | let area_id = AreaId::new(id as u32); 176 | 177 | let popup_menu = if let Some(popup_menu) = m.popup_menu { 178 | let mut menu = 179 | PopupMenuHandle::new(popup_menu.open_menu).map_err(BuildPopupMenu)?; 180 | build_menu(&mut menu, popup_menu.menu, popup_menu.default).map_err(SetupMenu)?; 181 | Some(menu) 182 | } else { 183 | None 184 | }; 185 | 186 | initial.push((area_id, m.initial)); 187 | menus.push(AreaHandle::new(area_id, popup_menu)); 188 | } 189 | 190 | let mut window = WindowLoop::new( 191 | &self.class_name, 192 | self.window_name.as_deref(), 193 | self.clipboard_events, 194 | menus, 195 | ) 196 | .await 197 | .map_err(WindowSetup)?; 198 | 199 | for menu in &window.areas { 200 | window 201 | .window 202 | .add_notification(menu.area_id) 203 | .map_err(AddNotification)?; 204 | } 205 | 206 | for (area_id, modify) in initial { 207 | let icon = modify.icon.and_then(|icon| icons.get(icon.as_usize())); 208 | 209 | window 210 | .window 211 | .modify_notification(area_id, icon, modify.tooltip.as_deref()) 212 | .map_err(ModifyNotification)?; 213 | } 214 | 215 | let event_loop = EventLoop::new(events_rx, window, icons); 216 | let system = Sender::new(events_tx); 217 | Ok((system, event_loop)) 218 | } 219 | 220 | fn setup_icons(&self, icons: &Icons) -> Result, SetupIconsError> { 221 | let mut handles = Vec::with_capacity(icons.icons.len()); 222 | 223 | for icon in icons.icons.iter() { 224 | handles.push( 225 | IconHandle::from_buffer(icon.as_bytes(), icon.width(), icon.height()) 226 | .map_err(SetupIconsError::BuildIcon)?, 227 | ); 228 | } 229 | 230 | Ok(handles) 231 | } 232 | } 233 | 234 | fn build_menu( 235 | menu: &mut PopupMenuHandle, 236 | menu_items: Vec, 237 | default: Option, 238 | ) -> Result<(), SetupMenuError> { 239 | for (index, item) in menu_items.into_iter().enumerate() { 240 | debug_assert!(u32::try_from(index).is_ok()); 241 | let menu_item_id = index as u32; 242 | 243 | match item.kind { 244 | MenuItemKind::Separator => { 245 | let default = default == Some(menu_item_id); 246 | 247 | menu.add_menu_separator(menu_item_id, default, &item.initial) 248 | .map_err(|e| SetupMenuError::AddMenuSeparator(index, e))?; 249 | } 250 | MenuItemKind::String { text } => { 251 | let default = default == Some(menu_item_id); 252 | 253 | menu.add_menu_entry(menu_item_id, text.as_str(), default, &item.initial) 254 | .map_err(|e| SetupMenuError::AddMenuEntry(index, e))?; 255 | } 256 | } 257 | } 258 | 259 | Ok(()) 260 | } 261 | -------------------------------------------------------------------------------- /src/clipboard/clipboard_format.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use windows_sys::Win32::System::Ole as ole; 4 | 5 | /// A clipboard format. 6 | #[derive(Clone, Copy, PartialEq, Eq)] 7 | #[repr(transparent)] 8 | pub struct ClipboardFormat(u16); 9 | 10 | #[allow(unused)] 11 | impl ClipboardFormat { 12 | /// A handle to a bitmap (HBITMAP). 13 | pub const BITMAP: Self = Self(ole::CF_BITMAP); 14 | 15 | /// A memory object containing a BITMAPINFO structure followed by the bitmap 16 | /// bits. 17 | pub const DIB: Self = Self(ole::CF_DIB); 18 | 19 | /// A memory object containing a BITMAPV5HEADER structure followed by the 20 | /// bitmap color space information and the bitmap bits. 21 | pub const DIBV5: Self = Self(ole::CF_DIBV5); 22 | 23 | /// Software Arts' Data Interchange Format. 24 | pub const DIF: Self = Self(ole::CF_DIF); 25 | 26 | /// Bitmap display format associated with a private format. The hMem 27 | /// parameter must be a handle to data that can be displayed in bitmap 28 | /// format in lieu of the privately formatted data. 29 | pub const DSPBITMAP: Self = Self(ole::CF_DSPBITMAP); 30 | 31 | /// Enhanced metafile display format associated with a private format. The 32 | /// hMem parameter must be a handle to data that can be displayed in 33 | /// enhanced metafile format in lieu of the privately formatted data. 34 | pub const DSPENHMETAFILE: Self = Self(ole::CF_DSPENHMETAFILE); 35 | 36 | /// Metafile-picture display format associated with a private format. The 37 | /// hMem parameter must be a handle to data that can be displayed in 38 | /// metafile-picture format in lieu of the privately formatted data. 39 | pub const DSPMETAFILEPICT: Self = Self(ole::CF_DSPMETAFILEPICT); 40 | 41 | /// Text display format associated with a private format. The hMem parameter 42 | /// must be a handle to data that can be displayed in text format in lieu of 43 | /// the privately formatted data. 44 | pub const DSPTEXT: Self = Self(ole::CF_DSPTEXT); 45 | 46 | /// A handle to an enhanced metafile (HENHMETAFILE). 47 | pub const ENHMETAFILE: Self = Self(ole::CF_ENHMETAFILE); 48 | 49 | /// Start of a range of integer values for application-defined GDI object 50 | /// clipboard formats. The end of the range is CF_GDIOBJLAST. Handles 51 | /// associated with clipboard formats in this range are not automatically 52 | /// deleted using the GlobalFree function when the clipboard is emptied. 53 | /// Also, when using values in this range, the hMem parameter is not a 54 | /// handle to a GDI object, but is a handle allocated by the GlobalAlloc 55 | /// function with the GMEM_MOVEABLE flag. 56 | pub const GDIOBJFIRST: Self = Self(ole::CF_GDIOBJFIRST); 57 | 58 | /// See CF_GDIOBJFIRST. 59 | pub const GDIOBJLAST: Self = Self(ole::CF_GDIOBJLAST); 60 | 61 | /// A handle to type HDROP that identifies a list of files. An application 62 | /// can retrieve information about the files by passing the handle to the 63 | /// DragQueryFile function. 64 | pub const HDROP: Self = Self(ole::CF_HDROP); 65 | 66 | /// The data is a handle (HGLOBAL) to the locale identifier (LCID) 67 | /// associated with text in the clipboard. When you close the clipboard, if 68 | /// it contains CF_TEXT data but no CF_LOCALE data, the system automatically 69 | /// sets the CF_LOCALE format to the current input language. You can use the 70 | /// CF_LOCALE format to associate a different locale with the clipboard 71 | /// text. An application that pastes text from the clipboard can retrieve 72 | /// this format to determine which character set was used to generate the 73 | /// text. Note that the clipboard does not support plain text in multiple 74 | /// character sets. To achieve this, use a formatted text data type such as 75 | /// RTF instead. The system uses the code page associated with CF_LOCALE to 76 | /// implicitly convert from CF_TEXT to CF_UNICODETEXT. Therefore, the 77 | /// correct code page table is used for the conversion. 78 | pub const LOCALE: Self = Self(ole::CF_LOCALE); 79 | 80 | /// Handle to a metafile picture format as defined by the METAFILEPICT 81 | /// structure. When passing a CF_METAFILEPICT handle by means of DDE, the 82 | /// application responsible for deleting hMem should also free the metafile 83 | /// referred to by the CF_METAFILEPICT handle. 84 | pub const METAFILEPICT: Self = Self(ole::CF_METAFILEPICT); 85 | 86 | /// Text format containing characters in the OEM character set. Each line 87 | /// ends with a carriage return/linefeed (CR-LF) combination. A null 88 | /// character signals the end of the data. 89 | pub const OEMTEXT: Self = Self(ole::CF_OEMTEXT); 90 | 91 | /// Owner-display format. The clipboard owner must display and update the 92 | /// clipboard viewer window, and receive the WM_ASKCBFORMATNAME, 93 | /// WM_HSCROLLCLIPBOARD, WM_PAINTCLIPBOARD, WM_SIZECLIPBOARD, and 94 | /// WM_VSCROLLCLIPBOARD messages. The hMem parameter must be NULL. 95 | pub const OWNERDISPLAY: Self = Self(ole::CF_OWNERDISPLAY); 96 | 97 | /// Handle to a color palette. Whenever an application places data in the 98 | /// clipboard that depends on or assumes a color palette, it should place 99 | /// the palette on the clipboard as well. If the clipboard contains data in 100 | /// the CF_PALETTE (logical color palette) format, the application should 101 | /// use the SelectPalette and RealizePalette functions to realize (compare) 102 | /// any other data in the clipboard against that logical palette. When 103 | /// displaying clipboard data, the clipboard always uses as its current 104 | /// palette any object on the clipboard that is in the CF_PALETTE format. 105 | pub const PALETTE: Self = Self(ole::CF_PALETTE); 106 | 107 | /// Data for the pen extensions to the Microsoft Windows for Pen Computing. 108 | pub const PENDATA: Self = Self(ole::CF_PENDATA); 109 | 110 | /// Start of a range of integer values for private clipboard formats. The 111 | /// range ends with CF_PRIVATELAST. Handles associated with private 112 | /// clipboard formats are not freed automatically; the clipboard owner must 113 | /// free such handles, typically in response to the WM_DESTROYCLIPBOARD 114 | /// message. 115 | pub const PRIVATEFIRST: Self = Self(ole::CF_PRIVATEFIRST); 116 | 117 | /// See CF_PRIVATEFIRST. 118 | pub const PRIVATELAST: Self = Self(ole::CF_PRIVATELAST); 119 | 120 | /// Represents audio data more complex than can be represented in a CF_WAVE 121 | /// standard wave format. 122 | pub const RIFF: Self = Self(ole::CF_RIFF); 123 | 124 | /// Microsoft Symbolic Link (SYLK) format. 125 | pub const SYLK: Self = Self(ole::CF_SYLK); 126 | 127 | /// Text format. Each line ends with a carriage return/linefeed (CR-LF) 128 | /// combination. A null character signals the end of the data. Use this 129 | /// format for ANSI text. 130 | pub const TEXT: Self = Self(ole::CF_TEXT); 131 | 132 | /// Tagged-image file format. 133 | pub const TIFF: Self = Self(ole::CF_TIFF); 134 | 135 | /// Unicode text format. Each line ends with a carriage return/linefeed 136 | /// (CR-LF) combination. A null character signals the end of the data. 137 | pub const UNICODETEXT: Self = Self(ole::CF_UNICODETEXT); 138 | 139 | /// Represents audio data in one of the standard wave formats, such as 11 140 | /// kHz or 22 kHz PCM. 141 | pub const WAVE: Self = Self(ole::CF_WAVE); 142 | } 143 | 144 | impl ClipboardFormat { 145 | /// Construct a new clipboard format from the given raw value. 146 | pub(crate) const fn new(value: u16) -> Self { 147 | Self(value) 148 | } 149 | 150 | /// Get the raw value of this clipboard format. 151 | pub(super) const fn as_u16(self) -> u16 { 152 | self.0 153 | } 154 | } 155 | 156 | impl fmt::Debug for ClipboardFormat { 157 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 158 | match self { 159 | Self(ole::CF_BITMAP) => write!(f, "BITMAP"), 160 | Self(ole::CF_DIB) => write!(f, "DIB"), 161 | Self(ole::CF_DIBV5) => write!(f, "DIBV5"), 162 | Self(ole::CF_DIF) => write!(f, "DIF"), 163 | Self(ole::CF_DSPBITMAP) => write!(f, "DSPBITMAP"), 164 | Self(ole::CF_DSPENHMETAFILE) => write!(f, "DSPENHMETAFILE"), 165 | Self(ole::CF_DSPMETAFILEPICT) => write!(f, "DSPMETAFILEPICT"), 166 | Self(ole::CF_DSPTEXT) => write!(f, "DSPTEXT"), 167 | Self(ole::CF_ENHMETAFILE) => write!(f, "ENHMETAFILE"), 168 | Self(ole::CF_GDIOBJFIRST) => write!(f, "GDIOBJFIRST"), 169 | Self(ole::CF_GDIOBJLAST) => write!(f, "GDIOBJLAST"), 170 | Self(ole::CF_HDROP) => write!(f, "HDROP"), 171 | Self(ole::CF_LOCALE) => write!(f, "LOCALE"), 172 | Self(ole::CF_METAFILEPICT) => write!(f, "METAFILEPICT"), 173 | Self(ole::CF_OEMTEXT) => write!(f, "OEMTEXT"), 174 | Self(ole::CF_OWNERDISPLAY) => write!(f, "OWNERDISPLAY"), 175 | Self(ole::CF_PALETTE) => write!(f, "PALETTE"), 176 | Self(ole::CF_PENDATA) => write!(f, "PENDATA"), 177 | Self(ole::CF_PRIVATEFIRST) => write!(f, "PRIVATEFIRST"), 178 | Self(ole::CF_PRIVATELAST) => write!(f, "PRIVATELAST"), 179 | Self(ole::CF_RIFF) => write!(f, "RIFF"), 180 | Self(ole::CF_SYLK) => write!(f, "SYLK"), 181 | Self(ole::CF_TEXT) => write!(f, "TEXT"), 182 | Self(ole::CF_TIFF) => write!(f, "TIFF"), 183 | Self(ole::CF_UNICODETEXT) => write!(f, "UNICODETEXT"), 184 | Self(ole::CF_WAVE) => write!(f, "WAVE"), 185 | Self(format) => write!(f, "UNKNOWN({format})"), 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/window_loop/window_loop.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::field_reassign_with_default)] 2 | 3 | use std::ffi::OsStr; 4 | use std::io; 5 | use std::mem::size_of; 6 | use std::mem::ManuallyDrop; 7 | use std::mem::MaybeUninit; 8 | use std::ptr; 9 | use std::slice; 10 | use std::thread; 11 | 12 | use tokio::sync::mpsc; 13 | use tokio::sync::oneshot; 14 | use windows_sys::Win32::Foundation::{FALSE, HWND, LPARAM, LRESULT, WPARAM}; 15 | use windows_sys::Win32::System::DataExchange::AddClipboardFormatListener; 16 | use windows_sys::Win32::System::DataExchange::COPYDATASTRUCT; 17 | use windows_sys::Win32::UI::Shell as shellapi; 18 | use windows_sys::Win32::UI::WindowsAndMessaging as winuser; 19 | 20 | use crate::convert::ToWide; 21 | use crate::error::ErrorKind::*; 22 | use crate::error::{Error, WindowError}; 23 | use crate::event::{ClipboardEvent, MouseEvent}; 24 | use crate::window_loop::messages; 25 | use crate::AreaId; 26 | use crate::Result; 27 | 28 | use super::{AreaHandle, ClipboardManager, MenuManager, WindowClassHandle, WindowHandle}; 29 | 30 | #[derive(Debug)] 31 | pub(crate) enum WindowEvent { 32 | /// A meny item was clicked. 33 | MenuItemClicked(AreaId, u32, MouseEvent), 34 | /// Shutdown was requested. 35 | Shutdown, 36 | /// Clipboard event. 37 | Clipboard(ClipboardEvent), 38 | /// The notification icon has been clicked. 39 | IconClicked(AreaId, MouseEvent), 40 | /// Balloon was clicked. 41 | NotificationClicked(AreaId, MouseEvent), 42 | /// Balloon timed out. 43 | NotificationDismissed(AreaId), 44 | /// Data copied to this process. 45 | CopyData(usize, Vec), 46 | /// Non-fatal error. 47 | Error(Error), 48 | } 49 | 50 | unsafe extern "system" fn window_proc( 51 | hwnd: HWND, 52 | msg: u32, 53 | w_param: WPARAM, 54 | l_param: LPARAM, 55 | ) -> LRESULT { 56 | // Match over all messages we want to post back to the event loop. 57 | match msg { 58 | messages::ICON_ID => { 59 | if matches!( 60 | l_param as u32, 61 | shellapi::NIN_BALLOONUSERCLICK 62 | | shellapi::NIN_BALLOONTIMEOUT 63 | | winuser::WM_LBUTTONUP 64 | | winuser::WM_RBUTTONUP 65 | ) { 66 | winuser::PostMessageW(hwnd, msg, w_param, l_param); 67 | return 0; 68 | } 69 | } 70 | winuser::WM_MENUCOMMAND => { 71 | winuser::PostMessageW(hwnd, msg, w_param, l_param); 72 | return 0; 73 | } 74 | winuser::WM_CLIPBOARDUPDATE => { 75 | winuser::PostMessageW(hwnd, msg, w_param, l_param); 76 | return 0; 77 | } 78 | winuser::WM_DESTROY => { 79 | winuser::PostMessageW(hwnd, msg, w_param, l_param); 80 | return 0; 81 | } 82 | winuser::WM_COPYDATA => { 83 | let data = &*(l_param as *const COPYDATASTRUCT); 84 | 85 | let len = data.cbData as usize; 86 | let mut vec = Vec::with_capacity(len + size_of::()); 87 | vec.extend_from_slice(slice::from_raw_parts(data.lpData.cast::(), len)); 88 | vec.extend_from_slice(&data.dwData.to_ne_bytes()); 89 | let mut vec = ManuallyDrop::new(vec); 90 | let bytes = vec.as_mut_ptr(); 91 | winuser::PostMessageW(hwnd, messages::BYTES_ID, len, bytes as isize); 92 | return 0; 93 | } 94 | _ => {} 95 | } 96 | 97 | winuser::DefWindowProcW(hwnd, msg, w_param, l_param) 98 | } 99 | 100 | unsafe fn init_window( 101 | class_name: Vec, 102 | window_name: Option>, 103 | ) -> io::Result<(WindowClassHandle, WindowHandle)> { 104 | let wnd = winuser::WNDCLASSW { 105 | style: 0, 106 | lpfnWndProc: Some(window_proc), 107 | cbClsExtra: 0, 108 | cbWndExtra: 0, 109 | hInstance: 0, 110 | hIcon: 0, 111 | hCursor: 0, 112 | hbrBackground: 0, 113 | lpszMenuName: ptr::null(), 114 | lpszClassName: class_name.as_ptr(), 115 | }; 116 | 117 | if winuser::RegisterClassW(&wnd) == 0 { 118 | return Err(io::Error::last_os_error()); 119 | } 120 | 121 | let class = WindowClassHandle { class_name }; 122 | 123 | let hwnd = winuser::CreateWindowExW( 124 | 0, 125 | class.class_name.as_ptr(), 126 | window_name.map(|n| n.as_ptr()).unwrap_or_else(ptr::null), 127 | winuser::WS_DISABLED, 128 | 0, 129 | 0, 130 | 0, 131 | 0, 132 | 0, 133 | 0, 134 | 0, 135 | ptr::null(), 136 | ); 137 | 138 | if hwnd == 0 { 139 | return Err(io::Error::last_os_error()); 140 | } 141 | 142 | let window = WindowHandle { hwnd }; 143 | Ok((class, window)) 144 | } 145 | 146 | /// A windows application window. 147 | /// 148 | /// Note: repr(C) is important here to ensure drop order. 149 | #[repr(C)] 150 | pub(crate) struct WindowLoop { 151 | pub(crate) areas: Vec, 152 | pub(crate) window: WindowHandle, 153 | window_class: WindowClassHandle, 154 | events_rx: mpsc::UnboundedReceiver, 155 | thread: Option>>, 156 | } 157 | 158 | impl WindowLoop { 159 | /// Construct a new window. 160 | pub(crate) async fn new( 161 | class_name: &OsStr, 162 | window_name: Option<&OsStr>, 163 | clipboard_events: bool, 164 | areas: Vec, 165 | ) -> Result { 166 | let class_name = class_name.to_wide_null(); 167 | let window_name = window_name.map(|n| n.to_wide_null()); 168 | 169 | if class_name.len() > 256 { 170 | return Err(WindowError::ClassNameTooLong(class_name.len())); 171 | } 172 | 173 | let (return_tx, return_rx) = oneshot::channel(); 174 | let (events_tx, events_rx) = mpsc::unbounded_channel(); 175 | 176 | let mut hmenus = Vec::with_capacity(areas.len()); 177 | 178 | for menu in &areas { 179 | hmenus.push( 180 | menu.popup_menu 181 | .as_ref() 182 | .map(|p| (p.hmenu, p.open_menu.copy_data())), 183 | ); 184 | } 185 | 186 | let thread = thread::spawn(move || unsafe { 187 | // NB: Don't move this, it's important that the window is 188 | // initialized in the background thread. 189 | let (window_class, window) = 190 | init_window(class_name, window_name).map_err(WindowError::Init)?; 191 | 192 | let mut clipboard_manager = if clipboard_events { 193 | if AddClipboardFormatListener(window.hwnd) == FALSE { 194 | return Err(WindowError::AddClipboardFormatListener( 195 | io::Error::last_os_error(), 196 | )); 197 | } 198 | 199 | Some(ClipboardManager::new(&events_tx)) 200 | } else { 201 | None 202 | }; 203 | 204 | let mut menu_manager = 205 | (!hmenus.is_empty()).then(|| MenuManager::new(&events_tx, &hmenus)); 206 | 207 | let hwnd = window.hwnd; 208 | 209 | if return_tx.send((window_class, window)).is_err() { 210 | return Ok(()); 211 | } 212 | 213 | let mut msg = MaybeUninit::zeroed(); 214 | 215 | while winuser::GetMessageW(msg.as_mut_ptr(), hwnd, 0, 0) != FALSE { 216 | let msg = &*msg.as_ptr(); 217 | 218 | if let Some(clipboard_manager) = &mut clipboard_manager { 219 | if clipboard_manager.dispatch(msg) { 220 | continue; 221 | } 222 | } 223 | 224 | if let Some(menu_manager) = &mut menu_manager { 225 | if menu_manager.dispatch(msg) { 226 | continue; 227 | } 228 | } 229 | 230 | match msg.message { 231 | winuser::WM_QUIT | winuser::WM_DESTROY => { 232 | break; 233 | } 234 | messages::BYTES_ID => { 235 | let len = msg.wParam; 236 | 237 | let bytes = Vec::from_raw_parts( 238 | msg.lParam as *mut u8, 239 | len, 240 | len + size_of::(), 241 | ); 242 | 243 | let ty = bytes 244 | .as_ptr() 245 | .add(bytes.len()) 246 | .cast::() 247 | .read_unaligned(); 248 | 249 | _ = events_tx.send(WindowEvent::CopyData(ty, bytes)); 250 | continue; 251 | } 252 | _ => {} 253 | } 254 | 255 | winuser::TranslateMessage(msg); 256 | winuser::DispatchMessageW(msg); 257 | } 258 | 259 | Ok(()) 260 | }); 261 | 262 | let Some((window_class, window)) = return_rx.await.ok() else { 263 | thread.join().map_err(|_| WindowError::ThreadPanicked)??; 264 | return Err(WindowError::ThreadExited); 265 | }; 266 | 267 | Ok(WindowLoop { 268 | areas, 269 | window, 270 | window_class, 271 | events_rx, 272 | thread: Some(thread), 273 | }) 274 | } 275 | 276 | /// Tick the window through a single event cycle. 277 | pub(crate) async fn tick(&mut self) -> WindowEvent { 278 | self.events_rx.recv().await.unwrap_or(WindowEvent::Shutdown) 279 | } 280 | 281 | /// Test if the window has been closed. 282 | pub(crate) fn is_closed(&self) -> bool { 283 | self.thread.is_none() 284 | } 285 | 286 | /// Join the current window. 287 | pub(crate) fn join(&mut self) -> Result<()> { 288 | if self.thread.is_none() { 289 | return Ok(()); 290 | } 291 | 292 | let result = unsafe { winuser::PostMessageW(self.window.hwnd, winuser::WM_DESTROY, 0, 0) }; 293 | 294 | if result == FALSE { 295 | return Err(Error::new(PostMessageDestroy)); 296 | } 297 | 298 | if let Some(thread) = self.thread.take() { 299 | thread 300 | .join() 301 | .map_err(|_| ThreadError(WindowError::ThreadPanicked))? 302 | .map_err(ThreadError)?; 303 | } 304 | 305 | Ok(()) 306 | } 307 | } 308 | 309 | impl Drop for WindowLoop { 310 | fn drop(&mut self) { 311 | for menu in &self.areas { 312 | _ = self.window.delete_notification(menu.area_id); 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/icon/stock_icon.rs: -------------------------------------------------------------------------------- 1 | use windows_sys::Win32::UI::Shell; 2 | 3 | /// A stock icon. 4 | #[derive(Debug)] 5 | #[repr(transparent)] 6 | pub struct StockIcon(i32); 7 | 8 | impl StockIcon { 9 | /// Document of a type with no associated application. 10 | pub const DOCNOASSOC: Self = Self(Shell::SIID_DOCNOASSOC); 11 | 12 | /// Document of a type with an associated application. 13 | pub const DOCASSOC: Self = Self(Shell::SIID_DOCASSOC); 14 | 15 | /// Generic application with no custom icon. 16 | pub const APPLICATION: Self = Self(Shell::SIID_APPLICATION); 17 | 18 | /// Folder (generic, unspecified state). 19 | pub const FOLDER: Self = Self(Shell::SIID_FOLDER); 20 | 21 | /// Folder (open). 22 | pub const FOLDEROPEN: Self = Self(Shell::SIID_FOLDEROPEN); 23 | 24 | /// 5.25-inch disk drive. 25 | pub const DRIVE525: Self = Self(Shell::SIID_DRIVE525); 26 | 27 | /// 3.5-inch disk drive. 28 | pub const DRIVE35: Self = Self(Shell::SIID_DRIVE35); 29 | 30 | /// Removable drive. 31 | pub const DRIVEREMOVE: Self = Self(Shell::SIID_DRIVEREMOVE); 32 | 33 | /// Fixed drive (hard disk). 34 | pub const DRIVEFIXED: Self = Self(Shell::SIID_DRIVEFIXED); 35 | 36 | /// Network drive (connected). 37 | pub const DRIVENET: Self = Self(Shell::SIID_DRIVENET); 38 | 39 | /// Network drive (disconnected). 40 | pub const DRIVENETDISABLED: Self = Self(Shell::SIID_DRIVENETDISABLED); 41 | 42 | /// CD drive. 43 | pub const DRIVECD: Self = Self(Shell::SIID_DRIVECD); 44 | 45 | /// RAM disk drive. 46 | pub const DRIVERAM: Self = Self(Shell::SIID_DRIVERAM); 47 | 48 | /// The entire network. 49 | pub const WORLD: Self = Self(Shell::SIID_WORLD); 50 | 51 | /// A computer on the network. 52 | pub const SERVER: Self = Self(Shell::SIID_SERVER); 53 | 54 | /// A local printer or print destination. 55 | pub const PRINTER: Self = Self(Shell::SIID_PRINTER); 56 | 57 | /// The Network virtual folder (FOLDERID_NetworkFolder/CSIDL_NETWORK). 58 | pub const MYNETWORK: Self = Self(Shell::SIID_MYNETWORK); 59 | 60 | /// The Search feature. 61 | pub const FIND: Self = Self(Shell::SIID_FIND); 62 | 63 | /// The Help and Support feature. 64 | pub const HELP: Self = Self(Shell::SIID_HELP); 65 | 66 | /// Overlay for a shared item. 67 | pub const SHARE: Self = Self(Shell::SIID_SHARE); 68 | 69 | /// Overlay for a shortcut. 70 | pub const LINK: Self = Self(Shell::SIID_LINK); 71 | 72 | /// Overlay for items that are expected to be slow to access. 73 | pub const SLOWFILE: Self = Self(Shell::SIID_SLOWFILE); 74 | 75 | /// The Recycle Bin (empty). 76 | pub const RECYCLER: Self = Self(Shell::SIID_RECYCLER); 77 | 78 | /// The Recycle Bin (not empty). 79 | pub const RECYCLERFULL: Self = Self(Shell::SIID_RECYCLERFULL); 80 | 81 | /// Audio CD media. 82 | pub const MEDIACDAUDIO: Self = Self(Shell::SIID_MEDIACDAUDIO); 83 | 84 | /// Security lock. 85 | pub const LOCK: Self = Self(Shell::SIID_LOCK); 86 | 87 | /// A virtual folder that contains the results of a search. 88 | pub const AUTOLIST: Self = Self(Shell::SIID_AUTOLIST); 89 | 90 | /// A network printer. 91 | pub const PRINTERNET: Self = Self(Shell::SIID_PRINTERNET); 92 | 93 | /// A server shared on a network. 94 | pub const SERVERSHARE: Self = Self(Shell::SIID_SERVERSHARE); 95 | 96 | /// A local fax printer. 97 | pub const PRINTERFAX: Self = Self(Shell::SIID_PRINTERFAX); 98 | 99 | /// A network fax printer. 100 | pub const PRINTERFAXNET: Self = Self(Shell::SIID_PRINTERFAXNET); 101 | 102 | /// A file that receives the output of a Print to file operation. 103 | pub const PRINTERFILE: Self = Self(Shell::SIID_PRINTERFILE); 104 | 105 | /// A category that results from a Stack by command to organize the contents 106 | /// of a folder. 107 | pub const STACK: Self = Self(Shell::SIID_STACK); 108 | 109 | /// Super Video CD (SVCD) media. 110 | pub const MEDIASVCD: Self = Self(Shell::SIID_MEDIASVCD); 111 | 112 | /// A folder that contains only subfolders as child items. 113 | pub const STUFFEDFOLDER: Self = Self(Shell::SIID_STUFFEDFOLDER); 114 | 115 | /// Unknown drive type. 116 | pub const DRIVEUNKNOWN: Self = Self(Shell::SIID_DRIVEUNKNOWN); 117 | 118 | /// DVD drive. 119 | pub const DRIVEDVD: Self = Self(Shell::SIID_DRIVEDVD); 120 | 121 | /// DVD media. 122 | pub const MEDIADVD: Self = Self(Shell::SIID_MEDIADVD); 123 | 124 | /// DVD-RAM media. 125 | pub const MEDIADVDRAM: Self = Self(Shell::SIID_MEDIADVDRAM); 126 | 127 | /// DVD-RW media. 128 | pub const MEDIADVDRW: Self = Self(Shell::SIID_MEDIADVDRW); 129 | 130 | /// DVD-R media. 131 | pub const MEDIADVDR: Self = Self(Shell::SIID_MEDIADVDR); 132 | 133 | /// DVD-ROM media. 134 | pub const MEDIADVDROM: Self = Self(Shell::SIID_MEDIADVDROM); 135 | 136 | /// CD+ (enhanced audio CD) media. 137 | pub const MEDIACDAUDIOPLUS: Self = Self(Shell::SIID_MEDIACDAUDIOPLUS); 138 | 139 | /// CD-RW media. 140 | pub const MEDIACDRW: Self = Self(Shell::SIID_MEDIACDRW); 141 | 142 | /// CD-R media. 143 | pub const MEDIACDR: Self = Self(Shell::SIID_MEDIACDR); 144 | 145 | /// A writable CD in the process of being burned. 146 | pub const MEDIACDBURN: Self = Self(Shell::SIID_MEDIACDBURN); 147 | 148 | /// Blank writable CD media. 149 | pub const MEDIABLANKCD: Self = Self(Shell::SIID_MEDIABLANKCD); 150 | 151 | /// CD-ROM media. 152 | pub const MEDIACDROM: Self = Self(Shell::SIID_MEDIACDROM); 153 | 154 | /// An audio file. 155 | pub const AUDIOFILES: Self = Self(Shell::SIID_AUDIOFILES); 156 | 157 | /// An image file. 158 | pub const IMAGEFILES: Self = Self(Shell::SIID_IMAGEFILES); 159 | 160 | /// A video file. 161 | pub const VIDEOFILES: Self = Self(Shell::SIID_VIDEOFILES); 162 | 163 | /// A mixed file. 164 | pub const MIXEDFILES: Self = Self(Shell::SIID_MIXEDFILES); 165 | 166 | /// Folder back. 167 | pub const FOLDERBACK: Self = Self(Shell::SIID_FOLDERBACK); 168 | 169 | /// Folder front. 170 | pub const FOLDERFRONT: Self = Self(Shell::SIID_FOLDERFRONT); 171 | 172 | /// Security shield. Use for UAC prompts only. 173 | pub const SHIELD: Self = Self(Shell::SIID_SHIELD); 174 | 175 | /// Warning. 176 | pub const WARNING: Self = Self(Shell::SIID_WARNING); 177 | 178 | /// Informational. 179 | pub const INFO: Self = Self(Shell::SIID_INFO); 180 | 181 | /// Error. 182 | pub const ERROR: Self = Self(Shell::SIID_ERROR); 183 | 184 | /// Key. 185 | pub const KEY: Self = Self(Shell::SIID_KEY); 186 | 187 | /// Software. 188 | pub const SOFTWARE: Self = Self(Shell::SIID_SOFTWARE); 189 | 190 | /// A UI item, such as a button, that issues a rename command. 191 | pub const RENAME: Self = Self(Shell::SIID_RENAME); 192 | 193 | /// A UI item, such as a button, that issues a delete command. 194 | pub const DELETE: Self = Self(Shell::SIID_DELETE); 195 | 196 | /// Audio DVD media. 197 | pub const MEDIAAUDIODVD: Self = Self(Shell::SIID_MEDIAAUDIODVD); 198 | 199 | /// Movie DVD media. 200 | pub const MEDIAMOVIEDVD: Self = Self(Shell::SIID_MEDIAMOVIEDVD); 201 | 202 | /// Enhanced CD media. 203 | pub const MEDIAENHANCEDCD: Self = Self(Shell::SIID_MEDIAENHANCEDCD); 204 | 205 | /// Enhanced DVD media. 206 | pub const MEDIAENHANCEDDVD: Self = Self(Shell::SIID_MEDIAENHANCEDDVD); 207 | 208 | /// High definition DVD media in the HD DVD format. 209 | pub const MEDIAHDDVD: Self = Self(Shell::SIID_MEDIAHDDVD); 210 | 211 | /// High definition DVD media in the Blu-ray Disc™ format. 212 | pub const MEDIABLURAY: Self = Self(Shell::SIID_MEDIABLURAY); 213 | 214 | /// Video CD (VCD) media. 215 | pub const MEDIAVCD: Self = Self(Shell::SIID_MEDIAVCD); 216 | 217 | /// DVD+R media. 218 | pub const MEDIADVDPLUSR: Self = Self(Shell::SIID_MEDIADVDPLUSR); 219 | 220 | /// DVD+RW media. 221 | pub const MEDIADVDPLUSRW: Self = Self(Shell::SIID_MEDIADVDPLUSRW); 222 | 223 | /// A desktop computer. 224 | pub const DESKTOPPC: Self = Self(Shell::SIID_DESKTOPPC); 225 | 226 | /// A mobile computer (laptop). 227 | pub const MOBILEPC: Self = Self(Shell::SIID_MOBILEPC); 228 | 229 | /// The User Accounts Control Panel item. 230 | pub const USERS: Self = Self(Shell::SIID_USERS); 231 | 232 | /// Smart media. 233 | pub const MEDIASMARTMEDIA: Self = Self(Shell::SIID_MEDIASMARTMEDIA); 234 | 235 | /// CompactFlash media. 236 | pub const MEDIACOMPACTFLASH: Self = Self(Shell::SIID_MEDIACOMPACTFLASH); 237 | 238 | /// A cell phone. 239 | pub const DEVICECELLPHONE: Self = Self(Shell::SIID_DEVICECELLPHONE); 240 | 241 | /// A digital camera. 242 | pub const DEVICECAMERA: Self = Self(Shell::SIID_DEVICECAMERA); 243 | 244 | /// A digital video camera. 245 | pub const DEVICEVIDEOCAMERA: Self = Self(Shell::SIID_DEVICEVIDEOCAMERA); 246 | 247 | /// An audio player. 248 | pub const DEVICEAUDIOPLAYER: Self = Self(Shell::SIID_DEVICEAUDIOPLAYER); 249 | 250 | /// Connect to network. 251 | pub const NETWORKCONNECT: Self = Self(Shell::SIID_NETWORKCONNECT); 252 | 253 | /// The Network and Internet Control Panel item. 254 | pub const INTERNET: Self = Self(Shell::SIID_INTERNET); 255 | 256 | /// A compressed file with a .zip file name extension. 257 | pub const ZIPFILE: Self = Self(Shell::SIID_ZIPFILE); 258 | 259 | /// The Additional Options Control Panel item. 260 | pub const SETTINGS: Self = Self(Shell::SIID_SETTINGS); 261 | 262 | /// Windows Vista with Service Pack 1 (SP1) and later. High definition DVD 263 | /// drive (any type - HD DVD-ROM, HD DVD-R, HD-DVD-RAM) that uses the HD DVD 264 | /// format. 265 | pub const DRIVEHDDVD: Self = Self(Shell::SIID_DRIVEHDDVD); 266 | 267 | /// Windows Vista with SP1 and later. High definition DVD drive (any type - 268 | /// BD-ROM, BD-R, BD-RE) that uses the Blu-ray Disc format. 269 | pub const DRIVEBD: Self = Self(Shell::SIID_DRIVEBD); 270 | 271 | /// Windows Vista with SP1 and later. High definition DVD-ROM media in the 272 | /// HD DVD-ROM format. 273 | pub const MEDIAHDDVDROM: Self = Self(Shell::SIID_MEDIAHDDVDROM); 274 | 275 | /// Windows Vista with SP1 and later. High definition DVD-R media in the HD 276 | /// DVD-R format. 277 | pub const MEDIAHDDVDR: Self = Self(Shell::SIID_MEDIAHDDVDR); 278 | 279 | /// Windows Vista with SP1 and later. High definition DVD-RAM media in the 280 | /// HD DVD-RAM format. 281 | pub const MEDIAHDDVDRAM: Self = Self(Shell::SIID_MEDIAHDDVDRAM); 282 | 283 | /// Windows Vista with SP1 and later. High definition DVD-ROM media in the 284 | /// Blu-ray Disc BD-ROM format. 285 | pub const MEDIABDROM: Self = Self(Shell::SIID_MEDIABDROM); 286 | 287 | /// Windows Vista with SP1 and later. High definition write-once media in 288 | /// the Blu-ray Disc BD-R format. 289 | pub const MEDIABDR: Self = Self(Shell::SIID_MEDIABDR); 290 | 291 | /// Windows Vista with SP1 and later. High definition read/write media in 292 | /// the Blu-ray Disc BD-RE format. 293 | pub const MEDIABDRE: Self = Self(Shell::SIID_MEDIABDRE); 294 | 295 | /// Windows Vista with SP1 and later. A cluster disk array. 296 | pub const CLUSTEREDDRIVE: Self = Self(Shell::SIID_CLUSTEREDDRIVE); 297 | 298 | /// Get the underlying icon identifier. 299 | pub(crate) fn as_id(&self) -> i32 { 300 | self.0 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/sender.rs: -------------------------------------------------------------------------------- 1 | //! Types related to modifying the window context. 2 | 3 | use std::fmt; 4 | use std::sync::atomic::AtomicU32; 5 | use std::sync::Arc; 6 | 7 | use tokio::sync::mpsc; 8 | 9 | use crate::icon::StockIcon; 10 | use crate::notification::NotificationIcon; 11 | use crate::{AreaId, IconId, ItemId, ModifyArea, ModifyMenuItem, Notification, NotificationId}; 12 | 13 | #[derive(Debug)] 14 | pub(super) enum InputEvent { 15 | Shutdown, 16 | ModifyArea { 17 | area_id: AreaId, 18 | modify: ModifyArea, 19 | }, 20 | ModifyMenuItem { 21 | item_id: ItemId, 22 | modify: ModifyMenuItem, 23 | }, 24 | Notification { 25 | area_id: AreaId, 26 | notification_id: NotificationId, 27 | notification: Notification, 28 | }, 29 | } 30 | 31 | struct Inner { 32 | notifications: AtomicU32, 33 | tx: mpsc::UnboundedSender, 34 | } 35 | 36 | /// Handle used to interact with the system integration. 37 | #[derive(Clone)] 38 | pub struct Sender { 39 | inner: Arc, 40 | } 41 | 42 | impl Sender { 43 | pub(crate) fn new(tx: mpsc::UnboundedSender) -> Self { 44 | Self { 45 | inner: Arc::new(Inner { 46 | notifications: AtomicU32::new(0), 47 | tx, 48 | }), 49 | } 50 | } 51 | 52 | /// Start a modify area request. 53 | /// 54 | /// This needs to be send using [`ModifyAreaBuilder::send`] to actually 55 | /// apply. 56 | pub fn modify_area(&self, area_id: AreaId) -> ModifyAreaBuilder<'_> { 57 | ModifyAreaBuilder { 58 | tx: &self.inner.tx, 59 | area_id, 60 | modify: ModifyArea::default(), 61 | } 62 | } 63 | 64 | /// Modify a menu item. 65 | pub fn modify_menu_item(&self, item_id: ItemId) -> ModifyMenuItemBuilder<'_> { 66 | ModifyMenuItemBuilder { 67 | tx: &self.inner.tx, 68 | item_id, 69 | modify: ModifyMenuItem::default(), 70 | } 71 | } 72 | 73 | /// Send the given notification. 74 | pub fn notification(&self, area_id: AreaId) -> NotificationBuilder<'_> { 75 | let id = self 76 | .inner 77 | .notifications 78 | .fetch_add(1, std::sync::atomic::Ordering::SeqCst); 79 | 80 | NotificationBuilder { 81 | tx: &self.inner.tx, 82 | area_id, 83 | id: NotificationId::new(id), 84 | notification: Notification::new(), 85 | } 86 | } 87 | 88 | /// Cause the window to shut down. 89 | pub fn shutdown(&self) { 90 | _ = self.inner.tx.send(InputEvent::Shutdown); 91 | } 92 | } 93 | 94 | /// A builder returned by [`Sender::modify_area`]. 95 | #[must_use = "Must call `send()` to apply changes"] 96 | pub struct ModifyAreaBuilder<'a> { 97 | tx: &'a mpsc::UnboundedSender, 98 | area_id: AreaId, 99 | modify: ModifyArea, 100 | } 101 | 102 | impl ModifyAreaBuilder<'_> { 103 | /// Set the icon of the notification area. 104 | pub fn icon(mut self, icon: IconId) -> Self { 105 | self.modify.icon(icon); 106 | self 107 | } 108 | 109 | /// Set the tooltip of the notification area. 110 | pub fn tooltip(mut self, tooltip: T) -> Self 111 | where 112 | T: fmt::Display, 113 | { 114 | self.modify.tooltip(tooltip); 115 | self 116 | } 117 | 118 | /// Send the modification. 119 | pub fn send(self) { 120 | _ = self.tx.send(InputEvent::ModifyArea { 121 | area_id: self.area_id, 122 | modify: self.modify, 123 | }); 124 | } 125 | } 126 | 127 | /// A builder returned by [`Sender::modify_menu_item`]. 128 | #[must_use = "Must call `send()` to apply changes"] 129 | pub struct ModifyMenuItemBuilder<'a> { 130 | tx: &'a mpsc::UnboundedSender, 131 | item_id: ItemId, 132 | modify: ModifyMenuItem, 133 | } 134 | 135 | impl ModifyMenuItemBuilder<'_> { 136 | /// Set the checked state of the menu item. 137 | pub fn checked(mut self, checked: bool) -> Self { 138 | self.modify.checked(checked); 139 | self 140 | } 141 | 142 | /// Set that the menu item should be highlighted. 143 | pub fn highlight(mut self, highlight: bool) -> Self { 144 | self.modify.highlight(highlight); 145 | self 146 | } 147 | 148 | /// Send the modification. 149 | pub fn send(self) { 150 | _ = self.tx.send(InputEvent::ModifyMenuItem { 151 | item_id: self.item_id, 152 | modify: self.modify, 153 | }); 154 | } 155 | } 156 | 157 | /// A builder returned by [`Sender::notification`]. 158 | #[must_use = "Must call `send()` to send the notification"] 159 | pub struct NotificationBuilder<'a> { 160 | tx: &'a mpsc::UnboundedSender, 161 | area_id: AreaId, 162 | id: NotificationId, 163 | notification: Notification, 164 | } 165 | 166 | impl NotificationBuilder<'_> { 167 | /// Set the message for the notification. 168 | /// 169 | /// # Examples 170 | /// 171 | /// ```no_run 172 | /// use winctx::CreateWindow; 173 | /// 174 | /// # async fn test() -> winctx::Result<()> { 175 | /// let mut window = CreateWindow::new("se.tedro.Example");; 176 | /// let area = window.new_area().id(); 177 | /// 178 | /// let (mut sender, _) = window.build().await?; 179 | /// 180 | /// let id = sender.notification(area) 181 | /// .message("This is a body") 182 | /// .send(); 183 | /// # Ok(()) } 184 | /// ``` 185 | pub fn message(mut self, message: M) -> Self 186 | where 187 | M: fmt::Display, 188 | { 189 | self.notification.message(message); 190 | self 191 | } 192 | 193 | /// Set the message for the notification. 194 | /// 195 | /// # Examples 196 | /// 197 | /// ```no_run 198 | /// use winctx::CreateWindow; 199 | /// 200 | /// # async fn test() -> winctx::Result<()> { 201 | /// let mut window = CreateWindow::new("se.tedro.Example");; 202 | /// let area = window.new_area().id(); 203 | /// 204 | /// let (mut sender, _) = window.build().await?; 205 | /// 206 | /// let id = sender.notification(area) 207 | /// .title("This is a title") 208 | /// .message("This is a body") 209 | /// .send(); 210 | /// # Ok(()) } 211 | /// ``` 212 | pub fn title(mut self, title: M) -> Self 213 | where 214 | M: fmt::Display, 215 | { 216 | self.notification.title(title); 217 | self 218 | } 219 | 220 | /// Set the notification to be informational. 221 | /// 222 | /// This among other things causes the icon to indicate that it's 223 | /// informational. 224 | /// 225 | /// # Examples 226 | /// 227 | /// ```no_run 228 | /// use winctx::CreateWindow; 229 | /// 230 | /// # async fn test() -> winctx::Result<()> { 231 | /// let mut window = CreateWindow::new("se.tedro.Example");; 232 | /// let area = window.new_area().id(); 233 | /// 234 | /// let (mut sender, _) = window.build().await?; 235 | /// 236 | /// let id = sender.notification(area) 237 | /// .info() 238 | /// .message("Something normal") 239 | /// .send(); 240 | /// # Ok(()) } 241 | /// ``` 242 | pub fn info(mut self) -> Self { 243 | self.notification.icon(NotificationIcon::Info); 244 | self 245 | } 246 | 247 | /// Set the notification to be a warning. 248 | /// 249 | /// This among other things causes the icon to indicate a warning. 250 | /// 251 | /// # Examples 252 | /// 253 | /// ```no_run 254 | /// use winctx::CreateWindow; 255 | /// 256 | /// # async fn test() -> winctx::Result<()> { 257 | /// let mut window = CreateWindow::new("se.tedro.Example");; 258 | /// let area = window.new_area().id(); 259 | /// 260 | /// let (mut sender, _) = window.build().await?; 261 | /// 262 | /// let id = sender.notification(area) 263 | /// .warning() 264 | /// .message("Something strange") 265 | /// .send(); 266 | /// # Ok(()) } 267 | /// ``` 268 | pub fn warning(mut self) -> Self { 269 | self.notification.icon(NotificationIcon::Warning); 270 | self 271 | } 272 | 273 | /// Set the notification to be an error. 274 | /// 275 | /// This among other things causes the icon to indicate an error. 276 | /// 277 | /// # Examples 278 | /// 279 | /// ```no_run 280 | /// use winctx::CreateWindow; 281 | /// 282 | /// # async fn test() -> winctx::Result<()> { 283 | /// let mut window = CreateWindow::new("se.tedro.Example");; 284 | /// let area = window.new_area().id(); 285 | /// 286 | /// let (mut sender, _) = window.build().await?; 287 | /// 288 | /// let id = sender.notification(area) 289 | /// .error() 290 | /// .message("Something broken") 291 | /// .send(); 292 | /// # Ok(()) } 293 | /// ``` 294 | pub fn error(mut self) -> Self { 295 | self.notification.icon(NotificationIcon::Error); 296 | self 297 | } 298 | 299 | /// Set a stock icon for the notification. 300 | /// 301 | /// # Examples 302 | /// 303 | /// ```no_run 304 | /// use winctx::CreateWindow; 305 | /// use winctx::icon::StockIcon; 306 | /// 307 | /// # async fn test() -> winctx::Result<()> { 308 | /// let mut window = CreateWindow::new("se.tedro.Example");; 309 | /// let area = window.new_area().id(); 310 | /// 311 | /// let (mut sender, _) = window.build().await?; 312 | /// 313 | /// let id = sender.notification(area) 314 | /// .error() 315 | /// .message("Something broken") 316 | /// .send(); 317 | /// # Ok(()) } 318 | /// ``` 319 | pub fn stock_icon(mut self, stock_icon: StockIcon) -> Self { 320 | self.notification 321 | .icon(NotificationIcon::StockIcon(stock_icon)); 322 | self 323 | } 324 | 325 | /// Do not play the sound associated with a notification. 326 | /// 327 | /// # Examples 328 | /// 329 | /// ```no_run 330 | /// use winctx::CreateWindow; 331 | /// 332 | /// # async fn test() -> winctx::Result<()> { 333 | /// let mut window = CreateWindow::new("se.tedro.Example");; 334 | /// let area = window.new_area().id(); 335 | /// 336 | /// let (mut sender, _) = window.build().await?; 337 | /// 338 | /// let id = sender.notification(area) 339 | /// .warning() 340 | /// .message("Something dangerous") 341 | /// .no_sound() 342 | /// .send(); 343 | /// # Ok(()) } 344 | /// ``` 345 | pub fn no_sound(mut self) -> Self { 346 | self.notification.no_sound(); 347 | self 348 | } 349 | 350 | /// The large version of the icon should be used as the notification icon. 351 | /// 352 | /// Note that this is a hint and might only have an effect in certain 353 | /// contexts. 354 | /// 355 | /// # Examples 356 | /// 357 | /// ```no_run 358 | /// use winctx::CreateWindow; 359 | /// 360 | /// # async fn test() -> winctx::Result<()> { 361 | /// let mut window = CreateWindow::new("se.tedro.Example");; 362 | /// let area = window.new_area().id(); 363 | /// 364 | /// let (mut sender, _) = window.build().await?; 365 | /// 366 | /// let id = sender.notification(area) 367 | /// .warning() 368 | /// .message("Something dangerous") 369 | /// .large_icon() 370 | /// .send(); 371 | /// # Ok(()) } 372 | /// ``` 373 | pub fn large_icon(mut self) -> Self { 374 | self.notification.large_icon(); 375 | self 376 | } 377 | 378 | /// Indicates that the icon should be highlighted for selection. 379 | /// 380 | /// Note that this only has an effect on certain icons: 381 | /// * Stock icons specified with [`NotificationBuilder::stock_icon`]. 382 | /// 383 | /// # Examples 384 | /// 385 | /// ```no_run 386 | /// use winctx::CreateWindow; 387 | /// use winctx::icon::StockIcon; 388 | /// 389 | /// # async fn test() -> winctx::Result<()> { 390 | /// let mut window = CreateWindow::new("se.tedro.Example");; 391 | /// let area = window.new_area().id(); 392 | /// 393 | /// let (mut sender, _) = window.build().await?; 394 | /// 395 | /// let id = sender.notification(area) 396 | /// .warning() 397 | /// .message("Something dangerous") 398 | /// .stock_icon(StockIcon::FOLDER) 399 | /// .icon_selected() 400 | /// .send(); 401 | /// # Ok(()) } 402 | /// ``` 403 | pub fn icon_selected(mut self) -> Self { 404 | self.notification.icon_selected(); 405 | self 406 | } 407 | 408 | /// Indicates that the icon should have the link overlay. 409 | /// 410 | /// Note that this only has an effect on certain icons: 411 | /// * Stock icons specified with [`NotificationBuilder::stock_icon`]. 412 | /// 413 | /// # Examples 414 | /// 415 | /// ```no_run 416 | /// use winctx::CreateWindow; 417 | /// use winctx::icon::StockIcon; 418 | /// 419 | /// # async fn test() -> winctx::Result<()> { 420 | /// let mut window = CreateWindow::new("se.tedro.Example");; 421 | /// let area = window.new_area().id(); 422 | /// 423 | /// let (mut sender, _) = window.build().await?; 424 | /// 425 | /// let id = sender.notification(area) 426 | /// .warning() 427 | /// .message("Something dangerous") 428 | /// .stock_icon(StockIcon::FOLDER) 429 | /// .icon_link_overlay() 430 | /// .send(); 431 | /// # Ok(()) } 432 | /// ``` 433 | pub fn icon_link_overlay(mut self) -> Self { 434 | self.notification.icon_link_overlay(); 435 | self 436 | } 437 | 438 | /// The notification should not be presented if the user is in "quiet time". 439 | /// 440 | /// # Examples 441 | /// 442 | /// ```no_run 443 | /// use winctx::CreateWindow; 444 | /// 445 | /// # async fn test() -> winctx::Result<()> { 446 | /// let mut window = CreateWindow::new("se.tedro.Example");; 447 | /// let area = window.new_area().id(); 448 | /// 449 | /// let (mut sender, _) = window.build().await?; 450 | /// 451 | /// let id = sender.notification(area) 452 | /// .warning() 453 | /// .message("Something dangerous") 454 | /// .respect_quiet_time() 455 | /// .send(); 456 | /// # Ok(()) } 457 | /// ``` 458 | pub fn respect_quiet_time(mut self) -> Self { 459 | self.notification.respect_quiet_time(); 460 | self 461 | } 462 | 463 | /// Send the modification and return the identifier of the sent 464 | /// notification. 465 | pub fn send(self) -> NotificationId { 466 | _ = self.tx.send(InputEvent::Notification { 467 | area_id: self.area_id, 468 | notification_id: self.id, 469 | notification: self.notification, 470 | }); 471 | self.id 472 | } 473 | } 474 | --------------------------------------------------------------------------------