commit
This commit is contained in:
parent
e7461a07c2
commit
2719b7afb8
16 changed files with 157 additions and 149 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
walkdir = "2.4.0"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
inactive_button_color = "#000060"
|
||||
active_button_color = "#eeffff"
|
||||
label_font_family = "Inter"
|
||||
|
||||
[buttons.0]
|
||||
key_page = "default"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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<String, IconPack>,
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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<String, IconPack>,
|
||||
#[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<String>,
|
||||
pub icon_packs: HashMap<String, IconPack>,
|
||||
pub buttons: HashMap<ButtonPosition, ButtonConfig>, // EnumMap
|
||||
pub initial: InitialConfig,
|
||||
}
|
||||
|
@ -30,11 +31,12 @@ pub struct WithFallbackId<T> {
|
|||
|
||||
#[derive(Debug)]
|
||||
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 knob_pages_by_id: HashMap<String, model::knob_page::Page>,
|
||||
pub icon_packs: HashMap<String, IconPack>,
|
||||
pub inactive_button_color: RGB8Wrapper,
|
||||
pub active_button_color: RGB8Wrapper,
|
||||
pub buttons: EnumMap<ButtonPosition, ButtonConfig>,
|
||||
pub initial: InitialConfig,
|
||||
}
|
||||
|
|
|
@ -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("<none>"),
|
||||
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()),
|
||||
}
|
||||
|
|
|
@ -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<RGB8Wrapper>,
|
||||
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<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(
|
||||
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<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 {
|
||||
let split_filter = filter.split_once('=');
|
||||
let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter {
|
||||
|
|
|
@ -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<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<KnobIndicatorBarConfig>,
|
||||
pub circle: Option<KnobIndicatorCircleConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct KnobIndicatorBarConfig {
|
||||
pub color: Option<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct KnobIndicatorCircleConfig {
|
||||
pub color: Option<RGB8WithOptionalA>,
|
||||
pub width: Option<u8>,
|
||||
pub radius: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
pub struct KnobStyle {
|
||||
pub label: Option<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<LabelRenderer>,
|
||||
buffer_endianness: Endianness,
|
||||
global_icon_filter_by_pack_id: HashMap<String, ImageFilterTransform>,
|
||||
loaded_icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||
pub struct GraphicsContext {
|
||||
pub label_renderer: RefCell<LabelRenderer>,
|
||||
pub buffer_endianness: Endianness,
|
||||
pub global_icon_filter_by_pack_id: HashMap<String, ImageFilter>,
|
||||
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(
|
||||
|
|
|
@ -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<model::config::Config>,
|
||||
icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||
label_renderer: RefCell<LabelRenderer>,
|
||||
device: LoupedeckDevice,
|
||||
state: State,
|
||||
graphics: GraphicsContext,
|
||||
}
|
||||
|
||||
fn do_io_work(
|
||||
config: Arc<model::config::Config>,
|
||||
icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||
icons: LoadedIconsMap,
|
||||
device: LoupedeckDevice,
|
||||
events_receiver: Receiver<LoupedeckEvent>,
|
||||
commands_sender: Sender<StateChangeCommand>,
|
||||
commands_receiver: Receiver<StateChangeCommand>,
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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) {
|
||||
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 },
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue