logicaffeine_web/ui/pages/studio_opening.rs
1//! What the Studio shows before anything has loaded.
2//!
3//! The Studio has no mode switch: the open file is the mode. Its first frame — the page the site
4//! prerenders, and the first one the app paints — must already know the file, or it paints one
5//! mode and flips to another once the file system answers. Every shipped example is compiled into
6//! the site, so the route alone decides what the Studio opens on, the same on the server and in the
7//! browser ([`StudioOpening::for_route`]). Only a reader's own file waits for the file system, and
8//! [`StudioOpening::arrival`] is the one rule for what the file system's answer may change.
9
10use crate::ui::examples::{ExampleSpec, ALL_CODE_EXAMPLES};
11use crate::ui::pages::studio_files::{example_at, examples_tree};
12use crate::ui::state::StudioMode;
13
14/// The folder whose first file, in the file browser's order, a bare `/studio` opens.
15const FIRST_FOLDER: &str = "/examples/code";
16
17const _: () = assert!(!ALL_CODE_EXAMPLES.is_empty(), "a bare /studio opens a Code example, so one must ship");
18
19/// The file the Studio opens with, the mode it opens in, and its text when the site carries it.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct StudioOpening {
22 /// The file's VFS path, with its leading slash.
23 pub path: String,
24 /// The mode the file opens in.
25 pub mode: StudioMode,
26 /// The file's text when the site carries it, as it does every shipped example. A reader's own
27 /// file has none until the file system reads it.
28 pub text: Option<&'static str>,
29}
30
31/// What the file system's answer for the opening's file changes.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub enum Arrival {
34 /// Nothing: the editor already shows the file, or the reader has typed since the Studio opened.
35 Keep,
36 /// Open the file with the text the file system holds.
37 Open(String),
38 /// The file system has no such file: open this one instead.
39 Fallback(StudioOpening),
40}
41
42impl StudioOpening {
43 /// What the Studio opens on for the route's `file`, a VFS path with or without its leading
44 /// slash: a shipped example in its own mode with its text; a reader's file in the mode its path
45 /// names, its text left to the file system; and with no file, the first example.
46 pub fn for_route(file: Option<&str>) -> Self {
47 match file.filter(|file| !file.is_empty()) {
48 None => Self::first_example(),
49 Some(file) if file.starts_with('/') => Self::at(file),
50 Some(file) => Self::at(&format!("/{file}")),
51 }
52 }
53
54 /// A bare `/studio`: the first Code example, in the order the file browser shows the examples.
55 pub fn first_example() -> Self {
56 let first = examples_tree().first_file_under(FIRST_FOLDER).and_then(example_at);
57 Self::of(first.unwrap_or(&ALL_CODE_EXAMPLES[0]))
58 }
59
60 fn at(path: &str) -> Self {
61 match example_at(path) {
62 Some(spec) => Self::of(spec),
63 // A file that names no mode opens in the one a bare /studio opens in.
64 None => Self {
65 path: path.to_string(),
66 mode: StudioMode::for_path(path).unwrap_or_else(|| Self::first_example().mode),
67 text: None,
68 },
69 }
70 }
71
72 fn of(spec: &'static ExampleSpec) -> Self {
73 Self {
74 path: spec.vfs_path.to_string(),
75 // Every shipped example names its mode (`every_example_opens_in_its_declared_mode`).
76 mode: StudioMode::for_file(spec.vfs_path, spec.source).unwrap_or(StudioMode::Code),
77 text: Some(spec.source),
78 }
79 }
80
81 /// The text `mode`'s editor starts with: the opening's text in its own mode's editor — Code and
82 /// Web share one — and nothing in the others.
83 pub fn editor_text(&self, mode: StudioMode) -> String {
84 let shared = |m: StudioMode| matches!(m, StudioMode::Code | StudioMode::Web);
85 if mode == self.mode || (shared(mode) && shared(self.mode)) {
86 self.text.unwrap_or_default().to_string()
87 } else {
88 String::new()
89 }
90 }
91
92 /// The file the Studio shows open from its first frame: the opening's, when the site carries its
93 /// text. A reader's own file is open only once the file system has read it.
94 pub fn current_file(&self) -> Option<String> {
95 self.text.map(|_| self.path.clone())
96 }
97
98 /// What the file system's answer changes: `open` is the file open now, `editor` what the
99 /// opening's editor holds, and `read` the file's text, `None` when the file system has no such
100 /// file.
101 ///
102 /// What the reader does is theirs: once they have opened another file, or the editor holds
103 /// anything but what the Studio opened with, nothing replaces it. Otherwise a reader's file
104 /// opens with its text, an example whose stored copy differs opens with that copy, and a file
105 /// nobody has falls back to the first example. An example already on screen stays as it is,
106 /// stored or not.
107 pub fn arrival(&self, open: Option<&str>, editor: &str, read: Option<String>) -> Arrival {
108 if open != self.current_file().as_deref() || editor != self.text.unwrap_or_default() {
109 return Arrival::Keep;
110 }
111 match read {
112 Some(text) if self.text == Some(text.as_str()) => Arrival::Keep,
113 Some(text) => Arrival::Open(text),
114 None if self.text.is_some() => Arrival::Keep,
115 None => Arrival::Fallback(Self::first_example()),
116 }
117 }
118}
119
120#[cfg(test)]
121#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/pages/studio_opening/tests.rs"]
122mod tests;