diff --git a/.github/workflows/rust-build.yml b/.github/workflows/rust-build.yml index 55e9a3a..7e5095c 100644 --- a/.github/workflows/rust-build.yml +++ b/.github/workflows/rust-build.yml @@ -2,9 +2,9 @@ name: Rust build on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] env: CARGO_TERM_COLOR: always @@ -14,6 +14,10 @@ jobs: name: cargo build runs-on: ubuntu-latest steps: + - name: Update apt-get + run: sudo apt-get update + - name: Install pangocairo + run: sudo apt-get install librust-pangocairo-sys-dev - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo build --all-features diff --git a/.github/workflows/rust-clippy.yml b/.github/workflows/rust-clippy.yml index 60091af..064c143 100644 --- a/.github/workflows/rust-clippy.yml +++ b/.github/workflows/rust-clippy.yml @@ -14,6 +14,10 @@ jobs: name: cargo clippy runs-on: ubuntu-latest steps: + - name: Update apt-get + run: sudo apt-get update + - name: Install pangocairo + run: sudo apt-get install librust-pangocairo-sys-dev - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 59ff29c..3af581d 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -2,15 +2,19 @@ name: Rust test on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: test: name: cargo test runs-on: ubuntu-latest steps: + - name: Update apt-get + run: sudo apt-get update + - name: Install pangocairo + run: sudo apt-get install librust-pangocairo-sys-dev - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - run: cargo test --all-features diff --git a/Cargo.lock b/Cargo.lock index 68e4b85..1eb446e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,22 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ab_glyph" -version = "0.2.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb" -dependencies = [ - "ab_glyph_rasterizer", - "owned_ttf_parser", -] - -[[package]] -name = "ab_glyph_rasterizer" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" - [[package]] name = "addr2line" version = "0.22.0" @@ -453,6 +437,29 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +[[package]] +name = "cairo-rs" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" +dependencies = [ + "bitflags 2.6.0", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df" +dependencies = [ + "glib-sys", + "libc", + "system-deps 7.0.3", +] + [[package]] name = "cc" version = "1.2.1" @@ -474,6 +481,16 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "cfg-expr" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -907,12 +924,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -954,9 +971,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fdeflate" @@ -1031,7 +1048,7 @@ dependencies = [ "memmap2", "slotmap", "tinyvec", - "ttf-parser 0.24.1", + "ttf-parser", ] [[package]] @@ -1047,12 +1064,32 @@ dependencies = [ "xdg", ] +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -1072,6 +1109,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -1092,6 +1140,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1137,12 +1186,97 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "gio" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a517657589a174be9f60c667f1fec8b7ac82ed5db4ebf56cf073a3b5955d8e2e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 7.0.3", + "windows-sys 0.59.0", +] + +[[package]] +name = "glib" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +dependencies = [ + "bitflags 2.6.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +dependencies = [ + "libc", + "system-deps 7.0.3", +] + [[package]] name = "glob" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gobject-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +dependencies = [ + "glib-sys", + "libc", + "system-deps 7.0.3", +] + [[package]] name = "half" version = "2.4.1" @@ -1336,15 +1470,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.14" @@ -1708,19 +1833,60 @@ dependencies = [ ] [[package]] -name = "owned_ttf_parser" -version = "0.24.0" +name = "owo-colors" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90" +checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" + +[[package]] +name = "pango" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e89bd74250a03a05cec047b43465469102af803be2bf5e5a1088f8b8455e087" dependencies = [ - "ttf-parser 0.24.1", + "gio", + "glib", + "libc", + "pango-sys", ] [[package]] -name = "owo-colors" -version = "4.0.0" +name = "pango-sys" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" +checksum = "71787e0019b499a5eda889279e4adb455a4f3fdd6870cd5ab7f4a5aa25df6699" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 7.0.3", +] + +[[package]] +name = "pangocairo" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4690509a2fea2a6552a0ef8aa3e5f790c1365365ee0712afa1aedb39af3997b6" +dependencies = [ + "cairo-rs", + "glib", + "libc", + "pango", + "pangocairo-sys", +] + +[[package]] +name = "pangocairo-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be6ac24147911a6a46783922fc288cf02f67570bc0d360e563b5b26aead6767" +dependencies = [ + "cairo-sys-rs", + "glib-sys", + "libc", + "pango-sys", + "system-deps 7.0.3", +] [[package]] name = "parking" @@ -1988,7 +2154,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools 0.12.1", + "itertools", "libc", "libfuzzer-sys", "log", @@ -2003,7 +2169,7 @@ dependencies = [ "rand", "rand_chacha", "simd_helpers", - "system-deps", + "system-deps 6.2.2", "thiserror", "v_frame", "wasm-bindgen", @@ -2097,21 +2263,19 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" name = "render" version = "0.1.0" dependencies = [ - "ab_glyph", "anyhow", + "cairo-rs", "config", "dbus", "derive_builder", "derive_more", "freedesktop-icons", "image", - "itertools 0.13.0", - "libc", "log", "macros", + "pangocairo", "resvg", "shared", - "ttf-parser 0.25.1", ] [[package]] @@ -2170,15 +2334,15 @@ checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2192,7 +2356,7 @@ dependencies = [ "core_maths", "log", "smallvec", - "ttf-parser 0.24.1", + "ttf-parser", "unicode-bidi-mirroring", "unicode-ccc", "unicode-properties", @@ -2473,7 +2637,20 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr", + "cfg-expr 0.15.8", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +dependencies = [ + "cfg-expr 0.17.2", "heck", "pkg-config", "toml", @@ -2482,15 +2659,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.15" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ "cfg-if", "fastrand", @@ -2715,12 +2892,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "ttf-parser" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" - [[package]] name = "typenum" version = "1.17.0" diff --git a/crates/backend/src/backend_manager.rs b/crates/backend/src/backend_manager.rs index 6561113..4f5c741 100644 --- a/crates/backend/src/backend_manager.rs +++ b/crates/backend/src/backend_manager.rs @@ -1,5 +1,5 @@ -use crate::dispatcher::Dispatcher; use crate::idle_manager::IdleManager; +use crate::{dispatcher::Dispatcher, error::Error}; use config::Config; use dbus::{actions::Signal, notification::Notification}; @@ -31,7 +31,7 @@ impl BackendManager { debug!("Backend Manager: Received notification id {notification_id} to close"); } - pub(crate) fn poll(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn poll(&mut self, config: &Config) -> Result<(), Error> { let Self { idle_manager, window_manager, @@ -66,7 +66,7 @@ impl BackendManager { self.window_manager.pop_signal() } - pub(crate) fn update_config(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn update_config(&mut self, config: &Config) -> Result<(), Error> { let Self { window_manager, idle_manager, diff --git a/crates/backend/src/banner.rs b/crates/backend/src/banner.rs index 89ef204..8992d84 100644 --- a/crates/backend/src/banner.rs +++ b/crates/backend/src/banner.rs @@ -5,35 +5,36 @@ use config::{ Config, }; use dbus::notification::Notification; -use log::{debug, trace}; +use log::{debug, error, trace}; use render::{ - color::{Bgra, Color}, drawer::Drawer, - font::FontCollection, types::RectSize, widget::{ self, Alignment, Draw, FlexContainerBuilder, Position, WImage, WText, WTextKind, Widget, WidgetConfiguration, }, + PangoContext, }; use shared::cached_data::CachedData; use crate::cache::CachedLayout; -pub struct BannerRect { +pub struct Banner { data: Notification, + layout: Option, created_at: time::Instant, framebuffer: Vec, } -impl BannerRect { +impl Banner { pub(crate) fn init(notification: Notification) -> Self { debug!("Banner (id={}): Created", notification.id); Self { data: notification, + layout: None, created_at: time::Instant::now(), framebuffer: vec![], @@ -68,6 +69,22 @@ impl BannerRect { ); } + // TODO: use it for resize + #[allow(unused)] + pub(crate) fn width(&self) -> usize { + self.layout + .as_ref() + .map(|layout| layout.width()) + .unwrap_or_default() + } + + pub(crate) fn height(&self) -> usize { + self.layout + .as_ref() + .map(|layout| layout.height()) + .unwrap_or_default() + } + #[inline] pub(crate) fn framebuffer(&self) -> &[u8] { &self.framebuffer @@ -75,10 +92,10 @@ impl BannerRect { pub(crate) fn draw( &mut self, - font_collection: &FontCollection, + pango_context: &PangoContext, config: &Config, cached_layouts: &CachedData, - ) { + ) -> DrawState { debug!("Banner (id={}): Beginning of draw", self.data.id); let rect_size = RectSize::new( @@ -87,7 +104,13 @@ impl BannerRect { ); let display = config.display_by_app(&self.data.app_name); - let mut drawer = Drawer::new(Color::Fill(Bgra::new()), rect_size.clone()); + let mut drawer = match Drawer::create(rect_size) { + Ok(drawer) => drawer, + Err(err) => { + error!("Failed to create drawer for Banner(id={}), avoided to draw banner. Error: {err}", self.data.id); + return DrawState::Failure; + } + }; let mut layout = match &display.layout { config::display::Layout::Default => Self::default_layout(display), @@ -104,15 +127,33 @@ impl BannerRect { display_config: display, theme: config.theme_by_app(&self.data.app_name), notification: &self.data, - font_collection, + pango_context, override_properties: display.layout.is_default(), }, ); - layout.draw(&mut drawer); - self.framebuffer = drawer.into(); + if let Err(err) = layout.draw(pango_context, &mut drawer) { + error!( + "Failed to draw banner Banner(id={}). Error: {err}", + self.data.id + ); + return DrawState::Failure; + } + + self.layout = Some(layout); + self.framebuffer = match drawer.try_into() { + Ok(val) => val, + Err(err) => { + error!( + "Failed to get data after drawing Banner(id={}). Error: {err}", + self.data.id + ); + return DrawState::Failure; + } + }; debug!("Banner (id={}): Complete draw", self.data.id); + DrawState::Success } fn default_layout(display_config: &DisplayConfig) -> Widget { @@ -143,8 +184,13 @@ impl BannerRect { } } -impl<'a> From<&'a BannerRect> for &'a Notification { - fn from(value: &'a BannerRect) -> Self { +impl<'a> From<&'a Banner> for &'a Notification { + fn from(value: &'a Banner) -> Self { &value.data } } + +pub(crate) enum DrawState { + Success, + Failure, +} diff --git a/crates/backend/src/error.rs b/crates/backend/src/error.rs new file mode 100644 index 0000000..7c4dd69 --- /dev/null +++ b/crates/backend/src/error.rs @@ -0,0 +1,18 @@ +use dbus::notification::Notification; + +pub(crate) enum Error { + UnrenderedNotifications(Vec), + Fatal(anyhow::Error), +} + +impl From for Error { + fn from(value: anyhow::Error) -> Self { + Self::Fatal(value) + } +} + +impl From> for Error { + fn from(value: Vec) -> Self { + Self::UnrenderedNotifications(value) + } +} diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index e2add01..718c0c0 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,4 +1,5 @@ use config::Config; +use error::Error; use log::{debug, info, warn}; use scheduler::Scheduler; use shared::file_watcher::FileState; @@ -8,6 +9,7 @@ mod backend_manager; mod banner; mod cache; mod dispatcher; +mod error; mod idle_manager; mod idle_notifier; mod scheduler; @@ -68,19 +70,19 @@ pub async fn run(mut config: Config) -> anyhow::Result<()> { ); }); - backend_manager.poll(&config)?; + backend_manager.poll(&config).handle_error()?; match config.check_updates() { FileState::Updated => { partially_default_config = false; config.update(); - backend_manager.update_config(&config)?; + backend_manager.update_config(&config).handle_error()?; info!("Renderer: Detected changes of config files and updated") } FileState::NotFound if !partially_default_config => { partially_default_config = true; config.update(); - backend_manager.update_config(&config)?; + backend_manager.update_config(&config).handle_error()?; info!("The main or imported configuration file is not found, reverting this part to default values."); } FileState::NotFound | FileState::NothingChanged => (), @@ -104,6 +106,25 @@ pub async fn run(mut config: Config) -> anyhow::Result<()> { } } +trait HandleError { + fn handle_error(self) -> anyhow::Result; +} + +impl HandleError for Result { + fn handle_error(self) -> anyhow::Result { + match self { + Ok(val) => Ok(val), + Err(err) => match err { + Error::UnrenderedNotifications(_vec) => { + //TODO: handle unrenedered banners + Ok(Default::default()) + } + Error::Fatal(error) => Err(error)?, + }, + } + } +} + fn debug_signal(signal: &Signal) { match signal { Signal::ActionInvoked { diff --git a/crates/backend/src/window.rs b/crates/backend/src/window.rs index 720e320..34674d0 100644 --- a/crates/backend/src/window.rs +++ b/crates/backend/src/window.rs @@ -36,14 +36,17 @@ use dbus::{ notification::{self, Notification}, }; -use crate::{banner::BannerRect, cache::CachedLayout}; -use render::{font::FontCollection, types::RectSize}; +use crate::{ + banner::{Banner, DrawState}, + cache::CachedLayout, +}; +use render::{types::RectSize, PangoContext}; pub(super) struct Window { - banners: IndexMap, - font_collection: Rc>, + banners: IndexMap, + pango_context: Rc>, - rect_size: RectSize, + rect_size: RectSize, margin: Margin, compositor: Option, @@ -68,12 +71,12 @@ pub(super) enum ConfigurationState { } impl Window { - pub(super) fn init(font_collection: Rc>, config: &Config) -> Self { + pub(super) fn init(pango_context: Rc>, config: &Config) -> Self { debug!("Window: Initialized"); Self { banners: indexmap! {}, - font_collection, + pango_context, rect_size: RectSize::new( config.general().width.into(), @@ -158,7 +161,7 @@ impl Window { pub(super) fn reconfigure(&mut self, config: &Config) { self.relocate(config.general().offset, &config.general().anchor); self.banners - .sort_by_values(config.general().sorting.get_cmp::()); + .sort_by_values(config.general().sorting.get_cmp::()); debug!("Window: Re-sorted the notification banners"); debug!("Window: Reconfigured by updated config"); @@ -202,19 +205,31 @@ impl Window { notifications: Vec, config: &Config, cached_layouts: &CachedData, - ) { - self.banners - .extend(notifications.into_iter().map(|notification| { - let mut banner_rect = BannerRect::init(notification); - banner_rect.draw(&self.font_collection.borrow(), config, cached_layouts); - (banner_rect.notification().id, banner_rect) - })); + ) -> Result<(), Vec> { + let mut failed_to_draw_banners = vec![]; + for notification in notifications { + let mut banner = Banner::init(notification); + match banner.draw(&self.pango_context.borrow(), config, cached_layouts) { + crate::banner::DrawState::Success => { + self.banners.insert(banner.notification().id, banner); + } + crate::banner::DrawState::Failure => { + failed_to_draw_banners.push(banner.destroy_and_get_notification()); + } + } + } self.banners - .sort_by_values(config.general().sorting.get_cmp::()); + .sort_by_values(config.general().sorting.get_cmp::()); debug!("Window: Sorted the notification banners"); - debug!("Window: Completed update the notification banners") + debug!("Window: Completed update the notification banners"); + + if failed_to_draw_banners.is_empty() { + Ok(()) + } else { + Err(failed_to_draw_banners) + } } pub(super) fn replace_by_indices( @@ -222,24 +237,40 @@ impl Window { notifications: &mut VecDeque, config: &Config, cached_layouts: &CachedData, - ) { + ) -> Result<(), Vec> { let matching_indices: Vec = notifications .iter() .enumerate() .filter_map(|(i, notification)| self.banners.get(¬ification.id).map(|_| i)) .collect(); + let mut failed_to_redraw_banners = vec![]; for notification_index in matching_indices.into_iter().rev() { let notification = notifications.remove(notification_index).unwrap(); + let id = notification.id; - let rect = &mut self.banners[¬ification.id]; - rect.update_data(notification); - rect.draw(&self.font_collection.borrow(), config, cached_layouts); + let banner = &mut self.banners[&id]; + banner.update_data(notification); - debug!( - "Window: Replaced notification by id {}", - rect.notification().id - ); + match banner.draw(&self.pango_context.borrow(), config, cached_layouts) { + DrawState::Success => { + debug!("Window: Replaced notification by id {id}",); + } + DrawState::Failure => { + failed_to_redraw_banners.push( + self.banners + .shift_remove(&id) + .expect("There is should be banner") + .destroy_and_get_notification(), + ); + } + } + } + + if failed_to_redraw_banners.is_empty() { + Ok(()) + } else { + Err(failed_to_redraw_banners) } } @@ -306,9 +337,7 @@ impl Window { } pub(super) fn reset_timeouts(&mut self) { - self.banners - .values_mut() - .for_each(BannerRect::reset_timeout); + self.banners.values_mut().for_each(Banner::reset_timeout); } pub(super) fn handle_click(&mut self, config: &Config) -> Vec { @@ -320,7 +349,7 @@ impl Window { if let Some(id) = self.get_hovered_banner(config) { if config.general().anchor.is_bottom() { self.pointer_state.y -= - config.general().height as f64 + config.general().gap as f64; + self.banners[&id].height() as f64 + config.general().gap as f64; } debug!("Window: Clicked to notification banner with id {id}"); @@ -343,25 +372,24 @@ impl Window { return None; } - let rect_height = config.general().height as usize; - let gap = config.general().gap as usize; - - let region_iter = (0..self.rect_size.height).step_by(rect_height + gap); - let finder = |(banner, rect_top): (&BannerRect, usize)| { - let rect_bottom = rect_top + rect_height; - (rect_top as f64..rect_bottom as f64) - .contains(&(self.pointer_state.y)) - .then(|| banner.notification().id) + let mut offset = 0.0; + let gap = config.general().gap as f64; + + let finder = |banner: &Banner| { + let banner_height = banner.height() as f64; + let bottom = offset + banner_height; + if (offset..bottom).contains(&self.pointer_state.y) { + Some(banner.notification().id) + } else { + offset += banner_height + gap; + None + } }; if config.general().anchor.is_top() { - self.banners - .values() - .rev() - .zip(region_iter) - .find_map(finder) + self.banners.values().rev().find_map(finder) } else { - self.banners.values().zip(region_iter).find_map(finder) + self.banners.values().find_map(finder) } } @@ -370,14 +398,35 @@ impl Window { qhandle: &QueueHandle, config: &Config, cached_layouts: &CachedData, - ) { - self.banners - .values_mut() - .for_each(|banner| banner.draw(&self.font_collection.borrow(), config, cached_layouts)); + ) -> Result<(), Vec> { + let mut failed_to_redraw_banners = vec![]; + for (id, banner) in &mut self.banners { + if let DrawState::Failure = + banner.draw(&self.pango_context.borrow(), config, cached_layouts) + { + failed_to_redraw_banners.push(*id); + } + } + + let failed_to_redraw_banners: Vec<_> = failed_to_redraw_banners + .into_iter() + .map(|id| { + self.banners + .shift_remove(&id) + .expect("There is should be a banner") + .destroy_and_get_notification() + }) + .collect(); self.draw(qhandle, config); debug!("Window: Redrawed banners"); + + if failed_to_redraw_banners.is_empty() { + Ok(()) + } else { + Err(failed_to_redraw_banners) + } } pub(super) fn draw(&mut self, qhandle: &QueueHandle, config: &Config) { @@ -385,7 +434,10 @@ impl Window { self.resize(RectSize::new( config.general().width.into(), - self.banners.len() * config.general().height as usize + self.banners + .values() + .map(|banner| banner.height()) + .sum::() + self.banners.len().saturating_sub(1) * gap as usize, )); @@ -396,7 +448,7 @@ impl Window { self.build_buffer(qhandle); } - fn resize(&mut self, rect_size: RectSize) { + fn resize(&mut self, rect_size: RectSize) { self.rect_size = rect_size; let layer_surface = unsafe { self.layer_surface.as_ref().unwrap_unchecked() }; @@ -419,26 +471,29 @@ impl Window { unsafe { buffer.unwrap_unchecked() }.push(data); } - let writer = |(i, rect): (usize, &BannerRect)| { - write(self.buffer.as_mut(), rect.framebuffer()); + let threshold = self.banners.len().saturating_sub(1); + let mut total = 0; + let writer = |banner: &Banner| { + write(self.buffer.as_mut(), banner.framebuffer()); - if i < self.banners.len().saturating_sub(1) { + if total < threshold { write(self.buffer.as_mut(), gap_buffer); } + + total += 1; }; if anchor.is_top() { - self.banners.values().rev().enumerate().for_each(writer) + self.banners.values().rev().for_each(writer) } else { - self.banners.values().enumerate().for_each(writer) + self.banners.values().for_each(writer) } debug!("Window: Writed banners to buffer"); } fn create_buffer(&mut self, qhandle: &QueueHandle) { - if self.buffer.is_some() { - let buffer = unsafe { self.buffer.as_mut().unwrap_unchecked() }; + if let Some(buffer) = self.buffer.as_mut() { buffer.reset(); return; } @@ -478,7 +533,9 @@ impl Window { self.buffer .as_ref() .is_some_and(|buffer| buffer.size() >= self.rect_size.area() * 4), - "Buffer size must be greater or equal to window size!" + "Buffer size must be greater or equal to window size. Buffer size: {}. Window area: {}.", + self.buffer.as_ref().map(|buffer| buffer.size).unwrap_or_default(), + self.rect_size.area() ); let shm_pool = unsafe { self.shm_pool.as_ref().unwrap_unchecked() }; @@ -543,6 +600,9 @@ impl Buffer { } fn reset(&mut self) { + self.file + .write_all_at(&vec![0; self.cursor as usize + 1], 0) + .expect("Must be possibility to write into file"); self.cursor = 0; debug!("Buffer: Reset"); } @@ -550,7 +610,7 @@ impl Buffer { fn push(&mut self, data: &[u8]) { self.file .write_all_at(data, self.cursor) - .expect("Must be possibility to write into file!"); + .expect("Must be possibility to write into file"); self.cursor += data.len() as u64; self.size = std::cmp::max(self.size, self.cursor as usize); diff --git a/crates/backend/src/window_manager.rs b/crates/backend/src/window_manager.rs index fc1e212..e72ef69 100644 --- a/crates/backend/src/window_manager.rs +++ b/crates/backend/src/window_manager.rs @@ -1,17 +1,17 @@ use std::{cell::RefCell, collections::VecDeque, path::PathBuf, rc::Rc}; use log::debug; +use render::PangoContext; use shared::cached_data::CachedData; use wayland_client::{Connection, EventQueue, QueueHandle}; -use crate::cache::CachedLayout; use crate::dispatcher::Dispatcher; +use crate::{cache::CachedLayout, error::Error}; use config::Config; use dbus::{actions::Signal, notification::Notification}; use super::window::{ConfigurationState, Window}; -use render::font::FontCollection; pub(crate) struct WindowManager { connection: Connection, @@ -19,7 +19,7 @@ pub(crate) struct WindowManager { qhandle: Option>, window: Option, - font_collection: Rc>, + pango_context: Rc>, cached_layouts: CachedData, signals: Vec, @@ -41,8 +41,8 @@ impl Dispatcher for WindowManager { impl WindowManager { pub(crate) fn init(config: &Config) -> anyhow::Result { let connection = Connection::connect_to_env()?; - let font_collection = - Rc::new(FontCollection::load_by_font_name(&config.general().font.name)?.into()); + let pango_context = + Rc::new(PangoContext::from_font_family(&config.general().font.name).into()); let cached_layouts = config .displays() .filter_map(|display| match &display.layout { @@ -57,7 +57,7 @@ impl WindowManager { qhandle: None, window: None, - font_collection, + pango_context, cached_layouts, signals: vec![], @@ -74,7 +74,7 @@ impl WindowManager { self.cached_layouts.update() } - pub(crate) fn update_by_config(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn update_by_config(&mut self, config: &Config) -> Result<(), Error> { self.cached_layouts.extend_by_keys( config .displays() @@ -85,22 +85,24 @@ impl WindowManager { .collect(), ); - self.font_collection + self.pango_context .borrow_mut() - .update_by_font_name(&config.general().font.name)?; + .update_font_family(&config.general().font.name); + let mut unrendered_notifcations = Ok(()); if let Some(window) = self.window.as_mut() { let qhandle = unsafe { self.qhandle.as_ref().unwrap_unchecked() }; window.reconfigure(config); - window.redraw(qhandle, config, &self.cached_layouts); + unrendered_notifcations = window.redraw(qhandle, config, &self.cached_layouts); window.frame(qhandle); window.commit(); } debug!("Window Manager: Updated the windows by updated config"); - self.roundtrip_event_queue() + self.roundtrip_event_queue()?; + Ok(unrendered_notifcations?) } pub(crate) fn create_notification(&mut self, notification: Box) { @@ -111,7 +113,7 @@ impl WindowManager { self.close_notifications.push(notification_id); } - pub(crate) fn show_window(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn show_window(&mut self, config: &Config) -> Result<(), Error> { let mut notifications_limit = config.general().limit as usize; if notifications_limit == 0 { notifications_limit = usize::MAX; @@ -130,7 +132,8 @@ impl WindowManager { Ok(()) } - fn process_notification_queue(&mut self, config: &Config) -> anyhow::Result<()> { + fn process_notification_queue(&mut self, config: &Config) -> Result<(), Error> { + let mut unrendered_notifications = Ok(()); if let Some(window) = self.window.as_mut() { let mut notifications_limit = config.general().limit as usize; @@ -138,7 +141,11 @@ impl WindowManager { notifications_limit = usize::MAX } - window.replace_by_indices(&mut self.notification_queue, config, &self.cached_layouts); + window.replace_by_indices( + &mut self.notification_queue, + config, + &self.cached_layouts, + )?; let available_slots = notifications_limit.saturating_sub(window.total_banners()); let notifications_to_display: Vec<_> = self @@ -146,16 +153,17 @@ impl WindowManager { .drain(..available_slots.min(self.notification_queue.len())) .collect(); - window.update_banners(notifications_to_display, config, &self.cached_layouts); + unrendered_notifications = + window.update_banners(notifications_to_display, config, &self.cached_layouts); self.update_window(config)?; self.roundtrip_event_queue()?; } - Ok(()) + Ok(unrendered_notifications?) } - pub(crate) fn handle_close_notifications(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn handle_close_notifications(&mut self, config: &Config) -> Result<(), Error> { if self.window.as_ref().is_some() && !self.close_notifications.is_empty() { let window = self.window.as_mut().unwrap(); @@ -182,7 +190,7 @@ impl WindowManager { Ok(()) } - pub(crate) fn remove_expired(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn remove_expired(&mut self, config: &Config) -> Result<(), Error> { if let Some(window) = self.window.as_mut() { let notifications = window.remove_expired_banners(config); @@ -207,7 +215,7 @@ impl WindowManager { self.signals.pop() } - pub(crate) fn handle_actions(&mut self, config: &Config) -> anyhow::Result<()> { + pub(crate) fn handle_actions(&mut self, config: &Config) -> Result<(), Error> { //TODO: change it to actions which defines in config file if let Some(window) = self.window.as_mut() { @@ -268,7 +276,7 @@ impl WindowManager { let display = self.connection.display(); display.get_registry(&qhandle, ()); - let mut window = Window::init(self.font_collection.clone(), config); + let mut window = Window::init(self.pango_context.clone(), config); while let ConfigurationState::NotConfiured = window.configuration_state() { event_queue.blocking_dispatch(&mut window)?; diff --git a/crates/config/src/text.rs b/crates/config/src/text.rs index d208abd..dde8bdb 100644 --- a/crates/config/src/text.rs +++ b/crates/config/src/text.rs @@ -14,7 +14,10 @@ public! { wrap: bool, #[gbuilder(default)] - ellipsize_at: EllipsizeAt, + wrap_mode: WrapMode, + + #[gbuilder(default)] + ellipsize: Ellipsize, #[gbuilder(default)] style: TextStyle, @@ -23,7 +26,10 @@ public! { margin: Spacing, #[gbuilder(default)] - justification: TextJustification, + alignment: TextAlignment, + + #[gbuilder(default(false))] + justify: bool, #[cfg_prop(default(12))] font_size: u8, @@ -42,6 +48,32 @@ impl Default for TextProperty { impl TryFromValue for TextProperty {} +#[derive(Debug, Clone, Default, Deserialize)] +pub enum WrapMode { + #[serde(rename = "word")] + Word, + #[serde(rename = "word-char")] + #[default] + WordChar, + #[serde(rename = "char")] + Char, +} + +impl TryFromValue for WrapMode { + fn try_from_string(value: String) -> Result { + Ok(match value.to_lowercase().as_str() { + "word" => WrapMode::Word, + "char" => WrapMode::Char, + "word-char" | "word_char" => WrapMode::WordChar, + _ => Err(shared::error::ConversionError::InvalidValue { + expected: "word, char, word-char or word_char", + actual: value, + })?, + }) + + } +} + #[derive(Debug, Deserialize, Default, Clone)] pub enum TextStyle { #[default] @@ -71,7 +103,7 @@ impl TryFromValue for TextStyle { } #[derive(Debug, Deserialize, Default, Clone)] -pub enum TextJustification { +pub enum TextAlignment { #[serde(rename = "center")] Center, #[default] @@ -79,19 +111,16 @@ pub enum TextJustification { Left, #[serde(rename = "right")] Right, - #[serde(rename = "space-between")] - SpaceBetween, } -impl TryFromValue for TextJustification { +impl TryFromValue for TextAlignment { fn try_from_string(value: String) -> Result { Ok(match value.to_lowercase().as_str() { - "center" => TextJustification::Center, - "left" => TextJustification::Left, - "right" => TextJustification::Right, - "space-between" | "space_between" => TextJustification::SpaceBetween, + "center" => TextAlignment::Center, + "left" => TextAlignment::Left, + "right" => TextAlignment::Right, _ => Err(shared::error::ConversionError::InvalidValue { - expected: "center, left, right, space-between or space_between", + expected: "center, left or right", actual: value, })?, }) @@ -102,26 +131,30 @@ impl TomlTextProperty { pub(super) fn default_title() -> Self { Self { style: Some(TextStyle::Bold), - justification: Some(TextJustification::Center), + alignment: Some(TextAlignment::Center), ..Default::default() } } } #[derive(Debug, Deserialize, Default, Clone)] -pub enum EllipsizeAt { +pub enum Ellipsize { + #[serde(rename = "start")] + Start, #[serde(rename = "middle")] Middle, #[default] #[serde(rename = "end")] End, + #[serde(rename = "none")] + None, } -impl TryFromValue for EllipsizeAt { +impl TryFromValue for Ellipsize { fn try_from_string(value: String) -> Result { Ok(match value.to_lowercase().as_str() { - "middle" => EllipsizeAt::Middle, - "end" => EllipsizeAt::End, + "middle" => Ellipsize::Middle, + "end" => Ellipsize::End, _ => Err(shared::error::ConversionError::InvalidValue { expected: "middle or end", actual: value, diff --git a/crates/dbus/src/text.rs b/crates/dbus/src/text.rs index 588c3a6..7417abd 100644 --- a/crates/dbus/src/text.rs +++ b/crates/dbus/src/text.rs @@ -20,6 +20,7 @@ struct Parser<'a> { entities: Vec, stack: Vec, pos: usize, + byte_pos: usize, cursor: unic_segment::GraphemeCursor, } @@ -31,6 +32,7 @@ impl<'a> Parser<'a> { entities: Vec::new(), stack: Vec::new(), pos: 0, + byte_pos: 0, cursor: unic_segment::GraphemeCursor::new(0, input.len()), } } @@ -57,6 +59,8 @@ impl<'a> Parser<'a> { ); self.pos += unic_segment::Graphemes::new(&decoded_html_entity).count(); + self.byte_pos += decoded_html_entity.len(); + self.body.push_str(&decoded_html_entity); self.cursor.set_cursor(end_html_entity_pos); @@ -70,6 +74,7 @@ impl<'a> Parser<'a> { } self.pos += 1; + self.byte_pos += self.cursor.cur_cursor() - byte_pos; self.body.push_str(grapheme); byte_pos = self.cursor.cur_cursor(); @@ -181,6 +186,7 @@ impl<'a> Parser<'a> { self.stack.push(ParsedTag { tag, begin_position: self.pos, + begin_position_byte: self.byte_pos, }); } TagType::Closing => { @@ -203,6 +209,7 @@ impl<'a> Parser<'a> { let ParsedTag { tag, begin_position, + begin_position_byte, } = self.stack.pop().unwrap(); let length = self.pos - begin_position; @@ -214,7 +221,9 @@ impl<'a> Parser<'a> { { self.entities.push(Entity { offset: begin_position, + offset_in_byte: begin_position_byte, length, + length_in_byte: self.byte_pos - begin_position_byte, kind: tag.kind, }); } @@ -232,7 +241,9 @@ impl<'a> Parser<'a> { fn handle_self_closing_tag(&mut self, tag: Tag) { self.entities.push(Entity { offset: self.pos, + offset_in_byte: self.byte_pos, length: 0, + length_in_byte: 0, kind: tag.kind, }); } @@ -241,6 +252,7 @@ impl<'a> Parser<'a> { while let Some(ParsedTag { tag, begin_position, + begin_position_byte, }) = self.stack.pop() { let length = self.pos - begin_position; @@ -253,7 +265,9 @@ impl<'a> Parser<'a> { { self.entities.push(Entity { offset: begin_position, + offset_in_byte: begin_position_byte, length, + length_in_byte: self.byte_pos - begin_position_byte, kind: tag.kind, }); } @@ -264,7 +278,9 @@ impl<'a> Parser<'a> { #[derive(Debug, PartialEq, Eq)] pub struct Entity { pub offset: usize, + pub offset_in_byte: usize, pub length: usize, + pub length_in_byte: usize, pub kind: EntityKind, } @@ -297,6 +313,7 @@ impl EntityKind { struct ParsedTag { tag: Tag, begin_position: usize, + begin_position_byte: usize, } #[derive(Debug, PartialEq, Eq)] @@ -566,7 +583,9 @@ mod tests { body: String::from("coffee ☕️"), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 8, + length_in_byte: 13, kind: EntityKind::Bold }] } @@ -598,17 +617,23 @@ mod tests { entities: vec![ Entity { offset: 4, + offset_in_byte: 4, length: 4, + length_in_byte: 4, kind: EntityKind::Bold, }, Entity { offset: 9, + offset_in_byte: 9, length: 7, + length_in_byte: 7, kind: EntityKind::Italic, }, Entity { offset: 16, + offset_in_byte: 16, length: 3, + length_in_byte: 3, kind: EntityKind::Underline, }, ], @@ -626,7 +651,9 @@ mod tests { body: String::from("hello world!!!"), entities: vec![Entity { offset: 6, + offset_in_byte: 6, length: 8, + length_in_byte: 8, kind: EntityKind::Bold, }], } @@ -644,12 +671,16 @@ mod tests { entities: vec![ Entity { offset: 0, + offset_in_byte: 0, length: 14, + length_in_byte: 14, kind: EntityKind::Italic, }, Entity { offset: 6, + offset_in_byte: 6, length: 8, + length_in_byte: 8, kind: EntityKind::Bold, }, ], @@ -667,7 +698,9 @@ mod tests { body: String::from("hello"), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 5, + length_in_byte: 5, kind: EntityKind::Italic, },], } @@ -698,12 +731,16 @@ mod tests { entities: vec![ Entity { offset: 6, + offset_in_byte: 6, length: 5, + length_in_byte: 5, kind: EntityKind::Bold, }, Entity { offset: 8, + offset_in_byte: 8, length: 3, + length_in_byte: 3, kind: EntityKind::Italic, }, ], @@ -721,7 +758,9 @@ mod tests { body: String::from("hello click!!!"), entities: vec![Entity { offset: 6, + offset_in_byte: 6, length: 5, + length_in_byte: 5, kind: EntityKind::Link { href: Some("link.com".to_string()) }, @@ -740,7 +779,9 @@ mod tests { body: String::from("link"), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 4, + length_in_byte: 4, kind: EntityKind::Link { href: None }, },], } @@ -757,7 +798,9 @@ mod tests { body: String::from("image:"), entities: vec![Entity { offset: 6, + offset_in_byte: 6, length: 0, + length_in_byte: 0, kind: EntityKind::Image { src: Some("/path/to/image.png".to_string()), alt: None @@ -777,7 +820,9 @@ mod tests { body: String::from("some text"), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 0, + length_in_byte: 0, kind: EntityKind::Image { src: Some("/path/to/image.png".to_string()), alt: None @@ -798,7 +843,9 @@ mod tests { body: String::from("image: !!!"), entities: vec![Entity { offset: 7, + offset_in_byte: 7, length: 0, + length_in_byte: 0, kind: EntityKind::Image { src: Some("/path/to/image.png".to_string()), alt: Some("some cool image".to_string()), @@ -819,12 +866,16 @@ mod tests { entities: vec![ Entity { offset: 0, + offset_in_byte: 0, length: 4, + length_in_byte: 4, kind: EntityKind::Italic }, Entity { offset: 0, + offset_in_byte: 0, length: 4, + length_in_byte: 4, kind: EntityKind::Bold }, ], @@ -881,7 +932,9 @@ mod tests { body: String::from("hello\""), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 6, + length_in_byte: 6, kind: EntityKind::Bold }] } @@ -898,7 +951,9 @@ mod tests { body: String::from("hello""), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 11, + length_in_byte: 11, kind: EntityKind::Bold }] } @@ -928,7 +983,9 @@ mod tests { body: String::from("penis"), entities: vec![Entity { offset: 0, + offset_in_byte: 0, length: 12, + length_in_byte: 12, kind: EntityKind::Bold }] } diff --git a/crates/filetype/src/converter.rs b/crates/filetype/src/converter.rs index 7960f58..6e0ab26 100644 --- a/crates/filetype/src/converter.rs +++ b/crates/filetype/src/converter.rs @@ -104,7 +104,10 @@ fn convert_node_type<'a>( } } - Ok(widget_gbuilder.try_build()?.try_downcast()?) + match widget_gbuilder.try_build()?.try_downcast() { + Ok(val) => Ok(val), + Err(err) => bail!("{err}"), + } } fn convert_properties<'a>( @@ -303,7 +306,7 @@ impl GBuilder { Ok(self) } - fn try_build(self) -> anyhow::Result> { + fn try_build(self) -> anyhow::Result> { macro_rules! implement_variants { ($($variant:ident into $dest_type:path),*) => { match self { diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index ef0bf50..4025b7d 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -15,10 +15,8 @@ anyhow.workspace = true derive_more.workspace = true derive_builder = "0.20.0" -ab_glyph = "0.2.28" resvg = "0.43.0" -image = "0.25.2" -itertools = "0.13.0" +image = { version = "0.25.2", features = ["png"] } freedesktop-icons = "0.2.6" -libc = "0.2.169" -ttf-parser = "0.25.1" +pangocairo = "0.20.7" +cairo-rs = { version = "0.20.7", features = ["png", "svg"] } diff --git a/crates/render/src/border.rs b/crates/render/src/border.rs deleted file mode 100644 index 4af9d88..0000000 --- a/crates/render/src/border.rs +++ /dev/null @@ -1,348 +0,0 @@ -use crate::color::{Bgra, Color}; -use derive_builder::Builder; -use log::warn; - -use crate::drawer::Drawer; - -use super::{ - types::Offset, - widget::{Coverage, Draw, DrawColor}, -}; - -type Matrix = Vec>; -type MaybeColor = Option; - -#[derive(Default, Builder, Clone)] -pub struct Border { - color: Color, - frame_width: usize, - frame_height: usize, - - #[builder(setter(into))] - size: usize, - #[builder(setter(into))] - radius: usize, - - #[builder(setter(skip))] - corner_coverage: Option>, - - #[builder(setter(skip), default = "false")] - compiled: bool, -} - -impl BorderBuilder { - pub fn compile(&self) -> anyhow::Result { - let mut border = self.build()?; - border.compile(); - Ok(border) - } -} - -enum Corner { - TopLeft, - TopRight, - BottomLeft, - BottomRight, -} - -#[derive(Clone)] -enum DrawingMethod { - Replace(Coverage), - Transparent(Coverage), - DependingOnBorderAlpha(Coverage, Coverage), -} - -impl Border { - pub fn compile(&mut self) { - self.compiled = true; - - self.corner_coverage = Some(match (self.size, self.radius) { - (0, 0) => return, - (size, 0) => self.get_bordered_coverage(size), - (0, radius) => self.get_corner_coverage(radius), - (size, radius) => self.get_bordered_corner_coverage(size, radius), - }); - } - - pub fn get_color_at(&self, x: usize, y: usize) -> Option { - assert!(x <= self.frame_width && y <= self.frame_height); - - let corner = self.corner_coverage.as_ref()?; - let corner_size = corner.len(); - - if (corner_size..self.frame_width - corner_size).contains(&x) - && (corner_size..self.frame_height - corner_size).contains(&y) - { - return None; - } - - if (corner_size..self.frame_width - corner_size).contains(&x) { - return if y < self.size || y > self.frame_height - self.size { - Some(DrawColor::Replace(self.get_color(x, y))) - } else { - None - }; - } - - if (corner_size..self.frame_height - corner_size).contains(&y) { - return if x < self.size || x > self.frame_width - self.size { - Some(DrawColor::Replace(self.get_color(x, y))) - } else { - None - }; - } - - let x_pos = if x < corner_size { - x - } else { - self.frame_width - x - 1 - }; - - let y_pos = if y < corner_size { - y - } else { - self.frame_height - y - 1 - }; - - corner[x_pos][y_pos] - .as_ref() - .map(|drawing_method| self.map_color(drawing_method, x, y)) - } - - #[inline] - fn get_bordered_coverage(&self, width: usize) -> Matrix { - vec![vec![Some(DrawingMethod::Replace(Coverage(1.0))); width]; width] - } - - fn get_corner_coverage(&self, radius: usize) -> Matrix { - let radius = std::cmp::min(radius, self.frame_height / 2); - let mut corner = vec![vec![None; radius]; radius]; - - self.traverse_circle_with(radius, |inner_x, inner_y, rev_x, rev_y| { - let cell_coverage = Self::get_coverage_by(radius as f32, rev_x as f32, rev_y as f32); - - if cell_coverage == 1.0 { - return false; - } - - corner[inner_x][inner_y] = Some(DrawingMethod::Transparent(Coverage(cell_coverage))); - corner[inner_y][inner_x] = Some(DrawingMethod::Transparent(Coverage(cell_coverage))); - - true - }); - - corner - } - - fn get_bordered_corner_coverage(&self, size: usize, radius: usize) -> Matrix { - let radius = std::cmp::min(radius, self.frame_height / 2); - let inner_radius = radius.saturating_sub(size); - - let mut corner = vec![vec![None; radius]; radius]; - - self.traverse_circle_with(radius, |inner_x, inner_y, rev_x, rev_y| { - let (x_f32, y_f32) = (rev_x as f32, rev_y as f32); - - let border_cell_coverage = Self::get_coverage_by(radius as f32, x_f32, y_f32); - - let mut to_continue = true; - - let color = if inner_radius != 0 { - let inner_cell_coverage = Self::get_coverage_by(inner_radius as f32, x_f32, y_f32); - - match inner_cell_coverage { - 0.0 => DrawingMethod::Replace(Coverage(border_cell_coverage)), - 1.0 => { - to_continue = false; - DrawingMethod::Transparent(Coverage(1.0)) - } - cell_coverage => DrawingMethod::DependingOnBorderAlpha( - Coverage(border_cell_coverage), - Coverage(cell_coverage), - ), - } - } else { - DrawingMethod::Replace(Coverage(border_cell_coverage)) - }; - - corner[inner_x][inner_y] = Some(color.clone()); - corner[inner_y][inner_x] = Some(color); - - to_continue - }); - - corner - } - - fn traverse_circle_with bool>( - &self, - radius: usize, - mut calc: Calc, - ) { - for x in 0..radius { - let rev_x = radius - x - 1; - - for y in 0..radius { - let rev_y = radius - y - 1; - let to_continue = calc(x, y, rev_x, rev_y); - - if !to_continue { - break; - } - } - } - } - - #[inline] - fn get_coverage_by(radius: f32, x: f32, y: f32) -> f32 { - let inner_hypot = f32::hypot(x, y); - let inner_diff = radius - inner_hypot; - let outer_hypot = f32::hypot(x + 1.0, y + 1.0); - - if inner_hypot >= radius { - 0.0 - } else if outer_hypot >= radius { - inner_diff.clamp(0.0, 1.0) - } else { - 1.0 - } - } - - #[inline] - fn draw_corner(&self, offset: Offset, corner_type: Corner, drawer: &mut Drawer) { - let Some(corner) = &self.corner_coverage else { - return; - }; - - let corner_size = corner.len(); - let mut x_range = offset.x..offset.x + corner_size; - let y_range = offset.y..offset.y + corner_size; - - let x_range: &mut dyn Iterator = match corner_type { - Corner::TopLeft | Corner::BottomLeft => &mut x_range, - Corner::TopRight | Corner::BottomRight => &mut x_range.rev(), - }; - - for (x, corner_row) in x_range.zip(corner) { - let y_range: &mut dyn Iterator = match corner_type { - Corner::TopLeft | Corner::TopRight => &mut y_range.clone(), - Corner::BottomLeft | Corner::BottomRight => &mut y_range.clone().rev(), - }; - - for (y, corner_cell) in y_range.zip(corner_row) { - if let Some(color) = corner_cell { - drawer.draw_color(x, y, self.map_color(color, x, y)); - } else { - break; - } - } - } - } - - #[inline] - fn draw_rectangle(&self, offset: Offset, width: usize, height: usize, drawer: &mut Drawer) { - for x in offset.x..width + offset.x { - for y in offset.y..height + offset.y { - drawer.draw_color(x, y, DrawColor::Replace(self.get_color(x, y))) - } - } - } - - fn get_color(&self, x: usize, y: usize) -> Bgra { - match &self.color { - Color::Fill(bgra) => *bgra, - Color::LinearGradient(gradient) => gradient.color_at( - x as f32 / self.frame_width as f32, - y as f32 / self.frame_height as f32, - ), - } - } - - fn map_color(&self, method: &DrawingMethod, x: usize, y: usize) -> DrawColor { - match method { - DrawingMethod::Replace(coverage) => { - DrawColor::Replace(self.get_color(x, y) * coverage.0) - } - DrawingMethod::Transparent(coverage) => DrawColor::Transparent(*coverage), - DrawingMethod::DependingOnBorderAlpha( - Coverage(border_cell_coverage), - Coverage(inner_cell_coverage), - ) => { - let border_color = self.get_color(x, y); - match border_color.alpha { - 0.0 => DrawColor::Transparent(Coverage(*inner_cell_coverage)), - _ => DrawColor::OverlayWithCoverage( - border_color * *border_cell_coverage, - Coverage(1.0 - inner_cell_coverage), - ), - } - } - } - } -} - -impl Draw for Border { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - let Some(corner_size) = self.corner_coverage.as_ref().map(|corner| corner.len()) else { - if !self.compiled { - warn!("Border: Not compiled, refused to draw itself"); - } - return; - }; - - self.draw_corner(*offset, Corner::TopLeft, drawer); - self.draw_corner( - *offset + Offset::new_x(self.frame_width - corner_size), - Corner::TopRight, - drawer, - ); - self.draw_corner( - *offset - + Offset::new( - self.frame_width - corner_size, - self.frame_height - corner_size, - ), - Corner::BottomRight, - drawer, - ); - self.draw_corner( - *offset + Offset::new_y(self.frame_height - corner_size), - Corner::BottomLeft, - drawer, - ); - - if self.size != 0 { - // Top - self.draw_rectangle( - *offset + Offset::new_x(corner_size), - self.frame_width - corner_size * 2, - self.size, - drawer, - ); - - // Bottom - self.draw_rectangle( - *offset + Offset::new(corner_size, self.frame_height - self.size), - self.frame_width - corner_size * 2, - self.size, - drawer, - ); - - // Left - self.draw_rectangle( - *offset + Offset::new_y(corner_size), - self.size, - self.frame_height - corner_size * 2, - drawer, - ); - - // Right - self.draw_rectangle( - *offset + Offset::new(self.frame_width - self.size, corner_size), - self.size, - self.frame_height - corner_size * 2, - drawer, - ); - } - } -} diff --git a/crates/render/src/color.rs b/crates/render/src/color.rs index 16e7898..0056228 100644 --- a/crates/render/src/color.rs +++ b/crates/render/src/color.rs @@ -1,17 +1,12 @@ -use std::{ - f32::consts::{FRAC_PI_2, FRAC_PI_4, PI}, - ops::{Mul, MulAssign}, -}; +use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI}; use config::color::{Color as CfgColor, LinearGradient as CfgLinearGradient, Rgba as CfgRgba}; use shared::value::TryFromValue; -use super::widget::Coverage; - #[derive(Clone)] pub enum Color { LinearGradient(LinearGradient), - Fill(Bgra), + Fill(Bgra), } impl Color { @@ -31,8 +26,8 @@ impl From for Color { } } -impl From for Color { - fn from(value: Bgra) -> Self { +impl From> for Color { + fn from(value: Bgra) -> Self { Color::Fill(value) } } @@ -50,7 +45,7 @@ impl From for Color { impl Default for Color { fn default() -> Self { - Color::Fill(Bgra::new()) + Color::Fill(Bgra::default()) } } @@ -58,16 +53,15 @@ impl TryFromValue for Color {} #[derive(Clone)] pub struct LinearGradient { - angle: f32, - grad_vector: [f32; 2], - doubled_norm: f32, - colors: Vec, - segment_per_color: f32, + pub angle: f64, + pub grad_vector: [f64; 2], + pub colors: Vec>, + pub segment_per_color: f64, } impl LinearGradient { /// 3π/4 - const FRAC_3_PI_4: f32 = FRAC_PI_2 + FRAC_PI_4; + const FRAC_3_PI_4: f64 = FRAC_PI_2 + FRAC_PI_4; pub fn new(mut angle: i16, mut colors: Vec) -> Self { if angle < 0 { @@ -83,7 +77,7 @@ impl LinearGradient { angle -= 180 } - let angle = (angle as f32).to_radians(); + let angle = (angle as f64).to_radians(); let grad_vector = match angle { x @ 0.0..=FRAC_PI_4 => [1.0, x.tan()], @@ -94,54 +88,15 @@ impl LinearGradient { _ => unreachable!(), }; - let norm = (grad_vector[0] * grad_vector[0] + grad_vector[1] * grad_vector[1]).sqrt(); - let doubled_norm = norm * norm; - let segment_per_color = 1.0 / (colors.len() - 1) as f32; + let segment_per_color = 1.0 / (colors.len() - 1) as f64; Self { angle, grad_vector, - doubled_norm, colors: colors.into_iter().map(Bgra::from).collect(), segment_per_color, } } - - /// Returns a concrete color from square color space of linear gradient considering the angle. - /// - /// Note that the 'x' and 'y' values which you pass into function should be in range - /// $0.0 <= x, y <= 1.0$ - /// The other values will be cause of incorrect color! - /// - /// To acheive it you can simply divide x or y position by frame width or hegiht respectively. - pub fn color_at(&self, mut x: f32, y: f32) -> Bgra { - if self.angle > FRAC_PI_2 { - x -= 1.0 - } - - let mut position_on_grad_line = self.grad_vector.dot_product(&[x, y]) / self.doubled_norm; - - while position_on_grad_line > 1.0 { - position_on_grad_line = 1.0 - position_on_grad_line; - } - - while position_on_grad_line < 0.0 { - position_on_grad_line += 1.0; - } - - let left_color_index = (position_on_grad_line / self.segment_per_color).floor() as usize; - - if left_color_index == self.colors.len() - 1 { - return self.colors[left_color_index]; - } - - let difference = (position_on_grad_line - - (left_color_index as f32) * self.segment_per_color) - / self.segment_per_color; - - self.colors[left_color_index + 1] - .linearly_interpolate(&self.colors[left_color_index], difference) - } } impl From for LinearGradient { @@ -150,101 +105,28 @@ impl From for LinearGradient { } } -trait DotProduct -where - T: std::ops::Mul + std::ops::Add, -{ - fn dot_product(&self, other: &Self) -> T; -} - -impl DotProduct for [T; 2] +/// The RGBA color representation by ARGB format in little endian. +/// +/// The struct was made in as adapter between config Rgba color and cairo's ARgb32 format because +/// the first one is very simple and the second one is complex and accepts only floats. +#[derive(Clone, Copy, Default)] +pub struct Bgra where - T: std::ops::Mul + std::ops::Add + Copy, + T: Copy + Default, { - fn dot_product(&self, other: &Self) -> T { - self[0] * other[0] + self[1] * other[1] - } -} - -#[derive(Clone, Copy, Default)] -pub struct Bgra { - pub blue: f32, - pub green: f32, - pub red: f32, - pub alpha: f32, + pub blue: T, + pub green: T, + pub red: T, + pub alpha: T, } -impl Bgra { - pub fn new() -> Self { - Self { - blue: 0.0, - green: 0.0, - red: 0.0, - alpha: 0.0, - } - } - - #[allow(unused)] - pub fn new_black() -> Self { - Self { - blue: 0.0, - green: 0.0, - red: 0.0, - alpha: 1.0, - } - } - - #[allow(unused)] - pub fn new_white() -> Self { - Self { - blue: 1.0, - green: 1.0, - red: 1.0, - alpha: 1.0, - } - } - +impl Bgra { pub fn is_transparent(&self) -> bool { self.alpha == 0.0 } - - pub fn into_rgba(self) -> Rgba { - self.into() - } - - pub fn into_slice(self) -> [u8; 4] { - [ - (self.blue * 255.0).round() as u8, - (self.green * 255.0).round() as u8, - (self.red * 255.0).round() as u8, - (self.alpha * 255.0).round() as u8, - ] - } -} - -impl From<&[u8; 4]> for Bgra { - fn from(value: &[u8; 4]) -> Self { - Self { - blue: value[0] as f32 / 255.0, - green: value[1] as f32 / 255.0, - red: value[2] as f32 / 255.0, - alpha: value[3] as f32 / 255.0, - } - } } -impl From<&[f32; 4]> for Bgra { - fn from(value: &[f32; 4]) -> Self { - Self { - blue: value[0], - green: value[1], - red: value[2], - alpha: value[3], - } - } -} - -impl From<&CfgRgba> for Bgra { +impl From<&CfgRgba> for Bgra { fn from( &CfgRgba { red, @@ -254,15 +136,15 @@ impl From<&CfgRgba> for Bgra { }: &CfgRgba, ) -> Self { Bgra { - blue: blue as f32 / 255.0, - green: green as f32 / 255.0, - red: red as f32 / 255.0, - alpha: alpha as f32 / 255.0, + blue: blue as f64 / 255.0, + green: green as f64 / 255.0, + red: red as f64 / 255.0, + alpha: alpha as f64 / 255.0, } } } -impl From for Bgra { +impl From for Bgra { fn from( CfgRgba { red, @@ -272,73 +154,26 @@ impl From for Bgra { }: CfgRgba, ) -> Self { Bgra { - blue: blue as f32 / 255.0, - green: green as f32 / 255.0, - red: red as f32 / 255.0, - alpha: alpha as f32 / 255.0, + blue: blue as f64 / 255.0, + green: green as f64 / 255.0, + red: red as f64 / 255.0, + alpha: alpha as f64 / 255.0, } } } -impl From for [u8; 4] { - fn from(value: Bgra) -> Self { - value.into_slice() - } -} - -impl From for Rgba { - fn from(value: Bgra) -> Self { - let Bgra { - blue, - green, - red, - alpha, - } = value; - Rgba { - red, - green, - blue, - alpha, +impl From> for Bgra { + fn from(value: Bgra) -> Self { + Self { + blue: (value.blue * u16::MAX as f64).round() as u16, + green: (value.green * u16::MAX as f64).round() as u16, + red: (value.red * u16::MAX as f64).round() as u16, + alpha: (value.alpha * u16::MAX as f64).round() as u16, } } } -impl Mul for Bgra { - type Output = Bgra; - - fn mul(mut self, rhs: f32) -> Self::Output { - self.blue *= rhs; - self.green *= rhs; - self.red *= rhs; - self.alpha *= rhs; - self - } -} - -impl Mul for Bgra { - type Output = Bgra; - - fn mul(self, Coverage(val): Coverage) -> Self::Output { - self * val - } -} - -impl MulAssign for Bgra { - fn mul_assign(&mut self, rhs: f32) { - self.blue *= rhs; - self.green *= rhs; - self.red *= rhs; - self.alpha *= rhs; - } -} - -impl MulAssign for Bgra { - fn mul_assign(&mut self, Coverage(val): Coverage) { - *self *= val - } -} - -impl TryFromValue for Bgra { +impl TryFromValue for Bgra { fn try_from_string(value: String) -> Result { >::try_from(value.clone()) .map(Into::into) @@ -348,169 +183,3 @@ impl TryFromValue for Bgra { }) } } - -pub struct Rgba { - pub red: f32, - pub green: f32, - pub blue: f32, - pub alpha: f32, -} - -impl Rgba { - pub fn new() -> Self { - Self { - red: 0.0, - green: 0.0, - blue: 0.0, - alpha: 0.0, - } - } - - #[allow(unused)] - pub fn new_white() -> Self { - Self { - red: 1.0, - green: 1.0, - blue: 1.0, - alpha: 1.0, - } - } - - pub fn into_bgra(self) -> Bgra { - self.into() - } - - pub fn into_slice(self) -> [u8; 4] { - [ - (self.red * 255.0).round() as u8, - (self.green * 255.0).round() as u8, - (self.blue * 255.0).round() as u8, - (self.alpha * 255.0).round() as u8, - ] - } -} - -impl Default for Rgba { - fn default() -> Self { - Self::new() - } -} - -impl From<&[u8; 3]> for Rgba { - fn from(value: &[u8; 3]) -> Self { - Self { - red: value[0] as f32 / 255.0, - green: value[1] as f32 / 255.0, - blue: value[2] as f32 / 255.0, - alpha: 1.0, - } - } -} - -impl From<&[u8; 4]> for Rgba { - fn from(value: &[u8; 4]) -> Self { - Self { - red: value[0] as f32 / 255.0, - green: value[1] as f32 / 255.0, - blue: value[2] as f32 / 255.0, - alpha: value[3] as f32 / 255.0, - } - } -} - -impl From<&[f32; 4]> for Rgba { - fn from(value: &[f32; 4]) -> Self { - Self { - red: value[0], - green: value[1], - blue: value[2], - alpha: value[3], - } - } -} - -impl From for Bgra { - fn from(value: Rgba) -> Self { - let Rgba { - red, - green, - blue, - alpha, - } = value; - Bgra { - blue, - green, - red, - alpha, - } - } -} - -impl From for [u8; 4] { - fn from(value: Rgba) -> Self { - value.into_slice() - } -} - -impl Mul for Rgba { - type Output = Rgba; - - fn mul(mut self, rhs: f32) -> Self::Output { - self.blue *= rhs; - self.green *= rhs; - self.red *= rhs; - self.alpha *= rhs; - self - } -} - -impl MulAssign for Rgba { - fn mul_assign(&mut self, rhs: f32) { - self.blue *= rhs; - self.green *= rhs; - self.red *= rhs; - self.alpha *= rhs; - } -} - -// SOURCE: https://stackoverflow.com/questions/726549/algorithm-for-additive-color-mixing-for-rgb-values -// r.A = 1 - (1 - fg.A) * (1 - bg.A); -// if (r.A < 1.0e-6) return r; // Fully transparent -- R,G,B not important -// r.R = fg.R * fg.A / r.A + bg.R * bg.A * (1 - fg.A) / r.A; -// r.G = fg.G * fg.A / r.A + bg.G * bg.A * (1 - fg.A) / r.A; -// r.B = fg.B * fg.A / r.A + bg.B * bg.A * (1 - fg.A) / r.A; -macro_rules! overlay_on { - ($($type:path),+) => { - $(impl $type { - #[allow(unused)] - pub fn linearly_interpolate(&self, dst: &Bgra, alpha: f32) -> Bgra { - Bgra { - blue: self.blue * alpha + dst.blue * (1.0 - alpha), - green: self.green * alpha + dst.green * (1.0 - alpha), - red: self.red * alpha + dst.red * (1.0 - alpha), - alpha: self.alpha * alpha + dst.alpha * (1.0 - alpha), - } - } - - #[allow(unused)] - pub fn overlay_on(self, background: &Self) -> Self { - let mut new_color = Self::new(); - new_color.alpha = 1.0 - (1.0 - self.alpha) * (1.0 - background.alpha); - if new_color.alpha < f32::EPSILON { - return new_color; - } - - new_color.red = self.red * self.alpha / new_color.alpha - + background.red * background.alpha * (1.0 - self.alpha) / new_color.alpha; - new_color.green = self.green * self.alpha / new_color.alpha - + background.green * background.alpha * (1.0 - self.alpha) / new_color.alpha; - new_color.blue = self.blue * self.alpha / new_color.alpha - + background.blue * background.alpha * (1.0 - self.alpha) / new_color.alpha; - new_color - - } - })+ - }; -} - -overlay_on!(Bgra, Rgba); diff --git a/crates/render/src/drawer.rs b/crates/render/src/drawer.rs index b4392a2..f8a8fbc 100644 --- a/crates/render/src/drawer.rs +++ b/crates/render/src/drawer.rs @@ -1,171 +1,149 @@ +use std::f64::consts::{FRAC_PI_2, PI}; + +use pangocairo::cairo::{Context, ImageSurface, LinearGradient}; + use crate::{ - color::{Bgra, Color}, + color::Color, types::{Offset, RectSize}, - widget::{Coverage, DrawColor}, }; pub struct Drawer { - size: RectSize, - data: Vec, + pub(crate) surface: ImageSurface, + pub(crate) context: Context, } impl Drawer { - pub fn new(color: Color, size: RectSize) -> Self { - let data = - match color { - Color::Fill(bgra) => vec![bgra; size.area()], - Color::LinearGradient(gradient) => { - let mut data = Vec::with_capacity(size.area()); - - for y in (0..size.height).rev() { - for x in 0..size.width { - data.push(gradient.color_at( - x as f32 / size.width as f32, - y as f32 / size.height as f32, - )) - } - } - - data - } - }; + pub fn create(size: RectSize) -> pangocairo::cairo::Result { + let surface = ImageSurface::create( + pangocairo::cairo::Format::ARgb32, + size.width as i32, + size.height as i32, + )?; - Self { data, size } - } + let context = Context::new(&surface)?; - pub fn draw_area(&mut self, offset: &Offset, subdrawer: Drawer) { - for x in 0..subdrawer.size.width { - for y in 0..subdrawer.size.height { - let color = subdrawer.get_color_at(x, y); - self.draw_color( - offset.x + x, - offset.y + y, - if color.is_transparent() { - DrawColor::Overlay(*color) - } else { - DrawColor::Replace(*color) - }, - ); - } - } + Ok(Self { surface, context }) } +} - pub fn draw_area_optimized(&mut self, offset: &Offset, mut subdrawer: Drawer) { - // INFO: this is specific code and it may be hard to read because the main goal is - // drawing optimization. Previously just single loop was used but after optimization - // we reachd x3 drawing speed. So I've [jarkz] decided to only make it more readable - // and leave it with descriptoin. - // - // WARNING: this code may work not correct in custom border drawing and with - // semi-transparent gradients! - // - // Let's pick this table: - // +-+-+-+-+-+-+-+-+-+ - // |T|S|U|U|U|U|U|S|T| - // +-+-+-+-+-+-+-+-+-+ - // |S|U|U|U|U|U|U|U|S| - // +-+-+-+-+-+-+-+-+-+ - // |S|U|U|U|U|U|U|U|S| - // +-+-+-+-+-+-+-+-+-+ - // |T|S|U|U|U|U|U|S|T| - // +-+-+-+-+-+-+-+-+-+ - // - // Where T - transparent, S - Semi-transparent, U - untransparent. - // The triangles at corner with S and T cells we should draw cell by cell, but for - // U cells pick by slices from left to right and PUT them into other table. Sure, we - // can't put owned value from slice to slice, so we use `swap_with_slice()` method - // from Vec. - if let Some(untransparent_pos) = subdrawer - .data - .iter() - .position(|color| !color.is_transparent()) - { - let start_y = untransparent_pos / subdrawer.size.width; - let start_x = untransparent_pos - subdrawer.size.width * start_y; - - let end_x = subdrawer.size.width - start_x; - let end_y = subdrawer.size.height - start_y; - - for y in start_y..end_y { - let is_corner = - start_x.saturating_sub(y) > 0 || start_x.saturating_sub(end_y - y) > 0; - if is_corner { - let start_range = subdrawer.abs_pos_at(0, y)..subdrawer.abs_pos_at(start_x, y); - let end_range = subdrawer.abs_pos_at(end_x, y) - ..subdrawer.abs_pos_at(subdrawer.size.width, y); - subdrawer.data[start_range] - .iter() - .zip(0..start_x) - .chain( - subdrawer.data[end_range] - .iter() - .zip(end_x..subdrawer.size.width), - ) - .for_each(|(color, x)| { - self.draw_color( - offset.x + x, - offset.y + y, - if color.is_transparent() { - DrawColor::Overlay(*color) - } else { - DrawColor::Replace(*color) - }, - ) - }); - - let line_in_parent = self.abs_pos_at(offset.x + start_x, offset.y + y) - ..self.abs_pos_at(offset.x + end_x, offset.y + y); - let line_in_child = - subdrawer.abs_pos_at(start_x, y)..subdrawer.abs_pos_at(end_x, y); - self.data[line_in_parent].swap_with_slice(&mut subdrawer.data[line_in_child]); - } else { - let line_in_parent = self.abs_pos_at(offset.x, offset.y + y) - ..self.abs_pos_at(offset.x + subdrawer.size.width, offset.y + y); - let line_in_child = - subdrawer.abs_pos_at(0, y)..subdrawer.abs_pos_at(subdrawer.size.width, y); - - self.data[line_in_parent].swap_with_slice(&mut subdrawer.data[line_in_child]); - } - } - } - } +pub(crate) trait MakeRounding { + fn make_rounding( + &self, + offset: Offset, + rect_size: RectSize, + outer_radius: f64, + inner_radius: f64, + ); +} - pub fn draw_color(&mut self, x: usize, y: usize, color: DrawColor) { - self.put_color_at(x, y, Self::convert_color(color, self.get_color_at(x, y))); +impl MakeRounding for Context { + fn make_rounding( + &self, + offset: Offset, + rect_size: RectSize, + mut outer_radius: f64, + mut inner_radius: f64, + ) { + debug_assert!(outer_radius >= inner_radius); + let minimal_threshold = (rect_size.height / 2.0).min(rect_size.width / 2.0); + inner_radius = inner_radius.min(minimal_threshold); + outer_radius = outer_radius.min(minimal_threshold); + + self.arc( + offset.x + outer_radius, + offset.y + outer_radius, + inner_radius, + PI, + -FRAC_PI_2, + ); + self.arc( + offset.x + rect_size.width - outer_radius, + offset.y + outer_radius, + inner_radius, + -FRAC_PI_2, + 0.0, + ); + self.arc( + offset.x + rect_size.width - outer_radius, + offset.y + rect_size.height - outer_radius, + inner_radius, + 0.0, + FRAC_PI_2, + ); + self.arc( + offset.x + outer_radius, + offset.y + rect_size.height - outer_radius, + inner_radius, + FRAC_PI_2, + PI, + ); } +} - fn convert_color(color: DrawColor, background: &Bgra) -> Bgra { +pub(crate) trait SetSourceColor { + fn set_source_color( + &self, + color: &Color, + frame_size: RectSize, + ) -> pangocairo::cairo::Result<()>; +} + +impl SetSourceColor for Drawer { + fn set_source_color( + &self, + color: &Color, + frame_size: RectSize, + ) -> pangocairo::cairo::Result<()> { match color { - DrawColor::Replace(color) => color, - DrawColor::Overlay(foreground) => foreground.overlay_on(background), - DrawColor::OverlayWithCoverage(foreground, Coverage(factor)) => { - foreground.linearly_interpolate(background, factor) - } - DrawColor::Transparent(Coverage(factor)) => *background * factor, - } - } + Color::LinearGradient(linear_gradient) => { + fn dot_product(vec1: [f64; 2], vec2: [f64; 2]) -> f64 { + vec1[0] * vec2[0] + vec1[1] * vec2[1] + } - fn get_color_at(&self, x: usize, y: usize) -> &Bgra { - &self.data[self.abs_pos_at(x, y)] - } + let (half_width, half_height) = (frame_size.width / 2.0, frame_size.height / 2.0); + + // INFO: need to find a factor to multiply the x/y offsets to that distance where + // prependicular line hits top left and top right corners. Without it part of area + // will be filled by single non-gradientary color that is not acceptable. + let x_offset = linear_gradient.grad_vector[0] * half_width; + let y_offset = linear_gradient.grad_vector[1] * half_height; + let norm = (x_offset * x_offset + y_offset * y_offset).sqrt(); + let factor = + dot_product([x_offset, y_offset], [half_width, half_height]) / (norm * norm); + + let gradient = LinearGradient::new( + half_width - x_offset * factor, + half_height + y_offset * factor, + half_width + x_offset * factor, + half_height - y_offset * factor, + ); - fn put_color_at(&mut self, x: usize, y: usize, color: Bgra) { - let pos = self.abs_pos_at(x, y); - self.data[pos] = color; - } + let mut offset = 0.0; + for bgra in &linear_gradient.colors { + gradient + .add_color_stop_rgba(offset, bgra.red, bgra.green, bgra.blue, bgra.alpha); + offset += linear_gradient.segment_per_color; + } - #[inline(always = true)] - fn abs_pos_at(&self, x: usize, y: usize) -> usize { - self.size.width * y + x + self.context.set_source(gradient)?; + } + Color::Fill(bgra) => self + .context + .set_source_rgba(bgra.red, bgra.green, bgra.blue, bgra.alpha), + } + + Ok(()) } } -impl From for Vec { - fn from(drawer: Drawer) -> Self { - drawer - .data - .into_iter() - .flat_map(|color| color.into_slice()) - .collect() +impl TryFrom for Vec { + type Error = pangocairo::cairo::BorrowError; + + fn try_from(value: Drawer) -> Result { + let Drawer { + surface, context, .. + } = value; + drop(context); + Ok(surface.take_data()?.to_vec()) } } diff --git a/crates/render/src/font.rs b/crates/render/src/font.rs deleted file mode 100644 index 5755440..0000000 --- a/crates/render/src/font.rs +++ /dev/null @@ -1,586 +0,0 @@ -use ab_glyph::{point, Font as AbGlyphFont, OutlinedGlyph, ScaleFont}; -use derive_more::Display; -use log::{debug, error, info, warn}; -use std::{ - collections::HashMap, - ffi::c_void, - ops::{Add, AddAssign, Sub, SubAssign}, - os::fd::AsRawFd, - process::Command, -}; - -use config::text::TextStyle; -use dbus::text::EntityKind; - -use crate::drawer::Drawer; - -use super::{ - color::Bgra, - image::Image, - widget::{Coverage, Draw, DrawColor}, -}; - -pub struct FontCollection { - font_name: String, - font_map: HashMap, - math_font: Option, - emoji_font: Option, -} - -impl FontCollection { - const ELLIPSIS: char = '…'; - const ACCEPTED_STYLES: [&'static str; 3] = ["Regular", "Bold", "Italic"]; - - pub fn update_by_font_name(&mut self, font_name: &str) -> anyhow::Result<()> { - if self.font_name == font_name { - return Ok(()); - } - - *self = Self::load_by_font_name(font_name)?; - Ok(()) - } - - pub fn load_by_font_name(font_name: &str) -> anyhow::Result { - debug!("Font: Trying load font by name {font_name}"); - - let output: String = Command::new("fc-list") - .args([font_name, "--format", "%{file}:%{style}\n"]) - .output()? - .stdout - .into_iter() - .map(|data| data as char) - .collect(); - - let mut font_map = HashMap::new(); - - for (filepath, styles) in output - .split('\n') - .filter(|line| !line.is_empty()) - .map(|line| { - line.split_once(':') - .expect("Must be the colon delimiter in --format when calling fc-list") - }) - .filter(|(_, style)| { - Self::ACCEPTED_STYLES - .contains(&style.split_once(' ').map(|(lhs, _)| lhs).unwrap_or(style)) - }) - { - let font = match Font::try_read(filepath, styles) { - Ok(font) => font, - Err(err) => { - error!("Failed to read or parse font at {filepath}. Error: {err}"); - continue; - } - }; - - font_map.insert(font.style.clone(), font); - } - - info!("Font: Loaded fonts by name {font_name}"); - - let math_font = match MathFont::try_create() { - Ok(emoji) => Some(emoji), - Err(err) => { - warn!("Font: Not found the 'NotoSansMath' font, math symbols will not be displayed. Error: {err}"); - None - } - }; - - let emoji_font = match EmojiFont::try_create() { - Ok(emoji) => Some(emoji), - Err(err) => { - warn!("Font: Not found the 'NotoColorEmoj' font, emoji will not be displayed. Error: {err}"); - None - } - }; - - Ok(Self { - font_name: font_name.to_owned(), - font_map, - math_font, - emoji_font, - }) - } - - pub fn load_glyph_by_style(&self, font_style: &FontStyle, ch: char, px_size: f32) -> Glyph { - let font = self.font_map.get(font_style).unwrap_or(self.default_font()); - - font.load_glyph(ch, px_size) - .or_else(|| { - self.math_font - .as_ref() - .map(|math_font| math_font.load_glyph(ch, px_size)) - .unwrap_or_default() - }) - .or_else(|| { - self.emoji_font - .as_ref() - .and_then(|emoji| emoji.image(ch, px_size.round() as u16)) - .map(Glyph::Image) - .unwrap_or_default() - }) - } - - pub fn max_height(&self, px_size: f32) -> usize { - self.font_map - .values() - .map(|font| font.get_height(px_size).round() as usize) - .max() - .unwrap_or_default() - } - - pub fn get_spacebar_width(&self, px_size: f32) -> f32 { - self.default_font().get_glyph_width(' ', px_size) - } - - pub fn get_ellipsis(&self, px_size: f32) -> Glyph { - self.load_glyph_by_style(&FontStyle::Regular, Self::ELLIPSIS, px_size) - } - - fn default_font(&self) -> &Font { - self.font_map - .get(&FontStyle::Regular) - .expect("Not found regular font in Font collection") - } -} - -#[derive(Debug)] -pub struct Font { - style: FontStyle, - _buffer: Buffer, - /// WARNING: DON'T CLONE THIS FIELD - data: ab_glyph::FontRef<'static>, -} - -impl Font { - fn try_read(filepath: &str, styles: &str) -> anyhow::Result { - let style = FontStyle::from( - styles - .split_once(",") - .map(|(important_style, _)| important_style) - .unwrap_or(styles), - ); - - let file = std::fs::File::open(filepath)?; - let buffer = Buffer::from(file); - let data = ab_glyph::FontRef::try_from_slice(buffer.as_slice())?; - - Ok(Self { - style, - _buffer: buffer, - data, - }) - } - - pub fn get_height(&self, px_size: f32) -> f32 { - self.data.as_scaled(px_size).height() - } - - pub fn get_glyph_width(&self, ch: char, px_size: f32) -> f32 { - let scaled_font = self.data.as_scaled(px_size); - - let glyph_id = scaled_font.glyph_id(ch); - scaled_font.h_advance(glyph_id) - } - - pub fn load_glyph(&self, ch: char, px_size: f32) -> Glyph { - if ch.is_whitespace() { - return Glyph::Empty; - } - - let scaled_font = self.data.as_scaled(px_size); - let glyph_id = self.data.glyph_id(ch); - - if glyph_id.0 == 0 { - return Glyph::Empty; - } - - let glyph = glyph_id.with_scale_and_position(px_size, point(0.0, scaled_font.ascent())); - - if let Some(outlined_glyph) = self.data.outline_glyph(glyph) { - Glyph::Outline { - advance_width: scaled_font.h_advance(outlined_glyph.glyph().id), - outlined_glyph, - color: Bgra::new(), - } - } else { - Glyph::Empty - } - } -} - -struct MathFont(Font); - -impl MathFont { - fn try_create() -> anyhow::Result { - let filepath = Command::new("fc-list") - .args(["NotoSansMath", "--format", "%{file}"]) - .output()? - .stdout - .into_iter() - .map(|byte| byte as char) - .collect::(); - - Font::try_read(&filepath, "Regular").map(MathFont) - } -} - -impl std::ops::Deref for MathFont { - type Target = Font; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -struct EmojiFont { - _buffer: Buffer, - font_face: ttf_parser::Face<'static>, -} - -impl EmojiFont { - fn try_create() -> anyhow::Result { - let filepath = Command::new("fc-list") - .args(["NotoColorEmoji", "--format", "%{file}"]) - .output()? - .stdout - .into_iter() - .map(|byte| byte as char) - .collect::(); - let file = std::fs::File::open(filepath)?; - let buffer = Buffer::from(file); - - let font_face = ttf_parser::Face::parse(buffer.as_slice(), 0)?; - Ok(Self { - _buffer: buffer, - font_face, - }) - } - - pub fn image(&self, ch: char, size: u16) -> Option { - let glyph_id = self.font_face.glyph_index(ch)?; - Image::from_raster_glyph_image( - self.font_face.glyph_raster_image(glyph_id, size)?, - size as u32, - ) - } -} - -/// Container of data like Vec. -/// -/// Because of Rust allocator won't allocate memory if it can be reusable for other application -/// parts, the memory will grow when the big data allocates. Sometimes it won't be deallocated that -/// is not good. -/// -/// The implementation of Buffer uses libc allocator and forces to deallocate when it drops. It -/// should guarantee that the memory won't be used for other application's parts. -#[derive(Debug)] -struct Buffer { - ptr: *const c_void, - size: usize, - _phantom_data: std::marker::PhantomData<[T]>, -} - -impl Buffer { - /// Allocates new memory by len in bytes (u8). - fn new(size: usize) -> Buffer { - unsafe { - Self { - ptr: libc::malloc(size), - size, - _phantom_data: std::marker::PhantomData, - } - } - } - - /// Converts the buffer into Rust's slice. Make attention to **'static** lifetime. It made for - /// self-referential struct. - /// - /// **Safety**: - /// - /// It will be safe if the application WON'T use after buffer's drop. Otherwise the further - /// actions will be UB. - fn as_slice(&self) -> &'static [T] { - unsafe { std::slice::from_raw_parts(self.ptr.cast(), self.size) } - } -} - -impl From for Buffer { - fn from(file: std::fs::File) -> Self { - let buffer = Buffer::new(file.metadata().map(|m| m.len() as usize).ok().unwrap_or(0)); - unsafe { - // INFO: don't mind about buffer read size because the size always fits to buffer - let _read_size = libc::read(file.as_raw_fd(), buffer.ptr.cast_mut(), buffer.size); - } - - buffer - } -} - -impl Drop for Buffer { - fn drop(&mut self) { - unsafe { libc::free(self.ptr.cast_mut()) } - } -} - -#[derive(Default, Clone)] -pub enum Glyph { - Image(Image), - Outline { - color: Bgra, - advance_width: f32, - outlined_glyph: OutlinedGlyph, - }, - #[default] - Empty, -} - -impl Glyph { - pub fn is_empty(&self) -> bool { - matches!(self, Glyph::Empty) - } - - pub fn or_else Self>(self, other: F) -> Self { - match self { - Glyph::Empty => other(), - _ => self, - } - } - - pub fn set_color(&mut self, new_color: Bgra) { - if let Glyph::Outline { color, .. } = self { - *color = new_color; - } - } - - pub fn advance_width(&self) -> usize { - match self { - Glyph::Image(img) => img.width().unwrap_or_default(), - Glyph::Outline { advance_width, .. } => advance_width.round() as usize, - Glyph::Empty => 0, - } - } -} - -impl Draw for Glyph { - fn draw_with_offset(&self, offset: &super::types::Offset, drawer: &mut Drawer) { - match self { - Glyph::Image(img) => { - img.draw_with_offset(offset, drawer); - } - Glyph::Outline { - color, - outlined_glyph, - .. - } => { - let bounds = outlined_glyph.px_bounds(); - outlined_glyph.draw(|x, y, coverage| { - drawer.draw_color( - (bounds.min.x.round() as i32 + x as i32).clamp(0, i32::MAX) as usize - + offset.x, - (bounds.min.y.round() as i32 + y as i32).clamp(0, i32::MAX) as usize - + offset.y, - DrawColor::OverlayWithCoverage(color.to_owned(), Coverage(coverage)), - ) - }) - } - Glyph::Empty => unreachable!(), - } - } -} - -#[derive(Debug, Display, Hash, PartialEq, Eq, Clone)] -pub enum FontStyle { - #[display("Regular")] - Regular, - #[display("Bold")] - Bold, - #[display("Italic")] - Italic, - #[display("BoldItalic")] - BoldItalic, -} - -impl From<&str> for FontStyle { - fn from(value: &str) -> Self { - match value { - "Regular" => Self::Regular, - "Bold" => Self::Bold, - "Italic" => Self::Italic, - "Bold Italic" => Self::BoldItalic, - other => panic!("Unsupported style: {other}"), - } - } -} - -impl From for FontStyle { - fn from(value: EntityKind) -> Self { - FontStyle::from(&value) - } -} - -impl From<&EntityKind> for FontStyle { - fn from(value: &EntityKind) -> Self { - match value { - EntityKind::Bold => FontStyle::Bold, - EntityKind::Italic => FontStyle::Italic, - other => todo!("Unsupported style {other:?} at current moment"), - } - } -} - -impl From for FontStyle { - fn from(value: TextStyle) -> Self { - FontStyle::from(&value) - } -} - -impl From<&TextStyle> for FontStyle { - fn from(value: &TextStyle) -> Self { - match value { - TextStyle::Regular => FontStyle::Regular, - TextStyle::Bold => FontStyle::Bold, - TextStyle::Italic => FontStyle::Italic, - TextStyle::BoldItalic => FontStyle::BoldItalic, - } - } -} - -fn union_font_styles(lhs: &FontStyle, rhs: &FontStyle) -> FontStyle { - match lhs { - FontStyle::Bold => match rhs { - FontStyle::Regular | FontStyle::Bold => FontStyle::Bold, - FontStyle::Italic | FontStyle::BoldItalic => FontStyle::BoldItalic, - }, - FontStyle::Italic => match rhs { - FontStyle::Regular | FontStyle::Italic => FontStyle::Italic, - FontStyle::Bold | FontStyle::BoldItalic => FontStyle::BoldItalic, - }, - FontStyle::BoldItalic => lhs.clone(), - FontStyle::Regular => rhs.clone(), - } -} - -impl Add for FontStyle { - type Output = FontStyle; - - fn add(self, rhs: Self) -> Self::Output { - union_font_styles(&self, &rhs) - } -} - -impl Add for &FontStyle { - type Output = FontStyle; - - fn add(self, rhs: Self) -> Self::Output { - union_font_styles(self, rhs) - } -} - -impl AddAssign for FontStyle { - fn add_assign(&mut self, rhs: Self) { - *self = union_font_styles(self, &rhs); - } -} - -fn intersect_font_styles(lhs: &FontStyle, rhs: &FontStyle) -> FontStyle { - match lhs { - FontStyle::Bold => match rhs { - FontStyle::Regular => FontStyle::Bold, - FontStyle::Bold => FontStyle::Regular, - other => panic!("Incorrect intersection from {lhs} by {other}"), - }, - FontStyle::Italic => match rhs { - FontStyle::Regular => FontStyle::Italic, - FontStyle::Italic => FontStyle::Regular, - other => panic!("Incorrect intersection from {lhs} by {other}"), - }, - FontStyle::BoldItalic => match rhs { - FontStyle::Bold => FontStyle::Italic, - FontStyle::Italic => FontStyle::Bold, - FontStyle::BoldItalic => FontStyle::Regular, - FontStyle::Regular => FontStyle::BoldItalic, - }, - FontStyle::Regular => match rhs { - FontStyle::Regular => FontStyle::Regular, - other => panic!("Incorrect intersection from {lhs} by {other}"), - }, - } -} - -impl Sub for FontStyle { - type Output = FontStyle; - - fn sub(self, rhs: Self) -> Self::Output { - intersect_font_styles(&self, &rhs) - } -} - -impl Sub for &FontStyle { - type Output = FontStyle; - - fn sub(self, rhs: Self) -> Self::Output { - intersect_font_styles(self, rhs) - } -} - -impl SubAssign for FontStyle { - fn sub_assign(&mut self, rhs: Self) { - *self = intersect_font_styles(self, &rhs); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn add_font_styles() { - assert_eq!(FontStyle::Bold + FontStyle::Italic, FontStyle::BoldItalic); - assert_eq!( - FontStyle::Bold + FontStyle::Italic + FontStyle::Regular, - FontStyle::BoldItalic - ); - assert_eq!( - FontStyle::BoldItalic + FontStyle::Bold, - FontStyle::BoldItalic - ); - assert_eq!(FontStyle::Bold + FontStyle::Bold, FontStyle::Bold); - assert_eq!(FontStyle::Regular + FontStyle::Bold, FontStyle::Bold); - assert_eq!(FontStyle::Regular + FontStyle::Italic, FontStyle::Italic); - assert_eq!( - FontStyle::Regular + FontStyle::Italic + FontStyle::Bold, - FontStyle::BoldItalic - ); - assert_eq!(FontStyle::Regular + FontStyle::Regular, FontStyle::Regular); - assert_eq!(FontStyle::Italic + FontStyle::Italic, FontStyle::Italic); - assert_eq!( - FontStyle::Regular + FontStyle::BoldItalic + FontStyle::BoldItalic, - FontStyle::BoldItalic - ); - } - - #[test] - fn sub_font_styles() { - assert_eq!(FontStyle::BoldItalic - FontStyle::Italic, FontStyle::Bold); - assert_eq!( - FontStyle::BoldItalic - FontStyle::Italic - FontStyle::Regular, - FontStyle::Bold - ); - assert_eq!(FontStyle::Regular - FontStyle::Regular, FontStyle::Regular); - assert_eq!(FontStyle::BoldItalic - FontStyle::Bold, FontStyle::Italic); - assert_eq!( - FontStyle::BoldItalic - FontStyle::Bold - FontStyle::Italic, - FontStyle::Regular - ); - assert_eq!( - FontStyle::BoldItalic - FontStyle::Regular - FontStyle::Italic, - FontStyle::Bold - ); - } - - #[test] - #[should_panic] - fn panicky_sub_font_style() { - let _ = FontStyle::Bold - FontStyle::Italic; - } -} diff --git a/crates/render/src/image.rs b/crates/render/src/image.rs index 3a8e168..bddb65c 100644 --- a/crates/render/src/image.rs +++ b/crates/render/src/image.rs @@ -1,23 +1,24 @@ +use image::codecs::png::PngEncoder; use log::{debug, error, warn}; -use ttf_parser::{RasterGlyphImage, RasterImageFormat}; +use pangocairo::cairo::ImageSurface; use config::display::{ImageProperty, ResizingMethod}; use dbus::image::ImageData; -use crate::{color::Color, drawer::Drawer, types::RectSize}; - -use super::{ - border::{Border, BorderBuilder}, - color::{Bgra, Rgba}, - types::Offset, - widget::{Coverage, Draw, DrawColor}, +use crate::{ + drawer::{Drawer, MakeRounding}, + types::RectSize, + PangoContext, }; +use super::{types::Offset, widget::Draw}; + #[derive(Clone)] pub enum Image { Exists { + // INFO: the image storage always store image in png format data: ImageData, - border: Option, + rounding_radius: f64, }, Unknown, } @@ -26,7 +27,7 @@ impl Image { pub fn from_image_data( image_data: ImageData, image_property: &ImageProperty, - max_size: &RectSize, + max_size: &RectSize, ) -> Self { let origin_width = image_data.width as u32; let origin_height = image_data.height as u32; @@ -55,7 +56,6 @@ impl Image { height as u32, image_property.resizing_method.to_filter_type(), ) - .to_vec() }); let Some(resized_image) = resized_image else { @@ -63,6 +63,12 @@ impl Image { return Image::Unknown; }; + let mut storage = vec![]; + let cursor = std::io::Cursor::new(&mut storage); + resized_image + .write_with_encoder(PngEncoder::new(cursor)) + .unwrap(); + debug!("Image: Created from 'image_data'"); Image::Exists { @@ -73,20 +79,16 @@ impl Image { has_alpha: true, bits_per_sample: image_data.bits_per_sample, channels: 4, - data: resized_image, + data: storage, }, - border: Some(Self::border_with_rounding( - width, - height, - image_property.rounding, - )), + rounding_radius: image_property.rounding as f64, } } pub fn from_path( image_path: &std::path::Path, image_property: &ImageProperty, - max_size: &RectSize, + max_size: &RectSize, ) -> Image { let data = match std::fs::read(image_path) { Ok(data) => data, @@ -122,13 +124,17 @@ impl Image { return Image::Unknown; }; - let resized_image = image::imageops::resize( + let mut storage = vec![]; + let cursor = std::io::Cursor::new(&mut storage); + + image::imageops::resize( &image, width as u32, height as u32, image_property.resizing_method.to_filter_type(), ) - .to_vec(); + .write_with_encoder(PngEncoder::new(cursor)) + .unwrap(); Image::Exists { data: ImageData { @@ -138,87 +144,16 @@ impl Image { has_alpha: true, bits_per_sample: 8, channels: 4, - data: resized_image, + data: storage, }, - border: Some(Self::border_with_rounding( - width, - height, - image_property.rounding, - )), + rounding_radius: image_property.rounding as f64, } } - pub fn from_raster_glyph_image( - RasterGlyphImage { - width, - height, - format, - data, - .. - }: RasterGlyphImage, - size: u32, - ) -> Option { - let rgba_image = match format { - RasterImageFormat::PNG => { - image::load_from_memory_with_format(data, image::ImageFormat::Png) - .ok()? - .to_rgba8() - } - RasterImageFormat::BitmapMono - | RasterImageFormat::BitmapMonoPacked - | RasterImageFormat::BitmapGray2 - | RasterImageFormat::BitmapGray2Packed - | RasterImageFormat::BitmapGray4 - | RasterImageFormat::BitmapGray4Packed - | RasterImageFormat::BitmapGray8 => { - image::load_from_memory_with_format(data, image::ImageFormat::Bmp) - .ok()? - .to_rgba8() - } - RasterImageFormat::BitmapPremulBgra32 => image::RgbaImage::from_vec( - width as u32, - height as u32, - data.chunks_exact(4) - .flat_map(|chunk| { - Bgra::from( - TryInto::<&[u8; 4]>::try_into(chunk) - .expect("The image should have 4 channels"), - ) - .into_rgba() - .into_slice() - }) - .collect::>(), - )?, - }; - - let (mut width, mut height) = (width as i32, height as i32); - Self::limit_size(&mut width, &mut height, size as u16); - - let rgba_image = image::imageops::resize( - &rgba_image, - width as u32, - width as u32, - image::imageops::FilterType::Gaussian, - ); - - Some(Image::Exists { - data: ImageData { - width, - height, - rowstride: width * 4, - has_alpha: true, - bits_per_sample: 8, - channels: 4, - data: rgba_image.to_vec(), - }, - border: None, - }) - } - pub fn from_svg( image_path: &std::path::Path, image_property: &ImageProperty, - max_size: &RectSize, + max_size: &RectSize, ) -> Self { if !image_path.is_file() { return Image::Unknown; @@ -284,7 +219,7 @@ impl Image { Image::Exists { data: ImageData { - data: pixmap.data().to_vec(), + data: pixmap.encode_png().unwrap(), width, height, rowstride: width * 4, @@ -292,11 +227,7 @@ impl Image { bits_per_sample: 8, channels: 4, }, - border: Some(Self::border_with_rounding( - width, - height, - image_property.rounding, - )), + rounding_radius: image_property.rounding as f64, } } @@ -354,7 +285,7 @@ impl Image { mut width: i32, mut height: i32, image_property: &ImageProperty, - max_size: &RectSize, + max_size: &RectSize, ) -> Option<(i32, i32)> { Self::limit_size(&mut width, &mut height, image_property.max_size); let (horizontal_spacing, vertical_spacing) = { @@ -392,65 +323,48 @@ impl Image { std::mem::swap(width, height); } } - - fn border_with_rounding(width: i32, height: i32, rounding_radius: u16) -> Border { - BorderBuilder::default() - .color(Color::default()) - .size(0_usize) - .radius(rounding_radius) - .frame_width(width as usize) - .frame_height(height as usize) - .compile() - .expect("Create Border for image rounding") - } - - fn converter(has_alpha: bool) -> fn(&[u8]) -> Rgba { - //SAFETY: it always safe way while the framebuffer have ARGB format and gives the correct - //postiton. - if has_alpha { - |chunk: &[u8]| unsafe { - Rgba::from(TryInto::<&[u8; 4]>::try_into(chunk).unwrap_unchecked()) - } - } else { - |chunk: &[u8]| unsafe { - Rgba::from(TryInto::<&[u8; 3]>::try_into(chunk).unwrap_unchecked()) - } - } - } } impl Draw for Image { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - let Image::Exists { data, border } = self else { - return; + fn draw_with_offset( + &self, + offset: &Offset, + _pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { + let Image::Exists { + data, + rounding_radius, + } = self + else { + return Ok(()); + }; + debug_assert!(data.has_alpha); + + let mut storage_cursor = std::io::Cursor::new(&data.data); + let source_surface = match ImageSurface::create_from_png(&mut storage_cursor) { + Ok(source_surface) => source_surface, + Err(err) => match err { + cairo::IoError::Cairo(error) => Err(error)?, + cairo::IoError::Io(error) => { + error!("Happened something wrong with IO opertaion during image rendering. Error: {error}"); + return Ok(()); + } + }, }; - let mut chunks = data - .data - .chunks_exact(data.channels as usize) - .map(Self::converter(data.has_alpha)); - - for y in 0..data.height as usize { - for x in 0..data.width as usize { - let border_coverage = - match border.as_ref().and_then(|border| border.get_color_at(x, y)) { - Some(DrawColor::Transparent(Coverage(factor))) => factor, - None => 1.0, - _ => unreachable!(), - }; - - let color = unsafe { chunks.next().unwrap_unchecked() }.into_bgra(); - drawer.draw_color( - x + offset.x, - y + offset.y, - if border_coverage == 1.0 { - DrawColor::Overlay(color) - } else { - DrawColor::OverlayWithCoverage(color, Coverage(border_coverage)) - }, - ); - } - } + drawer.context.make_rounding( + (*offset).into(), + RectSize::new(data.width as f64, data.height as f64), + *rounding_radius, + *rounding_radius, + ); + drawer + .context + .set_source_surface(source_surface, offset.x as f64, offset.y as f64)?; + + drawer.context.fill()?; + Ok(()) } } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 63b6109..dd7ef9b 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1,8 +1,7 @@ -pub mod border; pub mod color; pub mod drawer; -pub mod font; pub mod image; -pub mod text; pub mod types; pub mod widget; + +pub use widget::text::PangoContext; diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs deleted file mode 100644 index cde0da1..0000000 --- a/crates/render/src/text.rs +++ /dev/null @@ -1,577 +0,0 @@ -use std::collections::VecDeque; - -use derive_builder::Builder; -use itertools::Itertools; - -use config::{ - spacing::Spacing, - text::{EllipsizeAt, TextJustification}, -}; -use dbus::text::Text; - -use crate::drawer::Drawer; - -use super::{ - color::Bgra, - font::{FontCollection, FontStyle, Glyph}, - types::{Offset, RectSize}, - widget::Draw, -}; - -#[derive(Default)] -pub struct TextRect { - paragraphs: VecDeque>, - current_paragraph: VecDeque, - - lines: Vec, - wrap: bool, - - rect_size: RectSize, - - spacebar_width: usize, - line_height: usize, - line_spacing: usize, - - ellipsize_at: EllipsizeAt, - ellipsis: Glyph, - justification: TextJustification, - - foreground: Bgra, - - margin: Spacing, -} - -impl TextRect { - pub fn from_str + Clone>( - string: &str, - px_size: f32, - base_style: Style, - font_collection: &FontCollection, - ) -> Self { - let font_style = base_style.into(); - let paragraphs: VecDeque> = string - .chars() - .chunk_by(|char| *char != '\n') - .into_iter() - .filter_map(|(matches, chunk)| { - matches.then(|| { - chunk - .into_iter() - .map(|ch| font_collection.load_glyph_by_style(&font_style, ch, px_size)) - .collect() - }) - }) - .map(Self::convert_to_words) - .collect(); - - Self { - paragraphs, - wrap: true, - spacebar_width: Self::get_spacebar_width(font_collection, px_size), - ellipsis: font_collection.get_ellipsis(px_size), - line_height: font_collection.max_height(px_size), - ..Default::default() - } - } - - pub fn from_text>( - text: &Text, - px_size: f32, - base_style: Style, - font_collection: &FontCollection, - ) -> Self { - let Text { body, entities } = text; - let base_style: FontStyle = base_style.into(); - - let mut entities = VecDeque::from_iter(entities.iter()); - let mut current_entities = VecDeque::new(); - let mut current_style = FontStyle::Regular; - - let mut paragraphs = VecDeque::new(); - let mut current_paragraph = vec![]; - - for (position, ch) in body.chars().enumerate() { - while let Some(entity) = entities.front() { - if entity.offset == position { - current_style += FontStyle::from(&entity.kind); - current_entities.push_back(entities.pop_front().unwrap()); - } else { - break; - } - } - - if ch == '\n' { - paragraphs.push_back(Self::convert_to_words(current_paragraph)); - current_paragraph = vec![]; - } else { - current_paragraph.push(font_collection.load_glyph_by_style( - &(&base_style + ¤t_style), - ch, - px_size, - )) - } - - while let Some(entity) = current_entities.front() { - if entity.offset + entity.length <= position { - let entity = current_entities.pop_front().unwrap(); - current_style -= FontStyle::from(&entity.kind); - } else { - break; - } - } - } - - if !current_paragraph.is_empty() { - paragraphs.push_back(Self::convert_to_words(current_paragraph)); - } - - Self { - paragraphs, - wrap: true, - spacebar_width: Self::get_spacebar_width(font_collection, px_size), - ellipsis: font_collection.get_ellipsis(px_size), - line_height: font_collection.max_height(px_size), - ..Default::default() - } - } - - fn convert_to_words(glyph_collection: Vec) -> VecDeque { - glyph_collection - .into_iter() - .chunk_by(|glyph| !glyph.is_empty()) - .into_iter() - .filter_map(|(matches, word)| matches.then_some(WordRect::from_glyphs(word.collect()))) - .collect() - } - - fn get_spacebar_width(font_collection: &FontCollection, px_size: f32) -> usize { - font_collection.get_spacebar_width(px_size).round() as usize - } - - pub fn set_wrap(&mut self, wrap: bool) { - self.wrap = wrap; - } - - pub fn set_line_spacing(&mut self, line_spacing: usize) { - self.line_spacing = line_spacing; - } - - pub fn set_margin(&mut self, margin: &Spacing) { - self.margin = margin.clone(); - } - - pub fn set_foreground(&mut self, color: Bgra) { - self.foreground = color; - } - - pub fn set_ellipsize_at(&mut self, ellipsize_at: &EllipsizeAt) { - self.ellipsize_at = ellipsize_at.clone(); - } - - pub fn set_justification(&mut self, justification: &TextJustification) { - self.justification = justification.to_owned(); - } - - pub fn compile(&mut self, mut rect_size: RectSize) { - self.rect_size.width = rect_size.width; - rect_size.shrink_by(&self.margin); - - let mut paragraph_num = 0; - self.current_paragraph = self.paragraphs.pop_front().unwrap_or_default(); - - let mut lines = vec![]; - - for y in (0..rect_size.height) - .step_by(self.line_height + self.line_spacing) - .take_while(|y| rect_size.height - *y >= self.line_height) - .take(if self.wrap { usize::MAX } else { 1 }) - { - let mut line = LineRectBuilder::create_empty() - .paragraph_num(paragraph_num) - .y_offset(y) - .available_space(rect_size.width as isize) - .spacebar_width(self.spacebar_width) - .justification(self.justification.to_owned()) - .words(vec![]) - .build() - .expect("Can't create a Line rect from existing components. Please contact with developers with this information."); - - while let Some(word) = self.current_paragraph.pop_front() { - line.push_word(word); - - if line.is_overflow() { - // INFO: here is a logic when the line have single word which overflows current - // line and use it for ellipsization. Otherwise (when it is not single word) - // remove last word to clean up the overflow and return it to `self.words`. - if line.len() > 1 { - self.current_paragraph.push_front(line.pop_word().unwrap()) - } - - break; - } - } - - let is_overflow = line.is_overflow(); - lines.push(line); - - if is_overflow { - break; - } - - if self.current_paragraph.is_empty() { - self.current_paragraph = self.paragraphs.pop_front().unwrap_or_default(); - paragraph_num += 1; - - if self.current_paragraph.is_empty() { - break; - } - } - } - - self.lines = lines; - self.ellipsize(paragraph_num); - self.apply_color(); - } - - fn ellipsize(&mut self, paragraph_num: u8) { - let mut state = self - .lines - .last_mut() - .map(|last_line| { - last_line.ellipsize( - paragraph_num, - self.current_paragraph.pop_front(), - self.ellipsis.clone(), - &self.ellipsize_at, - ) - }) - .unwrap_or_default(); - - while let EllipsizationState::Continue { - paragraph_num, - last_word, - } = state - { - self.lines.pop(); - - state = self - .lines - .last_mut() - .map(|last_line| { - last_line.ellipsize( - paragraph_num, - last_word, - self.ellipsis.clone(), - &self.ellipsize_at, - ) - }) - .unwrap_or_default(); - } - } - - fn apply_color(&mut self) { - self.lines - .iter_mut() - .for_each(|line| line.set_color(self.foreground)); - } - - pub(crate) fn is_empty(&self) -> bool { - self.lines.is_empty() || self.lines.iter().all(|line| line.is_empty()) - } - - #[allow(unused)] - pub fn width(&self) -> usize { - self.rect_size.width - } - - pub fn height(&self) -> usize { - let total_lines = self.lines.len(); - total_lines * self.line_height - + total_lines.saturating_sub(1) * self.line_spacing - + self.margin.top() as usize - + self.margin.bottom() as usize - } -} - -impl Draw for TextRect { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - let offset = Offset::from(&self.margin) + *offset; - - self.lines - .iter() - .for_each(|line| line.draw_with_offset(&offset, drawer)) - } -} - -#[derive(Builder, Default)] -#[builder(pattern = "owned")] -struct LineRect { - paragraph_num: u8, - y_offset: usize, - - available_space: isize, - spacebar_width: usize, - - justification: TextJustification, - - words: Vec, -} - -impl LineRect { - fn available_space(&self) -> usize { - assert!( - self.available_space >= 0, - "The available space of line rect is negative. Maybe you forgot ellipsize it." - ); - - self.available_space as usize - } - - fn blank_space(&self) -> usize { - assert!( - self.available_space >= 0, - "The available space of line rect is negative. Maybe you forgot ellipsize it." - ); - self.available_space as usize + self.spacebar_width * self.words.len().saturating_sub(1) - } - - fn ellipsize( - &mut self, - paragraph_num: u8, - mut last_word: Option, - ellipsis: Glyph, - ellipsize_at: &EllipsizeAt, - ) -> EllipsizationState { - match ellipsize_at { - EllipsizeAt::Middle => { - self.ellipsize_middle(last_word, ellipsis); - EllipsizationState::Complete - } - EllipsizeAt::End => { - if last_word.is_some() || self.available_space < 0 { - if paragraph_num != self.paragraph_num { - last_word.replace(WordRect::new_empty()); - } - self.ellipsize_end(ellipsis) - } else { - EllipsizationState::Complete - } - } - } - } - - fn ellipsize_middle(&mut self, last_word: Option, ellipsis: Glyph) { - let ellipsis_width = ellipsis.advance_width(); - - if let Some(mut last_word) = last_word { - while !last_word.is_blank() - && (last_word.width() + self.spacebar_width + ellipsis_width) as isize - > self.available_space - { - last_word.pop_glyph(); - } - - if !last_word.is_blank() { - last_word.push_glyph(ellipsis); - self.push_word(last_word); - return; - } else if ellipsis_width as isize <= self.available_space { - self.push_ellipsis_to_last_word(ellipsis); - return; - } - } else if self.available_space >= 0 { - return; - } - - let mut last_word = self.pop_word().expect( - "Here must have a WordRect struct in the LineRect. \ - But it doesn't have, so the LineRect is not correct. Please to contact the devs of \ - the Noti application with this information, please.", - ); - - while !last_word.is_blank() - && (last_word.width() + self.spacebar_width + ellipsis_width) as isize - > self.available_space - { - last_word.pop_glyph(); - } - - if last_word.is_blank() { - return; - } - - // INFO: here MUST be enough space for cutting word and ellipsization - // so here doesn't check if the last word is blank - last_word.push_glyph(ellipsis); - self.push_word(last_word); - } - - fn ellipsize_end(&mut self, ellipsis: Glyph) -> EllipsizationState { - if self.words.is_empty() { - return EllipsizationState::Continue { - paragraph_num: self.paragraph_num, - last_word: Some(WordRect::new_empty()), - }; - } - - if ellipsis.advance_width() as isize <= self.available_space { - self.push_ellipsis_to_last_word(ellipsis); - EllipsizationState::Complete - } else { - self.pop_word(); - self.ellipsize_end(ellipsis) - } - } - - fn pop_word(&mut self) -> Option { - let last_word = self.words.pop()?; - - self.available_space += (last_word.width() - + if !self.words.is_empty() { - self.spacebar_width - } else { - 0 - }) as isize; - - Some(last_word) - } - - fn push_word(&mut self, word: WordRect) { - self.available_space -= (word.width() - + if !self.words.is_empty() { - self.spacebar_width - } else { - 0 - }) as isize; - self.words.push(word); - } - - fn push_ellipsis_to_last_word(&mut self, ellipsis: Glyph) { - if let Some(last_word) = self.words.last_mut() { - self.available_space -= ellipsis.advance_width() as isize; - last_word.push_glyph(ellipsis); - } - } - - fn len(&self) -> usize { - self.words.len() - } - - fn is_empty(&self) -> bool { - self.words.is_empty() - } - - fn is_overflow(&self) -> bool { - self.available_space < 0 - } - - fn set_color(&mut self, color: Bgra) { - self.words.iter_mut().for_each(|word| word.set_color(color)); - } -} - -impl Draw for LineRect { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - let (x, x_incrementor) = match &self.justification { - TextJustification::Center => (self.available_space() / 2, self.spacebar_width), - TextJustification::Left => (0, self.spacebar_width), - TextJustification::Right => (self.available_space(), self.spacebar_width), - TextJustification::SpaceBetween => ( - 0, - if self.words.len() == 1 { - 0 - } else { - self.blank_space() / self.words.len().saturating_sub(1) - }, - ), - }; - - let mut offset = *offset + Offset::new(x, self.y_offset); - - self.words.iter().for_each(|word| { - word.draw_with_offset(&offset, drawer); - offset.x += x_incrementor + word.width(); - }); - } -} - -#[derive(Default)] -enum EllipsizationState { - Continue { - paragraph_num: u8, - last_word: Option, - }, - #[default] - Complete, -} - -pub struct WordRect { - advance_width: usize, - glyphs: Vec, -} - -impl WordRect { - fn new_empty() -> Self { - WordRect { - advance_width: 0, - glyphs: vec![], - } - } - - fn from_glyphs(outlined_glyphs: Vec) -> Self { - let advance_width = outlined_glyphs - .iter() - .map(|glyph| glyph.advance_width()) - .sum(); - - Self { - advance_width, - glyphs: outlined_glyphs, - } - } - - #[inline(always = true)] - fn set_color(&mut self, color: Bgra) { - self.glyphs - .iter_mut() - .for_each(|glyph| glyph.set_color(color)); - } - - #[inline(always = true)] - fn width(&self) -> usize { - self.advance_width - } - - #[inline(always = true)] - fn push_glyph(&mut self, new_glyph: Glyph) { - self.advance_width += new_glyph.advance_width(); - self.glyphs.push(new_glyph); - } - - #[inline(always = true)] - fn pop_glyph(&mut self) -> Option { - let last_glyph = self.glyphs.pop(); - if let Some(last_glyph) = last_glyph.as_ref() { - self.advance_width = self - .advance_width - .saturating_sub(last_glyph.advance_width()); - } - - last_glyph - } - - #[inline(always = true)] - fn is_blank(&self) -> bool { - self.glyphs.is_empty() - } -} - -impl Draw for WordRect { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - let mut offset = offset.to_owned(); - self.glyphs.iter().for_each(|glyph| { - glyph.draw_with_offset(&offset, drawer); - offset.x += glyph.advance_width(); - }) - } -} diff --git a/crates/render/src/types.rs b/crates/render/src/types.rs index 66b0467..f47a146 100644 --- a/crates/render/src/types.rs +++ b/crates/render/src/types.rs @@ -1,28 +1,54 @@ -use std::ops::{Add, AddAssign}; +use std::ops::{Add, AddAssign, Mul}; use config::spacing::Spacing; -#[derive(Debug, Default, Clone)] -pub struct RectSize { - pub width: usize, - pub height: usize, +#[derive(Debug, Default, Clone, Copy)] +pub struct RectSize +where + T: Default + Copy, +{ + pub width: T, + pub height: T, } -impl RectSize { - pub fn new(width: usize, height: usize) -> Self { +impl RectSize +where + T: Default + Copy, +{ + pub fn new(width: T, height: T) -> Self { Self { width, height } } - #[allow(dead_code)] - pub fn new_width(width: usize) -> Self { - Self { width, height: 0 } + pub fn new_square(side: T) -> Self { + Self { + width: side, + height: side, + } + } + + pub fn new_width(width: T) -> Self { + Self { + width, + ..Default::default() + } + } + + pub fn new_height(height: T) -> Self { + Self { + height, + ..Default::default() + } } - #[allow(dead_code)] - pub fn new_height(height: usize) -> Self { - Self { width: 0, height } + pub fn area(&self) -> T + where + T: Mul, + { + self.width * self.height } +} +impl RectSize { pub fn shrink_by(&mut self, spacing: &Spacing) { self.width = self .width @@ -31,16 +57,33 @@ impl RectSize { .height .saturating_sub(spacing.top() as usize + spacing.bottom() as usize); } +} - pub fn area(&self) -> usize { - self.width * self.height +impl From> for RectSize { + fn from(value: RectSize) -> Self { + Self { + width: value.width as f64, + height: value.height as f64, + } + } +} + +impl From> for RectSize { + fn from(value: RectSize) -> Self { + Self { + width: value.width.round() as usize, + height: value.height.round() as usize, + } } } -impl Add for RectSize { - type Output = RectSize; +impl Add> for RectSize +where + T: Add + Default + Copy, +{ + type Output = RectSize; - fn add(self, rhs: RectSize) -> Self::Output { + fn add(self, rhs: RectSize) -> Self::Output { Self { width: self.width + rhs.width, height: self.height + rhs.height, @@ -48,41 +91,59 @@ impl Add for RectSize { } } -impl AddAssign for RectSize { - fn add_assign(&mut self, rhs: RectSize) { +impl AddAssign> for RectSize +where + T: AddAssign + Default + Copy, +{ + fn add_assign(&mut self, rhs: RectSize) { self.width += rhs.width; self.height += rhs.height; } } #[derive(Debug, Default, Clone, Copy)] -pub struct Offset { - pub x: usize, - pub y: usize, +pub struct Offset +where + T: Add + Default + Copy, +{ + pub x: T, + pub y: T, } -impl Offset { - pub fn new(x: usize, y: usize) -> Self { - Offset { x, y } +impl Offset +where + T: Add + Default + Copy, +{ + pub fn new(x: T, y: T) -> Self { + Self { x, y } } - pub fn new_x(x: usize) -> Self { - Offset { x, y: 0 } + pub fn new_x(x: T) -> Self { + Self { + x, + ..Default::default() + } } - pub fn new_y(y: usize) -> Self { - Offset { x: 0, y } + pub fn new_y(y: T) -> Self { + Self { + y, + ..Default::default() + } } pub fn no_offset() -> Self { - Self { x: 0, y: 0 } + Self::default() } } -impl Add for Offset { - type Output = Offset; +impl Add> for Offset +where + T: Add + Default + Copy, +{ + type Output = Offset; - fn add(self, rhs: Offset) -> Self::Output { + fn add(self, rhs: Offset) -> Self::Output { Offset { x: self.x + rhs.x, y: self.y + rhs.y, @@ -90,14 +151,26 @@ impl Add for Offset { } } -impl AddAssign for Offset { - fn add_assign(&mut self, rhs: Offset) { +impl AddAssign> for Offset +where + T: Add + AddAssign + Default + Copy, +{ + fn add_assign(&mut self, rhs: Offset) { self.x += rhs.x; self.y += rhs.y; } } -impl From for Offset { +impl From> for Offset { + fn from(value: Offset) -> Self { + Self { + x: value.x as f64, + y: value.y as f64, + } + } +} + +impl From for Offset { fn from(value: Spacing) -> Self { Offset { x: value.left() as usize, @@ -106,7 +179,7 @@ impl From for Offset { } } -impl From<&Spacing> for Offset { +impl From<&Spacing> for Offset { fn from(value: &Spacing) -> Self { Offset { x: value.left() as usize, diff --git a/crates/render/src/widget.rs b/crates/render/src/widget.rs index d216743..a08735d 100644 --- a/crates/render/src/widget.rs +++ b/crates/render/src/widget.rs @@ -1,18 +1,15 @@ use config::{display::DisplayConfig, theme::Theme}; use dbus::notification::Notification; use log::warn; +use text::PangoContext; use crate::drawer::Drawer; -use super::{ - color::Bgra, - font::FontCollection, - types::{Offset, RectSize}, -}; +use super::types::{Offset, RectSize}; mod flex_container; mod image; -mod text; +pub(crate) mod text; pub use flex_container::{ Alignment, Direction, FlexContainer, FlexContainerBuilder, GBuilderAlignment, @@ -21,22 +18,20 @@ pub use flex_container::{ pub use image::{GBuilderWImage, WImage}; pub use text::{GBuilderWText, WText, WTextKind}; -#[derive(Clone, Copy)] -pub struct Coverage(pub f32); - -#[derive(Clone)] -pub enum DrawColor { - Replace(Bgra), - Overlay(Bgra), - OverlayWithCoverage(Bgra, Coverage), - Transparent(Coverage), -} - pub trait Draw { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer); - - fn draw(&self, drawer: &mut Drawer) { - self.draw_with_offset(&Default::default(), drawer); + fn draw_with_offset( + &self, + offset: &Offset, + pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()>; + + fn draw( + &self, + pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { + self.draw_with_offset(&Default::default(), pango_context, drawer) } } @@ -62,7 +57,7 @@ impl Widget { } } - pub fn compile(&mut self, rect_size: RectSize, configuration: &WidgetConfiguration) { + pub fn compile(&mut self, rect_size: RectSize, configuration: &WidgetConfiguration) { let state = match self { Widget::Image(image) => image.compile(rect_size, configuration), Widget::Text(text) => text.compile(rect_size, configuration), @@ -106,12 +101,19 @@ impl Widget { } impl Draw for Widget { - fn draw_with_offset(&self, offset: &Offset, output: &mut Drawer) { + fn draw_with_offset( + &self, + offset: &Offset, + pango_context: &PangoContext, + output: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { match self { - Widget::Image(image) => image.draw_with_offset(offset, output), - Widget::Text(text) => text.draw_with_offset(offset, output), - Widget::FlexContainer(container) => container.draw_with_offset(offset, output), - Widget::Unknown => (), + Widget::Image(image) => image.draw_with_offset(offset, pango_context, output), + Widget::Text(text) => text.draw_with_offset(offset, pango_context, output), + Widget::FlexContainer(container) => { + container.draw_with_offset(offset, pango_context, output) + } + Widget::Unknown => Ok(()), } } } @@ -123,7 +125,7 @@ pub enum CompileState { pub struct WidgetConfiguration<'a> { pub notification: &'a Notification, - pub font_collection: &'a FontCollection, + pub pango_context: &'a PangoContext, pub theme: &'a Theme, pub display_config: &'a DisplayConfig, pub override_properties: bool, diff --git a/crates/render/src/widget/flex_container.rs b/crates/render/src/widget/flex_container.rs index 00c3b91..b05448e 100644 --- a/crates/render/src/widget/flex_container.rs +++ b/crates/render/src/widget/flex_container.rs @@ -3,10 +3,10 @@ use log::warn; use shared::{error::ConversionError, value::TryFromValue}; use crate::{ - border::{Border, BorderBuilder}, color::{Bgra, Color}, - drawer::Drawer, + drawer::{Drawer, MakeRounding, SetSourceColor}, types::{Offset, RectSize}, + PangoContext, }; use super::{CompileState, Draw, Widget, WidgetConfiguration}; @@ -17,12 +17,16 @@ use super::{CompileState, Draw, Widget, WidgetConfiguration}; pub struct FlexContainer { #[builder(private, setter(skip))] #[gbuilder(hidden, default(None))] - rect_size: Option, + rect_size: Option>, #[builder(private, default)] - #[gbuilder(hidden, default(Bgra::new().into()))] + #[gbuilder(hidden, default(Bgra::default().into()))] background_color: Color, + #[builder(private, default)] + #[gbuilder(hidden, default(Bgra::default().into()))] + border_color: Color, + #[builder(default = "false")] #[gbuilder(default(false))] transparent_background: bool, @@ -41,10 +45,6 @@ pub struct FlexContainer { #[gbuilder(default)] border: config::display::Border, - #[builder(private, setter(skip))] - #[gbuilder(hidden, default)] - compiled_border: Option, - direction: Direction, alignment: Alignment, @@ -54,7 +54,7 @@ pub struct FlexContainer { impl FlexContainer { pub fn compile( &mut self, - mut rect_size: RectSize, + mut rect_size: RectSize, configuration: &WidgetConfiguration, ) -> CompileState { self.max_width = self.max_width.min(rect_size.width); @@ -63,23 +63,14 @@ impl FlexContainer { width: self.max_width, height: self.max_height, }; - self.rect_size = Some(rect_size.clone()); + self.rect_size = Some(rect_size); let colors = &configuration .theme .by_urgency(&configuration.notification.hints.urgency); self.background_color = colors.background.clone().into(); - self.compiled_border = Some( - BorderBuilder::default() - .color(colors.border.clone().into()) - .frame_width(rect_size.width) - .frame_height(rect_size.height) - .size(self.border.size) - .radius(self.border.radius) - .compile() - .expect("Border should be have possibility to compile"), - ); + self.border_color = colors.border.clone().into(); rect_size.shrink_by(&(self.spacing.clone() + Spacing::all_directional(self.border.size))); let mut container_axes = FlexContainerPlane::new(rect_size, &self.direction); @@ -98,10 +89,9 @@ impl FlexContainer { "The flex container is empty! Did you add the widgets? \ Or check them, maybe they doesn't fit available space." ); - CompileState::Failure - } else { - CompileState::Success } + + CompileState::Success } pub(super) fn max_width(&self) -> usize { @@ -174,31 +164,93 @@ impl FlexContainer { Direction::Vertical => &self.alignment.horizontal, } } + + fn rounded_fill( + &self, + offset: Offset, + rect_size: RectSize, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { + let outer_radius = (self.border.radius as f64) + .min(rect_size.width / 2.0) + .min(rect_size.height / 2.0); + let inner_radius = (outer_radius - self.border.size as f64).max(0.0); + + drawer.context.new_sub_path(); + drawer + .context + .make_rounding(offset, rect_size, outer_radius, inner_radius); + drawer.context.close_path(); + + drawer.set_source_color(&self.background_color, rect_size)?; + drawer.context.fill() + } + + fn outline_border( + &self, + offset: Offset, + rect_size: RectSize, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { + if self.border.radius == 0 || self.border.size == 0 { + return Ok(()); + } + + let outer_radius = (self.border.radius as f64) + .min(rect_size.width / 2.0) + .min(rect_size.height / 2.0); + let inner_radius = outer_radius - self.border.size as f64; + + drawer.context.new_sub_path(); + drawer + .context + .make_rounding(offset, rect_size, outer_radius, outer_radius); + drawer.context.close_path(); + drawer.set_source_color(&self.border_color, rect_size)?; + + if inner_radius <= 0.0 { + let border_size = self.border.size as f64; + drawer.context.rectangle( + border_size, + border_size, + rect_size.width - border_size * 2.0, + rect_size.height - border_size * 2.0, + ); + } else { + drawer.context.new_sub_path(); + drawer + .context + .make_rounding(offset, rect_size, outer_radius, inner_radius); + drawer.context.close_path(); + } + + drawer.context.set_fill_rule(cairo::FillRule::EvenOdd); + drawer.context.fill() + } } impl Draw for FlexContainer { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { + fn draw_with_offset( + &self, + offset: &Offset, + pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { let Some(mut rect_size) = self.rect_size.as_ref().cloned() else { panic!( "The rectangle size must be computed by `compile()` method of parent container!" ); }; + let original_rect_size = rect_size; // NOTE: if the background color is transparent or forces to be transparent, no need to use // another layer as new Drawer instance. Instead of this use the current Drawer instance. // It will avoid to use costly methods `draw_area` and `draw_with_offset`. let transparent_bg = self.transparent_background || self.background_color.is_transparent(); - let mut subdrawer = if transparent_bg { - Drawer::new(Bgra::new().into(), RectSize::new(0, 0)) - } else { - Drawer::new(self.background_color.clone(), rect_size.clone()) - }; - let (picked_drawer, base_offset) = if transparent_bg { - (&mut *drawer, *offset) - } else { - (&mut subdrawer, Offset::no_offset()) - }; + if !transparent_bg { + self.rounded_fill((*offset).into(), rect_size.into(), drawer)?; + } rect_size.shrink_by(&(self.spacing.clone() + Spacing::all_directional(self.border.size))); let mut plane = FlexContainerPlane::new(rect_size, &self.direction); @@ -225,29 +277,20 @@ impl Draw for FlexContainer { } }; - self.children.iter().for_each(|child| { + let auxiliary_axis_alignment = self.auxiliary_axis_alignment().clone(); + for child in &self.children { plane.auxiliary_axis_offset = initial_plane.auxiliary_axis_offset - + self.auxiliary_axis_alignment().compute_initial_pos( + + auxiliary_axis_alignment.compute_initial_pos( plane.auxiliary_len, child.len_by_direction(&self.direction.orthogonalize()), ); - child.draw_with_offset(&(plane.as_offset() + base_offset), picked_drawer); + child.draw_with_offset(&(plane.as_offset() + *offset), pango_context, drawer)?; plane.main_axis_offset += child.len_by_direction(&self.direction) + incrementor; - plane.auxiliary_axis_offset = initial_plane.auxiliary_axis_offset; - }); - - if let Some(compiled_border) = self.compiled_border.as_ref() { - compiled_border.draw_with_offset(&base_offset, picked_drawer); } - if !transparent_bg { - match &self.background_color { - Color::Fill(_) => drawer.draw_area_optimized(offset, subdrawer), - Color::LinearGradient(_) => drawer.draw_area(offset, subdrawer), - } - } + self.outline_border((*offset).into(), original_rect_size.into(), drawer) } } @@ -349,7 +392,7 @@ impl<'a> FlexContainerPlane<'a> { RectSize { mut width, mut height, - }: RectSize, + }: RectSize, direction: &'a Direction, ) -> Self { if let Direction::Vertical = direction { @@ -365,7 +408,7 @@ impl<'a> FlexContainerPlane<'a> { } } - fn new_only_offset(Offset { mut x, mut y }: Offset, direction: &'a Direction) -> Self { + fn new_only_offset(Offset { mut x, mut y }: Offset, direction: &'a Direction) -> Self { if let Direction::Vertical = direction { (x, y) = (y, x); } @@ -379,7 +422,7 @@ impl<'a> FlexContainerPlane<'a> { } } - fn relocate(&mut self, Offset { mut x, mut y }: &Offset) { + fn relocate(&mut self, Offset { mut x, mut y }: &Offset) { if let Direction::Vertical = self.direction { (x, y) = (y, x); } @@ -388,7 +431,7 @@ impl<'a> FlexContainerPlane<'a> { self.auxiliary_axis_offset = y; } - fn as_rect_size(&self) -> RectSize { + fn as_rect_size(&self) -> RectSize { let (mut width, mut height) = (self.main_len, self.auxiliary_len); if let Direction::Vertical = self.direction { @@ -398,7 +441,7 @@ impl<'a> FlexContainerPlane<'a> { RectSize::new(width, height) } - fn as_offset(&self) -> Offset { + fn as_offset(&self) -> Offset { let (mut x, mut y) = (self.main_axis_offset, self.auxiliary_axis_offset); if let Direction::Vertical = self.direction { diff --git a/crates/render/src/widget/image.rs b/crates/render/src/widget/image.rs index 82e198c..61702d7 100644 --- a/crates/render/src/widget/image.rs +++ b/crates/render/src/widget/image.rs @@ -5,6 +5,7 @@ use crate::{ drawer::Drawer, image::Image, types::{Offset, RectSize}, + PangoContext, }; use super::{CompileState, Draw, WidgetConfiguration}; @@ -36,7 +37,7 @@ impl WImage { pub fn compile( &mut self, - rect_size: RectSize, + rect_size: RectSize, WidgetConfiguration { notification, display_config, @@ -121,13 +122,18 @@ impl Default for WImage { } impl Draw for WImage { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { + fn draw_with_offset( + &self, + offset: &Offset, + pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { if !self.content.is_exists() { - return; + return Ok(()); } - // INFO: The ImageProperty initializes with Image so we can calmly unwrap let offset = Offset::from(&self.property.margin) + *offset; - self.content.draw_with_offset(&offset, drawer); + self.content + .draw_with_offset(&offset, pango_context, drawer) } } diff --git a/crates/render/src/widget/text.rs b/crates/render/src/widget/text.rs index 119d5f8..9be420f 100644 --- a/crates/render/src/widget/text.rs +++ b/crates/render/src/widget/text.rs @@ -1,12 +1,18 @@ use config::text::{GBuilderTextProperty, TextProperty}; -use dbus::text::Text; +use dbus::text::{EntityKind, Text}; use log::warn; +use pangocairo::{ + pango::{ + ffi::PANGO_SCALE, AttrColor, AttrInt, AttrList, AttrSize, Context, FontDescription, + Layout as PangoLayout, + }, + FontMap, +}; use shared::{error::ConversionError, value::TryFromValue}; use crate::{ color::Bgra, drawer::Drawer, - text::TextRect, types::{Offset, RectSize}, }; @@ -16,11 +22,15 @@ use super::{CompileState, Draw, WidgetConfiguration}; #[gbuilder(name(GBuilderWText))] pub struct WText { kind: WTextKind, + #[gbuilder(hidden, default(None))] - content: Option, + layout: Option, #[gbuilder(use_gbuilder(GBuilderTextProperty), default)] property: TextProperty, + + #[gbuilder(hidden, default)] + inner_size: RectSize, } impl Clone for WText { @@ -28,8 +38,9 @@ impl Clone for WText { // INFO: we shouldn't clone compiled info about text Self { kind: self.kind.clone(), - content: None, + layout: None, property: self.property.clone(), + inner_size: RectSize::default(), } } } @@ -38,8 +49,9 @@ impl Clone for GBuilderWText { fn clone(&self) -> Self { Self { kind: self.kind.as_ref().cloned(), - content: None, + layout: None, property: self.property.clone(), + inner_size: Some(RectSize::default()), } } } @@ -69,18 +81,19 @@ impl WText { pub fn new(kind: WTextKind) -> Self { Self { kind, - content: None, + layout: None, property: Default::default(), + inner_size: RectSize::default(), } } pub fn compile( &mut self, - rect_size: RectSize, + mut rect_size: RectSize, WidgetConfiguration { display_config, notification, - font_collection, + pango_context, override_properties, theme, }: &WidgetConfiguration, @@ -91,8 +104,10 @@ impl WText { } }; + let layout = PangoLayout::new(&pango_context.0); + let colors = theme.by_urgency(¬ification.hints.urgency); - let foreground = Bgra::from(&colors.foreground); + let foreground: Bgra = colors.foreground.clone().into(); let notification_content: NotificationContent = match self.kind { WTextKind::Title => { @@ -109,21 +124,22 @@ impl WText { } }; - let px_size = self.property.font_size as f32; - let mut content = match notification_content { - NotificationContent::Text(text) => { - TextRect::from_text(text, px_size, &self.property.style, font_collection) - } - NotificationContent::String(str) => { - TextRect::from_str(str, px_size, &self.property.style, font_collection) - } - }; + rect_size.shrink_by(&self.property.margin); + layout.set_width(rect_size.width as i32 * PANGO_SCALE); + layout.set_height(rect_size.height as i32 * PANGO_SCALE); + + let (text, attributes) = notification_content.as_str_with_attributes(); + if text.trim().is_empty() { + warn!("The text with kind {} is blank", self.kind); + return CompileState::Failure; + } - Self::apply_properties(&mut content, &self.property); - Self::apply_color(&mut content, foreground); + layout.set_text(text); + Self::apply_colors(&attributes, foreground.into()); + self.apply_properties(&layout, attributes); - content.compile(rect_size.clone()); - if content.is_empty() { + let (computed_width, computed_height) = layout.pixel_size(); + if computed_width > rect_size.width as i32 || computed_height > rect_size.height as i32 { warn!( "The text with kind {} doesn't fit to available space. \ Available space: width={}, height={}.", @@ -131,43 +147,106 @@ impl WText { ); CompileState::Failure } else { - self.content = Some(content); + self.inner_size = rect_size; + self.layout = Some(layout); CompileState::Success } } - fn apply_properties(element: &mut TextRect, properties: &TextProperty) { - element.set_wrap(properties.wrap); - element.set_margin(&properties.margin); - element.set_line_spacing(properties.line_spacing as usize); - element.set_ellipsize_at(&properties.ellipsize_at); - element.set_justification(&properties.justification); + fn apply_colors(attributes: &AttrList, foreground: Bgra) { + attributes.insert(AttrColor::new_foreground( + foreground.red, + foreground.green, + foreground.blue, + )); + attributes.insert(AttrInt::new_foreground_alpha(foreground.alpha)); } - fn apply_color(element: &mut TextRect, foreground: Bgra) { - element.set_foreground(foreground); + fn apply_properties(&self, layout: &PangoLayout, attributes: AttrList) { + fn from_px_to_pt(px: f32) -> i32 { + ((px * 72.0) / 96.0).round() as i32 + } + + attributes.insert(AttrSize::new_size_absolute( + from_px_to_pt(self.property.font_size as f32) * PANGO_SCALE, + )); + + match &self.property.style { + config::text::TextStyle::Regular => (), + config::text::TextStyle::Bold => { + attributes.insert(AttrInt::new_weight(pangocairo::pango::Weight::Bold)) + } + config::text::TextStyle::Italic => { + attributes.insert(AttrInt::new_style(pangocairo::pango::Style::Italic)) + } + config::text::TextStyle::BoldItalic => { + attributes.insert(AttrInt::new_weight(pangocairo::pango::Weight::Bold)); + attributes.insert(AttrInt::new_style(pangocairo::pango::Style::Italic)); + } + } + + if !self.property.wrap { + layout.set_height(0); + } + + let wrap_mode = match &self.property.wrap_mode { + config::text::WrapMode::Word => pangocairo::pango::WrapMode::Word, + config::text::WrapMode::WordChar => pangocairo::pango::WrapMode::WordChar, + config::text::WrapMode::Char => pangocairo::pango::WrapMode::Char, + }; + layout.set_wrap(wrap_mode); + + let ellipsize = match self.property.ellipsize { + config::text::Ellipsize::Start => pangocairo::pango::EllipsizeMode::Start, + config::text::Ellipsize::Middle => pangocairo::pango::EllipsizeMode::Middle, + config::text::Ellipsize::End => pangocairo::pango::EllipsizeMode::End, + config::text::Ellipsize::None => pangocairo::pango::EllipsizeMode::None, + }; + layout.set_ellipsize(ellipsize); + + let alignment = match self.property.alignment { + config::text::TextAlignment::Center => pangocairo::pango::Alignment::Center, + config::text::TextAlignment::Left => pangocairo::pango::Alignment::Left, + config::text::TextAlignment::Right => pangocairo::pango::Alignment::Right, + }; + layout.set_alignment(alignment); + layout.set_justify(self.property.justify); + layout.set_spacing(self.property.line_spacing as i32 * PANGO_SCALE); + + layout.set_attributes(Some(&attributes)); } pub fn width(&self) -> usize { - self.content - .as_ref() - .map(|content| content.width()) - .unwrap_or(0) + // INFO: the width should get all available width but height should get only renderable + // rows. + self.inner_size.width + self.property.margin.horizontal() as usize } pub fn height(&self) -> usize { - self.content + self.layout .as_ref() - .map(|content| content.height()) - .unwrap_or(0) + .map(|layout| layout.pixel_size().1 + self.property.margin.vertical() as i32) + .unwrap_or(0) as usize } } impl Draw for WText { - fn draw_with_offset(&self, offset: &Offset, drawer: &mut Drawer) { - if let Some(content) = self.content.as_ref() { - content.draw_with_offset(offset, drawer) + fn draw_with_offset( + &self, + offset: &Offset, + pango_context: &PangoContext, + drawer: &mut Drawer, + ) -> pangocairo::cairo::Result<()> { + if let Some(layout) = self.layout.as_ref() { + drawer.context.move_to( + (offset.x + self.property.margin.left() as usize) as f64, + (offset.y + self.property.margin.top() as usize) as f64, + ); + pangocairo::functions::update_context(&drawer.context, &pango_context.0); + layout.context_changed(); + pangocairo::functions::show_layout(&drawer.context, layout); } + Ok(()) } } @@ -176,6 +255,49 @@ enum NotificationContent<'a> { Text(&'a Text), } +impl NotificationContent<'_> { + fn as_str_with_attributes(&self) -> (&str, AttrList) { + fn get_attribute_style(kind: &EntityKind) -> Option { + Some(match kind { + dbus::text::EntityKind::Bold => { + AttrInt::new_weight(pangocairo::pango::Weight::Bold) + } + dbus::text::EntityKind::Italic => { + AttrInt::new_style(pangocairo::pango::Style::Italic) + } + dbus::text::EntityKind::Underline => { + AttrInt::new_underline(pangocairo::pango::Underline::SingleLine) + } + _ => None?, // Images and Links will be ignored because they're useless + // for pango + }) + } + + let string; + let attributes = AttrList::new(); + match self { + NotificationContent::Text(text) => { + string = &*text.body; + + for entity in &text.entities { + let Some(mut attribute) = get_attribute_style(&entity.kind) else { + continue; + }; + + attribute.set_start_index(entity.offset_in_byte as u32); + attribute.set_end_index((entity.offset_in_byte + entity.length_in_byte) as u32); + attributes.insert(attribute); + } + } + NotificationContent::String(str) => { + string = *str; + } + } + + (string, attributes) + } +} + impl<'a> From<&'a str> for NotificationContent<'a> { fn from(value: &'a str) -> Self { NotificationContent::String(value) @@ -187,3 +309,21 @@ impl<'a> From<&'a Text> for NotificationContent<'a> { NotificationContent::Text(value) } } + +pub struct PangoContext(Context); + +impl PangoContext { + pub fn from_font_family(font_family: &str) -> Self { + let context = Context::new(); + let font_map = FontMap::new(); + context.set_font_map(Some(&font_map)); + context.set_font_description(Some(&FontDescription::from_string(font_family))); + + Self(context) + } + + pub fn update_font_family(&mut self, font_family: &str) { + self.0 + .set_font_description(Some(&FontDescription::from_string(font_family))); + } +} diff --git a/crates/shared/src/value.rs b/crates/shared/src/value.rs index 4d18a6a..386f894 100644 --- a/crates/shared/src/value.rs +++ b/crates/shared/src/value.rs @@ -4,7 +4,7 @@ use crate::error::ConversionError; pub enum Value { UInt(usize), String(String), - Any(Box), + Any(Box), } pub trait TryFromValue: Sized + 'static { @@ -41,7 +41,7 @@ pub trait TryDowncast: Sized { fn try_downcast_ref(&self) -> Result<&T, ConversionError>; } -impl TryDowncast for Box { +impl TryDowncast for Box { fn try_downcast(self) -> Result { Ok(*self .downcast()