diff --git a/Cargo.lock b/Cargo.lock index db3c76e..0dedfb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -94,15 +94,15 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -145,9 +145,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cairo-rs" @@ -197,9 +197,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ "android-tzdata", "iana-time-zone", @@ -211,9 +211,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", @@ -262,14 +262,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.3" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -305,6 +305,25 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "cstr" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68523903c8ae5aacfa32a0d9ae60cadeb764e1da14ee0d26b1f3089f13a54636" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + [[package]] name = "epoxy" version = "0.1.0" @@ -383,6 +402,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "fontconfig" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9b79b619a4ae048ea79e927376b1d10294979bda195b0c052fc958be96c62d9" +dependencies = [ + "yeslogic-fontconfig-sys", +] + [[package]] name = "fragile" version = "2.0.0" @@ -445,7 +473,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -637,7 +665,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -869,9 +897,9 @@ checksum = "44feda355f4159a7c757171a77de25daf6411e217b4cabd03bd6650690468126" [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown", @@ -879,9 +907,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "js-sys" @@ -976,15 +1004,15 @@ checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1101,9 +1129,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1270,7 +1298,7 @@ checksum = "0774e846889823aa5766f5b62cface3189a5b36280e65b2faaa6df0319da1726" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -1342,7 +1370,7 @@ dependencies = [ [[package]] name = "satty" -version = "0.11.3" +version = "0.12.0" dependencies = [ "anyhow", "chrono", @@ -1352,6 +1380,7 @@ dependencies = [ "clap_complete_nushell", "epoxy", "femtovg", + "fontconfig", "gdk-pixbuf", "glib", "glib-macros", @@ -1398,14 +1427,14 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1460,9 +1489,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" @@ -1491,9 +1520,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" @@ -1508,9 +1537,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.53" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -1553,14 +1582,14 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -1583,7 +1612,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -1595,7 +1624,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.8", + "toml_edit 0.22.9", ] [[package]] @@ -1631,9 +1660,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap", "serde", @@ -1661,7 +1690,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", ] [[package]] @@ -1776,7 +1805,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -1798,7 +1827,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.53", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2024,6 +2053,18 @@ dependencies = [ "bitflags 1.3.2", ] +[[package]] +name = "yeslogic-fontconfig-sys" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb6b23999a8b1a997bf47c7bb4d19ad4029c3327bb3386ebe0a5ff584b33c7a" +dependencies = [ + "cstr", + "dlib", + "once_cell", + "pkg-config", +] + [[package]] name = "zvariant" version = "3.15.2" diff --git a/Cargo.toml b/Cargo.toml index 6ff1fe6..da2ac47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "satty" -version = "0.11.3" +version = "0.12.0" edition = "2021" authors = ["Matthias Gabriel "] description = "Modern Screenshot Annotation. A Screenshot Annotation Tool inspired by Swappy and Flameshot." @@ -45,6 +45,7 @@ glow = "0.13.1" glib-macros = "0.19.2" glib = "0.19.2" resource = "0.5.0" # font emedding +fontconfig = "0.8.0" # font loading [dependencies.relm4-icons] version = "0.8.2" diff --git a/README.md b/README.md index e595090..cdb26a6 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Satty has been created to provide the following improvements over existing scree - very simple and easy to understand toolset (like Swappy) - fullscreen annotation mode and post shot cropping (like Flameshot) +- extremely smooth rendering thanks to HW acceleration (OpenGL) - working on wlroots based compositors (Sway, Hyprland, River, ...) - minimal, modern looking UI, thanks to GTK and Adwaita - be a playground for new features (post window selection, post paint editing, ...) @@ -77,6 +78,10 @@ output-filename = "/tmp/test-%Y-%m-%d_%H:%M:%S.png" save-after-copy = false # Hide toolbars by default default-hide-toolbars = false +# Font to use for text annotations +[font] +family = "Roboto" +style = "Bold" # custom colours for the colour palette [color-palette] @@ -117,6 +122,10 @@ Options: After copying the screenshot, save it to a file as well -d, --default-hide-toolbars Hide toolbars by default + --font-family + Font family to use for text annotations + --font-style + Font style to use for text annotations -h, --help Print help -V, --version @@ -156,6 +165,7 @@ Satty is based on GTK-4 and Adwaita. - libgtk-4-1 - libadwaita-1-0 - libepoxy +- fontconfig ### Arch Linux & Gentoo @@ -164,6 +174,7 @@ Satty is based on GTK-4 and Adwaita. - gtk4 - gdk-pixbuf2 - libepoxy +- fontconfig ## License diff --git a/config.toml b/config.toml index 4c2f992..67515d9 100644 --- a/config.toml +++ b/config.toml @@ -14,7 +14,11 @@ output-filename = "/tmp/test-%Y-%m-%d_%H:%M:%S.png" # After copying the screenshot, save it to a file as well save-after-copy = false # Hide toolbars by default -dafault-hide-toolbars = false +default-hide-toolbars = false +# Font to use for text annotations +[font] +family = "Roboto" +style = "Regular" # custom colours for the colour palette [color-palette] diff --git a/flake.nix b/flake.nix index 29726ff..488f650 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,7 @@ gtk4 wrapGAppsHook4 # this is needed for relm4-icons to properly load after gtk::init() libadwaita + fontconfig (rust-bin.stable.latest.default.override { diff --git a/icons.toml b/icons.toml index cd41309..b705806 100644 --- a/icons.toml +++ b/icons.toml @@ -5,6 +5,7 @@ icons = [ "cursor-regular", "number-circle-1-regular", "drop-regular", + "highlight-regular", "arrow-redo-filled", "arrow-undo-filled", "save-regular", @@ -16,4 +17,4 @@ icons = [ "crop-filled", "arrow-up-right-filled", "rectangle-landscape-regular", -] \ No newline at end of file +] diff --git a/src/command_line.rs b/src/command_line.rs index 9774bb5..0aa9008 100644 --- a/src/command_line.rs +++ b/src/command_line.rs @@ -43,6 +43,14 @@ pub struct CommandLine { /// Hide toolbars by default #[arg(short, long)] pub default_hide_toolbars: bool, + + /// Font family to use for text annotations + #[arg(long)] + pub font_family: Option, + + /// Font style to use for text annotations + #[arg(long)] + pub font_style: Option, } #[derive(Debug, Clone, Copy, Default, ValueEnum)] @@ -56,6 +64,7 @@ pub enum Tools { Text, Marker, Blur, + Highlight, Brush, } @@ -71,6 +80,7 @@ impl std::fmt::Display for Tools { Text => "text", Marker => "marker", Blur => "blur", + Highlight => "highlight", Brush => "brush", }; f.write_str(s) diff --git a/src/configuration.rs b/src/configuration.rs index 4d60c04..acbd546 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -38,6 +38,30 @@ pub struct Configuration { save_after_copy: bool, color_palette: ColorPalette, default_hide_toolbars: bool, + font: FontConfiguration, +} + +#[derive(Default)] +pub struct FontConfiguration { + family: Option, + style: Option, +} + +impl FontConfiguration { + pub fn family(&self) -> Option<&str> { + self.family.as_deref() + } + pub fn style(&self) -> Option<&str> { + self.style.as_deref() + } + fn merge(&mut self, file_font: FontFile) { + if let Some(v) = file_font.family { + self.family = Some(v); + } + if let Some(v) = file_font.style { + self.style = Some(v); + } + } } pub struct ColorPalette { @@ -159,6 +183,9 @@ impl Configuration { if let Some(v) = file.color_palette { self.color_palette.merge(v); } + if let Some(v) = file.font { + self.font.merge(v); + } } // overwrite with all specified values from command line @@ -186,6 +213,12 @@ impl Configuration { if command_line.save_after_copy { self.save_after_copy = command_line.save_after_copy; } + if let Some(v) = command_line.font_family { + self.font.family = Some(v); + } + if let Some(v) = command_line.font_style { + self.font.style = Some(v); + } } pub fn early_exit(&self) -> bool { @@ -227,6 +260,10 @@ impl Configuration { pub fn default_hide_toolbars(&self) -> bool { self.default_hide_toolbars } + + pub fn font(&self) -> &FontConfiguration { + &self.font + } } impl Default for Configuration { @@ -242,6 +279,7 @@ impl Default for Configuration { save_after_copy: false, color_palette: ColorPalette::default(), default_hide_toolbars: false, + font: FontConfiguration::default(), } } } @@ -264,6 +302,14 @@ impl Default for ColorPalette { struct ConfigurationFile { general: Option, color_palette: Option, + font: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case", deny_unknown_fields)] +struct FontFile { + family: Option, + style: Option, } #[derive(Deserialize)] diff --git a/src/femtovg_area/imp.rs b/src/femtovg_area/imp.rs index 7d7b0c1..7ca45d6 100644 --- a/src/femtovg_area/imp.rs +++ b/src/femtovg_area/imp.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{anyhow, Error, Result}; use glow::HasContext; use std::{ cell::{RefCell, RefMut}, @@ -12,6 +12,7 @@ use femtovg::{ rgb::{RGB, RGBA, RGBA8}, Canvas, FontId, ImageFlags, ImageId, ImageSource, Paint, Path, PixelFormat, Transform2D, }; +use fontconfig::Fontconfig; use gdk_pixbuf::Pixbuf; use gtk::{glib, prelude::*, subclass::prelude::*}; use relm4::{gtk, Sender}; @@ -21,6 +22,7 @@ use crate::{ math::Vec2D, sketch_board::{Action, SketchBoardInput}, tools::{CropTool, Drawable, Tool}, + APP_CONFIG, }; #[derive(Default)] @@ -162,14 +164,40 @@ impl FemtoVGArea { self.canvas.borrow_mut().replace(c); } - self.font.borrow_mut().replace( - self.canvas - .borrow_mut() - .as_mut() - .unwrap() // this unwrap is safe because it gets placed above - .add_font_mem(&resource!("src/assets/Roboto-Regular.ttf")) - .expect("Cannot add font"), - ); + let app_config = APP_CONFIG.read(); + let font = app_config + .font() + .family() + .map(|font| { + let font = Fontconfig::new() + .ok_or_else(|| anyhow!("Error while initializing fontconfig"))? + .find(font, app_config.font().style()) + .ok_or_else(|| anyhow!("Can not find font"))?; + let font_id = self + .canvas + .borrow_mut() + .as_mut() + .unwrap() // this unwrap is safe because it gets placed above + .add_font(font.path)?; + Ok(font_id) + }) + .transpose() + .unwrap_or_else(|e: Error| { + println!("Error while loading font. Using default font: {e}"); + None + }); + if let Some(font) = font { + self.font.borrow_mut().replace(font); + } else { + self.font.borrow_mut().replace( + self.canvas + .borrow_mut() + .as_mut() + .unwrap() // this unwrap is safe because it gets placed above + .add_font_mem(&resource!("src/assets/Roboto-Regular.ttf")) + .expect("Cannot add font"), + ); + } } fn setup_canvas(&self) -> Result> { diff --git a/src/main.rs b/src/main.rs index a2967ba..fd94bbf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use std::{io, time::Duration}; use configuration::{Configuration, APP_CONFIG}; use gdk_pixbuf::{Pixbuf, PixbufLoader}; use gtk::prelude::*; + use relm4::gtk::gdk::Rectangle; use relm4::{ @@ -15,13 +16,13 @@ use relm4::{ use anyhow::{anyhow, Context, Result}; use sketch_board::SketchBoardOutput; -use ui::toast::Toast; use ui::toolbars::{StyleToolbar, StyleToolbarInput, ToolsToolbar, ToolsToolbarInput}; mod command_line; mod configuration; mod femtovg_area; mod math; +mod notification; mod sketch_board; mod style; mod tools; @@ -33,7 +34,6 @@ use crate::sketch_board::{KeyEventMsg, SketchBoard, SketchBoardInput}; struct App { image_dimensions: (i32, i32), sketch_board: Controller, - toast: Controller, tools_toolbar: Controller, style_toolbar: Controller, } @@ -41,7 +41,6 @@ struct App { #[derive(Debug)] enum AppInput { Realized, - ShowToast(String), ToggleToolbarsDisplay, } @@ -185,8 +184,6 @@ impl Component for App { add_overlay = model.style_toolbar.widget(), - add_overlay = model.toast.widget(), - model.sketch_board.widget(), } } @@ -195,7 +192,6 @@ impl Component for App { fn update(&mut self, message: Self::Input, sender: ComponentSender, root: &Self::Root) { match message { AppInput::Realized => self.resize_window_initial(root, sender), - AppInput::ShowToast(msg) => self.toast.emit(ui::toast::ToastMessage::Show(msg)), AppInput::ToggleToolbarsDisplay => { self.tools_toolbar .sender() @@ -227,15 +223,11 @@ impl Component for App { let image_dimensions = (image.width(), image.height()); - // Toast - let toast = Toast::builder().launch(3000).detach(); - // SketchBoard let sketch_board = SketchBoard::builder() .launch(image) .forward(sender.input_sender(), |t| match t { - SketchBoardOutput::ShowToast(msg) => AppInput::ShowToast(msg), SketchBoardOutput::ToggleToolbarsDisplay => AppInput::ToggleToolbarsDisplay, }); @@ -253,7 +245,6 @@ impl Component for App { // Model let model = App { sketch_board, - toast, tools_toolbar, style_toolbar, image_dimensions, diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..2e463b6 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,36 @@ +use gdk_pixbuf::gio::FileIcon; +use relm4::gtk::gio::{prelude::ApplicationExt, Notification}; + +use relm4::gtk::{IconLookupFlags, IconTheme, TextDirection}; + +pub fn log_result(msg: &str) { + println!("{}", msg); + show_notification(msg); +} + +fn show_notification(msg: &str) { + // construct + let notification = Notification::new("Satty"); + notification.set_body(Some(msg)); + + // lookup sattys icon + let theme = IconTheme::default(); + if theme.has_icon("satty") { + if let Some(icon_file) = theme + .lookup_icon( + "satty", + &[], + 96, + 1, + TextDirection::Ltr, + IconLookupFlags::empty(), + ) + .file() + { + notification.set_icon(&FileIcon::new(&icon_file)); + } + } + + // send notification + relm4::main_application().send_notification(None, ¬ification); +} diff --git a/src/sketch_board.rs b/src/sketch_board.rs index fec1f71..dbddc70 100644 --- a/src/sketch_board.rs +++ b/src/sketch_board.rs @@ -18,6 +18,7 @@ use relm4::{gtk, Component, ComponentParts, ComponentSender}; use crate::configuration::APP_CONFIG; use crate::femtovg_area::FemtoVGArea; use crate::math::Vec2D; +use crate::notification::log_result; use crate::style::Style; use crate::tools::{Tool, ToolEvent, ToolUpdateResult, ToolsManager}; use crate::ui::toolbars::ToolbarEvent; @@ -39,7 +40,6 @@ pub enum Action { #[derive(Debug, Clone)] pub enum SketchBoardOutput { - ShowToast(String), ToggleToolbarsDisplay, } @@ -162,24 +162,17 @@ impl SketchBoard { ) } - fn handle_render_result( - &self, - sender: ComponentSender, - image: RenderedImage, - action: Action, - ) { + fn handle_render_result(&self, image: RenderedImage, action: Action) { match action { - Action::SaveToClipboard => { - self.handle_copy_clipboard(sender, Self::image_to_pixbuf(image)) - } - Action::SaveToFile => self.handle_save(sender, Self::image_to_pixbuf(image)), + Action::SaveToClipboard => self.handle_copy_clipboard(Self::image_to_pixbuf(image)), + Action::SaveToFile => self.handle_save(Self::image_to_pixbuf(image)), }; if APP_CONFIG.read().early_exit() { relm4::main_application().quit(); } } - fn handle_save(&self, sender: ComponentSender, image: Pixbuf) { + fn handle_save(&self, image: Pixbuf) { let output_filename = match APP_CONFIG.read().output_filename() { None => { println!("No Output filename specified!"); @@ -193,11 +186,7 @@ impl SketchBoard { // TODO: we could support more data types if !output_filename.ends_with(".png") { - let msg = "The only supported format is png, but the filename does not end in png"; - println!("{msg}"); - sender - .output_sender() - .emit(SketchBoardOutput::ShowToast(msg.to_string())); + log_result("The only supported format is png, but the filename does not end in png"); return; } @@ -209,14 +198,10 @@ impl SketchBoard { } }; - let msg = match fs::write(&output_filename, data) { - Err(e) => format!("Error while saving file: {e}"), - Ok(_) => format!("File saved to '{}'.", &output_filename), + match fs::write(&output_filename, data) { + Err(e) => log_result(&format!("Error while saving file: {e}")), + Ok(_) => log_result(&format!("File saved to '{}'.", &output_filename)), }; - - sender - .output_sender() - .emit(SketchBoardOutput::ShowToast(msg)); } fn save_to_clipboard(&self, texture: &impl IsA) -> anyhow::Result<()> { @@ -248,7 +233,7 @@ impl SketchBoard { Ok(()) } - fn handle_copy_clipboard(&self, sender: ComponentSender, image: Pixbuf) { + fn handle_copy_clipboard(&self, image: Pixbuf) { let texture = Texture::for_pixbuf(&image); let result = if let Some(command) = APP_CONFIG.read().copy_command() { @@ -260,20 +245,20 @@ impl SketchBoard { match result { Err(e) => println!("Error saving {e}"), Ok(()) => { - sender.output_sender().emit(SketchBoardOutput::ShowToast( - "Copied to clipboard.".to_string(), - )); + log_result("Copied to clipboard."); // TODO: rethink order and messaging patterns if APP_CONFIG.read().save_after_copy() { - self.handle_save(sender, image); + self.handle_save(image); }; } } } fn handle_undo(&mut self) -> ToolUpdateResult { - if self.renderer.undo() { + if self.active_tool.borrow().active() { + self.active_tool.borrow_mut().handle_undo() + } else if self.renderer.undo() { ToolUpdateResult::Redraw } else { ToolUpdateResult::Unmodified @@ -281,7 +266,9 @@ impl SketchBoard { } fn handle_redo(&mut self) -> ToolUpdateResult { - if self.renderer.redo() { + if self.active_tool.borrow().active() { + self.active_tool.borrow_mut().handle_redo() + } else if self.renderer.redo() { ToolUpdateResult::Redraw } else { ToolUpdateResult::Unmodified @@ -452,7 +439,7 @@ impl Component for SketchBoard { self.handle_toolbar_event(toolbar_event) } SketchBoardInput::RenderResult(img, action) => { - self.handle_render_result(sender, img, action); + self.handle_render_result(img, action); ToolUpdateResult::Unmodified } }; diff --git a/src/style.rs b/src/style.rs index c2f1a25..ccbcc15 100644 --- a/src/style.rs +++ b/src/style.rs @@ -204,4 +204,12 @@ impl Size { Size::Large => 30.0 * size_factor, } } + + pub fn to_highlight_opacity(self) -> u8 { + match self { + Size::Small => 50, + Size::Medium => 100, + Size::Large => 150, + } + } } diff --git a/src/tools/highlight.rs b/src/tools/highlight.rs new file mode 100644 index 0000000..e67d375 --- /dev/null +++ b/src/tools/highlight.rs @@ -0,0 +1,133 @@ +use anyhow::Result; +use femtovg::{Paint, Path}; + +use relm4::gtk::gdk::Key; + +use crate::{ + math::{self, Vec2D}, + sketch_board::{MouseEventMsg, MouseEventType}, + style::{Size, Style}, +}; + +use super::{Drawable, DrawableClone, Tool, ToolUpdateResult}; + +#[derive(Clone, Debug)] +pub struct Highlight { + top_left: Vec2D, + size: Option, + style: Style, + editing: bool, +} + +impl Drawable for Highlight { + fn draw( + &self, + canvas: &mut femtovg::Canvas, + _font: femtovg::FontId, + ) -> Result<()> { + let size = match self.size { + Some(s) => s, + None => return Ok(()), // early exit if size is none + }; + + let (pos, size) = math::rect_ensure_positive_size(self.top_left, size); + + if self.editing { + // include a border when selecting an area. + let border_paint = + Paint::color(self.style.color.into()).with_line_width(Size::Small.to_line_width()); + let mut border_path = Path::new(); + border_path.rect(pos.x, pos.y, size.x, size.y); + canvas.stroke_path(&border_path, &border_paint); + } + + let mut shadow_path = Path::new(); + shadow_path.rect(pos.x, pos.y, size.x, size.y); + + let shadow_paint = Paint::color(femtovg::Color::rgba( + self.style.color.r, + self.style.color.g, + self.style.color.b, + self.style.size.to_highlight_opacity(), + )); + + canvas.fill_path(&shadow_path, &shadow_paint); + Ok(()) + } +} + +#[derive(Default)] +pub struct HighlightTool { + highlight: Option, + style: Style, +} + +impl Tool for HighlightTool { + fn handle_mouse_event(&mut self, event: MouseEventMsg) -> ToolUpdateResult { + match event.type_ { + MouseEventType::BeginDrag => { + self.highlight = Some(Highlight { + top_left: event.pos, + size: None, + style: self.style, + editing: true, + }); + + ToolUpdateResult::Redraw + } + MouseEventType::EndDrag => { + if let Some(a) = &mut self.highlight { + if event.pos == Vec2D::zero() { + self.highlight = None; + + ToolUpdateResult::Redraw + } else { + a.size = Some(event.pos); + a.editing = false; + + let result = a.clone_box(); + self.highlight = None; + + ToolUpdateResult::Commit(result) + } + } else { + ToolUpdateResult::Unmodified + } + } + MouseEventType::UpdateDrag => { + if let Some(a) = &mut self.highlight { + if event.pos == Vec2D::zero() { + return ToolUpdateResult::Unmodified; + } + a.size = Some(event.pos); + + ToolUpdateResult::Redraw + } else { + ToolUpdateResult::Unmodified + } + } + _ => ToolUpdateResult::Unmodified, + } + } + + fn handle_key_event(&mut self, event: crate::sketch_board::KeyEventMsg) -> ToolUpdateResult { + if event.key == Key::Escape && self.highlight.is_some() { + self.highlight = None; + ToolUpdateResult::Redraw + } else { + ToolUpdateResult::Unmodified + } + } + + fn handle_style_event(&mut self, style: Style) -> ToolUpdateResult { + self.style = style; + ToolUpdateResult::Unmodified + } + + fn get_drawable(&self) -> Option<&dyn Drawable> { + match &self.highlight { + Some(d) => Some(d), + None => None, + } + } +} diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 3b010bd..75ecfd6 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -20,6 +20,7 @@ mod arrow; mod blur; mod brush; mod crop; +mod highlight; mod line; mod marker; mod pointer; @@ -79,6 +80,18 @@ pub trait Tool { ToolUpdateResult::Unmodified } + fn active(&self) -> bool { + false + } + + fn handle_undo(&mut self) -> ToolUpdateResult { + ToolUpdateResult::Unmodified + } + + fn handle_redo(&mut self) -> ToolUpdateResult { + ToolUpdateResult::Unmodified + } + fn get_drawable(&self) -> Option<&dyn Drawable>; } @@ -113,6 +126,7 @@ pub enum ToolUpdateResult { pub use arrow::ArrowTool; pub use blur::BlurTool; pub use crop::CropTool; +pub use highlight::HighlightTool; pub use line::LineTool; pub use rectangle::RectangleTool; pub use text::TextTool; @@ -130,7 +144,8 @@ pub enum Tools { Text = 5, Marker = 6, Blur = 7, - Brush = 8, + Highlight = 8, + Brush = 9, } pub struct ToolsManager { @@ -154,6 +169,10 @@ impl ToolsManager { ); tools.insert(Tools::Text, Rc::new(RefCell::new(TextTool::default()))); tools.insert(Tools::Blur, Rc::new(RefCell::new(BlurTool::default()))); + tools.insert( + Tools::Highlight, + Rc::new(RefCell::new(HighlightTool::default())), + ); tools.insert(Tools::Marker, Rc::new(RefCell::new(MarkerTool::default()))); tools.insert(Tools::Brush, Rc::new(RefCell::new(BrushTool::default()))); @@ -202,7 +221,8 @@ impl FromVariant for Tools { 5 => Some(Tools::Text), 6 => Some(Tools::Marker), 7 => Some(Tools::Blur), - 8 => Some(Tools::Brush), + 8 => Some(Tools::Highlight), + 9 => Some(Tools::Brush), _ => None, }) } @@ -219,6 +239,7 @@ impl From for Tools { command_line::Tools::Text => Self::Text, command_line::Tools::Marker => Self::Marker, command_line::Tools::Blur => Self::Blur, + command_line::Tools::Highlight => Self::Highlight, command_line::Tools::Brush => Self::Brush, } } diff --git a/src/tools/text.rs b/src/tools/text.rs index 99ff584..a9043c9 100644 --- a/src/tools/text.rs +++ b/src/tools/text.rs @@ -23,6 +23,20 @@ pub struct Text { style: Style, } +impl Text { + fn new(pos: Vec2D, style: Style) -> Self { + let text_buffer = TextBuffer::new(None); + text_buffer.set_enable_undo(true); + + Self { + pos, + text_buffer, + editing: true, + style, + } + } +} + impl Drawable for Text { fn draw( &self, @@ -274,12 +288,7 @@ impl Tool for TextTool { }; // create a new Text - self.text = Some(Text { - pos: event.pos, - text_buffer: TextBuffer::new(None), - editing: true, - style: self.style, - }); + self.text = Some(Text::new(event.pos, self.style)); return_value } else { @@ -300,6 +309,28 @@ impl Tool for TextTool { ToolUpdateResult::Unmodified } } + + fn active(&self) -> bool { + self.text.is_some() + } + + fn handle_undo(&mut self) -> ToolUpdateResult { + if let Some(t) = &self.text { + t.text_buffer.undo(); + ToolUpdateResult::Redraw + } else { + ToolUpdateResult::Unmodified + } + } + + fn handle_redo(&mut self) -> ToolUpdateResult { + if let Some(t) = &self.text { + t.text_buffer.redo(); + ToolUpdateResult::Redraw + } else { + ToolUpdateResult::Unmodified + } + } } enum ActionScope { ForwardChar, diff --git a/src/ui/mod.rs b/src/ui/mod.rs index b243ddd..21ae71f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,2 +1 @@ -pub mod toast; pub mod toolbars; diff --git a/src/ui/toast.rs b/src/ui/toast.rs deleted file mode 100644 index fb920b7..0000000 --- a/src/ui/toast.rs +++ /dev/null @@ -1,96 +0,0 @@ -use gtk::prelude::*; -use relm4::{gtk::Align, prelude::*}; - -pub struct Toast { - text: String, - visible: bool, - next_timer: u64, - timeout: u64, -} - -#[derive(Debug)] -pub enum ToastMessage { - Show(String), -} - -#[derive(Debug)] -pub enum ToastCommand { - Hide(u64), -} - -#[relm4::component(pub)] -impl Component for Toast { - type Init = u64; - type Input = ToastMessage; - type Output = (); - type CommandOutput = ToastCommand; - - view! { - root = gtk::Box { - set_valign: Align::Start, - set_halign: Align::Center, - add_css_class: "toast", - - #[watch] - set_visible: model.visible, - - gtk::Label { - set_margin_top: 10, - set_margin_bottom: 10, - set_margin_start: 16, - set_margin_end: 16, - - - #[watch] - set_text: &model.text - } - }, - } - fn update(&mut self, message: Self::Input, sender: ComponentSender, _root: &Self::Root) { - match message { - ToastMessage::Show(text) => { - self.text = text; - self.visible = true; - self.next_timer += 1; - - let next_timer = self.next_timer; - let timeout = self.timeout; - sender.oneshot_command(async move { - tokio::time::sleep(std::time::Duration::from_millis(timeout)).await; - ToastCommand::Hide(next_timer) - }); - } - } - } - - fn update_cmd( - &mut self, - command: Self::CommandOutput, - _: ComponentSender, - _root: &Self::Root, - ) { - match command { - ToastCommand::Hide(this_timer) => { - if self.next_timer == this_timer { - self.visible = false; - } - } - } - } - - fn init( - timeout: Self::Init, - root: Self::Root, - _sender: ComponentSender, - ) -> ComponentParts { - let model = Self { - text: String::new(), - visible: false, - next_timer: 0, - timeout, - }; - - let widgets = view_output!(); - ComponentParts { model, widgets } - } -} diff --git a/src/ui/toolbars.rs b/src/ui/toolbars.rs index 333b88f..2d22e72 100644 --- a/src/ui/toolbars.rs +++ b/src/ui/toolbars.rs @@ -152,7 +152,6 @@ impl SimpleComponent for ToolsToolbar { set_icon_name: "text-case-title-regular", set_tooltip: "Text tool", ActionablePlus::set_action::: Tools::Text, - }, gtk::ToggleButton { set_focusable: false, @@ -161,7 +160,6 @@ impl SimpleComponent for ToolsToolbar { set_icon_name: "number-circle-1-regular", set_tooltip: "Numbered Marker", ActionablePlus::set_action::: Tools::Marker, - }, gtk::ToggleButton { set_focusable: false, @@ -170,7 +168,14 @@ impl SimpleComponent for ToolsToolbar { set_icon_name: "drop-regular", set_tooltip: "Blur", ActionablePlus::set_action::: Tools::Blur, + }, + gtk::ToggleButton { + set_focusable: false, + set_hexpand: false, + set_icon_name: "highlight-regular", + set_tooltip: "Highlight", + ActionablePlus::set_action::: Tools::Highlight, }, gtk::Separator {}, gtk::Button {