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",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "user_attention"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "usvg"
|
name = "usvg"
|
||||||
version = "0.28.0"
|
version = "0.28.0"
|
||||||
|
|
|
||||||
|
|
@ -802,6 +802,20 @@ impl Frame {
|
||||||
self.output.focus = Some(true);
|
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)
|
/// Maximize or unmaximize window. (native only)
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn set_maximized(&mut self, maximized: bool) {
|
pub fn set_maximized(&mut self, maximized: bool) {
|
||||||
|
|
@ -1126,6 +1140,10 @@ pub(crate) mod backend {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub focus: Option<bool>,
|
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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub screenshot_requested: bool,
|
pub screenshot_requested: bool,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,7 @@ pub fn handle_app_output(
|
||||||
minimized,
|
minimized,
|
||||||
maximized,
|
maximized,
|
||||||
focus,
|
focus,
|
||||||
|
attention,
|
||||||
} = app_output;
|
} = app_output;
|
||||||
|
|
||||||
if let Some(decorated) = decorated {
|
if let Some(decorated) = decorated {
|
||||||
|
|
@ -289,8 +290,17 @@ pub fn handle_app_output(
|
||||||
window_state.maximized = maximized;
|
window_state.maximized = maximized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if focus == Some(true) {
|
if !window.has_focus() {
|
||||||
window.focus_window();
|
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.visible = app_output.visible; // this is handled by post_present
|
||||||
self.frame.output.screenshot_requested = app_output.screenshot_requested;
|
self.frame.output.screenshot_requested = app_output.screenshot_requested;
|
||||||
|
if self.frame.output.attention.is_some() {
|
||||||
|
self.frame.output.attention = None;
|
||||||
|
}
|
||||||
handle_app_output(
|
handle_app_output(
|
||||||
window,
|
window,
|
||||||
self.egui_ctx.pixels_per_point(),
|
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.
|
/// A mouse cursor icon.
|
||||||
///
|
///
|
||||||
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
||||||
|
|
|
||||||
|
|
@ -357,7 +357,7 @@ pub use {
|
||||||
context::Context,
|
context::Context,
|
||||||
data::{
|
data::{
|
||||||
input::*,
|
input::*,
|
||||||
output::{self, CursorIcon, FullOutput, PlatformOutput, WidgetInfo},
|
output::{self, CursorIcon, FullOutput, PlatformOutput, UserAttentionType, WidgetInfo},
|
||||||
},
|
},
|
||||||
grid::Grid,
|
grid::Grid,
|
||||||
id::{Id, IdMap},
|
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