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()