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); }