mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* claim: #5227 * Web: leave Ctrl+Tab and Ctrl+Shift+Tab to the browser (#5227) eframe cancels the default action of every Tab keydown on its canvas (egui moves focus with Tab), so browsers that let a page cancel their shortcuts, such as Vivaldi, could not switch tabs while SolidCraft had the keyboard. A capture listener on the window now stops Ctrl+Tab and Ctrl+Shift+Tab before they reach the canvas, without cancelling them, so the browser acts on them as usual. Plain Tab and Shift+Tab still move between the app's fields. Desktop builds are unchanged. web-api-test checks it in headless Chrome: Ctrl+Tab and Ctrl+Shift+Tab keydowns are not cancelled, Tab still is. --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
This commit is contained in:
@@ -30,5 +30,5 @@ web-sys = { version = "0.3", features = [
|
||||
"FileSystemHandle", "FileSystemDirectoryHandle", "FileSystemFileHandle", "FileSystemGetFileOptions", "FileSystemWritableFileStream",
|
||||
"WritableStream", "File", "FileList", "Blob", "BlobPropertyBag", "Url", "HtmlAnchorElement", "HtmlInputElement", "Storage",
|
||||
"IdbFactory", "IdbDatabase", "IdbObjectStore", "IdbOpenDbRequest", "IdbRequest", "IdbTransaction", "IdbTransactionMode",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
"BeforeUnloadEvent", "Event", "EventTarget", "KeyboardEvent", "Response", "MediaQueryList", "Gamepad", "Performance",
|
||||
] }
|
||||
|
||||
@@ -17,6 +17,14 @@ mod install;
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod space_mouse;
|
||||
|
||||
/// The browser switches tabs with Ctrl+Tab and Ctrl+Shift+Tab, so the page leaves those keys to
|
||||
/// it (#5227); plain Tab and Shift+Tab still move between the app's fields. (Ctrl+PageUp and
|
||||
/// Ctrl+PageDown already reach the browser.)
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
fn browser_switches_tabs(key: &str, ctrl: bool) -> bool {
|
||||
ctrl && key == "Tab"
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web {
|
||||
use std::rc::Rc;
|
||||
@@ -84,6 +92,20 @@ mod web {
|
||||
on_unload.forget();
|
||||
}
|
||||
|
||||
/// eframe cancels the browser's action for every Tab it gets, so the browser couldn't switch
|
||||
/// tabs while the app has the keyboard. Its tab-switching keys stop here, on the way down to
|
||||
/// the canvas, before eframe sees them; the browser then acts on them as usual.
|
||||
fn leave_tab_switching_to_browser(window: &web_sys::Window) {
|
||||
let on_key = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::KeyboardEvent)>::new(|e: web_sys::KeyboardEvent| {
|
||||
if crate::browser_switches_tabs(&e.key(), e.ctrl_key()) {
|
||||
e.stop_propagation();
|
||||
}
|
||||
});
|
||||
let _ = window.add_event_listener_with_callback_and_bool("keydown", on_key.as_ref().unchecked_ref(), true);
|
||||
// The listener lives as long as the page.
|
||||
on_key.forget();
|
||||
}
|
||||
|
||||
const JAPANESE_FONT_PATH: &str = "./fonts/BIZUDPGothic-Regular.ttf";
|
||||
|
||||
type FontLoadResult = Rc<std::cell::RefCell<Option<Result<Vec<u8>, String>>>>;
|
||||
@@ -553,6 +575,7 @@ mod web {
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(window) = web_sys::window() else { return };
|
||||
ask_before_unload(&window);
|
||||
leave_tab_switching_to_browser(&window);
|
||||
let Some(document) = window.document() else { return };
|
||||
let Some(canvas) = document.get_element_by_id(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||
@@ -675,3 +698,17 @@ fn main() {
|
||||
fn main() {
|
||||
eprintln!("solidcraft-web only runs in the browser: build it with `trunk build --release` in apps/solidcraft-web");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_ctrl_tab_is_left_to_the_browser() {
|
||||
// Ctrl+Tab and Ctrl+Shift+Tab (the event's key is "Tab" either way) switch browser tabs.
|
||||
assert!(browser_switches_tabs("Tab", true));
|
||||
// Tab and Shift+Tab move between the app's fields; Ctrl with other keys stays the app's.
|
||||
assert!(!browser_switches_tabs("Tab", false));
|
||||
assert!(!browser_switches_tabs("s", true));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,4 +47,5 @@ cargo xtask web-api-test --site dist/web
|
||||
|
||||
The test starts a local server and headless Chrome, discovers commands, creates a box in the
|
||||
live document, checks undo and redo, and verifies error handling and the pending-request limit.
|
||||
It also checks that Ctrl+Tab and Ctrl+Shift+Tab are left to the browser, which switches tabs with them.
|
||||
Set `CHROME` to a Chrome or Chromium executable if it is not installed in a usual location.
|
||||
|
||||
@@ -87,13 +87,35 @@ pub fn run_api(site: &str) -> Result<(), String> {
|
||||
let browser = Chrome::launch(&chrome, &work.0.join("profile"))?;
|
||||
let mut cdp = browser.connect()?;
|
||||
cdp.call("Emulation.setDeviceMetricsOverride", json!({"width": 1280, "height": 900, "deviceScaleFactor": 1, "mobile": false}))?;
|
||||
// The page has the keyboard, as a tab the user is looking at does (for KEYS_TEST).
|
||||
cdp.call("Emulation.setFocusEmulationEnabled", json!({"enabled": true}))?;
|
||||
cdp.call("Page.navigate", json!({"url": format!("http://127.0.0.1:{}{PREFIX}?webgl", server.port)}))?;
|
||||
wait_for(&mut cdp, "!!window.solidcraft && !document.getElementById('solidcraft_loading')", "the browser API is ready", |v| v == true)?;
|
||||
let result = cdp.eval(API_TEST)?;
|
||||
println!("web-api-test: {result}");
|
||||
let keys = cdp.eval(KEYS_TEST)?;
|
||||
println!("web-api-test: {keys}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The browser's own tab switching keys reach it while the app has the keyboard (#5227): their
|
||||
/// default action isn't cancelled, while Tab's is (the app moves between its fields with it).
|
||||
const KEYS_TEST: &str = r#"(async () => {
|
||||
const check = (condition, message) => { if (!condition) throw new Error(message); };
|
||||
const canvas = document.getElementById('solidcraft_canvas');
|
||||
canvas.focus();
|
||||
canvas.dispatchEvent(new FocusEvent('focus'));
|
||||
const cancelled = init => {
|
||||
const e = new KeyboardEvent('keydown', {key: 'Tab', code: 'Tab', bubbles: true, cancelable: true, ...init});
|
||||
canvas.dispatchEvent(e);
|
||||
return e.defaultPrevented;
|
||||
};
|
||||
check(!cancelled({ctrlKey: true}), 'Ctrl+Tab is left to the browser');
|
||||
check(!cancelled({ctrlKey: true, shiftKey: true}), 'Ctrl+Shift+Tab is left to the browser');
|
||||
check(cancelled({}), 'the app takes Tab (it has the keyboard)');
|
||||
return {ctrl_tab: 'browser', ctrl_shift_tab: 'browser', tab: 'app'};
|
||||
})()"#;
|
||||
|
||||
const API_TEST: &str = r#"(async () => {
|
||||
const app = window.solidcraft;
|
||||
const check = (condition, message) => { if (!condition) throw new Error(message); };
|
||||
|
||||
Reference in New Issue
Block a user