authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 15:55:34-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 16:11:25-07:00
logf3b369e572b4a689a2e3c0337a0ea4be5da31fd6
treed0d0009e5599beb778a54a6f4aab19cc7a37cbff
parent1a9bd5592f8f9bd6781eb91b4b2c383af90a4f9b
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

feat: a notebook's colour recolours its binder art on the desktop and in iOS's list

The binder is three layers, its back and tabs, the cover currentColor paints and what lies on the cover, which iOS's asset catalog links to and composes with the cover as a template. The desktop shades the colour as it shades a section's, for each appearance. Assisted-by: claude-opus-5.5

20 files changed, 138 insertions(+), 36 deletions(-)

apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/Contents.json created+16
...@@ -0,0 +1,16 @@
1{
2 "images" : [
3 {
4 "filename" : "notebook.svg",
5 "idiom" : "universal"
6 }
7 ],
8 "info" : {
9 "author" : "xcode",
10 "version" : 1
11 },
12 "properties" : {
13 "preserves-vector-representation" : true,
14 "template-rendering-intent" : "original"
15 }
16}
apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/notebook.svg created+1
...@@ -0,0 +1 @@
1../../../../../crates/snowbound/assets/icons/notebook.svg
\ No newline at end of file
apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/Contents.json created+16
...@@ -0,0 +1,16 @@
1{
2 "images" : [
3 {
4 "filename" : "notebook-back.svg",
5 "idiom" : "universal"
6 }
7 ],
8 "info" : {
9 "author" : "xcode",
10 "version" : 1
11 },
12 "properties" : {
13 "preserves-vector-representation" : true,
14 "template-rendering-intent" : "original"
15 }
16}
apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/notebook-back.svg created+1
...@@ -0,0 +1 @@
1../../../../../crates/snowbound/assets/icons/notebook-back.svg
\ No newline at end of file
apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/Contents.json created+16
...@@ -0,0 +1,16 @@
1{
2 "images" : [
3 {
4 "filename" : "notebook-cover.svg",
5 "idiom" : "universal"
6 }
7 ],
8 "info" : {
9 "author" : "xcode",
10 "version" : 1
11 },
12 "properties" : {
13 "preserves-vector-representation" : true,
14 "template-rendering-intent" : "template"
15 }
16}
apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/notebook-cover.svg created+1
...@@ -0,0 +1 @@
1../../../../../crates/snowbound/assets/icons/notebook-cover.svg
\ No newline at end of file
apps/ios/Snowbound/Lists.swift+1-2
...@@ -307,8 +307,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke...@@ -307,8 +307,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke
307 case .notebook(let id):307 case .notebook(let id):
308 guard let notebook = notebook(id) else { break }308 guard let notebook = notebook(id) else { break }
309 content.text = notebook.name309 content.text = notebook.name
310 content.image = UIImage(systemName: "book.closed.fill")310 content.image = .notebook(notebook.color)
311 content.imageProperties.tintColor = notebook.color
312 var details: [String] = []311 var details: [String] = []
313 if case .server(let server) = notebook.source { details.append(server.host) }312 if case .server(let server) = notebook.source { details.append(server.host) }
314 if let state = attention[id]?.state {313 if let state = attention[id]?.state {
apps/ios/Snowbound/Notebooks.swift+15-1
...@@ -140,6 +140,20 @@ struct Tab: Decodable, Equatable {...@@ -140,6 +140,20 @@ struct Tab: Decodable, Equatable {
140 var uiColor: UIColor { UIColor(rgb: color) }140 var uiColor: UIColor { UIColor(rgb: color) }
141}141}
142142
143extension UIImage {
144 /// A notebook's binder, the desktop's art, `side` points square: its cover in `color`, or
145 /// the art's own orange.
146 static func notebook(_ color: UIColor?, side: CGFloat = 28) -> UIImage {
147 let frame = CGRect(x: 0, y: 0, width: side, height: side)
148 let cover = color ?? UIColor(rgb: [0xf3, 0x9c, 0x28])
149 return UIGraphicsImageRenderer(size: frame.size).image { _ in
150 UIImage(named: "NotebookBack")?.draw(in: frame)
151 UIImage(named: "NotebookCover")?.withTintColor(cover, renderingMode: .alwaysOriginal).draw(in: frame)
152 UIImage(named: "Notebook")?.draw(in: frame)
153 }.withRenderingMode(.alwaysOriginal)
154 }
155}
156
143extension UIViewController {157extension UIViewController {
144 /// Tells the reader `title` and `message` in an alert they dismiss with OK.158 /// Tells the reader `title` and `message` in an alert they dismiss with OK.
145 func alert(_ title: String, _ message: String) {159 func alert(_ title: String, _ message: String) {
...@@ -175,7 +189,7 @@ final class Notebook {...@@ -175,7 +189,7 @@ final class Notebook {
175 private(set) var name: String189 private(set) var name: String
176 private(set) var handle: OpaquePointer?190 private(set) var handle: OpaquePointer?
177 private(set) var tabs: [Tab] = []191 private(set) var tabs: [Tab] = []
178 /// The notebook's colour, which tints its book in the list; none where it has none.192 /// The notebook's colour, which its binder's cover takes in the list; none where it has none.
179 private(set) var color: UIColor?193 private(set) var color: UIColor?
180 /// Why the notebook could not be opened, while it cannot.194 /// Why the notebook could not be opened, while it cannot.
181 private(set) var problem: String?195 private(set) var problem: String?
apps/ios/Snowbound/Prototypes.swift+2-2
...@@ -38,7 +38,7 @@ extension Prototype {...@@ -38,7 +38,7 @@ extension Prototype {
38 ) -> UIContentUnavailableConfiguration? {38 ) -> UIContentUnavailableConfiguration? {
39 guard Notebooks.all.isEmpty, !ICloud.signedIn || ICloud.documents != nil else { return nil }39 guard Notebooks.all.isEmpty, !ICloud.signedIn || ICloud.documents != nil else { return nil }
40 var welcome = UIContentUnavailableConfiguration.empty()40 var welcome = UIContentUnavailableConfiguration.empty()
41 welcome.image = UIImage(systemName: "book.closed")41 welcome.image = .notebook(nil, side: 64)
42 welcome.text = "No Notebooks Yet"42 welcome.text = "No Notebooks Yet"
43 welcome.secondaryText =43 welcome.secondaryText =
44 ICloud.documents != nil44 ICloud.documents != nil
...@@ -286,7 +286,7 @@ enum QuickNote {...@@ -286,7 +286,7 @@ enum QuickNote {
286 let notebooks = Notebooks.all.filter { $0.tabs.contains(where: \.readable) }286 let notebooks = Notebooks.all.filter { $0.tabs.contains(where: \.readable) }
287 let choices = notebooks.map { notebook in287 let choices = notebooks.map { notebook in
288 UIMenu(288 UIMenu(
289 title: notebook.name, image: UIImage(systemName: "book.closed"),289 title: notebook.name, image: .notebook(notebook.color),
290 children: notebook.tabs.filter(\.readable).map { tab in290 children: notebook.tabs.filter(\.readable).map { tab in
291 let chosen = Place(notebook: notebook.id, section: tab.path)291 let chosen = Place(notebook: notebook.id, section: tab.path)
292 let action = UIAction(292 let action = UIAction(
crates/draw/src/render/icon.rs+22-3
...@@ -33,7 +33,7 @@ impl Palette {...@@ -33,7 +33,7 @@ impl Palette {
33/// Rasterizes 16×16 SVG sources over each other at a whole `size` of device pixels. Paths are filled and stroked with `#rrggbb`, `currentColor`,33/// Rasterizes 16×16 SVG sources over each other at a whole `size` of device pixels. Paths are filled and stroked with `#rrggbb`, `currentColor`,
34/// which paints the linear `ink`, or two-stop `linearGradient`s with SVG's defaults34/// which paints the linear `ink`, or two-stop `linearGradient`s with SVG's defaults
35/// (`objectBoundingBox` unless `userSpaceOnUse`, `x1`..`y2`, stop offsets, pad spread; no35/// (`objectBoundingBox` unless `userSpaceOnUse`, `x1`..`y2`, stop offsets, pad spread; no
36/// `gradientTransform`). `fill-opacity` and `stroke-opacity` apply, and strokes have round36/// `gradientTransform`). `fill-opacity`, `stroke-opacity` and `stop-opacity` apply, and strokes have round
37/// caps and joins. A slot's paths keep the shading among their colours: the hue turn,37/// caps and joins. A slot's paths keep the shading among their colours: the hue turn,
38/// saturation scale and lightness shift that carry the mean of the slot's colours in a38/// saturation scale and lightness shift that carry the mean of the slot's colours in a
39/// source onto the palette's colour apply to each of them. A source's `image`s of PNG39/// source onto the palette's colour apply to each of them. A source's `image`s of PNG
...@@ -141,7 +141,8 @@ pub(crate) fn rasterize(sources: &[&str], size: u32, ink: [f32; 3], palette: &Pa...@@ -141,7 +141,8 @@ pub(crate) fn rasterize(sources: &[&str], size: u32, ink: [f32; 3], palette: &Pa
141 let [x, y] = [index % side as usize, index / side as usize]141 let [x, y] = [index % side as usize, index / side as usize]
142 .map(|device| device as f32 + 0.5)142 .map(|device| device as f32 + 0.5)
143 .map(|device| device * 16.0 / size);143 .map(|device| device * 16.0 / size);
144 over(pixel, color([x, y]), alpha);144 let color = color([x, y]);
145 over(pixel, color, alpha * color[3]);
145 }146 }
146 }147 }
147 }148 }
...@@ -287,8 +288,12 @@ impl Paint {...@@ -287,8 +288,12 @@ impl Paint {
287 .children()288 .children()
288 .filter(|node| node.has_tag_name("stop"))289 .filter(|node| node.has_tag_name("stop"))
289 .map(|node| {290 .map(|node| {
291 let [red, green, blue, _] = hex(node.attribute("stop-color").unwrap());
292 let opacity = node
293 .attribute("stop-opacity")
294 .map_or(1.0, |value| value.parse().unwrap());
290 (295 (
291 hex(node.attribute("stop-color").unwrap()),296 [red, green, blue, opacity],
292 node.attribute("offset")297 node.attribute("offset")
293 .map_or(0.0, |offset| number(offset, 1.0)),298 .map_or(0.0, |offset| number(offset, 1.0)),
294 )299 )
...@@ -567,6 +572,20 @@ mod tests {...@@ -567,6 +572,20 @@ mod tests {
567 assert_eq!(alpha(1, 8), 64);572 assert_eq!(alpha(1, 8), 64);
568 }573 }
569574
575 #[test]
576 fn stop_opacity_fades_a_gradient() {
577 const FADE: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="g" x2="0" y2="1"><stop offset="0" stop-color="#ffffff"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></linearGradient></defs><path d="M0 0H16V16H0Z" fill="url(#g)"/></svg>"##;
578 let image = rasterize(&[FADE], 16, [1.0; 3], &Palette::default());
579 let alpha = |y: usize| image.data[(y * 16 + 8) * 4 + 3];
580 assert!(
581 alpha(0) > 240 && alpha(15) < 15,
582 "{} {}",
583 alpha(0),
584 alpha(15)
585 );
586 assert!((i32::from(alpha(8)) - 120).abs() < 10, "{}", alpha(8));
587 }
588
570 /// A 2×1 PNG, red then half-transparent blue, fits the icon's width, centred, and paints589 /// A 2×1 PNG, red then half-transparent blue, fits the icon's width, centred, and paints
571 /// beneath the paths of later sources.590 /// beneath the paths of later sources.
572 #[test]591 #[test]
crates/snowbound/assets/icons/notebook-back.svg created+14
...@@ -0,0 +1,14 @@
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="#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>
5 <linearGradient id="g2" x2="0" y2="1"><stop offset="0" stop-color="#79c2ff"/><stop offset="1" stop-color="#1f7cf0"/></linearGradient>
6 </defs>
7 <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"/>
8 <path d="M12.1 3H14.4A0.6 0.6 0 0 1 15 3.6V4.9A0.6 0.6 0 0 1 14.4 5.5H12.1A0.6 0.6 0 0 1 11.5 4.9V3.6A0.6 0.6 0 0 1 12.1 3Z" fill="url(#g0)" stroke="#c42418" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
9 <path d="M12.1 7.25L14.4 7.25A0.6 0.6 0 0 1 15 7.85L15 9.15A0.6 0.6 0 0 1 14.4 9.75L12.1 9.75A0.6 0.6 0 0 1 11.5 9.15L11.5 7.85A0.6 0.6 0 0 1 12.1 7.25Z" fill="#000000" fill-opacity="0.16"/>
10 <path d="M12.1 6.5H14.4A0.6 0.6 0 0 1 15 7.1V8.4A0.6 0.6 0 0 1 14.4 9H12.1A0.6 0.6 0 0 1 11.5 8.4V7.1A0.6 0.6 0 0 1 12.1 6.5Z" fill="url(#g1)" stroke="#1b8a36" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
11 <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"/>
12 <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"/>
13 <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"/>
14</svg>
crates/snowbound/assets/icons/notebook-cover.svg created+3
...@@ -0,0 +1,3 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <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"/>
3</svg>
crates/snowbound/assets/icons/notebook.svg+5-16
...@@ -1,21 +1,10 @@...@@ -1,21 +1,10 @@
1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">1<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
2 <defs>2 <defs>
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="#ffffff" stop-opacity="0.4"/><stop offset="1" stop-color="#ffffff" stop-opacity="0"/></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="#fffdf3"/><stop offset="1" stop-color="#fbeec7"/></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="#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>
8 </defs>5 </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"/>6 <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(#g0)" stroke="#000000" stroke-opacity="0.22" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
10 <path d="M12.1 3H14.4A0.6 0.6 0 0 1 15 3.6V4.9A0.6 0.6 0 0 1 14.4 5.5H12.1A0.6 0.6 0 0 1 11.5 4.9V3.6A0.6 0.6 0 0 1 12.1 3Z" fill="url(#g0)" stroke="#c42418" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>7 <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(#g1)"/>
11 <path d="M12.1 7.25L14.4 7.25A0.6 0.6 0 0 1 15 7.85L15 9.15A0.6 0.6 0 0 1 14.4 9.75L12.1 9.75A0.6 0.6 0 0 1 11.5 9.15L11.5 7.85A0.6 0.6 0 0 1 12.1 7.25Z" fill="#000000" fill-opacity="0.16"/>8 <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.6"/>
12 <path d="M12.1 6.5H14.4A0.6 0.6 0 0 1 15 7.1V8.4A0.6 0.6 0 0 1 14.4 9H12.1A0.6 0.6 0 0 1 11.5 8.4V7.1A0.6 0.6 0 0 1 12.1 6.5Z" fill="url(#g1)" stroke="#1b8a36" stroke-width="0.8" stroke-linecap="round" stroke-linejoin="round"/>
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"/>
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="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"/>
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)"/>
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"/>
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"/>9 <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"/>
21</svg>10</svg>
crates/snowbound/src/art.rs+9-1
...@@ -27,10 +27,18 @@ pub const INDENT: &[&str] = art!("icons/indent");...@@ -27,10 +27,18 @@ pub const INDENT: &[&str] = art!("icons/indent");
27pub const INFO: &[&str] = art!("icons/info");27pub 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");30/// A notebook: its back and tabs, the cover `currentColor` paints, and what lies on the cover.
31/// iOS layers the same files.
32pub const NOTEBOOK: &[&str] = &[
33 include_str!("../assets/icons/notebook-back.svg"),
34 include_str!("../assets/icons/notebook-cover.svg"),
35 include_str!("../assets/icons/notebook.svg"),
36];
31/// A notebook iCloud Drive keeps: its glyph with a cloud at the corner.37/// A notebook iCloud Drive keeps: its glyph with a cloud at the corner.
32pub const NOTEBOOK_ICLOUD: &[&str] = &[38pub const NOTEBOOK_ICLOUD: &[&str] = &[
33 NOTEBOOK[0],39 NOTEBOOK[0],
40 NOTEBOOK[1],
41 NOTEBOOK[2],
34 include_str!("../assets/icons/icloud-badge.svg"),42 include_str!("../assets/icons/icloud-badge.svg"),
35];43];
36pub const NUMBERING: &[&str] = art!("icons/numbering");44pub const NUMBERING: &[&str] = art!("icons/numbering");
crates/snowbound/src/main.rs+6-4
...@@ -4474,10 +4474,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] {...@@ -4474,10 +4474,12 @@ fn section_color(color: Option<u32>) -> [f32; 4] {
4474 color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref)4474 color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref)
4475}4475}
44764476
4477/// The colour a notebook's glyph takes from its COLORREF, as OneNote's navigation bar tints4477/// The colour a notebook's cover takes in `theme`: its COLORREF's hue at the shade a section's
4478/// it; the glyph's own orange without one.4478/// takes, or the glyph's own orange without one.
4479fn notebook_color(color: Option<u32>) -> [f32; 4] {4479fn notebook_color(theme: &Theme, color: Option<u32>) -> [f32; 4] {
4480 color.map_or(draw::srgb(0xf3, 0x9c, 0x28), canvas::gpu::colorref)4480 color.map_or(draw::srgb(0xf3, 0x9c, 0x28), |color| {
4481 theme.section(canvas::gpu::colorref(color)).accent
4482 })
4481}4483}
44824484
4483/// OneNote's information bar for conflicting changes, clicked anywhere: above a page with4485/// OneNote's information bar for conflicting changes, clicked anywhere: above a page with
crates/snowbound/src/menus.rs+1-1
...@@ -578,7 +578,7 @@ impl State {...@@ -578,7 +578,7 @@ impl State {
578 tint: place578 tint: place
579 .path579 .path
580 .is_empty()580 .is_empty()
581 .then(|| crate::notebook_color(library.color())),581 .then(|| crate::notebook_color(theme, library.color())),
582 })582 })
583 .collect()583 .collect()
584 }584 }
crates/snowbound/src/palette.rs+2-2
...@@ -97,7 +97,7 @@ impl State {...@@ -97,7 +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 tint: Some(crate::notebook_color(&self.ui.theme, library.color())),
101 target: Some(Target::Notebook(Arc::clone(library))),101 target: Some(Target::Notebook(Arc::clone(library))),
102 ..Row::default()102 ..Row::default()
103 }),103 }),
...@@ -288,7 +288,7 @@ impl State {...@@ -288,7 +288,7 @@ impl State {
288 .to_owned(),288 .to_owned(),
289 after: "Closed".to_owned(),289 after: "Closed".to_owned(),
290 icon: Some(art::NOTEBOOK),290 icon: Some(art::NOTEBOOK),
291 tint: Some(crate::notebook_color(None)),291 tint: Some(crate::notebook_color(&self.ui.theme, None)),
292 target: Some(target),292 target: Some(target),
293 ..Row::default()293 ..Row::default()
294 });294 });
crates/snowbound/src/properties.rs+1-1
...@@ -169,7 +169,7 @@ impl State {...@@ -169,7 +169,7 @@ impl State {
169 .map(|&(color, text)| Item {169 .map(|&(color, text)| Item {
170 text,170 text,
171 icon: Some(art::SWATCH),171 icon: Some(art::SWATCH),
172 tint: Some(crate::notebook_color(Some(color))),172 tint: Some(crate::notebook_color(&theme, Some(color))),
173 current: Some(color) == shown,173 current: Some(color) == shown,
174 ..Item::default()174 ..Item::default()
175 })175 })
crates/snowbound/src/sidebar.rs+5-2
...@@ -613,7 +613,7 @@ impl Leading {...@@ -613,7 +613,7 @@ impl Leading {
613 fn art(self, theme: &Theme) -> (&'static [&'static str], [f32; 4]) {613 fn art(self, theme: &Theme) -> (&'static [&'static str], [f32; 4]) {
614 match self {614 match self {
615 Self::Icon(icon) => (icon, theme.text),615 Self::Icon(icon) => (icon, theme.text),
616 Self::Notebook(icon, color) => (icon, crate::notebook_color(color)),616 Self::Notebook(icon, color) => (icon, crate::notebook_color(theme, color)),
617 Self::Section(section) => (art::SECTION, theme.section(section).accent),617 Self::Section(section) => (art::SECTION, theme.section(section).accent),
618 }618 }
619 }619 }
...@@ -867,7 +867,10 @@ impl crate::State {...@@ -867,7 +867,10 @@ impl crate::State {
867 ..Spec::default()867 ..Spec::default()
868 },868 },
869 );869 );
870 let tint = crate::notebook_color(self.notebook().and_then(|library| library.color()));870 let tint = crate::notebook_color(
871 &self.ui.theme,
872 self.notebook().and_then(|library| library.color()),
873 );
871 if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked {874 if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked {
872 self.sidebar = !self.sidebar;875 self.sidebar = !self.sidebar;
873 self.save_settings();876 self.save_settings();
crates/snowbound/src/sync.rs+1-1
...@@ -681,7 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {...@@ -681,7 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked {
681 size: [fill(), px(line)],681 size: [fill(), px(line)],
682 text: Some(facts.name),682 text: Some(facts.name),
683 icon: Some(art::NOTEBOOK),683 icon: Some(art::NOTEBOOK),
684 tint: Some(crate::notebook_color(facts.color)),684 tint: Some(crate::notebook_color(&theme, facts.color)),
685 bold: true,685 bold: true,
686 overflow: Overflow::Ellipsis,686 overflow: Overflow::Ellipsis,
687 ..Spec::default()687 ..Spec::default()