use std::{
cell::RefCell,
error::Error,
fmt::Display,
rc::Rc,
};
use freya_core::{
integration::FxHashSet,
prelude::*,
};
use crate::{
components::child_router::consume_child_route_mapping,
memory::MemoryHistory,
navigation::NavigationTarget,
prelude::SiteMapSegment,
routable::Routable,
router_cfg::RouterConfig,
};
#[derive(Debug, Clone)]
pub struct ParseRouteError {
message: String,
}
impl Error for ParseRouteError {}
impl Display for ParseRouteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.message.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct ExternalNavigationFailure(pub String);
impl Error for ExternalNavigationFailure {}
impl Display for ExternalNavigationFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "External navigation failed: {}", self.0)
}
}
struct RouterContextInner {
subscribers: Rc<RefCell<FxHashSet<ReactiveContext>>>,
internal_route: fn(&str) -> bool,
site_map: &'static [SiteMapSegment],
history: MemoryHistory,
}
impl RouterContextInner {
fn update_subscribers(&self) {
for id in self.subscribers.borrow().iter() {
id.notify();
}
}
fn subscribe_to_current_context(&self) {
if let Some(mut rc) = ReactiveContext::try_current() {
rc.subscribe(&self.subscribers);
}
}
fn external(&mut self, external: String) -> Result<(), ExternalNavigationFailure> {
let failure = ExternalNavigationFailure(external);
self.update_subscribers();
Err(failure)
}
}
#[derive(Clone, Copy)]
pub struct RouterContext {
inner: State<RouterContextInner>,
}
impl RouterContext {
pub(crate) fn create<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
let subscribers = Rc::new(RefCell::new(FxHashSet::default()));
let history = if let Some(initial_path) = cfg.initial_path {
MemoryHistory::with_initial_path(initial_path)
} else {
MemoryHistory::default()
};
Self {
inner: State::create(RouterContextInner {
subscribers,
internal_route: |route| R::from_str(route).is_ok(),
site_map: R::SITE_MAP,
history,
}),
}
}
pub fn create_global<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self {
let subscribers = Rc::new(RefCell::new(FxHashSet::default()));
let history = if let Some(initial_path) = cfg.initial_path {
MemoryHistory::with_initial_path(initial_path)
} else {
MemoryHistory::default()
};
Self {
inner: State::create_global(RouterContextInner {
subscribers,
internal_route: |route| R::from_str(route).is_ok(),
site_map: R::SITE_MAP,
history,
}),
}
}
pub fn try_get() -> Option<Self> {
try_consume_context()
}
#[track_caller]
pub fn get() -> Self {
consume_context()
}
#[must_use]
pub fn can_go_back(&self) -> bool {
self.inner.peek().history.can_go_back()
}
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.inner.peek().history.can_go_forward()
}
pub fn go_back(&self) {
self.inner.peek().history.go_back();
self.change_route();
}
pub fn go_forward(&self) {
self.inner.peek().history.go_forward();
self.change_route();
}
pub fn push(
&self,
target: impl Into<NavigationTarget>,
) -> Result<(), ExternalNavigationFailure> {
let target = target.into();
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.push(p),
NavigationTarget::External(e) => return write.external(e),
}
}
self.change_route();
Ok(())
}
pub fn replace(
&self,
target: impl Into<NavigationTarget>,
) -> Result<(), ExternalNavigationFailure> {
let target = target.into();
{
let mut write = self.inner.write_unchecked();
match target {
NavigationTarget::Internal(p) => write.history.replace(p),
NavigationTarget::External(e) => return write.external(e),
}
}
self.change_route();
Ok(())
}
pub fn current<R: Routable>(&self) -> R {
let absolute_route = self.full_route_string();
let mapping = consume_child_route_mapping::<R>();
let route = match mapping.as_ref() {
Some(mapping) => mapping
.parse_route_from_root_route(&absolute_route)
.ok_or_else(|| "Failed to parse route".to_string()),
None => {
R::from_str(&absolute_route).map_err(|err| format!("Failed to parse route {err}"))
}
};
match route {
Ok(route) => route,
Err(_err) => "/".parse().unwrap_or_else(|err| panic!("{err}")),
}
}
pub fn full_route_string(&self) -> String {
let inner = self.inner.read();
inner.subscribe_to_current_context();
self.inner.peek().history.current_route()
}
pub fn site_map(&self) -> &'static [SiteMapSegment] {
self.inner.read().site_map
}
fn change_route(&self) {
self.inner.read().update_subscribers();
}
pub(crate) fn internal_route(&self, route: &str) -> bool {
(self.inner.read().internal_route)(route)
}
}
pub struct GenericRouterContext<R> {
inner: RouterContext,
_marker: std::marker::PhantomData<R>,
}
impl<R> GenericRouterContext<R>
where
R: Routable,
{
#[must_use]
pub fn can_go_back(&self) -> bool {
self.inner.can_go_back()
}
#[must_use]
pub fn can_go_forward(&self) -> bool {
self.inner.can_go_forward()
}
pub fn go_back(&self) {
self.inner.go_back();
}
pub fn go_forward(&self) {
self.inner.go_forward();
}
pub fn push(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Result<(), ExternalNavigationFailure> {
self.inner.push(target.into())
}
pub fn replace(
&self,
target: impl Into<NavigationTarget<R>>,
) -> Result<(), ExternalNavigationFailure> {
self.inner.replace(target.into())
}
pub fn current(&self) -> R
where
R: Clone,
{
self.inner.current()
}
}