use std::{
cell::{Cell, RefCell},
fmt::Write,
rc::Rc,
};
use cranpose_core::{DisposableEffect, SideEffect, remember};
use cranpose_services::{BrowserHistoryObserver, BrowserHistoryRef, Saver, browser_history};
use crate::controller::NavController;
#[track_caller]
pub fn BindBrowserHistory<R: Clone + PartialEq + 'static>(
controller: NavController<R>,
saver: Saver<R>,
) {
let Some(history) = browser_history() else {
return;
};
controller.stack.read(|_| ());
let sync = remember(move || {
Rc::new(HistorySync {
history,
saver,
controller,
synced: RefCell::new(Vec::new()),
started: Cell::new(false),
adopt: Cell::new(false),
landing: Cell::new(None),
})
})
.with(Rc::clone);
let started = Rc::clone(&sync);
DisposableEffect(controller, move |scope| {
let observer = HistorySync::start(&started);
scope.on_dispose(move || drop(observer))
});
SideEffect(move || sync.stack_changed(sync.controller.back_stack()));
}
struct HistorySync<R: Clone + 'static> {
history: BrowserHistoryRef,
saver: Saver<R>,
controller: NavController<R>,
synced: RefCell<Vec<R>>,
started: Cell<bool>,
adopt: Cell<bool>,
landing: Cell<Option<usize>>,
}
impl<R: Clone + PartialEq + 'static> HistorySync<R> {
fn start(this: &Rc<Self>) -> BrowserHistoryObserver {
let opened = this.fragment_route();
if let Some(route) =
opened.filter(|route| this.controller.current_route().as_ref() != Some(route))
{
this.controller.navigate(route);
}
let routes = this.controller.back_stack();
this.history
.replace(routes.len(), &this.fragment_of(routes.last()));
*this.synced.borrow_mut() = routes;
this.started.set(true);
let sync = Rc::clone(this);
this.history.observe(Rc::new(move || sync.history_moved()))
}
fn stack_changed(&self, routes: Vec<R>) {
if !self.started.get() {
return;
}
let previous = self.synced.replace(routes);
let synced = self.synced.borrow();
if *synced == previous || self.adopt.replace(false) {
return;
}
let (before, after) = (previous.len(), synced.len());
if after == before + 1 && synced.starts_with(&previous) {
self.history.push(after, &self.fragment_of(synced.last()));
} else if after > 0 && after < before && previous.starts_with(&synced) {
self.landing.set(Some(after));
self.history.go(after as isize - before as isize);
} else if after > 0 {
self.history
.replace(after, &self.fragment_of(synced.last()));
}
}
fn history_moved(&self) {
let depth = self.history.depth();
if self
.landing
.take()
.is_some_and(|landing| Some(landing) == depth)
{
return;
}
let before = self.controller.back_stack();
match depth {
Some(depth) if depth > 0 && depth < before.len() => self.controller.pop_to_depth(depth),
Some(depth) if depth <= before.len() => {}
_ => {
if let Some(route) = self.fragment_route() {
self.controller.navigate(route);
}
}
}
if self.controller.back_stack() != before {
self.adopt.set(true);
}
}
fn fragment_route(&self) -> Option<R> {
let fragment = self.history.fragment();
let saved = decode(fragment.strip_prefix('/')?)?;
self.saver.restore(&saved)
}
fn fragment_of(&self, route: Option<&R>) -> String {
route.map_or_else(String::new, |route| {
format!("/{}", encode(&self.saver.save(route)))
})
}
}
fn encode(text: &str) -> String {
let mut encoded = String::with_capacity(text.len());
for byte in text.bytes() {
if byte.is_ascii_alphanumeric() || b"-._~/".contains(&byte) {
encoded.push(char::from(byte));
} else {
let _ = write!(encoded, "%{byte:02X}");
}
}
encoded
}
fn decode(text: &str) -> Option<String> {
let mut bytes = Vec::with_capacity(text.len());
let mut rest = text.as_bytes();
while let Some((&byte, tail)) = rest.split_first() {
if byte == b'%' {
let hex = std::str::from_utf8(tail.get(..2)?).ok()?;
bytes.push(u8::from_str_radix(hex, 16).ok()?);
rest = tail.get(2..)?;
} else {
bytes.push(byte);
rest = tail;
}
}
String::from_utf8(bytes).ok()
}