Skip to main content

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;