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}