From b5c8f034e7ddf367e9010a40ad684028da0a5c0d Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 17 Feb 2022 16:46:43 +0100 Subject: [PATCH] Add web location info to egui_web/epi (#1258) This adds all parts of the web "location" (URL) to frame.info().web_info, included a HashMap of the query parameters, percent-decoded and ready to go. This lets you easily pass key-value pairs to your eframe web app. --- Cargo.lock | 1 + eframe/CHANGELOG.md | 1 + egui_demo_lib/src/backend_panel.rs | 6 +++ egui_demo_lib/src/wrap_app.rs | 2 +- egui_web/CHANGELOG.md | 1 + egui_web/Cargo.toml | 1 + egui_web/src/backend.rs | 73 +++++++++++++++++++++++++++++- egui_web/src/lib.rs | 23 ++++++++-- epi/src/lib.rs | 56 ++++++++++++++++++++++- 9 files changed, 156 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63ebdb0fe..1d0beef37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1101,6 +1101,7 @@ dependencies = [ "egui_glow", "epi", "js-sys", + "percent-encoding", "ron", "serde", "tracing", diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index 5b64eafaa..d2861f732 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -14,6 +14,7 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG * Added `NativeOptions::initial_window_pos`. * Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)). * Log using the `tracing` crate. Log to stdout by adding `tracing_subscriber::fmt::init();` to your `main` ([#1192](https://github.com/emilk/egui/pull/1192)). +* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)). ## 0.16.0 - 2021-12-29 diff --git a/egui_demo_lib/src/backend_panel.rs b/egui_demo_lib/src/backend_panel.rs index 81903af17..5ead1cced 100644 --- a/egui_demo_lib/src/backend_panel.rs +++ b/egui_demo_lib/src/backend_panel.rs @@ -172,6 +172,12 @@ impl BackendPanel { show_integration_name(ui, &frame.info()); + if let Some(web_info) = &frame.info().web_info { + ui.collapsing("Web info (location)", |ui| { + ui.monospace(format!("{:#?}", web_info.location)); + }); + } + // For instance: `egui_web` sets `pixels_per_point` every frame to force // egui to use the same scale as the web zoom factor. let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some(); diff --git a/egui_demo_lib/src/wrap_app.rs b/egui_demo_lib/src/wrap_app.rs index d07bc072d..2913e149a 100644 --- a/egui_demo_lib/src/wrap_app.rs +++ b/egui_demo_lib/src/wrap_app.rs @@ -69,7 +69,7 @@ impl epi::App for WrapApp { fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { if let Some(web_info) = frame.info().web_info.as_ref() { - if let Some(anchor) = web_info.web_location_hash.strip_prefix('#') { + if let Some(anchor) = web_info.location.hash.strip_prefix('#') { self.selected_anchor = anchor.to_owned(); } } diff --git a/egui_web/CHANGELOG.md b/egui_web/CHANGELOG.md index f76bdb309..99bbbce75 100644 --- a/egui_web/CHANGELOG.md +++ b/egui_web/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to the `egui_web` integration will be noted in this file. * Shift-scroll will now result in horizontal scrolling ([#1136](https://github.com/emilk/egui/pull/1136)). * Updated `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)). * Panics will now be logged using `console.error`. +* Parse and percent-decode the web location query string ([#1258](https://github.com/emilk/egui/pull/1258)). ## 0.16.0 - 2021-12-29 diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index 2210fa6d4..a015ba215 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -57,6 +57,7 @@ epi = { version = "0.16.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" diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index 805e96030..570d02b13 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -81,6 +81,77 @@ impl epi::backend::RepaintSignal for NeedRepaint { // ---------------------------------------------------------------------------- +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, @@ -108,7 +179,7 @@ impl AppRunner { info: epi::IntegrationInfo { name: painter.name(), web_info: Some(epi::WebInfo { - web_location_hash: location_hash().unwrap_or_default(), + location: web_location(), }), prefer_dark_mode, cpu_usage: None, diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 7fcec9d18..a30dbf6e8 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -33,6 +33,7 @@ pub use web_sys; pub use painter::Painter; use std::cell::Cell; +use std::collections::BTreeMap; use std::rc::Rc; use std::sync::Arc; use wasm_bindgen::prelude::*; @@ -353,9 +354,23 @@ pub fn open_url(url: &str, new_tab: bool) -> Option<()> { Some(()) } -/// e.g. "#fragment" part of "www.example.com/index.html#fragment" -pub fn location_hash() -> Option { - web_sys::window()?.location().hash().ok() +/// 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() } /// Web sends all keys as strings, so it is up to us to figure out if it is @@ -661,7 +676,7 @@ fn install_document_events(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { // `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here if let Some(web_info) = &mut frame_lock.info.web_info { - web_info.web_location_hash = location_hash().unwrap_or_default(); + web_info.location.hash = location_hash(); } }) as Box); window.add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())?; diff --git a/epi/src/lib.rs b/epi/src/lib.rs index 7d646cfd7..b0dda5918 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -361,10 +361,62 @@ impl Frame { /// Information about the web environment (if applicable). #[derive(Clone, Debug)] pub struct WebInfo { - /// e.g. "#fragment" part of "www.example.com/index.html#fragment". + /// Information about the URL. + pub location: Location, +} + +/// Information about the URL. +/// +/// Everything has been percent decoded (`%20` -> ` ` etc). +#[derive(Clone, Debug)] +pub struct Location { + /// The full URL (`location.href`) without the hash. + /// + /// Example: "http://www.example.com:80/index.html?foo=bar". + pub url: String, + + /// `location.protocol` + /// + /// Example: "http:". + pub protocol: String, + + /// `location.host` + /// + /// Example: "example.com:80". + pub host: String, + + /// `location.hostname` + /// + /// Example: "example.com". + pub hostname: String, + + /// `location.port` + /// + /// Example: "80". + pub port: String, + + /// The "#fragment" part of "www.example.com/index.html?query#fragment". + /// /// Note that the leading `#` is included in the string. /// Also known as "hash-link" or "anchor". - pub web_location_hash: String, + pub hash: String, + + /// The "query" part of "www.example.com/index.html?query#fragment". + /// + /// Note that the leading `?` is NOT included in the string. + /// + /// Use [`Self::web_query_map]` to get the parsed version of it. + pub query: String, + + /// The parsed "query" part of "www.example.com/index.html?query#fragment". + /// + /// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}` + pub query_map: std::collections::BTreeMap, + + /// `location.origin` + /// + /// Example: "http://www.example.com:80" + pub origin: String, } /// Information about the integration passed to the use app each frame.