Skip to main content

blockworx_web/
main.rs

1//! Blockworx in a browser: a Dioxus shell over the same kernel the desktop
2//! stands on.
3//!
4//! The diagram is `Canvas2D`, painted imperatively by [`blockworx_canvas2d`] and
5//! never entering the component tree; the chrome is HTML bound to the model
6//! the same call answers. What is Dioxus-specific is small on purpose: one
7//! `use_hook` holding the shell, one signal holding the chrome, and components
8//! that only read it and raise commands.
9//!
10//! The chrome is `docs/cad-ui-spec.md`'s: three persistent regions and only
11//! three — a docked strip, a docked activity bar with the one panel it opens
12//! floating over the diagram, and a floating toolbar — plus the search
13//! palette, which is summoned rather than persistent.
14
15mod canvas;
16mod chords;
17mod chrome;
18mod control;
19mod diagrams;
20mod editor;
21mod exchange;
22mod frame_rate;
23mod icons;
24mod library;
25mod log;
26mod meter;
27mod mode;
28mod notices;
29mod overlay;
30mod pacing;
31mod palette;
32mod prefs;
33mod settings;
34mod sheet;
35mod shell;
36mod sidebar;
37mod status_line;
38mod tool_cluster;
39mod top_bar;
40
41#[cfg(test)]
42mod snapshots;
43
44use std::cell::RefCell;
45
46use blockworx_store::doc::Viewing;
47use dioxus::prelude::*;
48
49use crate::canvas::Canvas;
50use crate::editor::InPlaceEditor;
51use crate::exchange::FilePick;
52use crate::frame_rate::FrameRateBadge;
53use crate::mode::Prefers;
54use crate::notices::{Notices, Toast};
55use crate::overlay::SelectionOverlay;
56use crate::palette::Palette;
57use crate::prefs::Store;
58use crate::shell::Shell;
59use crate::sidebar::{ActivityBar, SidebarPanel};
60use crate::status_line::StatusLine;
61use crate::tool_cluster::ToolCluster;
62use crate::top_bar::TopBar;
63
64/// Tailwind's output, which `dx` compiles from `tailwind.css` at the crate
65/// root on every build.
66const TAILWIND: Asset = asset!("/assets/tailwind.css");
67
68// What opening the origin left for the first render to stand on. A hand-off
69// through a cell rather than a parameter because `launch` takes a plain
70// function, which can carry nothing — and the open has to have happened
71// before the first frame runs, since a kernel call over a document that is
72// about to be replaced would fit the view to the wrong one.
73thread_local! {
74    static OPENED: RefCell<Option<library::Opened>> = const { RefCell::new(None) };
75}
76
77fn main() {
78    log::install();
79    shell::spawn(async {
80        let opened = library::Library::opening(Store::of(shell::window().as_ref())).await;
81        OPENED.with(|cell| cell.replace(Some(opened)));
82        dioxus::launch(App);
83    });
84}
85
86#[component]
87fn App() -> Element {
88    // Everything that outlives a render, made once: where the preferences are
89    // kept, what they say, what the browser prefers, the faces the in-place
90    // editor types in, and the shell itself with the chrome its priming call
91    // left.
92    let (shell, chrome, store, prefs, prefers) = use_hook(|| {
93        let window = shell::window();
94        let store = Store::of(window.as_ref());
95        let prefs = store.read();
96        let prefers = window.as_ref().map_or_else(Prefers::default, Prefers::of);
97        editor::register_faces();
98        let opened = OPENED
99            .with(|cell| cell.take())
100            .unwrap_or_else(library::Opened::detached);
101        let (shell, chrome) = Shell::opening(&prefs, prefers, opened);
102        (
103            shell,
104            chrome,
105            store,
106            Signal::new(prefs),
107            Signal::new(prefers),
108        )
109    });
110
111    use_hook({
112        let shell = shell.clone();
113        move || canvas::listens(&shell)
114    });
115
116    // `Mode::System` follows the browser for as long as the page is open,
117    // rather than sampling it once at startup.
118    use_hook(|| {
119        if let Some(window) = shell::window() {
120            let mut prefers = prefers;
121            mode::watch(&window, move |told| prefers.set(told));
122        }
123    });
124
125    // One reading of the axis drives both halves: the class the chrome varies
126    // on below, and the palette the diagram is painted in here.
127    use_effect({
128        let shell = shell.clone();
129        move || {
130            let prefs = prefs.read();
131            store.write(&prefs);
132            shell.wears(&prefs, prefers());
133        }
134    });
135
136    let at = prefs.read().luminance(prefers());
137    let field = chrome.read().edit_text.clone();
138    let vantage = chrome.read().vantage;
139    // The one state class the spec asks five regions to vary on at once: the
140    // strip washes amber, the rail goes inert, the diagram desaturates.
141    let viewing = matches!(chrome.read().top_bar.lens.viewing, Viewing::Past(_));
142    rsx! {
143        document::Stylesheet { href: TAILWIND }
144        div {
145            class: "{mode::root_class(at)} group flex h-screen w-screen flex-col \
146                    overflow-hidden bg-white text-zinc-900 dark:bg-zinc-950 dark:text-zinc-100",
147            "data-viewing": if viewing { "true" } else { "false" },
148            TopBar { shell: shell.clone() }
149            div { class: "flex min-h-0 flex-1",
150                ActivityBar { shell: shell.clone() }
151                div { class: "relative min-h-0 min-w-0 flex-1",
152                    Canvas { shell: shell.clone() }
153                    ToolCluster { shell: shell.clone() }
154                    SelectionOverlay { shell: shell.clone() }
155                    if let Some(field) = field {
156                        InPlaceEditor {
157                            key: "{field.id:?}",
158                            shell: shell.clone(),
159                            field,
160                            vantage,
161                            typeface: shell.typeface(),
162                        }
163                    }
164                    Notices { shell: shell.clone() }
165                    Toast { shell: shell.clone() }
166                    StatusLine { shell: shell.clone() }
167                    FrameRateBadge { shell: shell.clone() }
168                    SidebarPanel { shell: shell.clone(), prefs, prefers }
169                }
170            }
171            Palette { shell: shell.clone(), open: shell.palette() }
172            FilePick { shell }
173        }
174    }
175}