| author | |
| committer | |
| log | 384e5ef14c8d95bb74b186fe57af0cc578fa9276 |
| tree | bedca9cbba5765e76940e53b2398b50247f6559b |
| parent | 0fa3ee68b6f6521e11d14b69f59fbb546c6ad09e |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
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.517 files changed, 127 insertions(+), 36 deletions(-)
apps/ios/Snowbound/Lists.swift+2-1| ... | ... | @@ -232,7 +232,8 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke |
| 232 | 232 | case .notebook(let id): |
| 233 | 233 | guard let notebook = notebook(id) else { break } |
| 234 | 234 | 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 | 237 | if case .server(let server) = notebook.source { content.secondaryText = server.host } |
| 237 | 238 | let more = UIButton(type: .system) |
| 238 | 239 | more.setImage(UIImage(systemName: "ellipsis.circle"), for: .normal) |
apps/ios/Snowbound/Notebooks.swift+7-1| ... | ... | @@ -175,6 +175,8 @@ final class Notebook { |
| 175 | 175 | private(set) var name: String |
| 176 | 176 | private(set) var handle: OpaquePointer? |
| 177 | 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 | 180 | /// Why the notebook could not be opened, while it cannot. |
| 179 | 181 | private(set) var problem: String? |
| 180 | 182 | /// The Files location being read, held open while the notebook is. |
| ... | ... | @@ -288,8 +290,12 @@ final class Notebook { |
| 288 | 290 | return done() |
| 289 | 291 | } |
| 290 | 292 | 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 | |
| 292 | 297 | if let tabs { self.tabs = tabs } |
| 298 | self.color = color < 0 ? nil : UIColor(rgb: [16, 8, 0].map { UInt8(truncatingIfNeeded: color >> $0) }) | |
| 293 | 299 | download() |
| 294 | 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 | 29 | char **error); |
| 30 | 30 | bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error); |
| 31 | 31 | char *sb_library_sections(const Library *library); |
| 32 | int32_t sb_library_color(const Library *library); | |
| 32 | 33 | char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author, |
| 33 | 34 | const char *date, const char *time); |
| 34 | 35 | char *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 | 541 | Ok(unlocked) |
| 542 | 542 | } |
| 543 | 543 | |
| 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 | 549 | /// The notebook's sections, read again. |
| 545 | 550 | pub(crate) fn tabs(&self) -> Result<Vec<Tab>> { |
| 546 | 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 | 1410 | json(library.tabs()) |
| 1406 | 1411 | } |
| 1407 | 1412 | |
| 1413 | /// The notebook's colour as `0xRRGGBB`, as its sections were last read; -1 where it has none. | |
| 1414 | #[unsafe(no_mangle)] | |
| 1415 | pub 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 | 1422 | /// Creates a section named `name` in the group at catalog path `folder` ("" for the |
| 1409 | 1423 | /// notebook's top) holding one page titled with `date` and `time`, as OneNote creates one; |
| 1410 | 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 | 105 | } |
| 106 | 106 | |
| 107 | 107 | #[test] |
| 108 | fn a_new_notebook_opens_with_one_section_and_refuses_a_taken_name() { | |
| 108 | fn a_new_notebook_opens_with_one_section_in_its_colour_and_refuses_a_taken_name() { | |
| 109 | 109 | let directory = tempfile::tempdir().unwrap(); |
| 110 | 110 | let root = directory.path().join("Mine"); |
| 111 | 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 | 128 | let tabs = library.tabs().unwrap(); |
| 129 | 129 | assert_eq!(tabs.len(), 1); |
| 130 | 130 | assert!(tabs[0].readable); |
| 131 | assert_eq!(library::sb_library_color(&library), 0x91baae); | |
| 131 | 132 | assert!(!create(&mut error)); |
| 132 | 133 | assert!(!error.is_null()); |
| 133 | 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 | 3 | <linearGradient id="g0" x2="0" y2="1"><stop offset="0" stop-color="#ff8f80"/><stop offset="1" stop-color="#f03b30"/></linearGradient> |
| 4 | 4 | <linearGradient id="g1" x2="0" y2="1"><stop offset="0" stop-color="#86e88a"/><stop offset="1" stop-color="#28b04a"/></linearGradient> |
| 5 | 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 | 7 | <linearGradient id="g4" x2="0" y2="1"><stop offset="0" stop-color="#fffdf3"/><stop offset="1" stop-color="#fbeec7"/></linearGradient> |
| 8 | 8 | </defs> |
| 9 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 28 | pub const ITALIC: &[&str] = art!("icons/italic"); |
| 29 | 29 | pub const LINK: &[&str] = art!("icons/link"); |
| 30 | 30 | pub const NOTEBOOK: &[&str] = art!("icons/notebook"); |
| 31 | /// A notebook iCloud Drive keeps: its glyph with a cloud at the corner. | |
| 32 | pub const NOTEBOOK_ICLOUD: &[&str] = &[ | |
| 33 | NOTEBOOK[0], | |
| 34 | include_str!("../assets/icons/icloud-badge.svg"), | |
| 35 | ]; | |
| 31 | 36 | pub const NUMBERING: &[&str] = art!("icons/numbering"); |
| 32 | 37 | pub const OPTIONS: &[&str] = art!("icons/options"); |
| 33 | 38 | pub const OUTDENT: &[&str] = art!("icons/outdent"); |
| ... | ... | @@ -67,6 +72,7 @@ pub const RECORD_AUDIO: &[&str] = art!("icons/record-audio"); |
| 67 | 72 | pub const RECORD_VIDEO: &[&str] = art!("icons/record-video"); |
| 68 | 73 | pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping"); |
| 69 | 74 | pub const SPELLING: &[&str] = art!("icons/spelling"); |
| 75 | pub const SWATCH: &[&str] = art!("icons/swatch"); | |
| 70 | 76 | pub const SYMBOL: &[&str] = art!("icons/symbol"); |
| 71 | 77 | pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy"); |
| 72 | 78 | pub const SYNC_DONE: &[&str] = art!("icons/sync-done"); |
crates/snowbound/src/commands.rs+1-1| ... | ... | @@ -1373,7 +1373,7 @@ impl State { |
| 1373 | 1373 | } |
| 1374 | 1374 | |
| 1375 | 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 | 1377 | self.session |
| 1378 | 1378 | .as_ref() |
| 1379 | 1379 | .map(|session| &session.library) |
crates/snowbound/src/main.rs+9-3| ... | ... | @@ -1724,7 +1724,7 @@ impl State { |
| 1724 | 1724 | if self.navigation_bar_right { |
| 1725 | 1725 | self.sidebar(&theme, sidebar); |
| 1726 | 1726 | } |
| 1727 | self.sidebar_button(&theme, height); | |
| 1727 | self.sidebar_button(height); | |
| 1728 | 1728 | self.ui.close(); |
| 1729 | 1729 | self.context_menu(); |
| 1730 | 1730 | return Ok((section, open_tab, None)); |
| ... | ... | @@ -1737,7 +1737,7 @@ impl State { |
| 1737 | 1737 | if self.navigation_bar_right { |
| 1738 | 1738 | self.sidebar(&theme, sidebar); |
| 1739 | 1739 | } |
| 1740 | self.sidebar_button(&theme, height); | |
| 1740 | self.sidebar_button(height); | |
| 1741 | 1741 | self.ui.close(); |
| 1742 | 1742 | self.context_menu(); |
| 1743 | 1743 | return Ok((section, open_tab, None)); |
| ... | ... | @@ -1823,7 +1823,7 @@ impl State { |
| 1823 | 1823 | self.sidebar(&theme, sidebar); |
| 1824 | 1824 | } |
| 1825 | 1825 | self.task_pane(&theme); |
| 1826 | self.sidebar_button(&theme, height); | |
| 1826 | self.sidebar_button(height); | |
| 1827 | 1827 | self.ui.close(); |
| 1828 | 1828 | self.context_menu(); |
| 1829 | 1829 | // No tab stands on the frame in Full Page View. |
| ... | ... | @@ -4467,6 +4467,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] { |
| 4467 | 4467 | color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref) |
| 4468 | 4468 | } |
| 4469 | 4469 | |
| 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. | |
| 4472 | fn notebook_color(color: Option<u32>) -> [f32; 4] { | |
| 4473 | color.map_or(draw::srgb(0xf3, 0x9c, 0x28), canvas::gpu::colorref) | |
| 4474 | } | |
| 4475 | ||
| 4470 | 4476 | /// OneNote's information bar for conflicting changes, clicked anywhere: above a page with |
| 4471 | 4477 | /// conflict pages it shows or hides them; above a conflict page it opens OneNote's menu, |
| 4472 | 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 | 575 | } else { |
| 576 | 576 | 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 | 583 | .collect() |
| 581 | 584 | } |
crates/snowbound/src/palette.rs+2| ... | ... | @@ -97,6 +97,7 @@ impl State { |
| 97 | 97 | .map(|library| Row { |
| 98 | 98 | text: library.name.clone(), |
| 99 | 99 | icon: Some(art::NOTEBOOK), |
| 100 | tint: Some(crate::notebook_color(library.color())), | |
| 100 | 101 | target: Some(Target::Notebook(Arc::clone(library))), |
| 101 | 102 | ..Row::default() |
| 102 | 103 | }), |
| ... | ... | @@ -287,6 +288,7 @@ impl State { |
| 287 | 288 | .to_owned(), |
| 288 | 289 | after: "Closed".to_owned(), |
| 289 | 290 | icon: Some(art::NOTEBOOK), |
| 291 | tint: Some(crate::notebook_color(None)), | |
| 290 | 292 | target: Some(target), |
| 291 | 293 | ..Row::default() |
| 292 | 294 | }); |
crates/snowbound/src/properties.rs+2-2| ... | ... | @@ -139,8 +139,8 @@ impl State { |
| 139 | 139 | .iter() |
| 140 | 140 | .map(|&(color, text)| Item { |
| 141 | 141 | 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))), | |
| 144 | 144 | current: Some(color) == shown, |
| 145 | 145 | ..Item::default() |
| 146 | 146 | }) |
crates/snowbound/src/sidebar.rs+53-22| ... | ... | @@ -208,11 +208,14 @@ fn sidebar( |
| 208 | 208 | ui.id(("notebook", index)), |
| 209 | 209 | Row { |
| 210 | 210 | 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 | ), | |
| 216 | 219 | depth: 0, |
| 217 | 220 | dim: library.notebook.is_err(), |
| 218 | 221 | fold: Some(unfolded), |
| ... | ... | @@ -250,9 +253,24 @@ fn sidebar( |
| 250 | 253 | }, |
| 251 | 254 | ); |
| 252 | 255 | 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 | ), | |
| 256 | 274 | ] { |
| 257 | 275 | let (row, _) = tree_row( |
| 258 | 276 | ui, |
| ... | ... | @@ -260,7 +278,7 @@ fn sidebar( |
| 260 | 278 | ui.id(part), |
| 261 | 279 | Row { |
| 262 | 280 | label, |
| 263 | icon: Leading::Icon(icon), | |
| 281 | icon, | |
| 264 | 282 | depth: 0, |
| 265 | 283 | dim: false, |
| 266 | 284 | fold: None, |
| ... | ... | @@ -584,10 +602,23 @@ fn entry(ui: &Ui, tree: &mut Tree, notebook: usize, path: &str, id: Id, group: b |
| 584 | 602 | #[derive(Clone, Copy)] |
| 585 | 603 | enum Leading { |
| 586 | 604 | Icon(&'static [&'static str]), |
| 605 | /// A notebook's glyph, drawn in its colour (COLORREF). | |
| 606 | Notebook(&'static [&'static str], Option<u32>), | |
| 587 | 607 | /// A section's tab, drawn in its colour. |
| 588 | 608 | Section([f32; 4]), |
| 589 | 609 | } |
| 590 | 610 | |
| 611 | impl 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 | ||
| 591 | 622 | struct Row<'a> { |
| 592 | 623 | label: &'a str, |
| 593 | 624 | icon: Leading, |
| ... | ... | @@ -633,10 +664,7 @@ fn tree_row(ui: &mut Ui, tree: &mut Tree, id: Id, row: Row) -> (Signal, bool) { |
| 633 | 664 | } |
| 634 | 665 | } |
| 635 | 666 | 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); | |
| 640 | 668 | // Coloured art keeps its colours, so a dim row fades its icon. |
| 641 | 669 | let alpha = if row.dim { 0.5 } else { 1.0 }; |
| 642 | 670 | ui.leaf( |
| ... | ... | @@ -814,7 +842,7 @@ impl crate::State { |
| 814 | 842 | |
| 815 | 843 | /// The notebook button, floating at the body's corner over the section tabs' row, `height` |
| 816 | 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 | 846 | if self.temporary { |
| 819 | 847 | return; |
| 820 | 848 | } |
| ... | ... | @@ -839,7 +867,8 @@ impl crate::State { |
| 839 | 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 | 872 | self.sidebar = !self.sidebar; |
| 844 | 873 | self.save_settings(); |
| 845 | 874 | } |
| ... | ... | @@ -1155,7 +1184,7 @@ impl crate::State { |
| 1155 | 1184 | #[cfg(target_os = "linux")] |
| 1156 | 1185 | let install = crate::desktop::installable().then_some(( |
| 1157 | 1186 | "install", |
| 1158 | art::PLUS, | |
| 1187 | Leading::Icon(art::PLUS), | |
| 1159 | 1188 | "Install Snowbound", |
| 1160 | 1189 | crate::Command::Install, |
| 1161 | 1190 | )); |
| ... | ... | @@ -1169,19 +1198,19 @@ impl crate::State { |
| 1169 | 1198 | [ |
| 1170 | 1199 | ( |
| 1171 | 1200 | "new", |
| 1172 | art::PLUS, | |
| 1201 | Leading::Icon(art::PLUS), | |
| 1173 | 1202 | "New Notebook", |
| 1174 | 1203 | crate::Command::NewNotebook, |
| 1175 | 1204 | ), |
| 1176 | 1205 | ( |
| 1177 | 1206 | "open", |
| 1178 | art::NOTEBOOK, | |
| 1207 | Leading::Notebook(art::NOTEBOOK, None), | |
| 1179 | 1208 | "Open Existing", |
| 1180 | 1209 | crate::Command::OpenNotebook, |
| 1181 | 1210 | ), |
| 1182 | 1211 | ( |
| 1183 | 1212 | "server", |
| 1184 | art::SERVER, | |
| 1213 | Leading::Icon(art::SERVER), | |
| 1185 | 1214 | "Open Notebook from Server…", |
| 1186 | 1215 | crate::Command::OpenFromServer(None), |
| 1187 | 1216 | ), |
| ... | ... | @@ -1189,7 +1218,7 @@ impl crate::State { |
| 1189 | 1218 | .into_iter() |
| 1190 | 1219 | .chain(crate::guide::OFFERED.then_some(( |
| 1191 | 1220 | "guide", |
| 1192 | art::PAGE, | |
| 1221 | Leading::Icon(art::PAGE), | |
| 1193 | 1222 | "Open the Snowbound Guide", |
| 1194 | 1223 | crate::Command::OpenGuide, |
| 1195 | 1224 | ))) |
| ... | ... | @@ -1214,7 +1243,7 @@ impl crate::State { |
| 1214 | 1243 | } else { |
| 1215 | 1244 | ( |
| 1216 | 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 | 1249 | self.notice(id, title, &[], buttons); |
| ... | ... | @@ -1227,7 +1256,7 @@ impl crate::State { |
| 1227 | 1256 | id: ui::Id, |
| 1228 | 1257 | title: &str, |
| 1229 | 1258 | servers: &[String], |
| 1230 | buttons: Vec<(&str, &'static [&'static str], &str, crate::Command)>, | |
| 1259 | buttons: Vec<(&str, Leading, &str, crate::Command)>, | |
| 1231 | 1260 | ) { |
| 1232 | 1261 | const BUTTON: [f32; 2] = [240.0, 32.0]; |
| 1233 | 1262 | let theme = self.page_area_theme(); |
| ... | ... | @@ -1266,12 +1295,14 @@ impl crate::State { |
| 1266 | 1295 | ); |
| 1267 | 1296 | let mut chosen = None; |
| 1268 | 1297 | for (part, icon, label, command) in buttons { |
| 1298 | let (icon, tint) = icon.art(&theme); | |
| 1269 | 1299 | let button = self.ui.leaf( |
| 1270 | 1300 | part, |
| 1271 | 1301 | Spec { |
| 1272 | 1302 | flags: Flags::CLICKABLE, |
| 1273 | 1303 | size: [fill(), px(BUTTON[1])], |
| 1274 | 1304 | icon: Some(icon), |
| 1305 | tint: Some(tint), | |
| 1275 | 1306 | text: Some(label), |
| 1276 | 1307 | fill: Some(theme.chip), |
| 1277 | 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 | 249 | /// What the popup shows of the notebook. |
| 250 | 250 | struct Facts<'a> { |
| 251 | 251 | name: &'a str, |
| 252 | /// The notebook's colour, COLORREF. | |
| 253 | color: Option<u32>, | |
| 252 | 254 | /// The notebook's folder or address in full. |
| 253 | 255 | location: &'a str, |
| 254 | 256 | place: Vec<String>, |
| ... | ... | @@ -679,6 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { |
| 679 | 681 | size: [fill(), px(line)], |
| 680 | 682 | text: Some(facts.name), |
| 681 | 683 | icon: Some(art::NOTEBOOK), |
| 684 | tint: Some(crate::notebook_color(facts.color)), | |
| 682 | 685 | bold: true, |
| 683 | 686 | overflow: Overflow::Ellipsis, |
| 684 | 687 | ..Spec::default() |
| ... | ... | @@ -1026,6 +1029,7 @@ impl State { |
| 1026 | 1029 | }; |
| 1027 | 1030 | let facts = Facts { |
| 1028 | 1031 | name: &library.name, |
| 1032 | color: library.color(), | |
| 1029 | 1033 | location: &library.location, |
| 1030 | 1034 | place: library.place(), |
| 1031 | 1035 | notice: library.notice.as_deref(), |
| ... | ... | @@ -1132,6 +1136,7 @@ mod tests { |
| 1132 | 1136 | fn facts(sections: Vec<(String, SyncStatus)>) -> Facts<'static> { |
| 1133 | 1137 | Facts { |
| 1134 | 1138 | name: "OneNote", |
| 1139 | color: None, | |
| 1135 | 1140 | location: "/Volumes/clover/Documents/OneNote", |
| 1136 | 1141 | place: ["zenith", "clover", "Documents"] |
| 1137 | 1142 | .map(str::to_owned) |
crates/ui/src/lib.rs+5-2| ... | ... | @@ -368,8 +368,9 @@ pub struct Spec<'a> { |
| 368 | 368 | pub hover_border: Option<[f32; 4]>, |
| 369 | 369 | pub radius: f32, |
| 370 | 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 | 372 | pub icon: Option<&'static [&'static str]>, |
| 373 | pub tint: Option<[f32; 4]>, | |
| 373 | 374 | /// A 16 px picture in the icon's place beside a label; without one, the picture fills |
| 374 | 375 | /// the box inside its padding. |
| 375 | 376 | pub image: Option<&'a RasterImage>, |
| ... | ... | @@ -522,6 +523,7 @@ struct Built { |
| 522 | 523 | radius: f32, |
| 523 | 524 | shape: Shape, |
| 524 | 525 | icon: Option<&'static [&'static str]>, |
| 526 | tint: [f32; 4], | |
| 525 | 527 | image: Option<RasterImage>, |
| 526 | 528 | pad: [f32; 2], |
| 527 | 529 | gap: f32, |
| ... | ... | @@ -1705,7 +1707,7 @@ impl Ui { |
| 1705 | 1707 | self.display.push(Display::Icon { |
| 1706 | 1708 | sources, |
| 1707 | 1709 | origin: [x, top], |
| 1708 | tint: node.color, | |
| 1710 | tint: node.tint, | |
| 1709 | 1711 | }); |
| 1710 | 1712 | x += ICON + ICON_GAP; |
| 1711 | 1713 | } else if let Some(image) = &node.image { |
| ... | ... | @@ -2030,6 +2032,7 @@ impl Built { |
| 2030 | 2032 | radius: spec.radius, |
| 2031 | 2033 | shape: spec.shape, |
| 2032 | 2034 | icon: spec.icon, |
| 2035 | tint: spec.tint.or(spec.color).unwrap_or(text), | |
| 2033 | 2036 | image: spec.image.cloned(), |
| 2034 | 2037 | pad: spec.pad, |
| 2035 | 2038 | gap: spec.gap, |