From 514ee0c4334a0917855f9c7082e03b253851a4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Proch=C3=A1zka?= Date: Wed, 29 May 2024 12:54:33 +0200 Subject: [PATCH] Improve web text agent (#4561) - Closes https://github.com/emilk/egui/issues/4060 - no longer aligned to top - Closes https://github.com/emilk/egui/issues/4479 - `canvas.style` is not set anywhere anymore - Closes https://github.com/emilk/egui/issues/2231 - same as #4060 - Closes https://github.com/emilk/egui/issues/3618 - there is now one `` per `eframe` app, and it's removed transitively by `WebRunner::destroy -> AppRunner::drop -> TextAgent::drop` This PR improves the text agent to make fewer assumptions about how `egui` is embedded into the page: - Text agent no longer sets the canvas position - There is now a text agent for each instance of `WebRunner` - The input element is now moved to the correct position, so the OS can display the IME window in the correct place. Before it would typically be outside of the viewport The best way to test this is to build & server the web demo locally: ``` scripts/build_demo_web.sh && scripts/start_server.sh ``` Then open the EasyMark editor, and try using IME to input some emojis: http://localhost:8888/#EasyMarkEditor To open the emoji keyboard use: - win + . on Windows - ctrl + cmd + space on Mac Tested on: - [x] Windows - [x] Linux - [x] MacOS - [x] Android - [x] iOS ## Migration guide The canvas no longer controls its own size/position on the page. This means that those properties can now be controlled entirely via HTML and CSS, and multiple separate `eframe` apps can coexist better on a single page. To match the old behavior, set the `canvas` width and height to 100% of the `body` element: ```html ``` ```css /* remove default margins and use full viewport */ html, body { margin: 0; width: 100%; height: 100%; } canvas { /* match parent element size */ width: 100%; height: 100%; } ``` Note that there is no need to set `position: absolute`/`left: 50%; transform: translateX(-50%)`/etc., and setting those properties may poorly affect the sharpness of `egui`-rendered text. Because `eframe` no longer updates the canvas style in any way, it also means that on mobile, the canvas no longer collapses upwards to make space for a mobile keyboard. This should be solved in other ways: https://github.com/emilk/egui/issues/4572 --- crates/eframe/src/web/app_runner.rs | 15 +- crates/eframe/src/web/events.rs | 18 +- crates/eframe/src/web/mod.rs | 23 ++ crates/eframe/src/web/text_agent.rs | 371 ++++++++++++---------------- crates/eframe/src/web/web_runner.rs | 7 +- crates/egui/src/data/output.rs | 2 +- 6 files changed, 204 insertions(+), 232 deletions(-) diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 75fee203..f8b7f14a 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -2,7 +2,7 @@ use egui::TexturesDelta; use crate::{epi, App}; -use super::{now_sec, web_painter::WebPainter, NeedRepaint}; +use super::{now_sec, text_agent::TextAgent, web_painter::WebPainter, NeedRepaint}; pub struct AppRunner { #[allow(dead_code)] @@ -14,7 +14,7 @@ pub struct AppRunner { app: Box, pub(crate) needs_repaint: std::sync::Arc, last_save_time: f64, - pub(crate) ime: Option, + pub(crate) text_agent: TextAgent, pub(crate) mutable_text_under_cursor: bool, // Output for the last run: @@ -35,6 +35,7 @@ impl AppRunner { canvas_id: &str, web_options: crate::WebOptions, app_creator: epi::AppCreator, + text_agent: TextAgent, ) -> Result { let painter = super::ActiveWebPainter::new(canvas_id, &web_options).await?; @@ -119,7 +120,7 @@ impl AppRunner { app, needs_repaint, last_save_time: now_sec(), - ime: None, + text_agent, mutable_text_under_cursor: false, textures_delta: Default::default(), clipped_primitives: None, @@ -270,9 +271,11 @@ impl AppRunner { self.mutable_text_under_cursor = mutable_text_under_cursor; - if self.ime != ime { - super::text_agent::move_text_cursor(ime, self.canvas()); - self.ime = ime; + if let Err(err) = self.text_agent.move_to(ime, self.canvas()) { + log::error!( + "failed to update text agent position: {}", + super::string_from_js_value(&err) + ); } } } diff --git a/crates/eframe/src/web/events.rs b/crates/eframe/src/web/events.rs index 33d36c35..b2f371bd 100644 --- a/crates/eframe/src/web/events.rs +++ b/crates/eframe/src/web/events.rs @@ -94,8 +94,8 @@ pub(crate) fn install_document_events(runner_ref: &WebRunner) -> Result<(), JsVa if !modifiers.ctrl && !modifiers.command && !should_ignore_key(&key) - // When text agent is shown, it sends text event instead. - && text_agent::text_agent().hidden() + // When text agent is focused, it is responsible for handling input events + && !runner.text_agent.has_focus() { runner.input.raw.events.push(egui::Event::Text(key)); } @@ -375,10 +375,12 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu // event callback, which is why we run the app logic here and now: runner.logic(); + runner + .text_agent + .set_focus(runner.mutable_text_under_cursor); + // Make sure we paint the output of the above logic call asap: runner.needs_repaint.repaint_asap(); - - text_agent::update_text_agent(runner); } event.stop_propagation(); event.prevent_default(); @@ -467,13 +469,15 @@ pub(crate) fn install_canvas_events(runner_ref: &WebRunner) -> Result<(), JsValu runner.input.raw.events.push(egui::Event::PointerGone); push_touches(runner, egui::TouchPhase::End, &event); + + runner + .text_agent + .set_focus(runner.mutable_text_under_cursor); + runner.needs_repaint.repaint_asap(); event.stop_propagation(); event.prevent_default(); } - - // Finally, focus or blur text agent to toggle mobile keyboard: - text_agent::update_text_agent(runner); }, )?; diff --git a/crates/eframe/src/web/mod.rs b/crates/eframe/src/web/mod.rs index f98bbc1e..50e7aeb3 100644 --- a/crates/eframe/src/web/mod.rs +++ b/crates/eframe/src/web/mod.rs @@ -53,6 +53,29 @@ pub(crate) fn string_from_js_value(value: &JsValue) -> String { value.as_string().unwrap_or_else(|| format!("{value:#?}")) } +/// Returns the `Element` with active focus. +/// +/// Elements can only be focused if they are: +/// - ``/`` with an `href` attribute +/// - ``/`