diff --git a/Cargo.lock b/Cargo.lock
index 600772b4..2db9bda5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -197,6 +197,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
[[package]]
name = "bit-set"
version = "0.5.3"
@@ -301,6 +307,12 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.11.1"
@@ -514,6 +526,12 @@ dependencies = [
"unicode-width",
]
+[[package]]
+name = "color_quant"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
+
[[package]]
name = "com"
version = "0.6.0"
@@ -604,18 +622,27 @@ dependencies = [
"libc",
]
+[[package]]
+name = "core_maths"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
+dependencies = [
+ "libm",
+]
+
[[package]]
name = "cosmic-text"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75acbfb314aeb4f5210d379af45ed1ec2c98c7f1790bf57b8a4c562ac0c51b71"
dependencies = [
- "fontdb",
+ "fontdb 0.15.0",
"libm",
"log",
"rangemap",
"rustc-hash",
- "rustybuzz",
+ "rustybuzz 0.11.0",
"self_cell",
"swash",
"sys-locale",
@@ -669,6 +696,12 @@ dependencies = [
"winapi",
]
+[[package]]
+name = "data-url"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
+
[[package]]
name = "deranged"
version = "0.5.8"
@@ -912,6 +945,12 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "float-cmp"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
+
[[package]]
name = "float-cmp"
version = "0.10.0"
@@ -965,6 +1004,20 @@ dependencies = [
"ttf-parser 0.19.2",
]
+[[package]]
+name = "fontdb"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3a6f9af55fb97ad673fb7a69533eb2f967648a06fa21f8c9bb2cd6d33975716"
+dependencies = [
+ "fontconfig-parser",
+ "log",
+ "memmap2 0.9.10",
+ "slotmap",
+ "tinyvec",
+ "ttf-parser 0.24.1",
+]
+
[[package]]
name = "foreign-types"
version = "0.5.0"
@@ -1128,6 +1181,16 @@ dependencies = [
"wasip3",
]
+[[package]]
+name = "gif"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
+dependencies = [
+ "color_quant",
+ "weezl",
+]
+
[[package]]
name = "gio"
version = "0.21.5"
@@ -1515,7 +1578,7 @@ dependencies = [
"bytemuck",
"cosmic-text",
"iced_graphics",
- "kurbo",
+ "kurbo 0.10.4",
"log",
"rustc-hash",
"softbuffer",
@@ -1591,6 +1654,22 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+[[package]]
+name = "image-webp"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f79afb8cbee2ef20f59ccd477a218c12a93943d075b492015ecb1bb81f8ee904"
+dependencies = [
+ "byteorder-lite",
+ "quick-error",
+]
+
+[[package]]
+name = "imagesize"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285"
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -1700,6 +1779,17 @@ dependencies = [
"smallvec",
]
+[[package]]
+name = "kurbo"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62"
+dependencies = [
+ "arrayvec",
+ "euclid",
+ "smallvec",
+]
+
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -2502,6 +2592,12 @@ dependencies = [
"siphasher",
]
+[[package]]
+name = "pico-args"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315"
+
[[package]]
name = "pin-project-lite"
version = "0.2.16"
@@ -2561,7 +2657,7 @@ checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
dependencies = [
"anstyle",
"difflib",
- "float-cmp",
+ "float-cmp 0.10.0",
"normalize-line-endings",
"predicates-core",
"regex",
@@ -2623,6 +2719,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
+[[package]]
+name = "quick-error"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
+
[[package]]
name = "quick-xml"
version = "0.38.4"
@@ -2781,6 +2883,32 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
+[[package]]
+name = "resvg"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4a325d5e8d1cebddd070b13f44cec8071594ab67d1012797c121f27a669b7958"
+dependencies = [
+ "gif",
+ "image-webp",
+ "log",
+ "pico-args",
+ "rgb",
+ "svgtypes",
+ "tiny-skia",
+ "usvg",
+ "zune-jpeg",
+]
+
+[[package]]
+name = "rgb"
+version = "0.8.53"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
+dependencies = [
+ "bytemuck",
+]
+
[[package]]
name = "roxmltree"
version = "0.20.0"
@@ -2836,8 +2964,26 @@ dependencies = [
"libm",
"smallvec",
"ttf-parser 0.20.0",
- "unicode-bidi-mirroring",
- "unicode-ccc",
+ "unicode-bidi-mirroring 0.1.0",
+ "unicode-ccc 0.1.2",
+ "unicode-properties",
+ "unicode-script",
+]
+
+[[package]]
+name = "rustybuzz"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c85d1ccd519e61834798eb52c4e886e8c2d7d698dd3d6ce0b1b47eb8557f1181"
+dependencies = [
+ "bitflags 2.11.0",
+ "bytemuck",
+ "core_maths",
+ "log",
+ "smallvec",
+ "ttf-parser 0.24.1",
+ "unicode-bidi-mirroring 0.3.0",
+ "unicode-ccc 0.3.0",
"unicode-properties",
"unicode-script",
]
@@ -3019,6 +3165,15 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
+[[package]]
+name = "simplecss"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c"
+dependencies = [
+ "log",
+]
+
[[package]]
name = "siphasher"
version = "1.0.2"
@@ -3193,6 +3348,9 @@ name = "strict-num"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
+dependencies = [
+ "float-cmp 0.9.0",
+]
[[package]]
name = "svg_fmt"
@@ -3200,6 +3358,16 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
+[[package]]
+name = "svgtypes"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc"
+dependencies = [
+ "kurbo 0.11.3",
+ "siphasher",
+]
+
[[package]]
name = "swash"
version = "0.1.19"
@@ -3550,6 +3718,15 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4"
+[[package]]
+name = "ttf-parser"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
+dependencies = [
+ "core_maths",
+]
+
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -3579,12 +3756,24 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56d12260fb92d52f9008be7e4bca09f584780eb2266dc8fecc6a192bec561694"
+[[package]]
+name = "unicode-bidi-mirroring"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64af057ad7466495ca113126be61838d8af947f41d93a949980b2389a118082f"
+
[[package]]
name = "unicode-ccc"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc2520efa644f8268dce4dcd3050eaa7fc044fca03961e9998ac7e2e92b77cf1"
+[[package]]
+name = "unicode-ccc"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42"
+
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -3615,6 +3804,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+[[package]]
+name = "unicode-vo"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94"
+
[[package]]
name = "unicode-width"
version = "0.1.14"
@@ -3627,6 +3822,33 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "usvg"
+version = "0.44.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6"
+dependencies = [
+ "base64",
+ "data-url",
+ "flate2",
+ "fontdb 0.22.0",
+ "imagesize",
+ "kurbo 0.11.3",
+ "log",
+ "pico-args",
+ "roxmltree",
+ "rustybuzz 0.18.0",
+ "simplecss",
+ "siphasher",
+ "strict-num",
+ "svgtypes",
+ "tiny-skia-path",
+ "unicode-bidi",
+ "unicode-script",
+ "unicode-vo",
+ "xmlwriter",
+]
+
[[package]]
name = "uuid"
version = "1.21.0"
@@ -3981,6 +4203,7 @@ dependencies = [
"pangocairo",
"png",
"predicates",
+ "resvg",
"schemars",
"serde",
"serde_json",
@@ -3989,6 +4212,7 @@ dependencies = [
"tempfile",
"thiserror 2.0.18",
"time",
+ "tiny-skia",
"tokio",
"toml",
"wayland-client",
@@ -4026,6 +4250,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "weezl"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
+
[[package]]
name = "wgpu"
version = "0.19.4"
@@ -4737,6 +4967,12 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+[[package]]
+name = "xmlwriter"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9"
+
[[package]]
name = "xxhash-rust"
version = "0.8.15"
@@ -4837,6 +5073,21 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+[[package]]
+name = "zune-core"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
+
+[[package]]
+name = "zune-jpeg"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
+dependencies = [
+ "zune-core",
+]
+
[[package]]
name = "zvariant"
version = "5.10.0"
diff --git a/Cargo.toml b/Cargo.toml
index 8397bbcb..0ed52ba3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,10 @@ calloop = "0.14"
cairo-rs = { version = "0.21", features = ["png"] }
cairo-sys-rs = "0.21"
+# SVG icon rendering
+resvg = "0.44"
+tiny-skia = "0.11"
+
# Pango for advanced text rendering and font support
pango = "0.21"
pangocairo = "0.21"
diff --git a/assets/icons/arrow-up-right.svg b/assets/icons/arrow-up-right.svg
new file mode 100644
index 00000000..3a864fa9
--- /dev/null
+++ b/assets/icons/arrow-up-right.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/circle.svg b/assets/icons/circle.svg
new file mode 100644
index 00000000..80a34e64
--- /dev/null
+++ b/assets/icons/circle.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/eraser.svg b/assets/icons/eraser.svg
new file mode 100644
index 00000000..521dbab2
--- /dev/null
+++ b/assets/icons/eraser.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/highlighter.svg b/assets/icons/highlighter.svg
new file mode 100644
index 00000000..540c636d
--- /dev/null
+++ b/assets/icons/highlighter.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/list-ordered.svg b/assets/icons/list-ordered.svg
new file mode 100644
index 00000000..7a00c991
--- /dev/null
+++ b/assets/icons/list-ordered.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/minus.svg b/assets/icons/minus.svg
new file mode 100644
index 00000000..3dedcc64
--- /dev/null
+++ b/assets/icons/minus.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/mouse-pointer-click.svg b/assets/icons/mouse-pointer-click.svg
new file mode 100644
index 00000000..c9f14491
--- /dev/null
+++ b/assets/icons/mouse-pointer-click.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/mouse-pointer.svg b/assets/icons/mouse-pointer.svg
new file mode 100644
index 00000000..a002442c
--- /dev/null
+++ b/assets/icons/mouse-pointer.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/pen-tool.svg b/assets/icons/pen-tool.svg
new file mode 100644
index 00000000..83f6edb0
--- /dev/null
+++ b/assets/icons/pen-tool.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/pen.svg b/assets/icons/pen.svg
new file mode 100644
index 00000000..519601d4
--- /dev/null
+++ b/assets/icons/pen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/rectangle-horizontal.svg b/assets/icons/rectangle-horizontal.svg
new file mode 100644
index 00000000..f235ced1
--- /dev/null
+++ b/assets/icons/rectangle-horizontal.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/sticky-note.svg b/assets/icons/sticky-note.svg
new file mode 100644
index 00000000..14feca3d
--- /dev/null
+++ b/assets/icons/sticky-note.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/type.svg b/assets/icons/type.svg
new file mode 100644
index 00000000..d01a7b94
--- /dev/null
+++ b/assets/icons/type.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/toolbar_icons/mod.rs b/src/toolbar_icons/mod.rs
index 0876d1d4..8fba9fd0 100644
--- a/src/toolbar_icons/mod.rs
+++ b/src/toolbar_icons/mod.rs
@@ -1,11 +1,14 @@
//! Icon drawing functions for the toolbar UI.
//!
-//! All icons are drawn using Cairo paths for perfect scaling at any DPI.
+//! Tool icons are rendered from embedded SVG files (see `svg` module).
+//! Other icons (actions, controls, history, zoom, security) still use
+//! procedural Cairo paths.
mod actions;
mod controls;
mod history;
mod security;
+pub(crate) mod svg;
mod tools;
mod zoom;
diff --git a/src/toolbar_icons/svg.rs b/src/toolbar_icons/svg.rs
new file mode 100644
index 00000000..14ef14a4
--- /dev/null
+++ b/src/toolbar_icons/svg.rs
@@ -0,0 +1,181 @@
+//! SVG icon rendering via resvg.
+//!
+//! SVG icons are embedded at compile time and lazily rasterized.
+//! Rendered surfaces are cached per pixel-size so only the first draw at a
+//! given size incurs the resvg rasterization cost. Icons are painted via
+//! [`cairo::Context::mask_surface`] so they automatically inherit the callers
+//! current source color.
+
+use cairo::{Context, Format, ImageSurface};
+use std::collections::HashMap;
+use std::sync::{LazyLock, Mutex};
+
+// Cached render entry
+struct CachedRender {
+ /// Pre-converted Cairo ARGB32 pixel data (native byte-order, premultiplied).
+ data: Vec,
+ width: i32,
+ height: i32,
+ stride: i32,
+}
+
+// Parsed + cached SVG icon
+struct SvgIcon {
+ tree: resvg::usvg::Tree,
+ /// Per-pixel-size cache of rasterised data. The `Mutex` is uncontended in
+ /// practice because the Wayland event loop is single-threaded.
+ cache: Mutex>,
+}
+
+impl SvgIcon {
+ fn parse(svg_data: &str) -> Self {
+ let tree =
+ resvg::usvg::Tree::from_str(svg_data, &resvg::usvg::Options::default())
+ .expect("embedded SVG must be valid");
+ Self {
+ tree,
+ cache: Mutex::new(HashMap::new()),
+ }
+ }
+
+ /// Render this icon into ctx at (`x`, `y`) with the given square
+ /// `size`. The icon is painted using the context's current source color
+ fn render(&self, ctx: &Context, x: f64, y: f64, size: f64) {
+ if size <= 0.0 {
+ return;
+ }
+ let px = size.ceil() as u32;
+ if let Some(surface) = self.surface_for(px) {
+ let _ = ctx.mask_surface(&surface, x, y);
+ }
+ }
+
+ /// Return a Cairo ['ImageSurface'] for the requested pixel size, creating
+ /// and caching the rasterised data on first call
+ fn surface_for(&self, px: u32) -> Option {
+ let mut cache = self.cache.lock().ok()?;
+ if !cache.contains_key(&px) {
+ cache.insert(px, self.rasterize(px)?);
+ }
+ let c = cache.get(&px)?;
+ ImageSurface::create_for_data(
+ c.data.clone(),
+ Format::ARgb32,
+ c.width,
+ c.height,
+ c.stride,
+ )
+ .ok()
+ }
+
+ /// Rasterize the SVG tree at `px x px` and convert the pixel data from
+ /// tiny-skia premultiplied RGBA to Cairo premultiplied ARGB32 (BGRA on
+ /// little-endian).
+ fn rasterize(&self, px: u32) -> Option {
+ let mut pixmap = tiny_skia::Pixmap::new(px, px)?;
+
+ let sz = self.tree.size();
+ let sx = px as f32 / sz.width();
+ let sy = px as f32 / sz.height();
+ resvg::render(
+ &self.tree,
+ tiny_skia::Transform::from_scale(sx, sy),
+ &mut pixmap.as_mut(),
+ );
+
+ let stride = Format::ARgb32.stride_for_width(px).ok()? as usize;
+ let w = px as usize;
+ let h = px as usize;
+ let src = pixmap.data();
+ let mut data = vec![0u8; stride * h];
+
+ for row in 0..h {
+ for col in 0..w {
+ let si = (row * w + col) * 4;
+ let di = row * stride + col * 4;
+ // tiny-skia RGBA to Cairo ARGB32 little-endian (BGRA in memory)
+ data[di] = src[si + 2]; // B
+ data[di + 1] = src[si + 1]; // G
+ data[di + 2] = src[si]; // R
+ data[di + 3] = src[si + 3]; // A
+ }
+ }
+
+ Some(CachedRender {
+ data,
+ width: px as i32,
+ height: px as i32,
+ stride: stride as i32,
+ })
+ }
+}
+
+// Embed SVG files and create lazy-parsed statics
+macro_rules! svg_icon {
+ ($name:ident, $path:expr) => {
+ static $name: LazyLock =
+ LazyLock::new(|| SvgIcon::parse(include_str!($path)));
+ };
+}
+
+svg_icon!(SELECT, "../../assets/icons/mouse-pointer.svg");
+svg_icon!(PEN, "../../assets/icons/pen-tool.svg");
+svg_icon!(LINE, "../../assets/icons/minus.svg");
+svg_icon!(RECT, "../../assets/icons/rectangle-horizontal.svg");
+svg_icon!(CIRCLE, "../../assets/icons/circle.svg");
+svg_icon!(ARROW, "../../assets/icons/arrow-up-right.svg");
+svg_icon!(ERASER, "../../assets/icons/eraser.svg");
+svg_icon!(TEXT, "../../assets/icons/type.svg");
+svg_icon!(NOTE, "../../assets/icons/sticky-note.svg");
+svg_icon!(HIGHLIGHT, "../../assets/icons/mouse-pointer-click.svg");
+svg_icon!(MARKER, "../../assets/icons/highlighter.svg");
+svg_icon!(STEP_MARKER, "../../assets/icons/list-ordered.svg");
+
+// Render helpers, matching the draw_icon_* signatures
+pub fn render_select(ctx: &Context, x: f64, y: f64, size: f64) {
+ SELECT.render(ctx, x, y, size);
+}
+
+pub fn render_pen(ctx: &Context, x: f64, y: f64, size: f64) {
+ PEN.render(ctx, x, y, size);
+}
+
+pub fn render_line(ctx: &Context, x: f64, y: f64, size: f64) {
+ LINE.render(ctx, x, y, size);
+}
+
+pub fn render_rect(ctx: &Context, x: f64, y: f64, size: f64) {
+ RECT.render(ctx, x, y, size);
+}
+
+pub fn render_circle(ctx: &Context, x: f64, y: f64, size: f64) {
+ CIRCLE.render(ctx, x, y, size);
+}
+
+pub fn render_arrow(ctx: &Context, x: f64, y: f64, size: f64) {
+ ARROW.render(ctx, x, y, size);
+}
+
+pub fn render_eraser(ctx: &Context, x: f64, y: f64, size: f64) {
+ ERASER.render(ctx, x, y, size);
+}
+
+pub fn render_text(ctx: &Context, x: f64, y: f64, size: f64) {
+ TEXT.render(ctx, x, y, size);
+}
+
+pub fn render_note(ctx: &Context, x: f64, y: f64, size: f64) {
+ NOTE.render(ctx, x, y, size);
+}
+
+pub fn render_highlight(ctx: &Context, x: f64, y: f64, size: f64) {
+ HIGHLIGHT.render(ctx, x, y, size);
+}
+
+pub fn render_marker(ctx: &Context, x: f64, y: f64, size: f64) {
+ MARKER.render(ctx, x, y, size);
+}
+
+pub fn render_step_marker(ctx: &Context, x: f64, y: f64, size: f64) {
+ STEP_MARKER.render(ctx, x, y, size);
+}
diff --git a/src/toolbar_icons/tools.rs b/src/toolbar_icons/tools.rs
index 6e0dd113..aeb0b75a 100644
--- a/src/toolbar_icons/tools.rs
+++ b/src/toolbar_icons/tools.rs
@@ -1,332 +1,63 @@
use cairo::Context;
-use std::f64::consts::PI;
/// Draw a cursor/select icon (arrow pointer)
pub fn draw_icon_select(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.06).max(1.2);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Compact cursor arrow with a short tail.
- ctx.move_to(x + s * 0.2, y + s * 0.12);
- ctx.line_to(x + s * 0.2, y + s * 0.78);
- ctx.line_to(x + s * 0.36, y + s * 0.62);
- ctx.line_to(x + s * 0.5, y + s * 0.88);
- ctx.line_to(x + s * 0.62, y + s * 0.82);
- ctx.line_to(x + s * 0.46, y + s * 0.56);
- ctx.line_to(x + s * 0.78, y + s * 0.46);
- ctx.close_path();
-
- let _ = ctx.fill_preserve();
- let _ = ctx.stroke();
+ super::svg::render_select(ctx, x, y, size);
}
/// Draw a pen/freehand icon (nib with a short stroke)
pub fn draw_icon_pen(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.08).max(1.4);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Fountain pen nib (diamond) with a short drawing stroke.
- let cx = x + s * 0.6;
- let cy = y + s * 0.38;
- let nib_w = s * 0.32;
- let nib_h = s * 0.36;
-
- ctx.move_to(cx, cy - nib_h * 0.5);
- ctx.line_to(cx + nib_w * 0.5, cy);
- ctx.line_to(cx, cy + nib_h * 0.5);
- ctx.line_to(cx - nib_w * 0.5, cy);
- ctx.close_path();
- let _ = ctx.stroke();
-
- ctx.move_to(cx, cy + nib_h * 0.05);
- ctx.line_to(cx, cy + nib_h * 0.5);
- let _ = ctx.stroke();
-
- ctx.set_line_width((s * 0.1).max(1.4));
- ctx.move_to(x + s * 0.16, y + s * 0.78);
- ctx.curve_to(
- x + s * 0.3,
- y + s * 0.68,
- x + s * 0.42,
- y + s * 0.86,
- x + s * 0.62,
- y + s * 0.74,
- );
- let _ = ctx.stroke();
+ super::svg::render_pen(ctx, x, y, size);
}
/// Draw a line tool icon
pub fn draw_icon_line(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.5);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
-
- ctx.move_to(x + s * 0.2, y + s * 0.8);
- ctx.line_to(x + s * 0.8, y + s * 0.2);
- let _ = ctx.stroke();
+ super::svg::render_line(ctx, x, y, size);
}
/// Draw a rectangle tool icon
pub fn draw_icon_rect(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.5);
- ctx.set_line_width(stroke);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- let margin = s * 0.2;
- ctx.rectangle(x + margin, y + margin, s - margin * 2.0, s - margin * 2.0);
- let _ = ctx.stroke();
+ super::svg::render_rect(ctx, x, y, size);
}
/// Draw a circle/ellipse tool icon
pub fn draw_icon_circle(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.5);
- ctx.set_line_width(stroke);
-
- let cx = x + s * 0.5;
- let cy = y + s * 0.5;
- let r = s * 0.35;
- ctx.arc(cx, cy, r, 0.0, PI * 2.0);
- let _ = ctx.stroke();
+ super::svg::render_circle(ctx, x, y, size);
}
/// Draw an arrow tool icon
pub fn draw_icon_arrow(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.5);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Arrow line
- ctx.move_to(x + s * 0.2, y + s * 0.8);
- ctx.line_to(x + s * 0.8, y + s * 0.2);
- let _ = ctx.stroke();
-
- // Arrow head
- ctx.move_to(x + s * 0.8, y + s * 0.2);
- ctx.line_to(x + s * 0.55, y + s * 0.25);
- let _ = ctx.stroke();
- ctx.move_to(x + s * 0.8, y + s * 0.2);
- ctx.line_to(x + s * 0.75, y + s * 0.45);
- let _ = ctx.stroke();
+ super::svg::render_arrow(ctx, x, y, size);
}
/// Draw an eraser tool icon
#[allow(dead_code)]
pub fn draw_icon_eraser(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.08).max(1.4);
- ctx.set_line_width(stroke);
- ctx.set_line_join(cairo::LineJoin::Round);
- ctx.set_line_cap(cairo::LineCap::Round);
-
- // Slanted eraser block with a band near the tip.
- let body_w = s * 0.72;
- let body_h = s * 0.34;
- let angle = -PI * 0.22;
- let cx = x + s * 0.55;
- let cy = y + s * 0.55;
-
- let _ = ctx.save();
- ctx.translate(cx, cy);
- ctx.rotate(angle);
- ctx.rectangle(-body_w / 2.0, -body_h / 2.0, body_w, body_h);
- let _ = ctx.stroke();
-
- let band_x = -body_w * 0.18;
- ctx.move_to(band_x, -body_h / 2.0);
- ctx.line_to(band_x, body_h / 2.0);
- let _ = ctx.stroke();
- let _ = ctx.restore();
+ super::svg::render_eraser(ctx, x, y, size);
}
/// Draw a text tool icon (letter T)
pub fn draw_icon_text(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.12).max(2.0);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
-
- // Top bar of T
- ctx.move_to(x + s * 0.2, y + s * 0.25);
- ctx.line_to(x + s * 0.8, y + s * 0.25);
- let _ = ctx.stroke();
-
- // Vertical bar of T
- ctx.move_to(x + s * 0.5, y + s * 0.25);
- ctx.line_to(x + s * 0.5, y + s * 0.8);
- let _ = ctx.stroke();
+ super::svg::render_text(ctx, x, y, size);
}
/// Draw a sticky note icon (square with folded corner)
pub fn draw_icon_note(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.08).max(1.4);
- let margin = s * 0.18;
- let fold = s * 0.22;
-
- ctx.set_line_width(stroke);
- ctx.set_line_join(cairo::LineJoin::Round);
- ctx.set_line_cap(cairo::LineCap::Round);
-
- ctx.move_to(x + margin, y + margin);
- ctx.line_to(x + s - margin - fold, y + margin);
- ctx.line_to(x + s - margin, y + margin + fold);
- ctx.line_to(x + s - margin, y + s - margin);
- ctx.line_to(x + margin, y + s - margin);
- ctx.close_path();
- let _ = ctx.stroke();
-
- ctx.move_to(x + s - margin - fold, y + margin);
- ctx.line_to(x + s - margin - fold, y + margin + fold);
- ctx.line_to(x + s - margin, y + margin + fold);
- let _ = ctx.stroke();
+ super::svg::render_note(ctx, x, y, size);
}
/// Draw a highlighter tool icon (cursor with click ripple effect)
#[allow(dead_code)]
pub fn draw_icon_highlight(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.5);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Pointer/cursor arrow
- ctx.move_to(x + s * 0.2, y + s * 0.15);
- ctx.line_to(x + s * 0.2, y + s * 0.65);
- ctx.line_to(x + s * 0.35, y + s * 0.52);
- ctx.line_to(x + s * 0.5, y + s * 0.75);
- ctx.line_to(x + s * 0.58, y + s * 0.7);
- ctx.line_to(x + s * 0.43, y + s * 0.47);
- ctx.line_to(x + s * 0.58, y + s * 0.4);
- ctx.close_path();
- let _ = ctx.fill();
-
- // Ripple circles around click point
- ctx.set_line_width((s * 0.08).max(1.0));
- // Inner ripple
- ctx.arc(x + s * 0.7, y + s * 0.55, s * 0.12, 0.0, PI * 2.0);
- let _ = ctx.stroke();
- // Outer ripple
- ctx.arc(x + s * 0.7, y + s * 0.55, s * 0.22, 0.0, PI * 2.0);
- let _ = ctx.stroke();
+ super::svg::render_highlight(ctx, x, y, size);
}
-/// Draw a marker/highlighter icon (tilted marker with translucent swatch)
+/// Draw a marker/highlighter icon
pub fn draw_icon_marker(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.08).max(1.4);
- let swatch_color = (1.0, 0.92, 0.35, 0.4);
-
- // Underlying swatch to imply transparent ink
- ctx.set_source_rgba(
- swatch_color.0,
- swatch_color.1,
- swatch_color.2,
- swatch_color.3,
- );
- let swatch_h = s * 0.28;
- ctx.rectangle(x + s * 0.08, y + s * 0.65, s * 0.84, swatch_h);
- let _ = ctx.fill();
-
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Marker body (tilted)
- ctx.save().ok();
- ctx.translate(x + s * 0.55, y + s * 0.35);
- ctx.rotate(-PI * 0.18);
-
- // Body outline
- ctx.set_source_rgba(0.95, 0.95, 0.98, 0.95);
- ctx.rectangle(-s * 0.24, -s * 0.08, s * 0.38, s * 0.26);
- let _ = ctx.fill_preserve();
- ctx.set_source_rgba(0.25, 0.28, 0.35, 0.9);
- let _ = ctx.stroke();
-
- // Chisel tip with ink color
- ctx.set_source_rgba(swatch_color.0, swatch_color.1, swatch_color.2, 0.85);
- ctx.move_to(-s * 0.24, -s * 0.08);
- ctx.line_to(-s * 0.32, 0.0);
- ctx.line_to(-s * 0.24, s * 0.18);
- ctx.close_path();
- let _ = ctx.fill();
- ctx.restore().ok();
+ super::svg::render_marker(ctx, x, y, size);
}
-/// Draw a step marker icon (numbered list: 1, 2, 3)
+/// Draw a step marker icon (numbered list)
pub fn draw_icon_step_marker(ctx: &Context, x: f64, y: f64, size: f64) {
- let s = size;
- let stroke = (s * 0.1).max(1.6);
- ctx.set_line_width(stroke);
- ctx.set_line_cap(cairo::LineCap::Round);
- ctx.set_line_join(cairo::LineJoin::Round);
-
- // Draw "1" - top left
- let one_x = x + s * 0.22;
- let one_top = y + s * 0.12;
- let one_bot = y + s * 0.38;
- let one_w = s * 0.12;
- // Serif at top
- ctx.move_to(one_x - one_w * 0.6, one_top + s * 0.06);
- ctx.line_to(one_x, one_top);
- let _ = ctx.stroke();
- // Vertical stroke
- ctx.move_to(one_x, one_top);
- ctx.line_to(one_x, one_bot);
- let _ = ctx.stroke();
- // Base
- ctx.move_to(one_x - one_w, one_bot);
- ctx.line_to(one_x + one_w, one_bot);
- let _ = ctx.stroke();
-
- // Draw "2" - top right
- let two_x = x + s * 0.62;
- let two_top = y + s * 0.12;
- let two_bot = y + s * 0.38;
- let two_w = s * 0.14;
- // Top curve of 2
- ctx.arc(two_x, two_top + s * 0.08, two_w, -PI * 0.9, PI * 0.15);
- let _ = ctx.stroke();
- // Diagonal and base
- ctx.move_to(two_x + two_w * 0.9, two_top + s * 0.14);
- ctx.line_to(two_x - two_w, two_bot);
- ctx.line_to(two_x + two_w, two_bot);
- let _ = ctx.stroke();
-
- // Draw "3" - bottom center
- let three_x = x + s * 0.5;
- let three_top = y + s * 0.54;
- let three_bot = y + s * 0.88;
- let three_w = s * 0.16;
- let three_h = (three_bot - three_top) / 2.0;
- // Top arc of 3
- ctx.arc(
- three_x,
- three_top + three_h * 0.45,
- three_w * 0.85,
- -PI * 0.85,
- PI * 0.35,
- );
- let _ = ctx.stroke();
- // Bottom arc of 3
- ctx.arc(
- three_x,
- three_bot - three_h * 0.45,
- three_w * 0.85,
- -PI * 0.35,
- PI * 0.85,
- );
- let _ = ctx.stroke();
+ super::svg::render_step_marker(ctx, x, y, size);
}