authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 14:10:05-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 14:52:40-07:00
log384e5ef14c8d95bb74b186fe57af0cc578fa9276
treebedca9cbba5765e76940e53b2398b50247f6559b
parent0fa3ee68b6f6521e11d14b69f59fbb546c6ad09e
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: a notebook's colour tints its book in the sidebar, on the notebook button and in iOS's list

OneNote 2010's navigation bar draws each notebook's icon in the colour Notebook Properties picks. The notebook glyph's cover now paints in its tint, shaded over it, and every place that draws a particular notebook passes its colour: the sidebar's rows, the notebook button (the notebook shown), the palette, Move's destinations and the sync popup. A notebook without a colour, and Open Existing, keep the glyph's orange. A notebook in iCloud Drive shows its tinted book with a small cloud at the corner instead of the plain cloud. ui's Spec gains `tint`, an icon colour apart from the label's. Notebook Properties' colour menu shows a swatch of each colour. iOS reads the colour with sb_library_color and tints the list's book. Assisted-by: claude-opus-5.5

17 files changed, 127 insertions(+), 36 deletions(-)

apps/ios/Snowbound/Lists.swift+2-1
...@@ -232,7 +232,8 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke...@@ -232,7 +232,8 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
232 case .notebook(let id):232 case .notebook(let id):
233 guard let notebook = notebook(id) else { break }233 guard let notebook = notebook(id) else { break }
234 content.text = notebook.name234 content.text = notebook.name
235 content.image = UIImage(systemName: "book.closed")235 content.image = UIImage(systemName: "book.closed.fill")
236 content.imageProperties.tintColor = notebook.color
236 if case .server(let server) = notebook.source { content.secondaryText = server.host }237 if case .server(let server) = notebook.source { content.secondaryText = server.host }
237 let more = UIButton(type: .system)238 let more = UIButton(type: .system)
238 more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)239 more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
apps/ios/Snowbound/Notebooks.swift+7-1
...@@ -175,6 +175,8 @@ final class Notebook {...@@ -175,6 +175,8 @@ final class Notebook {
175 private(set) var name: String175 private(set) var name: String
176 private(set) var handle: OpaquePointer?176 private(set) var handle: OpaquePointer?
177 private(set) var tabs: [Tab] = []177 private(set) var tabs: [Tab] = []
178 /// The notebook's colour, which tints its book in the list; none where it has none.
179 private(set) var color: UIColor?
178 /// Why the notebook could not be opened, while it cannot.180 /// Why the notebook could not be opened, while it cannot.
179 private(set) var problem: String?181 private(set) var problem: String?
180 /// The Files location being read, held open while the notebook is.182 /// The Files location being read, held open while the notebook is.
...@@ -288,8 +290,12 @@ final class Notebook {...@@ -288,8 +290,12 @@ final class Notebook {
288 return done()290 return done()
289 }291 }
290 let pointer = Int(bitPattern: handle)292 let pointer = Int(bitPattern: handle)
291 background({ decode([Tab].self, sb_library_sections(OpaquePointer(bitPattern: pointer))) }) { [self] tabs in293 background({ () -> ([Tab]?, Int32) in
294 let library = OpaquePointer(bitPattern: pointer)
295 return (decode([Tab].self, sb_library_sections(library)), sb_library_color(library))
296 }) { [self] (tabs, color) in
292 if let tabs { self.tabs = tabs }297 if let tabs { self.tabs = tabs }
298 self.color = color < 0 ? nil : UIColor(rgb: [16, 8, 0].map { UInt8(truncatingIfNeeded: color >> $0) })
293 download()299 download()
294 done()300 done()
295 }301 }
crates/mobile/include/snowbound.h+1
...@@ -29,6 +29,7 @@ bool sb_notebook_create(const char *path, const char *cache, const char *author,...@@ -29,6 +29,7 @@ bool sb_notebook_create(const char *path, const char *cache, const char *author,
29 char **error);29 char **error);
30bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error);30bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error);
31char *sb_library_sections(const Library *library);31char *sb_library_sections(const Library *library);
32int32_t sb_library_color(const Library *library);
32char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author,33char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author,
33 const char *date, const char *time);34 const char *date, const char *time);
34char *sb_library_search(const Library *library, const Section *open, const char *path, const char *query);35char *sb_library_search(const Library *library, const Section *open, const char *path, const char *query);
crates/mobile/src/library.rs+14
...@@ -541,6 +541,11 @@ impl Library {...@@ -541,6 +541,11 @@ impl Library {
541 Ok(unlocked)541 Ok(unlocked)
542 }542 }
543543
544 /// The notebook's colour, COLORREF, as its table of contents held it when last read.
545 fn color(&self) -> Option<u32> {
546 self.notebook().as_ref()?.catalog().toc.as_ref()?.color
547 }
548
544 /// The notebook's sections, read again.549 /// The notebook's sections, read again.
545 pub(crate) fn tabs(&self) -> Result<Vec<Tab>> {550 pub(crate) fn tabs(&self) -> Result<Vec<Tab>> {
546 if let Place::File(file) = &self.place {551 if let Place::File(file) = &self.place {
...@@ -1405,6 +1410,15 @@ pub extern "C" fn sb_library_sections(library: &Library) -> *mut c_char {...@@ -1405,6 +1410,15 @@ pub extern "C" fn sb_library_sections(library: &Library) -> *mut c_char {
1405 json(library.tabs())1410 json(library.tabs())
1406}1411}
14071412
1413/// The notebook's colour as `0xRRGGBB`, as its sections were last read; -1 where it has none.
1414#[unsafe(no_mangle)]
1415pub extern "C" fn sb_library_color(library: &Library) -> i32 {
1416 library.color().map_or(-1, |color| {
1417 let [red, green, blue] = srgb(color);
1418 i32::from_be_bytes([0, red, green, blue])
1419 })
1420}
1421
1408/// Creates a section named `name` in the group at catalog path `folder` ("" for the1422/// Creates a section named `name` in the group at catalog path `folder` ("" for the
1409/// notebook's top) holding one page titled with `date` and `time`, as OneNote creates one;1423/// notebook's top) holding one page titled with `date` and `time`, as OneNote creates one;
1410/// returns its catalog path, or null.1424/// returns its catalog path, or null.
crates/mobile/src/tests.rs+2-1
...@@ -105,7 +105,7 @@ fn a_notebook_folder_lists_sections_in_order_with_groups_and_colours() {...@@ -105,7 +105,7 @@ fn a_notebook_folder_lists_sections_in_order_with_groups_and_colours() {
105}105}
106106
107#[test]107#[test]
108fn a_new_notebook_opens_with_one_section_and_refuses_a_taken_name() {108fn a_new_notebook_opens_with_one_section_in_its_colour_and_refuses_a_taken_name() {
109 let directory = tempfile::tempdir().unwrap();109 let directory = tempfile::tempdir().unwrap();
110 let root = directory.path().join("Mine");110 let root = directory.path().join("Mine");
111 let cache = directory.path().join("cache");111 let cache = directory.path().join("cache");
...@@ -128,6 +128,7 @@ fn a_new_notebook_opens_with_one_section_and_refuses_a_taken_name() {...@@ -128,6 +128,7 @@ fn a_new_notebook_opens_with_one_section_and_refuses_a_taken_name() {
128 let tabs = library.tabs().unwrap();128 let tabs = library.tabs().unwrap();
129 assert_eq!(tabs.len(), 1);129 assert_eq!(tabs.len(), 1);
130 assert!(tabs[0].readable);130 assert!(tabs[0].readable);
131 assert_eq!(library::sb_library_color(&library), 0x91baae);
131 assert!(!create(&mut error));132 assert!(!create(&mut error));
132 assert!(!error.is_null());133 assert!(!error.is_null());
133 unsafe { sb_string_free(error) };134 unsafe { sb_string_free(error) };
crates/snowbound/assets/icons/icloud-badge.svg created+7
...@@ -0,0 +1,7 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <defs>
3 <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#9fd2ff"/><stop offset="1" stop-color="#3b8ff0"/></linearGradient>
4 </defs>
5 <path d="M9.30 15.40H13.95A1.80 1.80 0 0 0 14.22 11.83A2.58 2.58 0 0 0 9.30 11.29A2.07 2.07 0 0 0 9.30 15.40Z" fill="#ffffff" stroke="#ffffff" stroke-width="1.6" stroke-linejoin="round"/>
6 <path d="M9.30 15.40H13.95A1.80 1.80 0 0 0 14.22 11.83A2.58 2.58 0 0 0 9.30 11.29A2.07 2.07 0 0 0 9.30 15.40Z" fill="url(#g0)" stroke="#1560c4" stroke-width="0.6" stroke-linejoin="round"/>
7</svg>
crates/snowbound/assets/icons/notebook.svg+3-2
...@@ -3,7 +3,7 @@...@@ -3,7 +3,7 @@
3 <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#ff8f80"/><stop offset="1" stop-color="#f03b30"/></linearGradient>3 <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#ff8f80"/><stop offset="1" stop-color="#f03b30"/></linearGradient>
4 <linearGradient id="g1" x2="0" y2="1"><stop offset="0" stop-color="#86e88a"/><stop offset="1" stop-color="#28b04a"/></linearGradient>4 <linearGradient id="g1" x2="0" y2="1"><stop offset="0" stop-color="#86e88a"/><stop offset="1" stop-color="#28b04a"/></linearGradient>
5 <linearGradient id="g2" x2="0" y2="1"><stop offset="0" stop-color="#79c2ff"/><stop offset="1" stop-color="#1f7cf0"/></linearGradient>5 <linearGradient id="g2" x2="0" y2="1"><stop offset="0" stop-color="#79c2ff"/><stop offset="1" stop-color="#1f7cf0"/></linearGradient>
6 <linearGradient id="g3" x2="0" y2="1"><stop offset="0" stop-color="#ffdc80"/><stop offset="1" stop-color="#f39c28"/></linearGradient>6 <linearGradient id="g3" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#000000"/></linearGradient>
7 <linearGradient id="g4" x2="0" y2="1"><stop offset="0" stop-color="#fffdf3"/><stop offset="1" stop-color="#fbeec7"/></linearGradient>7 <linearGradient id="g4" x2="0" y2="1"><stop offset="0" stop-color="#fffdf3"/><stop offset="1" stop-color="#fbeec7"/></linearGradient>
8 </defs>8 </defs>
9 <path d="M12.1 3.75L14.4 3.75A0.6 0.6 0 0 1 15 4.35L15 5.65A0.6 0.6 0 0 1 14.4 6.25L12.1 6.25A0.6 0.6 0 0 1 11.5 5.65L11.5 4.35A0.6 0.6 0 0 1 12.1 3.75Z" fill="#000000" fill-opacity="0.16"/>9 <path d="M12.1 3.75L14.4 3.75A0.6 0.6 0 0 1 15 4.35L15 5.65A0.6 0.6 0 0 1 14.4 6.25L12.1 6.25A0.6 0.6 0 0 1 11.5 5.65L11.5 4.35A0.6 0.6 0 0 1 12.1 3.75Z" fill="#000000" fill-opacity="0.16"/>
...@@ -13,7 +13,8 @@...@@ -13,7 +13,8 @@
13 <path d="M12.1 10.75L14.4 10.75A0.6 0.6 0 0 1 15 11.35L15 12.65A0.6 0.6 0 0 1 14.4 13.25L12.1 13.25A0.6 0.6 0 0 1 11.5 12.65L11.5 11.35A0.6 0.6 0 0 1 12.1 10.75Z" fill="#000000" fill-opacity="0.16"/>13 <path d="M12.1 10.75L14.4 10.75A0.6 0.6 0 0 1 15 11.35L15 12.65A0.6 0.6 0 0 1 14.4 13.25L12.1 13.25A0.6 0.6 0 0 1 11.5 12.65L11.5 11.35A0.6 0.6 0 0 1 12.1 10.75Z" fill="#000000" fill-opacity="0.16"/>
14 <path class="accent" d="M12.1 10H14.4A0.6 0.6 0 0 1 15 10.6V11.9A0.6 0.6 0 0 1 14.4 12.5H12.1A0.6 0.6 0 0 1 11.5 11.9V10.6A0.6 0.6 0 0 1 12.1 10Z" fill="url(#g2)" stroke="#1560c4" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>14 <path class="accent" d="M12.1 10H14.4A0.6 0.6 0 0 1 15 10.6V11.9A0.6 0.6 0 0 1 14.4 12.5H12.1A0.6 0.6 0 0 1 11.5 11.9V10.6A0.6 0.6 0 0 1 12.1 10Z" fill="url(#g2)" stroke="#1560c4" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
15 <path d="M3.5 2.25L11.5 2.25A1 1 0 0 1 12.5 3.25L12.5 14.25A1 1 0 0 1 11.5 15.25L3.5 15.25A1 1 0 0 1 2.5 14.25L2.5 3.25A1 1 0 0 1 3.5 2.25Z" fill="#000000" fill-opacity="0.16"/>15 <path d="M3.5 2.25L11.5 2.25A1 1 0 0 1 12.5 3.25L12.5 14.25A1 1 0 0 1 11.5 15.25L3.5 15.25A1 1 0 0 1 2.5 14.25L2.5 3.25A1 1 0 0 1 3.5 2.25Z" fill="#000000" fill-opacity="0.16"/>
16 <path d="M3.5 1.5H11.5A1 1 0 0 1 12.5 2.5V13.5A1 1 0 0 1 11.5 14.5H3.5A1 1 0 0 1 2.5 13.5V2.5A1 1 0 0 1 3.5 1.5Z" fill="url(#g3)" stroke="#cc7a10" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>16 <path d="M3.5 1.5H11.5A1 1 0 0 1 12.5 2.5V13.5A1 1 0 0 1 11.5 14.5H3.5A1 1 0 0 1 2.5 13.5V2.5A1 1 0 0 1 3.5 1.5Z" fill="currentColor" stroke="currentColor" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
17 <path d="M3.5 1.5H11.5A1 1 0 0 1 12.5 2.5V13.5A1 1 0 0 1 11.5 14.5H3.5A1 1 0 0 1 2.5 13.5V2.5A1 1 0 0 1 3.5 1.5Z" fill="url(#g3)" fill-opacity="0.3" stroke="#000000" stroke-opacity="0.3" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
17 <path d="M6 4H10A0.5 0.5 0 0 1 10.5 4.5V6.5A0.5 0.5 0 0 1 10 7H6A0.5 0.5 0 0 1 5.5 6.5V4.5A0.5 0.5 0 0 1 6 4Z" fill="url(#g4)"/>18 <path d="M6 4H10A0.5 0.5 0 0 1 10.5 4.5V6.5A0.5 0.5 0 0 1 10 7H6A0.5 0.5 0 0 1 5.5 6.5V4.5A0.5 0.5 0 0 1 6 4Z" fill="url(#g4)"/>
18 <path d="M3.5 13.5V3A0.5 0.5 0 0 1 4 2.5H11" fill="none" stroke="#ffffff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.85"/>19 <path d="M3.5 13.5V3A0.5 0.5 0 0 1 4 2.5H11" fill="none" stroke="#ffffff" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" stroke-opacity="0.85"/>
19 <path d="M1.5 3.75H4.5M1.5 6.25H4.5M1.5 8.75H4.5M1.5 11.25H4.5M1.5 13.5H4.5" fill="none" stroke="#3b4350" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>20 <path d="M1.5 3.75H4.5M1.5 6.25H4.5M1.5 8.75H4.5M1.5 11.25H4.5M1.5 13.5H4.5" fill="none" stroke="#3b4350" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
crates/snowbound/assets/icons/swatch.svg created+4
...@@ -0,0 +1,4 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <path d="M4 2.5H12A1.5 1.5 0 0 1 13.5 4V12A1.5 1.5 0 0 1 12 13.5H4A1.5 1.5 0 0 1 2.5 12V4A1.5 1.5 0 0 1 4 2.5Z" fill="currentColor" stroke="currentColor" stroke-width="1"/>
3 <path d="M4 2.5H12A1.5 1.5 0 0 1 13.5 4V12A1.5 1.5 0 0 1 12 13.5H4A1.5 1.5 0 0 1 2.5 12V4A1.5 1.5 0 0 1 4 2.5Z" fill="none" stroke="#000000" stroke-opacity="0.3" stroke-width="1"/>
4</svg>
crates/snowbound/src/art.rs+6
...@@ -28,6 +28,11 @@ pub const INFO: &[&str] = art!("icons/info");...@@ -28,6 +28,11 @@ pub const INFO: &[&str] = art!("icons/info");
28pub const ITALIC: &[&str] = art!("icons/italic");28pub const ITALIC: &[&str] = art!("icons/italic");
29pub const LINK: &[&str] = art!("icons/link");29pub const LINK: &[&str] = art!("icons/link");
30pub const NOTEBOOK: &[&str] = art!("icons/notebook");30pub const NOTEBOOK: &[&str] = art!("icons/notebook");
31/// A notebook iCloud Drive keeps: its glyph with a cloud at the corner.
32pub const NOTEBOOK_ICLOUD: &[&str] = &[
33 NOTEBOOK[0],
34 include_str!("../assets/icons/icloud-badge.svg"),
35];
31pub const NUMBERING: &[&str] = art!("icons/numbering");36pub const NUMBERING: &[&str] = art!("icons/numbering");
32pub const OPTIONS: &[&str] = art!("icons/options");37pub const OPTIONS: &[&str] = art!("icons/options");
33pub const OUTDENT: &[&str] = art!("icons/outdent");38pub const OUTDENT: &[&str] = art!("icons/outdent");
...@@ -67,6 +72,7 @@ pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio");...@@ -67,6 +72,7 @@ pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio");
67pub const RECORD_VIDEO: &[&str] = art!("icons/record-video");72pub const RECORD_VIDEO: &[&str] = art!("icons/record-video");
68pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping");73pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping");
69pub const SPELLING: &[&str] = art!("icons/spelling");74pub const SPELLING: &[&str] = art!("icons/spelling");
75pub const SWATCH: &[&str] = art!("icons/swatch");
70pub const SYMBOL: &[&str] = art!("icons/symbol");76pub const SYMBOL: &[&str] = art!("icons/symbol");
71pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy");77pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy");
72pub const SYNC_DONE: &[&str] = art!("icons/sync-done");78pub const SYNC_DONE: &[&str] = art!("icons/sync-done");
crates/snowbound/src/commands.rs+1-1
...@@ -1373,7 +1373,7 @@ impl State {...@@ -1373,7 +1373,7 @@ impl State {
1373 }1373 }
13741374
1375 /// The notebook the open section, or the notebook showing none, belongs to.1375 /// The notebook the open section, or the notebook showing none, belongs to.
1376 fn notebook(&self) -> Option<&Arc<crate::Library>> {1376 pub(crate) fn notebook(&self) -> Option<&Arc<crate::Library>> {
1377 self.session1377 self.session
1378 .as_ref()1378 .as_ref()
1379 .map(|session| &session.library)1379 .map(|session| &session.library)
crates/snowbound/src/main.rs+9-3
...@@ -1724,7 +1724,7 @@ impl State {...@@ -1724,7 +1724,7 @@ impl State {
1724 if self.navigation_bar_right {1724 if self.navigation_bar_right {
1725 self.sidebar(&theme, sidebar);1725 self.sidebar(&theme, sidebar);
1726 }1726 }
1727 self.sidebar_button(&theme, height);1727 self.sidebar_button(height);
1728 self.ui.close();1728 self.ui.close();
1729 self.context_menu();1729 self.context_menu();
1730 return Ok((section, open_tab, None));1730 return Ok((section, open_tab, None));
...@@ -1737,7 +1737,7 @@ impl State {...@@ -1737,7 +1737,7 @@ impl State {
1737 if self.navigation_bar_right {1737 if self.navigation_bar_right {
1738 self.sidebar(&theme, sidebar);1738 self.sidebar(&theme, sidebar);
1739 }1739 }
1740 self.sidebar_button(&theme, height);1740 self.sidebar_button(height);
1741 self.ui.close();1741 self.ui.close();
1742 self.context_menu();1742 self.context_menu();
1743 return Ok((section, open_tab, None));1743 return Ok((section, open_tab, None));
...@@ -1823,7 +1823,7 @@ impl State {...@@ -1823,7 +1823,7 @@ impl State {
1823 self.sidebar(&theme, sidebar);1823 self.sidebar(&theme, sidebar);
1824 }1824 }
1825 self.task_pane(&theme);1825 self.task_pane(&theme);
1826 self.sidebar_button(&theme, height);1826 self.sidebar_button(height);
1827 self.ui.close();1827 self.ui.close();
1828 self.context_menu();1828 self.context_menu();
1829 // No tab stands on the frame in Full Page View.1829 // No tab stands on the frame in Full Page View.
...@@ -4467,6 +4467,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] {...@@ -4467,6 +4467,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] {
4467 color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref)4467 color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref)
4468}4468}
44694469
4470/// The colour a notebook's glyph takes from its COLORREF, as OneNote's navigation bar tints
4471/// it; the glyph's own orange without one.
4472fn notebook_color(color: Option<u32>) -> [f32; 4] {
4473 color.map_or(draw::srgb(0xf3, 0x9c, 0x28), canvas::gpu::colorref)
4474}
4475
4470/// OneNote's information bar for conflicting changes, clicked anywhere: above a page with4476/// OneNote's information bar for conflicting changes, clicked anywhere: above a page with
4471/// conflict pages it shows or hides them; above a conflict page it opens OneNote's menu,4477/// conflict pages it shows or hides them; above a conflict page it opens OneNote's menu,
4472/// whose previous and next changes `steps` enables and whose Copy Page To lists `sections`.4478/// whose previous and next changes `steps` enables and whose Copy Page To lists `sections`.
crates/snowbound/src/menus.rs+4-1
...@@ -575,7 +575,10 @@ impl State {...@@ -575,7 +575,10 @@ impl State {
575 } else {575 } else {
576 crate::art::SECTION_GROUP576 crate::art::SECTION_GROUP
577 },577 },
578 tint: None,578 tint: place
579 .path
580 .is_empty()
581 .then(|| crate::notebook_color(library.color())),
579 })582 })
580 .collect()583 .collect()
581 }584 }
crates/snowbound/src/palette.rs+2
...@@ -97,6 +97,7 @@ impl State {...@@ -97,6 +97,7 @@ impl State {
97 .map(|library| Row {97 .map(|library| Row {
98 text: library.name.clone(),98 text: library.name.clone(),
99 icon: Some(art::NOTEBOOK),99 icon: Some(art::NOTEBOOK),
100 tint: Some(crate::notebook_color(library.color())),
100 target: Some(Target::Notebook(Arc::clone(library))),101 target: Some(Target::Notebook(Arc::clone(library))),
101 ..Row::default()102 ..Row::default()
102 }),103 }),
...@@ -287,6 +288,7 @@ impl State {...@@ -287,6 +288,7 @@ impl State {
287 .to_owned(),288 .to_owned(),
288 after: "Closed".to_owned(),289 after: "Closed".to_owned(),
289 icon: Some(art::NOTEBOOK),290 icon: Some(art::NOTEBOOK),
291 tint: Some(crate::notebook_color(None)),
290 target: Some(target),292 target: Some(target),
291 ..Row::default()293 ..Row::default()
292 });294 });
crates/snowbound/src/properties.rs+2-2
...@@ -139,8 +139,8 @@ impl State {...@@ -139,8 +139,8 @@ impl State {
139 .iter()139 .iter()
140 .map(|&(color, text)| Item {140 .map(|&(color, text)| Item {
141 text,141 text,
142 icon: Some(art::SECTION),142 icon: Some(art::SWATCH),
143 tint: Some(theme.section(crate::section_color(Some(color))).accent),143 tint: Some(crate::notebook_color(Some(color))),
144 current: Some(color) == shown,144 current: Some(color) == shown,
145 ..Item::default()145 ..Item::default()
146 })146 })
crates/snowbound/src/sidebar.rs+53-22
...@@ -208,11 +208,14 @@ fn sidebar(...@@ -208,11 +208,14 @@ fn sidebar(
208 ui.id(("notebook", index)),208 ui.id(("notebook", index)),
209 Row {209 Row {
210 label: &library.name,210 label: &library.name,
211 icon: Leading::Icon(if library.in_icloud() {211 icon: Leading::Notebook(
212 art::ICLOUD212 if library.in_icloud() {
213 } else {213 art::NOTEBOOK_ICLOUD
214 art::NOTEBOOK214 } else {
215 }),215 art::NOTEBOOK
216 },
217 library.color(),
218 ),
216 depth: 0,219 depth: 0,
217 dim: library.notebook.is_err(),220 dim: library.notebook.is_err(),
218 fold: Some(unfolded),221 fold: Some(unfolded),
...@@ -250,9 +253,24 @@ fn sidebar(...@@ -250,9 +253,24 @@ fn sidebar(
250 },253 },
251 );254 );
252 for (part, icon, label, chosen) in [255 for (part, icon, label, chosen) in [
253 ("new", art::PLUS, "New Notebook", Action::NewNotebook),256 (
254 ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook),257 "new",
255 ("options", art::OPTIONS, "Options", Action::Options),258 Leading::Icon(art::PLUS),
259 "New Notebook",
260 Action::NewNotebook,
261 ),
262 (
263 "open",
264 Leading::Notebook(art::NOTEBOOK, None),
265 "Open Existing",
266 Action::OpenNotebook,
267 ),
268 (
269 "options",
270 Leading::Icon(art::OPTIONS),
271 "Options",
272 Action::Options,
273 ),
256 ] {274 ] {
257 let (row, _) = tree_row(275 let (row, _) = tree_row(
258 ui,276 ui,
...@@ -260,7 +278,7 @@ fn sidebar(...@@ -260,7 +278,7 @@ fn sidebar(
260 ui.id(part),278 ui.id(part),
261 Row {279 Row {
262 label,280 label,
263 icon: Leading::Icon(icon),281 icon,
264 depth: 0,282 depth: 0,
265 dim: false,283 dim: false,
266 fold: None,284 fold: None,
...@@ -584,10 +602,23 @@ fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: b...@@ -584,10 +602,23 @@ fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: b
584#[derive(Clone, Copy)]602#[derive(Clone, Copy)]
585enum Leading {603enum Leading {
586 Icon(&'static [&'static str]),604 Icon(&'static [&'static str]),
605 /// A notebook's glyph, drawn in its colour (COLORREF).
606 Notebook(&'static [&'static str], Option<u32>),
587 /// A section's tab, drawn in its colour.607 /// A section's tab, drawn in its colour.
588 Section([f32; 4]),608 Section([f32; 4]),
589}609}
590610
611impl Leading {
612 /// The art and the colour its `currentColor` paints in `theme`.
613 fn art(self, theme: &Theme) -> (&'static [&'static str], [f32; 4]) {
614 match self {
615 Self::Icon(icon) => (icon, theme.text),
616 Self::Notebook(icon, color) => (icon, crate::notebook_color(color)),
617 Self::Section(section) => (art::SECTION, theme.section(section).accent),
618 }
619 }
620}
621
591struct Row<'a> {622struct Row<'a> {
592 label: &'a str,623 label: &'a str,
593 icon: Leading,624 icon: Leading,
...@@ -633,10 +664,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {...@@ -633,10 +664,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {
633 }664 }
634 }665 }
635 let color = if row.dim { theme.text_dim } else { theme.text };666 let color = if row.dim { theme.text_dim } else { theme.text };
636 let (icon, [red, green, blue, _]) = match row.icon {667 let (icon, [red, green, blue, _]) = row.icon.art(theme);
637 Leading::Icon(icon) => (icon, theme.text),
638 Leading::Section(section) => (art::SECTION, theme.section(section).accent),
639 };
640 // Coloured art keeps its colours, so a dim row fades its icon.668 // Coloured art keeps its colours, so a dim row fades its icon.
641 let alpha = if row.dim { 0.5 } else { 1.0 };669 let alpha = if row.dim { 0.5 } else { 1.0 };
642 ui.leaf(670 ui.leaf(
...@@ -814,7 +842,7 @@ impl crate::State {...@@ -814,7 +842,7 @@ impl crate::State {
814842
815 /// The notebook button, floating at the body's corner over the section tabs' row, `height`843 /// The notebook button, floating at the body's corner over the section tabs' row, `height`
816 /// tall as it eases, or over the sidebar's header while that is open.844 /// tall as it eases, or over the sidebar's header while that is open.
817 pub(crate) fn sidebar_button(&mut self, theme: &Theme, height: f32) {845 pub(crate) fn sidebar_button(&mut self, height: f32) {
818 if self.temporary {846 if self.temporary {
819 return;847 return;
820 }848 }
...@@ -839,7 +867,8 @@ impl crate::State {...@@ -839,7 +867,8 @@ impl crate::State {
839 ..Spec::default()867 ..Spec::default()
840 },868 },
841 );869 );
842 if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, theme.text, None).clicked {870 let tint = crate::notebook_color(self.notebook().and_then(|library| library.color()));
871 if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked {
843 self.sidebar = !self.sidebar;872 self.sidebar = !self.sidebar;
844 self.save_settings();873 self.save_settings();
845 }874 }
...@@ -1155,7 +1184,7 @@ impl crate::State {...@@ -1155,7 +1184,7 @@ impl crate::State {
1155 #[cfg(target_os = "linux")]1184 #[cfg(target_os = "linux")]
1156 let install = crate::desktop::installable().then_some((1185 let install = crate::desktop::installable().then_some((
1157 "install",1186 "install",
1158 art::PLUS,1187 Leading::Icon(art::PLUS),
1159 "Install Snowbound",1188 "Install Snowbound",
1160 crate::Command::Install,1189 crate::Command::Install,
1161 ));1190 ));
...@@ -1169,19 +1198,19 @@ impl crate::State {...@@ -1169,19 +1198,19 @@ impl crate::State {
1169 [1198 [
1170 (1199 (
1171 "new",1200 "new",
1172 art::PLUS,1201 Leading::Icon(art::PLUS),
1173 "New Notebook",1202 "New Notebook",
1174 crate::Command::NewNotebook,1203 crate::Command::NewNotebook,
1175 ),1204 ),
1176 (1205 (
1177 "open",1206 "open",
1178 art::NOTEBOOK,1207 Leading::Notebook(art::NOTEBOOK, None),
1179 "Open Existing",1208 "Open Existing",
1180 crate::Command::OpenNotebook,1209 crate::Command::OpenNotebook,
1181 ),1210 ),
1182 (1211 (
1183 "server",1212 "server",
1184 art::SERVER,1213 Leading::Icon(art::SERVER),
1185 "Open Notebook from Server…",1214 "Open Notebook from Server…",
1186 crate::Command::OpenFromServer(None),1215 crate::Command::OpenFromServer(None),
1187 ),1216 ),
...@@ -1189,7 +1218,7 @@ impl crate::State {...@@ -1189,7 +1218,7 @@ impl crate::State {
1189 .into_iter()1218 .into_iter()
1190 .chain(crate::guide::OFFERED.then_some((1219 .chain(crate::guide::OFFERED.then_some((
1191 "guide",1220 "guide",
1192 art::PAGE,1221 Leading::Icon(art::PAGE),
1193 "Open the Snowbound Guide",1222 "Open the Snowbound Guide",
1194 crate::Command::OpenGuide,1223 crate::Command::OpenGuide,
1195 )))1224 )))
...@@ -1214,7 +1243,7 @@ impl crate::State {...@@ -1214,7 +1243,7 @@ impl crate::State {
1214 } else {1243 } else {
1215 (1244 (
1216 "No sections in this notebook",1245 "No sections in this notebook",
1217 vec![("new", art::PLUS, "New Section", new)],1246 vec![("new", Leading::Icon(art::PLUS), "New Section", new)],
1218 )1247 )
1219 };1248 };
1220 self.notice(id, title, &[], buttons);1249 self.notice(id, title, &[], buttons);
...@@ -1227,7 +1256,7 @@ impl crate::State {...@@ -1227,7 +1256,7 @@ impl crate::State {
1227 id: ui::Id,1256 id: ui::Id,
1228 title: &str,1257 title: &str,
1229 servers: &[String],1258 servers: &[String],
1230 buttons: Vec<(&str, &'static [&'static str], &str, crate::Command)>,1259 buttons: Vec<(&str, Leading, &str, crate::Command)>,
1231 ) {1260 ) {
1232 const BUTTON: [f32; 2] = [240.0, 32.0];1261 const BUTTON: [f32; 2] = [240.0, 32.0];
1233 let theme = self.page_area_theme();1262 let theme = self.page_area_theme();
...@@ -1266,12 +1295,14 @@ impl crate::State {...@@ -1266,12 +1295,14 @@ impl crate::State {
1266 );1295 );
1267 let mut chosen = None;1296 let mut chosen = None;
1268 for (part, icon, label, command) in buttons {1297 for (part, icon, label, command) in buttons {
1298 let (icon, tint) = icon.art(&theme);
1269 let button = self.ui.leaf(1299 let button = self.ui.leaf(
1270 part,1300 part,
1271 Spec {1301 Spec {
1272 flags: Flags::CLICKABLE,1302 flags: Flags::CLICKABLE,
1273 size: [fill(), px(BUTTON[1])],1303 size: [fill(), px(BUTTON[1])],
1274 icon: Some(icon),1304 icon: Some(icon),
1305 tint: Some(tint),
1275 text: Some(label),1306 text: Some(label),
1276 fill: Some(theme.chip),1307 fill: Some(theme.chip),
1277 hover_fill: Some(theme.hover()),1308 hover_fill: Some(theme.hover()),
crates/snowbound/src/sync.rs+5
...@@ -249,6 +249,8 @@ pub(crate) fn control(ui: &mut Ui, session: &Session, update: &update::Status, t...@@ -249,6 +249,8 @@ pub(crate) fn control(ui: &mut Ui, session: &Session, update: &update::Status, t
249/// What the popup shows of the notebook.249/// What the popup shows of the notebook.
250struct Facts<'a> {250struct Facts<'a> {
251 name: &'a str,251 name: &'a str,
252 /// The notebook's colour, COLORREF.
253 color: Option<u32>,
252 /// The notebook's folder or address in full.254 /// The notebook's folder or address in full.
253 location: &'a str,255 location: &'a str,
254 place: Vec<String>,256 place: Vec<String>,
...@@ -679,6 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {...@@ -679,6 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
679 size: [fill(), px(line)],681 size: [fill(), px(line)],
680 text: Some(facts.name),682 text: Some(facts.name),
681 icon: Some(art::NOTEBOOK),683 icon: Some(art::NOTEBOOK),
684 tint: Some(crate::notebook_color(facts.color)),
682 bold: true,685 bold: true,
683 overflow: Overflow::Ellipsis,686 overflow: Overflow::Ellipsis,
684 ..Spec::default()687 ..Spec::default()
...@@ -1026,6 +1029,7 @@ impl State {...@@ -1026,6 +1029,7 @@ impl State {
1026 };1029 };
1027 let facts = Facts {1030 let facts = Facts {
1028 name: &library.name,1031 name: &library.name,
1032 color: library.color(),
1029 location: &library.location,1033 location: &library.location,
1030 place: library.place(),1034 place: library.place(),
1031 notice: library.notice.as_deref(),1035 notice: library.notice.as_deref(),
...@@ -1132,6 +1136,7 @@ mod tests {...@@ -1132,6 +1136,7 @@ mod tests {
1132 fn facts(sections: Vec<(String, SyncStatus)>) -> Facts<'static> {1136 fn facts(sections: Vec<(String, SyncStatus)>) -> Facts<'static> {
1133 Facts {1137 Facts {
1134 name: "OneNote",1138 name: "OneNote",
1139 color: None,
1135 location: "/Volumes/clover/Documents/OneNote",1140 location: "/Volumes/clover/Documents/OneNote",
1136 place: ["zenith", "clover", "Documents"]1141 place: ["zenith", "clover", "Documents"]
1137 .map(str::to_owned)1142 .map(str::to_owned)
crates/ui/src/lib.rs+5-2
...@@ -368,8 +368,9 @@ pub struct Spec<'a> {...@@ -368,8 +368,9 @@ pub struct Spec<'a> {
368 pub hover_border: Option<[f32; 4]>,368 pub hover_border: Option<[f32; 4]>,
369 pub radius: f32,369 pub radius: f32,
370 pub shape: Shape,370 pub shape: Shape,
371 /// 16 px artwork before the label, tinted with the label's colour.371 /// 16 px artwork before the label, tinted with `tint` or else the label's colour.
372 pub icon: Option<&'static [&'static str]>,372 pub icon: Option<&'static [&'static str]>,
373 pub tint: Option<[f32; 4]>,
373 /// A 16 px picture in the icon's place beside a label; without one, the picture fills374 /// A 16 px picture in the icon's place beside a label; without one, the picture fills
374 /// the box inside its padding.375 /// the box inside its padding.
375 pub image: Option<&'a RasterImage>,376 pub image: Option<&'a RasterImage>,
...@@ -522,6 +523,7 @@ struct Built {...@@ -522,6 +523,7 @@ struct Built {
522 radius: f32,523 radius: f32,
523 shape: Shape,524 shape: Shape,
524 icon: Option<&'static [&'static str]>,525 icon: Option<&'static [&'static str]>,
526 tint: [f32; 4],
525 image: Option<RasterImage>,527 image: Option<RasterImage>,
526 pad: [f32; 2],528 pad: [f32; 2],
527 gap: f32,529 gap: f32,
...@@ -1705,7 +1707,7 @@ impl Ui {...@@ -1705,7 +1707,7 @@ impl Ui {
1705 self.display.push(Display::Icon {1707 self.display.push(Display::Icon {
1706 sources,1708 sources,
1707 origin: [x, top],1709 origin: [x, top],
1708 tint: node.color,1710 tint: node.tint,
1709 });1711 });
1710 x += ICON + ICON_GAP;1712 x += ICON + ICON_GAP;
1711 } else if let Some(image) = &node.image {1713 } else if let Some(image) = &node.image {
...@@ -2030,6 +2032,7 @@ impl Built {...@@ -2030,6 +2032,7 @@ impl Built {
2030 radius: spec.radius,2032 radius: spec.radius,
2031 shape: spec.shape,2033 shape: spec.shape,
2032 icon: spec.icon,2034 icon: spec.icon,
2035 tint: spec.tint.or(spec.color).unwrap_or(text),
2033 image: spec.image.cloned(),2036 image: spec.image.cloned(),
2034 pad: spec.pad,2037 pad: spec.pad,
2035 gap: spec.gap,2038 gap: spec.gap,