diff --git a/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/Contents.json b/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..76f6e8720706c05356c7dad8ead62cb90da88b6d --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "notebook.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/notebook.svg b/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/notebook.svg new file mode 120000 index 0000000000000000000000000000000000000000..12209db51e60353b127d53b0d945349624f5db19 --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/Notebook.imageset/notebook.svg @@ -0,0 +1 @@ +../../../../../crates/snowbound/assets/icons/notebook.svg \ No newline at end of file diff --git a/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/Contents.json b/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..2ba53ad9a963afde07fc042c8f967f75722edd36 --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "notebook-back.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "original" + } +} diff --git a/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/notebook-back.svg b/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/notebook-back.svg new file mode 120000 index 0000000000000000000000000000000000000000..2d0c53812d7bad84a1798e7ddb8beb22b57f5cd9 --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/NotebookBack.imageset/notebook-back.svg @@ -0,0 +1 @@ +../../../../../crates/snowbound/assets/icons/notebook-back.svg \ No newline at end of file diff --git a/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/Contents.json b/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/Contents.json new file mode 100644 index 0000000000000000000000000000000000000000..e431fe0e19567ced5a3ecf19ae694eed178fd020 --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "notebook-cover.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/notebook-cover.svg b/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/notebook-cover.svg new file mode 120000 index 0000000000000000000000000000000000000000..f3365539ce53d390585c441f1f990d0e42df1129 --- /dev/null +++ b/apps/ios/Snowbound/Assets.xcassets/NotebookCover.imageset/notebook-cover.svg @@ -0,0 +1 @@ +../../../../../crates/snowbound/assets/icons/notebook-cover.svg \ No newline at end of file diff --git a/apps/ios/Snowbound/Lists.swift b/apps/ios/Snowbound/Lists.swift index 35a0ca01c1b9eb1ab1e7f80c50acf5125c1a8c27..f21abbe15f369c6b5a66f60203fe30484c30fd7f 100644 --- a/apps/ios/Snowbound/Lists.swift +++ b/apps/ios/Snowbound/Lists.swift @@ -307,8 +307,7 @@ final class NotebooksViewController: UICollectionViewController, UIDocumentPicke case .notebook(let id): guard let notebook = notebook(id) else { break } content.text = notebook.name - content.image = UIImage(systemName: "book.closed.fill") - content.imageProperties.tintColor = notebook.color + content.image = .notebook(notebook.color) var details: [String] = [] if case .server(let server) = notebook.source { details.append(server.host) } if let state = attention[id]?.state { diff --git a/apps/ios/Snowbound/Notebooks.swift b/apps/ios/Snowbound/Notebooks.swift index 3db56082965da243ae83a71207667f6b4a47749c..88f959e34e99093a551b56921eb91a92d7b38982 100644 --- a/apps/ios/Snowbound/Notebooks.swift +++ b/apps/ios/Snowbound/Notebooks.swift @@ -140,6 +140,20 @@ struct Tab: Decodable, Equatable { var uiColor: UIColor { UIColor(rgb: color) } } +extension UIImage { + /// A notebook's binder, the desktop's art, `side` points square: its cover in `color`, or + /// the art's own orange. + static func notebook(_ color: UIColor?, side: CGFloat = 28) -> UIImage { + let frame = CGRect(x: 0, y: 0, width: side, height: side) + let cover = color ?? UIColor(rgb: [0xf3, 0x9c, 0x28]) + return UIGraphicsImageRenderer(size: frame.size).image { _ in + UIImage(named: "NotebookBack")?.draw(in: frame) + UIImage(named: "NotebookCover")?.withTintColor(cover, renderingMode: .alwaysOriginal).draw(in: frame) + UIImage(named: "Notebook")?.draw(in: frame) + }.withRenderingMode(.alwaysOriginal) + } +} + extension UIViewController { /// Tells the reader `title` and `message` in an alert they dismiss with OK. func alert(_ title: String, _ message: String) { @@ -175,7 +189,7 @@ final class Notebook { private(set) var name: String private(set) var handle: OpaquePointer? private(set) var tabs: [Tab] = [] - /// The notebook's colour, which tints its book in the list; none where it has none. + /// The notebook's colour, which its binder's cover takes in the list; none where it has none. private(set) var color: UIColor? /// Why the notebook could not be opened, while it cannot. private(set) var problem: String? diff --git a/apps/ios/Snowbound/Prototypes.swift b/apps/ios/Snowbound/Prototypes.swift index cea5b2d3b02cb5f8fbc1eb7a9be78291bc112038..f957a7f21f12206a62c76ab08cc808210d0ab2c3 100644 --- a/apps/ios/Snowbound/Prototypes.swift +++ b/apps/ios/Snowbound/Prototypes.swift @@ -38,7 +38,7 @@ extension Prototype { ) -> UIContentUnavailableConfiguration? { guard Notebooks.all.isEmpty, !ICloud.signedIn || ICloud.documents != nil else { return nil } var welcome = UIContentUnavailableConfiguration.empty() - welcome.image = UIImage(systemName: "book.closed") + welcome.image = .notebook(nil, side: 64) welcome.text = "No Notebooks Yet" welcome.secondaryText = ICloud.documents != nil @@ -286,7 +286,7 @@ enum QuickNote { let notebooks = Notebooks.all.filter { $0.tabs.contains(where: \.readable) } let choices = notebooks.map { notebook in UIMenu( - title: notebook.name, image: UIImage(systemName: "book.closed"), + title: notebook.name, image: .notebook(notebook.color), children: notebook.tabs.filter(\.readable).map { tab in let chosen = Place(notebook: notebook.id, section: tab.path) let action = UIAction( diff --git a/crates/draw/src/render/icon.rs b/crates/draw/src/render/icon.rs index 8b4574381d3835d3a2f988e0e18c874529c495c2..0167a1c1b0312e437c653da77fb4a34f3d6c9b08 100644 --- a/crates/draw/src/render/icon.rs +++ b/crates/draw/src/render/icon.rs @@ -33,7 +33,7 @@ impl Palette { /// Rasterizes 16×16 SVG sources over each other at a whole `size` of device pixels. Paths are filled and stroked with `#rrggbb`, `currentColor`, /// which paints the linear `ink`, or two-stop `linearGradient`s with SVG's defaults /// (`objectBoundingBox` unless `userSpaceOnUse`, `x1`..`y2`, stop offsets, pad spread; no -/// `gradientTransform`). `fill-opacity` and `stroke-opacity` apply, and strokes have round +/// `gradientTransform`). `fill-opacity`, `stroke-opacity` and `stop-opacity` apply, and strokes have round /// caps and joins. A slot's paths keep the shading among their colours: the hue turn, /// saturation scale and lightness shift that carry the mean of the slot's colours in a /// 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 let [x, y] = [index % side as usize, index / side as usize] .map(|device| device as f32 + 0.5) .map(|device| device * 16.0 / size); - over(pixel, color([x, y]), alpha); + let color = color([x, y]); + over(pixel, color, alpha * color[3]); } } } @@ -287,8 +288,12 @@ impl Paint { .children() .filter(|node| node.has_tag_name("stop")) .map(|node| { + let [red, green, blue, _] = hex(node.attribute("stop-color").unwrap()); + let opacity = node + .attribute("stop-opacity") + .map_or(1.0, |value| value.parse().unwrap()); ( - hex(node.attribute("stop-color").unwrap()), + [red, green, blue, opacity], node.attribute("offset") .map_or(0.0, |offset| number(offset, 1.0)), ) @@ -567,6 +572,20 @@ mod tests { assert_eq!(alpha(1, 8), 64); } + #[test] + fn stop_opacity_fades_a_gradient() { + const FADE: &str = r##""##; + let image = rasterize(&[FADE], 16, [1.0; 3], &Palette::default()); + let alpha = |y: usize| image.data[(y * 16 + 8) * 4 + 3]; + assert!( + alpha(0) > 240 && alpha(15) < 15, + "{} {}", + alpha(0), + alpha(15) + ); + assert!((i32::from(alpha(8)) - 120).abs() < 10, "{}", alpha(8)); + } + /// A 2×1 PNG, red then half-transparent blue, fits the icon's width, centred, and paints /// beneath the paths of later sources. #[test] diff --git a/crates/snowbound/assets/icons/notebook-back.svg b/crates/snowbound/assets/icons/notebook-back.svg new file mode 100644 index 0000000000000000000000000000000000000000..872d4f47707214cf3f5eb106a14bf2f7a13a3e8b --- /dev/null +++ b/crates/snowbound/assets/icons/notebook-back.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/crates/snowbound/assets/icons/notebook-cover.svg b/crates/snowbound/assets/icons/notebook-cover.svg new file mode 100644 index 0000000000000000000000000000000000000000..ad542b2a560edd7e27e88a61b8e583952d7ef65a --- /dev/null +++ b/crates/snowbound/assets/icons/notebook-cover.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/snowbound/assets/icons/notebook.svg b/crates/snowbound/assets/icons/notebook.svg index 9ba6a7f690207e1e0bd1bb5295c6d652241512ce..f8d356f52f14bd1d5a4b60ef30c817ff284d4cb4 100644 --- a/crates/snowbound/assets/icons/notebook.svg +++ b/crates/snowbound/assets/icons/notebook.svg @@ -1,21 +1,10 @@ - - - - - + + - - - - - - - - - - - + + + diff --git a/crates/snowbound/src/art.rs b/crates/snowbound/src/art.rs index c13dd6adc92fa9d322651a05ac4cfaa973ae36de..ae7b4fe72debcf94cca1c646f4f7ea02cf8db9da 100644 --- a/crates/snowbound/src/art.rs +++ b/crates/snowbound/src/art.rs @@ -27,10 +27,18 @@ pub const INDENT: &[&str] = art!("icons/indent"); pub const INFO: &[&str] = art!("icons/info"); pub const ITALIC: &[&str] = art!("icons/italic"); pub const LINK: &[&str] = art!("icons/link"); -pub const NOTEBOOK: &[&str] = art!("icons/notebook"); +/// A notebook: its back and tabs, the cover `currentColor` paints, and what lies on the cover. +/// iOS layers the same files. +pub const NOTEBOOK: &[&str] = &[ + include_str!("../assets/icons/notebook-back.svg"), + include_str!("../assets/icons/notebook-cover.svg"), + include_str!("../assets/icons/notebook.svg"), +]; /// A notebook iCloud Drive keeps: its glyph with a cloud at the corner. pub const NOTEBOOK_ICLOUD: &[&str] = &[ NOTEBOOK[0], + NOTEBOOK[1], + NOTEBOOK[2], include_str!("../assets/icons/icloud-badge.svg"), ]; pub const NUMBERING: &[&str] = art!("icons/numbering"); diff --git a/crates/snowbound/src/main.rs b/crates/snowbound/src/main.rs index ce1f294ad0ac219fd2ae0e4b70e94063cfef2b0d..17cacb4f0dc04e53db3fb00e96ef82c44d50d66e 100644 --- a/crates/snowbound/src/main.rs +++ b/crates/snowbound/src/main.rs @@ -4474,10 +4474,12 @@ fn section_color(color: Option) -> [f32; 4] { color.map_or(draw::srgb(0x8a, 0xa8, 0xe4), canvas::gpu::colorref) } -/// The colour a notebook's glyph takes from its COLORREF, as OneNote's navigation bar tints -/// it; the glyph's own orange without one. -fn notebook_color(color: Option) -> [f32; 4] { - color.map_or(draw::srgb(0xf3, 0x9c, 0x28), canvas::gpu::colorref) +/// The colour a notebook's cover takes in `theme`: its COLORREF's hue at the shade a section's +/// takes, or the glyph's own orange without one. +fn notebook_color(theme: &Theme, color: Option) -> [f32; 4] { + color.map_or(draw::srgb(0xf3, 0x9c, 0x28), |color| { + theme.section(canvas::gpu::colorref(color)).accent + }) } /// OneNote's information bar for conflicting changes, clicked anywhere: above a page with diff --git a/crates/snowbound/src/menus.rs b/crates/snowbound/src/menus.rs index 1995ca308721b1a2a603c40f478abeed060e7b90..c316fe3adaf26d20b684929aac62589670ea9ab2 100644 --- a/crates/snowbound/src/menus.rs +++ b/crates/snowbound/src/menus.rs @@ -578,7 +578,7 @@ impl State { tint: place .path .is_empty() - .then(|| crate::notebook_color(library.color())), + .then(|| crate::notebook_color(theme, library.color())), }) .collect() } diff --git a/crates/snowbound/src/palette.rs b/crates/snowbound/src/palette.rs index 528fa9e4123ef455fe573efdda64192049414f3e..4aca480a7d674177eb4441de8f9532b385619866 100644 --- a/crates/snowbound/src/palette.rs +++ b/crates/snowbound/src/palette.rs @@ -97,7 +97,7 @@ impl State { .map(|library| Row { text: library.name.clone(), icon: Some(art::NOTEBOOK), - tint: Some(crate::notebook_color(library.color())), + tint: Some(crate::notebook_color(&self.ui.theme, library.color())), target: Some(Target::Notebook(Arc::clone(library))), ..Row::default() }), @@ -288,7 +288,7 @@ impl State { .to_owned(), after: "Closed".to_owned(), icon: Some(art::NOTEBOOK), - tint: Some(crate::notebook_color(None)), + tint: Some(crate::notebook_color(&self.ui.theme, None)), target: Some(target), ..Row::default() }); diff --git a/crates/snowbound/src/properties.rs b/crates/snowbound/src/properties.rs index d6ad958bd3088342af5681d845d735d2b1cee263..da21d467243ba97d48b6338550e2ff111ced347c 100644 --- a/crates/snowbound/src/properties.rs +++ b/crates/snowbound/src/properties.rs @@ -169,7 +169,7 @@ impl State { .map(|&(color, text)| Item { text, icon: Some(art::SWATCH), - tint: Some(crate::notebook_color(Some(color))), + tint: Some(crate::notebook_color(&theme, Some(color))), current: Some(color) == shown, ..Item::default() }) diff --git a/crates/snowbound/src/sidebar.rs b/crates/snowbound/src/sidebar.rs index ebd1eb7057ea8d15956921b4c11bec44afb1734d..6d4420d2dda45b678d4063af838fdff7fbf8b96e 100644 --- a/crates/snowbound/src/sidebar.rs +++ b/crates/snowbound/src/sidebar.rs @@ -613,7 +613,7 @@ impl Leading { fn art(self, theme: &Theme) -> (&'static [&'static str], [f32; 4]) { match self { Self::Icon(icon) => (icon, theme.text), - Self::Notebook(icon, color) => (icon, crate::notebook_color(color)), + Self::Notebook(icon, color) => (icon, crate::notebook_color(theme, color)), Self::Section(section) => (art::SECTION, theme.section(section).accent), } } @@ -867,7 +867,10 @@ impl crate::State { ..Spec::default() }, ); - let tint = crate::notebook_color(self.notebook().and_then(|library| library.color())); + let tint = crate::notebook_color( + &self.ui.theme, + self.notebook().and_then(|library| library.color()), + ); if ui::shell::tool_button(&mut self.ui, "button", art::NOTEBOOK, tint, None).clicked { self.sidebar = !self.sidebar; self.save_settings(); diff --git a/crates/snowbound/src/sync.rs b/crates/snowbound/src/sync.rs index 5ef992d1a02bf6bb73aa37a4f4e64dd5b7bb6f27..10454611d0abce12e73a826c7fd11ea03b50d707 100644 --- a/crates/snowbound/src/sync.rs +++ b/crates/snowbound/src/sync.rs @@ -681,7 +681,7 @@ fn build(ui: &mut Ui, facts: &Facts) -> Picked { size: [fill(), px(line)], text: Some(facts.name), icon: Some(art::NOTEBOOK), - tint: Some(crate::notebook_color(facts.color)), + tint: Some(crate::notebook_color(&theme, facts.color)), bold: true, overflow: Overflow::Ellipsis, ..Spec::default()