Skip to content

Commit b5c8f03

Browse files
authored
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.
1 parent 4e316d3 commit b5c8f03

File tree

9 files changed

+156
-8
lines changed

9 files changed

+156
-8
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

eframe/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ NOTE: [`egui_web`](../egui_web/CHANGELOG.md), [`egui-winit`](../egui-winit/CHANG
1414
* Added `NativeOptions::initial_window_pos`.
1515
* Shift-scroll will now result in horizontal scrolling on all platforms ([#1136](https://github.com/emilk/egui/pull/1136)).
1616
* 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)).
17+
* Expose all parts of the location/url in `frame.info().web_info` ([#1258](https://github.com/emilk/egui/pull/1258)).
1718

1819

1920
## 0.16.0 - 2021-12-29

egui_demo_lib/src/backend_panel.rs

+6
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ impl BackendPanel {
172172

173173
show_integration_name(ui, &frame.info());
174174

175+
if let Some(web_info) = &frame.info().web_info {
176+
ui.collapsing("Web info (location)", |ui| {
177+
ui.monospace(format!("{:#?}", web_info.location));
178+
});
179+
}
180+
175181
// For instance: `egui_web` sets `pixels_per_point` every frame to force
176182
// egui to use the same scale as the web zoom factor.
177183
let integration_controls_pixels_per_point = ui.input().raw.pixels_per_point.is_some();

egui_demo_lib/src/wrap_app.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ impl epi::App for WrapApp {
6969

7070
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) {
7171
if let Some(web_info) = frame.info().web_info.as_ref() {
72-
if let Some(anchor) = web_info.web_location_hash.strip_prefix('#') {
72+
if let Some(anchor) = web_info.location.hash.strip_prefix('#') {
7373
self.selected_anchor = anchor.to_owned();
7474
}
7575
}

egui_web/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ All notable changes to the `egui_web` integration will be noted in this file.
1010
* Shift-scroll will now result in horizontal scrolling ([#1136](https://github.com/emilk/egui/pull/1136)).
1111
* Updated `epi::IntegrationInfo::web_location_hash` on `hashchange` event ([#1140](https://github.com/emilk/egui/pull/1140)).
1212
* Panics will now be logged using `console.error`.
13+
* Parse and percent-decode the web location query string ([#1258](https://github.com/emilk/egui/pull/1258)).
1314

1415

1516
## 0.16.0 - 2021-12-29

egui_web/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ epi = { version = "0.16.0", path = "../epi" }
5757

5858
bytemuck = "1.7"
5959
js-sys = "0.3"
60+
percent-encoding = "2.1"
6061
tracing = "0.1"
6162
wasm-bindgen = "0.2"
6263
wasm-bindgen-futures = "0.4"

egui_web/src/backend.rs

+72-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,77 @@ impl epi::backend::RepaintSignal for NeedRepaint {
8181

8282
// ----------------------------------------------------------------------------
8383

84+
fn web_location() -> epi::Location {
85+
let location = web_sys::window().unwrap().location();
86+
87+
let hash = percent_decode(&location.hash().unwrap_or_default());
88+
89+
let query = location
90+
.search()
91+
.unwrap_or_default()
92+
.strip_prefix('?')
93+
.map(percent_decode)
94+
.unwrap_or_default();
95+
96+
let query_map = parse_query_map(&query)
97+
.iter()
98+
.map(|(k, v)| (k.to_string(), v.to_string()))
99+
.collect();
100+
101+
epi::Location {
102+
url: percent_decode(&location.href().unwrap_or_default()),
103+
protocol: percent_decode(&location.protocol().unwrap_or_default()),
104+
host: percent_decode(&location.host().unwrap_or_default()),
105+
hostname: percent_decode(&location.hostname().unwrap_or_default()),
106+
port: percent_decode(&location.port().unwrap_or_default()),
107+
hash,
108+
query,
109+
query_map,
110+
origin: percent_decode(&location.origin().unwrap_or_default()),
111+
}
112+
}
113+
114+
fn parse_query_map(query: &str) -> BTreeMap<&str, &str> {
115+
query
116+
.split('&')
117+
.filter_map(|pair| {
118+
if pair.is_empty() {
119+
None
120+
} else {
121+
Some(if let Some((key, value)) = pair.split_once('=') {
122+
(key, value)
123+
} else {
124+
(pair, "")
125+
})
126+
}
127+
})
128+
.collect()
129+
}
130+
131+
#[test]
132+
fn test_parse_query() {
133+
assert_eq!(parse_query_map(""), BTreeMap::default());
134+
assert_eq!(parse_query_map("foo"), BTreeMap::from_iter([("foo", "")]));
135+
assert_eq!(
136+
parse_query_map("foo=bar"),
137+
BTreeMap::from_iter([("foo", "bar")])
138+
);
139+
assert_eq!(
140+
parse_query_map("foo=bar&baz=42"),
141+
BTreeMap::from_iter([("foo", "bar"), ("baz", "42")])
142+
);
143+
assert_eq!(
144+
parse_query_map("foo&baz=42"),
145+
BTreeMap::from_iter([("foo", ""), ("baz", "42")])
146+
);
147+
assert_eq!(
148+
parse_query_map("foo&baz&&"),
149+
BTreeMap::from_iter([("foo", ""), ("baz", "")])
150+
);
151+
}
152+
153+
// ----------------------------------------------------------------------------
154+
84155
pub struct AppRunner {
85156
pub(crate) frame: epi::Frame,
86157
egui_ctx: egui::Context,
@@ -108,7 +179,7 @@ impl AppRunner {
108179
info: epi::IntegrationInfo {
109180
name: painter.name(),
110181
web_info: Some(epi::WebInfo {
111-
web_location_hash: location_hash().unwrap_or_default(),
182+
location: web_location(),
112183
}),
113184
prefer_dark_mode,
114185
cpu_usage: None,

egui_web/src/lib.rs

+19-4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub use web_sys;
3333

3434
pub use painter::Painter;
3535
use std::cell::Cell;
36+
use std::collections::BTreeMap;
3637
use std::rc::Rc;
3738
use std::sync::Arc;
3839
use wasm_bindgen::prelude::*;
@@ -353,9 +354,23 @@ pub fn open_url(url: &str, new_tab: bool) -> Option<()> {
353354
Some(())
354355
}
355356

356-
/// e.g. "#fragment" part of "www.example.com/index.html#fragment"
357-
pub fn location_hash() -> Option<String> {
358-
web_sys::window()?.location().hash().ok()
357+
/// e.g. "#fragment" part of "www.example.com/index.html#fragment",
358+
///
359+
/// Percent decoded
360+
pub fn location_hash() -> String {
361+
percent_decode(
362+
&web_sys::window()
363+
.unwrap()
364+
.location()
365+
.hash()
366+
.unwrap_or_default(),
367+
)
368+
}
369+
370+
pub fn percent_decode(s: &str) -> String {
371+
percent_encoding::percent_decode_str(s)
372+
.decode_utf8_lossy()
373+
.to_string()
359374
}
360375

361376
/// 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> {
661676

662677
// `epi::Frame::info(&self)` clones `epi::IntegrationInfo`, but we need to modify the original here
663678
if let Some(web_info) = &mut frame_lock.info.web_info {
664-
web_info.web_location_hash = location_hash().unwrap_or_default();
679+
web_info.location.hash = location_hash();
665680
}
666681
}) as Box<dyn FnMut()>);
667682
window.add_event_listener_with_callback("hashchange", closure.as_ref().unchecked_ref())?;

epi/src/lib.rs

+54-2
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,62 @@ impl Frame {
361361
/// Information about the web environment (if applicable).
362362
#[derive(Clone, Debug)]
363363
pub struct WebInfo {
364-
/// e.g. "#fragment" part of "www.example.com/index.html#fragment".
364+
/// Information about the URL.
365+
pub location: Location,
366+
}
367+
368+
/// Information about the URL.
369+
///
370+
/// Everything has been percent decoded (`%20` -> ` ` etc).
371+
#[derive(Clone, Debug)]
372+
pub struct Location {
373+
/// The full URL (`location.href`) without the hash.
374+
///
375+
/// Example: "http://www.example.com:80/index.html?foo=bar".
376+
pub url: String,
377+
378+
/// `location.protocol`
379+
///
380+
/// Example: "http:".
381+
pub protocol: String,
382+
383+
/// `location.host`
384+
///
385+
/// Example: "example.com:80".
386+
pub host: String,
387+
388+
/// `location.hostname`
389+
///
390+
/// Example: "example.com".
391+
pub hostname: String,
392+
393+
/// `location.port`
394+
///
395+
/// Example: "80".
396+
pub port: String,
397+
398+
/// The "#fragment" part of "www.example.com/index.html?query#fragment".
399+
///
365400
/// Note that the leading `#` is included in the string.
366401
/// Also known as "hash-link" or "anchor".
367-
pub web_location_hash: String,
402+
pub hash: String,
403+
404+
/// The "query" part of "www.example.com/index.html?query#fragment".
405+
///
406+
/// Note that the leading `?` is NOT included in the string.
407+
///
408+
/// Use [`Self::web_query_map]` to get the parsed version of it.
409+
pub query: String,
410+
411+
/// The parsed "query" part of "www.example.com/index.html?query#fragment".
412+
///
413+
/// "foo=42&bar%20" is parsed as `{"foo": "42", "bar ": ""}`
414+
pub query_map: std::collections::BTreeMap<String, String>,
415+
416+
/// `location.origin`
417+
///
418+
/// Example: "http://www.example.com:80"
419+
pub origin: String,
368420
}
369421

370422
/// Information about the integration passed to the use app each frame.

0 commit comments

Comments
 (0)