egui_router 0.7.0

A SPA router for egui
Documentation
use eframe::NativeOptions;
use egui::{CentralPanel, Color32, Frame, ScrollArea, Ui};
use egui_inbox::{UiInbox, UiInboxSender};
use egui_router::{EguiRouter, HandlerError, HandlerResult, OwnedRequest, Route};

type AppState = UiInboxSender<RouterMessage>;

enum RouterMessage {
    Navigate(String),
    Back,
}

#[tokio::main]
async fn main() -> eframe::Result<()> {
    let mut router: Option<EguiRouter<AppState>> = None;

    let inbox = UiInbox::new();
    let mut sender = inbox.sender();

    eframe::run_ui_native(
        "Router Example",
        NativeOptions::default(),
        move |ui, _frame| {
            ui.ctx().all_styles_mut(|style| {
                style.interaction.selectable_labels = false;
            });
            let router = router.get_or_insert_with(|| {
                EguiRouter::builder()
                    .swipe_back_gesture(true)
                    .error_ui(|ui, state: &AppState, error| {
                        ui.label(format!("Error: {error}"));
                        if ui.button("back").clicked() {
                            state.clone().send(RouterMessage::Back).ok();
                        }
                    })
                    .loading_ui(|ui, _| {
                        ui.label("Loading...");
                        ui.spinner();
                    })
                    .route("/", home)
                    .async_route("/post/{id}", post)
                    .default_path("/")
                    .build(&mut sender)
            });

            inbox.read(ui).for_each(|msg| match msg {
                RouterMessage::Navigate(route) => {
                    router.navigate(&mut sender, route).ok();
                }
                RouterMessage::Back => {
                    router.back().ok();
                }
            });

            CentralPanel::default().show_inside(ui, |ui| {
                router.ui(ui, &mut sender);
            });
        },
    )
}

fn home() -> impl Route<AppState> {
    |ui: &mut Ui, inbox: &mut UiInboxSender<RouterMessage>| {
        background(ui, ui.style().visuals.faint_bg_color, |ui| {
            ui.heading("Home!");

            ui.label("Navigate to post:");

            if ui.link("Post 1").clicked() {
                inbox
                    .send(RouterMessage::Navigate("/post/1".to_string()))
                    .ok();
            }

            if ui.link("Post 2").clicked() {
                inbox
                    .send(RouterMessage::Navigate("/post/2".to_string()))
                    .ok();
            }

            if ui.link("Error Post").clicked() {
                inbox
                    .send(RouterMessage::Navigate("/post/error".to_string()))
                    .ok();
            }
        });
    }
}

async fn post(request: OwnedRequest<AppState>) -> HandlerResult<impl Route<AppState>> {
    let id = request.params.get("id").map(ToOwned::to_owned);

    tokio::time::sleep(std::time::Duration::from_secs(1)).await;

    if id.as_deref() == Some("error") {
        Err(HandlerError::Message("Error Loading Post!".to_string()))?;
    }

    Ok(move |ui: &mut Ui, sender: &mut AppState| {
        background(ui, ui.style().visuals.extreme_bg_color, |ui| {
            ScrollArea::vertical().show(ui, |ui| {
                if let Some(id) = &id {
                    ui.label(format!("Post: {id}"));

                    if ui.button("back").clicked() {
                        sender.send(RouterMessage::Back).ok();
                    }

                    ui.label(include_str!("../../../README.md"));
                } else {
                    ui.label("Post not found");
                    if ui.button("back").clicked() {
                        sender.send(RouterMessage::Back).ok();
                    }
                }
            });
        });
    })
}

fn background(ui: &mut Ui, color: Color32, content: impl FnOnce(&mut Ui)) {
    Frame::NONE.fill(color).inner_margin(16.0).show(ui, |ui| {
        ui.set_width(ui.available_width());
        ui.set_height(ui.available_height());
        content(ui);
    });
}