Skip to main content

app_ui/
lib.rs

1//! `app-ui` — the Leptos CSR app served by Trunk into the Tauri webview.
2//!
3//! Composes the workshopped components from
4//! [`ui_storybook::components`] into the recorder's shell surface:
5//!
6//! - [`RecordingToolbar`](ui_storybook::components::RecordingToolbar) at top.
7//! - [`DropZone`](ui_storybook::components::DropZone) when no recording is
8//!   loaded.
9//! - [`PlayerControls`](ui_storybook::components::PlayerControls) +
10//!   placeholder preview when a recording is loaded.
11//! - [`StatusBar`](ui_storybook::components::StatusBar) at the bottom.
12//!
13//! As of M-PLAY.2, the shell drives the screen-app player via Tauri IPC:
14//! the file-drop event opens the file, the transport buttons toggle
15//! play/pause, and a pushed `player-status` event keeps the UI in sync
16//! with the Rust-side player. See [`player_ipc`] for the JS-bridge
17//! `extern` declarations and the [`player_ipc::PlayerStatus`] mirror.
18
19// The Recorder surface composes 4 picker components (Camera + Mic +
20// SystemAudio + Screen) plus the preview canvas + diagnostics overlay
21// + bubble toggles, all returning deeply-nested Leptos `view!{}`
22// types. Type resolution overflows the default 128-frame recursion
23// limit during `cargo test --no-run` (which monomorphises the
24// nested `IntoAny::into_any::resolve<...>` futures). 256 is the
25// compiler's suggested bump and has plenty of headroom for the next
26// few picker additions before we'd need to revisit.
27#![recursion_limit = "256"]
28#![allow(
29    clippy::must_use_candidate,
30    clippy::needless_pass_by_value,
31    reason = "Leptos `#[component]` macro rewrites these patterns; lints fire on generated code"
32)]
33
34pub mod app;
35pub mod bubble;
36pub mod bubble_ipc;
37#[cfg(feature = "tray-appshell-preview")]
38pub mod dev_appshell;
39pub mod player_ipc;
40
41use wasm_bindgen::prelude::*;
42
43/// Trunk entry point — installs panic hooks and mounts the app to `<body>`.
44///
45/// **URL-based view selection (M-TRAY.3 / AUT-252, M-BUBBLE.0 / AUT-273):**
46/// the dispatch routes on the page's query string via
47/// [`routing::parse_mount_point`]:
48///
49/// * `?surface=<recorder|library|editor|cursor|prefs>` → full
50///   ui-storybook `AppShell` rooted at the requested surface (the
51///   tray-launched main window).
52/// * `?mount=bubble` → the borderless `<BubbleRoot />` component for
53///   the webcam-bubble window.
54/// * Otherwise → existing M-INT.1 drop-zone shell (`<App />`) — keeps
55///   `trunk serve` browser flow working for one-off Leptos iteration.
56///
57/// **`AppShell` CSR preview (M-TRAY.1 / AUT-250):** when built with
58/// the `tray-appshell-preview` Cargo feature, `mount_default` (the
59/// no-query branch) mounts [`dev_appshell::DevAppShellPreview`]
60/// instead of `<App />`. This stays for now alongside the
61/// query-routed path so the M-TRAY.1 audit smoke recipe
62/// (`just dev-appshell`) keeps working — production builds without
63/// the feature are unaffected.
64#[wasm_bindgen(start)]
65pub fn run() {
66    console_error_panic_hook::set_once();
67    match mount_point_from_query() {
68        routing::MountPoint::AppShell(section) => {
69            leptos::mount::mount_to_body(move || {
70                app_shell_mount::AppShellRoot(app_shell_mount::AppShellRootProps {
71                    initial: section,
72                })
73            });
74        }
75        routing::MountPoint::Bubble => {
76            leptos::mount::mount_to_body(bubble::BubbleRoot);
77        }
78        routing::MountPoint::DropZone => mount_default(),
79    }
80}
81
82/// Pull the live page URL's query string, parse it via
83/// [`routing::parse_mount_point`]. Falls back to
84/// [`routing::MountPoint::DropZone`] outside a browser so the calling
85/// site's match arm has a sensible default.
86#[must_use]
87pub fn mount_point_from_query() -> routing::MountPoint {
88    let Some(window) = web_sys::window() else {
89        return routing::MountPoint::DropZone;
90    };
91    let Ok(search) = window.location().search() else {
92        return routing::MountPoint::DropZone;
93    };
94    routing::parse_mount_point(&search)
95}
96
97#[cfg(not(feature = "tray-appshell-preview"))]
98fn mount_default() {
99    leptos::mount::mount_to_body(app::App);
100}
101
102#[cfg(feature = "tray-appshell-preview")]
103fn mount_default() {
104    leptos::mount::mount_to_body(dev_appshell::DevAppShellPreview);
105}
106
107/// Parse the live page URL's `?surface=` query — thin wrapper around
108/// [`routing::parse_surface`] that pulls the string from
109/// `window.location.search()`. Returns `None` outside a browser
110/// (so the calling site falls through to the default mount path).
111#[must_use]
112pub fn parse_surface_from_query() -> Option<ui_storybook::components::shell::AppSection> {
113    let search = web_sys::window()?.location().search().ok()?;
114    routing::parse_surface(&search)
115}
116
117/// Convert an [`AppSection`](ui_storybook::components::shell::AppSection)
118/// to its URL slug — thin wrapper around [`routing::surface_slug`].
119#[must_use]
120pub fn surface_to_query(section: ui_storybook::components::shell::AppSection) -> &'static str {
121    routing::surface_slug(section)
122}
123
124mod app_shell_mount;
125pub mod camera_ipc;
126pub mod camera_picker;
127pub mod camera_preview;
128pub mod clip_inspector;
129pub mod cursor_inspector;
130pub mod editor_edits;
131pub mod editor_ipc;
132pub mod editor_preview_canvas;
133pub mod editor_surface;
134pub mod export_bar;
135pub mod filmstrip;
136pub mod framing_inspector;
137pub mod mic_ipc;
138pub mod mic_picker;
139pub mod recorder_page;
140pub mod recording_ipc;
141pub mod recordings_library;
142pub mod routing;
143pub mod screen_ipc;
144pub mod screen_picker;
145pub mod screen_preview_canvas;
146pub mod settings_ipc;
147pub mod style_inspector;
148pub mod system_audio_ipc;
149pub mod system_audio_picker;
150pub mod timeline_view;
151pub mod waveform;
152pub mod zoom_dopesheet;
153pub mod zoom_lane;