Skip to main content

draw_webapp/
lib.rs

1use axum::Router;
2use axum::extract::State;
3use axum::http::{HeaderMap, HeaderValue, header};
4use axum::response::{Html, IntoResponse};
5use axum::routing::{delete, get, post};
6use std::net::SocketAddr;
7use std::sync::Arc;
8
9mod api;
10
11const INDEX_HTML: &str = include_str!("../frontend/index.html");
12const STYLE_CSS: &str = include_str!("../frontend/style.css");
13const APP_JS: &str = include_str!("../frontend/app.js");
14const CANVAS_JS: &str = include_str!("../frontend/canvas.js");
15const INTERACTIONS_JS: &str = include_str!("../frontend/interactions.js");
16const API_JS: &str = include_str!("../frontend/api.js");
17const THEME_JS: &str = include_str!("../frontend/theme.js");
18const FAVICON: &[u8] = include_bytes!("../frontend/favicon.png");
19const WASM_GLUE_JS: &str = include_str!("../frontend/wasm_glue.js");
20const WASM_BG: &[u8] = include_bytes!("../frontend/draw_wasm_bg.wasm");
21
22const PORT: u16 = 1213;
23
24struct AppState {
25    open_id: Option<String>,
26}
27
28pub fn create_router(open_id: Option<String>) -> Router {
29    let state = Arc::new(AppState { open_id });
30
31    Router::new()
32        .route("/", get(index))
33        .route("/style.css", get(serve_css))
34        .route("/favicon.png", get(serve_favicon))
35        .route("/theme.js", get(serve_theme_js))
36        .route("/app.js", get(serve_app_js))
37        .route("/canvas.js", get(serve_canvas_js))
38        .route("/interactions.js", get(serve_interactions_js))
39        .route("/api.js", get(serve_api_js))
40        .route("/wasm_glue.js", get(serve_wasm_glue_js))
41        .route("/draw_wasm_bg.wasm", get(serve_wasm_bg))
42        .route("/api/drawings", get(api::list_drawings))
43        .route("/api/drawings", post(api::save_drawing))
44        .route("/api/drawings/{id}", get(api::load_drawing))
45        .route("/api/drawings/{id}", delete(api::delete_drawing))
46        .route("/api/export/svg", post(api::export_svg))
47        .route("/api/export/png", post(api::export_png))
48        .with_state(state)
49}
50
51async fn index(State(state): State<Arc<AppState>>) -> Html<String> {
52    let open_script = if let Some(id) = &state.open_id {
53        format!(r#"<script>window.__OPEN_DRAWING_ID = "{id}";</script>"#)
54    } else {
55        String::new()
56    };
57    Html(INDEX_HTML.replace("<!-- OPEN_SCRIPT -->", &open_script))
58}
59
60async fn serve_css() -> impl IntoResponse {
61    let mut headers = HeaderMap::new();
62    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/css"));
63    (headers, STYLE_CSS)
64}
65
66async fn serve_favicon() -> impl IntoResponse {
67    let mut headers = HeaderMap::new();
68    headers.insert(header::CONTENT_TYPE, HeaderValue::from_static("image/png"));
69    (headers, FAVICON)
70}
71async fn serve_theme_js() -> impl IntoResponse {
72    js_response(THEME_JS)
73}
74async fn serve_app_js() -> impl IntoResponse {
75    js_response(APP_JS)
76}
77async fn serve_canvas_js() -> impl IntoResponse {
78    js_response(CANVAS_JS)
79}
80async fn serve_interactions_js() -> impl IntoResponse {
81    js_response(INTERACTIONS_JS)
82}
83async fn serve_api_js() -> impl IntoResponse {
84    js_response(API_JS)
85}
86async fn serve_wasm_glue_js() -> impl IntoResponse {
87    js_response(WASM_GLUE_JS)
88}
89async fn serve_wasm_bg() -> impl IntoResponse {
90    let mut headers = HeaderMap::new();
91    headers.insert(
92        header::CONTENT_TYPE,
93        HeaderValue::from_static("application/wasm"),
94    );
95    (headers, WASM_BG)
96}
97
98fn js_response(content: &'static str) -> impl IntoResponse {
99    let mut headers = HeaderMap::new();
100    headers.insert(
101        header::CONTENT_TYPE,
102        HeaderValue::from_static("application/javascript"),
103    );
104    (headers, content)
105}
106
107pub fn run_webapp(open_id: Option<String>) -> anyhow::Result<()> {
108    let rt = tokio::runtime::Runtime::new()?;
109    rt.block_on(async {
110        let port = PORT;
111        let app = create_router(open_id);
112        let addr = SocketAddr::from(([127, 0, 0, 1], port));
113
114        let listener = tokio::net::TcpListener::bind(addr).await?;
115        let actual_addr = listener.local_addr()?;
116
117        println!("draw webapp: http://localhost:{}", actual_addr.port());
118        let _ = open::that(format!("http://localhost:{}", actual_addr.port()));
119
120        axum::serve(listener, app)
121            .with_graceful_shutdown(async {
122                tokio::signal::ctrl_c().await.ok();
123                println!("\nshutting down...");
124            })
125            .await?;
126        Ok(())
127    })
128}