diff --git a/Cargo.lock b/Cargo.lock index bc9fca8..2420690 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1482,6 +1482,7 @@ dependencies = [ "egui_suspense", "js-sys", "matchit", + "thiserror", "tokio", "wasm-bindgen", "web-sys", diff --git a/crates/egui_router/CHANGELOG.md b/crates/egui_router/CHANGELOG.md index a10d588..abed8c7 100644 --- a/crates/egui_router/CHANGELOG.md +++ b/crates/egui_router/CHANGELOG.md @@ -1,5 +1,9 @@ # egui_router changelog +## 0.1.1 + +- Added missing documentation + ## 0.1.0 - Initial release \ No newline at end of file diff --git a/crates/egui_router/Cargo.toml b/crates/egui_router/Cargo.toml index 655d1c8..533e354 100644 --- a/crates/egui_router/Cargo.toml +++ b/crates/egui_router/Cargo.toml @@ -28,6 +28,7 @@ egui_inbox.workspace = true egui_suspense = { workspace = true, optional = true } matchit = "0.8" +thiserror = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = { version = "0.3", features = ["History", "PopStateEvent", "HtmlCollection"] } diff --git a/crates/egui_router/examples/router_minimal.rs b/crates/egui_router/examples/router_minimal.rs index b50b9d5..afdb582 100644 --- a/crates/egui_router/examples/router_minimal.rs +++ b/crates/egui_router/examples/router_minimal.rs @@ -45,28 +45,28 @@ async fn main() -> eframe::Result<()> { } fn home(_request: Request) -> impl Route { - |ui: &mut Ui, inbox: &mut UiInbox| { + |ui: &mut Ui, state: &mut AppState| { background(ui, ui.style().visuals.faint_bg_color, |ui| { ui.heading("Home!"); ui.label("Navigate to post:"); if ui.link("Post 1").clicked() { - inbox + state .sender() .send(RouterMessage::Navigate("/post/1".to_string())) .ok(); } if ui.link("Post 2").clicked() { - inbox + state .sender() .send(RouterMessage::Navigate("/post/2".to_string())) .ok(); } if ui.link("Invalid Post").clicked() { - inbox + state .sender() .send(RouterMessage::Navigate("/post/".to_string())) .ok(); diff --git a/crates/egui_router/src/async_route.rs b/crates/egui_router/src/async_route.rs index 672f267..048de05 100644 --- a/crates/egui_router/src/async_route.rs +++ b/crates/egui_router/src/async_route.rs @@ -3,7 +3,7 @@ use crate::Route; use egui::Ui; use egui_suspense::EguiSuspense; -pub struct AsyncRoute { +pub(crate) struct AsyncRoute { pub suspense: EguiSuspense + Send + Sync>, HandlerError>, } diff --git a/crates/egui_router/src/handler.rs b/crates/egui_router/src/handler.rs index 7f6b80d..8d8d7a3 100644 --- a/crates/egui_router/src/handler.rs +++ b/crates/egui_router/src/handler.rs @@ -1,38 +1,31 @@ use crate::{Request, Route}; -use std::fmt::Display; -#[derive(Debug)] +/// Error returned from a [Handler] +#[derive(Debug, thiserror::Error)] pub enum HandlerError { + /// Not found error + #[error("Page not found")] NotFound, + /// Custom error message + #[error("{0}")] Message(String), - Error(Box), -} - -impl Display for HandlerError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Message(msg) => write!(f, "{}", msg), - Self::NotFound => write!(f, "Handler not found"), - Self::Error(err) => write!(f, "Handler error: {}", err), - } - } + /// Boxed error + #[error("Handler error: {0}")] + Boxed(Box), } +/// Handler Result type pub type HandlerResult = Result; -impl From for HandlerError { - fn from(err: T) -> Self { - Self::Error(Box::new(err)) - } -} - +/// Trait for a route handler. // The args argument is just so we can implement multiple specializations, like explained here: // https://geo-ant.github.io/blog/2021/rust-traits-and-variadic-functions/ pub trait MakeHandler { fn handle(&mut self, state: Request) -> HandlerResult>>; } -pub type Handler = Box) -> HandlerResult>>>; +pub(crate) type Handler = + Box) -> HandlerResult>>>; impl MakeHandler, ())> for F where @@ -75,17 +68,11 @@ where } #[cfg(feature = "async")] -pub mod async_impl { +mod async_impl { use crate::handler::HandlerResult; - use crate::{Request, Route}; - use std::collections::BTreeMap; + use crate::{OwnedRequest, Request, Route}; use std::future::Future; - pub struct OwnedRequest { - pub params: BTreeMap, - pub state: State, - } - pub trait AsyncMakeHandler { fn handle( &self, @@ -153,3 +140,6 @@ pub mod async_impl { } } } + +#[cfg(feature = "async")] +pub use async_impl::*; diff --git a/crates/egui_router/src/history/browser.rs b/crates/egui_router/src/history/browser.rs index 780a3d1..00998ed 100644 --- a/crates/egui_router/src/history/browser.rs +++ b/crates/egui_router/src/history/browser.rs @@ -5,6 +5,7 @@ use wasm_bindgen::closure::Closure; use wasm_bindgen::JsCast; use web_sys::window; +/// Browser history implementation pub struct BrowserHistory { base_href: String, inbox: UiInbox, @@ -19,6 +20,11 @@ impl Default for BrowserHistory { } impl BrowserHistory { + /// Create a new [BrowserHistory] instance. Optionally pass a base href. + /// If no base href is passed, it will be read from the base tag in the document or default to "" + /// If you are deploying to github pages you could e.g. use `Some("/my-repo/#")` as base href + /// so that navigation only happens in the fragment and any route will load the index.html + /// (otherwise there might be a 404 error when refreshing the page). pub fn new(base_href: Option) -> Self { let window = window().unwrap(); diff --git a/crates/egui_router/src/history/memory.rs b/crates/egui_router/src/history/memory.rs index c370a7c..5e4342d 100644 --- a/crates/egui_router/src/history/memory.rs +++ b/crates/egui_router/src/history/memory.rs @@ -2,6 +2,8 @@ use crate::history::{History, HistoryEvent, HistoryResult}; use egui::Context; use std::iter; +/// A memory history implementation. Currently, this is a no-op, since [EguiRouter] stores the +/// history itself, but this could change in the future. #[derive(Debug, Clone, Default)] pub struct MemoryHistory {} diff --git a/crates/egui_router/src/history/mod.rs b/crates/egui_router/src/history/mod.rs index 9249cf4..da4b79b 100644 --- a/crates/egui_router/src/history/mod.rs +++ b/crates/egui_router/src/history/mod.rs @@ -7,31 +7,46 @@ use crate::history; pub use browser::BrowserHistory; pub use memory::MemoryHistory; +/// Implement this trait to provide a custom history implementation pub trait History { + /// Check whether there is a new HistoryEvent (a navigation occurred) fn update(&mut self, ctx: &egui::Context) -> impl Iterator + 'static; + /// Get the currently active route fn active_route(&self) -> Option<(String, Option)>; + /// Push a new route to the history fn push(&mut self, url: &str, state: u32) -> HistoryResult; + /// Replace the current route in the history fn replace(&mut self, url: &str, state: u32) -> HistoryResult; + /// Go back in the history fn back(&mut self) -> HistoryResult; + /// Go forward in the history fn forward(&mut self) -> HistoryResult; } +/// Default history. Uses [BrowserHistory] on wasm32 and [MemoryHistory] otherwise #[cfg(target_arch = "wasm32")] pub type DefaultHistory = history::BrowserHistory; +/// Default history. Uses [BrowserHistory] on wasm32 and [MemoryHistory] otherwise #[cfg(not(target_arch = "wasm32"))] pub type DefaultHistory = history::MemoryHistory; +/// Result type returned by [History::update] #[derive(Debug, Clone)] pub struct HistoryEvent { + /// The path we are navigating to pub location: String, + /// The state of the history pub state: Option, } +/// History Result type type HistoryResult = Result; -#[derive(Debug)] +/// History error +#[derive(Debug, thiserror::Error)] pub enum HistoryError { #[cfg(target_arch = "wasm32")] + #[error("History error: {0:?}")] JsError(wasm_bindgen::JsValue), } diff --git a/crates/egui_router/src/lib.rs b/crates/egui_router/src/lib.rs index 4ba4599..6598f01 100644 --- a/crates/egui_router/src/lib.rs +++ b/crates/egui_router/src/lib.rs @@ -1,10 +1,16 @@ +#![doc = include_str!("../README.md")] +#![forbid(unsafe_code)] +#![warn(missing_docs)] + #[cfg(feature = "async")] mod async_route; mod handler; +/// History types pub mod history; mod route_kind; mod router; mod router_builder; +/// Transition types pub mod transition; use crate::history::HistoryError; @@ -16,13 +22,18 @@ use std::sync::atomic::AtomicUsize; pub use handler::{HandlerError, HandlerResult}; pub use router::EguiRouter; -#[cfg(feature = "async")] -pub use handler::async_impl::OwnedRequest; - -pub trait Route { +/// A route instance created by a [handler::Handler] +pub trait Route { + /// Render the route ui fn ui(&mut self, ui: &mut egui::Ui, state: &mut State); } +impl Route for F { + fn ui(&mut self, ui: &mut egui::Ui, state: &mut State) { + self(ui, state) + } +} + static ID: AtomicUsize = AtomicUsize::new(0); struct RouteState { @@ -32,11 +43,17 @@ struct RouteState { state: u32, } +/// Router Result type pub type RouterResult = Result; -#[derive(Debug)] +/// Router error +#[derive(Debug, thiserror::Error)] pub enum RouterError { + /// Error when updating history + #[error("History error: {0}")] HistoryError(HistoryError), + /// Not found error + #[error("Route not found")] NotFound, } @@ -46,6 +63,7 @@ impl From for RouterError { } } +/// Page transition configuration #[derive(Debug, Clone)] pub struct TransitionConfig { duration: Option, @@ -66,6 +84,7 @@ impl Default for TransitionConfig { } impl TransitionConfig { + /// Create a new transition pub fn new(_in: impl Into, out: impl Into) -> Self { Self { _in: _in.into(), @@ -74,10 +93,12 @@ impl TransitionConfig { } } + /// A iOS-like slide transition (Same as [TransitionConfig::default]) pub fn slide() -> Self { Self::default() } + /// A android-like fade up transition pub fn fade_up() -> Self { Self::new( SlideFadeTransition( @@ -88,19 +109,23 @@ impl TransitionConfig { ) } + /// A basic fade transition pub fn fade() -> Self { Self::new(transition::FadeTransition, transition::FadeTransition) } + /// No transition pub fn none() -> Self { Self::new(transition::NoTransition, transition::NoTransition) } + /// Customise the easing function pub fn with_easing(mut self, easing: fn(f32) -> f32) -> Self { self.easing = easing; self } + /// Customise the duration pub fn with_duration(mut self, duration: f32) -> Self { self.duration = Some(duration); self @@ -112,23 +137,19 @@ struct CurrentTransition { leaving_route: Option>, } +/// Request passed to a [handler::MakeHandler] pub struct Request<'a, State = ()> { + /// The parsed path params pub params: matchit::Params<'a, 'a>, + /// The custom state pub state: &'a mut State, } -// impl Handler for F -// where -// F: Fn(&mut State) -> Fut, -// Fut: std::future::Future, -// { -// async fn handle(&mut self, state: &mut State) -> Box> { -// Box::new((self(state)).await) -// } -// } - -impl Route for F { - fn ui(&mut self, ui: &mut egui::Ui, state: &mut State) { - self(ui, state) - } +#[cfg(feature = "async")] +/// Owned request, passed to [handler::AsyncMakeHandler] +pub struct OwnedRequest { + /// The parsed path params + pub params: std::collections::BTreeMap, + /// The custom state + pub state: State, } diff --git a/crates/egui_router/src/route_kind.rs b/crates/egui_router/src/route_kind.rs index dc80c3d..77db28f 100644 --- a/crates/egui_router/src/route_kind.rs +++ b/crates/egui_router/src/route_kind.rs @@ -1,6 +1,6 @@ use crate::handler::Handler; -pub enum RouteKind { +pub(crate) enum RouteKind { Route(Handler), Redirect(String), } diff --git a/crates/egui_router/src/router.rs b/crates/egui_router/src/router.rs index 296a58c..6199610 100644 --- a/crates/egui_router/src/router.rs +++ b/crates/egui_router/src/router.rs @@ -9,6 +9,7 @@ use egui::Ui; use matchit::MatchError; use std::sync::atomic::Ordering; +/// A router instance pub struct EguiRouter { router: matchit::Router>, history: Vec>, @@ -26,6 +27,7 @@ pub struct EguiRouter { } impl EguiRouter { + /// Create a new [RouterBuilder] pub fn builder() -> RouterBuilder { RouterBuilder::new() } @@ -56,6 +58,7 @@ impl EguiRouter { router } + /// Get the active route pub fn active_route(&self) -> Option<&str> { self.history.last().map(|r| r.path.as_str()) } @@ -110,6 +113,7 @@ impl EguiRouter { result } + /// Navigate with a custom transition pub fn navigate_transition( &mut self, state: &mut State, @@ -124,6 +128,11 @@ impl EguiRouter { Ok(()) } + /// Navigate with the default transition + pub fn navigate(&mut self, state: &mut State, route: impl Into) -> RouterResult { + self.navigate_transition(state, route, self.forward_transition.clone()) + } + fn back_impl(&mut self, transition_config: TransitionConfig) -> RouterResult { if self.history.len() > 1 { let leaving_route = self.history.pop(); @@ -136,19 +145,18 @@ impl EguiRouter { Ok(()) } + /// Go back with a custom transition pub fn back_transition(&mut self, transition_config: TransitionConfig) -> RouterResult { self.history_kind.back()?; self.back_impl(transition_config) } - pub fn navigate(&mut self, state: &mut State, route: impl Into) -> RouterResult { - self.navigate_transition(state, route, self.forward_transition.clone()) - } - + /// Go back with the default transition pub fn back(&mut self) -> RouterResult { self.back_transition(self.backward_transition.clone()) } + /// Replace the current route with a custom transition pub fn replace_transition( &mut self, state: &mut State, @@ -203,10 +211,12 @@ impl EguiRouter { result } + /// Replace the current route with the default transition pub fn replace(&mut self, state: &mut State, path: impl Into) -> RouterResult { self.replace_transition(state, path, self.replace_transition.clone()) } + /// Render the router pub fn ui(&mut self, ui: &mut Ui, state: &mut State) { for e in self.history_kind.update(ui.ctx()) { let state_index = e.state.unwrap_or(0); diff --git a/crates/egui_router/src/router_builder.rs b/crates/egui_router/src/router_builder.rs index f6f2bf3..eb560ea 100644 --- a/crates/egui_router/src/router_builder.rs +++ b/crates/egui_router/src/router_builder.rs @@ -4,10 +4,11 @@ use crate::route_kind::RouteKind; use crate::{EguiRouter, TransitionConfig}; use std::sync::Arc; -pub type ErrorUi = +pub(crate) type ErrorUi = Arc>; -pub type LoadingUi = Arc>; +pub(crate) type LoadingUi = Arc>; +/// Builder to create a [EguiRouter] pub struct RouterBuilder { pub(crate) router: matchit::Router>, pub(crate) default_route: Option, @@ -31,6 +32,7 @@ impl Default for RouterBuilder { } impl RouterBuilder { + /// Create a new router builder pub fn new() -> Self { Self { router: matchit::Router::new(), @@ -49,42 +51,50 @@ impl RouterBuilder { } } + /// Set the transition for both forward and backward transitions pub fn transition(mut self, transition: TransitionConfig) -> Self { self.forward_transition = transition.clone(); self.backward_transition = transition; self } + /// Set the transition for forward transitions pub fn forward_transition(mut self, transition: TransitionConfig) -> Self { self.forward_transition = transition; self } + /// Set the transition for backward transitions pub fn backward_transition(mut self, transition: TransitionConfig) -> Self { self.backward_transition = transition; self } + /// Set the transition for replace transitions pub fn replace_transition(mut self, transition: TransitionConfig) -> Self { self.replace_transition = transition; self } + /// Set the default duration for transitions pub fn default_duration(mut self, duration: f32) -> Self { self.default_duration = Some(duration); self } + /// Set the default route (when using [history::BrowserHistory], window.location.pathname will be used instead) pub fn default_path(mut self, route: impl Into) -> Self { self.default_route = Some(route.into()); self } + /// Set the history implementation pub fn history(mut self, history: H) -> Self { self.history_kind = Some(history); self } + /// Set the error UI /// Call this *before* you call `.async_route()`, otherwise the error UI will not be used in async routes. pub fn error_ui( mut self, @@ -94,12 +104,38 @@ impl RouterBuilder { self } + /// Set the loading UI /// Call this *before* you call `.async_route()`, otherwise the loading UI will not be used in async routes. pub fn loading_ui(mut self, f: impl Fn(&mut egui::Ui, &State) + 'static + Send + Sync) -> Self { self.loading_ui = Arc::new(Box::new(f)); self } + /// Add a route. Check the [matchit] documentation for information about the route syntax. + /// The handler will be called with [crate::Request] and should return a [Route]. + /// + /// # Example + /// ```rust + /// # use egui::Ui; + /// # use egui_router::{EguiRouter, HandlerError, HandlerResult, Request, Route}; + /// + /// pub fn my_handler(_req: Request) -> impl Route { + /// |ui: &mut Ui, _: &mut ()| { + /// ui.label("Hello, world!"); + /// } + /// } + /// + /// pub fn my_fallible_handler(req: Request) -> HandlerResult { + /// let post = req.params.get("post").ok_or_else(|| HandlerError::NotFound)?.to_owned(); + /// Ok(move |ui: &mut Ui, _: &mut ()| { + /// ui.label(format!("Post: {}", post)); + /// }) + /// } + /// + /// let router: EguiRouter<()> = EguiRouter::builder() + /// .route("/", my_handler) + /// .route("/:post", my_fallible_handler) + /// .build(&mut ()); pub fn route + 'static>( mut self, route: &str, @@ -114,14 +150,39 @@ impl RouterBuilder { self } + /// Add an async route. Check the [matchit] documentation for information about the route syntax. + /// The handler will be called with [crate::OwnedRequest] and should return a [Route]. + /// + /// # Example + /// ```rust + /// # use egui::Ui; + /// # use egui_router::{EguiRouter, HandlerError, HandlerResult, Request, Route}; + /// # #[cfg(feature = "async")] + /// async fn my_handler(_req: egui_router::OwnedRequest) -> HandlerResult { + /// Ok(move |ui: &mut Ui, _: &mut ()| { + /// ui.label("Hello, world!"); + /// }) + /// } + /// + /// # #[cfg(feature = "async")] + /// async fn my_fallible_handler(req: egui_router::OwnedRequest) -> HandlerResult { + /// let post = req.params.get("post").ok_or_else(|| HandlerError::NotFound)?.to_owned(); + /// Ok(move |ui: &mut Ui, _: &mut ()| { + /// ui.label(format!("Post: {}", post)); + /// }) + /// } + /// + /// # #[cfg(feature = "async")] + /// let router: EguiRouter<()> = EguiRouter::builder() + /// .async_route("/", my_handler) + /// .async_route("/:post", my_fallible_handler) + /// .build(&mut ()); + /// + /// #[cfg(feature = "async")] pub fn async_route(mut self, route: &str, handler: Han) -> Self where - Han: crate::handler::async_impl::AsyncMakeHandler - + 'static - + Clone - + Send - + Sync, + Han: crate::handler::AsyncMakeHandler + 'static + Clone + Send + Sync, State: Clone + 'static + Send + Sync, { let loading_ui = self.loading_ui.clone(); @@ -162,6 +223,7 @@ impl RouterBuilder { self } + /// Add a redirect route. Whenever this route matches, it'll redirect to the route you specified. pub fn route_redirect(mut self, route: &str, redirect: impl Into) -> Self { self.router .insert(route, RouteKind::Redirect(redirect.into())) @@ -169,6 +231,7 @@ impl RouterBuilder { self } + /// Build the router pub fn build(self, state: &mut State) -> EguiRouter { EguiRouter::from_builder(self, state) } diff --git a/crates/egui_router/src/transition.rs b/crates/egui_router/src/transition.rs index c8be4ae..f5bf99d 100644 --- a/crates/egui_router/src/transition.rs +++ b/crates/egui_router/src/transition.rs @@ -1,11 +1,16 @@ use crate::TransitionConfig; use egui::{Id, Ui, Vec2}; +/// Trait for declaring a transition. +/// Prefer [ComposableTransitionTrait] unless you need to create a new ui to apply the transition. pub trait TransitionTrait { + /// Create a child ui with the transition applied fn create_child_ui(&self, ui: &mut Ui, t: f32, with_id: Id) -> Ui; } +/// Trait for declaring a composable transition. pub trait ComposableTransitionTrait { + /// Apply the transition to the ui fn apply(&self, ui: &mut Ui, t: f32); } @@ -17,11 +22,16 @@ impl TransitionTrait for T { } } +/// Enum containing all possible transitions #[derive(Debug, Clone)] pub enum Transition { + /// Simple fade transition Fade(FadeTransition), + /// No transition NoTransition(NoTransition), + /// Slide transition Slide(SlideTransition), + /// Combined slide and fade transitions SlideFade(SlideFadeTransition), } @@ -38,17 +48,22 @@ impl TransitionTrait for Transition { } } +/// Simple fade transition #[derive(Debug, Clone)] pub struct FadeTransition; +/// No transition #[derive(Debug, Clone)] pub struct NoTransition; +/// Slide transition #[derive(Debug, Clone)] pub struct SlideTransition { + /// Amount and direction to slide. Default is [Vec2::X] (so it will slide in from the right) pub amount: Vec2, } +/// Combining slide and fade transitions #[derive(Debug, Clone)] pub struct SlideFadeTransition(pub SlideTransition, pub FadeTransition); @@ -59,6 +74,7 @@ impl Default for SlideTransition { } impl SlideTransition { + /// Create a new slide transition. Default is [Vec2::X] (so it will slide in from the right) pub fn new(amount: Vec2) -> Self { Self { amount } } @@ -116,12 +132,27 @@ impl From for Transition { } } +/// Configuration for a transition, containing the in and out transitions +/// The in transition is the transition that will be applied to the page that is being navigated to +/// The out transition is the transition that will be applied to the page that is being navigated from pub enum TransitionType { - Forward { _in: Transition, out: Transition }, - Backward { _in: Transition, out: Transition }, + /// Forward transition + Forward { + /// Will be applied to the page that is being navigated to + _in: Transition, + /// Will be applied to the page that is being navigated from + out: Transition, + }, + /// Backward transition (will play the out transition in reverse) + Backward { + /// Will be applied to the page that is being navigated to + _in: Transition, + /// Will be applied to the page that is being navigated from + out: Transition, + }, } -pub struct ActiveTransition { +pub(crate) struct ActiveTransition { duration: Option, progress: f32, easing: fn(f32) -> f32, @@ -130,7 +161,7 @@ pub struct ActiveTransition { backward: bool, } -pub enum ActiveTransitionResult { +pub(crate) enum ActiveTransitionResult { Done, Continue, }