From 2719b7afb83d834889283e4c091c4744b4ad37ff Mon Sep 17 00:00:00 2001 From: Moritz Ruth Date: Mon, 8 Jan 2024 22:37:17 +0100 Subject: [PATCH] commit --- Cargo.lock | 7 ++ deckster/Cargo.toml | 3 +- deckster/examples/full/deckster.toml | 1 + deckster/examples/full/key-pages/default.toml | 7 +- deckster/examples/full/key-pages/numpad.toml | 14 ++-- deckster/src/icons/mod.rs | 36 +++++----- deckster/src/main.rs | 5 +- deckster/src/model/config.rs | 14 ++-- deckster/src/model/icon_descriptor.rs | 14 ++-- deckster/src/model/image_filter.rs | 31 ++++---- deckster/src/model/key_page.rs | 8 +-- deckster/src/model/knob_page.rs | 8 +-- deckster/src/model/rgb.rs | 6 +- deckster/src/runner/graphics.rs | 69 ++++++++++-------- deckster/src/runner/mod.rs | 72 +++++++++---------- deckster/src/runner/state.rs | 11 ++- 16 files changed, 157 insertions(+), 149 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 50be929..416e122 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,7 @@ dependencies = [ "color-eyre", "cosmic-text", "derive_more", + "encode_unicode", "enum-map", "enum-ordinalize", "env_logger", @@ -400,6 +401,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "enum-map" version = "3.0.0-beta.2" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index 2f14f85..2afffcf 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.4.12", features = ["derive"] } color-eyre = "0.6.2" cosmic-text = "0.10.0" derive_more = "0.99.17" +encode_unicode = "1.0.0" enum-map = "3.0.0-beta.2" enum-ordinalize = "4.3.0" env_logger = "0.10.1" @@ -26,4 +27,4 @@ thiserror = "1.0.52" tiny-skia = "0.11.3" tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]} toml = "0.8.8" -walkdir = "2.4.0" \ No newline at end of file +walkdir = "2.4.0" diff --git a/deckster/examples/full/deckster.toml b/deckster/examples/full/deckster.toml index abe8277..e3b33a6 100644 --- a/deckster/examples/full/deckster.toml +++ b/deckster/examples/full/deckster.toml @@ -1,5 +1,6 @@ inactive_button_color = "#000060" active_button_color = "#eeffff" +label_font_family = "Inter" [buttons.0] key_page = "default" diff --git a/deckster/examples/full/key-pages/default.toml b/deckster/examples/full/key-pages/default.toml index dc690b7..b111805 100644 --- a/deckster/examples/full/key-pages/default.toml +++ b/deckster/examples/full/key-pages/default.toml @@ -1,5 +1,5 @@ [keys.1x1] -icon = "@apps/spotify[scale=2.0|grayscale]" +icon = "@apps/spotify[scale=2.0|invert]" mode.vibrate.pattern = "low" mode.media__play_pause.icon.paused = "@ph/play" mode.media__play_pause.icon.playing = "@ph/pause" @@ -17,14 +17,15 @@ mode.timer.vibrate_when_finished = true mode.timer.needy = true [keys.3x3] -icon = "@fad/thunderbolt[border=#00ff00]" +icon = "@fad/thunderbolt" label = "Dock" +border= "#00ff00" mode.vibrate.pattern = "low" mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock" mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]" [keys.4x3] -icon = "@ph/computer-tower[border=#00ff00]" +icon = "@ph/computer-tower" label = "Tower PC unnötig lang" mode.vibrate.pattern = "low" mode.home_assistant__switch.name = "switch.mwin" diff --git a/deckster/examples/full/key-pages/numpad.toml b/deckster/examples/full/key-pages/numpad.toml index 39dab2a..8736120 100644 --- a/deckster/examples/full/key-pages/numpad.toml +++ b/deckster/examples/full/key-pages/numpad.toml @@ -1,20 +1,20 @@ -[keys.1x4] +[keys.4x1] label = "9" mode.keyboard.key = "9" -[keys.1x3] +[keys.3x1] label = "8" mode.keyboard.key = "8" -[keys.1x2] +[keys.2x1] label = "7" mode.keyboard.key = "7" -[keys.2x4] +[keys.4x2] label = "6" mode.keyboard.key = "6" -[keys.2x3] +[keys.3x2] label = "5" mode.keyboard.key = "5" @@ -22,7 +22,7 @@ mode.keyboard.key = "5" label = "4" mode.keyboard.key = "4" -[keys.3x4] +[keys.4x3] label = "3" mode.keyboard.key = "3" @@ -30,6 +30,6 @@ mode.keyboard.key = "3" label = "2" mode.keyboard.key = "2" -[keys.3x2] +[keys.2x3] label = "1" mode.keyboard.key = "1" \ No newline at end of file diff --git a/deckster/src/icons/mod.rs b/deckster/src/icons/mod.rs index 71c7064..efe64bd 100644 --- a/deckster/src/icons/mod.rs +++ b/deckster/src/icons/mod.rs @@ -14,7 +14,7 @@ use crate::model::{key_page, knob_page}; mod destructive_filter; -type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>; +pub type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>; #[derive(Debug)] pub struct LoadedIcon { @@ -125,36 +125,36 @@ pub fn load_icons( for descriptor in descriptors { let icon_pack = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source { - Some(&icon_packs_by_id[pack_id]) + Some(icon_packs_by_id.get(pack_id).wrap_err_with(|| format!("Unknown icon pack: @{}", 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) + let filter = if let Some(global_filter) = icon_pack.and_then(|p| p.global_filter.as_ref()) { + descriptor.filter.destructive.merge_over(&global_filter.destructive) } else { descriptor.filter.destructive }; let id = (descriptor.source, filter); - if descriptor.source == IconDescriptorSource::None || icons.contains(&id) { + if icons.contains_key(&id) { continue; } - let (original_image, original_image_scale) = match unfiltered_pixmap_and_scale_by_source.entry(descriptor.source.clone()) { + let (original_image, original_image_scale) = match unfiltered_pixmap_and_scale_by_source.entry(id.0.clone()) { Entry::Occupied(o) => o.into_mut(), Entry::Vacant(v) => v.insert(read_image_and_get_scale( config_directory, - icon_packs_by_id, dpi, &fonts_db, - descriptor.source.clone(), - highest_scale_by_source[&descriptor.source], + &id.0, + icon_pack, + highest_scale_by_source[&id.0], )?), }; - let pixmap = destructive_filter::apply(original_image, &filter)?; + let pixmap = destructive_filter::apply(original_image, &id.1)?; icons.insert( id, @@ -170,24 +170,22 @@ pub fn load_icons( fn read_image_and_get_scale( config_directory: &Path, - icon_packs_by_id: &HashMap, dpi: f32, fonts_db: &resvg::usvg::fontdb::Database, - source: IconDescriptorSource, + source: &IconDescriptorSource, + icon_pack: Option<&IconPack>, highest_scale: f32, ) -> Result<(Pixmap, f32)> { let path = match source { - IconDescriptorSource::None => return Ok((Pixmap::new(1, 1).unwrap(), 1.0)), - IconDescriptorSource::Path(path) => path, - IconDescriptorSource::IconPack { pack_id, icon_id } => { - let pack = icon_packs_by_id.get(&pack_id).wrap_err_with(|| format!("Unknown icon pack: @{}", pack_id))?; - - let extension = match pack.format { + IconDescriptorSource::Path(path) => path.clone(), + IconDescriptorSource::IconPack { icon_id, .. } => { + let icon_pack = icon_pack.unwrap(); + let extension = match icon_pack.format { IconFormat::Png => "png", IconFormat::Svg => "svg", }; - config_directory.join(&pack.path).join(icon_id + "." + extension) + config_directory.join(&icon_pack.path).join(icon_id.to_owned() + "." + extension) } }; diff --git a/deckster/src/main.rs b/deckster/src/main.rs index 64b24d7..37de021 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -61,13 +61,14 @@ pub async fn main() -> Result<()> { .collect(); let config = model::config::Config { + active_button_color: deckster_file.active_button_color, + inactive_button_color: deckster_file.inactive_button_color, + label_font_family: deckster_file.label_font_family, key_pages_by_id, knob_pages_by_id, buttons: deckster_file.buttons.into_iter().collect(), icon_packs: deckster_file.icon_packs, initial: deckster_file.initial, - active_button_color: deckster_file.active_button_color, - inactive_button_color: deckster_file.inactive_button_color, } .validate()?; diff --git a/deckster/src/model/config.rs b/deckster/src/model/config.rs index 0db97a6..6d60bb5 100644 --- a/deckster/src/model/config.rs +++ b/deckster/src/model/config.rs @@ -8,16 +8,17 @@ use serde::{Deserialize, Serialize}; use crate::model; use crate::model::image_filter::ImageFilter; +use crate::model::position::ButtonPosition; use crate::model::rgb::RGB8Wrapper; -use crate::model::ButtonPosition; #[derive(Debug, Deserialize)] pub struct File { - pub icon_packs: HashMap, - #[serde(default = "inactive_button_color_default")] - pub inactive_button_color: RGB8Wrapper, #[serde(default = "active_button_color_default")] pub active_button_color: RGB8Wrapper, + #[serde(default = "inactive_button_color_default")] + pub inactive_button_color: RGB8Wrapper, + pub label_font_family: Option, + pub icon_packs: HashMap, pub buttons: HashMap, // EnumMap pub initial: InitialConfig, } @@ -30,11 +31,12 @@ pub struct WithFallbackId { #[derive(Debug)] pub struct Config { + pub active_button_color: RGB8Wrapper, + pub inactive_button_color: RGB8Wrapper, + pub label_font_family: Option, pub key_pages_by_id: HashMap, pub knob_pages_by_id: HashMap, pub icon_packs: HashMap, - pub inactive_button_color: RGB8Wrapper, - pub active_button_color: RGB8Wrapper, pub buttons: EnumMap, pub initial: InitialConfig, } diff --git a/deckster/src/model/icon_descriptor.rs b/deckster/src/model/icon_descriptor.rs index f88dd6d..73d9419 100644 --- a/deckster/src/model/icon_descriptor.rs +++ b/deckster/src/model/icon_descriptor.rs @@ -1,4 +1,4 @@ -use std::fmt::{Display, Formatter, Write}; +use std::fmt::{Display, Formatter}; use std::path::PathBuf; use std::str::FromStr; @@ -8,7 +8,7 @@ use thiserror::Error; use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; -#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)] pub struct IconDescriptor { pub source: IconDescriptorSource, pub filter: ImageFilter, @@ -65,14 +65,9 @@ impl Display for IconDescriptor { } } -#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)] pub enum IconDescriptorSource { - #[default] - None, - IconPack { - pack_id: String, - icon_id: String, - }, + IconPack { pack_id: String, icon_id: String }, Path(PathBuf), } @@ -88,7 +83,6 @@ impl IconDescriptorSource { impl Display for IconDescriptorSource { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - IconDescriptorSource::None => f.write_str(""), IconDescriptorSource::IconPack { pack_id, icon_id } => f.write_fmt(format_args!("@{}/{}", pack_id, icon_id)), IconDescriptorSource::Path(path) => f.write_str(&path.to_string_lossy()), } diff --git a/deckster/src/model/image_filter.rs b/deckster/src/model/image_filter.rs index 869fd82..26c5c4d 100644 --- a/deckster/src/model/image_filter.rs +++ b/deckster/src/model/image_filter.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; -use crate::model::geometry::IntRectWrapper; use crate::model::rgb::RGB8Wrapper; #[derive(Debug, PartialEq, Clone)] @@ -17,7 +16,7 @@ pub struct ImageFilterTransform { } impl ImageFilterTransform { - pub fn combine_after(&self, other: &ImageFilterTransform) -> ImageFilterTransform { + pub fn merge_over(&self, other: &ImageFilterTransform) -> ImageFilterTransform { ImageFilterTransform { scale: self.scale * other.scale, alpha: self.alpha * other.alpha, @@ -26,7 +25,7 @@ impl ImageFilterTransform { } } -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, Eq, PartialEq, Hash, Clone)] pub struct ImageFilterDestructive { pub color: Option, pub grayscale: bool, @@ -34,7 +33,7 @@ pub struct ImageFilterDestructive { } impl ImageFilterDestructive { - pub fn combine_after(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive { + pub fn merge_over(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive { ImageFilterDestructive { color: self.color.or(other.color), grayscale: self.grayscale || other.grayscale, @@ -49,6 +48,15 @@ pub struct ImageFilter { pub destructive: ImageFilterDestructive, } +impl ImageFilter { + pub fn merge_over(&self, other: &ImageFilter) -> ImageFilter { + ImageFilter { + transform: self.transform.merge_over(&other.transform), + destructive: self.destructive.merge_over(&other.destructive), + } + } +} + impl Eq for ImageFilter { fn assert_receiver_is_total_eq(&self) {} } @@ -106,13 +114,6 @@ impl FromStr for ImageFilter { type Err = ImageFilterFromStringError; fn from_str<'a>(s: &str) -> Result { - fn parse_rect_filter_value(filter_name: &str, raw_value: String) -> Result { - IntRectWrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable { - filter_name: filter_name.to_string(), - raw_value, - }) - } - fn parse_f32_filter_value( filter_name: &str, raw_value: String, @@ -135,11 +136,15 @@ impl FromStr for ImageFilter { Ok(value) } - let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect(); - let mut result = ImageFilter::default(); let mut previous_filter_names: Vec = Vec::new(); + let filters: Vec<&str> = s.split('|').map(|f| f.trim()).filter(|s| !s.is_empty()).collect(); + + if filters.is_empty() { + return Ok(result); + } + for filter in filters { let split_filter = filter.split_once('='); let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter { diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index eb9258c..f22e4cf 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::model::geometry::UIntVec2; use crate::model::icon_descriptor::IconDescriptor; @@ -45,7 +45,7 @@ pub enum ScrollingConfigAxis { Horizontal, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct KeyStyle { pub label: Option, pub icon: Option, @@ -55,8 +55,8 @@ pub struct KeyStyle { 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), + label: self.label.as_ref().or(base.label.as_ref()).cloned(), + icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), border: self.border.or(base.border), } } diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index 22199f0..858a767 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -28,25 +28,25 @@ pub struct Knob { pub mode: KnobModes, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] pub struct KnobIndicators { pub bar: Option, pub circle: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct KnobIndicatorBarConfig { pub color: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct KnobIndicatorCircleConfig { pub color: Option, pub width: Option, pub radius: Option, } -#[derive(Debug, Default, Deserialize)] +#[derive(Debug, Default, Clone, Deserialize)] pub struct KnobStyle { pub label: Option, pub icon: Option, diff --git a/deckster/src/model/rgb.rs b/deckster/src/model/rgb.rs index f1bf4b5..6e6998d 100644 --- a/deckster/src/model/rgb.rs +++ b/deckster/src/model/rgb.rs @@ -54,7 +54,7 @@ fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> R } } -#[derive(Debug, Copy, Clone, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] pub struct RGB8Wrapper(RGB8); impl FromStr for RGB8Wrapper { @@ -71,7 +71,7 @@ impl Display for RGB8Wrapper { } } -#[derive(Debug, Copy, Clone, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] #[repr(transparent)] pub struct RGBA8Wrapper(pub RGBA8); @@ -89,7 +89,7 @@ impl Display for RGBA8Wrapper { } } -#[derive(Debug, Copy, Clone, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)] #[repr(transparent)] pub struct RGB8WithOptionalA(RGBA8); diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index 434146a..c7bffa3 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -9,18 +9,17 @@ use tiny_skia::{ use loupedeck_serial::util::Endianness; -use crate::icons::LoadedIcon; -use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; -use crate::model::image_filter::ImageFilterTransform; +use crate::icons::LoadedIconsMap; +use crate::model::image_filter::ImageFilter; use crate::runner::graphics::labels::LabelRenderer; use crate::runner::state::Key; #[derive(Debug)] -struct GraphicsContext { - label_renderer: RefCell, - buffer_endianness: Endianness, - global_icon_filter_by_pack_id: HashMap, - loaded_icons: HashMap, +pub struct GraphicsContext { + pub label_renderer: RefCell, + pub buffer_endianness: Endianness, + pub global_icon_filter_by_pack_id: HashMap, + pub loaded_icons: LoadedIconsMap, } pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes { @@ -29,35 +28,33 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K if let Some(state) = state { 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) + if let Some(icon) = style.icon { + let filter = if let Some(global_filter) = icon.source.pack_id().and_then(|i| context.global_icon_filter_by_pack_id.get(i)) { + icon.filter.merge_over(global_filter) } else { - &state.icon.filter.transform + icon.filter.clone() }; - let scale = filter.scale / loaded_icon.pre_scale; + let loaded_icon = &context.loaded_icons[&(icon.source.clone(), filter.destructive)]; + + let scale = filter.transform.scale / loaded_icon.pre_scale; let scaled_size = IntSize::from_wh(loaded_icon.pixmap.width(), loaded_icon.pixmap.height()) .unwrap() .scale_by(scale) .unwrap(); - static PAINT: PixmapPaint = PixmapPaint { - opacity: 1.0, - blend_mode: BlendMode::SourceOver, - quality: FilterQuality::Bicubic, - }; - pixmap.draw_pixmap( (((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, + &PixmapPaint { + opacity: filter.transform.alpha, + blend_mode: BlendMode::SourceOver, + quality: FilterQuality::Bicubic, + }, Transform::from_scale(scale, scale).post_rotate_at( - (filter.clockwise_quarter_rotations as f32) * 90.0, + (filter.transform.clockwise_quarter_rotations as f32) * 90.0, key_size.width() as f32 / 2.0, key_size.height() as f32 / 2.0, ), @@ -65,15 +62,17 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K ); } - if !state.label.is_empty() { - context.label_renderer.borrow_mut().render(&mut pixmap, &state.label); + if let Some(label) = style.label { + if !label.is_empty() { + context.label_renderer.borrow_mut().render(&mut pixmap, &label); + } } - if let Some(color) = state.st { + if let Some(color) = style.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)), + shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, color.a)), ..Paint::default() }; @@ -89,7 +88,7 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K } } - convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze() + convert_pixels_to_rgb565(pixmap.pixels(), context.buffer_endianness).freeze() } fn convert_pixels_to_rgb565(pixels: &[PremultipliedColorU8], endianness: Endianness) -> BytesMut { @@ -114,8 +113,10 @@ fn convert_pixels_to_rgb565(pixels: &[PremultipliedColorU8], endianness: Endiann pub mod labels { use cosmic_text::{Align, Attrs, AttrsList, Buffer, BufferLine, FontSystem, Metrics, Shaping, SwashCache}; + use encode_unicode::StrExt; use tiny_skia::{Color, Paint, Pixmap, Rect, Shader, Transform}; + #[derive(Debug)] pub struct LabelRenderer { font_system: FontSystem, swash_cache: SwashCache, @@ -123,8 +124,12 @@ pub mod labels { } impl LabelRenderer { - pub fn new() -> Self { + pub fn new(font_family: Option<&String>) -> Self { let mut font_system = FontSystem::new(); + if let Some(f) = font_family { + font_system.db_mut().set_sans_serif_family(f); + } + let buffer = Buffer::new(&mut font_system, Metrics::new(11.0, 11.0)); LabelRenderer { @@ -135,8 +140,7 @@ pub mod labels { } pub fn render(&mut self, pixmap: &mut Pixmap, text: &String) { - let attrs = Attrs::new(); - let mut line = BufferLine::new(text, AttrsList::new(attrs), Shaping::Advanced); + let mut line = BufferLine::new(text, AttrsList::new(Attrs::new()), Shaping::Advanced); line.set_align(Some(Align::Center)); self.buffer.lines.clear(); @@ -149,6 +153,9 @@ pub mod labels { pixmap.height() as f32 - PADDING * 2.0, ); + let font_size = if text.utf8chars().count() == 1 { 40.0 } else { 11.0 }; + self.buffer.set_metrics(&mut self.font_system, Metrics::new(font_size, font_size)); + self.buffer.shape_until_scroll(&mut self.font_system); self.buffer.draw( diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index cee91e6..4c626c5 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -19,12 +19,13 @@ use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::events::LoupedeckEvent; -use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIcon}; +use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap}; use crate::model; -use crate::model::icon_descriptor::IconDescriptor; -use crate::model::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; +use crate::model::key_page::KeyStyle; +use crate::model::knob_page::KnobStyle; +use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; use crate::runner::graphics::labels::LabelRenderer; -use crate::runner::graphics::render_key; +use crate::runner::graphics::{render_key, GraphicsContext}; use crate::runner::state::{Key, State, StateChangeCommand}; mod graphics; @@ -91,8 +92,8 @@ fn create_state(config: &model::config::Config) -> State { page_id: p.id.clone(), position: *position, }, - label: k.label.clone().unwrap_or_default(), - icon: k.icon.clone().unwrap_or_default(), + base_style: k.base_style.clone(), + style: KeyStyle::default(), }) .map(|k| (k.path.position, k)) .collect(), @@ -113,8 +114,8 @@ fn create_state(config: &model::config::Config) -> State { page_id: p.id.clone(), position, }, - label: knob_config.label.clone(), - icon: knob_config.icon.clone(), + base_style: knob_config.base_style.clone(), + style: KnobStyle::default(), value: 0.0, } }), @@ -137,27 +138,39 @@ enum IoWork { struct IoWorkerContext { config: Arc, - icons: HashMap, - label_renderer: RefCell, device: LoupedeckDevice, state: State, + graphics: GraphicsContext, } fn do_io_work( config: Arc, - icons: HashMap, + icons: LoadedIconsMap, device: LoupedeckDevice, events_receiver: Receiver, commands_sender: Sender, commands_receiver: Receiver, ) { let state = create_state(&config); + let buffer_endianness = device.characteristics().key_grid.display.endianness; + let global_icon_filter_by_pack_id = config + .icon_packs + .iter() + .filter_map(|(i, p)| p.global_filter.clone().map(|f| (i.clone(), f))) + .collect(); + + let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref())); + let mut context = IoWorkerContext { config, - icons, - label_renderer: RefCell::new(LabelRenderer::new()), device, state, + graphics: GraphicsContext { + loaded_icons: icons, + buffer_endianness, + label_renderer, + global_icon_filter_by_pack_id, + }, }; loop { @@ -243,26 +256,10 @@ fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) { context.device.refresh_display(&key_grid.display).unwrap(); } - StateChangeCommand::SetKeyLabel { path, value } => { - context.state.mutate_key_for_command( - "SetKeyLabel", - &path, - Box::new(|k| { - k.label = value; - }), - ); - - draw_key_at_path_if_visible(context, path); - context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); - } - StateChangeCommand::SetKeyIcon { path, value } => { - context.state.mutate_key_for_command( - "SetKeyIcon", - &path, - Box::new(|k| { - k.icon = value; - }), - ); + StateChangeCommand::SetKeyStyle { path, value } => { + context.state.mutate_key_for_command("SetKeyStyle", &path, |k| { + k.style = value; + }); draw_key_at_path_if_visible(context, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); @@ -315,13 +312,8 @@ fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) { let key_grid = &context.device.characteristics().key_grid; let (x, y, w, h) = key_grid.get_local_key_rect_xywh(index).unwrap(); - let p = render_key( - &mut context.label_renderer.borrow_mut(), - IntSize::from_wh(w as u32, h as u32).unwrap(), - key_grid.display.endianness, - &context.icons, - key, - ); + let p = render_key(&context.graphics, IntSize::from_wh(w as u32, h as u32).unwrap(), key); + context.device.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, p).unwrap(); } diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs index c5bac40..b8e190b 100644 --- a/deckster/src/runner/state.rs +++ b/deckster/src/runner/state.rs @@ -4,8 +4,8 @@ use enum_map::EnumMap; use log::error; use serde::{Deserialize, Serialize}; -use crate::model::icon_descriptor::IconDescriptor; use crate::model::key_page::KeyStyle; +use crate::model::knob_page::KnobStyle; use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition}; #[derive(Debug)] @@ -17,7 +17,7 @@ pub struct State { } impl State { - pub fn mutate_key_for_command(&mut self, command_name: &'static str, path: &KeyPath, mutator: Box R>) -> Option { + pub fn mutate_key_for_command(&mut self, command_name: &'static str, path: &KeyPath, mutator: impl FnOnce(&mut Key) -> R) -> Option { match self.key_pages_by_id.get_mut(&path.page_id) { None => error!("Received {} command with invalid path.page_id: {}", command_name, &path.page_id), Some(key_page) => match key_page.keys_by_position.get_mut(&path.position) { @@ -64,8 +64,8 @@ pub struct Key { #[derive(Debug)] pub struct Knob { pub path: KnobPath, - pub icon: IconDescriptor, - pub label: String, + pub base_style: KnobStyle, + pub style: KnobStyle, pub value: f32, } @@ -73,6 +73,5 @@ pub struct Knob { #[allow(clippy::enum_variant_names)] pub enum StateChangeCommand { SetActivePages { key_page_id: String, knob_page_id: String }, - SetKeyLabel { path: KeyPath, value: String }, - SetKeyIcon { path: KeyPath, value: IconDescriptor }, + SetKeyStyle { path: KeyPath, value: KeyStyle }, }