diff --git a/.changes/badge_count.md b/.changes/badge_count.md new file mode 100644 index 000000000..018e128fc --- /dev/null +++ b/.changes/badge_count.md @@ -0,0 +1,5 @@ +--- +"tao": patch +--- + +Add `WindowExtUnix::set_badge_count` for Linux, `WindowExtIos::set_badge_count` for iOS, `WindowExtMacos::set_badge_label` for Macos, `MacdowExtWindows::set_overlay_icon` for Windows diff --git a/examples/icon.ico b/examples/icon.ico new file mode 100644 index 000000000..ecf0cf1de Binary files /dev/null and b/examples/icon.ico differ diff --git a/examples/overlay.rs b/examples/overlay.rs new file mode 100644 index 000000000..66215d301 --- /dev/null +++ b/examples/overlay.rs @@ -0,0 +1,129 @@ +// Copyright 2014-2021 The winit contributors +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +use std::env::current_dir; +use tao::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + keyboard::{Key, ModifiersState}, + window::WindowBuilder, +}; + +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +use tao::platform::unix::WindowExtUnix; + +#[cfg(target_os = "macos")] +use tao::platform::macos::WindowExtMacOS; + +#[cfg(target_os = "ios")] +use tao::platform::ios::WindowExtIOS; + +#[cfg(windows)] +use tao::{ + dpi::PhysicalSize, platform::windows::IconExtWindows, platform::windows::WindowExtWindows, + window::Icon, +}; + +#[allow(clippy::single_match)] +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + + let window = WindowBuilder::new().build(&event_loop).unwrap(); + + let mut modifiers = ModifiersState::default(); + + eprintln!("Key mappings:"); + #[cfg(windows)] + eprintln!(" [any key]: Show the Overlay Icon"); + #[cfg(not(windows))] + eprintln!(" [1-5]: Show a Badge count"); + eprintln!(" Ctrl+1: Clear"); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { event, .. } => match event { + WindowEvent::ModifiersChanged(new_state) => { + modifiers = new_state; + } + WindowEvent::KeyboardInput { + event: + KeyEvent { + logical_key: Key::Character(key_str), + state: ElementState::Released, + .. + }, + .. + } => { + let _count = match key_str { + "1" => 1, + "2" => 2, + "3" => 3, + "4" => 4, + "5" => 5, + _ => 20, + }; + + if modifiers.is_empty() { + #[cfg(windows)] + { + let mut path = current_dir().unwrap(); + path.push("./examples/icon.ico"); + let icon = Icon::from_path(path, Some(PhysicalSize::new(32, 32))).unwrap(); + + window.set_overlay_icon(Some(&icon)); + } + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + window.set_badge_count(Some(_count), None); + + #[cfg(target_os = "macos")] + window.set_badge_label(_count.to_string().into()); + + #[cfg(target_os = "ios")] + window.set_badge_count(_count); + } else if modifiers.control_key() && key_str == "1" { + #[cfg(windows)] + window.set_overlay_icon(None); + + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + window.set_badge_count(None, None); + + #[cfg(target_os = "macos")] + window.set_badge_label(None); + + #[cfg(target_os = "ios")] + window.set_badge_count(0); + } + } + _ => {} + }, + _ => {} + } + }); +} diff --git a/src/platform/ios.rs b/src/platform/ios.rs index 6b9141c3a..42b1f8a27 100644 --- a/src/platform/ios.rs +++ b/src/platform/ios.rs @@ -7,8 +7,9 @@ use std::os::raw::c_void; use crate::{ - event_loop::EventLoop, + event_loop::{EventLoop, EventLoopWindowTarget}, monitor::{MonitorHandle, VideoMode}, + platform_impl::set_badge_count, window::{Window, WindowBuilder}, }; @@ -98,6 +99,9 @@ pub trait WindowExtIOS { /// and then calls /// [`-[UIViewController setNeedsStatusBarAppearanceUpdate]`](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621354-setneedsstatusbarappearanceupdat?language=objc). fn set_prefers_status_bar_hidden(&self, hidden: bool); + + /// Sets the badge count on iOS launcher. 0 hides the count + fn set_badge_count(&self, count: i32); } impl WindowExtIOS for Window { @@ -142,6 +146,22 @@ impl WindowExtIOS for Window { fn set_prefers_status_bar_hidden(&self, hidden: bool) { self.window.set_prefers_status_bar_hidden(hidden) } + + #[inline] + fn set_badge_count(&self, count: i32) { + self.window.set_badge_count(count) + } +} + +pub trait EventLoopWindowTargetExtIOS { + /// Sets the badge count on iOS launcher. 0 hides the count + fn set_badge_count(&self, count: i32); +} + +impl EventLoopWindowTargetExtIOS for EventLoopWindowTarget { + fn set_badge_count(&self, count: i32) { + set_badge_count(count) + } } /// Additional methods on [`WindowBuilder`] that are specific to iOS. diff --git a/src/platform/macos.rs b/src/platform/macos.rs index f288895b6..932e8127f 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -10,7 +10,7 @@ use crate::{ dpi::{LogicalSize, Position}, event_loop::{EventLoop, EventLoopWindowTarget}, monitor::MonitorHandle, - platform_impl::{get_aux_state_mut, Parent}, + platform_impl::{get_aux_state_mut, set_badge_label, Parent}, window::{Window, WindowBuilder}, }; @@ -84,6 +84,9 @@ pub trait WindowExtMacOS { /// /// fn set_titlebar_transparent(&self, transparent: bool); + + /// Sets the badge label on the taskbar + fn set_badge_label(&self, label: Option); } impl WindowExtMacOS for Window { @@ -161,6 +164,11 @@ impl WindowExtMacOS for Window { fn set_titlebar_transparent(&self, transparent: bool) { self.window.set_titlebar_transparent(transparent); } + + #[inline] + fn set_badge_label(&self, label: Option) { + self.window.set_badge_label(label); + } } /// Corresponds to `NSApplicationActivationPolicy`. @@ -388,6 +396,9 @@ pub trait EventLoopWindowTargetExtMacOS { /// To set the activation policy before the app starts running, see /// [`EventLoopExtMacOS::set_activation_policy`](crate::platform::macos::EventLoopExtMacOS::set_activation_policy). fn set_activation_policy_at_runtime(&self, activation_policy: ActivationPolicy); + + /// Sets the badge label on macos dock + fn set_badge_label(&self, label: Option); } impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { @@ -415,4 +426,8 @@ impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget { let ns_activation_policy: NSApplicationActivationPolicy = activation_policy.into(); unsafe { msg_send![app, setActivationPolicy: ns_activation_policy] } } + + fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } } diff --git a/src/platform/unix.rs b/src/platform/unix.rs index 743f5be9a..eb784a92b 100644 --- a/src/platform/unix.rs +++ b/src/platform/unix.rs @@ -78,6 +78,8 @@ pub trait WindowExtUnix { /// Whether to show the window icon in the taskbar or not. fn set_skip_taskbar(&self, skip: bool) -> Result<(), ExternalError>; + + fn set_badge_count(&self, count: Option, desktop_filename: Option); } impl WindowExtUnix for Window { @@ -100,6 +102,10 @@ impl WindowExtUnix for Window { let window = UnixWindow::new_from_gtk_window(&event_loop_window_target.p, window)?; Ok(Window { window: window }) } + + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + self.window.set_badge_count(count, desktop_filename); + } } pub trait WindowBuilderExtUnix { @@ -208,6 +214,9 @@ pub trait EventLoopWindowTargetExtUnix { /// Returns the gtk application for this event loop. fn gtk_app(&self) -> >k::Application; + + /// Sets the badge count on the taskbar + fn set_badge_count(&self, count: Option, desktop_filename: Option); } impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { @@ -249,6 +258,11 @@ impl EventLoopWindowTargetExtUnix for EventLoopWindowTarget { fn gtk_app(&self) -> >k::Application { &self.p.app } + + #[inline] + fn set_badge_count(&self, count: Option, desktop_filename: Option) { + self.p.set_badge_count(count, desktop_filename); + } } unsafe extern "C" fn x_error_callback( diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 0d266b133..59d0d7c3d 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -159,6 +159,9 @@ pub trait WindowExtWindows { /// This sets `ICON_BIG`. A good ceiling here is 256x256. fn set_taskbar_icon(&self, taskbar_icon: Option); + /// This sets the overlay icon + fn set_overlay_icon(&self, icon: Option<&Icon>); + /// Returns the current window theme. fn theme(&self) -> Theme; @@ -238,6 +241,11 @@ impl WindowExtWindows for Window { fn set_rtl(&self, rtl: bool) { self.window.set_rtl(rtl) } + + #[inline] + fn set_overlay_icon(&self, icon: Option<&Icon>) { + self.window.set_overlay_icon(icon); + } } /// Additional methods on `WindowBuilder` that are specific to Windows. diff --git a/src/platform_impl/ios/badge.rs b/src/platform_impl/ios/badge.rs new file mode 100644 index 000000000..a2f04bdb3 --- /dev/null +++ b/src/platform_impl/ios/badge.rs @@ -0,0 +1,10 @@ +use objc::runtime::{Class, Object}; +use objc::{msg_send, sel, sel_impl}; + +pub fn set_badge_count(count: i32) { + unsafe { + let ui_application = Class::get("UIApplication").expect("Failed to get UIApplication class"); + let app: *mut Object = msg_send![ui_application, sharedApplication]; + let _: () = msg_send![app, setApplicationIconBadgeNumber:count]; + } +} diff --git a/src/platform_impl/ios/event_loop.rs b/src/platform_impl/ios/event_loop.rs index 6971ddfaf..ffc1b1bb8 100644 --- a/src/platform_impl/ios/event_loop.rs +++ b/src/platform_impl/ios/event_loop.rs @@ -31,7 +31,7 @@ use crate::platform_impl::platform::{ CFRunLoopSourceInvalidate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp, NSStringRust, UIApplicationMain, UIUserInterfaceIdiom, }, - monitor, view, MonitorHandle, + monitor, set_badge_count, view, MonitorHandle, }; #[non_exhaustive] @@ -94,6 +94,11 @@ impl EventLoopWindowTarget { debug!("`EventLoopWindowTarget::cursor_position` is ignored on iOS"); Ok((0, 0).into()) } + + /// Sets badge count on iOS launcher. 0 hides the count + pub fn set_badge_count(&self, count: i32) { + set_badge_count(count); + } } #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)] diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index b788c57d6..c0149563a 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -74,6 +74,7 @@ macro_rules! assert_main_thread { } mod app_state; +mod badge; mod event_loop; mod ffi; mod keycode; @@ -90,8 +91,8 @@ pub use self::{ monitor::{MonitorHandle, VideoMode}, window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId}, }; - pub(crate) use crate::icon::NoIcon as PlatformIcon; +pub(crate) use badge::set_badge_count; // todo: implement iOS keyboard event #[derive(Debug, Clone, Eq, PartialEq, Hash)] diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 26db35e06..2103fbace 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -23,7 +23,7 @@ use crate::{ id, CGFloat, CGPoint, CGRect, CGSize, UIEdgeInsets, UIInterfaceOrientationMask, UIRectEdge, UIScreenOverscanCompensation, }, - monitor, view, EventLoopWindowTarget, MonitorHandle, + monitor, set_badge_count, view, EventLoopWindowTarget, MonitorHandle, }, window::{ CursorIcon, Fullscreen, ResizeDirection, Theme, UserAttentionType, WindowAttributes, @@ -440,6 +440,11 @@ impl Inner { pub fn theme(&self) -> Theme { Theme::Light } + + /// Sets badge count on iOS launcher. 0 hides the count + pub fn set_badge_count(&self, count: i32) { + set_badge_count(count); + } } pub struct Window { diff --git a/src/platform_impl/linux/event_loop.rs b/src/platform_impl/linux/event_loop.rs index 1d776e550..e39ee9f19 100644 --- a/src/platform_impl/linux/event_loop.rs +++ b/src/platform_impl/linux/event_loop.rs @@ -160,6 +160,16 @@ impl EventLoopWindowTarget { } } + #[inline] + pub fn set_badge_count(&self, count: Option, desktop_filename: Option) { + if let Err(e) = self.window_requests_tx.send(( + WindowId::dummy(), + WindowRequest::BadgeCount(count, desktop_filename), + )) { + log::warn!("Fail to send update progress bar request: {e}"); + } + } + #[inline] pub fn set_theme(&self, theme: Option) { if let Err(e) = self @@ -432,6 +442,7 @@ impl EventLoop { }; } WindowRequest::ProgressBarState(_) => unreachable!(), + WindowRequest::BadgeCount(_, _) => unreachable!(), WindowRequest::SetTheme(_) => unreachable!(), WindowRequest::WireUpEvents { transparent, @@ -921,6 +932,9 @@ impl EventLoop { WindowRequest::ProgressBarState(state) => { taskbar.update(state); } + WindowRequest::BadgeCount(count, desktop_filename) => { + taskbar.update_count(count, desktop_filename); + } WindowRequest::SetTheme(theme) => { if let Some(settings) = Settings::default() { match theme { diff --git a/src/platform_impl/linux/taskbar.rs b/src/platform_impl/linux/taskbar.rs index 4b951ac34..ae7b98584 100644 --- a/src/platform_impl/linux/taskbar.rs +++ b/src/platform_impl/linux/taskbar.rs @@ -12,6 +12,9 @@ struct UnityLib { unity_launcher_entry_set_progress: unsafe extern "C" fn(entry: *const isize, value: f64) -> i32, unity_launcher_entry_set_progress_visible: unsafe extern "C" fn(entry: *const isize, value: i32) -> i32, + unity_launcher_entry_set_count: unsafe extern "C" fn(entry: *const isize, value: i64) -> i32, + unity_launcher_entry_set_count_visible: + unsafe extern "C" fn(entry: *const isize, value: bool) -> bool, } pub struct TaskbarIndicator { @@ -120,4 +123,39 @@ impl TaskbarIndicator { } } } + + pub fn update_count(&mut self, count: Option, desktop_filename: Option) { + if let Some(uri) = desktop_filename { + self.desktop_filename = Some(uri); + } + + self.ensure_lib_load(); + + if !self.is_unity_running() { + return; + } + + if let Some(uri) = &self.desktop_filename { + self.desktop_filename_c_str = Some(CString::new(uri.as_str()).unwrap_or_default()); + } + + if self.unity_entry.is_none() { + self.ensure_entry_load(); + } + + if let Some(unity_lib) = &self.unity_lib { + if let Some(unity_entry) = &self.unity_entry { + // Sets count + if let Some(count) = count { + unsafe { (unity_lib.unity_launcher_entry_set_count)(*unity_entry, count) }; + unsafe { (unity_lib.unity_launcher_entry_set_count_visible)(*unity_entry, true) }; + } + // removes the count + else { + unsafe { (unity_lib.unity_launcher_entry_set_count)(*unity_entry, 0) }; + unsafe { (unity_lib.unity_launcher_entry_set_count_visible)(*unity_entry, false) }; + } + } + } + } } diff --git a/src/platform_impl/linux/window.rs b/src/platform_impl/linux/window.rs index bb2c3de98..08c53a12e 100644 --- a/src/platform_impl/linux/window.rs +++ b/src/platform_impl/linux/window.rs @@ -1006,6 +1006,15 @@ impl Window { } } + pub fn set_badge_count(&self, count: Option, desktop_filename: Option) { + if let Err(e) = self.window_requests_tx.send(( + WindowId::dummy(), + WindowRequest::BadgeCount(count, desktop_filename), + )) { + log::warn!("Fail to send update badge count request: {}", e); + } + } + pub fn theme(&self) -> Theme { if let Some(theme) = *self.preferred_theme.borrow() { return theme; @@ -1068,6 +1077,7 @@ pub enum WindowRequest { }, SetVisibleOnAllWorkspaces(bool), ProgressBarState(ProgressBarState), + BadgeCount(Option, Option), SetTheme(Option), BackgroundColor(CssProvider, Option), } diff --git a/src/platform_impl/macos/badge.rs b/src/platform_impl/macos/badge.rs new file mode 100644 index 000000000..d7c5cf9e0 --- /dev/null +++ b/src/platform_impl/macos/badge.rs @@ -0,0 +1,12 @@ +use cocoa::{appkit::NSApp, base::nil, foundation::NSString}; + +pub fn set_badge_label(label: Option) { + unsafe { + let label = match label { + None => nil, + Some(label) => NSString::alloc(nil).init_str(&label), + }; + let dock_tile: cocoa::base::id = msg_send![NSApp(), dockTile]; + let _: cocoa::base::id = msg_send![dock_tile, setBadgeLabel: label]; + } +} diff --git a/src/platform_impl/macos/event_loop.rs b/src/platform_impl/macos/event_loop.rs index df24aed87..964252b65 100644 --- a/src/platform_impl/macos/event_loop.rs +++ b/src/platform_impl/macos/event_loop.rs @@ -37,7 +37,7 @@ use crate::{ observer::*, util::{self, IdRef}, }, - set_progress_indicator, + set_badge_label, set_progress_indicator, }, window::{ProgressBarState, Theme}, }; @@ -125,6 +125,16 @@ impl EventLoopWindowTarget { set_progress_indicator(progress); } + #[inline] + pub fn set_badge_count(&self, count: Option, _desktop_filename: Option) { + set_badge_label(count.map(|c| c.to_string())); + } + + #[inline] + pub fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } + #[inline] pub fn set_theme(&self, theme: Option) { set_ns_theme(theme) diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 5eada728d..fd53a5e86 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -7,6 +7,7 @@ mod app; mod app_delegate; mod app_state; +mod badge; mod event; mod event_loop; mod ffi; @@ -35,6 +36,7 @@ pub use self::{ use crate::{ error::OsError as RootOsError, event::DeviceId as RootDeviceId, window::WindowAttributes, }; +pub(crate) use badge::set_badge_label; pub(crate) use icon::PlatformIcon; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index c19827407..62c14b665 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -31,7 +31,7 @@ use crate::{ window_delegate::new_delegate, OsError, }, - set_progress_indicator, + set_badge_label, set_progress_indicator, }, window::{ CursorIcon, Fullscreen, ProgressBarState, ResizeDirection, Theme, UserAttentionType, @@ -1498,6 +1498,10 @@ impl UnownedWindow { pub fn set_progress_bar(&self, progress: ProgressBarState) { set_progress_indicator(progress); } + + pub fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } } impl WindowExtMacOS for UnownedWindow { @@ -1691,6 +1695,10 @@ impl WindowExtMacOS for UnownedWindow { .setTitlebarAppearsTransparent_(transparent as BOOL); } } + + fn set_badge_label(&self, label: Option) { + set_badge_label(label); + } } impl Drop for UnownedWindow { diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index d9143bd35..a1847b8d5 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -1023,6 +1023,22 @@ impl Window { } } + #[inline] + pub fn set_overlay_icon(&self, icon: Option<&Icon>) { + let taskbar: ITaskbarList = + unsafe { CoCreateInstance(&TaskbarList, None, CLSCTX_SERVER).unwrap() }; + + let icon = icon + .map(|i| i.inner.as_raw_handle()) + .unwrap_or(HICON::default()); + + unsafe { + taskbar + .SetOverlayIcon(self.window.0, icon, None) + .unwrap_or(()); + } + } + #[inline] pub fn set_undecorated_shadow(&self, shadow: bool) { let window = self.window.clone(); diff --git a/src/window.rs b/src/window.rs index 22f438db2..170c17a66 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1099,7 +1099,7 @@ impl Window { /// /// ## Platform-specific /// - /// - **Linux / macOS**: Progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME). + /// - **Linux / macOS**: Unlike windows, progress bar is app-wide and not specific to this window. Only supported desktop environments with `libunity` (e.g. GNOME). /// - **iOS / Android:** Unsupported. #[inline] pub fn set_progress_bar(&self, _progress: ProgressBarState) {