1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-29 12:50:04 -04:00

Remove egui_web and epi (#1545)

* Remove integration name (it is always eframe)

* Remove egui_web crate

* Move egui_web/CHANGELOG.md into eframe/CHANGELOG.md

* Remove all mentions of egui_web

* Remove epi crate and absorb into eframe

* egui_glow: only use puffin on native

* Remove WASM doc from CI (we don't generate it anyways!)

* Remove eframe::epi and improve eframe docs
This commit is contained in:
Emil Ernerfeldt
2022-04-30 10:44:35 +02:00
committed by GitHub
parent 18d529203a
commit bb421c7e8a
41 changed files with 244 additions and 447 deletions

View File

@@ -1,13 +1,7 @@
# Changelog for egui_web
All notable changes to the `egui_web` integration will be noted in this file.
# Legacy changelog for egui_web
Between versions 0.17 and 0.18, `egui_web` was absorbed into `eframe`. Most of this changelog was then merged into [the `eframe` changelog](../eframe/CHANGELOG.md). This changelog is now only kept for historical reasons.
## Unreleased
* egui code will no longer be called after panic ([#1306](https://github.com/emilk/egui/pull/1306)).
* Remove the "webgl" feature. `egui_web` now always use `glow` (which in turn wraps WebGL) ([#1356](https://github.com/emilk/egui/pull/1356)).
* Use full browser width by default ([#1378](https://github.com/emilk/egui/pull/1378)).
* MSRV (Minimum Supported Rust Version) is now `1.60.0` ([#1467](https://github.com/emilk/egui/pull/1467)).
## 0.17.0 - 2022-02-22
* The default painter is now glow instead of WebGL ([#1020](https://github.com/emilk/egui/pull/1020)).

View File

@@ -1,104 +0,0 @@
[package]
name = "egui_web"
version = "0.17.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Bindings for compiling egui code to WASM for a web page"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.60"
homepage = "https://github.com/emilk/egui/tree/master/egui_web"
readme = "README.md"
repository = "https://github.com/emilk/egui/tree/master/egui_web"
categories = ["gui", "web-programming"]
keywords = ["wasm", "web", "egui", "gui", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
"src/shader/*.glsl",
]
[package.metadata.docs.rs]
all-features = true
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["default_fonts"]
# If set, egui will use `include_bytes!` to bundle some fonts.
# If you plan on specifying your own fonts you may disable this feature.
default_fonts = ["egui/default_fonts"]
# enable persisting egui memory
persistence = ["egui/persistence", "ron", "serde"]
# enable screen reader support (requires `ctx.options().screen_reader = true;`)
screen_reader = ["tts"]
[dependencies]
egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
"bytemuck",
"tracing",
] }
egui_glow = { version = "0.17.0", path = "../egui_glow", default-features = false }
epi = { version = "0.17.0", path = "../epi" }
bytemuck = "1.7"
js-sys = "0.3"
percent-encoding = "2.1"
tracing = "0.1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
# Optional:
ron = { version = "0.7", optional = true }
serde = { version = "1", optional = true }
tts = { version = "0.20", optional = true } # feature screen_reader
web-sys = { version = "0.3.52", features = [
"BinaryType",
"Blob",
"Clipboard",
"ClipboardEvent",
"CompositionEvent",
"console",
"CssStyleDeclaration",
"DataTransfer",
"DataTransferItem",
"DataTransferItemList",
"Document",
"DomRect",
"DragEvent",
"Element",
"Event",
"EventListener",
"EventTarget",
"ExtSRgb",
"File",
"FileList",
"FocusEvent",
"HtmlCanvasElement",
"HtmlElement",
"HtmlInputElement",
"InputEvent",
"KeyboardEvent",
"Location",
"MediaQueryList",
"MouseEvent",
"Navigator",
"Performance",
"Storage",
"Touch",
"TouchEvent",
"TouchList",
"WebGl2RenderingContext",
"WebglDebugRendererInfo",
"WebGlRenderingContext",
"WheelEvent",
"Window",
] }

View File

@@ -1,29 +1 @@
# egui_web
[![Latest version](https://img.shields.io/crates/v/egui_web.svg)](https://crates.io/crates/egui_web)
[![Documentation](https://docs.rs/egui_web/badge.svg)](https://docs.rs/egui_web)
[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
This crates allows you to compile GUI code written with [egui](https://crates.io/crates/egui) to [WASM](https://en.wikipedia.org/wiki/WebAssembly) to run on a web page.
[Run the web demo](https://www.egui.rs/#demo) to try it now.
Check out [eframe_template](https://github.com/emilk/eframe_template) for an example of how to set it up.
## Downsides with using egui on the web
`egui_web` uses WebGL (via [`glow`](https://crates.io/crates/glow)) and WASM, and almost nothing else from the web tech stack. This has some benefits, but also produces some challenges and serious downsides.
* Rendering: Getting pixel-perfect rendering right on the web is very difficult.
* Search: you cannot search an egui web page like you would a normal web page.
* Bringing up an on-screen keyboard on mobile: there is no JS function to do this, so `egui_web` fakes it by adding some invisible DOM elements. It doesn't always work.
* Mobile text editing is not as good as for a normal web app.
* Accessibility: There is an experimental screen reader for `egui_web`, but it has to be enabled explicitly. There is no JS function to ask "Does the user want a screen reader?" (and there should probably not be such a function, due to user tracking/integrity concerns).
* No integration with browser settings for colors and fonts.
* On Linux and Mac, Firefox will copy the WebGL render target from GPU, to CPU and then back again (https://bugzilla.mozilla.org/show_bug.cgi?id=1010527#c0), slowing down egui.
In many ways, `egui_web` is trying to make the browser do something it wasn't designed to do (though there are many things browser vendors could do to improve how well libraries like egui work).
The suggested use for `egui_web` are for web apps where performance and responsiveness are more important than accessibility and mobile text editing.
`egui_web` used to be a standalone crate, but has now been moved into [`eframe`](https://github.com/emilk/egui/tree/master/eframe).

View File

@@ -1,443 +0,0 @@
use crate::{glow_wrapping::WrappedGlowPainter, *};
use egui::TexturesDelta;
pub use egui::{pos2, Color32};
// ----------------------------------------------------------------------------
/// Data gathered between frames.
#[derive(Default)]
pub struct WebInput {
/// Required because we don't get a position on touched
pub latest_touch_pos: Option<egui::Pos2>,
/// Required to maintain a stable touch position for multi-touch gestures.
pub latest_touch_pos_id: Option<egui::TouchId>,
pub raw: egui::RawInput,
}
impl WebInput {
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
pixels_per_point: Some(native_pixels_per_point()), // We ALWAYS use the native pixels-per-point
time: Some(now_sec()),
..self.raw.take()
}
}
}
// ----------------------------------------------------------------------------
use std::sync::atomic::Ordering::SeqCst;
pub struct NeedRepaint(std::sync::atomic::AtomicBool);
impl Default for NeedRepaint {
fn default() -> Self {
Self(true.into())
}
}
impl NeedRepaint {
pub fn fetch_and_clear(&self) -> bool {
self.0.swap(false, SeqCst)
}
pub fn set_true(&self) {
self.0.store(true, SeqCst);
}
}
// ----------------------------------------------------------------------------
fn web_location() -> epi::Location {
let location = web_sys::window().unwrap().location();
let hash = percent_decode(&location.hash().unwrap_or_default());
let query = location
.search()
.unwrap_or_default()
.strip_prefix('?')
.map(percent_decode)
.unwrap_or_default();
let query_map = parse_query_map(&query)
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
epi::Location {
url: percent_decode(&location.href().unwrap_or_default()),
protocol: percent_decode(&location.protocol().unwrap_or_default()),
host: percent_decode(&location.host().unwrap_or_default()),
hostname: percent_decode(&location.hostname().unwrap_or_default()),
port: percent_decode(&location.port().unwrap_or_default()),
hash,
query,
query_map,
origin: percent_decode(&location.origin().unwrap_or_default()),
}
}
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
query
.split('&')
.filter_map(|pair| {
if pair.is_empty() {
None
} else {
Some(if let Some((key, value)) = pair.split_once('=') {
(key, value)
} else {
(pair, "")
})
}
})
.collect()
}
#[test]
fn test_parse_query() {
assert_eq!(parse_query_map(""), BTreeMap::default());
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
assert_eq!(
parse_query_map("foo=bar"),
BTreeMap::from_iter([("foo", "bar")])
);
assert_eq!(
parse_query_map("foo=bar&baz=42"),
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz=42"),
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
);
assert_eq!(
parse_query_map("foo&baz&&"),
BTreeMap::from_iter([("foo", ""), ("baz", "")])
);
}
// ----------------------------------------------------------------------------
pub struct AppRunner {
pub(crate) frame: epi::Frame,
egui_ctx: egui::Context,
painter: WrappedGlowPainter,
pub(crate) input: WebInput,
app: Box<dyn epi::App>,
pub(crate) needs_repaint: std::sync::Arc<NeedRepaint>,
last_save_time: f64,
screen_reader: crate::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
textures_delta: TexturesDelta,
}
impl AppRunner {
pub fn new(canvas_id: &str, app_creator: epi::AppCreator) -> Result<Self, JsValue> {
let painter = WrappedGlowPainter::new(canvas_id).map_err(JsValue::from)?;
let prefer_dark_mode = crate::prefer_dark_mode();
let frame = epi::Frame {
info: epi::IntegrationInfo {
name: "egui_web",
web_info: Some(epi::WebInfo {
location: web_location(),
}),
prefer_dark_mode,
cpu_usage: None,
native_pixels_per_point: Some(native_pixels_per_point()),
},
output: Default::default(),
storage: Some(Box::new(LocalStorage::default())),
gl: painter.gl().clone(),
};
let needs_repaint: std::sync::Arc<NeedRepaint> = Default::default();
let egui_ctx = egui::Context::default();
{
let needs_repaint = needs_repaint.clone();
egui_ctx.set_request_repaint_callback(move || {
needs_repaint.0.store(true, SeqCst);
});
}
load_memory(&egui_ctx);
if prefer_dark_mode == Some(true) {
egui_ctx.set_visuals(egui::Visuals::dark());
} else {
egui_ctx.set_visuals(egui::Visuals::light());
}
let app = app_creator(&epi::CreationContext {
egui_ctx: egui_ctx.clone(),
integration_info: frame.info(),
storage: frame.storage(),
gl: painter.painter.gl().clone(),
});
let mut runner = Self {
frame,
egui_ctx,
painter,
input: Default::default(),
app,
needs_repaint,
last_save_time: now_sec(),
screen_reader: Default::default(),
text_cursor_pos: None,
mutable_text_under_cursor: false,
textures_delta: Default::default(),
};
runner.input.raw.max_texture_side = Some(runner.painter.max_texture_side());
Ok(runner)
}
pub fn egui_ctx(&self) -> &egui::Context {
&self.egui_ctx
}
pub fn auto_save(&mut self) {
let now = now_sec();
let time_since_last_save = now - self.last_save_time;
if time_since_last_save > self.app.auto_save_interval().as_secs_f64() {
if self.app.persist_egui_memory() {
save_memory(&self.egui_ctx);
}
if let Some(storage) = self.frame.storage_mut() {
self.app.save(storage);
}
self.last_save_time = now;
}
}
pub fn canvas_id(&self) -> &str {
self.painter.canvas_id()
}
pub fn warm_up(&mut self) -> Result<(), JsValue> {
if self.app.warm_up_enabled() {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true);
self.logic()?;
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations();
}
Ok(())
}
/// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> Result<(bool, Vec<egui::ClippedPrimitive>), JsValue> {
let frame_start = now_sec();
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
let canvas_size = canvas_size_in_points(self.canvas_id());
let raw_input = self.input.new_frame(canvas_size);
let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
self.app.update(egui_ctx, &mut self.frame);
});
let egui::FullOutput {
platform_output,
needs_repaint,
textures_delta,
shapes,
} = full_output;
self.handle_platform_output(platform_output);
self.textures_delta.append(textures_delta);
let clipped_primitives = self.egui_ctx.tessellate(shapes);
{
let app_output = self.frame.take_app_output();
let epi::backend::AppOutput {
quit: _, // Can't quit a web page
window_size: _, // Can't resize a web page
window_title: _, // TODO: change title of window
decorated: _, // Can't toggle decorations
drag_window: _, // Can't be dragged
window_pos: _, // Can't set position of a web page
} = app_output;
}
self.frame.info.cpu_usage = Some((now_sec() - frame_start) as f32);
Ok((needs_repaint, clipped_primitives))
}
pub fn clear_color_buffer(&self) {
self.painter.clear(self.app.clear_color());
}
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_primitives: &[egui::ClippedPrimitive]) -> Result<(), JsValue> {
let textures_delta = std::mem::take(&mut self.textures_delta);
self.painter.paint_and_update_textures(
clipped_primitives,
self.egui_ctx.pixels_per_point(),
&textures_delta,
)?;
Ok(())
}
fn handle_platform_output(&mut self, platform_output: egui::PlatformOutput) {
if self.egui_ctx.options().screen_reader {
self.screen_reader
.speak(&platform_output.events_description());
}
let egui::PlatformOutput {
cursor_icon,
open_url,
copied_text,
events: _, // already handled
mutable_text_under_cursor,
text_cursor_pos,
} = platform_output;
set_cursor_icon(cursor_icon);
if let Some(open) = open_url {
crate::open_url(&open.url, open.new_tab);
}
#[cfg(web_sys_unstable_apis)]
if !copied_text.is_empty() {
set_clipboard_text(&copied_text);
}
#[cfg(not(web_sys_unstable_apis))]
let _ = copied_text;
self.mutable_text_under_cursor = mutable_text_under_cursor;
if self.text_cursor_pos != text_cursor_pos {
text_agent::move_text_cursor(text_cursor_pos, self.canvas_id());
self.text_cursor_pos = text_cursor_pos;
}
}
}
// ----------------------------------------------------------------------------
pub type AppRunnerRef = Arc<Mutex<AppRunner>>;
pub struct AppRunnerContainer {
pub runner: AppRunnerRef,
/// Set to `true` if there is a panic.
/// Used to ignore callbacks after a panic.
pub panicked: Arc<AtomicBool>,
}
impl AppRunnerContainer {
/// Convenience function to reduce boilerplate and ensure that all event handlers
/// are dealt with in the same way
pub fn add_event_listener<E: wasm_bindgen::JsCast>(
&self,
target: &EventTarget,
event_name: &'static str,
mut closure: impl FnMut(E, MutexGuard<'_, AppRunner>) + 'static,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
// Create a JS closure based on the FnMut provided
let closure = Closure::wrap({
// Clone atomics
let runner_ref = self.runner.clone();
let panicked = self.panicked.clone();
Box::new(move |event: web_sys::Event| {
// Only call the wrapped closure if the egui code has not panicked
if !panicked.load(Ordering::SeqCst) {
// Cast the event to the expected event type
let event = event.unchecked_into::<E>();
closure(event, runner_ref.lock());
}
}) as Box<dyn FnMut(_)>
});
// Add the event listener to the target
target.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
// Bypass closure drop so that event handler can call the closure
closure.forget();
Ok(())
}
}
// ----------------------------------------------------------------------------
/// Install event listeners to register different input events
/// and start running the given app.
pub fn start(canvas_id: &str, app_creator: epi::AppCreator) -> Result<AppRunnerRef, JsValue> {
let mut runner = AppRunner::new(canvas_id, app_creator)?;
runner.warm_up()?;
start_runner(runner)
}
/// Install event listeners to register different input events
/// and starts running the given [`AppRunner`].
fn start_runner(app_runner: AppRunner) -> Result<AppRunnerRef, JsValue> {
let runner_container = AppRunnerContainer {
runner: Arc::new(Mutex::new(app_runner)),
panicked: Arc::new(AtomicBool::new(false)),
};
crate::events::install_canvas_events(&runner_container)?;
crate::events::install_document_events(&runner_container)?;
text_agent::install_text_agent(&runner_container)?;
crate::events::repaint_every_ms(&runner_container, 1000)?; // just in case. TODO: make it a parameter
crate::events::paint_and_schedule(&runner_container.runner, runner_container.panicked.clone())?;
// Disable all event handlers on panic
std::panic::set_hook(Box::new({
let previous_hook = std::panic::take_hook();
let panicked = runner_container.panicked;
move |panic_info| {
tracing::info_span!("egui_panic_handler").in_scope(|| {
tracing::trace!("setting panicked flag");
panicked.store(true, SeqCst);
tracing::info!("egui disabled all event handlers due to panic");
});
// Propagate panic info to the previously registered panic hook
previous_hook(panic_info);
}
}));
Ok(runner_container.runner)
}
// ----------------------------------------------------------------------------
#[derive(Default)]
struct LocalStorage {}
impl epi::Storage for LocalStorage {
fn get_string(&self, key: &str) -> Option<String> {
local_storage_get(key)
}
fn set_string(&mut self, key: &str, value: String) {
local_storage_set(key, &value);
}
fn flush(&mut self) {}
}

View File

@@ -1,544 +0,0 @@
use crate::*;
use std::sync::atomic::{AtomicBool, Ordering};
pub fn paint_and_schedule(
runner_ref: &AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let mut runner_lock = runner_ref.lock();
if runner_lock.needs_repaint.fetch_and_clear() {
runner_lock.clear_color_buffer();
let (needs_repaint, clipped_primitives) = runner_lock.logic()?;
runner_lock.paint(&clipped_primitives)?;
if needs_repaint {
runner_lock.needs_repaint.set_true();
}
runner_lock.auto_save();
}
Ok(())
}
fn request_animation_frame(
runner_ref: AppRunnerRef,
panicked: Arc<AtomicBool>,
) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let closure = Closure::once(move || paint_and_schedule(&runner_ref, panicked));
window.request_animation_frame(closure.as_ref().unchecked_ref())?;
closure.forget(); // We must forget it, or else the callback is canceled on drop
Ok(())
}
// Only paint and schedule if there has been no panic
if !panicked.load(Ordering::SeqCst) {
paint_if_needed(runner_ref)?;
request_animation_frame(runner_ref.clone(), panicked)?;
}
Ok(())
}
pub fn install_document_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
runner_container.add_event_listener(
&document,
"keydown",
|event: web_sys::KeyboardEvent, mut runner_lock| {
if event.is_composing() || event.key_code() == 229 {
// https://www.fxsitecompat.dev/en-CA/docs/2018/keydown-and-keyup-events-are-now-fired-during-ime-composition/
return;
}
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
let key = event.key();
if let Some(key) = translate_key(&key) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: true,
modifiers,
});
}
if !modifiers.ctrl
&& !modifiers.command
&& !should_ignore_key(&key)
// When text agent is shown, it sends text event instead.
&& text_agent::text_agent().hidden()
{
runner_lock.input.raw.events.push(egui::Event::Text(key));
}
runner_lock.needs_repaint.set_true();
let egui_wants_keyboard = runner_lock.egui_ctx().wants_keyboard_input();
let prevent_default = if matches!(event.key().as_str(), "Tab") {
// Always prevent moving cursor to url bar.
// egui wants to use tab to move to the next text field.
true
} 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
};
// tracing::debug!(
// "On key-down {:?}, egui_wants_keyboard: {}, prevent_default: {}",
// event.key().as_str(),
// egui_wants_keyboard,
// prevent_default
// );
if prevent_default {
event.prevent_default();
}
},
)?;
runner_container.add_event_listener(
&document,
"keyup",
|event: web_sys::KeyboardEvent, mut runner_lock| {
let modifiers = modifiers_from_event(&event);
runner_lock.input.raw.modifiers = modifiers;
if let Some(key) = translate_key(&event.key()) {
runner_lock.input.raw.events.push(egui::Event::Key {
key,
pressed: false,
modifiers,
});
}
runner_lock.needs_repaint.set_true();
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"paste",
|event: web_sys::ClipboardEvent, mut runner_lock| {
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() {
runner_lock.input.raw.events.push(egui::Event::Paste(text));
runner_lock.needs_repaint.set_true();
}
event.stop_propagation();
event.prevent_default();
}
}
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"cut",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Cut);
runner_lock.needs_repaint.set_true();
},
)?;
#[cfg(web_sys_unstable_apis)]
runner_container.add_event_listener(
&document,
"copy",
|_: web_sys::ClipboardEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::Copy);
runner_lock.needs_repaint.set_true();
},
)?;
for event_name in &["load", "pagehide", "pageshow", "resize"] {
runner_container.add_event_listener(
&window,
event_name,
|_: web_sys::Event, runner_lock| {
runner_lock.needs_repaint.set_true();
},
)?;
}
runner_container.add_event_listener(
&window,
"hashchange",
|_: web_sys::Event, mut runner_lock| {
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
if let Some(web_info) = &mut runner_lock.frame.info.web_info {
web_info.location.hash = location_hash();
}
},
)?;
Ok(())
}
/// Repaint at least every `ms` milliseconds.
pub fn repaint_every_ms(
runner_container: &AppRunnerContainer,
milliseconds: i32,
) -> Result<(), JsValue> {
assert!(milliseconds >= 0);
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let closure = Closure::wrap(Box::new({
let runner = runner_container.runner.clone();
let panicked = runner_container.panicked.clone();
move || {
// Do not lock the runner if the code has panicked
if !panicked.load(Ordering::SeqCst) {
runner.lock().needs_repaint.set_true();
}
}
}) as Box<dyn FnMut()>);
window.set_interval_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
milliseconds,
)?;
closure.forget();
Ok(())
}
pub fn install_canvas_events(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let canvas = canvas_element(runner_container.runner.lock().canvas_id()).unwrap();
{
// By default, right-clicks open a context menu.
// We don't want to do that (right clicks is handled by egui):
let event_name = "contextmenu";
let closure = Closure::wrap(Box::new(move |event: web_sys::MouseEvent| {
event.prevent_default();
}) as Box<dyn FnMut(_)>);
canvas.add_event_listener_with_callback(event_name, closure.as_ref().unchecked_ref())?;
closure.forget();
}
runner_container.add_event_listener(
&canvas,
"mousedown",
|event: web_sys::MouseEvent, mut runner_lock| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: true,
modifiers,
});
runner_lock.needs_repaint.set_true();
}
event.stop_propagation();
// Note: prevent_default breaks VSCode tab focusing, hence why we don't call it here.
},
)?;
runner_container.add_event_listener(
&canvas,
"mousemove",
|event: web_sys::MouseEvent, mut runner_lock| {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"mouseup",
|event: web_sys::MouseEvent, mut runner_lock| {
if let Some(button) = button_from_mouse_event(&event) {
let pos = pos_from_mouse_event(runner_lock.canvas_id(), &event);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button,
pressed: false,
modifiers,
});
runner_lock.needs_repaint.set_true();
text_agent::update_text_agent(runner_lock);
}
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"mouseleave",
|event: web_sys::MouseEvent, mut runner_lock| {
runner_lock.input.raw.events.push(egui::Event::PointerGone);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchstart",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
let modifiers = runner_lock.input.raw.modifiers;
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: true,
modifiers,
});
push_touches(&mut *runner_lock, egui::TouchPhase::Start, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchmove",
|event: web_sys::TouchEvent, mut runner_lock| {
let mut latest_touch_pos_id = runner_lock.input.latest_touch_pos_id;
let pos =
pos_from_touch_event(runner_lock.canvas_id(), &event, &mut latest_touch_pos_id);
runner_lock.input.latest_touch_pos_id = latest_touch_pos_id;
runner_lock.input.latest_touch_pos = Some(pos);
runner_lock
.input
.raw
.events
.push(egui::Event::PointerMoved(pos));
push_touches(&mut *runner_lock, egui::TouchPhase::Move, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"touchend",
|event: web_sys::TouchEvent, mut runner_lock| {
if let Some(pos) = runner_lock.input.latest_touch_pos {
let modifiers = runner_lock.input.raw.modifiers;
// First release mouse to click:
runner_lock
.input
.raw
.events
.push(egui::Event::PointerButton {
pos,
button: egui::PointerButton::Primary,
pressed: false,
modifiers,
});
// Then remove hover effect:
runner_lock.input.raw.events.push(egui::Event::PointerGone);
push_touches(&mut *runner_lock, egui::TouchPhase::End, &event);
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
// Finally, focus or blur text agent to toggle mobile keyboard:
text_agent::update_text_agent(runner_lock);
},
)?;
runner_container.add_event_listener(
&canvas,
"touchcancel",
|event: web_sys::TouchEvent, mut runner_lock| {
push_touches(&mut runner_lock, egui::TouchPhase::Cancel, &event);
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"wheel",
|event: web_sys::WheelEvent, mut runner_lock| {
let scroll_multiplier = match event.delta_mode() {
web_sys::WheelEvent::DOM_DELTA_PAGE => {
canvas_size_in_points(runner_lock.canvas_id()).y
}
web_sys::WheelEvent::DOM_DELTA_LINE => {
#[allow(clippy::let_and_return)]
let points_per_scroll_line = 8.0; // Note that this is intentionally different from what we use in egui_glium / winit.
points_per_scroll_line
}
_ => 1.0, // DOM_DELTA_PIXEL
};
let mut delta =
-scroll_multiplier * egui::vec2(event.delta_x() as f32, event.delta_y() as f32);
// Report a zoom event in case CTRL (on Windows or Linux) or CMD (on Mac) is pressed.
// This if-statement is equivalent to how `Modifiers.command` is determined in
// `modifiers_from_event()`, but we cannot directly use that fn for a [`WheelEvent`].
if event.ctrl_key() || event.meta_key() {
let factor = (delta.y / 200.0).exp();
runner_lock.input.raw.events.push(egui::Event::Zoom(factor));
} else {
if event.shift_key() {
// Treat as horizontal scrolling.
// Note: one Mac we already get horizontal scroll events when shift is down.
delta = egui::vec2(delta.x + delta.y, 0.0);
}
runner_lock
.input
.raw
.events
.push(egui::Event::Scroll(delta));
}
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(
&canvas,
"dragover",
|event: web_sys::DragEvent, mut runner_lock| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
for i in 0..data_transfer.items().length() {
if let Some(item) = data_transfer.items().get(i) {
runner_lock.input.raw.hovered_files.push(egui::HoveredFile {
mime: item.type_(),
..Default::default()
});
}
}
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
}
},
)?;
runner_container.add_event_listener(
&canvas,
"dragleave",
|event: web_sys::DragEvent, mut runner_lock| {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.set_true();
event.stop_propagation();
event.prevent_default();
},
)?;
runner_container.add_event_listener(&canvas, "drop", {
let runner_ref = runner_container.runner.clone();
move |event: web_sys::DragEvent, mut runner_lock| {
if let Some(data_transfer) = event.data_transfer() {
runner_lock.input.raw.hovered_files.clear();
runner_lock.needs_repaint.set_true();
// Unlock the runner so it can be locked after a future await point
drop(runner_lock);
if let Some(files) = data_transfer.files() {
for i in 0..files.length() {
if let Some(file) = files.get(i) {
let name = file.name();
let last_modified = std::time::UNIX_EPOCH
+ std::time::Duration::from_millis(file.last_modified() as u64);
tracing::debug!("Loading {:?} ({} bytes)…", name, file.size());
let future = wasm_bindgen_futures::JsFuture::from(file.array_buffer());
let runner_ref = runner_ref.clone();
let future = async move {
match future.await {
Ok(array_buffer) => {
let bytes = js_sys::Uint8Array::new(&array_buffer).to_vec();
tracing::debug!(
"Loaded {:?} ({} bytes).",
name,
bytes.len()
);
// Re-lock the mutex on the other side of the await point
let mut runner_lock = runner_ref.lock();
runner_lock.input.raw.dropped_files.push(
egui::DroppedFile {
name,
last_modified: Some(last_modified),
bytes: Some(bytes.into()),
..Default::default()
},
);
runner_lock.needs_repaint.set_true();
}
Err(err) => {
tracing::error!("Failed to read file: {:?}", err);
}
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
event.stop_propagation();
event.prevent_default();
}
}
})?;
Ok(())
}

View File

@@ -1,169 +0,0 @@
use egui::{ClippedPrimitive, Rgba};
use egui_glow::glow;
use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue;
use web_sys::HtmlCanvasElement;
#[cfg(not(target_arch = "wasm32"))]
use web_sys::{WebGl2RenderingContext, WebGlRenderingContext};
pub(crate) struct WrappedGlowPainter {
pub(crate) canvas: HtmlCanvasElement,
pub(crate) canvas_id: String,
pub(crate) painter: egui_glow::Painter,
}
impl WrappedGlowPainter {
pub fn new(canvas_id: &str) -> Result<Self, String> {
let canvas = crate::canvas_element_or_die(canvas_id);
let (gl, shader_prefix) = init_glow_context_from_canvas(&canvas)?;
let gl = std::rc::Rc::new(gl);
let dimension = [canvas.width() as i32, canvas.height() as i32];
let painter = egui_glow::Painter::new(gl, Some(dimension), shader_prefix)
.map_err(|error| format!("Error starting glow painter: {}", error))?;
Ok(Self {
canvas,
canvas_id: canvas_id.to_owned(),
painter,
})
}
}
impl WrappedGlowPainter {
pub fn gl(&self) -> &std::rc::Rc<glow::Context> {
self.painter.gl()
}
pub fn max_texture_side(&self) -> usize {
self.painter.max_texture_side()
}
pub fn canvas_id(&self) -> &str {
&self.canvas_id
}
pub fn set_texture(&mut self, tex_id: egui::TextureId, delta: &egui::epaint::ImageDelta) {
self.painter.set_texture(tex_id, delta);
}
pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.painter.free_texture(tex_id);
}
pub fn clear(&self, clear_color: Rgba) {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
egui_glow::painter::clear(self.painter.gl(), canvas_dimension, clear_color);
}
pub fn paint_primitives(
&mut self,
clipped_primitives: &[ClippedPrimitive],
pixels_per_point: f32,
) -> Result<(), JsValue> {
let canvas_dimension = [self.canvas.width(), self.canvas.height()];
self.painter
.paint_primitives(canvas_dimension, pixels_per_point, clipped_primitives);
Ok(())
}
pub fn paint_and_update_textures(
&mut self,
clipped_primitives: &[egui::ClippedPrimitive],
pixels_per_point: f32,
textures_delta: &egui::TexturesDelta,
) -> Result<(), JsValue> {
for (id, image_delta) in &textures_delta.set {
self.set_texture(*id, image_delta);
}
self.paint_primitives(clipped_primitives, pixels_per_point)?;
for &id in &textures_delta.free {
self.free_texture(id);
}
Ok(())
}
}
/// Returns glow context and shader prefix.
fn init_glow_context_from_canvas(
canvas: &HtmlCanvasElement,
) -> Result<(glow::Context, &'static str), String> {
const BEST_FIRST: bool = true;
let result = if BEST_FIRST {
// Trying WebGl2 first
init_webgl2(canvas).or_else(|| init_webgl1(canvas))
} else {
// Trying WebGl1 first (useful for testing).
tracing::warn!("Looking for WebGL1 first");
init_webgl1(canvas).or_else(|| init_webgl2(canvas))
};
if let Some(result) = result {
Ok(result)
} else {
Err("WebGL isn't supported".into())
}
}
fn init_webgl1(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
let gl1_ctx = canvas
.get_context("webgl")
.expect("Failed to query about WebGL2 context");
let gl1_ctx = gl1_ctx?;
tracing::debug!("WebGL1 selected.");
let gl1_ctx = gl1_ctx
.dyn_into::<web_sys::WebGlRenderingContext>()
.unwrap();
let shader_prefix = if crate::webgl1_requires_brightening(&gl1_ctx) {
tracing::debug!("Enabling webkitGTK brightening workaround.");
"#define APPLY_BRIGHTENING_GAMMA"
} else {
""
};
let gl = glow::Context::from_webgl1_context(gl1_ctx);
Some((gl, shader_prefix))
}
fn init_webgl2(canvas: &HtmlCanvasElement) -> Option<(glow::Context, &'static str)> {
let gl2_ctx = canvas
.get_context("webgl2")
.expect("Failed to query about WebGL2 context");
let gl2_ctx = gl2_ctx?;
tracing::debug!("WebGL2 selected.");
let gl2_ctx = gl2_ctx
.dyn_into::<web_sys::WebGl2RenderingContext>()
.unwrap();
let gl = glow::Context::from_webgl2_context(gl2_ctx);
let shader_prefix = "";
Some((gl, shader_prefix))
}
trait DummyWebGLConstructor {
fn from_webgl1_context(context: web_sys::WebGlRenderingContext) -> Self;
fn from_webgl2_context(context: web_sys::WebGl2RenderingContext) -> Self;
}
#[cfg(not(target_arch = "wasm32"))]
impl DummyWebGLConstructor for glow::Context {
fn from_webgl1_context(_context: WebGlRenderingContext) -> Self {
panic!("you can't use egui_web(glow) on native")
}
fn from_webgl2_context(_context: WebGl2RenderingContext) -> Self {
panic!("you can't use egui_web(glow) on native")
}
}

View File

@@ -1,189 +0,0 @@
use crate::{canvas_element, canvas_origin, AppRunner};
pub fn pos_from_mouse_event(canvas_id: &str, event: &web_sys::MouseEvent) -> egui::Pos2 {
let canvas = canvas_element(canvas_id).unwrap();
let rect = canvas.get_bounding_client_rect();
egui::Pos2 {
x: event.client_x() as f32 - rect.left() as f32,
y: event.client_y() as f32 - rect.top() as f32,
}
}
pub fn button_from_mouse_event(event: &web_sys::MouseEvent) -> Option<egui::PointerButton> {
match event.button() {
0 => Some(egui::PointerButton::Primary),
1 => Some(egui::PointerButton::Middle),
2 => Some(egui::PointerButton::Secondary),
_ => None,
}
}
/// A single touch is translated to a pointer movement. When a second touch is added, the pointer
/// should not jump to a different position. Therefore, we do not calculate the average position
/// of all touches, but we keep using the same touch as long as it is available.
///
/// `touch_id_for_pos` is the [`TouchId`](egui::TouchId) of the [`Touch`](web_sys::Touch) we previously used to determine the
/// pointer position.
pub fn pos_from_touch_event(
canvas_id: &str,
event: &web_sys::TouchEvent,
touch_id_for_pos: &mut Option<egui::TouchId>,
) -> egui::Pos2 {
let touch_for_pos = if let Some(touch_id_for_pos) = touch_id_for_pos {
// search for the touch we previously used for the position
// (unfortunately, `event.touches()` is not a rust collection):
(0..event.touches().length())
.into_iter()
.map(|i| event.touches().get(i).unwrap())
.find(|touch| egui::TouchId::from(touch.identifier()) == *touch_id_for_pos)
} else {
None
};
// Use the touch found above or pick the first, or return a default position if there is no
// touch at all. (The latter is not expected as the current method is only called when there is
// at least one touch.)
touch_for_pos
.or_else(|| event.touches().get(0))
.map_or(Default::default(), |touch| {
*touch_id_for_pos = Some(egui::TouchId::from(touch.identifier()));
pos_from_touch(canvas_origin(canvas_id), &touch)
})
}
fn pos_from_touch(canvas_origin: egui::Pos2, touch: &web_sys::Touch) -> egui::Pos2 {
egui::Pos2 {
x: touch.page_x() as f32 - canvas_origin.x as f32,
y: touch.page_y() as f32 - canvas_origin.y as f32,
}
}
pub fn push_touches(runner: &mut AppRunner, phase: egui::TouchPhase, event: &web_sys::TouchEvent) {
let canvas_origin = canvas_origin(runner.canvas_id());
for touch_idx in 0..event.changed_touches().length() {
if let Some(touch) = event.changed_touches().item(touch_idx) {
runner.input.raw.events.push(egui::Event::Touch {
device_id: egui::TouchDeviceId(0),
id: egui::TouchId::from(touch.identifier()),
phase,
pos: pos_from_touch(canvas_origin, &touch),
force: touch.force(),
});
}
}
}
/// 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 {
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"
)
}
/// Web sends all 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 translate_key(key: &str) -> Option<egui::Key> {
match key {
"ArrowDown" => Some(egui::Key::ArrowDown),
"ArrowLeft" => Some(egui::Key::ArrowLeft),
"ArrowRight" => Some(egui::Key::ArrowRight),
"ArrowUp" => Some(egui::Key::ArrowUp),
"Esc" | "Escape" => Some(egui::Key::Escape),
"Tab" => Some(egui::Key::Tab),
"Backspace" => Some(egui::Key::Backspace),
"Enter" => Some(egui::Key::Enter),
"Space" | " " => Some(egui::Key::Space),
"Help" | "Insert" => Some(egui::Key::Insert),
"Delete" => Some(egui::Key::Delete),
"Home" => Some(egui::Key::Home),
"End" => Some(egui::Key::End),
"PageUp" => Some(egui::Key::PageUp),
"PageDown" => Some(egui::Key::PageDown),
"0" => Some(egui::Key::Num0),
"1" => Some(egui::Key::Num1),
"2" => Some(egui::Key::Num2),
"3" => Some(egui::Key::Num3),
"4" => Some(egui::Key::Num4),
"5" => Some(egui::Key::Num5),
"6" => Some(egui::Key::Num6),
"7" => Some(egui::Key::Num7),
"8" => Some(egui::Key::Num8),
"9" => Some(egui::Key::Num9),
"a" | "A" => Some(egui::Key::A),
"b" | "B" => Some(egui::Key::B),
"c" | "C" => Some(egui::Key::C),
"d" | "D" => Some(egui::Key::D),
"e" | "E" => Some(egui::Key::E),
"f" | "F" => Some(egui::Key::F),
"g" | "G" => Some(egui::Key::G),
"h" | "H" => Some(egui::Key::H),
"i" | "I" => Some(egui::Key::I),
"j" | "J" => Some(egui::Key::J),
"k" | "K" => Some(egui::Key::K),
"l" | "L" => Some(egui::Key::L),
"m" | "M" => Some(egui::Key::M),
"n" | "N" => Some(egui::Key::N),
"o" | "O" => Some(egui::Key::O),
"p" | "P" => Some(egui::Key::P),
"q" | "Q" => Some(egui::Key::Q),
"r" | "R" => Some(egui::Key::R),
"s" | "S" => Some(egui::Key::S),
"t" | "T" => Some(egui::Key::T),
"u" | "U" => Some(egui::Key::U),
"v" | "V" => Some(egui::Key::V),
"w" | "W" => Some(egui::Key::W),
"x" | "X" => Some(egui::Key::X),
"y" | "Y" => Some(egui::Key::Y),
"z" | "Z" => Some(egui::Key::Z),
_ => None,
}
}
pub fn modifiers_from_event(event: &web_sys::KeyboardEvent) -> egui::Modifiers {
egui::Modifiers {
alt: event.alt_key(),
ctrl: event.ctrl_key(),
shift: event.shift_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
mac_cmd: event.meta_key(),
// Ideally we should know if we are running or mac or not,
// but this works good enough for now.
command: event.ctrl_key() || event.meta_key(),
}
}

View File

@@ -1,284 +0,0 @@
//! [`egui`] bindings for web apps (compiling to WASM).
//!
//! This library is an [`epi`] backend.
//!
//! If you are writing an app, you may want to look at [`eframe`](https://docs.rs/eframe) instead.
#![allow(clippy::missing_errors_doc)] // So many `-> Result<_, JsValue>`
pub mod backend;
mod events;
mod glow_wrapping;
mod input;
pub mod screen_reader;
pub mod storage;
mod text_agent;
pub use backend::*;
pub use events::*;
pub use storage::*;
use egui::mutex::{Mutex, MutexGuard};
pub use wasm_bindgen;
pub use web_sys;
use input::*;
use web_sys::EventTarget;
use std::collections::BTreeMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use wasm_bindgen::prelude::*;
// ----------------------------------------------------------------------------
/// Current time in seconds (since undefined point in time)
pub fn now_sec() -> f64 {
web_sys::window()
.expect("should have a Window")
.performance()
.expect("should have a Performance")
.now()
/ 1000.0
}
pub fn screen_size_in_native_points() -> Option<egui::Vec2> {
let window = web_sys::window()?;
Some(egui::vec2(
window.inner_width().ok()?.as_f64()? as f32,
window.inner_height().ok()?.as_f64()? as f32,
))
}
pub fn native_pixels_per_point() -> f32 {
let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32;
if pixels_per_point > 0.0 && pixels_per_point.is_finite() {
pixels_per_point
} else {
1.0
}
}
pub fn prefer_dark_mode() -> Option<bool> {
Some(
web_sys::window()?
.match_media("(prefers-color-scheme: dark)")
.ok()??
.matches(),
)
}
pub fn canvas_element(canvas_id: &str) -> Option<web_sys::HtmlCanvasElement> {
use wasm_bindgen::JsCast;
let document = web_sys::window()?.document()?;
let canvas = document.get_element_by_id(canvas_id)?;
canvas.dyn_into::<web_sys::HtmlCanvasElement>().ok()
}
pub fn canvas_element_or_die(canvas_id: &str) -> web_sys::HtmlCanvasElement {
crate::canvas_element(canvas_id)
.unwrap_or_else(|| panic!("Failed to find canvas with id '{}'", canvas_id))
}
fn canvas_origin(canvas_id: &str) -> egui::Pos2 {
let rect = canvas_element(canvas_id)
.unwrap()
.get_bounding_client_rect();
egui::Pos2::new(rect.left() as f32, rect.top() as f32)
}
pub fn canvas_size_in_points(canvas_id: &str) -> egui::Vec2 {
let canvas = canvas_element(canvas_id).unwrap();
let pixels_per_point = native_pixels_per_point();
egui::vec2(
canvas.width() as f32 / pixels_per_point,
canvas.height() as f32 / pixels_per_point,
)
}
pub fn resize_canvas_to_screen_size(canvas_id: &str, max_size_points: egui::Vec2) -> Option<()> {
let canvas = canvas_element(canvas_id)?;
let screen_size_points = screen_size_in_native_points()?;
let pixels_per_point = native_pixels_per_point();
let max_size_pixels = pixels_per_point * max_size_points;
let canvas_size_pixels = pixels_per_point * screen_size_points;
let canvas_size_pixels = canvas_size_pixels.min(max_size_pixels);
let canvas_size_points = canvas_size_pixels / pixels_per_point;
// Make sure that the height and width are always even numbers.
// otherwise, the page renders blurry on some platforms.
// See https://github.com/emilk/egui/issues/103
fn round_to_even(v: f32) -> f32 {
(v / 2.0).round() * 2.0
}
canvas
.style()
.set_property(
"width",
&format!("{}px", round_to_even(canvas_size_points.x)),
)
.ok()?;
canvas
.style()
.set_property(
"height",
&format!("{}px", round_to_even(canvas_size_points.y)),
)
.ok()?;
canvas.set_width(round_to_even(canvas_size_pixels.x) as u32);
canvas.set_height(round_to_even(canvas_size_pixels.y) as u32);
Some(())
}
// ----------------------------------------------------------------------------
pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> {
let document = web_sys::window()?.document()?;
document
.body()?
.style()
.set_property("cursor", cursor_web_name(cursor))
.ok()
}
#[cfg(web_sys_unstable_apis)]
pub fn set_clipboard_text(s: &str) {
if let Some(window) = web_sys::window() {
if let Some(clipboard) = window.navigator().clipboard() {
let promise = clipboard.write_text(s);
let future = wasm_bindgen_futures::JsFuture::from(promise);
let future = async move {
if let Err(err) = future.await {
tracing::error!("Copy/cut action denied: {:?}", err);
}
};
wasm_bindgen_futures::spawn_local(future);
}
}
}
pub fn spawn_future<F>(future: F)
where
F: std::future::Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str {
match cursor {
egui::CursorIcon::Alias => "alias",
egui::CursorIcon::AllScroll => "all-scroll",
egui::CursorIcon::Cell => "cell",
egui::CursorIcon::ContextMenu => "context-menu",
egui::CursorIcon::Copy => "copy",
egui::CursorIcon::Crosshair => "crosshair",
egui::CursorIcon::Default => "default",
egui::CursorIcon::Grab => "grab",
egui::CursorIcon::Grabbing => "grabbing",
egui::CursorIcon::Help => "help",
egui::CursorIcon::Move => "move",
egui::CursorIcon::NoDrop => "no-drop",
egui::CursorIcon::None => "none",
egui::CursorIcon::NotAllowed => "not-allowed",
egui::CursorIcon::PointingHand => "pointer",
egui::CursorIcon::Progress => "progress",
egui::CursorIcon::ResizeHorizontal => "ew-resize",
egui::CursorIcon::ResizeNeSw => "nesw-resize",
egui::CursorIcon::ResizeNwSe => "nwse-resize",
egui::CursorIcon::ResizeVertical => "ns-resize",
egui::CursorIcon::ResizeEast => "e-resize",
egui::CursorIcon::ResizeSouthEast => "se-resize",
egui::CursorIcon::ResizeSouth => "s-resize",
egui::CursorIcon::ResizeSouthWest => "sw-resize",
egui::CursorIcon::ResizeWest => "w-resize",
egui::CursorIcon::ResizeNorthWest => "nw-resize",
egui::CursorIcon::ResizeNorth => "n-resize",
egui::CursorIcon::ResizeNorthEast => "ne-resize",
egui::CursorIcon::ResizeColumn => "col-resize",
egui::CursorIcon::ResizeRow => "row-resize",
egui::CursorIcon::Text => "text",
egui::CursorIcon::VerticalText => "vertical-text",
egui::CursorIcon::Wait => "wait",
egui::CursorIcon::ZoomIn => "zoom-in",
egui::CursorIcon::ZoomOut => "zoom-out",
}
}
pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
let name = if new_tab { "_blank" } else { "_self" };
web_sys::window()?
.open_with_url_and_target(url, name)
.ok()?;
Some(())
}
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
///
/// Percent decoded
pub fn location_hash() -> String {
percent_decode(
&web_sys::window()
.unwrap()
.location()
.hash()
.unwrap_or_default(),
)
}
pub fn percent_decode(s: &str) -> String {
percent_encoding::percent_decode_str(s)
.decode_utf8_lossy()
.to_string()
}
// ----------------------------------------------------------------------------
pub(crate) fn webgl1_requires_brightening(gl: &web_sys::WebGlRenderingContext) -> bool {
// See https://github.com/emilk/egui/issues/794
// detect WebKitGTK
// WebKitGTK use WebKit default unmasked vendor and renderer
// but safari use same vendor and renderer
// so exclude "Mac OS X" user-agent.
let user_agent = web_sys::window().unwrap().navigator().user_agent().unwrap();
!user_agent.contains("Mac OS X") && crate::is_safari_and_webkit_gtk(gl)
}
/// detecting Safari and `webkitGTK`.
///
/// Safari and `webkitGTK` use unmasked renderer :Apple GPU
///
/// If we detect safari or `webkitGTKs` returns true.
///
/// This function used to avoid displaying linear color with `sRGB` supported systems.
fn is_safari_and_webkit_gtk(gl: &web_sys::WebGlRenderingContext) -> bool {
// This call produces a warning in Firefox ("WEBGL_debug_renderer_info is deprecated in Firefox and will be removed.")
// but unless we call it we get errors in Chrome when we call `get_parameter` below.
// TODO: do something smart based on user agent?
if gl
.get_extension("WEBGL_debug_renderer_info")
.unwrap()
.is_some()
{
if let Ok(renderer) =
gl.get_parameter(web_sys::WebglDebugRendererInfo::UNMASKED_RENDERER_WEBGL)
{
if let Some(renderer) = renderer.as_string() {
if renderer.contains("Apple") {
return true;
}
}
}
}
false
}

View File

@@ -1,49 +0,0 @@
pub struct ScreenReader {
#[cfg(feature = "screen_reader")]
tts: Option<tts::Tts>,
}
#[cfg(not(feature = "screen_reader"))]
#[allow(clippy::derivable_impls)] // False positive
impl Default for ScreenReader {
fn default() -> Self {
Self {}
}
}
#[cfg(feature = "screen_reader")]
impl Default for ScreenReader {
fn default() -> Self {
let tts = match tts::Tts::default() {
Ok(screen_reader) => {
tracing::debug!("Initialized screen reader.");
Some(screen_reader)
}
Err(err) => {
tracing::warn!("Failed to load screen reader: {}", err);
None
}
};
Self { tts }
}
}
impl ScreenReader {
#[cfg(not(feature = "screen_reader"))]
#[allow(clippy::unused_self)]
pub fn speak(&mut self, _text: &str) {}
#[cfg(feature = "screen_reader")]
pub fn speak(&mut self, text: &str) {
if text.is_empty() {
return;
}
if let Some(tts) = &mut self.tts {
tracing::debug!("Speaking: {:?}", text);
let interrupt = true;
if let Err(err) = tts.speak(text, interrupt) {
tracing::warn!("Failed to read: {}", err);
}
}
}
}

View File

@@ -1,47 +0,0 @@
fn local_storage() -> Option<web_sys::Storage> {
web_sys::window()?.local_storage().ok()?
}
pub fn local_storage_get(key: &str) -> Option<String> {
local_storage().map(|storage| storage.get_item(key).ok())??
}
pub fn local_storage_set(key: &str, value: &str) {
local_storage().map(|storage| storage.set_item(key, value));
}
pub fn local_storage_remove(key: &str) {
local_storage().map(|storage| storage.remove_item(key));
}
#[cfg(feature = "persistence")]
pub fn load_memory(ctx: &egui::Context) {
if let Some(memory_string) = local_storage_get("egui_memory_ron") {
match ron::from_str(&memory_string) {
Ok(memory) => {
*ctx.memory() = memory;
}
Err(err) => {
tracing::error!("Failed to parse memory RON: {}", err);
}
}
}
}
#[cfg(not(feature = "persistence"))]
pub fn load_memory(_: &egui::Context) {}
#[cfg(feature = "persistence")]
pub fn save_memory(ctx: &egui::Context) {
match ron::to_string(&*ctx.memory()) {
Ok(ron) => {
local_storage_set("egui_memory_ron", &ron);
}
Err(err) => {
tracing::error!("Failed to serialize memory as RON: {}", err);
}
}
}
#[cfg(not(feature = "persistence"))]
pub fn save_memory(_: &egui::Context) {}

View File

@@ -1,228 +0,0 @@
//! The text agent is an `<input>` element used to trigger
//! mobile keyboard and IME input.
use crate::{canvas_element, AppRunner, AppRunnerContainer};
use egui::mutex::MutexGuard;
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::prelude::*;
static AGENT_ID: &str = "egui_text_agent";
pub fn text_agent() -> web_sys::HtmlInputElement {
use wasm_bindgen::JsCast;
web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id(AGENT_ID)
.unwrap()
.dyn_into()
.unwrap()
}
/// Text event handler,
pub fn install_text_agent(runner_container: &AppRunnerContainer) -> Result<(), JsValue> {
use wasm_bindgen::JsCast;
let window = web_sys::window().unwrap();
let document = window.document().unwrap();
let body = document.body().expect("document should have a body");
let input = document
.create_element("input")?
.dyn_into::<web_sys::HtmlInputElement>()?;
let input = std::rc::Rc::new(input);
input.set_id(AGENT_ID);
let is_composing = Rc::new(Cell::new(false));
{
let style = input.style();
// Transparent
style.set_property("opacity", "0").unwrap();
// Hide under canvas
style.set_property("z-index", "-1").unwrap();
}
// Set size as small as possible, in case user may click on it.
input.set_size(1);
input.set_autofocus(true);
input.set_hidden(true);
// When IME is off
runner_container.add_event_listener(&input, "input", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::InputEvent, mut runner_lock| {
let text = input_clone.value();
if !text.is_empty() && !is_composing.get() {
input_clone.set_value("");
runner_lock.input.raw.events.push(egui::Event::Text(text));
runner_lock.needs_repaint.set_true();
}
}
})?;
{
// When IME is on, handle composition event
runner_container.add_event_listener(&input, "compositionstart", {
let input_clone = input.clone();
let is_composing = is_composing.clone();
move |_event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
is_composing.set(true);
input_clone.set_value("");
runner_lock
.input
.raw
.events
.push(egui::Event::CompositionStart);
runner_lock.needs_repaint.set_true();
}
})?;
runner_container.add_event_listener(
&input,
"compositionupdate",
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
if let Some(event) = event.data().map(egui::Event::CompositionUpdate) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.set_true();
}
},
)?;
runner_container.add_event_listener(&input, "compositionend", {
let input_clone = input.clone();
move |event: web_sys::CompositionEvent, mut runner_lock: MutexGuard<'_, AppRunner>| {
is_composing.set(false);
input_clone.set_value("");
if let Some(event) = event.data().map(egui::Event::CompositionEnd) {
runner_lock.input.raw.events.push(event);
runner_lock.needs_repaint.set_true();
}
}
})?;
}
// When input lost focus, focus on it again.
// It is useful when user click somewhere outside canvas.
runner_container.add_event_listener(
&input,
"focusout",
move |_event: web_sys::MouseEvent, _| {
// Delay 10 ms, and focus again.
let func = js_sys::Function::new_no_args(&format!(
"document.getElementById('{}').focus()",
AGENT_ID
));
window
.set_timeout_with_callback_and_timeout_and_arguments_0(&func, 10)
.unwrap();
},
)?;
body.append_child(&input)?;
Ok(())
}
/// Focus or blur text agent to toggle mobile keyboard.
pub fn update_text_agent(runner: MutexGuard<'_, 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 canvas_style = canvas_element(runner.canvas_id())?.style();
if runner.mutable_text_under_cursor {
let is_already_editing = input.hidden();
if is_already_editing {
input.set_hidden(false);
input.focus().ok()?;
// 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;
// estimated amount of screen covered by keyboard
let keyboard_fraction = 0.5;
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 = format!("{}%", (delta * 100.0).round());
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", &new_pos_percent).ok()?;
}
}
}
} else {
// Drop runner lock
drop(runner);
// Holding the runner lock while calling input.blur() causes a panic.
// This is most probably caused by the browser running the event handler
// for the triggered blur event synchronously, meaning that the mutex
// lock does not get dropped by the time another event handler is called.
//
// Why this didn't exist before #1290 is a mystery to me, but it exists now
// and this apparently is the fix for it
//
// ¯\_(ツ)_/¯ - @DusterTheFirst
input.blur().ok()?;
input.set_hidden(true);
canvas_style.set_property("position", "absolute").ok()?;
canvas_style.set_property("top", "0%").ok()?; // move back to normal position
}
Some(())
}
/// If context is running under mobile device?
fn is_mobile() -> Option<bool> {
const MOBILE_DEVICE: [&str; 6] = ["Android", "iPhone", "iPad", "iPod", "webOS", "BlackBerry"];
let user_agent = web_sys::window()?.navigator().user_agent().ok()?;
let is_mobile = MOBILE_DEVICE.iter().any(|&name| user_agent.contains(name));
Some(is_mobile)
}
// Move text agent to text cursor's position, on desktop/laptop,
// candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that.
pub fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) {
cursor.as_ref().and_then(|&egui::Pos2 { x, y }| {
let canvas = canvas_element(canvas_id)?;
let bounding_rect = text_agent().get_bounding_client_rect();
let y = (y + (canvas.scroll_top() + canvas.offset_top()) as f32)
.min(canvas.client_height() as f32 - bounding_rect.height() as f32);
let x = x + (canvas.scroll_left() + canvas.offset_left()) as f32;
// Canvas is translated 50% horizontally in html.
let x = (x - canvas.offset_width() as f32 / 2.0)
.min(canvas.client_width() as f32 - bounding_rect.width() as f32);
style.set_property("position", "absolute").ok()?;
style.set_property("top", &format!("{}px", y)).ok()?;
style.set_property("left", &format!("{}px", x)).ok()
})
} else {
style.set_property("position", "absolute").ok()?;
style.set_property("top", "0px").ok()?;
style.set_property("left", "0px").ok()
}
}