diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index bd36c1b6..9947df86 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -29,7 +29,11 @@ pub struct Output { /// Events that may be useful to e.g. a screen reader. pub events: Vec, - /// 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, } @@ -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); } diff --git a/egui/src/widgets/text_edit.rs b/egui/src/widgets/text_edit.rs index 343c3b5e..28779cf7 100644 --- a/egui/src/widgets/text_edit.rs +++ b/egui/src/widgets/text_edit.rs @@ -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) diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index 5c9029f1..049d8c6c 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -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 diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 08996853..4c3220d1 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -137,7 +137,8 @@ pub struct AppRunner { prefer_dark_mode: Option, last_save_time: f64, screen_reader: crate::screen_reader::ScreenReader, - pub(crate) last_text_cursor_pos: Option, + pub(crate) text_cursor_pos: Option, + 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, }; { diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 293267b5..8325de61 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -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); 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) -> 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(()) }