This commit is contained in:
Moritz Ruth 2024-01-08 22:37:17 +01:00
parent e7461a07c2
commit 2719b7afb8
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
16 changed files with 157 additions and 149 deletions

7
Cargo.lock generated
View file

@ -357,6 +357,7 @@ dependencies = [
"color-eyre", "color-eyre",
"cosmic-text", "cosmic-text",
"derive_more", "derive_more",
"encode_unicode",
"enum-map", "enum-map",
"enum-ordinalize", "enum-ordinalize",
"env_logger", "env_logger",
@ -400,6 +401,12 @@ dependencies = [
"syn 1.0.109", "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]] [[package]]
name = "enum-map" name = "enum-map"
version = "3.0.0-beta.2" version = "3.0.0-beta.2"

View file

@ -9,6 +9,7 @@ clap = { version = "4.4.12", features = ["derive"] }
color-eyre = "0.6.2" color-eyre = "0.6.2"
cosmic-text = "0.10.0" cosmic-text = "0.10.0"
derive_more = "0.99.17" derive_more = "0.99.17"
encode_unicode = "1.0.0"
enum-map = "3.0.0-beta.2" enum-map = "3.0.0-beta.2"
enum-ordinalize = "4.3.0" enum-ordinalize = "4.3.0"
env_logger = "0.10.1" env_logger = "0.10.1"
@ -26,4 +27,4 @@ thiserror = "1.0.52"
tiny-skia = "0.11.3" tiny-skia = "0.11.3"
tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]} tokio = { version = "1.35.1", features = ["macros", "parking_lot", "rt", "rt-multi-thread", "sync"]}
toml = "0.8.8" toml = "0.8.8"
walkdir = "2.4.0" walkdir = "2.4.0"

View file

@ -1,5 +1,6 @@
inactive_button_color = "#000060" inactive_button_color = "#000060"
active_button_color = "#eeffff" active_button_color = "#eeffff"
label_font_family = "Inter"
[buttons.0] [buttons.0]
key_page = "default" key_page = "default"

View file

@ -1,5 +1,5 @@
[keys.1x1] [keys.1x1]
icon = "@apps/spotify[scale=2.0|grayscale]" icon = "@apps/spotify[scale=2.0|invert]"
mode.vibrate.pattern = "low" mode.vibrate.pattern = "low"
mode.media__play_pause.icon.paused = "@ph/play" mode.media__play_pause.icon.paused = "@ph/play"
mode.media__play_pause.icon.playing = "@ph/pause" mode.media__play_pause.icon.playing = "@ph/pause"
@ -17,14 +17,15 @@ mode.timer.vibrate_when_finished = true
mode.timer.needy = true mode.timer.needy = true
[keys.3x3] [keys.3x3]
icon = "@fad/thunderbolt[border=#00ff00]" icon = "@fad/thunderbolt"
label = "Dock" label = "Dock"
border= "#00ff00"
mode.vibrate.pattern = "low" mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock" mode.home_assistant__switch.name = "switch.moritz_thunderbolt_dock"
mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]" mode.home_assistant__switch.icon.on = "@fad/thunderbolt[color=#58fc11]"
[keys.4x3] [keys.4x3]
icon = "@ph/computer-tower[border=#00ff00]" icon = "@ph/computer-tower"
label = "Tower PC unnötig lang" label = "Tower PC unnötig lang"
mode.vibrate.pattern = "low" mode.vibrate.pattern = "low"
mode.home_assistant__switch.name = "switch.mwin" mode.home_assistant__switch.name = "switch.mwin"

View file

@ -1,20 +1,20 @@
[keys.1x4] [keys.4x1]
label = "9" label = "9"
mode.keyboard.key = "9" mode.keyboard.key = "9"
[keys.1x3] [keys.3x1]
label = "8" label = "8"
mode.keyboard.key = "8" mode.keyboard.key = "8"
[keys.1x2] [keys.2x1]
label = "7" label = "7"
mode.keyboard.key = "7" mode.keyboard.key = "7"
[keys.2x4] [keys.4x2]
label = "6" label = "6"
mode.keyboard.key = "6" mode.keyboard.key = "6"
[keys.2x3] [keys.3x2]
label = "5" label = "5"
mode.keyboard.key = "5" mode.keyboard.key = "5"
@ -22,7 +22,7 @@ mode.keyboard.key = "5"
label = "4" label = "4"
mode.keyboard.key = "4" mode.keyboard.key = "4"
[keys.3x4] [keys.4x3]
label = "3" label = "3"
mode.keyboard.key = "3" mode.keyboard.key = "3"
@ -30,6 +30,6 @@ mode.keyboard.key = "3"
label = "2" label = "2"
mode.keyboard.key = "2" mode.keyboard.key = "2"
[keys.3x2] [keys.2x3]
label = "1" label = "1"
mode.keyboard.key = "1" mode.keyboard.key = "1"

View file

@ -14,7 +14,7 @@ use crate::model::{key_page, knob_page};
mod destructive_filter; mod destructive_filter;
type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>; pub type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>;
#[derive(Debug)] #[derive(Debug)]
pub struct LoadedIcon { pub struct LoadedIcon {
@ -125,36 +125,36 @@ pub fn load_icons(
for descriptor in descriptors { for descriptor in descriptors {
let icon_pack = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source { 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 { } else {
None None
}; };
let filter = if let Some(global_filter) = icon_pack.map(|p| p.global_filter).flatten() { let filter = if let Some(global_filter) = icon_pack.and_then(|p| p.global_filter.as_ref()) {
descriptor.filter.destructive.combine_after(&global_filter.destructive) descriptor.filter.destructive.merge_over(&global_filter.destructive)
} else { } else {
descriptor.filter.destructive descriptor.filter.destructive
}; };
let id = (descriptor.source, filter); let id = (descriptor.source, filter);
if descriptor.source == IconDescriptorSource::None || icons.contains(&id) { if icons.contains_key(&id) {
continue; 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::Occupied(o) => o.into_mut(),
Entry::Vacant(v) => v.insert(read_image_and_get_scale( Entry::Vacant(v) => v.insert(read_image_and_get_scale(
config_directory, config_directory,
icon_packs_by_id,
dpi, dpi,
&fonts_db, &fonts_db,
descriptor.source.clone(), &id.0,
highest_scale_by_source[&descriptor.source], 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( icons.insert(
id, id,
@ -170,24 +170,22 @@ pub fn load_icons(
fn read_image_and_get_scale( fn read_image_and_get_scale(
config_directory: &Path, config_directory: &Path,
icon_packs_by_id: &HashMap<String, IconPack>,
dpi: f32, dpi: f32,
fonts_db: &resvg::usvg::fontdb::Database, fonts_db: &resvg::usvg::fontdb::Database,
source: IconDescriptorSource, source: &IconDescriptorSource,
icon_pack: Option<&IconPack>,
highest_scale: f32, highest_scale: f32,
) -> Result<(Pixmap, f32)> { ) -> Result<(Pixmap, f32)> {
let path = match source { let path = match source {
IconDescriptorSource::None => return Ok((Pixmap::new(1, 1).unwrap(), 1.0)), IconDescriptorSource::Path(path) => path.clone(),
IconDescriptorSource::Path(path) => path, IconDescriptorSource::IconPack { icon_id, .. } => {
IconDescriptorSource::IconPack { pack_id, icon_id } => { let icon_pack = icon_pack.unwrap();
let pack = icon_packs_by_id.get(&pack_id).wrap_err_with(|| format!("Unknown icon pack: @{}", pack_id))?; let extension = match icon_pack.format {
let extension = match pack.format {
IconFormat::Png => "png", IconFormat::Png => "png",
IconFormat::Svg => "svg", IconFormat::Svg => "svg",
}; };
config_directory.join(&pack.path).join(icon_id + "." + extension) config_directory.join(&icon_pack.path).join(icon_id.to_owned() + "." + extension)
} }
}; };

View file

@ -61,13 +61,14 @@ pub async fn main() -> Result<()> {
.collect(); .collect();
let config = model::config::Config { 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, key_pages_by_id,
knob_pages_by_id, knob_pages_by_id,
buttons: deckster_file.buttons.into_iter().collect(), buttons: deckster_file.buttons.into_iter().collect(),
icon_packs: deckster_file.icon_packs, icon_packs: deckster_file.icon_packs,
initial: deckster_file.initial, initial: deckster_file.initial,
active_button_color: deckster_file.active_button_color,
inactive_button_color: deckster_file.inactive_button_color,
} }
.validate()?; .validate()?;

View file

@ -8,16 +8,17 @@ use serde::{Deserialize, Serialize};
use crate::model; use crate::model;
use crate::model::image_filter::ImageFilter; use crate::model::image_filter::ImageFilter;
use crate::model::position::ButtonPosition;
use crate::model::rgb::RGB8Wrapper; use crate::model::rgb::RGB8Wrapper;
use crate::model::ButtonPosition;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct File { pub struct File {
pub icon_packs: HashMap<String, IconPack>,
#[serde(default = "inactive_button_color_default")]
pub inactive_button_color: RGB8Wrapper,
#[serde(default = "active_button_color_default")] #[serde(default = "active_button_color_default")]
pub active_button_color: RGB8Wrapper, pub active_button_color: RGB8Wrapper,
#[serde(default = "inactive_button_color_default")]
pub inactive_button_color: RGB8Wrapper,
pub label_font_family: Option<String>,
pub icon_packs: HashMap<String, IconPack>,
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
pub initial: InitialConfig, pub initial: InitialConfig,
} }
@ -30,11 +31,12 @@ pub struct WithFallbackId<T> {
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub active_button_color: RGB8Wrapper,
pub inactive_button_color: RGB8Wrapper,
pub label_font_family: Option<String>,
pub key_pages_by_id: HashMap<String, model::key_page::Page>, pub key_pages_by_id: HashMap<String, model::key_page::Page>,
pub knob_pages_by_id: HashMap<String, model::knob_page::Page>, pub knob_pages_by_id: HashMap<String, model::knob_page::Page>,
pub icon_packs: HashMap<String, IconPack>, pub icon_packs: HashMap<String, IconPack>,
pub inactive_button_color: RGB8Wrapper,
pub active_button_color: RGB8Wrapper,
pub buttons: EnumMap<ButtonPosition, ButtonConfig>, pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
pub initial: InitialConfig, pub initial: InitialConfig,
} }

View file

@ -1,4 +1,4 @@
use std::fmt::{Display, Formatter, Write}; use std::fmt::{Display, Formatter};
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
@ -8,7 +8,7 @@ use thiserror::Error;
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; 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 struct IconDescriptor {
pub source: IconDescriptorSource, pub source: IconDescriptorSource,
pub filter: ImageFilter, 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 { pub enum IconDescriptorSource {
#[default] IconPack { pack_id: String, icon_id: String },
None,
IconPack {
pack_id: String,
icon_id: String,
},
Path(PathBuf), Path(PathBuf),
} }
@ -88,7 +83,6 @@ impl IconDescriptorSource {
impl Display for IconDescriptorSource { impl Display for IconDescriptorSource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
IconDescriptorSource::None => f.write_str("<none>"),
IconDescriptorSource::IconPack { pack_id, icon_id } => f.write_fmt(format_args!("@{}/{}", pack_id, icon_id)), 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()), IconDescriptorSource::Path(path) => f.write_str(&path.to_string_lossy()),
} }

View file

@ -5,7 +5,6 @@ use std::str::FromStr;
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error; use thiserror::Error;
use crate::model::geometry::IntRectWrapper;
use crate::model::rgb::RGB8Wrapper; use crate::model::rgb::RGB8Wrapper;
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
@ -17,7 +16,7 @@ pub struct ImageFilterTransform {
} }
impl ImageFilterTransform { impl ImageFilterTransform {
pub fn combine_after(&self, other: &ImageFilterTransform) -> ImageFilterTransform { pub fn merge_over(&self, other: &ImageFilterTransform) -> ImageFilterTransform {
ImageFilterTransform { ImageFilterTransform {
scale: self.scale * other.scale, scale: self.scale * other.scale,
alpha: self.alpha * other.alpha, 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 struct ImageFilterDestructive {
pub color: Option<RGB8Wrapper>, pub color: Option<RGB8Wrapper>,
pub grayscale: bool, pub grayscale: bool,
@ -34,7 +33,7 @@ pub struct ImageFilterDestructive {
} }
impl ImageFilterDestructive { impl ImageFilterDestructive {
pub fn combine_after(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive { pub fn merge_over(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive {
ImageFilterDestructive { ImageFilterDestructive {
color: self.color.or(other.color), color: self.color.or(other.color),
grayscale: self.grayscale || other.grayscale, grayscale: self.grayscale || other.grayscale,
@ -49,6 +48,15 @@ pub struct ImageFilter {
pub destructive: ImageFilterDestructive, 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 { impl Eq for ImageFilter {
fn assert_receiver_is_total_eq(&self) {} fn assert_receiver_is_total_eq(&self) {}
} }
@ -106,13 +114,6 @@ impl FromStr for ImageFilter {
type Err = ImageFilterFromStringError; type Err = ImageFilterFromStringError;
fn from_str<'a>(s: &str) -> Result<Self, Self::Err> { fn from_str<'a>(s: &str) -> Result<Self, Self::Err> {
fn parse_rect_filter_value(filter_name: &str, raw_value: String) -> Result<IntRectWrapper, ImageFilterFromStringError> {
IntRectWrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})
}
fn parse_f32_filter_value( fn parse_f32_filter_value(
filter_name: &str, filter_name: &str,
raw_value: String, raw_value: String,
@ -135,11 +136,15 @@ impl FromStr for ImageFilter {
Ok(value) Ok(value)
} }
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).collect();
let mut result = ImageFilter::default(); let mut result = ImageFilter::default();
let mut previous_filter_names: Vec<String> = Vec::new(); let mut previous_filter_names: Vec<String> = 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 { for filter in filters {
let split_filter = filter.split_once('='); let split_filter = filter.split_once('=');
let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter { let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter {

View file

@ -1,6 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::model::geometry::UIntVec2; use crate::model::geometry::UIntVec2;
use crate::model::icon_descriptor::IconDescriptor; use crate::model::icon_descriptor::IconDescriptor;
@ -45,7 +45,7 @@ pub enum ScrollingConfigAxis {
Horizontal, Horizontal,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyStyle { pub struct KeyStyle {
pub label: Option<String>, pub label: Option<String>,
pub icon: Option<IconDescriptor>, pub icon: Option<IconDescriptor>,
@ -55,8 +55,8 @@ pub struct KeyStyle {
impl KeyStyle { impl KeyStyle {
pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle { pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle {
KeyStyle { KeyStyle {
label: self.label.or_else(|| base.label.clone()), label: self.label.as_ref().or(base.label.as_ref()).cloned(),
icon: self.icon.or(base.icon), icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
border: self.border.or(base.border), border: self.border.or(base.border),
} }
} }

View file

@ -28,25 +28,25 @@ pub struct Knob {
pub mode: KnobModes, pub mode: KnobModes,
} }
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
pub struct KnobIndicators { pub struct KnobIndicators {
pub bar: Option<KnobIndicatorBarConfig>, pub bar: Option<KnobIndicatorBarConfig>,
pub circle: Option<KnobIndicatorCircleConfig>, pub circle: Option<KnobIndicatorCircleConfig>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct KnobIndicatorBarConfig { pub struct KnobIndicatorBarConfig {
pub color: Option<RGB8WithOptionalA>, pub color: Option<RGB8WithOptionalA>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct KnobIndicatorCircleConfig { pub struct KnobIndicatorCircleConfig {
pub color: Option<RGB8WithOptionalA>, pub color: Option<RGB8WithOptionalA>,
pub width: Option<u8>, pub width: Option<u8>,
pub radius: Option<u8>, pub radius: Option<u8>,
} }
#[derive(Debug, Default, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
pub struct KnobStyle { pub struct KnobStyle {
pub label: Option<String>, pub label: Option<String>,
pub icon: Option<IconDescriptor>, pub icon: Option<IconDescriptor>,

View file

@ -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); pub struct RGB8Wrapper(RGB8);
impl FromStr for RGB8Wrapper { 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)] #[repr(transparent)]
pub struct RGBA8Wrapper(pub RGBA8); 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)] #[repr(transparent)]
pub struct RGB8WithOptionalA(RGBA8); pub struct RGB8WithOptionalA(RGBA8);

View file

@ -9,18 +9,17 @@ use tiny_skia::{
use loupedeck_serial::util::Endianness; use loupedeck_serial::util::Endianness;
use crate::icons::LoadedIcon; use crate::icons::LoadedIconsMap;
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; use crate::model::image_filter::ImageFilter;
use crate::model::image_filter::ImageFilterTransform;
use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::state::Key; use crate::runner::state::Key;
#[derive(Debug)] #[derive(Debug)]
struct GraphicsContext { pub struct GraphicsContext {
label_renderer: RefCell<LabelRenderer>, pub label_renderer: RefCell<LabelRenderer>,
buffer_endianness: Endianness, pub buffer_endianness: Endianness,
global_icon_filter_by_pack_id: HashMap<String, ImageFilterTransform>, pub global_icon_filter_by_pack_id: HashMap<String, ImageFilter>,
loaded_icons: HashMap<IconDescriptor, LoadedIcon>, pub loaded_icons: LoadedIconsMap,
} }
pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes { 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 { if let Some(state) = state {
let style = state.style.merge_over(&state.base_style); let style = state.style.merge_over(&state.base_style);
if style.icon.source != IconDescriptorSource::None { if let Some(icon) = style.icon {
let loaded_icon = &context.loaded_icons[&state.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)
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 { } 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()) let scaled_size = IntSize::from_wh(loaded_icon.pixmap.width(), loaded_icon.pixmap.height())
.unwrap() .unwrap()
.scale_by(scale) .scale_by(scale)
.unwrap(); .unwrap();
static PAINT: PixmapPaint = PixmapPaint {
opacity: 1.0,
blend_mode: BlendMode::SourceOver,
quality: FilterQuality::Bicubic,
};
pixmap.draw_pixmap( pixmap.draw_pixmap(
(((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32, (((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, (((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / scale).round() as i32,
loaded_icon.pixmap.as_ref(), 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( 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.width() as f32 / 2.0,
key_size.height() 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() { if let Some(label) = style.label {
context.label_renderer.borrow_mut().render(&mut pixmap, &state.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 path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap());
let paint = Paint { 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() ..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 { 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 { pub mod labels {
use cosmic_text::{Align, Attrs, AttrsList, Buffer, BufferLine, FontSystem, Metrics, Shaping, SwashCache}; 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}; use tiny_skia::{Color, Paint, Pixmap, Rect, Shader, Transform};
#[derive(Debug)]
pub struct LabelRenderer { pub struct LabelRenderer {
font_system: FontSystem, font_system: FontSystem,
swash_cache: SwashCache, swash_cache: SwashCache,
@ -123,8 +124,12 @@ pub mod labels {
} }
impl LabelRenderer { impl LabelRenderer {
pub fn new() -> Self { pub fn new(font_family: Option<&String>) -> Self {
let mut font_system = FontSystem::new(); 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)); let buffer = Buffer::new(&mut font_system, Metrics::new(11.0, 11.0));
LabelRenderer { LabelRenderer {
@ -135,8 +140,7 @@ pub mod labels {
} }
pub fn render(&mut self, pixmap: &mut Pixmap, text: &String) { pub fn render(&mut self, pixmap: &mut Pixmap, text: &String) {
let attrs = Attrs::new(); let mut line = BufferLine::new(text, AttrsList::new(Attrs::new()), Shaping::Advanced);
let mut line = BufferLine::new(text, AttrsList::new(attrs), Shaping::Advanced);
line.set_align(Some(Align::Center)); line.set_align(Some(Align::Center));
self.buffer.lines.clear(); self.buffer.lines.clear();
@ -149,6 +153,9 @@ pub mod labels {
pixmap.height() as f32 - PADDING * 2.0, 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.shape_until_scroll(&mut self.font_system);
self.buffer.draw( self.buffer.draw(

View file

@ -19,12 +19,13 @@ use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::LoupedeckEvent; 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;
use crate::model::icon_descriptor::IconDescriptor; use crate::model::key_page::KeyStyle;
use crate::model::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; use crate::model::knob_page::KnobStyle;
use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath};
use crate::runner::graphics::labels::LabelRenderer; 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}; use crate::runner::state::{Key, State, StateChangeCommand};
mod graphics; mod graphics;
@ -91,8 +92,8 @@ fn create_state(config: &model::config::Config) -> State {
page_id: p.id.clone(), page_id: p.id.clone(),
position: *position, position: *position,
}, },
label: k.label.clone().unwrap_or_default(), base_style: k.base_style.clone(),
icon: k.icon.clone().unwrap_or_default(), style: KeyStyle::default(),
}) })
.map(|k| (k.path.position, k)) .map(|k| (k.path.position, k))
.collect(), .collect(),
@ -113,8 +114,8 @@ fn create_state(config: &model::config::Config) -> State {
page_id: p.id.clone(), page_id: p.id.clone(),
position, position,
}, },
label: knob_config.label.clone(), base_style: knob_config.base_style.clone(),
icon: knob_config.icon.clone(), style: KnobStyle::default(),
value: 0.0, value: 0.0,
} }
}), }),
@ -137,27 +138,39 @@ enum IoWork {
struct IoWorkerContext { struct IoWorkerContext {
config: Arc<model::config::Config>, config: Arc<model::config::Config>,
icons: HashMap<IconDescriptor, LoadedIcon>,
label_renderer: RefCell<LabelRenderer>,
device: LoupedeckDevice, device: LoupedeckDevice,
state: State, state: State,
graphics: GraphicsContext,
} }
fn do_io_work( fn do_io_work(
config: Arc<model::config::Config>, config: Arc<model::config::Config>,
icons: HashMap<IconDescriptor, LoadedIcon>, icons: LoadedIconsMap,
device: LoupedeckDevice, device: LoupedeckDevice,
events_receiver: Receiver<LoupedeckEvent>, events_receiver: Receiver<LoupedeckEvent>,
commands_sender: Sender<StateChangeCommand>, commands_sender: Sender<StateChangeCommand>,
commands_receiver: Receiver<StateChangeCommand>, commands_receiver: Receiver<StateChangeCommand>,
) { ) {
let state = create_state(&config); 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 { let mut context = IoWorkerContext {
config, config,
icons,
label_renderer: RefCell::new(LabelRenderer::new()),
device, device,
state, state,
graphics: GraphicsContext {
loaded_icons: icons,
buffer_endianness,
label_renderer,
global_icon_filter_by_pack_id,
},
}; };
loop { loop {
@ -243,26 +256,10 @@ fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) {
context.device.refresh_display(&key_grid.display).unwrap(); context.device.refresh_display(&key_grid.display).unwrap();
} }
StateChangeCommand::SetKeyLabel { path, value } => { StateChangeCommand::SetKeyStyle { path, value } => {
context.state.mutate_key_for_command( context.state.mutate_key_for_command("SetKeyStyle", &path, |k| {
"SetKeyLabel", k.style = value;
&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;
}),
);
draw_key_at_path_if_visible(context, path); draw_key_at_path_if_visible(context, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); 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 key_grid = &context.device.characteristics().key_grid;
let (x, y, w, h) = key_grid.get_local_key_rect_xywh(index).unwrap(); let (x, y, w, h) = key_grid.get_local_key_rect_xywh(index).unwrap();
let p = render_key( let p = render_key(&context.graphics, IntSize::from_wh(w as u32, h as u32).unwrap(), key);
&mut context.label_renderer.borrow_mut(),
IntSize::from_wh(w as u32, h as u32).unwrap(),
key_grid.display.endianness,
&context.icons,
key,
);
context.device.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, p).unwrap(); context.device.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, p).unwrap();
} }

View file

@ -4,8 +4,8 @@ use enum_map::EnumMap;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::key_page::KeyStyle; use crate::model::key_page::KeyStyle;
use crate::model::knob_page::KnobStyle;
use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition}; use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition};
#[derive(Debug)] #[derive(Debug)]
@ -17,7 +17,7 @@ pub struct State {
} }
impl State { impl State {
pub fn mutate_key_for_command<R>(&mut self, command_name: &'static str, path: &KeyPath, mutator: Box<dyn FnOnce(&mut Key) -> R>) -> Option<R> { pub fn mutate_key_for_command<R>(&mut self, command_name: &'static str, path: &KeyPath, mutator: impl FnOnce(&mut Key) -> R) -> Option<R> {
match self.key_pages_by_id.get_mut(&path.page_id) { 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), 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) { Some(key_page) => match key_page.keys_by_position.get_mut(&path.position) {
@ -64,8 +64,8 @@ pub struct Key {
#[derive(Debug)] #[derive(Debug)]
pub struct Knob { pub struct Knob {
pub path: KnobPath, pub path: KnobPath,
pub icon: IconDescriptor, pub base_style: KnobStyle,
pub label: String, pub style: KnobStyle,
pub value: f32, pub value: f32,
} }
@ -73,6 +73,5 @@ pub struct Knob {
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
pub enum StateChangeCommand { pub enum StateChangeCommand {
SetActivePages { key_page_id: String, knob_page_id: String }, SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyLabel { path: KeyPath, value: String }, SetKeyStyle { path: KeyPath, value: KeyStyle },
SetKeyIcon { path: KeyPath, value: IconDescriptor },
} }