eframe web: ignore keyboard events unless canvas has focus (#4718)
* Fixes https://github.com/rerun-io/rerun/issues/6638 * Related? https://github.com/emilk/egui/issues/4563 This improves how an eframe canvas works inside of a larger web page, and how it works when there are multiple eframe apps in the same page. `eframe` will set `tabindex="0"` on the canvas automatically, making it focusable. It will also set `outline: none` on the CSS, so the focused canvas won't have an ugly outline. ## Breaking changes You may wanna add this to your `index.html` to give the canvas focus on startup: ```js document.getElementById("the_canvas_id").focus(); ``` ## Test setup ```sh ./scripts/build_demo_web.sh ./scripts/start_server.sh open http://localhost:8888/multiple_apps.html ``` Then open the "Input Event History" and "Text Edit" windows ## Tested * Chromium * [x] drag-and-drop of files * Test both when a `TextEdit` is focused and when it is not: * [x] `Event::Key` * [x] `Event::Text` * [x] copy-cut-paste * [x] Wheel scroll * [x] `Event::PointerGone` * [x] Mouse drag * [x] Mouse click * [x] Mouse right-click * [x] Defocus all eframe canvas, and then start typing text * [x] Firefox (all of the above) * [x] Desktop Safari (all of the above) * [x] Mobile Safari ## Future work (pre-existing issues) * https://github.com/emilk/egui/issues/4723 * https://github.com/emilk/egui/issues/4724 * https://github.com/emilk/egui/issues/4725 * https://github.com/emilk/egui/issues/4726
This commit is contained in:
parent
779312ac0c
commit
3b9f964aed
|
@ -187,6 +187,10 @@ impl AppRunner {
|
|||
///
|
||||
/// The result can be painted later with a call to [`Self::run_and_paint`] or [`Self::paint`].
|
||||
pub fn logic(&mut self) {
|
||||
// We sometimes miss blur/focus events due to the text agent, so let's just poll each frame:
|
||||
self.input
|
||||
.set_focus(super::has_focus(self.canvas()) || self.text_agent.has_focus());
|
||||
|
||||
let canvas_size = super::canvas_size_in_points(self.canvas(), self.egui_ctx());
|
||||
let mut raw_input = self.input.new_frame(canvas_size);
|
||||
|
||||
|
|
|
@ -36,8 +36,12 @@ impl WebInput {
|
|||
raw_input
|
||||
}
|
||||
|
||||
/// On alt-tab and similar.
|
||||
pub fn on_web_page_focus_change(&mut self, focused: bool) {
|
||||
/// On alt-tab, or user clicking another HTML element.
|
||||
pub fn set_focus(&mut self, focused: bool) {
|
||||
if self.raw.focused == focused {
|
||||
return;
|
||||
}
|
||||
|
||||
// log::debug!("on_web_page_focus_change: {focused}");
|
||||
self.raw.modifiers = egui::Modifiers::default(); // Avoid sticky modifier keys on alt-tab:
|
||||
self.raw.focused = focused;
|
||||
|
|
|
@ -2,6 +2,9 @@ use web_sys::EventTarget;
|
|||
|
||||
use super::*;
|
||||
|
||||
// TODO(emilk): there are more calls to `prevent_default` and `stop_propagaton`
|
||||
// than what is probably needed.
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/// Calls `request_animation_frame` to schedule repaint.
|
||||
|
@ -54,10 +57,9 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
|||
let document = window.document().unwrap();
|
||||
let canvas = runner_ref.try_lock().unwrap().canvas().clone();
|
||||
|
||||
install_blur_focus(runner_ref, &document)?;
|
||||
install_blur_focus(runner_ref, &window)?;
|
||||
install_blur_focus(runner_ref, &canvas)?;
|
||||
|
||||
prevent_default(
|
||||
prevent_default_and_stop_propagation(
|
||||
runner_ref,
|
||||
&canvas,
|
||||
&[
|
||||
|
@ -69,8 +71,11 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
|||
],
|
||||
)?;
|
||||
|
||||
install_keydown(runner_ref, &document)?;
|
||||
install_keyup(runner_ref, &document)?;
|
||||
install_keydown(runner_ref, &canvas)?;
|
||||
install_keyup(runner_ref, &canvas)?;
|
||||
|
||||
// It seems copy/cut/paste events only work on the document,
|
||||
// so we check if we have focus inside of the handler.
|
||||
install_copy_cut_paste(runner_ref, &document)?;
|
||||
|
||||
install_mousedown(runner_ref, &canvas)?;
|
||||
|
@ -94,9 +99,12 @@ pub(crate) fn install_event_handlers(runner_ref: &WebRunner) -> Result<(), JsVal
|
|||
}
|
||||
|
||||
fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
// NOTE: because of the text agent we sometime miss 'blur' events,
|
||||
// so we also poll the focus state each frame in `AppRunner::logic`.
|
||||
for event_name in ["blur", "focus"] {
|
||||
let closure = move |_event: web_sys::MouseEvent, runner: &mut AppRunner| {
|
||||
// log::debug!("{event_name:?}");
|
||||
|
||||
let has_focus = event_name == "focus";
|
||||
|
||||
if !has_focus {
|
||||
|
@ -104,7 +112,7 @@ fn install_blur_focus(runner_ref: &WebRunner, target: &EventTarget) -> Result<()
|
|||
runner.save();
|
||||
}
|
||||
|
||||
runner.input.on_web_page_focus_change(has_focus);
|
||||
runner.input.set_focus(has_focus);
|
||||
runner.egui_ctx().request_repaint();
|
||||
};
|
||||
|
||||
|
@ -118,94 +126,140 @@ fn install_keydown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), J
|
|||
target,
|
||||
"keydown",
|
||||
|event: web_sys::KeyboardEvent, runner| {
|
||||
if event.is_composing() || event.key_code() == 229 {
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
if !runner.input.raw.focused {
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
let egui_key = translate_key(&key);
|
||||
|
||||
if let Some(key) = egui_key {
|
||||
runner.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
physical_key: None, // TODO(fornwall)
|
||||
pressed: true,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
if !modifiers.ctrl
|
||||
&& !modifiers.command
|
||||
&& !should_ignore_key(&key)
|
||||
// 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));
|
||||
}
|
||||
runner.needs_repaint.repaint_asap();
|
||||
if let Some(text) = text_from_keyboard_event(&event) {
|
||||
runner.input.raw.events.push(egui::Event::Text(text));
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
let egui_wants_keyboard = runner.egui_ctx().wants_keyboard_input();
|
||||
// If this is indeed text, then prevent any other action.
|
||||
event.prevent_default();
|
||||
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
let prevent_default = if egui_key == Some(egui::Key::Tab) {
|
||||
// Always prevent moving cursor to url bar.
|
||||
// egui wants to use tab to move to the next text field.
|
||||
true
|
||||
} else if egui_key == Some(egui::Key::P) {
|
||||
#[allow(clippy::needless_bool)]
|
||||
if modifiers.ctrl || modifiers.command || modifiers.mac_cmd {
|
||||
true // Prevent ctrl-P opening the print dialog. Users may want to use it for a command palette.
|
||||
} else {
|
||||
false // let normal P:s through
|
||||
// Assume egui uses all key events, and don't let them propagate to parent elements.
|
||||
event.stop_propagation();
|
||||
}
|
||||
} else if egui_wants_keyboard {
|
||||
matches!(
|
||||
event.key().as_str(),
|
||||
"Backspace" // so we don't go back to previous page when deleting text
|
||||
| "ArrowDown" | "ArrowLeft" | "ArrowRight" | "ArrowUp" // cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58)
|
||||
)
|
||||
} else {
|
||||
// We never want to prevent:
|
||||
// * F5 / cmd-R (refresh)
|
||||
// * cmd-shift-C (debug tools)
|
||||
// * cmd/ctrl-c/v/x (or we stop copy/past/cut events)
|
||||
false
|
||||
};
|
||||
|
||||
// log::debug!(
|
||||
// "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}",
|
||||
// event.key().as_str(),
|
||||
// egui_wants_keyboard,
|
||||
// prevent_default
|
||||
// );
|
||||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
}
|
||||
|
||||
on_keydown(event, runner);
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "keyup", |event: web_sys::KeyboardEvent, runner| {
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
runner.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
physical_key: None, // TODO(fornwall)
|
||||
pressed: false,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keydown(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if !has_focus {
|
||||
return;
|
||||
}
|
||||
|
||||
if event.is_composing() || event.key_code() == 229 {
|
||||
// https://web.archive.org/web/20200526195704/https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
|
||||
return;
|
||||
}
|
||||
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
||||
let key = event.key();
|
||||
let egui_key = translate_key(&key);
|
||||
|
||||
if let Some(egui_key) = egui_key {
|
||||
runner.input.raw.events.push(egui::Event::Key {
|
||||
key: egui_key,
|
||||
physical_key: None, // TODO(fornwall)
|
||||
pressed: true,
|
||||
repeat: false, // egui will fill this in for us!
|
||||
modifiers,
|
||||
});
|
||||
runner.needs_repaint.repaint_asap();
|
||||
})
|
||||
|
||||
let prevent_default = should_prevent_default_for_key(runner, &modifiers, egui_key);
|
||||
|
||||
// log::debug!(
|
||||
// "On keydown {:?} {egui_key:?}, has_focus: {has_focus}, egui_wants_keyboard: {}, prevent_default: {prevent_default}",
|
||||
// event.key().as_str(),
|
||||
// runner.egui_ctx().wants_keyboard_input()
|
||||
// );
|
||||
|
||||
if prevent_default {
|
||||
event.prevent_default();
|
||||
}
|
||||
|
||||
// Assume egui uses all key events, and don't let them propagate to parent elements.
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
/// If the canvas (or text agent) has focus:
|
||||
/// should we prevent the default browser event action when the user presses this key?
|
||||
fn should_prevent_default_for_key(
|
||||
runner: &AppRunner,
|
||||
modifiers: &egui::Modifiers,
|
||||
egui_key: egui::Key,
|
||||
) -> bool {
|
||||
// NOTE: We never want to prevent:
|
||||
// * F5 / cmd-R (refresh)
|
||||
// * cmd-shift-C (debug tools)
|
||||
// * cmd/ctrl-c/v/x (lest we prevent copy/paste/cut events)
|
||||
|
||||
// Prevent ctrl-P from opening the print dialog. Users may want to use it for a command palette.
|
||||
if egui_key == egui::Key::P && (modifiers.ctrl || modifiers.command || modifiers.mac_cmd) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if egui_key == egui::Key::Space && !runner.text_agent.has_focus() {
|
||||
// Space scrolls the web page, but we don't want that while canvas has focus
|
||||
// However, don't prevent it if text agent has focus, or we can't type space!
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
egui_key,
|
||||
// Prevent browser from focusing the next HTML element.
|
||||
// egui uses Tab to move focus within the egui app.
|
||||
egui::Key::Tab
|
||||
|
||||
// So we don't go back to previous page while canvas has focus
|
||||
| egui::Key::Backspace
|
||||
|
||||
// Don't scroll web page while canvas has focus.
|
||||
// Also, cmd-left is "back" on Mac (https://github.com/emilk/egui/issues/58)
|
||||
| egui::Key::ArrowDown | egui::Key::ArrowLeft | egui::Key::ArrowRight | egui::Key::ArrowUp
|
||||
)
|
||||
}
|
||||
|
||||
fn install_keyup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
runner_ref.add_event_listener(target, "keyup", on_keyup)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // So that we can pass it directly to `add_event_listener`
|
||||
pub(crate) fn on_keyup(event: web_sys::KeyboardEvent, runner: &mut AppRunner) {
|
||||
let modifiers = modifiers_from_kb_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
if let Some(key) = translate_key(&event.key()) {
|
||||
runner.input.raw.events.push(egui::Event::Key {
|
||||
key,
|
||||
physical_key: None, // TODO(fornwall)
|
||||
pressed: false,
|
||||
repeat: false,
|
||||
modifiers,
|
||||
});
|
||||
}
|
||||
runner.needs_repaint.repaint_asap();
|
||||
|
||||
let has_focus = runner.input.raw.focused;
|
||||
if has_focus {
|
||||
// Assume egui uses all key events, and don't let them propagate to parent elements.
|
||||
event.stop_propagation();
|
||||
}
|
||||
}
|
||||
|
||||
fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
|
@ -214,7 +268,7 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
|
|||
if let Some(data) = event.clipboard_data() {
|
||||
if let Ok(text) = data.get_data("text") {
|
||||
let text = text.replace("\r\n", "\n");
|
||||
if !text.is_empty() {
|
||||
if !text.is_empty() && runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Paste(text));
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
@ -226,14 +280,16 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
|
|||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_ref.add_event_listener(target, "cut", |event: web_sys::ClipboardEvent, runner| {
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Cut);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
|
@ -241,14 +297,16 @@ fn install_copy_cut_paste(runner_ref: &WebRunner, target: &EventTarget) -> Resul
|
|||
|
||||
#[cfg(web_sys_unstable_apis)]
|
||||
runner_ref.add_event_listener(target, "copy", |event: web_sys::ClipboardEvent, runner| {
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
if runner.input.raw.focused {
|
||||
runner.input.raw.events.push(egui::Event::Copy);
|
||||
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
// In Safari we are only allowed to write to the clipboard during the
|
||||
// event callback, which is why we run the app logic here and now:
|
||||
runner.logic();
|
||||
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
// Make sure we paint the output of the above logic call asap:
|
||||
runner.needs_repaint.repaint_asap();
|
||||
}
|
||||
|
||||
event.stop_propagation();
|
||||
event.prevent_default();
|
||||
|
@ -299,7 +357,7 @@ pub(crate) fn install_color_scheme_change_event(runner_ref: &WebRunner) -> Resul
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn prevent_default(
|
||||
fn prevent_default_and_stop_propagation(
|
||||
runner_ref: &WebRunner,
|
||||
target: &EventTarget,
|
||||
event_names: &[&'static str],
|
||||
|
@ -307,7 +365,7 @@ fn prevent_default(
|
|||
for event_name in event_names {
|
||||
let closure = move |event: web_sys::MouseEvent, _runner: &mut AppRunner| {
|
||||
event.prevent_default();
|
||||
// event.stop_propagation();
|
||||
event.stop_propagation();
|
||||
// log::debug!("Preventing event {event_name:?}");
|
||||
};
|
||||
|
||||
|
@ -348,9 +406,6 @@ fn install_mousedown(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
|||
}
|
||||
|
||||
fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
// NOTE: we register "mousemove" on `document` instead of just the canvas
|
||||
// in order to track a dragged mouse outside the canvas.
|
||||
// See https://github.com/emilk/egui/issues/3157
|
||||
runner_ref.add_event_listener(target, "mousemove", |event: web_sys::MouseEvent, runner| {
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
@ -363,11 +418,10 @@ fn install_mousemove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
|||
}
|
||||
|
||||
fn install_mouseup(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
// Use `document` here to notice if the user releases a drag outside of the canvas.
|
||||
// See https://github.com/emilk/egui/issues/3157
|
||||
runner_ref.add_event_listener(target, "mouseup", |event: web_sys::MouseEvent, runner| {
|
||||
let modifiers = modifiers_from_mouse_event(&event);
|
||||
runner.input.raw.modifiers = modifiers;
|
||||
|
||||
if let Some(button) = button_from_mouse_event(&event) {
|
||||
let pos = pos_from_mouse_event(runner.canvas(), &event, runner.egui_ctx());
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
|
@ -458,8 +512,6 @@ fn install_touchmove(runner_ref: &WebRunner, target: &EventTarget) -> Result<(),
|
|||
}
|
||||
|
||||
fn install_touchend(runner_ref: &WebRunner, target: &EventTarget) -> Result<(), JsValue> {
|
||||
// Use `document` here to notice if the user releases a drag outside of the canvas.
|
||||
// See https://github.com/emilk/egui/issues/3157
|
||||
runner_ref.add_event_listener(target, "touchend", |event: web_sys::TouchEvent, runner| {
|
||||
if let Some(pos) = runner.input.latest_touch_pos {
|
||||
let modifiers = runner.input.raw.modifiers;
|
||||
|
|
|
@ -83,40 +83,50 @@ pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web
|
|||
}
|
||||
}
|
||||
|
||||
/// Web sends all keys as strings, so it is up to us to figure out if it is
|
||||
/// a real text input or the name of a key.
|
||||
pub fn should_ignore_key(key: &str) -> bool {
|
||||
/// The text input from a keyboard event (e.g. `X` when pressing the `X` key).
|
||||
pub fn text_from_keyboard_event(event: &web_sys::KeyboardEvent) -> Option<String> {
|
||||
let key = event.key();
|
||||
|
||||
let is_function_key = key.starts_with('F') && key.len() > 1;
|
||||
is_function_key
|
||||
|| matches!(
|
||||
key,
|
||||
"Alt"
|
||||
| "ArrowDown"
|
||||
| "ArrowLeft"
|
||||
| "ArrowRight"
|
||||
| "ArrowUp"
|
||||
| "Backspace"
|
||||
| "CapsLock"
|
||||
| "ContextMenu"
|
||||
| "Control"
|
||||
| "Delete"
|
||||
| "End"
|
||||
| "Enter"
|
||||
| "Esc"
|
||||
| "Escape"
|
||||
| "GroupNext" // https://github.com/emilk/egui/issues/510
|
||||
| "Help"
|
||||
| "Home"
|
||||
| "Insert"
|
||||
| "Meta"
|
||||
| "NumLock"
|
||||
| "PageDown"
|
||||
| "PageUp"
|
||||
| "Pause"
|
||||
| "ScrollLock"
|
||||
| "Shift"
|
||||
| "Tab"
|
||||
)
|
||||
if is_function_key {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_control_key = matches!(
|
||||
key.as_str(),
|
||||
"Alt"
|
||||
| "ArrowDown"
|
||||
| "ArrowLeft"
|
||||
| "ArrowRight"
|
||||
| "ArrowUp"
|
||||
| "Backspace"
|
||||
| "CapsLock"
|
||||
| "ContextMenu"
|
||||
| "Control"
|
||||
| "Delete"
|
||||
| "End"
|
||||
| "Enter"
|
||||
| "Esc"
|
||||
| "Escape"
|
||||
| "GroupNext" // https://github.com/emilk/egui/issues/510
|
||||
| "Help"
|
||||
| "Home"
|
||||
| "Insert"
|
||||
| "Meta"
|
||||
| "NumLock"
|
||||
| "PageDown"
|
||||
| "PageUp"
|
||||
| "Pause"
|
||||
| "ScrollLock"
|
||||
| "Shift"
|
||||
| "Tab"
|
||||
);
|
||||
|
||||
if is_control_key {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// Web sends all keys as strings, so it is up to us to figure out if it is
|
||||
|
|
|
@ -88,6 +88,11 @@ impl TextAgent {
|
|||
runner_ref.add_event_listener(&input, "compositionupdate", on_composition_update)?;
|
||||
runner_ref.add_event_listener(&input, "compositionend", on_composition_end)?;
|
||||
|
||||
// The canvas doesn't get keydown/keyup events when the text agent is focused,
|
||||
// so we need to forward them to the runner:
|
||||
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(),
|
||||
|
|
|
@ -68,6 +68,16 @@ impl WebRunner {
|
|||
let text_agent = TextAgent::attach(self)?;
|
||||
|
||||
let runner = AppRunner::new(canvas_id, web_options, app_creator, text_agent).await?;
|
||||
|
||||
{
|
||||
// Make sure the canvas can be given focus.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex
|
||||
runner.canvas().set_tab_index(0);
|
||||
|
||||
// Don't outline the canvas when it has focus:
|
||||
runner.canvas().style().set_property("outline", "none")?;
|
||||
}
|
||||
|
||||
self.runner.replace(Some(runner));
|
||||
|
||||
{
|
||||
|
|
|
@ -96,8 +96,8 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<!-- The WASM code will resize the canvas dynamically -->
|
||||
<canvas id="the_canvas_id"></canvas>
|
||||
|
||||
<div class="centered" id="center_text">
|
||||
<p style="font-size:16px">
|
||||
Loading…
|
||||
|
@ -175,6 +175,9 @@
|
|||
|
||||
console.debug("App started.");
|
||||
document.getElementById("center_text").innerHTML = '';
|
||||
|
||||
// Make sure the canvas is focused so it can receive keyboard events right away:
|
||||
document.getElementById("the_canvas_id").focus();
|
||||
}
|
||||
|
||||
function on_error(error) {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
margin: 8px;
|
||||
padding: 8px;
|
||||
width: 45%;
|
||||
height: 98%;
|
||||
height: 110%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
|
@ -94,7 +94,6 @@
|
|||
Stop one app
|
||||
</button>
|
||||
|
||||
|
||||
<canvas id="canvas_id_one"></canvas>
|
||||
|
||||
<canvas id="canvas_id_two"></canvas>
|
||||
|
|
Loading…
Reference in New Issue