egui_web: improve text input on mobile and for IME

This commit is contained in:
Emil Ernerfeldt 2021-10-23 16:23:29 +02:00
parent 9a9b1b8746
commit 316202c33a
5 changed files with 62 additions and 30 deletions

View file

@ -29,7 +29,11 @@ pub struct Output {
/// Events that may be useful to e.g. a screen reader.
pub events: Vec<OutputEvent>,
/// Position of text edit cursor (used for IME).
/// Is there a mutable `TextEdit` under the cursor?
/// Use by `egui_web` to show/hide mobile keyboard and IME agent.
pub mutable_text_under_cursor: bool,
/// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>,
}
@ -65,6 +69,7 @@ impl Output {
copied_text,
needs_repaint,
mut events,
mutable_text_under_cursor,
text_cursor_pos,
} = newer;
@ -77,6 +82,7 @@ impl Output {
}
self.needs_repaint = needs_repaint; // if the last frame doesn't need a repaint, then we don't need to repaint
self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor;
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
}

View file

@ -611,6 +611,10 @@ impl<'t> TextEdit<'t> {
if interactive {
if let Some(pointer_pos) = ui.input().pointer.interact_pos() {
if response.hovered() && text.is_mutable() {
ui.output().mutable_text_under_cursor = true;
}
// TODO: triple-click to select whole paragraph
// TODO: drag selected text to either move or clone (ctrl on windows, alt on mac)
let singleline_offset = vec2(state.singleline_offset, 0.0);
@ -910,7 +914,9 @@ impl<'t> TextEdit<'t> {
&cursorp.primary,
);
if interactive {
if interactive && text.is_mutable() {
// egui_web uses `text_cursor_pos` when showing IME,
// so only set it when text is editable!
ui.ctx().output().text_cursor_pos = Some(
galley
.pos_from_cursor(&cursorp.primary)

View file

@ -12,6 +12,7 @@ All notable changes to the `egui_web` integration will be noted in this file.
### Fixed 🐛
* Fix multiline paste.
* Fix painting with non-opaque backgrounds.
* Improve text input on mobile and for IME.
## 0.14.1 - 2021-08-28

View file

@ -137,7 +137,8 @@ pub struct AppRunner {
prefer_dark_mode: Option<bool>,
last_save_time: f64,
screen_reader: crate::screen_reader::ScreenReader,
pub(crate) last_text_cursor_pos: Option<egui::Pos2>,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
}
impl AppRunner {
@ -163,7 +164,8 @@ impl AppRunner {
prefer_dark_mode,
last_save_time: now_sec(),
screen_reader: Default::default(),
last_text_cursor_pos: None,
text_cursor_pos: None,
mutable_text_under_cursor: false,
};
{

View file

@ -300,6 +300,7 @@ pub fn handle_output(output: &egui::Output, runner: &mut AppRunner) {
copied_text,
needs_repaint: _, // handled elsewhere
events: _, // we ignore these (TODO: accessibility screen reader)
mutable_text_under_cursor,
text_cursor_pos,
} = output;
@ -316,9 +317,11 @@ pub fn handle_output(output: &egui::Output, runner: &mut AppRunner) {
#[cfg(not(web_sys_unstable_apis))]
let _ = copied_text;
if &runner.last_text_cursor_pos != text_cursor_pos {
runner.mutable_text_under_cursor = *mutable_text_under_cursor;
if &runner.text_cursor_pos != text_cursor_pos {
move_text_cursor(text_cursor_pos, runner.canvas_id());
runner.last_text_cursor_pos = *text_cursor_pos;
runner.text_cursor_pos = *text_cursor_pos;
}
}
@ -903,7 +906,8 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
modifiers,
});
runner_lock.needs_repaint.set_true();
manipulate_agent(runner_lock.canvas_id(), runner_lock.input.latest_touch_pos);
update_text_agent(&runner_lock);
}
event.stop_propagation();
event.prevent_default();
@ -987,6 +991,7 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let runner_ref = runner_ref.clone();
let closure = Closure::wrap(Box::new(move |event: web_sys::TouchEvent| {
let mut runner_lock = runner_ref.0.lock();
if let Some(pos) = runner_lock.input.latest_touch_pos {
let modifiers = runner_lock.input.raw.modifiers;
// First release mouse to click:
@ -1007,10 +1012,10 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
// Finally, focus or blur on agent to toggle keyboard
manipulate_agent(runner_lock.canvas_id(), runner_lock.input.latest_touch_pos);
}
// Finally, focus or blur text agent to toggle mobile keyboard:
update_text_agent(&runner_lock);
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
@ -1169,40 +1174,52 @@ fn install_canvas_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
Ok(())
}
fn manipulate_agent(canvas_id: &str, latest_cursor: Option<egui::Pos2>) -> Option<()> {
/// Focus or blur text agent to toggle mobile keyboard.
fn update_text_agent(runner: &AppRunner) -> Option<()> {
use wasm_bindgen::JsCast;
use web_sys::HtmlInputElement;
let window = web_sys::window()?;
let document = window.document()?;
let input: HtmlInputElement = document.get_element_by_id(AGENT_ID)?.dyn_into().unwrap();
let cutsor_txt = document.body()?.style().get_property_value("cursor").ok()?;
let style = canvas_element(canvas_id)?.style();
if cutsor_txt == cursor_web_name(egui::CursorIcon::Text) {
input.set_hidden(false);
input.focus().ok()?;
// Panning canvas so that text edit is shown at 30%
// Only on touch screens, when keyboard popups
if let Some(p) = latest_cursor {
let inner_height = window.inner_height().ok()?.as_f64()? as f32;
let current_rel = p.y / inner_height;
let canvas_style = canvas_element(runner.canvas_id())?.style();
if current_rel > 0.5 {
// probably below the keyboard
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
if is_already_editing {
input.set_hidden(false);
input.focus().ok()?;
let target_rel = 0.3;
// Move up canvas so that text edit is shown at ~30% of screen height.
// Only on touch screens, when keyboard popups.
if let Some(latest_touch_pos) = runner.input.latest_touch_pos {
let window_height = window.inner_height().ok()?.as_f64()? as f32;
let current_rel = latest_touch_pos.y / window_height;
let delta = target_rel - current_rel;
let new_pos_percent = (delta * 100.0).round().to_string() + "%";
// estimated amount of screen covered by keyboard
let keyboard_fraction = 0.5;
style.set_property("position", "absolute").ok()?;
style.set_property("top", &new_pos_percent).ok()?;
if current_rel > keyboard_fraction {
// below the keyboard
let target_rel = 0.3;
// Note: `delta` is negative, since we are moving the canvas UP
let delta = target_rel - current_rel;
let delta = delta.max(-keyboard_fraction); // Don't move it crazy much
let new_pos_percent = (delta * 100.0).round().to_string() + "%";
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", &new_pos_percent).ok()?;
}
}
}
} else {
input.blur().ok()?;
input.set_hidden(true);
style.set_property("position", "absolute").ok()?;
style.set_property("top", "0%").ok()?; // move back to normal position
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
}
Some(())
}