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
232232 case .notebook(let id):
233233 guard let notebook = notebook(id) else { break }
234234 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
236237 if case .server(let server) = notebook.source { content.secondaryText = server.host }
237238 let more = UIButton(type: .system)
238239 more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal)
apps/ios/Snowbound/Notebooks.swift+7-1
......@@ -175,6 +175,8 @@ final class Notebook {
175175 private(set) var name: String
176176 private(set) var handle: OpaquePointer?
177177 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?
178180 /// Why the notebook could not be opened, while it cannot.
179181 private(set) var problem: String?
180182 /// The Files location being read, held open while the notebook is.
......@@ -288,8 +290,12 @@ final class Notebook {
288290 return done()
289291 }
290292 let pointer = Int(bitPattern: handle)
291 background({ decode([Tab].self, sb_library_sections(OpaquePointer(bitPattern: pointer))) }) { [self] tabs in
293 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
292297 if let tabs { self.tabs = tabs }
298 self.color = color < 0 ? nil : UIColor(rgb: [16, 8, 0].map { UInt8(truncatingIfNeeded: color >> $0) })
293299 download()
294300 done()
295301 }
crates/mobile/include/snowbound.h+1
......@@ -29,6 +29,7 @@ bool sb_notebook_create(const char *path, const char *cache, const char *author,
2929 char **error);
3030bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error);
3131char *sb_library_sections(const Library *library);
32int32_t sb_library_color(const Library *library);
3233char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author,
3334 const char *date, const char *time);
3435char *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 {
541541 Ok(unlocked)
542542 }
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
544549 /// The notebook's sections, read again.
545550 pub(crate) fn tabs(&self) -> Result<Vec<Tab>> {
546551 if let Place::File(file) = &self.place {
......@@ -1405,6 +1410,15 @@ pub extern "C" fn sb_library_sections(library: &Library) -> *mut c_char {
14051410 json(library.tabs())
14061411}
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
14081422/// Creates a section named `name` in the group at catalog path `folder` ("" for the
14091423/// notebook's top) holding one page titled with `date` and `time`, as OneNote creates one;
14101424/// 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() {
105105}
106106
107107#[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() {
109109 let directory = tempfile::tempdir().unwrap();
110110 let root = directory.path().join("Mine");
111111 let cache = directory.path().join("cache");
......@@ -128,6 +128,7 @@ fn a_new_notebook_opens_with_one_section_and_refuses_a_taken_name() {
128128 let tabs = library.tabs().unwrap();
129129 assert_eq!(tabs.len(), 1);
130130 assert!(tabs[0].readable);
131 assert_eq!(library::sb_library_color(&library), 0x91baae);
131132 assert!(!create(&mut error));
132133 assert!(!error.is_null());
133134 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 @@
33 <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#ff8f80"/><stop offset="1" stop-color="#f03b30"/></linearGradient>
44 <linearGradient id="g1" x2="0" y2="1"><stop offset="0" stop-color="#86e88a"/><stop offset="1" stop-color="#28b04a"/></linearGradient>
55 <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>
77 <linearGradient id="g4" x2="0" y2="1"><stop offset="0" stop-color="#fffdf3"/><stop offset="1" stop-color="#fbeec7"/></linearGradient>
88 </defs>
99 <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 @@
1313 <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"/>
1414 <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"/>
1515 <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"/>
1718 <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)"/>
1819 <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"/>
1920 <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");
2828pub const ITALIC: &[&str] = art!("icons/italic");
2929pub const LINK: &[&str] = art!("icons/link");
3030pub 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];
3136pub const NUMBERING: &[&str] = art!("icons/numbering");
3237pub const OPTIONS: &[&str] = art!("icons/options");
3338pub const OUTDENT: &[&str] = art!("icons/outdent");
......@@ -67,6 +72,7 @@ pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio");
6772pub const RECORD_VIDEO: &[&str] = art!("icons/record-video");
6873pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping");
6974pub const SPELLING: &[&str] = art!("icons/spelling");
75pub const SWATCH: &[&str] = art!("icons/swatch");
7076pub const SYMBOL: &[&str] = art!("icons/symbol");
7177pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy");
7278pub const SYNC_DONE: &[&str] = art!("icons/sync-done");
crates/snowbound/src/commands.rs+1-1
......@@ -1373,7 +1373,7 @@ impl State {
13731373 }
13741374
13751375 /// 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>> {
13771377 self.session
13781378 .as_ref()
13791379 .map(|session| &session.library)
crates/snowbound/src/main.rs+9-3
......@@ -1724,7 +1724,7 @@ impl State {
17241724 if self.navigation_bar_right {
17251725 self.sidebar(&theme, sidebar);
17261726 }
1727 self.sidebar_button(&theme, height);
1727 self.sidebar_button(height);
17281728 self.ui.close();
17291729 self.context_menu();
17301730 return Ok((section, open_tab, None));
......@@ -1737,7 +1737,7 @@ impl State {
17371737 if self.navigation_bar_right {
17381738 self.sidebar(&theme, sidebar);
17391739 }
1740 self.sidebar_button(&theme, height);
1740 self.sidebar_button(height);
17411741 self.ui.close();
17421742 self.context_menu();
17431743 return Ok((section, open_tab, None));
......@@ -1823,7 +1823,7 @@ impl State {
18231823 self.sidebar(&theme, sidebar);
18241824 }
18251825 self.task_pane(&theme);
1826 self.sidebar_button(&theme, height);
1826 self.sidebar_button(height);
18271827 self.ui.close();
18281828 self.context_menu();
18291829 // No tab stands on the frame in Full Page View.
......@@ -4467,6 +4467,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] {
44674467 color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref)
44684468}
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
44704476/// OneNote's information bar for conflicting changes, clicked anywhere: above a page with
44714477/// conflict pages it shows or hides them; above a conflict page it opens OneNote's menu,
44724478/// 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 {
575575 } else {
576576 crate::art::SECTION_GROUP
577577 },
578 tint: None,
578 tint: place
579 .path
580 .is_empty()
581 .then(|| crate::notebook_color(library.color())),
579582 })
580583 .collect()
581584 }
crates/snowbound/src/palette.rs+2
......@@ -97,6 +97,7 @@ impl State {
9797 .map(|library| Row {
9898 text: library.name.clone(),
9999 icon: Some(art::NOTEBOOK),
100 tint: Some(crate::notebook_color(library.color())),
100101 target: Some(Target::Notebook(Arc::clone(library))),
101102 ..Row::default()
102103 }),
......@@ -287,6 +288,7 @@ impl State {
287288 .to_owned(),
288289 after: "Closed".to_owned(),
289290 icon: Some(art::NOTEBOOK),
291 tint: Some(crate::notebook_color(None)),
290292 target: Some(target),
291293 ..Row::default()
292294 });
crates/snowbound/src/properties.rs+2-2
......@@ -139,8 +139,8 @@ impl State {
139139 .iter()
140140 .map(|&(color, text)| Item {
141141 text,
142 icon: Some(art::SECTION),
143 tint: Some(theme.section(crate::section_color(Some(color))).accent),
142 icon: Some(art::SWATCH),
143 tint: Some(crate::notebook_color(Some(color))),
144144 current: Some(color) == shown,
145145 ..Item::default()
146146 })
crates/snowbound/src/sidebar.rs+53-22
......@@ -208,11 +208,14 @@ fn sidebar(
208208 ui.id(("notebook", index)),
209209 Row {
210210 label: &library.name,
211 icon: Leading::Icon(if library.in_icloud() {
212 art::ICLOUD
213 } else {
214 art::NOTEBOOK
215 }),
211 icon: Leading::Notebook(
212 if library.in_icloud() {
213 art::NOTEBOOK_ICLOUD
214 } else {
215 art::NOTEBOOK
216 },
217 library.color(),
218 ),
216219 depth: 0,
217220 dim: library.notebook.is_err(),
218221 fold: Some(unfolded),
......@@ -250,9 +253,24 @@ fn sidebar(
250253 },
251254 );
252255 for (part, icon, label, chosen) in [
253 ("new", art::PLUS, "New Notebook", Action::NewNotebook),
254 ("open", art::NOTEBOOK, "Open Existing", Action::OpenNotebook),
255 ("options", art::OPTIONS, "Options", Action::Options),
256 (
257 "new",
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 ),
256274 ] {
257275 let (row, _) = tree_row(
258276 ui,
......@@ -260,7 +278,7 @@ fn sidebar(
260278 ui.id(part),
261279 Row {
262280 label,
263 icon: Leading::Icon(icon),
281 icon,
264282 depth: 0,
265283 dim: false,
266284 fold: None,
......@@ -584,10 +602,23 @@ fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: b
584602#[derive(Clone, Copy)]
585603enum Leading {
586604 Icon(&'static [&'static str]),
605 /// A notebook's glyph, drawn in its colour (COLORREF).
606 Notebook(&'static [&'static str], Option<u32>),
587607 /// A section's tab, drawn in its colour.
588608 Section([f32; 4]),
589609}
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
591622struct Row<'a> {
592623 label: &'a str,
593624 icon: Leading,
......@@ -633,10 +664,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) {
633664 }
634665 }
635666 let color = if row.dim { theme.text_dim } else { theme.text };
636 let (icon, [red, green, blue, _]) = match row.icon {
637 Leading::Icon(icon) => (icon, theme.text),
638 Leading::Section(section) => (art::SECTION, theme.section(section).accent),
639 };
667 let (icon, [red, green, blue, _]) = row.icon.art(theme);
640668 // Coloured art keeps its colours, so a dim row fades its icon.
641669 let alpha = if row.dim { 0.5 } else { 1.0 };
642670 ui.leaf(
......@@ -814,7 +842,7 @@ impl crate::State {
814842
815843 /// The notebook button, floating at the body's corner over the section tabs' row, `height`
816844 /// 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) {
818846 if self.temporary {
819847 return;
820848 }
......@@ -839,7 +867,8 @@ impl crate::State {
839867 ..Spec::default()
840868 },
841869 );
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 {
843872 self.sidebar = !self.sidebar;
844873 self.save_settings();
845874 }
......@@ -1155,7 +1184,7 @@ impl crate::State {
11551184 #[cfg(target_os = "linux")]
11561185 let install = crate::desktop::installable().then_some((
11571186 "install",
1158 art::PLUS,
1187 Leading::Icon(art::PLUS),
11591188 "Install Snowbound",
11601189 crate::Command::Install,
11611190 ));
......@@ -1169,19 +1198,19 @@ impl crate::State {
11691198 [
11701199 (
11711200 "new",
1172 art::PLUS,
1201 Leading::Icon(art::PLUS),
11731202 "New Notebook",
11741203 crate::Command::NewNotebook,
11751204 ),
11761205 (
11771206 "open",
1178 art::NOTEBOOK,
1207 Leading::Notebook(art::NOTEBOOK, None),
11791208 "Open Existing",
11801209 crate::Command::OpenNotebook,
11811210 ),
11821211 (
11831212 "server",
1184 art::SERVER,
1213 Leading::Icon(art::SERVER),
11851214 "Open Notebook from Server…",
11861215 crate::Command::OpenFromServer(None),
11871216 ),
......@@ -1189,7 +1218,7 @@ impl crate::State {
11891218 .into_iter()
11901219 .chain(crate::guide::OFFERED.then_some((
11911220 "guide",
1192 art::PAGE,
1221 Leading::Icon(art::PAGE),
11931222 "Open the Snowbound Guide",
11941223 crate::Command::OpenGuide,
11951224 )))
......@@ -1214,7 +1243,7 @@ impl crate::State {
12141243 } else {
12151244 (
12161245 "No sections in this notebook",
1217 vec![("new", art::PLUS, "New Section", new)],
1246 vec![("new", Leading::Icon(art::PLUS), "New Section", new)],
12181247 )
12191248 };
12201249 self.notice(id, title, &[], buttons);
......@@ -1227,7 +1256,7 @@ impl crate::State {
12271256 id: ui::Id,
12281257 title: &str,
12291258 servers: &[String],
1230 buttons: Vec<(&str, &'static [&'static str], &str, crate::Command)>,
1259 buttons: Vec<(&str, Leading, &str, crate::Command)>,
12311260 ) {
12321261 const BUTTON: [f32; 2] = [240.0, 32.0];
12331262 let theme = self.page_area_theme();
......@@ -1266,12 +1295,14 @@ impl crate::State {
12661295 );
12671296 let mut chosen = None;
12681297 for (part, icon, label, command) in buttons {
1298 let (icon, tint) = icon.art(&theme);
12691299 let button = self.ui.leaf(
12701300 part,
12711301 Spec {
12721302 flags: Flags::CLICKABLE,
12731303 size: [fill(), px(BUTTON[1])],
12741304 icon: Some(icon),
1305 tint: Some(tint),
12751306 text: Some(label),
12761307 fill: Some(theme.chip),
12771308 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
249249/// What the popup shows of the notebook.
250250struct Facts<'a> {
251251 name: &'a str,
252 /// The notebook's colour, COLORREF.
253 color: Option<u32>,
252254 /// The notebook's folder or address in full.
253255 location: &'a str,
254256 place: Vec<String>,
......@@ -679,6 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
679681 size: [fill(), px(line)],
680682 text: Some(facts.name),
681683 icon: Some(art::NOTEBOOK),
684 tint: Some(crate::notebook_color(facts.color)),
682685 bold: true,
683686 overflow: Overflow::Ellipsis,
684687 ..Spec::default()
......@@ -1026,6 +1029,7 @@ impl State {
10261029 };
10271030 let facts = Facts {
10281031 name: &library.name,
1032 color: library.color(),
10291033 location: &library.location,
10301034 place: library.place(),
10311035 notice: library.notice.as_deref(),
......@@ -1132,6 +1136,7 @@ mod tests {
11321136 fn facts(sections: Vec<(String, SyncStatus)>) -> Facts<'static> {
11331137 Facts {
11341138 name: "OneNote",
1139 color: None,
11351140 location: "/Volumes/clover/Documents/OneNote",
11361141 place: ["zenith", "clover", "Documents"]
11371142 .map(str::to_owned)
crates/ui/src/lib.rs+5-2
......@@ -368,8 +368,9 @@ pub struct Spec<'a> {
368368 pub hover_border: Option<[f32; 4]>,
369369 pub radius: f32,
370370 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.
372372 pub icon: Option<&'static [&'static str]>,
373 pub tint: Option<[f32; 4]>,
373374 /// A 16 px picture in the icon's place beside a label; without one, the picture fills
374375 /// the box inside its padding.
375376 pub image: Option<&'a RasterImage>,
......@@ -522,6 +523,7 @@ struct Built {
522523 radius: f32,
523524 shape: Shape,
524525 icon: Option<&'static [&'static str]>,
526 tint: [f32; 4],
525527 image: Option<RasterImage>,
526528 pad: [f32; 2],
527529 gap: f32,
......@@ -1705,7 +1707,7 @@ impl Ui {
17051707 self.display.push(Display::Icon {
17061708 sources,
17071709 origin: [x, top],
1708 tint: node.color,
1710 tint: node.tint,
17091711 });
17101712 x += ICON + ICON_GAP;
17111713 } else if let Some(image) = &node.image {
......@@ -2030,6 +2032,7 @@ impl Built {
20302032 radius: spec.radius,
20312033 shape: spec.shape,
20322034 icon: spec.icon,
2035 tint: spec.tint.or(spec.color).unwrap_or(text),
20332036 image: spec.image.cloned(),
20342037 pad: spec.pad,
20352038 gap: spec.gap,