From e7461a07c2d2e01ec09c5ecee3a21bda0425367e Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Mon, 8 Jan 2024 20:56:14 +0100 Subject: [PATCH] commit --- .../{filter.rs => destructive_filter.rs} | 19 +-- deckster/src/icons/mod.rs | 131 ++++++++++-------- deckster/src/model/icon_descriptor.rs | 11 +- deckster/src/model/image_filter.rs | 126 +++++++++-------- .../src/model/key_modes/home_assistant.rs | 8 +- deckster/src/model/key_modes/media.rs | 8 +- deckster/src/model/key_modes/spotify.rs | 8 +- deckster/src/model/key_page.rs | 28 +++- deckster/src/model/knob_modes/audio_volume.rs | 20 +-- deckster/src/model/knob_page.rs | 28 ++-- deckster/src/model/mod.rs | 124 +---------------- deckster/src/model/position.rs | 117 ++++++++++++++++ deckster/src/runner/graphics.rs | 87 +++++++----- deckster/src/runner/state.rs | 7 +- 14 files changed, 378 insertions(+), 344 deletions(-) rename deckster/src/icons/{filter.rs => destructive_filter.rs} (58%) create mode 100644 deckster/src/model/position.rs diff --git a/deckster/src/icons/filter.rs b/deckster/src/icons/destructive_filter.rs similarity index 58% rename from deckster/src/icons/filter.rs rename to deckster/src/icons/destructive_filter.rs index 331bd09..125558e 100644 --- a/deckster/src/icons/filter.rs +++ b/deckster/src/icons/destructive_filter.rs @@ -1,27 +1,16 @@ -use color_eyre::{eyre::ContextCompat, Result}; +use color_eyre::Result; use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8}; -use crate::model::image_filter::ImageFilter; +use crate::model::image_filter::ImageFilterDestructive; -pub fn apply(original: &Pixmap, filter: &ImageFilter) -> Result { - let mut result = if let Some(rect) = filter.crop { - original.clone_rect(*rect).wrap_err_with(|| format!("Invalid crop rect: {}", rect))? - } else { - original.clone() - }; - - // scale, rotate and border are handled in runner::graphics::render_key +pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result { + let mut result = original.clone(); for p in result.pixels_mut() { if filter.invert { *p = PremultipliedColorU8::from_rgba(p.alpha() - p.red(), p.alpha() - p.green(), p.alpha() - p.blue(), p.alpha()).unwrap(); } - if filter.alpha != 1.0 { - let d = p.demultiply(); - *p = ColorU8::from_rgba(d.red(), d.green(), d.blue(), (d.alpha() as f32 * filter.alpha).round() as u8).premultiply(); - } - if filter.grayscale { let a = p.alpha(); if a > 0 { diff --git a/deckster/src/icons/mod.rs b/deckster/src/icons/mod.rs index 66d0d40..71c7064 100644 --- a/deckster/src/icons/mod.rs +++ b/deckster/src/icons/mod.rs @@ -9,70 +9,82 @@ use tiny_skia::{Pixmap, Transform}; use crate::model::config::{Config, IconFormat, IconPack}; use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; -use crate::model::rgb::RGB8Wrapper; -use crate::model::IconMap; +use crate::model::image_filter::ImageFilterDestructive; +use crate::model::{key_page, knob_page}; -mod filter; +mod destructive_filter; + +type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>; #[derive(Debug)] pub struct LoadedIcon { pub pixmap: Pixmap, - pub scale: f32, - pub clockwise_quarter_rotations: u8, - pub border: Option, + pub pre_scale: f32, } pub fn get_used_icon_descriptors(config: &Config) -> HashSet { let mut result: HashSet = HashSet::new(); - fn insert_all_from_map(result: &mut HashSet, map: &IconMap) { + fn insert_all_from_key_style_by_state_map(result: &mut HashSet, map: &key_page::StyleByStateMap) { map.values().for_each(|v| { - result.insert(v.clone()); + if let Some(icon) = &v.icon { + result.insert(icon.clone()); + } + }); + } + + fn insert_all_from_knob_style_by_state_map(result: &mut HashSet, map: &knob_page::StyleByStateMap) { + map.values().for_each(|v| { + if let Some(icon) = &v.icon { + result.insert(icon.clone()); + } }); } for page in config.key_pages_by_id.values() { for key in page.keys.values() { - if let Some(d) = &key.icon { + if let Some(d) = &key.base_style.icon { result.insert(d.clone()); + } - if let Some(c) = &key.mode.media__next { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.media__next { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.media__play_pause { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.media__play_pause { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.media__previous { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.media__previous { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.home_assistant__button { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.home_assistant__button { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.home_assistant__switch { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.home_assistant__switch { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.spotify__repeat { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.spotify__repeat { + insert_all_from_key_style_by_state_map(&mut result, &c.style); + } - if let Some(c) = &key.mode.spotify__shuffle { - insert_all_from_map(&mut result, &c.icon); - } + if let Some(c) = &key.mode.spotify__shuffle { + insert_all_from_key_style_by_state_map(&mut result, &c.style); } } } for page in config.knob_pages_by_id.values() { for knob in page.knobs.values() { - result.insert(knob.icon.clone()); + if let Some(d) = &knob.base_style.icon { + result.insert(d.clone()); + } if let Some(c) = &knob.mode.audio_volume { - insert_all_from_map(&mut result, &c.icon); + insert_all_from_knob_style_by_state_map(&mut result, &c.style) } } } @@ -85,15 +97,15 @@ pub fn load_icons( icon_packs_by_id: &HashMap, descriptors: HashSet, dpi: f32, -) -> Result> { +) -> Result { let mut highest_scale_by_source: HashMap = HashMap::new(); for d in &descriptors { - let mut scale = d.filter.scale; + let mut scale = d.filter.transform.scale; if let IconDescriptorSource::IconPack { pack_id, .. } = &d.source { let pack = &icon_packs_by_id[pack_id]; if let Some(filter) = &pack.global_filter { - scale *= filter.scale; + scale *= filter.transform.scale; } } @@ -106,17 +118,31 @@ pub fn load_icons( } } - let mut unfiltered_scaled_pixmap_by_source: HashMap = HashMap::new(); - let mut icons_by_descriptor: HashMap = HashMap::new(); + let mut unfiltered_pixmap_and_scale_by_source: HashMap = HashMap::new(); + let mut icons: LoadedIconsMap = HashMap::new(); let mut fonts_db = resvg::usvg::fontdb::Database::new(); fonts_db.load_system_fonts(); for descriptor in descriptors { - if descriptor.source == IconDescriptorSource::None { + let icon_pack = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source { + Some(&icon_packs_by_id[pack_id]) + } else { + None + }; + + let filter = if let Some(global_filter) = icon_pack.map(|p| p.global_filter).flatten() { + descriptor.filter.destructive.combine_after(&global_filter.destructive) + } else { + descriptor.filter.destructive + }; + + let id = (descriptor.source, filter); + + if descriptor.source == IconDescriptorSource::None || icons.contains(&id) { continue; } - let (original_image, original_image_scale) = match unfiltered_scaled_pixmap_by_source.entry(descriptor.source.clone()) { + let (original_image, original_image_scale) = match unfiltered_pixmap_and_scale_by_source.entry(descriptor.source.clone()) { Entry::Occupied(o) => o.into_mut(), Entry::Vacant(v) => v.insert(read_image_and_get_scale( config_directory, @@ -128,35 +154,18 @@ pub fn load_icons( )?), }; - let (pixmap, scale) = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source { - let pack = &icon_packs_by_id[pack_id]; - if let Some(global_filter) = &pack.global_filter { - ( - filter::apply(&filter::apply(original_image, global_filter)?, &descriptor.filter)?, - descriptor.filter.scale * global_filter.scale, - ) - } else { - (filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale) - } - } else { - (filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale) - }; + let pixmap = destructive_filter::apply(original_image, &filter)?; - let scale = scale / *original_image_scale; - let clockwise_quarter_rotations = descriptor.filter.clockwise_quarter_rotations; - let border = descriptor.filter.border; - icons_by_descriptor.insert( - descriptor, + icons.insert( + id, LoadedIcon { pixmap, - scale, - clockwise_quarter_rotations, - border, + pre_scale: *original_image_scale, }, ); } - Ok(icons_by_descriptor) + Ok(icons) } fn read_image_and_get_scale( diff --git a/deckster/src/model/icon_descriptor.rs b/deckster/src/model/icon_descriptor.rs index d9ce6ad..f88dd6d 100644 --- a/deckster/src/model/icon_descriptor.rs +++ b/deckster/src/model/icon_descriptor.rs @@ -8,7 +8,7 @@ use thiserror::Error; use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; -#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)] +#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] pub struct IconDescriptor { pub source: IconDescriptorSource, pub filter: ImageFilter, @@ -76,6 +76,15 @@ pub enum IconDescriptorSource { Path(PathBuf), } +impl IconDescriptorSource { + pub fn pack_id(&self) -> Option<&String> { + match self { + IconDescriptorSource::IconPack { pack_id, .. } => Some(pack_id), + _ => None, + } + } +} + impl Display for IconDescriptorSource { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 6c976c8..869fd82 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -8,16 +8,45 @@ use thiserror::Error; use crate::model::geometry::IntRectWrapper; use crate::model::rgb::RGB8Wrapper; -#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)] -pub struct ImageFilter { - pub crop: Option, +#[derive(Debug, PartialEq, Clone)] +pub struct ImageFilterTransform { pub scale: f32, + // Must be in 0..=3 pub clockwise_quarter_rotations: u8, - pub color: Option, pub alpha: f32, +} + +impl ImageFilterTransform { + pub fn combine_after(&self, other: &ImageFilterTransform) -> ImageFilterTransform { + ImageFilterTransform { + scale: self.scale * other.scale, + alpha: self.alpha * other.alpha, + clockwise_quarter_rotations: (self.clockwise_quarter_rotations + other.clockwise_quarter_rotations) % 4, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct ImageFilterDestructive { + pub color: Option, pub grayscale: bool, pub invert: bool, - pub border: Option, +} + +impl ImageFilterDestructive { + pub fn combine_after(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive { + ImageFilterDestructive { + color: self.color.or(other.color), + grayscale: self.grayscale || other.grayscale, + invert: self.invert ^ other.invert, + } + } +} + +#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)] +pub struct ImageFilter { + pub transform: ImageFilterTransform, + pub destructive: ImageFilterDestructive, } impl Eq for ImageFilter { @@ -30,23 +59,25 @@ impl Hash for ImageFilter { } } -const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter { - scale: 1.0, - clockwise_quarter_rotations: 0, - crop: None, - color: None, - alpha: 1.0, - grayscale: false, - invert: false, - border: None, -}; - impl Default for ImageFilter { fn default() -> Self { DEFAULT_IMAGE_FILTER.clone() } } +const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter { + transform: ImageFilterTransform { + scale: 1.0, + clockwise_quarter_rotations: 0, + alpha: 1.0, + }, + destructive: ImageFilterDestructive { + color: None, + grayscale: false, + invert: false, + }, +}; + #[derive(Debug, Error)] pub enum ImageFilterFromStringError { #[error("Unknown filter: {name}")] @@ -141,8 +172,7 @@ impl FromStr for ImageFilter { }; match filter_name.as_str() { - "crop" => result.crop = Some(parse_rect_filter_value(&filter_name, use_raw_value()?)?), - "scale" => result.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?, + "scale" => result.transform.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?, "rotate" => { let raw_value = use_raw_value()?; let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable { @@ -150,7 +180,7 @@ impl FromStr for ImageFilter { raw_value, })?; - result.clockwise_quarter_rotations = match value { + result.transform.clockwise_quarter_rotations = match value { 0 => 0, 90 => 1, 180 => 2, @@ -158,29 +188,19 @@ impl FromStr for ImageFilter { _ => return Err(ImageFilterFromStringError::RotationNotAllowed), }; } + "alpha" => result.transform.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?, "color" => { let raw_value = use_raw_value()?; - result.color = Some( - RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable { - filter_name: filter_name.to_string(), - raw_value, - })?, - ) - } - "alpha" => result.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?, - "grayscale" => result.grayscale = use_bool_value()?, - "invert" => result.invert = use_bool_value()?, - "border" => { - let raw_value = use_raw_value()?; - - result.border = Some( + result.destructive.color = Some( RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable { filter_name: filter_name.to_string(), raw_value, })?, ) } + "grayscale" => result.destructive.grayscale = use_bool_value()?, + "invert" => result.destructive.invert = use_bool_value()?, _ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }), }; } @@ -193,32 +213,32 @@ impl Display for ImageFilter { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let mut is_first = true; - if let Some(rect) = &self.crop { - // if !is_first { - // f.write_str("|")? - // } - f.write_fmt(format_args!("crop={}", rect))?; - is_first = false; - } - - if self.scale != DEFAULT_IMAGE_FILTER.scale { + if self.transform.scale != DEFAULT_IMAGE_FILTER.transform.scale { if !is_first { f.write_str("|")? } - f.write_fmt(format_args!("scale={}", self.scale))?; + f.write_fmt(format_args!("scale={}", self.transform.scale))?; is_first = false; } - if self.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.clockwise_quarter_rotations { + if self.transform.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.transform.clockwise_quarter_rotations { if !is_first { f.write_str("|")? } - f.write_fmt(format_args!("rotate={}", self.clockwise_quarter_rotations as u16 * 90))?; + f.write_fmt(format_args!("rotate={}", self.transform.clockwise_quarter_rotations as u16 * 90))?; is_first = false; } - if let Some(color) = self.color { + if self.transform.alpha != DEFAULT_IMAGE_FILTER.transform.alpha { + if !is_first { + f.write_str("|")? + } + f.write_fmt(format_args!("alpha={}", self.transform.alpha))?; + is_first = false; + } + + if let Some(color) = self.destructive.color { if !is_first { f.write_str("|")? } @@ -227,15 +247,7 @@ impl Display for ImageFilter { is_first = false; } - if self.alpha != DEFAULT_IMAGE_FILTER.alpha { - if !is_first { - f.write_str("|")? - } - f.write_fmt(format_args!("alpha={}", self.alpha))?; - is_first = false; - } - - if self.grayscale { + if self.destructive.grayscale { if !is_first { f.write_str("|")? } @@ -243,7 +255,7 @@ impl Display for ImageFilter { is_first = false; } - if self.invert { + if self.destructive.invert { if !is_first { f.write_str("|")? } @@ -251,7 +263,7 @@ impl Display for ImageFilter { is_first = false; } - if let Some(color) = self.color { + if let Some(color) = self.destructive.color { if !is_first { f.write_str("|")? } diff --git a/deckster/src/model/key_modes/home_assistant.rs b/deckster/src/model/key_modes/home_assistant.rs index 6d71a2b..786ef68 100644 --- a/deckster/src/model/key_modes/home_assistant.rs +++ b/deckster/src/model/key_modes/home_assistant.rs @@ -1,21 +1,19 @@ -use std::collections::HashMap; - use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::key_page::StyleByStateMap; #[derive(Debug, Deserialize)] pub struct SwitchConfig { pub name: String, #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, } #[derive(Debug, Deserialize)] pub struct ButtonConfig { pub name: String, #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_modes/media.rs b/deckster/src/model/key_modes/media.rs index fe9ac1b..6470f76 100644 --- a/deckster/src/model/key_modes/media.rs +++ b/deckster/src/model/key_modes/media.rs @@ -1,13 +1,11 @@ -use std::collections::HashMap; - use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::key_page::StyleByStateMap; #[derive(Debug, Deserialize)] pub struct PlayPauseConfig { #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, #[serde(default)] pub action: PlayPauseAction, } @@ -24,7 +22,7 @@ pub enum PlayPauseAction { #[derive(Debug, Deserialize)] pub struct PreviousAndNextConfig { #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_modes/spotify.rs b/deckster/src/model/key_modes/spotify.rs index 612a424..b2b14fd 100644 --- a/deckster/src/model/key_modes/spotify.rs +++ b/deckster/src/model/key_modes/spotify.rs @@ -1,19 +1,17 @@ -use std::collections::HashMap; - use serde::Deserialize; -use crate::model::icon_descriptor::IconDescriptor; +use crate::model::key_page::StyleByStateMap; #[derive(Debug, Deserialize)] pub struct ShuffleConfig { #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, } #[derive(Debug, Deserialize)] pub struct RepeatConfig { #[serde(default)] - pub icon: HashMap, + pub style: StyleByStateMap, } #[derive(Debug, Eq, PartialEq, Hash, Deserialize)] diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index f3a8131..eb9258c 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -4,7 +4,9 @@ use serde::Deserialize; use crate::model::geometry::UIntVec2; use crate::model::icon_descriptor::IconDescriptor; -use crate::model::{key_modes, KeyPosition, KnobPosition}; +use crate::model::key_modes; +use crate::model::position::{KeyPosition, KnobPosition}; +use crate::model::rgb::RGB8WithOptionalA; #[derive(Debug, Deserialize)] pub struct File { @@ -44,15 +46,33 @@ pub enum ScrollingConfigAxis { } #[derive(Debug, Deserialize)] -pub struct KeyConfig { +pub struct KeyStyle { pub label: Option, pub icon: Option, + pub border: Option, +} + +impl KeyStyle { + pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle { + KeyStyle { + label: self.label.or_else(|| base.label.clone()), + icon: self.icon.or(base.icon), + border: self.border.or(base.border), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct KeyConfig { + #[serde(default, flatten)] + pub base_style: KeyStyle, + #[serde(default)] pub mode: KeyModes, } #[allow(non_snake_case)] -#[derive(Debug, Deserialize, Default)] +#[derive(Debug, Default, Deserialize)] pub struct KeyModes { pub vibrate: Option, pub media__play_pause: Option, @@ -63,3 +83,5 @@ pub struct KeyModes { pub home_assistant__switch: Option, pub home_assistant__button: Option, } + +pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/knob_modes/audio_volume.rs b/deckster/src/model/knob_modes/audio_volume.rs index 124937d..4ab592e 100644 --- a/deckster/src/model/knob_modes/audio_volume.rs +++ b/deckster/src/model/knob_modes/audio_volume.rs @@ -1,11 +1,9 @@ use std::collections::HashMap; -use std::num::NonZeroU8; use regex::Regex; use serde::Deserialize; -use crate::model::rgb::RGB8WithOptionalA; -use crate::model::IconMap; +use crate::model::knob_page::StyleByStateMap; #[derive(Debug, Deserialize)] pub struct Config { @@ -22,9 +20,7 @@ pub struct Config { #[serde(default)] pub label: HashMap, #[serde(default)] - pub icon: IconMap, - pub circle_indicator: Option, - pub bar_indicator: Option, + pub style: StyleByStateMap, } #[derive(Debug, Default, Eq, PartialEq, Deserialize)] @@ -51,15 +47,3 @@ pub enum State { Active, Muted, } - -#[derive(Debug, Deserialize)] -pub struct CircleIndicatorConfig { - pub color: RGB8WithOptionalA, - pub width: NonZeroU8, - pub radius: u8, -} - -#[derive(Debug, Deserialize)] -pub struct BarIndicatorConfig { - pub color: RGB8WithOptionalA, -} diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index 749794f..22199f0 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -4,8 +4,9 @@ use enum_map::EnumMap; use serde::Deserialize; use crate::model::icon_descriptor::IconDescriptor; +use crate::model::knob_modes; +use crate::model::position::KnobPosition; use crate::model::rgb::RGB8WithOptionalA; -use crate::model::{knob_modes, KnobPosition}; #[derive(Debug, Deserialize)] pub struct File { @@ -21,12 +22,8 @@ pub struct Page { #[derive(Debug, Default, Deserialize)] pub struct Knob { - #[serde(default)] - pub label: String, - #[serde(default)] - pub icon: IconDescriptor, - #[serde(default)] - pub indicator: KnobIndicators, + #[serde(default, flatten)] + pub base_style: KnobStyle, #[serde(default)] pub mode: KnobModes, } @@ -39,17 +36,26 @@ pub struct KnobIndicators { #[derive(Debug, Deserialize)] pub struct KnobIndicatorBarConfig { - pub color: RGB8WithOptionalA, + pub color: Option, } #[derive(Debug, Deserialize)] pub struct KnobIndicatorCircleConfig { - pub color: RGB8WithOptionalA, - pub width: u8, - pub radius: u8, + pub color: Option, + pub width: Option, + pub radius: Option, +} + +#[derive(Debug, Default, Deserialize)] +pub struct KnobStyle { + pub label: Option, + pub icon: Option, + pub indicators: Option, } #[derive(Debug, Default, Deserialize)] pub struct KnobModes { pub audio_volume: Option, } + +pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/mod.rs b/deckster/src/model/mod.rs index 6158191..c45e844 100644 --- a/deckster/src/model/mod.rs +++ b/deckster/src/model/mod.rs @@ -1,16 +1,3 @@ -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -use enum_map::Enum; -use serde::{Deserialize, Serialize}; -use serde_with::{DeserializeFromStr, SerializeDisplay}; -use thiserror::Error; - -use loupedeck_serial::characteristics::LoupedeckButton; - -use crate::model::icon_descriptor::IconDescriptor; - pub mod config; pub mod geometry; pub mod icon_descriptor; @@ -19,114 +6,5 @@ pub mod key_modes; pub mod key_page; pub mod knob_modes; pub mod knob_page; +pub mod position; pub mod rgb; - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] -/// One-based coordinates of a specific virtual (not physical) key. -pub struct KeyPosition { - pub x: u16, - pub y: u16, -} - -#[derive(Debug, Error)] -#[error("The input value does not match the required format of and separated by an 'x'")] -pub struct KeyPositionFromStrError {} - -impl FromStr for KeyPosition { - type Err = KeyPositionFromStrError; - - fn from_str(s: &str) -> Result { - let values = s.split_once('x'); - - if let Some((x, y)) = values { - if let Ok(x) = u16::from_str(x) { - if let Ok(y) = u16::from_str(y) { - return Ok(KeyPosition { x, y }); - } - } - } - - Err(KeyPositionFromStrError {}) - } -} - -impl Display for KeyPosition { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}x{}", self.x, self.y)) - } -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] -pub struct KeyPath { - pub page_id: String, - pub position: KeyPosition, -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)] -#[serde(rename_all = "kebab-case")] -pub enum KnobPosition { - LeftTop, - LeftMiddle, - LeftBottom, - RightTop, - RightMiddle, - RightBottom, -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] -pub struct KnobPath { - pub page_id: String, - pub position: KnobPosition, -} - -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] -pub enum ButtonPosition { - #[serde(rename = "0")] - N0, - #[serde(rename = "1")] - N1, - #[serde(rename = "2")] - N2, - #[serde(rename = "3")] - N3, - #[serde(rename = "4")] - N4, - #[serde(rename = "5")] - N5, - #[serde(rename = "6")] - N6, - #[serde(rename = "7")] - N7, -} - -impl ButtonPosition { - pub fn of(button: &LoupedeckButton) -> Self { - match button { - LoupedeckButton::N0 => Self::N0, - LoupedeckButton::N1 => Self::N1, - LoupedeckButton::N2 => Self::N2, - LoupedeckButton::N3 => Self::N3, - LoupedeckButton::N4 => Self::N4, - LoupedeckButton::N5 => Self::N5, - LoupedeckButton::N6 => Self::N6, - LoupedeckButton::N7 => Self::N7, - } - } -} - -impl Display for ButtonPosition { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.write_str(match self { - ButtonPosition::N0 => "0", - ButtonPosition::N1 => "1", - ButtonPosition::N2 => "2", - ButtonPosition::N3 => "3", - ButtonPosition::N4 => "4", - ButtonPosition::N5 => "5", - ButtonPosition::N6 => "6", - ButtonPosition::N7 => "7", - }) - } -} - -pub type IconMap = HashMap; diff --git a/deckster/src/model/position.rs b/deckster/src/model/position.rs new file mode 100644 index 0000000..1e0aeb9 --- /dev/null +++ b/deckster/src/model/position.rs @@ -0,0 +1,117 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +use enum_map::Enum; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use thiserror::Error; + +use loupedeck_serial::characteristics::LoupedeckButton; + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] +/// One-based coordinates of a specific virtual (not physical) key. +pub struct KeyPosition { + pub x: u16, + pub y: u16, +} + +#[derive(Debug, Error)] +#[error("The input value does not match the required format of and separated by an 'x'")] +pub struct KeyPositionFromStrError {} + +impl FromStr for KeyPosition { + type Err = KeyPositionFromStrError; + + fn from_str(s: &str) -> Result { + let values = s.split_once('x'); + + if let Some((x, y)) = values { + if let Ok(x) = u16::from_str(x) { + if let Ok(y) = u16::from_str(y) { + return Ok(KeyPosition { x, y }); + } + } + } + + Err(KeyPositionFromStrError {}) + } +} + +impl Display for KeyPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("{}x{}", self.x, self.y)) + } +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +pub struct KeyPath { + pub page_id: String, + pub position: KeyPosition, +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)] +#[serde(rename_all = "kebab-case")] +pub enum KnobPosition { + LeftTop, + LeftMiddle, + LeftBottom, + RightTop, + RightMiddle, + RightBottom, +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +pub struct KnobPath { + pub page_id: String, + pub position: KnobPosition, +} + +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] +pub enum ButtonPosition { + #[serde(rename = "0")] + N0, + #[serde(rename = "1")] + N1, + #[serde(rename = "2")] + N2, + #[serde(rename = "3")] + N3, + #[serde(rename = "4")] + N4, + #[serde(rename = "5")] + N5, + #[serde(rename = "6")] + N6, + #[serde(rename = "7")] + N7, +} + +impl ButtonPosition { + pub fn of(button: &LoupedeckButton) -> Self { + match button { + LoupedeckButton::N0 => Self::N0, + LoupedeckButton::N1 => Self::N1, + LoupedeckButton::N2 => Self::N2, + LoupedeckButton::N3 => Self::N3, + LoupedeckButton::N4 => Self::N4, + LoupedeckButton::N5 => Self::N5, + LoupedeckButton::N6 => Self::N6, + LoupedeckButton::N7 => Self::N7, + } + } +} + +impl Display for ButtonPosition { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + ButtonPosition::N0 => "0", + ButtonPosition::N1 => "1", + ButtonPosition::N2 => "2", + ButtonPosition::N3 => "3", + ButtonPosition::N4 => "4", + ButtonPosition::N5 => "5", + ButtonPosition::N6 => "6", + ButtonPosition::N7 => "7", + }) + } +} diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index 26fd6e2..434146a 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::HashMap; use bytes::{BufMut, Bytes, BytesMut}; @@ -10,24 +11,38 @@ use loupedeck_serial::util::Endianness; use crate::icons::LoadedIcon; use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; +use crate::model::image_filter::ImageFilterTransform; use crate::runner::graphics::labels::LabelRenderer; use crate::runner::state::Key; -pub fn render_key( - label_renderer: &mut LabelRenderer, - key_size: IntSize, +#[derive(Debug)] +struct GraphicsContext { + label_renderer: RefCell, buffer_endianness: Endianness, - icons: &HashMap, - state: Option<&Key>, -) -> Bytes { + global_icon_filter_by_pack_id: HashMap, + loaded_icons: HashMap, +} + +pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes { let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap(); if let Some(state) = state { - if state.icon.source != IconDescriptorSource::None { - let icon = &icons[&state.icon]; - let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height()) + let style = state.style.merge_over(&state.base_style); + + if style.icon.source != IconDescriptorSource::None { + let loaded_icon = &context.loaded_icons[&state.icon]; + + let filter = if let Some(global_filter) = state.icon.source.pack_id().map(|i| context.global_icon_filter_by_pack_id.get(i)).flatten() { + &state.icon.filter.transform.combine_after(global_filter) + } else { + &state.icon.filter.transform + }; + + let scale = filter.scale / loaded_icon.pre_scale; + + let scaled_size = IntSize::from_wh(loaded_icon.pixmap.width(), loaded_icon.pixmap.height()) .unwrap() - .scale_by(icon.scale) + .scale_by(scale) .unwrap(); static PAINT: PixmapPaint = PixmapPaint { @@ -37,43 +52,41 @@ pub fn render_key( }; pixmap.draw_pixmap( - (((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / icon.scale).round() as i32, - (((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / icon.scale).round() as i32, - icon.pixmap.as_ref(), + (((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32, + (((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / scale).round() as i32, + loaded_icon.pixmap.as_ref(), &PAINT, - Transform::from_scale(icon.scale, icon.scale).post_rotate_at( - (icon.clockwise_quarter_rotations as f32) * 90.0, + Transform::from_scale(scale, scale).post_rotate_at( + (filter.clockwise_quarter_rotations as f32) * 90.0, key_size.width() as f32 / 2.0, key_size.height() as f32 / 2.0, ), None, ); - - if let Some(color) = icon.border { - let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap()); - - let paint = Paint { - shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, 255)), - ..Paint::default() - }; - - static STROKE: Stroke = Stroke { - width: 15.0, - miter_limit: 4.0, - line_cap: LineCap::Butt, - line_join: LineJoin::Round, - dash: None, - }; - - pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None); - } } if !state.label.is_empty() { - label_renderer.render(&mut pixmap, &state.label); + context.label_renderer.borrow_mut().render(&mut pixmap, &state.label); + } + + if let Some(color) = state.st { + let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap()); + + let paint = Paint { + shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, 255)), + ..Paint::default() + }; + + static STROKE: Stroke = Stroke { + width: 15.0, + miter_limit: 4.0, + line_cap: LineCap::Butt, + line_join: LineJoin::Round, + dash: None, + }; + + pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None); } - } else { - pixmap.fill(Color::BLACK); } convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze() diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs index 20d23e3..c5bac40 100644 --- a/deckster/src/runner/state.rs +++ b/deckster/src/runner/state.rs @@ -5,7 +5,8 @@ use log::error; use serde::{Deserialize, Serialize}; use crate::model::icon_descriptor::IconDescriptor; -use crate::model::{KeyPath, KeyPosition, KnobPath, KnobPosition}; +use crate::model::key_page::KeyStyle; +use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition}; #[derive(Debug)] pub struct State { @@ -56,8 +57,8 @@ pub struct KnobPage { #[derive(Debug)] pub struct Key { pub path: KeyPath, - pub icon: IconDescriptor, - pub label: String, + pub base_style: KeyStyle, + pub style: KeyStyle, } #[derive(Debug)]