From 44cd304cdf80ad2ad3d1b8dba8caa3229a94be76 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 8 Mar 2021 20:58:01 +0100 Subject: [PATCH] Add experimental screen_reader feature Part of https://github.com/emilk/egui/issues/167 --- Cargo.lock | 318 +++++++++++++++++++++++++++++++- build_demo_web.sh | 12 +- eframe/Cargo.toml | 1 + egui/src/data/output.rs | 70 +++++++ egui_demo_app/Cargo.toml | 1 + egui_glium/Cargo.toml | 4 + egui_glium/src/backend.rs | 3 + egui_glium/src/lib.rs | 1 + egui_glium/src/screen_reader.rs | 44 +++++ egui_web/Cargo.toml | 2 + egui_web/src/backend.rs | 3 + egui_web/src/lib.rs | 1 + egui_web/src/screen_reader.rs | 47 +++++ 13 files changed, 504 insertions(+), 3 deletions(-) create mode 100644 egui_glium/src/screen_reader.rs create mode 100644 egui_web/src/screen_reader.rs diff --git a/Cargo.lock b/Cargo.lock index c9657112..47bea8e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,6 +66,15 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "000444226fcff248f2bc4c7625be32c63caccfecc2723a2b9f78a7487a49c407" +[[package]] +name = "ansi_term" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "atomic_refcell" version = "0.1.6" @@ -119,6 +128,29 @@ dependencies = [ "serde", ] +[[package]] +name = "bindgen" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd4865004a46a0aafb2a0a5eb19d3c9fc46ee5f063a6cfc605c69ac9ecf5263d" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "clap", + "env_logger", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "which", +] + [[package]] name = "bit-set" version = "0.5.2" @@ -176,6 +208,12 @@ version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + [[package]] name = "calloop" version = "0.6.5" @@ -201,6 +239,21 @@ version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4aedb84272dbe89af497cf81375129abda4fc0a9e7c5d317498c15cc30c0d27" +dependencies = [ + "nom 5.1.2", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -241,15 +294,30 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" +[[package]] +name = "clang-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54d78e30b388d4815220c8dd03fea5656b6c6d32adb59e89061552a102f8da1" +dependencies = [ + "glob", + "libc", + "libloading 0.7.0", +] + [[package]] name = "clap" version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ + "ansi_term", + "atty", "bitflags", + "strsim 0.8.0", "textwrap", "unicode-width", + "vec_map", ] [[package]] @@ -327,6 +395,16 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +[[package]] +name = "combine" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4369b5e4c0cddf64ad8981c0111e7df4f7078f4d6ba98fb31f2e17c4c57b7e" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "core-foundation" version = "0.7.0" @@ -541,7 +619,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.9.3", "syn", ] @@ -628,6 +706,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "dyn-clonable" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4" +dependencies = [ + "dyn-clonable-impl", + "dyn-clone", +] + +[[package]] +name = "dyn-clonable-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" + [[package]] name = "eframe" version = "0.10.0" @@ -678,6 +783,7 @@ dependencies = [ "glium", "serde", "serde_json", + "tts", "ureq", "webbrowser", ] @@ -691,6 +797,7 @@ dependencies = [ "js-sys", "serde", "serde_json", + "tts", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -709,6 +816,19 @@ dependencies = [ "serde", ] +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "epaint" version = "0.10.0" @@ -799,6 +919,12 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +[[package]] +name = "gcc" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f5f3913fa0bfe7ee1fd8248b6b9f42a5af4b9d65ec2dd2c3c26132b950ecfc2" + [[package]] name = "getrandom" version = "0.2.2" @@ -843,6 +969,12 @@ dependencies = [ "takeable-option", ] +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "glutin" version = "0.26.0" @@ -936,6 +1068,12 @@ dependencies = [ "libc", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "ident_case" version = "1.0.1" @@ -1021,6 +1159,20 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "jni" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1316,6 +1468,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "memchr", + "version_check", +] + [[package]] name = "nom" version = "6.1.2" @@ -1489,6 +1651,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1683,6 +1851,12 @@ version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.2.3" @@ -1814,6 +1988,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" + [[package]] name = "shared_library" version = "0.1.9" @@ -1824,6 +2004,12 @@ dependencies = [ "libc", ] +[[package]] +name = "shlex" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fdf1b9db47230893d76faad238fd6097fd6d6a9245cd7a4d90dbd639536bbd2" + [[package]] name = "slab" version = "0.4.2" @@ -1855,12 +2041,38 @@ dependencies = [ "wayland-protocols", ] +[[package]] +name = "speech-dispatcher" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076efad2c03a1c5cc41eaa82c30770130e014f6544769edea79e096a2f134412" +dependencies = [ + "lazy_static", + "speech-dispatcher-sys", +] + +[[package]] +name = "speech-dispatcher-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b662a91fe7e39d3d11edcf0297c717a8c05683a6c6445df9aba83b034b2b2db5" +dependencies = [ + "bindgen", + "gcc", +] + [[package]] name = "spin" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.9.3" @@ -1906,6 +2118,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36ae8932fcfea38b7d3883ae2ab357b0d57a02caaa18ebb4f5ece08beaec4aa0" +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -1994,6 +2215,37 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e5d7cd7ab3e47dda6e56542f4bbf3824c15234958c6e1bd6aaa347e93499fdc" +[[package]] +name = "tts" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592f268a2431704677a2cfa2d712dd0518b99bd1630f5977423e76db1cc826d2" +dependencies = [ + "cocoa-foundation", + "dyn-clonable", + "jni", + "lazy_static", + "libc", + "log", + "ndk-glue", + "objc", + "speech-dispatcher", + "thiserror", + "tts_winrt_bindings", + "wasm-bindgen", + "web-sys", + "winrt", +] + +[[package]] +name = "tts_winrt_bindings" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cf13b200c067076f7a6001b3eac9a97d4fa0a322b10a2f958fd1176a1e1461" +dependencies = [ + "winrt", +] + [[package]] name = "unicode-bidi" version = "0.3.4" @@ -2058,6 +2310,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.2" @@ -2270,6 +2528,15 @@ dependencies = [ "webpki", ] +[[package]] +name = "which" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +dependencies = [ + "libc", +] + [[package]] name = "widestring" version = "0.4.3" @@ -2350,6 +2617,53 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "winrt" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf62b1e56a9efd6caec59a053450dd9fe5bc3d0dc28a27d3054b85b68db95a4" +dependencies = [ + "sha1", + "winrt_macros", +] + +[[package]] +name = "winrt_gen" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497492d5f30d47cb397c3d5bee37700996822311062a3a609493523ad3bb9ae" +dependencies = [ + "proc-macro2", + "quote", + "serde_json", + "sha1", + "syn", + "winrt_gen_macros", +] + +[[package]] +name = "winrt_gen_macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cd78ebb3f0f35f3d9b7f3272b7afac1904cc3e3f636a0a6e5877d72c78b7d8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "winrt_macros" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d88594f878eee84909a5d20cad8bfef8587f9e1473ce81b74fbbd4b12748d662" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "winrt_gen", +] + [[package]] name = "ws2_32-sys" version = "0.2.1" @@ -2397,7 +2711,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9a231574ae78801646617cefd13bfe94be907c0e4fa979cfd8b770aa3c5d08" dependencies = [ - "nom", + "nom 6.1.2", ] [[package]] diff --git a/build_demo_web.sh b/build_demo_web.sh index 1d0773cc..4fab5395 100755 --- a/build_demo_web.sh +++ b/build_demo_web.sh @@ -13,7 +13,17 @@ rm -f docs/${CRATE_NAME}_bg.wasm echo "Building rust…" BUILD=release -cargo build --release --all-features -p ${CRATE_NAME} --lib --target wasm32-unknown-unknown +FEATURES="http,persistence" # screen_reader is experimental +# FEATURES="http,persistence,screen_reader" # screen_reader is experimental + +( + cd egui_demo_app && cargo build \ + -p ${CRATE_NAME} \ + --release \ + --lib \ + --target wasm32-unknown-unknown \ + --features ${FEATURES} +) echo "Generating JS bindings for wasm…" TARGET_NAME="${CRATE_NAME}.wasm" diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index a2091184..39970b54 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -30,4 +30,5 @@ egui_web = { version = "0.10.0", path = "../egui_web" } default = [] http = ["egui_glium/http", "egui_web/http"] persistence = ["epi/persistence", "egui_glium/persistence", "egui_web/persistence"] +screen_reader = ["egui_glium/screen_reader", "egui_web/screen_reader"] # experimental time = ["egui_glium/time"] # for seconds_since_midnight diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index c6f1a8cd..edca2ea7 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -31,6 +31,19 @@ impl Output { pub fn open_url(&mut self, url: impl Into) { self.open_url = Some(OpenUrl::same_tab(url)) } + + /// This can be used by a text-to-speech system to describe the events (if any). + pub fn events_description(&self) -> String { + // only describe last event: + for event in self.events.iter().rev() { + match event { + OutputEvent::WidgetEvent(WidgetEvent::Focus, widget_info) => { + return widget_info.description(); + } + } + } + Default::default() + } } #[derive(Clone, PartialEq)] @@ -202,6 +215,63 @@ impl WidgetInfo { ..Self::new(WidgetType::TextEdit) } } + + /// This can be used by a text-to-speech system to describe the widget. + pub fn description(&self) -> String { + let Self { + typ, + label, + edit_text, + selected, + value, + } = self; + + // TODO: localization + let widget_name = match typ { + WidgetType::Label => "", + WidgetType::Hyperlink => "link", + WidgetType::TextEdit => "text edit", + WidgetType::Button => "button", + WidgetType::Checkbox => "checkbox", + WidgetType::RadioButton => "radio", + WidgetType::SelectableLabel => "selectable", + WidgetType::ComboBox => "combo", + WidgetType::Slider => "slider", + WidgetType::DragValue => "drag value", + WidgetType::ColorButton => "color button", + WidgetType::ImageButton => "image button", + WidgetType::CollapsingHeader => "collapsing header", + WidgetType::Other => "", + }; + + let mut description = widget_name.to_owned(); + + if let Some(selected) = selected { + if *typ == WidgetType::Checkbox { + description += " "; + description += if *selected { "checked" } else { "unchecked" }; + } else { + description += if *selected { "selected" } else { "" }; + }; + } + + if let Some(label) = label { + description += " "; + description += label; + } + + if let Some(edit_text) = edit_text { + description += " "; + description += edit_text; + } + + if let Some(value) = value { + description += " "; + description += &value.to_string(); + } + + description.trim().to_owned() + } } /// The different types of built-in widgets in egui diff --git a/egui_demo_app/Cargo.toml b/egui_demo_app/Cargo.toml index cfe7a758..a4bc39f9 100644 --- a/egui_demo_app/Cargo.toml +++ b/egui_demo_app/Cargo.toml @@ -17,4 +17,5 @@ egui_demo_lib = { version = "0.10.0", path = "../egui_demo_lib" } default = ["persistence"] http = ["eframe/http", "egui_demo_lib/http"] persistence = ["eframe/persistence", "egui_demo_lib/persistence"] +screen_reader = ["eframe/screen_reader"] # experimental syntect = ["egui_demo_lib/syntect"] diff --git a/egui_glium/Cargo.toml b/egui_glium/Cargo.toml index 9a06ee38..9e30f39d 100644 --- a/egui_glium/Cargo.toml +++ b/egui_glium/Cargo.toml @@ -31,6 +31,9 @@ directories-next = { version = "2", optional = true } serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } +# feature screen_reader +tts = { version = "0.14", optional = true } + # feature "time" chrono = { version = "0.4", optional = true } @@ -46,3 +49,4 @@ persistence = [ "serde", ] time = ["chrono"] # for seconds_since_midnight +screen_reader = ["tts"] # experimental diff --git a/egui_glium/src/backend.rs b/egui_glium/src/backend.rs index abb777a2..c104d089 100644 --- a/egui_glium/src/backend.rs +++ b/egui_glium/src/backend.rs @@ -180,6 +180,8 @@ pub fn run(mut app: Box) -> ! { #[cfg(feature = "http")] let http = std::sync::Arc::new(crate::http::GliumHttp {}); + let mut screen_reader = crate::screen_reader::ScreenReader::default(); + if app.warm_up_enabled() { // let warm_up_start = Instant::now(); input_state.raw.time = Some(0.0); @@ -273,6 +275,7 @@ pub fn run(mut app: Box) -> ! { }; } + screen_reader.speak(&egui_output.events_description()); handle_output(egui_output, &display, clipboard.as_mut()); #[cfg(feature = "persistence")] diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index b8713d08..783391b3 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -15,6 +15,7 @@ pub mod http; mod painter; #[cfg(feature = "persistence")] pub mod persistence; +pub mod screen_reader; pub mod window_settings; pub use backend::*; diff --git a/egui_glium/src/screen_reader.rs b/egui_glium/src/screen_reader.rs new file mode 100644 index 00000000..098cf3aa --- /dev/null +++ b/egui_glium/src/screen_reader.rs @@ -0,0 +1,44 @@ +pub struct ScreenReader { + #[cfg(feature = "screen_reader")] + tts: Option, +} + +#[cfg(not(feature = "screen_reader"))] +impl Default for ScreenReader { + fn default() -> Self { + Self {} + } +} + +#[cfg(feature = "screen_reader")] +impl Default for ScreenReader { + fn default() -> Self { + let tts = match tts::TTS::default() { + Ok(screen_reader) => { + eprintln!("Initialized screen reader."); + Some(screen_reader) + } + Err(err) => { + eprintln!("Failed to load screen reader: {}", err); + None + } + }; + Self { tts } + } +} + +impl ScreenReader { + #[cfg(not(feature = "screen_reader"))] + pub fn speak(&mut self, _text: &str) {} + + #[cfg(feature = "screen_reader")] + pub fn speak(&mut self, text: &str) { + if let Some(tts) = &mut self.tts { + eprintln!("Speaking: {:?}", text); + let interrupt = true; + if let Err(err) = tts.speak(text, interrupt) { + eprintln!("Failed to read: {}", err); + } + } + } +} diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index d21872c0..89fa0365 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -25,6 +25,7 @@ epi = { version = "0.10.0", path = "../epi" } js-sys = "0.3" serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } +tts = { version = "0.14", optional = true } # feature screen_reader wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" @@ -39,6 +40,7 @@ http = [ "web-sys/Response", ] persistence = ["serde", "serde_json"] +screen_reader = ["tts"] # experimental [dependencies.web-sys] version = "0.3" diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index d78483ac..038b68ae 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -135,6 +135,7 @@ pub struct AppRunner { pub(crate) needs_repaint: std::sync::Arc, storage: LocalStorage, last_save_time: f64, + screen_reader: crate::screen_reader::ScreenReader, #[cfg(feature = "http")] http: Arc, } @@ -152,6 +153,7 @@ impl AppRunner { needs_repaint: Default::default(), storage, last_save_time: now_sec(), + screen_reader: Default::default(), #[cfg(feature = "http")] http: Arc::new(http::WebHttp {}), }) @@ -217,6 +219,7 @@ impl AppRunner { let egui_ctx = &self.web_backend.ctx; self.app.update(egui_ctx, &mut frame); let (egui_output, clipped_meshes) = self.web_backend.end_frame()?; + self.screen_reader.speak(&egui_output.events_description()); handle_output(&egui_output); { diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 42b6f151..e4886602 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -12,6 +12,7 @@ pub mod backend; #[cfg(feature = "http")] pub mod http; mod painter; +pub mod screen_reader; pub mod webgl1; pub mod webgl2; diff --git a/egui_web/src/screen_reader.rs b/egui_web/src/screen_reader.rs new file mode 100644 index 00000000..f3d75950 --- /dev/null +++ b/egui_web/src/screen_reader.rs @@ -0,0 +1,47 @@ +pub struct ScreenReader { + #[cfg(feature = "screen_reader")] + tts: Option, +} + +#[cfg(not(feature = "screen_reader"))] +impl Default for ScreenReader { + fn default() -> Self { + Self {} + } +} + +#[cfg(feature = "screen_reader")] +impl Default for ScreenReader { + fn default() -> Self { + let tts = match tts::TTS::default() { + Ok(screen_reader) => { + crate::console_log("Initialized screen reader."); + Some(screen_reader) + } + Err(err) => { + crate::console_warn(format!("Failed to load screen reader: {}", err)); + None + } + }; + Self { tts } + } +} + +impl ScreenReader { + #[cfg(not(feature = "screen_reader"))] + pub fn speak(&mut self, _text: &str) {} + + #[cfg(feature = "screen_reader")] + pub fn speak(&mut self, text: &str) { + if text.is_empty() { + return; + } + if let Some(tts) = &mut self.tts { + crate::console_log(format!("Speaking: {:?}", text)); + let interrupt = true; + if let Err(err) = tts.speak(text, interrupt) { + crate::console_warn(format!("Failed to read: {}", err)); + } + } + } +}