| 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,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.name | 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 | 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: String | 175 | 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 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 | 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); |
| 30 | bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error); | 30 | bool sb_notebook_moved(const char *cache, const char *from, const char *to, char **error); |
| 31 | char *sb_library_sections(const Library *library); | 31 | char *sb_library_sections(const Library *library); |
| 32 | int32_t sb_library_color(const Library *library); | ||
| 32 | char *sb_library_new_section(const Library *library, const char *folder, const char *name, const char *author, | 33 | char *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); |
| 34 | char *sb_library_search(const Library *library, const Section *open, const char *path, const char *query); | 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,6 +541,11 @@ impl Library { |
| 541 | Ok(unlocked) | 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 | /// 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 | } |
| 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 | /// Creates a section named `name` in the group at catalog path `folder` ("" for the | 1422 | /// 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 | } |
| 106 | 106 | ||
| 107 | #[test] | 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 | 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"); |
| 28 | pub const ITALIC: &[&str] = art!("icons/italic"); | 28 | pub const ITALIC: &[&str] = art!("icons/italic"); |
| 29 | pub const LINK: &[&str] = art!("icons/link"); | 29 | pub const LINK: &[&str] = art!("icons/link"); |
| 30 | pub const NOTEBOOK: &[&str] = art!("icons/notebook"); | 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 | pub const NUMBERING: &[&str] = art!("icons/numbering"); | 36 | pub const NUMBERING: &[&str] = art!("icons/numbering"); |
| 32 | pub const OPTIONS: &[&str] = art!("icons/options"); | 37 | pub const OPTIONS: &[&str] = art!("icons/options"); |
| 33 | pub const OUTDENT: &[&str] = art!("icons/outdent"); | 38 | pub 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"); |
| 67 | pub const RECORD_VIDEO: &[&str] = art!("icons/record-video"); | 72 | pub const RECORD_VIDEO: &[&str] = art!("icons/record-video"); |
| 68 | pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping"); | 73 | pub const SCREEN_CLIPPING: &[&str] = art!("icons/screen-clipping"); |
| 69 | pub const SPELLING: &[&str] = art!("icons/spelling"); | 74 | pub const SPELLING: &[&str] = art!("icons/spelling"); |
| 75 | pub const SWATCH: &[&str] = art!("icons/swatch"); | ||
| 70 | pub const SYMBOL: &[&str] = art!("icons/symbol"); | 76 | pub const SYMBOL: &[&str] = art!("icons/symbol"); |
| 71 | pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy"); | 77 | pub const SYNC_BUSY: &[&str] = art!("icons/sync-busy"); |
| 72 | pub const SYNC_DONE: &[&str] = art!("icons/sync-done"); | 78 | pub 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 | } |
| 1374 | 1374 | ||
| 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.session | 1377 | 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 | } |
| 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 | /// OneNote's information bar for conflicting changes, clicked anywhere: above a page with | 4476 | /// 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_GROUP | 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 | .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::ICLOUD | 212 | if library.in_icloud() { |
| 213 | } else { | 213 | art::NOTEBOOK_ICLOUD |
| 214 | art::NOTEBOOK | 214 | } 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)] |
| 585 | enum Leading { | 603 | enum 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 | } |
| 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 | struct Row<'a> { | 622 | struct 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 { |
| 814 | 842 | ||
| 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. |
| 250 | struct Facts<'a> { | 250 | struct 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 fills | 374 | /// 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, |