use frust_reactive::RwSignal;
use frust_widgets::{Location, NavChange, NavigatorView, RouteStack};
use reactive_graph::traits::{Get, Set};
#[derive(Clone, Copy)]
pub struct RouteObserver {
current: RwSignal<Option<Location>>,
stack: RwSignal<Vec<Option<Location>>>,
depth: RwSignal<usize>,
change: RwSignal<NavChange>,
generation: RwSignal<u64>,
}
const _: fn() = || {
fn assert<T: Send + Sync + 'static>() {}
assert::<RouteObserver>();
};
impl RouteObserver {
pub fn new() -> Self {
Self {
current: RwSignal::new(None),
stack: RwSignal::new(Vec::new()),
depth: RwSignal::new(0),
change: RwSignal::new(NavChange::default()),
generation: RwSignal::new(0),
}
}
pub fn observe<State: 'static>(self, view: NavigatorView<State>) -> NavigatorView<State> {
view.on_route_change(move |stack: &RouteStack| {
self.current.set(stack.current_route().cloned());
self.stack.set(stack.entries().to_vec());
self.depth.set(stack.depth());
self.change.set(stack.change());
self.generation.set(stack.generation());
})
}
pub fn current(&self) -> Option<Location> {
self.current.get()
}
pub fn path(&self) -> String {
self.current().map(|loc| loc.path).unwrap_or_default()
}
pub fn param(&self, key: &str) -> Option<String> {
self.current().and_then(|loc| loc.query.get(key).cloned())
}
pub fn stack(&self) -> Vec<Option<Location>> {
self.stack.get()
}
pub fn depth(&self) -> usize {
self.depth.get()
}
pub fn change(&self) -> NavChange {
self.change.get()
}
pub fn is_back(&self) -> bool {
self.change() == NavChange::Pop
}
}
impl Default for RouteObserver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use frust_core::{
AnyView, BuildCtx, ChangeFlags, FrameTime, PaintScene, RenderRoot, View, any,
};
use frust_reactive::ReactiveRuntime;
use frust_widgets::{NavigatorController, NavigatorView, PushOptions, navigator};
use kurbo::Size;
use std::sync::Arc;
struct SizedLeaf {
size: Size,
}
struct SizedLeafWidget {
size: Size,
}
impl View<()> for SizedLeaf {
type Element = SizedLeafWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> SizedLeafWidget {
SizedLeafWidget { size: self.size }
}
fn rebuild(
&self,
_prev: &Self,
element: &mut SizedLeafWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.size = self.size;
ChangeFlags::NONE
}
}
impl frust_core::Widget for SizedLeafWidget {
fn layout(
&mut self,
_ctx: &mut frust_core::LayoutCtx,
bc: &frust_core::BoxConstraints,
) -> Size {
bc.constrain(self.size)
}
fn paint(&mut self, _ctx: &mut frust_core::PaintCtx, _scene: &mut dyn PaintScene) {}
}
fn sized(w: f64, h: f64) -> AnyView<()> {
any(SizedLeaf {
size: Size::new(w, h),
})
}
#[derive(Default)]
struct RecordingScene;
impl PaintScene for RecordingScene {
fn fill_rect(&mut self, _origin: kurbo::Point, _size: Size, _color: peniko::Color) {}
fn draw_text(&mut self, _origin: kurbo::Point, _text: &str) {}
}
#[test]
fn route_observer_rides_context_and_updates_across_a_navigation() {
use frust_reactive::{provide_context, use_context};
let rt = ReactiveRuntime::init(Arc::new(|| {}));
let controller: NavigatorController<()> = NavigatorController::new();
let observer = rt.with_owner(RouteObserver::new);
let recovered = rt.with_owner(|| {
provide_context(observer);
use_context::<RouteObserver>()
});
let recovered = recovered.expect("a RouteObserver must survive provide_context");
let mut root: RenderRoot<(), NavigatorView<()>> = RenderRoot::new();
let mut app = {
let ctrl = controller.clone();
move |_: &mut ()| recovered.observe(navigator(&ctrl, || sized(10.0, 10.0)))
};
let mut state = ();
rt.with_owner(|| root.rebuild(&mut app, &mut state));
assert_eq!(recovered.depth(), 1);
assert_eq!(recovered.change(), NavChange::Initial);
controller.push_with_options(
|| sized(20.0, 20.0),
PushOptions::opaque().route(Location::parse("/detail")),
);
rt.with_owner(|| root.rebuild(&mut app, &mut state));
root.layout(Size::new(100.0, 100.0));
let mut scene = RecordingScene;
root.paint(&mut scene, FrameTime::ZERO);
assert_eq!(recovered.depth(), 2);
assert_eq!(recovered.change(), NavChange::Push);
assert_eq!(recovered.path(), "/detail");
assert!(!recovered.is_back());
}
}