diff --git a/Cargo.lock b/Cargo.lock index dbef430a..f567218c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3813,6 +3813,13 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "user_attention" +version = "0.1.0" +dependencies = [ + "eframe", +] + [[package]] name = "usvg" version = "0.28.0" diff --git a/crates/eframe/src/epi.rs b/crates/eframe/src/epi.rs index ba06562d..33166362 100644 --- a/crates/eframe/src/epi.rs +++ b/crates/eframe/src/epi.rs @@ -802,6 +802,20 @@ impl Frame { self.output.focus = Some(true); } + /// If the window is unfocused, attract the user's attention (native only). + /// + /// Typically, this means that the window will flash on the taskbar, or bounce, until it is interacted with. + /// + /// When the window comes into focus, or if `None` is passed, the attention request will be automatically reset. + /// + /// See [winit's documentation][user_attention_details] for platform-specific effect details. + /// + /// [user_attention_details]: https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html + #[cfg(not(target_arch = "wasm32"))] + pub fn request_user_attention(&mut self, kind: egui::UserAttentionType) { + self.output.attention = Some(kind); + } + /// Maximize or unmaximize window. (native only) #[cfg(not(target_arch = "wasm32"))] pub fn set_maximized(&mut self, maximized: bool) { @@ -1126,6 +1140,10 @@ pub(crate) mod backend { #[cfg(not(target_arch = "wasm32"))] pub focus: Option, + /// Set to request a user's attention to the native window. + #[cfg(not(target_arch = "wasm32"))] + pub attention: Option, + #[cfg(not(target_arch = "wasm32"))] pub screenshot_requested: bool, } diff --git a/crates/eframe/src/native/epi_integration.rs b/crates/eframe/src/native/epi_integration.rs index 9b6d4f36..f7e79a9b 100644 --- a/crates/eframe/src/native/epi_integration.rs +++ b/crates/eframe/src/native/epi_integration.rs @@ -235,6 +235,7 @@ pub fn handle_app_output( minimized, maximized, focus, + attention, } = app_output; if let Some(decorated) = decorated { @@ -289,8 +290,17 @@ pub fn handle_app_output( window_state.maximized = maximized; } - if focus == Some(true) { - window.focus_window(); + if !window.has_focus() { + if focus == Some(true) { + window.focus_window(); + } else if let Some(attention) = attention { + use winit::window::UserAttentionType; + window.request_user_attention(match attention { + egui::UserAttentionType::Reset => None, + egui::UserAttentionType::Critical => Some(UserAttentionType::Critical), + egui::UserAttentionType::Informational => Some(UserAttentionType::Informational), + }); + } } } @@ -487,6 +497,9 @@ impl EpiIntegration { } self.frame.output.visible = app_output.visible; // this is handled by post_present self.frame.output.screenshot_requested = app_output.screenshot_requested; + if self.frame.output.attention.is_some() { + self.frame.output.attention = None; + } handle_app_output( window, self.egui_ctx.pixels_per_point(), diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index b0bfa9e6..06dd2bfe 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -185,6 +185,23 @@ impl OpenUrl { } } +/// Types of attention to request from a user when a native window is not in focus. +/// +/// See [winit's documentation][user_attention_type] for platform-specific meaning of the attention types. +/// +/// [user_attention_type]: https://docs.rs/winit/latest/winit/window/enum.UserAttentionType.html +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UserAttentionType { + /// Request an elevated amount of animations and flair for the window and the task bar or dock icon. + Critical, + + /// Request a standard amount of attention-grabbing actions. + Informational, + + /// Reset the attention request and interrupt related animations and flashes. + Reset, +} + /// A mouse cursor icon. /// /// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration. diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 6280bbab..f0364dac 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -357,7 +357,7 @@ pub use { context::Context, data::{ input::*, - output::{self, CursorIcon, FullOutput, PlatformOutput, WidgetInfo}, + output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo}, }, grid::Grid, id::{Id, IdMap}, diff --git a/examples/user_attention/Cargo.toml b/examples/user_attention/Cargo.toml new file mode 100644 index 00000000..56836bfa --- /dev/null +++ b/examples/user_attention/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "user_attention" +version = "0.1.0" +authors = ["TicClick "] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.65" +publish = false + +[dependencies] +eframe = { path = "../../crates/eframe" } diff --git a/examples/user_attention/README.mg b/examples/user_attention/README.mg new file mode 100644 index 00000000..4fdc1793 --- /dev/null +++ b/examples/user_attention/README.mg @@ -0,0 +1,7 @@ +An example of requesting a user's attention to the main window, and resetting the ongoing attention animations when necessary. Only works on native platforms. + +```sh +cargo run -p user_attention +``` + +![](screenshot.png) diff --git a/examples/user_attention/screenshot.png b/examples/user_attention/screenshot.png new file mode 100644 index 00000000..3387d313 Binary files /dev/null and b/examples/user_attention/screenshot.png differ diff --git a/examples/user_attention/src/main.rs b/examples/user_attention/src/main.rs new file mode 100644 index 00000000..2512a8a8 --- /dev/null +++ b/examples/user_attention/src/main.rs @@ -0,0 +1,130 @@ +use eframe::egui::{Button, CentralPanel, Context, UserAttentionType}; +use eframe::{CreationContext, NativeOptions}; + +use std::time::{Duration, SystemTime}; + +fn repr(attention: UserAttentionType) -> String { + format!("{:?}", attention) +} + +struct Application { + attention: UserAttentionType, + request_at: Option, + + auto_reset: bool, + reset_at: Option, +} + +impl Application { + fn new(_cc: &CreationContext<'_>) -> Self { + Self { + attention: UserAttentionType::Informational, + request_at: None, + auto_reset: false, + reset_at: None, + } + } + + fn attention_reset_timeout() -> Duration { + Duration::from_secs(3) + } + + fn attention_request_timeout() -> Duration { + Duration::from_secs(2) + } + + fn repaint_max_timeout() -> Duration { + Duration::from_secs(1) + } +} + +impl eframe::App for Application { + fn update(&mut self, ctx: &Context, frame: &mut eframe::Frame) { + if let Some(request_at) = self.request_at { + if request_at < SystemTime::now() { + self.request_at = None; + frame.request_user_attention(self.attention); + if self.auto_reset { + self.auto_reset = false; + self.reset_at = Some(SystemTime::now() + Self::attention_reset_timeout()); + } + } + } + + if let Some(reset_at) = self.reset_at { + if reset_at < SystemTime::now() { + self.reset_at = None; + frame.request_user_attention(UserAttentionType::Reset); + } + } + + CentralPanel::default().show(ctx, |ui| { + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.label("Attention type:"); + eframe::egui::ComboBox::new("attention", "") + .selected_text(repr(self.attention)) + .show_ui(ui, |ui| { + for kind in [ + UserAttentionType::Informational, + UserAttentionType::Critical, + ] { + ui.selectable_value(&mut self.attention, kind, repr(kind)); + } + }) + }); + + let button_enabled = self.request_at.is_none() && self.reset_at.is_none(); + let button_text = if button_enabled { + format!( + "Request in {} seconds", + Self::attention_request_timeout().as_secs() + ) + } else { + match self.reset_at { + None => "Unfocus the window, fast!".to_owned(), + Some(t) => { + if let Ok(elapsed) = t.duration_since(SystemTime::now()) { + format!("Resetting attention in {} s...", elapsed.as_secs()) + } else { + "Resetting attention...".to_owned() + } + } + } + }; + + let resp = ui + .add_enabled(button_enabled, Button::new(button_text)) + .on_hover_text_at_pointer( + "After clicking, unfocus the application's window to see the effect", + ); + + ui.checkbox( + &mut self.auto_reset, + format!( + "Reset after {} seconds", + Self::attention_reset_timeout().as_secs() + ), + ); + + if resp.clicked() { + self.request_at = Some(SystemTime::now() + Self::attention_request_timeout()); + } + }); + }); + + ctx.request_repaint_after(Self::repaint_max_timeout()); + } +} + +fn main() -> eframe::Result<()> { + let native_options = NativeOptions { + initial_window_size: Some(eframe::egui::vec2(400., 200.)), + ..Default::default() + }; + eframe::run_native( + "User attention test", + native_options, + Box::new(|cc| Box::new(Application::new(cc))), + ) +}