mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Web: anchor the text agent to the canvas (#8297)
* Follow-up to #8296, part of <https://github.com/emilk/egui/issues/8295> * [x] I have followed the instructions in the PR template Stacked on #8296 (base branch), kept as a separate PR so it can be reverted independently. The hidden text-agent `<input>` is now inserted as a *sibling of the canvas* (instead of appended to `document.body`) and positioned with `offsetLeft`/`offsetTop` instead of `getBoundingClientRect`. Since the input and the canvas share the same containing block, the input stays anchored to the canvas top-left corner no matter how the page is scrolled or how the canvas is embedded. Consequences: * Fixes the IME popup position when the host page is scrolled — `move_to` previously wrote *viewport* coordinates from `getBoundingClientRect` into document-absolute `left`/`top`. * Subsumes the mobile Safari virtual-keyboard workaround (it replaced the flapping `getBoundingClientRect` y with `offsetTop`, which is now used everywhere), so `is_mobile_safari()` is removed. * Removes the special-casing of document vs shadow DOM roots — sibling insertion works uniformly in both. * The input is `position: absolute`, so it does not participate in flex/grid layout of the canvas' parent and causes no layout shift. Caveat: host CSS selectors like `div > canvas:only-child` would no longer match. Verified with `cargo clippy -p eframe --target wasm32-unknown-unknown --all-features` and `cargo fmt --all`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::Document;
|
|
||||||
|
|
||||||
use super::{AppRunner, WebRunner};
|
use super::{AppRunner, WebRunner};
|
||||||
|
|
||||||
@@ -28,10 +27,9 @@ impl TextAgent {
|
|||||||
input.set_type("text");
|
input.set_type("text");
|
||||||
input.set_attribute("autocapitalize", "off")?;
|
input.set_attribute("autocapitalize", "off")?;
|
||||||
|
|
||||||
// Hide the element, and park it over the canvas
|
// Hide the element, and park it over the top-left corner of the canvas
|
||||||
// so that focusing it can never scroll some other part
|
// so that focusing it can never scroll some other part
|
||||||
// of the page into view.
|
// of the page into view.
|
||||||
let canvas_rect = super::canvas_content_rect(canvas);
|
|
||||||
let style = input.style();
|
let style = input.style();
|
||||||
style.set_property("background-color", "transparent")?;
|
style.set_property("background-color", "transparent")?;
|
||||||
style.set_property("border", "none")?;
|
style.set_property("border", "none")?;
|
||||||
@@ -40,21 +38,22 @@ impl TextAgent {
|
|||||||
style.set_property("height", "1px")?;
|
style.set_property("height", "1px")?;
|
||||||
style.set_property("caret-color", "transparent")?;
|
style.set_property("caret-color", "transparent")?;
|
||||||
style.set_property("position", "absolute")?;
|
style.set_property("position", "absolute")?;
|
||||||
style.set_property("top", &format!("{}px", canvas_rect.min.y))?;
|
style.set_property("top", &format!("{}px", canvas.offset_top()))?;
|
||||||
style.set_property("left", &format!("{}px", canvas_rect.min.x))?;
|
style.set_property("left", &format!("{}px", canvas.offset_left()))?;
|
||||||
// Prevent auto-zoom on mobile browsers (requires at least 16px).
|
// Prevent auto-zoom on mobile browsers (requires at least 16px).
|
||||||
style.set_property("font-size", "16px")?;
|
style.set_property("font-size", "16px")?;
|
||||||
|
|
||||||
let root = canvas.get_root_node();
|
// Insert the input as a sibling of the canvas, so that its
|
||||||
if root.has_type::<Document>() {
|
// `position: absolute` resolves against the same containing block
|
||||||
// root object is a document, append to its body
|
// as the canvas' `offset_top`/`offset_left`.
|
||||||
root.dyn_into::<Document>()?
|
// This anchors the input to the canvas regardless of how the page
|
||||||
.body()
|
// is scrolled or how the canvas is embedded, and also works when
|
||||||
.unwrap()
|
// the canvas is inside a shadow DOM.
|
||||||
.append_child(&input)?;
|
if let Some(parent) = canvas.parent_node() {
|
||||||
} else {
|
parent.insert_before(&input, canvas.next_sibling().as_ref())?;
|
||||||
// append input into root directly
|
} else if let Some(body) = document.body() {
|
||||||
root.append_child(&input)?;
|
log::warn!("Canvas has no parent element - appending text agent to document body");
|
||||||
|
body.append_child(&input)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focus the app on startup, without scrolling the page.
|
// Focus the app on startup, without scrolling the page.
|
||||||
@@ -187,29 +186,34 @@ impl TextAgent {
|
|||||||
// composition.
|
// composition.
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut canvas_rect = super::canvas_content_rect(canvas);
|
|
||||||
// Fix for safari with virtual keyboard flapping position
|
|
||||||
if is_mobile_safari() {
|
|
||||||
canvas_rect.min.y = canvas.offset_top() as f32;
|
|
||||||
}
|
|
||||||
let cursor_rect = ime.cursor_rect.translate(canvas_rect.min.to_vec2());
|
|
||||||
|
|
||||||
let style = self.input.style();
|
let style = self.input.style();
|
||||||
let native_ppp = super::native_pixels_per_point();
|
let native_ppp = super::native_pixels_per_point();
|
||||||
|
|
||||||
|
// The input is a sibling of the canvas (see `attach`), so we position
|
||||||
|
// it relative to the same containing block using the canvas offset.
|
||||||
|
// Unlike `get_bounding_client_rect`, the offset is unaffected by page
|
||||||
|
// scrolling, and doesn't flap when the virtual keyboard is shown on
|
||||||
|
// mobile Safari.
|
||||||
|
|
||||||
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
|
// Clamp the input position within the canvas width to prevent unwanted horizontal scrolling.
|
||||||
let logical_canvas_width = canvas.width() as f32 / native_ppp;
|
let logical_canvas_width = canvas.width() as f32 / native_ppp;
|
||||||
let visible_x = cursor_rect.center().x * zoom_factor;
|
let visible_x = ime.cursor_rect.center().x * zoom_factor;
|
||||||
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
|
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
|
||||||
|
|
||||||
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
|
// Clamp the input position within the canvas height to prevent unwanted vertical scrolling.
|
||||||
let logical_canvas_height = canvas.height() as f32 / native_ppp;
|
let logical_canvas_height = canvas.height() as f32 / native_ppp;
|
||||||
let visible_y = cursor_rect.center().y * zoom_factor;
|
let visible_y = ime.cursor_rect.center().y * zoom_factor;
|
||||||
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
|
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
|
||||||
|
|
||||||
// This is where the IME input will point to:
|
// This is where the IME input will point to:
|
||||||
style.set_property("left", &format!("{clamped_x}px"))?;
|
style.set_property(
|
||||||
style.set_property("top", &format!("{clamped_y}px"))?;
|
"left",
|
||||||
|
&format!("{}px", canvas.offset_left() as f32 + clamped_x),
|
||||||
|
)?;
|
||||||
|
style.set_property(
|
||||||
|
"top",
|
||||||
|
&format!("{}px", canvas.offset_top() as f32 + clamped_y),
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -256,16 +260,3 @@ impl Drop for TextAgent {
|
|||||||
self.input.remove();
|
self.input.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the app is likely running on a mobile device on navigator Safari.
|
|
||||||
fn is_mobile_safari() -> bool {
|
|
||||||
(|| {
|
|
||||||
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
|
|
||||||
let is_ios = user_agent.contains("iPhone")
|
|
||||||
|| user_agent.contains("iPad")
|
|
||||||
|| user_agent.contains("iPod");
|
|
||||||
let is_safari = user_agent.contains("Safari");
|
|
||||||
Some(is_ios && is_safari)
|
|
||||||
})()
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user