Allow for requesting the user's attention to the window (#2905)
* add method for requesting attention to the main window * use another enum member for user attention type instead of nested `Option`s (also, document the enum members now that they don't mirror `winit`) * update the docstring Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * add an example app for testing window attention requests * Apply suggestions from code review Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com> * remove `chrono` dependency and improve the attention example's readability --------- Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
d1af798a9b
commit
e3a021eea6
|
|
@ -3813,6 +3813,13 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "user_attention"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usvg"
|
||||
version = "0.28.0"
|
||||
|
|
|
|||
|
|
@ -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<bool>,
|
||||
|
||||
/// Set to request a user's attention to the native window.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub attention: Option<egui::UserAttentionType>,
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub screenshot_requested: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
[package]
|
||||
name = "user_attention"
|
||||
version = "0.1.0"
|
||||
authors = ["TicClick <ya@ticclick.ch>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
eframe = { path = "../../crates/eframe" }
|
||||
|
|
@ -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
|
||||
```
|
||||
|
||||

|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
|
|
@ -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<SystemTime>,
|
||||
|
||||
auto_reset: bool,
|
||||
reset_at: Option<SystemTime>,
|
||||
}
|
||||
|
||||
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))),
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue