diff --git a/Cargo.lock b/Cargo.lock index 8c2b9ae5bc..7a25d92eab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,7 @@ dependencies = [ "sim", "svg_face", "wasm-bindgen", + "web-sys", "widgetry", ] diff --git a/game/Cargo.toml b/game/Cargo.toml index 1afd7f113f..f3dc0e69d2 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib", "lib"] [features] default = ["built", "map_gui/native", "widgetry/native-backend"] -wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "widgetry/wasm-backend"] +wasm = ["getrandom/js", "map_gui/wasm", "wasm-bindgen", "web-sys", "widgetry/wasm-backend"] [dependencies] aabb-quadtree = "0.1.0" @@ -45,6 +45,7 @@ serde_json = "1.0.61" svg_face = "0.1.3" sim = { path = "../sim" } wasm-bindgen = { version = "0.2.70", optional = true } +web-sys = { version = "0.3.47", optional = true, features=["History", "Location", "Window"] } widgetry = { path = "../widgetry" } [build-dependencies] diff --git a/game/src/common/mod.rs b/game/src/common/mod.rs index 4d876b1e68..2249bcaa1c 100644 --- a/game/src/common/mod.rs +++ b/game/src/common/mod.rs @@ -1,5 +1,7 @@ use std::collections::BTreeSet; +use anyhow::Result; + use geom::{Duration, Polygon}; use map_gui::ID; use map_model::{IntersectionID, Map, RoadID}; @@ -407,3 +409,103 @@ pub fn checkbox_per_mode( } Widget::custom_row(filters) } + +/// This does nothing on native. On web, it modifies the current URL to change the first free +/// parameter in the HTTP GET params to the specified value, adding it if needed. +#[allow(unused_variables)] +pub fn update_url(free_param: &str) -> Result<()> { + #[cfg(target_arch = "wasm32")] + { + let window = web_sys::window().ok_or(anyhow!("no window?"))?; + let url = window.location().href().map_err(|err| { + anyhow!(err + .as_string() + .unwrap_or("window.location.href failed".to_string())) + })?; + let new_url = change_url_free_query_param(url, free_param); + + // Setting window.location.href may seem like the obvious thing to do, but that actually + // refreshes the page. This method just changes the URL and doesn't mess up history. See + // https://developer.mozilla.org/en-US/docs/Web/API/History_API/Working_with_the_History_API. + let history = window.history().map_err(|err| { + anyhow!(err + .as_string() + .unwrap_or("window.history failed".to_string())) + })?; + history + .replace_state_with_url(&wasm_bindgen::JsValue::NULL, "", Some(&new_url)) + .map_err(|err| { + anyhow!(err + .as_string() + .unwrap_or("window.history.replace_state failed".to_string())) + })?; + } + Ok(()) +} + +#[allow(unused)] +fn change_url_free_query_param(url: String, free_param: &str) -> String { + // The URL parsing crates I checked had lots of dependencies and didn't even expose such a nice + // API for doing this anyway. + let url_parts = url.split("?").collect::>(); + if url_parts.len() == 1 { + return format!("{}?{}", url, free_param); + } + let mut query_params = String::new(); + let mut found_free = false; + let mut first = true; + for x in url_parts[1].split("&") { + if !first { + query_params.push('&'); + } + first = false; + + if x.starts_with("--") { + query_params.push_str(x); + } else if !found_free { + // Replace the first free parameter + query_params.push_str(free_param); + found_free = true; + } else { + query_params.push_str(x); + } + } + if !found_free { + if !first { + query_params.push('&'); + } + query_params.push_str(free_param); + } + + format!("{}?{}", url_parts[0], query_params) +} + +#[cfg(test)] +mod tests { + #[test] + fn test_change_url() { + use super::change_url_free_query_param; + + assert_eq!( + "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin", + change_url_free_query_param( + "http://0.0.0.0:8000/?--dev".to_string(), + "seattle/maps/montlake.bin" + ) + ); + assert_eq!( + "http://0.0.0.0:8000/?--dev&seattle/maps/qa.bin", + change_url_free_query_param( + "http://0.0.0.0:8000/?--dev&seattle/maps/montlake.bin".to_string(), + "seattle/maps/qa.bin" + ) + ); + assert_eq!( + "http://0.0.0.0:8000?seattle/maps/montlake.bin", + change_url_free_query_param( + "http://0.0.0.0:8000".to_string(), + "seattle/maps/montlake.bin" + ) + ); + } +} diff --git a/game/src/sandbox/gameplay/freeform.rs b/game/src/sandbox/gameplay/freeform.rs index 5def888d78..3ec4350ad1 100644 --- a/game/src/sandbox/gameplay/freeform.rs +++ b/game/src/sandbox/gameplay/freeform.rs @@ -15,7 +15,7 @@ use widgetry::{ }; use crate::app::{App, Transition}; -use crate::common::CommonState; +use crate::common::{update_url, CommonState}; use crate::edit::EditMode; use crate::sandbox::gameplay::{GameplayMode, GameplayState}; use crate::sandbox::{Actions, SandboxControls, SandboxMode}; @@ -26,7 +26,18 @@ pub struct Freeform { } impl Freeform { - pub fn new(ctx: &mut EventCtx) -> Box { + pub fn new(ctx: &mut EventCtx, app: &App) -> Box { + if let Err(err) = update_url( + app.primary + .map + .get_name() + .path() + .strip_prefix(&abstio::path("")) + .unwrap(), + ) { + warn!("Couldn't update URL: {}", err); + } + Box::new(Freeform { top_center: Panel::empty(ctx), }) diff --git a/game/src/sandbox/gameplay/mod.rs b/game/src/sandbox/gameplay/mod.rs index 252a2cc2db..eb9de6fa94 100644 --- a/game/src/sandbox/gameplay/mod.rs +++ b/game/src/sandbox/gameplay/mod.rs @@ -207,9 +207,9 @@ impl GameplayMode { /// after this, so each constructor doesn't need to. pub fn initialize(&self, ctx: &mut EventCtx, app: &mut App) -> Box { match self { - GameplayMode::Freeform(_) => freeform::Freeform::new(ctx), + GameplayMode::Freeform(_) => freeform::Freeform::new(ctx, app), GameplayMode::PlayScenario(_, ref scenario, ref modifiers) => { - play_scenario::PlayScenario::new(ctx, scenario, modifiers.clone()) + play_scenario::PlayScenario::new(ctx, app, scenario, modifiers.clone()) } GameplayMode::FixTrafficSignals => { fix_traffic_signals::FixTrafficSignals::new(ctx, app) diff --git a/game/src/sandbox/gameplay/play_scenario.rs b/game/src/sandbox/gameplay/play_scenario.rs index ef1374a21a..f6d586fd11 100644 --- a/game/src/sandbox/gameplay/play_scenario.rs +++ b/game/src/sandbox/gameplay/play_scenario.rs @@ -10,7 +10,7 @@ use widgetry::{ }; use crate::app::{App, Transition}; -use crate::common::checkbox_per_mode; +use crate::common::{checkbox_per_mode, update_url}; use crate::edit::EditMode; use crate::sandbox::gameplay::freeform::ChangeScenario; use crate::sandbox::gameplay::{GameplayMode, GameplayState}; @@ -25,9 +25,21 @@ pub struct PlayScenario { impl PlayScenario { pub fn new( ctx: &mut EventCtx, + app: &App, name: &String, modifiers: Vec, ) -> Box { + if let Err(err) = update_url( + // For dynamiclly generated scenarios like "random" and "home_to_work", this winds up + // making up a filename that doesn't actually exist. But if you pass that in, it winds + // up working, because we call abstio::parse_scenario_path() on the other side. + abstio::path_scenario(app.primary.map.get_name(), name) + .strip_prefix(&abstio::path("")) + .unwrap(), + ) { + warn!("Couldn't update URL: {}", err); + } + Box::new(PlayScenario { top_center: Panel::empty(ctx), scenario_name: name.to_string(),