use std::cell::Cell;
use wasm_bindgen::prelude::*;
use web_sys::{Document, Node};
use super::{AppRunner, WebRunner};
pub struct TextAgent {
input: web_sys::HtmlInputElement,
prev_ime_output: Cell<Option<egui::output::IMEOutput>>,
}
impl TextAgent {
pub fn attach(runner_ref: &WebRunner, root: Node) -> Result<Self, JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlElement>()?;
input.set_autofocus(true)?;
let input = input.dyn_into::<web_sys::HtmlInputElement>()?;
input.set_type("text");
input.set_attribute("autocapitalize", "off")?;
let style = input.style();
style.set_property("background-color", "transparent")?;
style.set_property("border", "none")?;
style.set_property("outline", "none")?;
style.set_property("width", "1px")?;
style.set_property("height", "1px")?;
style.set_property("caret-color", "transparent")?;
style.set_property("position", "absolute")?;
style.set_property("top", "0")?;
style.set_property("left", "0")?;
style.set_property("font-size", "16px")?;
if root.has_type::<Document>() {
root.dyn_into::<Document>()?
.body()
.unwrap()
.append_child(&input)?;
} else {
root.append_child(&input)?;
}
let on_input = {
let input = input.clone();
move |event: web_sys::InputEvent, runner: &mut AppRunner| {
let text = input.value();
if !event.is_composing() {
input.blur().ok();
input.focus().ok();
}
if event.is_composing() {
let Some(text) = event.data() else { return };
let selection_start = input
.selection_start()
.unwrap_or(None)
.map(|pos| pos as usize);
let selection_end = input
.selection_end()
.unwrap_or(None)
.map(|pos| pos as usize);
let active_range_chars = if let Some(selection_start) = selection_start
&& let Some(selection_end) = selection_end
{
let text_utf16 = text.encode_utf16().collect::<Vec<u16>>();
let text_before_selection =
String::from_utf16_lossy(&text_utf16[..selection_start]);
let text_in_selection =
String::from_utf16_lossy(&text_utf16[selection_start..selection_end]);
let count_before_selection = text_before_selection.chars().count();
let count_in_selection = text_in_selection.chars().count();
Some(count_before_selection..count_before_selection + count_in_selection)
} else {
None
};
let event = egui::Event::Ime(egui::ImeEvent::Preedit {
text,
active_range_chars,
});
runner.input.raw.events.push(event);
} else {
if text.is_empty() {
return;
}
input.set_value("");
let event = egui::Event::Text(text);
runner.input.raw.events.push(event);
}
runner.needs_repaint.repaint_asap();
}
};
let on_composition_start = {
move |_: web_sys::CompositionEvent, runner: &mut AppRunner| {
runner.needs_repaint.repaint_asap();
}
};
let on_composition_end = {
let input = input.clone();
move |event: web_sys::CompositionEvent, runner: &mut AppRunner| {
let Some(text) = event.data() else { return };
input.set_value("");
let event = egui::Event::Ime(egui::ImeEvent::Commit(text));
runner.input.raw.events.push(event);
runner.needs_repaint.repaint_asap();
}
};
runner_ref.add_event_listener(&input, "input", on_input)?;
runner_ref.add_event_listener(&input, "compositionstart", on_composition_start)?;
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
runner_ref.add_event_listener(&input, "keydown", super::events::on_keydown)?;
runner_ref.add_event_listener(&input, "keyup", super::events::on_keyup)?;
Ok(Self {
input,
prev_ime_output: Default::default(),
})
}
pub fn move_to(
&self,
ime: Option<egui::output::IMEOutput>,
canvas: &web_sys::HtmlCanvasElement,
zoom_factor: f32,
) -> Result<(), JsValue> {
if self.prev_ime_output.get() == ime {
return Ok(());
}
self.prev_ime_output.set(ime);
let Some(ime) = ime else { return Ok(()) };
if ime.should_interrupt_composition {
}
let mut canvas_rect = super::canvas_content_rect(canvas);
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 native_ppp = super::native_pixels_per_point();
let logical_canvas_width = canvas.width() as f32 / native_ppp;
let visible_x = cursor_rect.center().x * zoom_factor;
let clamped_x = visible_x.clamp(0.0, logical_canvas_width);
let logical_canvas_height = canvas.height() as f32 / native_ppp;
let visible_y = cursor_rect.center().y * zoom_factor;
let clamped_y = visible_y.clamp(0.0, logical_canvas_height);
style.set_property("left", &format!("{clamped_x}px"))?;
style.set_property("top", &format!("{clamped_y}px"))?;
Ok(())
}
pub fn set_focus(&self, on: bool) {
if on {
self.focus();
} else {
self.blur();
}
}
pub fn has_focus(&self) -> bool {
super::has_focus(&self.input)
}
pub fn focus(&self) {
if self.has_focus() {
return;
}
log::trace!("Focusing text agent");
if let Err(err) = self.input.focus() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
}
}
pub fn blur(&self) {
if !self.has_focus() {
return;
}
log::trace!("Blurring text agent");
if let Err(err) = self.input.blur() {
log::error!("failed to set focus: {}", super::string_from_js_value(&err));
}
}
}
impl Drop for TextAgent {
fn drop(&mut self) {
self.input.remove();
}
}
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)
}