use std::collections::BTreeMap;
use egui::mutex::Mutex;
use crate::epi;
use super::percent_decode;
#[derive(Default)]
pub(crate) struct WebInput {
pub primary_touch: Option<egui::TouchId>,
pub accumulated_scale: f32,
pub accumulated_rotation: f32,
pub raw: egui::RawInput,
}
impl WebInput {
pub fn new_frame(&mut self, canvas_size: egui::Vec2) -> egui::RawInput {
let mut raw_input = egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(Default::default(), canvas_size)),
time: Some(super::now_sec()),
..self.raw.take()
};
let viewport = raw_input
.viewports
.entry(egui::ViewportId::ROOT)
.or_default();
viewport.native_pixels_per_point = Some(super::native_pixels_per_point());
let hidden = web_sys::window()
.and_then(|w| w.document())
.is_some_and(|doc| doc.hidden());
viewport.occluded = Some(hidden);
raw_input
}
pub fn set_focus(&mut self, focused: bool) {
if self.raw.focused == focused {
return;
}
self.raw.modifiers = egui::Modifiers::default(); self.raw.focused = focused;
self.raw.events.push(egui::Event::WindowFocused(focused));
self.primary_touch = None;
}
}
pub(crate) struct NeedRepaint {
next_repaint: Mutex<f64>,
max_fps: u32,
}
impl NeedRepaint {
pub fn new(max_fps: Option<u32>) -> Self {
Self {
next_repaint: Mutex::new(f64::NEG_INFINITY), max_fps: max_fps.unwrap_or(0),
}
}
}
impl NeedRepaint {
pub fn when_to_repaint(&self) -> f64 {
*self.next_repaint.lock()
}
pub fn clear(&self) {
*self.next_repaint.lock() = f64::INFINITY;
}
pub fn repaint_after(&self, num_seconds: f64) {
let mut time = super::now_sec() + num_seconds;
time = self.round_repaint_time_to_rate(time);
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
pub fn repaint(&self) {
let time = self.round_repaint_time_to_rate(super::now_sec());
let mut repaint_time = self.next_repaint.lock();
*repaint_time = repaint_time.min(time);
}
pub fn repaint_asap(&self) {
*self.next_repaint.lock() = f64::NEG_INFINITY;
}
pub fn needs_repaint(&self) -> bool {
self.when_to_repaint() <= super::now_sec()
}
fn round_repaint_time_to_rate(&self, time: f64) -> f64 {
if self.max_fps == 0 {
time
} else {
let interval = 1.0 / self.max_fps as f64;
(time / interval).ceil() * interval
}
}
}
pub fn user_agent() -> Option<String> {
web_sys::window()?.navigator().user_agent().ok()
}
pub 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('?')
.unwrap_or_default()
.to_owned();
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_map: parse_query_map(&query),
query,
origin: percent_decode(&location.origin().unwrap_or_default()),
}
}
fn parse_query_map(query: &str) -> BTreeMap<String, Vec<String>> {
let mut map: BTreeMap<String, Vec<String>> = Default::default();
for pair in query.split('&') {
if !pair.is_empty() {
if let Some((key, value)) = pair.split_once('=') {
map.entry(percent_decode(key))
.or_default()
.push(percent_decode(value));
} else {
map.entry(percent_decode(pair))
.or_default()
.push(String::new());
}
}
}
map
}
#[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", "")])
);
assert_eq!(
parse_query_map("badger=data.rrd%3Fparam1%3Dfoo%26param2%3Dbar&mushroom=snake"),
BTreeMap::from_iter([
("badger", "data.rrd?param1=foo¶m2=bar"),
("mushroom", "snake")
])
);
}