From 12d02ba99c1881032feaeb079d60181d0f066945 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 02:20:50 -0700 Subject: [PATCH] fix: the sidebar offers Options everywhere and no New iCloud Notebook The desktop sidebar's footer now reads New Notebook, Open Existing and Options, as the web build's does; macOS left Options to the application menu. New iCloud Notebook goes from the footer and the welcome page with its naming dialog: New Notebook's panel already opens in Snowbound's iCloud Drive folder, and a notebook saved there appears on every Mac by itself. iOS already offers one New Notebook that creates in iCloud Drive where it is on. Assisted-by: claude-opus-5.5 --- crates/snowbound/src/main.rs | 7 -- crates/snowbound/src/manage.rs | 138 +------------------------------- crates/snowbound/src/sidebar.rs | 94 ++++++++-------------- tools/canvas/README.md | 2 +- 4 files changed, 36 insertions(+), 205 deletions(-) diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index 4e3422f147b058c42b23350db3705ce5a8a0f251..8b1d9581ba7be494df27d5a7f7c89f11cd8c5e74 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -653,8 +653,6 @@ enum Command { OpenFromServer(Option), /// Asks where to keep a new notebook and creates it. NewNotebook, - /// Asks for a name and creates a notebook at the top of the app's iCloud Drive folder. - NewICloudNotebook, /// Opens the Snowbound Guide from the user's documents, copying it there first. OpenGuide, /// Adds Snowbound to the app menu. @@ -801,8 +799,6 @@ struct State { peers: Option, /// Open Notebook from Server while it is open. server: Option, - /// New iCloud Notebook while it is open. - new_icloud: Option, /// Notebooks in iCloud Drive a thread is reading, or following the download of, by /// location. icloud_reading: HashSet, @@ -1178,7 +1174,6 @@ impl State { #[cfg(feature = "live")] peers: None, server: None, - new_icloud: None, icloud_reading: HashSet::new(), tags: stored .tags @@ -1347,7 +1342,6 @@ impl State { self.themes_dialog(); self.print_dialog(); self.server_dialog(); - self.new_icloud_dialog(); self.link_dialog()?; self.properties_dialog(); self.symbols_dialog(); @@ -3221,7 +3215,6 @@ impl State { ), Command::OpenFromServer(location) => self.open_server(location.as_deref()), Command::NewNotebook => self.new_notebook(), - Command::NewICloudNotebook => self.new_icloud_notebook(), Command::OpenGuide => self.open_guide()?, #[cfg(target_os = "linux")] Command::Install => desktop::install(), diff --git a/crates/snowbound/src/manage.rs b/crates/snowbound/src/manage.rs index d596d1a6ac0512cc2f1e20b299ddcc9205b013f6..bccb6087d49318d55210df6d7845a9420db80480 100644 --- a/crates/snowbound/src/manage.rs +++ b/crates/snowbound/src/manage.rs @@ -65,20 +65,6 @@ pub enum Structure { }, } -/// New iCloud Notebook while it is open: the name typed, and why the last was refused. -pub struct Naming { - name: String, - problem: Option, -} - -fn naming() -> ui::Id { - ui::Id::ROOT.child("new icloud notebook") -} - -fn naming_field() -> ui::Id { - naming().child("name") -} - /// Whether the notebook at `location` is a folder at the top of the app's iCloud Drive folder, /// which the sidebar lists as long as it is there. pub fn in_icloud_folder(location: &str) -> bool { @@ -260,7 +246,7 @@ impl State { } /// Asks where to create a notebook and creates it, as File, New does in OneNote; the - /// panel starts in iCloud Drive, as Notes keeps notes there. + /// panel starts in Snowbound's iCloud Drive folder, whose notebooks every Mac lists. pub(crate) fn new_notebook(&mut self) { let icloud = crate::icloud::folder().or_else(crate::icloud::drive); let reply = self.reply(|state, root| state.create_notebook(root)); @@ -273,128 +259,6 @@ impl State { ); } - /// New iCloud Notebook: asks for a name, the first free of "iCloud Notebook", "iCloud - /// Notebook 2"…, for a notebook at the top of the app's iCloud Drive folder. - pub(crate) fn new_icloud_notebook(&mut self) { - let taken: Vec = self - .notebooks - .iter() - .map(|library| library.name.clone()) - .collect(); - self.new_icloud = Some(Naming { - name: unused(&taken, "iCloud Notebook", false), - problem: None, - }); - self.ui.open_popup(naming()); - self.ui.focus_all(naming_field()); - } - - /// Builds New iCloud Notebook while it is open. - pub(crate) fn new_icloud_dialog(&mut self) { - use ui::{Anchor, Axis, Spec, children, fill, px}; - let Some(dialog) = &mut self.new_icloud else { - return; - }; - if !self.ui.popup_open(naming()) { - self.new_icloud = None; - return; - } - let ui = &mut self.ui; - let theme = ui.theme.clone(); - let row = theme.font_size * 2.0; - let entered = - ui::popup::navigation(ui, &[naming_field()], &[winit::keyboard::NamedKey::Enter]) - .contains(&winit::keyboard::NamedKey::Enter); - ui.open_as( - naming(), - Spec { - axis: Axis::Y, - size: [px(320.0), children()], - fill: Some(theme.popup), - border: Some(theme.chip), - shadow: Some(theme.shadow), - radius: 8.0, - pad: [16.0, 12.0], - gap: 6.0, - anchor: Some(Anchor::Dialog), - role: Some(accesskit::Role::Dialog), - ..Spec::default() - }, - ); - if let Some(node) = ui.access(naming()) { - node.set_label("New iCloud Notebook"); - } - ui.leaf( - "title", - Spec { - size: [fill(), px(row)], - text: Some("New iCloud Notebook"), - bold: true, - role: Some(accesskit::Role::Heading), - ..Spec::default() - }, - ); - ui::text_field( - ui, - naming_field(), - &mut dialog.name, - "", - Spec { - size: [fill(), px(row)], - fill: Some(theme.base), - border: Some(theme.accent), - radius: 4.0, - pad: [6.0, 0.0], - ..Spec::default() - }, - ); - crate::name(ui, naming_field(), "Name"); - if let Some(problem) = &dialog.problem { - ui.leaf( - "problem", - Spec { - size: [fill(), ui::fit()], - text: Some(problem), - overflow: ui::Overflow::Wrap, - pad: [0.0, 4.0], - role: Some(accesskit::Role::Status), - ..Spec::default() - }, - ); - } - crate::buttons(ui); - let cancel = ui::button(ui, "cancel", "Cancel").clicked; - let create = ui::button(ui, "create", "Create").clicked || entered; - ui.close(); - ui.close(); - if cancel { - self.ui.close_popup(naming()); - self.ui.set_focus(Some(crate::page())); - self.new_icloud = None; - } else if create { - let name = dialog.name.trim().to_owned(); - let folder = crate::icloud::folder(); - dialog.problem = match &folder { - None => Some("Turn on iCloud Drive in System Settings.".into()), - Some(_) if name.is_empty() => Some("Enter a name.".into()), - Some(_) if name.starts_with('.') || name.contains(['/', ':']) => { - Some("Use a name without “/”, “:” or a leading “.”.".into()) - } - Some(folder) if folder.join(&name).exists() => Some(format!( - "“{name}” is already in iCloud Drive. Choose another name." - )), - Some(_) => None, - }; - if let (None, Some(folder)) = (&dialog.problem, folder) { - self.ui.close_popup(naming()); - self.new_icloud = None; - if let Err(error) = self.create_notebook(folder.join(name)) { - platform::alert("Couldn't create the notebook", &error.to_string()); - } - } - } - } - /// Lists the folders at the top of the app's iCloud Drive folder in the sidebar, each a /// notebook, as they come and go, and brings down what iCloud Drive keeps elsewhere of /// every notebook in it. diff --git a/crates/snowbound/src/sidebar.rs b/crates/snowbound/src/sidebar.rs index 3ca4b59288b9c485a3921f7167b5b2794e972955..53d1303651e0fa6c2f82f98cb7d7ee5bd5404ab0 100644 --- a/crates/snowbound/src/sidebar.rs +++ b/crates/snowbound/src/sidebar.rs @@ -31,11 +31,9 @@ pub enum Action { /// Folds or unfolds a notebook's or section group's rows, by `Library::key`. Fold(String), NewNotebook, - NewICloudNotebook, OpenNotebook, /// Signs in again to the notebook opened from its server at this location. SignIn(String), - /// Opens the Options dialog, which macOS reaches from the application menu instead. Options, /// A row's context menu, opened here. Menu(Target, [f32; 2]), @@ -251,23 +249,11 @@ fn sidebar( ..Spec::default() }, ); - let options = ("options", art::OPTIONS, "Options", Action::Options); - let icloud = crate::icloud::folder().map(|_| { - ( - "new icloud", - art::ICLOUD, - "New iCloud Notebook", - Action::NewICloudNotebook, - ) - }); - for (part, icon, label, chosen) in icloud - .into_iter() - .chain([ - ("new", art::PLUS, "New Notebook", Action::NewNotebook), - ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook), - ]) - .chain((!cfg!(target_os = "macos")).then_some(options)) - { + for (part, icon, label, chosen) in [ + ("new", art::PLUS, "New Notebook", Action::NewNotebook), + ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook), + ("options", art::OPTIONS, "Options", Action::Options), + ] { let (row, _) = tree_row( ui, tree, @@ -815,9 +801,6 @@ impl crate::State { } Some(Action::Renamed(keep)) => self.finish_renaming(keep), Some(Action::NewNotebook) => self.commands.push(crate::Command::NewNotebook), - Some(Action::NewICloudNotebook) => { - self.commands.push(crate::Command::NewICloudNotebook) - } Some(Action::OpenNotebook) => self.commands.push(crate::Command::OpenNotebook), Some(Action::SignIn(location)) => self .commands @@ -1174,44 +1157,35 @@ impl crate::State { theme.base, "No notebooks open", &servers, - crate::icloud::folder() - .map(|_| { - ( - "icloud", - art::ICLOUD, - "New iCloud Notebook", - crate::Command::NewICloudNotebook, - ) - }) - .into_iter() - .chain([ - ( - "new", - art::PLUS, - "New Notebook", - crate::Command::NewNotebook, - ), - ( - "open", - art::NOTEBOOK, - "Open Existing", - crate::Command::OpenNotebook, - ), - ( - "server", - art::SERVER, - "Open Notebook from Server…", - crate::Command::OpenFromServer(None), - ), - ]) - .chain(crate::guide::OFFERED.then_some(( - "guide", - art::PAGE, - "Open the Snowbound Guide", - crate::Command::OpenGuide, - ))) - .chain(install) - .collect(), + [ + ( + "new", + art::PLUS, + "New Notebook", + crate::Command::NewNotebook, + ), + ( + "open", + art::NOTEBOOK, + "Open Existing", + crate::Command::OpenNotebook, + ), + ( + "server", + art::SERVER, + "Open Notebook from Server…", + crate::Command::OpenFromServer(None), + ), + ] + .into_iter() + .chain(crate::guide::OFFERED.then_some(( + "guide", + art::PAGE, + "Open the Snowbound Guide", + crate::Command::OpenGuide, + ))) + .chain(install) + .collect(), ); } diff --git a/tools/canvas/README.md b/tools/canvas/README.md index 9771e100366d91bb04b81b5a877a7d016fe90b1c..d13295a5d5992db93c6fa64354b3236bfe5c77f7 100644 --- a/tools/canvas/README.md +++ b/tools/canvas/README.md @@ -50,7 +50,7 @@ snowbound title bar · toolbar · section frame · page tabs · the page as a `--notebook FOLDER` opens a notebook laid out as OneNote lays one out. The title bar shows the application's icon and the notebook's name; below it two rows of toolbar buttons follow the Home ribbon's Basic Text group, the first nine tags and zoom (only zoom acts yet). Readable top-level sections are tabs in the notebook's order, each leaning over the next at 45° with a faint highlight along its top, and the open one on top, casting a soft shadow and framing the page and the page tabs on the right in its colour, which shades slightly down the window; a tab that opens rises to meet the frame as its outline fades in. The frame's top corners and the page's corners are rounded concentric with the window's. The open page's tab is the page's colour and joins it with rounded inside corners; subpages are indented. The interface follows the system's light or dark appearance, and in the dark one the page's paper darkens and text left at OneNote's automatic colour turns light (except on a highlight), resolved when painting so nothing re-lays out; explicit colours, highlights, pictures and ink keep theirs, except that the background art of OneNote 2010's page templates, recognised by the picture's hash, paints from bundled vector recreations drawn for the paper. Sections take their stored colour's hue at a saturation and lightness chosen for each appearance. The section's colour is also the interface accent, easing to the next section's when another opens. The search box, New Page and the page-tab toggle sit above the page tabs. Command-F searches the page titles; Enter opens the first match and Escape clears it. A page that also changed on another computer shows Keep mine / Keep theirs above it. `--section FILE TITLE` opens one section the same way at that page. Section files must be regular files; discovery rejects symbolic links. -Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings… (⌘,; Options in the sidebar and Ctrl+, on Linux) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`). +Launched without a file, the app reopens the notebooks open when it last closed, at the one shown last, or shows "No notebooks open" with New Notebook and Open Existing. Settings (the open notebooks, the sidebar, recent fonts and the user name edits are stored under, which Settings… (⌘,, Ctrl+, on Linux, or Options in the sidebar) changes and which starts as the account's full name) are JSON in `~/Library/Application Support/Snowbound/settings.json` or `$XDG_CONFIG_HOME/snowbound`; `--settings FILE` uses another file, `--notebook FOLDER` adds a notebook to them, and `--screenshot` never writes them. Open Existing takes a notebook folder, its `Open Notebook.onetoc2` or a section file, which opens in the notebook its folders make up. A notebook on a mounted SMB share opens through the embedded SMB client, signed in with the password the system keychain holds, and through the mount only when that fails. The notebook button left of the section tabs opens OneNote 2010's navigation bar: the open notebooks with their sections and section groups, the open section marked; dragging a row reorders a folder or, onto a group, moves into it, and right-clicking a row, a section tab or a page tab offers OneNote's commands (Rename, Delete to the notebook's recycle bin, New Section, New Section Group, Close This Notebook; Delete, New Page, Make Subpage, Promote Subpage). Page tabs drag to reorder. The `+` above the page tabs adds a page at the end of the section, titled with its date and time as OneNote titles one, and puts the caret in its title; until its body is typed in, drawn on or clicked, templates offer our recreations of OneNote 2010's art (Ivy first), its page colours and Informal Meeting Notes' content, written as ops (`corpus/notebook-management`). As in OneNote 2010, Notebook Recycle Bin (File menu, a notebook's context menu, or the palette) shows the notebook's recycle bin as its own row of tabs, read-only under a bar: a page's context menu restores it to a section, keeping its identity, copies it out, or deletes it for good, a section's restores it to a folder, and Empty Recycle Bin deletes what it holds (`corpus/recycle-bin-view`). Pages another author changed since they were viewed are bold in the page list, and so are their section's tab and row and the groups and notebook holding them, until left or marked read (Mark as Read, Ctrl+Q off macOS; Mark Notebook as Read; Next Unread; Show Unread Changes in This Notebook); what was read is kept in the cache folder's `read.json`, as OneNote keeps it in its cache. Save As (⇧⌘S) writes the page or section as a OneNote section, the notebook as a OneNote package, or any of them as a PDF; opening a `.onepkg` asks, as Unpack Notebook does, for its name, colour and folder (`corpus/notebook-package`). -- 2.54.0