From 31bdd75559e710aef4f6589340936035c214b0bb Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Tue, 13 Oct 2026 16:29:36 +0200 Subject: [PATCH] feat(app): turn on AccessKit, and name the controls drawn as icons eframe now builds with its accesskit feature, so the widget tree reaches the platform screen reader. Buttons drawn as an icon glyph were read as the icon font's private code point, and the hand-painted ones (play, poster tiles, the watched mark, download rings, the seek bar) as nothing. A Named trait gives them a name, and hint() makes the existing hover text that name. --- Cargo.lock | 324 +++++++++++++++++++++++++++- Cargo.toml | 4 +- crates/pstr-app/src/ui/account.rs | 3 +- crates/pstr-app/src/ui/history.rs | 3 +- crates/pstr-app/src/ui/library.rs | 12 +- crates/pstr-app/src/ui/mod.rs | 9 +- crates/pstr-app/src/ui/player.rs | 11 +- crates/pstr-app/src/ui/settings.rs | 6 +- crates/pstr-app/src/ui/title.rs | 2 + crates/pstr-app/src/ui/toast.rs | 3 +- crates/pstr-app/src/ui/transport.rs | 16 +- crates/pstr-app/src/ui/widgets.rs | 34 +++ 12 files changed, 401 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bbd3115..ec86380 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,110 @@ dependencies = [ "uuid", ] +[[package]] +name = "accesskit_atspi_common" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +dependencies = [ + "accesskit", + "accesskit_consumer 0.36.0", + "atspi-common", + "phf", + "serde", + "zvariant 5.15.0", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d10a236f96f87d70732e44520046785431ef01d5bcd6b041317bfadd2f88245" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce02dc63b43f0c9296af9ac946312a2dc8814427d7a64d2d600971dac55b6076" +dependencies = [ + "accesskit", + "accesskit_consumer 0.38.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus 5.19.0", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.62.2", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + [[package]] name = "adler2" version = "2.0.1" @@ -360,6 +464,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + [[package]] name = "async-io" version = "2.6.0" @@ -482,6 +600,43 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus 5.19.0", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names 4.3.4", + "zvariant 5.15.0", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus 5.19.0", +] + [[package]] name = "autocfg" version = "1.5.1" @@ -2017,6 +2172,7 @@ version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea6bf3608db949588b95b8b341ee358d0c3f95cf4dc3f53d8d76717edee87db" dependencies = [ + "accesskit_winit", "arboard", "bytemuck", "egui", @@ -5870,7 +6026,7 @@ dependencies = [ "rand 0.8.7", "serde", "sha2 0.10.9", - "zbus", + "zbus 4.4.0", ] [[package]] @@ -7229,6 +7385,11 @@ name = "uuid" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] [[package]] name = "valuable" @@ -8544,9 +8705,68 @@ dependencies = [ "uds_windows", "windows-sys 0.52.0", "xdg-home", - "zbus_macros", - "zbus_names", - "zvariant", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros 5.19.0", + "zbus_names 4.3.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant 5.15.0", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus-lockstep", + "zbus_xml", + "zvariant 5.15.0", ] [[package]] @@ -8559,7 +8779,22 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.3", + "zbus_names 4.3.4", + "zvariant 5.15.0", + "zvariant_utils 4.2.0", ] [[package]] @@ -8570,7 +8805,39 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" dependencies = [ "serde", "static_assertions", - "zvariant", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zbus_xml" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1586c021a01ca0a9216dcd874e546382e156a5cbab5fab6cb5f10087e22682a" +dependencies = [ + "serde", + "winnow 1.0.4", + "zbus_names 4.3.4", + "zvariant 5.15.0", +] + +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", ] [[package]] @@ -8704,7 +8971,22 @@ dependencies = [ "enumflags2", "serde", "static_assertions", - "zvariant_derive", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1d34c27cc6cdd1f458427519dd6b8612f7b7e3f7b9a0b2355d041dda9869147" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zcheapstr", + "zvariant_derive 5.15.0", + "zvariant_utils 4.2.0", ] [[package]] @@ -8717,7 +8999,20 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.119", - "zvariant_utils", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864155e69b4352db0c7f374917bf45d1e0c8d17659c8b3dbf9795f3673f8c497" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 3.0.3", + "zvariant_utils 4.2.0", ] [[package]] @@ -8730,3 +9025,16 @@ dependencies = [ "quote", "syn 2.0.119", ] + +[[package]] +name = "zvariant_utils" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 3.0.3", + "winnow 1.0.4", +] diff --git a/Cargo.toml b/Cargo.toml index 8c85630..805142f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,8 +73,10 @@ keyring = { version = "3" } clap = { version = "4", features = ["derive"] } # The UI. `glow` rather than `wgpu` because step two of the player hands mpv an # `MPV_RENDER_API_TYPE_OPENGL` context, and that has to be the same context -# eframe is drawing into. +# eframe is drawing into. `accesskit` hands the widget tree to the platform's +# screen reader (AT-SPI, UI Automation, NSAccessibility). eframe = { version = "0.35", default-features = false, features = [ + "accesskit", "default_fonts", "glow", "wayland", diff --git a/crates/pstr-app/src/ui/account.rs b/crates/pstr-app/src/ui/account.rs index 7e4d81e..0fe5d11 100644 --- a/crates/pstr-app/src/ui/account.rs +++ b/crates/pstr-app/src/ui/account.rs @@ -14,6 +14,7 @@ use crate::app::Action; use crate::engine::AccountStatus; use crate::theme; use crate::ui; +use crate::ui::widgets::Named; /// What the account section holds between frames. #[derive(Default)] @@ -379,7 +380,7 @@ pub fn browser( if !browser.trail.is_empty() && ui .button(egui_phosphor::regular::ARROW_LEFT) - .on_hover_text("Up a folder") + .hint("Up a folder") .clicked() { browser.up(actions); diff --git a/crates/pstr-app/src/ui/history.rs b/crates/pstr-app/src/ui/history.rs index e25579a..362f79a 100644 --- a/crates/pstr-app/src/ui/history.rs +++ b/crates/pstr-app/src/ui/history.rs @@ -10,6 +10,7 @@ use pstr_core::library::{Episode, Library, Title, TitleKind}; use crate::app::{Action, Page}; use crate::theme; +use crate::ui::widgets::Named; use crate::ui::{self, Art}; const STILL: egui::Vec2 = egui::vec2(128.0, 72.0); @@ -161,7 +162,7 @@ fn row( ) .frame(false), ) - .on_hover_text(if episode.is_watched() { + .hint(if episode.is_watched() { "Remove from history — marks it unwatched" } else { "Remove from history — forgets where you stopped" diff --git a/crates/pstr-app/src/ui/library.rs b/crates/pstr-app/src/ui/library.rs index 55f43a0..c110e71 100644 --- a/crates/pstr-app/src/ui/library.rs +++ b/crates/pstr-app/src/ui/library.rs @@ -315,8 +315,16 @@ fn shelf_controls(ui: &mut egui::Ui, view: &LibraryView, actions: &mut Vec egui::Respo ); let (rect, response) = ui.allocate_exact_size(size, Sense::click()); + let response = response.named(text); if !ui.is_rect_visible(rect) { return response; } @@ -626,6 +628,11 @@ pub fn card(ui: &mut egui::Ui, card: Card<'_>) -> egui::Response { let (rect, response) = ui.allocate_exact_size(Vec2::new(width, image_height + text_height), Sense::click()); + let response = if card.subtitle.is_empty() { + response.named(card.name) + } else { + response.named(format!("{}, {}", card.name, card.subtitle)) + }; // Reached with the arrow keys or Tab: bring it into view, before the // early return below would skip a tile that is just off screen. if response.gained_focus() { diff --git a/crates/pstr-app/src/ui/player.rs b/crates/pstr-app/src/ui/player.rs index 1734a9a..93f48b4 100644 --- a/crates/pstr-app/src/ui/player.rs +++ b/crates/pstr-app/src/ui/player.rs @@ -20,6 +20,7 @@ use egui::{Align2, Color32, CornerRadius, Rect, Sense, Vec2}; use crate::app::{Action, Adjacent}; use crate::playback::{Command, Playback}; use crate::theme; +use crate::ui::widgets::Named; use crate::ui::{self, transport::Neighbours}; /// How long the controls stay up after the pointer stops moving. @@ -586,7 +587,7 @@ fn up_next_card( theme::Role::Body.font(), Color32::WHITE, ); - if response.on_hover_text("Play now").clicked() { + if response.hint("Play now").clicked() { actions.push(Action::PlayAdjacent(Adjacent::Next)); } @@ -746,7 +747,7 @@ fn chrome( .corner_radius(CornerRadius::same(theme::radius::MD)) .min_size(Vec2::new(38.0, 32.0)), ) - .on_hover_text(if fullscreen { + .hint(if fullscreen { "Leave fullscreen (F)" } else { "Fullscreen (F)" @@ -778,7 +779,7 @@ fn chrome( .corner_radius(CornerRadius::same(theme::radius::MD)) .min_size(Vec2::new(38.0, 32.0)), ) - .on_hover_text("Episodes (E)") + .hint("Episodes (E)") .clicked() { actions.push(Action::ToggleEpisodes); @@ -798,7 +799,7 @@ fn chrome( .corner_radius(CornerRadius::same(theme::radius::MD)) .min_size(Vec2::new(38.0, 32.0)), ) - .on_hover_text("Stop") + .hint("Stop") .clicked() { actions.push(Action::Player(Command::Stop)); @@ -904,7 +905,7 @@ fn episode_drawer( ) .frame(false), ) - .on_hover_text("Close (E)") + .hint("Close (E)") .clicked() { actions.push(Action::ToggleEpisodes); diff --git a/crates/pstr-app/src/ui/settings.rs b/crates/pstr-app/src/ui/settings.rs index f17b534..efd9464 100644 --- a/crates/pstr-app/src/ui/settings.rs +++ b/crates/pstr-app/src/ui/settings.rs @@ -11,9 +11,10 @@ use pstr_core::prefs::PlaybackPrefs; use pstr_player::LANGUAGES; use crate::app::Action; -use crate::desktop_prefs::{CACHE_CHOICES, DesktopPrefs}; +use crate::desktop_prefs::{CACHE_CHOICES, DesktopPrefs, TEXT_SIZE_CHOICES}; use crate::theme; use crate::ui; +use crate::ui::widgets::Named; /// The settings that are a single value rather than a form. /// @@ -79,6 +80,7 @@ fn appearance_form(ui: &mut egui::Ui, appearance: Appearance, actions: &mut Vec< ui.horizontal_wrapped(|ui| { for flavor in Flavor::ALL { if palette_card(ui, appearance, flavor) + .named(flavor.label()) .on_hover_text(flavor.description()) .clicked() && appearance.flavor != flavor @@ -269,7 +271,7 @@ fn swatch(ui: &mut egui::Ui, appearance: Appearance, accent: Accent) -> egui::Re } } response - .on_hover_text(accent.label()) + .hint(accent.label()) .on_hover_cursor(egui::CursorIcon::PointingHand) } diff --git a/crates/pstr-app/src/ui/title.rs b/crates/pstr-app/src/ui/title.rs index 4c30864..da932ef 100644 --- a/crates/pstr-app/src/ui/title.rs +++ b/crates/pstr-app/src/ui/title.rs @@ -8,6 +8,7 @@ use crate::app::{Action, Page}; use crate::engine::{DownloadItem, DownloadKey, DownloadState}; use crate::playback::PlaybackTarget; use crate::theme; +use crate::ui::widgets::Named; use crate::ui::{self, Art, Card}; pub struct OfflineView<'a> { @@ -878,6 +879,7 @@ pub(crate) fn still( actions: &mut Vec, ) { let (rect, response) = ui.allocate_exact_size(size, egui::Sense::click()); + let response = response.named(format!("Play {}", episode.label())); if !ui.is_rect_visible(rect) { return; } diff --git a/crates/pstr-app/src/ui/toast.rs b/crates/pstr-app/src/ui/toast.rs index 19e4496..7ca46d1 100644 --- a/crates/pstr-app/src/ui/toast.rs +++ b/crates/pstr-app/src/ui/toast.rs @@ -9,6 +9,7 @@ use egui::{Align2, Color32, CornerRadius, Sense, Stroke, Vec2}; use crate::app::Action; use crate::theme; +use crate::ui::widgets::Named; /// How long a message stays up. const SECONDS: f64 = 5.0; @@ -214,7 +215,7 @@ fn card(ui: &mut egui::Ui, toast: &Toast) -> Pressed { [c + Vec2::new(-d, d), c + Vec2::new(d, -d)], Stroke::new(1.5, ink), ); - if response.on_hover_text("Dismiss").clicked() { + if response.hint("Dismiss").clicked() { pressed = Pressed::Close; } if let Some((label, _)) = &toast.action { diff --git a/crates/pstr-app/src/ui/transport.rs b/crates/pstr-app/src/ui/transport.rs index 9a34482..4de41c2 100644 --- a/crates/pstr-app/src/ui/transport.rs +++ b/crates/pstr-app/src/ui/transport.rs @@ -17,6 +17,7 @@ use crate::app::{Action, Adjacent, Page}; use crate::playback::{Command, Playback}; use crate::theme; use crate::ui; +use crate::ui::widgets::Named; /// Height of the clickable seek bar. Comfortably larger than the few pixels it /// draws, because a seek bar you have to aim at is a seek bar nobody uses. @@ -163,7 +164,7 @@ pub fn mini( egui::StrokeKind::Inside, ); } - if response.on_hover_text("Show the picture again").clicked() { + if response.hint("Show the picture again").clicked() { actions.push(Action::Goto(Page::Player)); } } @@ -313,7 +314,7 @@ fn play_pause(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec, } if response - .on_hover_text(if playback.paused { + .hint(if playback.paused { "Play (Space)" } else { "Pause (Space)" @@ -354,7 +355,7 @@ fn step_button( enabled, egui::Button::new(theme::Role::Subhead.rich(label)).min_size(Vec2::new(38.0, 30.0)), ); - if response.on_hover_text(hover).clicked() { + if response.hint(hover).clicked() { push(actions); } } @@ -436,7 +437,7 @@ fn volume(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec) { }; if ui .button(theme::Role::Subhead.rich(icon)) - .on_hover_text(format!("Mute (M) — {:.0}%", playback.volume)) + .hint(format!("Mute (M) — {:.0}%", playback.volume)) .clicked() { actions.push(Action::ToggleMute); @@ -567,6 +568,13 @@ fn seek_bar(ui: &mut egui::Ui, playback: &Playback, actions: &mut Vec, t let width = ui.available_width(); let (rect, response) = ui.allocate_exact_size(Vec2::new(width, SEEK_HIT_HEIGHT), Sense::click_and_drag()); + response.widget_info(|| { + egui::WidgetInfo::slider( + playback.duration.is_some(), + playback.position, + format!("Position, {}", ui::format_time(playback.position)), + ) + }); let active = response.hovered() || response.dragged(); // Grows rather than jumps when the pointer arrives. diff --git a/crates/pstr-app/src/ui/widgets.rs b/crates/pstr-app/src/ui/widgets.rs index eb0de65..590509b 100644 --- a/crates/pstr-app/src/ui/widgets.rs +++ b/crates/pstr-app/src/ui/widgets.rs @@ -8,6 +8,31 @@ use egui::{Color32, CornerRadius, Sense, Stroke, Vec2}; use crate::theme; +/// A name for a screen reader, on a control whose face does not say it. +/// +/// A button drawn as an icon glyph is announced as the icon font's private +/// code point, and one painted by hand as nothing at all. +pub trait Named { + /// The control's name to a screen reader. + fn named(self, name: impl Into) -> Self; + /// Hover text that is also the control's name. + fn hint(self, text: impl Into) -> Self; +} + +impl Named for egui::Response { + fn named(self, name: impl Into) -> Self { + let name = name.into(); + let enabled = self.enabled(); + self.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, enabled, &name)); + self + } + + fn hint(self, text: impl Into) -> Self { + let text = text.into(); + self.named(text.clone()).on_hover_text(text) + } +} + /// An on/off switch with its label beside it, for settings. /// /// Replaces egui's checkbox, whose unchecked box is drawn in the card colour — @@ -98,6 +123,9 @@ pub fn toggle(ui: &mut egui::Ui, on: &mut bool, text: &str) -> egui::Response { pub fn watched_mark(ui: &mut egui::Ui, watched: bool) -> egui::Response { const SIZE: f32 = 20.0; let (rect, response) = ui.allocate_exact_size(Vec2::splat(SIZE), Sense::click()); + response.widget_info(|| { + egui::WidgetInfo::selected(egui::WidgetType::Checkbox, true, watched, "Watched") + }); if ui.is_rect_visible(rect) { let ctx = ui.ctx(); let t = ctx.animate_bool_with_time(response.id, watched, theme::motion::STATE); @@ -278,6 +306,12 @@ pub enum DownloadGlyph { pub fn download_button(ui: &mut egui::Ui, glyph: DownloadGlyph) -> egui::Response { const SIZE: f32 = 30.0; let (rect, response) = ui.allocate_exact_size(Vec2::splat(SIZE), Sense::click()); + let response = response.named(match glyph { + DownloadGlyph::Download => "Download".to_owned(), + DownloadGlyph::Progress(done) => format!("Downloading, {:.0}%", done * 100.0), + DownloadGlyph::Paused(done) => format!("Download paused, {:.0}%", done * 100.0), + DownloadGlyph::Done => "Downloaded".to_owned(), + }); if !ui.is_rect_visible(rect) { return response; }