Skip to main content

blockworx/panels/
file_menu.rs

1//! The File menu: which diagram the session is editing, and where it lives.
2//!
3//! One thing opens here, and it is a diagram — a `.bwx` container, which on
4//! disk is a directory with that suffix. There is no second entry for the
5//! `document.json` beside the log: that file is a *projection*, carrying
6//! neither the log nor the assets, so it is not a diagram anybody can open. It
7//! is still what the CLI will read for a developer.
8//!
9//! The document bar's File section — a cascading dropdown, no dialogs of our
10//! own. Nothing here writes *changes*: a container takes every commit as it
11//! is made, so there is no save action and no UI implies one — and nothing
12//! here refreshes `document.json` either, since the projection keeps itself
13//! fresh. "Save as…" is the one entry that gives a scratch session a file for
14//! the first time.
15//!
16//! Native only: the browser has no container to open.
17
18use std::path::{Path, PathBuf};
19
20use blockworx_store::storage::DocumentRef;
21use egui::{TextWrapMode, Ui};
22
23use crate::file::{FileRequest, SaveScope};
24use crate::tools::commands::Effect;
25use blockworx_store::doc::Viewing;
26
27/// See `preferences_menu::no_wrap`: menu popups shrink-to-fit and proportional
28/// fonts can round a label just past the popup width, wrapping the last glyph.
29fn no_wrap(ui: &mut Ui) {
30    ui.style_mut().wrap_mode = Some(TextWrapMode::Extend);
31}
32
33/// Populate the File button's dropdown. Every door here is the shell's own
34/// effect — a dialog, a container opened — so what a click asks for is one.
35pub fn menu(ui: &mut Ui, recent: &[PathBuf], viewing: Viewing) -> Option<Effect> {
36    no_wrap(ui);
37    let mut action = None;
38    if ui.button("New diagram").clicked() {
39        action = Some(Effect::NewDocument);
40    }
41    if ui.button("Open diagram…").clicked() {
42        action = Some(Effect::PickFile(FileRequest::OpenContainer));
43    }
44    if let Some(picked) = recent_menu(ui, recent) {
45        action = Some(Effect::OpenRecent(DocumentRef::from(picked.as_path())));
46    }
47    ui.separator();
48    // One entry, whose meaning is the rev on the canvas. While the lens is
49    // open it writes the log through *that* rev, so a reader can take the
50    // document as they are looking at it and edit on top of it; at head it
51    // writes the whole log. A second "Save rev as…" would differ only by a
52    // condition the pill overhead is already announcing.
53    let scope = SaveScope::from(viewing);
54    if ui
55        .button("Save as…")
56        .on_hover_text(save_as_hint(scope))
57        .clicked()
58    {
59        action = Some(Effect::PickFile(FileRequest::SaveAsContainer(scope)));
60    }
61    action
62}
63
64/// What Save-as promises, which is not the same sentence in both places it
65/// can be pressed from.
66fn save_as_hint(scope: SaveScope) -> String {
67    match scope {
68        SaveScope::Whole => {
69            "Give this diagram a home on disk; every change lands in it from here on".to_owned()
70        }
71        SaveScope::Through(at) => format!(
72            "Saves the diagram as shown \u{2014} through rev {}",
73            at.get(),
74        ),
75    }
76}
77
78/// The remembered containers, as a submenu. Disabled rather than hidden
79/// while nothing has been opened yet, so the menu keeps its shape.
80fn recent_menu(ui: &mut Ui, recent: &[PathBuf]) -> Option<PathBuf> {
81    let mut picked = None;
82    ui.add_enabled_ui(!recent.is_empty(), |ui| {
83        ui.menu_button("Open recent", |ui| {
84            no_wrap(ui);
85            for path in recent {
86                if ui
87                    .button(entry_label(path))
88                    .on_hover_text(path.display().to_string())
89                    .clicked()
90                {
91                    picked = Some(path.clone());
92                }
93            }
94        });
95    });
96    picked
97}
98
99/// A recent entry's label: the container's own name, since the whole path
100/// would run the menu off the screen. Two containers can share a name, which
101/// is what the hover text is for.
102fn entry_label(path: &Path) -> String {
103    crate::file::container_name(path)
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    /// The hover says what the entry will do, and under the lens that is
111    /// not what it does at the head — the promise names the rev, so a
112    /// reader knows what they are about to get.
113    #[test]
114    fn the_hover_promises_the_rev_that_will_be_written() {
115        assert_eq!(
116            save_as_hint(SaveScope::from(Viewing::Past(
117                blockworx_doc::fixtures::rev(7)
118            ))),
119            "Saves the diagram as shown \u{2014} through rev 7",
120        );
121        assert!(
122            !save_as_hint(SaveScope::from(Viewing::Head)).contains("rev"),
123            "at the present there is no rev to promise",
124        );
125    }
126
127    #[test]
128    fn a_recent_entry_reads_as_the_container_name() {
129        assert_eq!(
130            entry_label(Path::new("/home/ada/work/engine.bwx")),
131            "engine.bwx"
132        );
133    }
134}