From 598353ea9f40dd3b660f4c22bd8527ea71f406a4 Mon Sep 17 00:00:00 2001 From: Pavel Belyavsky Date: Wed, 24 Jul 2024 09:35:17 +0300 Subject: [PATCH] feat: add padding and margins for text elements as `Spacing` --- README.md | 6 ++ src/config.rs | 27 +++-- src/config/spacing.rs | 240 ++++++++++++++++++++++++++++++++++++++++++ src/render/layer.rs | 50 +++++---- src/render/text.rs | 25 +++-- 5 files changed, 309 insertions(+), 39 deletions(-) create mode 100644 src/config/spacing.rs diff --git a/README.md b/README.md index 92082f0..e76bf4e 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ size = 4 radius = 10 color = "#000" +[display.title] +margin = { top = 5, left = 25 } + +[display.body] +margin = { top = 12, left = 25 } + [[app]] name = "Telegram Desktop" [app.display] diff --git a/src/config.rs b/src/config.rs index 05a2f1b..c260be2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,9 +2,11 @@ use once_cell::sync::Lazy; use serde::Deserialize; use std::{collections::HashMap, fs, path::Path, str::Chars}; -use self::sorting::Sorting; - pub mod sorting; +pub mod spacing; + +use sorting::Sorting; +use spacing::Spacing; pub static CONFIG: Lazy = Lazy::new(Config::init); @@ -250,7 +252,7 @@ impl From for Anchor { pub struct DisplayConfig { image_size: Option, - padding: Option, + padding: Option, border: Option, @@ -267,8 +269,8 @@ impl DisplayConfig { self.image_size.unwrap() } - pub fn padding(&self) -> u8 { - self.padding.unwrap() + pub fn padding(&self) -> &Spacing { + self.padding.as_ref().unwrap() } pub fn border(&self) -> &Border { @@ -301,7 +303,7 @@ impl DisplayConfig { } if self.padding.is_none() { - self.padding = Some(0); + self.padding = Some(Default::default()); } if self.border.is_none() { @@ -507,6 +509,7 @@ impl Border { #[derive(Debug, Deserialize, Default, Clone)] pub struct TextProperty { + margin: Option, alignment: Option, line_spacing: Option, } @@ -522,6 +525,10 @@ pub enum TextAlignment { } impl TextProperty { + pub fn margin(&self) -> &Spacing { + self.margin.as_ref().unwrap() + } + pub fn alignment(&self) -> &TextAlignment { self.alignment.as_ref().unwrap() } @@ -531,6 +538,10 @@ impl TextProperty { } pub fn fill_empty_by_default(&mut self, entity: &str) { + if self.margin.is_none() { + self.margin = Some(Default::default()); + } + if self.alignment.is_none() { if entity == "title" { self.alignment = Some(TextAlignment::Center); @@ -575,7 +586,7 @@ impl AppConfig { if let Some(display) = self.display.as_mut() { display.image_size = display.image_size.or(other.image_size); - display.padding = display.padding.or(other.padding); + display.padding = display.padding.clone().or(other.padding.clone()); if let Some(border) = display.border.as_mut() { let other_border = other.border(); // The other type shall have border @@ -600,6 +611,7 @@ impl AppConfig { if let Some(title) = display.title.as_mut() { let other_title = other.title(); + title.margin = title.margin.clone().or(other_title.margin.clone()); title.alignment = title.alignment.clone().or(other_title.alignment.clone()); title.line_spacing = title.line_spacing.or(other_title.line_spacing); } else { @@ -609,6 +621,7 @@ impl AppConfig { if let Some(body) = display.body.as_mut() { let other_body = other.body(); + body.margin = body.margin.clone().or(other_body.margin.clone()); body.alignment = body.alignment.clone().or(other_body.alignment.clone()); body.line_spacing = body.line_spacing.or(other_body.line_spacing); } else { diff --git a/src/config/spacing.rs b/src/config/spacing.rs new file mode 100644 index 0000000..4541d52 --- /dev/null +++ b/src/config/spacing.rs @@ -0,0 +1,240 @@ +use std::{collections::HashMap, marker::PhantomData}; + +use serde::{de::Visitor, Deserialize}; + +#[derive(Debug, Default, Clone)] +pub struct Spacing { + top: u8, + right: u8, + bottom: u8, + left: u8, +} + +impl Spacing { + const POSSIBLE_KEYS: [&'static str; 6] = + ["top", "right", "bottom", "left", "vertical", "horizontal"]; + + fn all_directional(val: u8) -> Self { + Self { + top: val, + bottom: val, + right: val, + left: val, + } + } + + fn cross(vertical: u8, horizontal: u8) -> Self { + Self { + top: vertical, + bottom: vertical, + right: horizontal, + left: horizontal, + } + } + + pub fn top(&self) -> u8 { + self.top + } + + pub fn right(&self) -> u8 { + self.right + } + + pub fn bottom(&self) -> u8 { + self.bottom + } + + pub fn left(&self) -> u8 { + self.left + } + + pub fn shrink(&self, width: &mut usize, height: &mut usize) { + *width -= self.left as usize + self.right as usize; + *height -= self.top as usize + self.bottom as usize; + } +} + +impl From for Spacing { + fn from(value: i64) -> Self { + Spacing::all_directional(value.clamp(0, u8::MAX as i64) as u8) + } +} + +impl From> for Spacing { + fn from(value: Vec) -> Self { + match value.len() { + 1 => Spacing::all_directional(value[0]), + 2 => Spacing::cross(value[0], value[1]), + 3 => Spacing { + top: value[0], + right: value[1], + left: value[1], + bottom: value[2], + }, + 4 => Spacing { + top: value[0], + right: value[1], + bottom: value[2], + left: value[3], + }, + _ => unreachable!(), + } + } +} + +impl From> for Spacing { + fn from(map: HashMap) -> Self { + let vertical = map.get("vertical"); + let horizontal = map.get("horizontal"); + let top = map.get("top"); + let bottom = map.get("bottom"); + let right = map.get("right"); + let left = map.get("left"); + + Self { + top: *top.or(vertical).unwrap_or(&0), + bottom: *bottom.or(vertical).unwrap_or(&0), + right: *right.or(horizontal).unwrap_or(&0), + left: *left.or(horizontal).unwrap_or(&0), + } + } +} + +impl<'de> Deserialize<'de> for Spacing { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(PaddingVisitor(PhantomData)) + } +} + +struct PaddingVisitor(PhantomData T>); + +impl<'de, T> Visitor<'de> for PaddingVisitor +where + T: Deserialize<'de> + From> + From> + From, +{ + type Value = T; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + formatter, + r#"either u8, [u8, u8], [u8, u8, u8], [u8, u8, u8, u8] or Table. + +Example: + +# All-directional margin +margin = 3 + +# The application can also apply the CSS-like values: +# Applies vertical and horizontal paddings respectively +padding = [0, 5] + +# Applies top, horizontal and bottom paddings respectively +margin = [3, 2, 5] + +# Applies top, right, bottom, left paddings respectively +padding = [1, 2, 3, 4] + +# When you want to declare in explicit way: + +# Sets only top padding +padding = {{ top = 3 }} + +# Sets only top and right padding +padding = {{ top = 5, right = 6 }} + +# Insead of +# padding = {{ top = 5, right = 6, bottom = 5 }} +# Write +padding = {{ vertical = 5, right = 6 }} + +# If gots collision of values the error will throws because of ambuguity +# padding = {{ top = 5, vertical = 6 }} + +# You can apply the same way for margin +margin = {{ top = 5, horizontal = 10 }}"# + ) + } + + fn visit_i64(self, v: i64) -> Result + where + E: serde::de::Error, + { + Ok(v.into()) + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut fields = vec![]; + while let Some(value) = seq.next_element::()? { + fields.push(value); + } + + match fields.len() { + 1..=4 => Ok(fields.into()), + other => Err(serde::de::Error::invalid_length(other, &self)), + } + } + + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut custom_padding = HashMap::new(); + + while let Some((key, value)) = map.next_entry::()? { + if !Spacing::POSSIBLE_KEYS.contains(&key.as_str()) { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(key.as_str()), + &self, + )); + } + + match key.as_str() { + "top" | "bottom" if custom_padding.contains_key("vertical") => { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(key.as_str()), + &self, + )) + } + "vertical" + if custom_padding.contains_key("top") + || custom_padding.contains_key("bottom") => + { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(key.as_str()), + &self, + )) + } + "right" | "left" if custom_padding.contains_key("horizontal") => { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(key.as_str()), + &self, + )) + } + "horizontal" + if custom_padding.contains_key("right") + || custom_padding.contains_key("left") => + { + return Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(key.as_str()), + &self, + )) + } + _ => (), + } + + custom_padding.insert(key, value); + } + + if !custom_padding.is_empty() { + Ok(custom_padding.into()) + } else { + Err(serde::de::Error::invalid_length(0, &self)) + } + } +} diff --git a/src/render/layer.rs b/src/render/layer.rs index 6c597c6..31ace6c 100644 --- a/src/render/layer.rs +++ b/src/render/layer.rs @@ -499,9 +499,9 @@ impl NotificationRect { } fn draw(&mut self) { - let (width, height) = ( - CONFIG.general().width() as i32, - CONFIG.general().height() as i32, + let (mut width, mut height) = ( + CONFIG.general().width() as usize, + CONFIG.general().height() as usize, ); let display = CONFIG.display_by_app(&self.data.app_name); @@ -538,7 +538,8 @@ impl NotificationRect { .unwrap_unchecked() = bgra.to_slice() }); - let padding = display.padding() as usize + border_cfg.size() as usize; + let padding = display.padding(); + padding.shrink(&mut width, &mut height); let image = Image::from_image_data(self.data.hints.image_data.as_ref(), display.image_size()) @@ -554,8 +555,8 @@ impl NotificationRect { let y_offset = img_height.map(|img_height| height as usize / 2 - img_height / 2); image.draw( - padding, - y_offset.unwrap_or_default(), + padding.left() as usize, + padding.top() as usize + y_offset.unwrap_or_default(), stride, |position, bgra| unsafe { *TryInto::<&mut [u8; 4]>::try_into( @@ -581,21 +582,23 @@ impl NotificationRect { ); let x_offset = img_width - .map(|width| (width + padding * 2) * 4) + .map(|width| (width + padding.left() as usize) * 4) .unwrap_or_default(); - summary.set_padding(padding); - summary.set_line_spacing(display.title().line_spacing() as usize); + + let title_cfg = display.title(); + + summary.set_margin(title_cfg.margin()); + summary.set_line_spacing(title_cfg.line_spacing() as usize); summary.set_foreground(foreground.clone()); - let y_offset = summary.draw( - width as usize - - img_width - .map(|width| width + padding * 2) - .unwrap_or_default(), + let summary_height = summary.draw( + width - img_width.unwrap_or_default(), height as usize, display.title().alignment(), |x, y, bgra| { - let position = (y * stride as isize + x_offset as isize + x * 4) as usize; + let position = ((y + padding.top() as isize) * stride as isize + + x_offset as isize + + x * 4) as usize; unsafe { *TryInto::<&mut [u8; 4]>::try_into( &mut self.framebuffer[position..position + 4], @@ -604,6 +607,7 @@ impl NotificationRect { } }, ); + height -= summary_height; let mut text = if display.markup() { TextRect::from_text( @@ -619,15 +623,17 @@ impl NotificationRect { ) }; - text.set_padding(padding); - text.set_line_spacing(display.body().line_spacing() as usize); + let body_cfg = display.body(); + + text.set_margin(body_cfg.margin()); + text.set_line_spacing(body_cfg.line_spacing() as usize); text.set_foreground(foreground); + + let y_offset = padding.top() as usize + summary_height; + text.draw( - width as usize - - img_width - .map(|width| width + padding * 2) - .unwrap_or_default(), - height as usize - y_offset, + width - img_width.unwrap_or_default(), + height, display.body().alignment(), |x, y, bgra| { let position = ((y + y_offset as isize) * stride as isize diff --git a/src/render/text.rs b/src/render/text.rs index 81c0b9f..b0dbba0 100644 --- a/src/render/text.rs +++ b/src/render/text.rs @@ -2,7 +2,11 @@ use std::{collections::VecDeque, sync::Arc}; use fontdue::Metrics; -use crate::{config::TextAlignment, data::text::Text, render::font::FontStyle}; +use crate::{ + config::{spacing::Spacing, TextAlignment}, + data::text::Text, + render::font::FontStyle, +}; use super::{color::Bgra, font::FontCollection, image::Image}; @@ -10,7 +14,7 @@ use super::{color::Bgra, font::FontCollection, image::Image}; pub(crate) struct TextRect { words: Vec, line_spacing: usize, - padding: usize, + margin: Spacing, fg_color: Bgra, } @@ -127,8 +131,8 @@ impl TextRect { self.line_spacing = line_spacing; } - pub(crate) fn set_padding(&mut self, padding: usize) { - self.padding = padding; + pub(crate) fn set_margin(&mut self, margin: &Spacing) { + self.margin = margin.clone(); } pub(crate) fn set_foreground(&mut self, color: Bgra) { @@ -143,7 +147,7 @@ impl TextRect { mut callback: O, ) -> usize { const SPACEBAR_WIDTH: isize = 15; - let bottom = height - self.padding; + let bottom = height - self.margin.bottom() as usize; let (bottom_spacing, line_height) = self .words @@ -154,14 +158,15 @@ impl TextRect { let total_height = bottom_spacing + line_height as usize; - let mut actual_height = self.padding; + let mut actual_height = self.margin.top() as usize; let mut word_index = 0; - for y in (self.padding as isize..bottom as isize) + for y in (self.margin.top() as isize..bottom as isize) .step_by(total_height + self.line_spacing) .take_while(|y| bottom - *y as usize > total_height) { - let mut remaining_width = (width - self.padding * 2) as isize; + let mut remaining_width = + (width - self.margin.left() as usize - self.margin.right() as usize) as isize; let mut words = vec![]; while let Some(word) = self.words.get(word_index) { let new_width = @@ -194,7 +199,7 @@ impl TextRect { }, ), }; - x += self.padding as isize; + x += self.margin.left() as isize; for word in words { word.glyphs.iter().for_each(|local_glyph| { @@ -214,7 +219,7 @@ impl TextRect { actual_height += total_height + self.line_spacing; } - if actual_height > self.padding { + if actual_height > self.margin.top() as usize { actual_height -= self.line_spacing; }