Skip to content

Commit

Permalink
Improve web text agent (#4561)
Browse files Browse the repository at this point in the history
- Closes #4060 - no longer aligned
to top
- Closes #4479 - `canvas.style` is
not set anywhere anymore
- Closes #2231 - same as #4060
- Closes #3618 - there is now one
`<input>` 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:
- <kbd>win + .</kbd> on Windows
- <kbd>ctrl + cmd + space</kbd> 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
<html>
  <body>
    <canvas></canvas>
  </body>
</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:
#4572
  • Loading branch information
jprochazk authored May 29, 2024
1 parent 5eee463 commit 514ee0c
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 216 deletions.
15 changes: 9 additions & 6 deletions crates/eframe/src/web/app_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -14,7 +14,7 @@ pub struct AppRunner {
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
last_save_time: f64,
pub(crate) ime: Option<egui::output::IMEOutput>,
pub(crate) text_agent: TextAgent,
pub(crate) mutable_text_under_cursor: bool,

// Output for the last run:
Expand All @@ -35,6 +35,7 @@ impl AppRunner {
canvas_id: &str,
web_options: crate::WebOptions,
app_creator: epi::AppCreator,
text_agent: TextAgent,
) -> Result<Self, String> {
let painter = super::ActiveWebPainter::new(canvas_id, &web_options).await?;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
);
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions crates/eframe/src/web/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
},
)?;

Expand Down
23 changes: 23 additions & 0 deletions crates/eframe/src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
/// - `<a>`/`<area>` with an `href` attribute
/// - `<input>`/`<select>`/`<textarea>`/`<button>` which aren't `disabled`
/// - any other element with a `tabindex` attribute
pub(crate) fn focused_element() -> Option<web_sys::Element> {
web_sys::window()?
.document()?
.active_element()?
.dyn_into()
.ok()
}

pub(crate) fn has_focus<T: JsCast>(element: &T) -> bool {
fn try_has_focus<T: JsCast>(element: &T) -> Option<bool> {
let element = element.dyn_ref::<web_sys::Element>()?;
let focused_element = focused_element()?;
Some(element == &focused_element)
}
try_has_focus(element).unwrap_or(false)
}

/// Current time in seconds (since undefined point in time).
///
/// Monotonically increasing.
Expand Down
Loading

0 comments on commit 514ee0c

Please sign in to comment.