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",
"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"

View file

@ -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"

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -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)
}
};

View file

@ -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()?;

View file

@ -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,
}

View file

@ -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()),
}

View file

@ -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 {

View file

@ -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),
}
}

View file

@ -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>,

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);
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);

View file

@ -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(

View file

@ -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();
}

View file

@ -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 },
}