diff --git a/Cargo.lock b/Cargo.lock index 811a844..fd3907e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,7 +76,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -86,9 +86,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys", + "windows-sys 0.52.0", ] +[[package]] +name = "anyhow" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" + [[package]] name = "arrayref" version = "0.3.7" @@ -128,6 +134,26 @@ version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +[[package]] +name = "bindgen" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" +dependencies = [ + "bitflags 2.4.1", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.48", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -167,6 +193,25 @@ dependencies = [ "libc", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-expr" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6100bc57b6209840798d95cb2775684849d332f7bd788db2a8c8caf7ef82a41a" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -186,6 +231,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.4.13" @@ -271,6 +327,21 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie-factory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" + [[package]] name = "core-foundation-sys" version = "0.8.6" @@ -366,6 +437,7 @@ dependencies = [ "log", "loupedeck_serial", "once_cell", + "pipewire", "regex", "resvg", "rgb", @@ -395,7 +467,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -495,7 +567,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -629,6 +701,12 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "hashbrown" version = "0.12.3" @@ -756,7 +834,7 @@ checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -795,18 +873,62 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libc" version = "0.2.151" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "libm" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +[[package]] +name = "libspa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0434617020ddca18b86067912970c55410ca654cdafd775480322f50b857a8c4" +dependencies = [ + "bitflags 2.4.1", + "cc", + "convert_case 0.6.0", + "cookie-factory", + "libc", + "libspa-sys", + "nix", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e70ca3f3e70f858ef363046d06178c427b4e0b63d210c95fd87d752679d345" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + [[package]] name = "libudev" version = "0.3.0" @@ -896,6 +1018,21 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -924,6 +1061,18 @@ dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", + "memoffset", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", ] [[package]] @@ -989,6 +1138,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + [[package]] name = "pico-args" version = "0.5.0" @@ -1001,6 +1156,40 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pipewire" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d009c8dd65e890b515a71950f7e4c801523b8894ff33863a40830bf762e9e9" +dependencies = [ + "anyhow", + "bitflags 2.4.1", + "libc", + "libspa", + "libspa-sys", + "nix", + "once_cell", + "pipewire-sys", + "thiserror", +] + +[[package]] +name = "pipewire-sys" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "890c084e7b737246cb4799c86b71a0e4da536031ff7473dd639eba9f95039f64" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.28" @@ -1166,7 +1355,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1342,6 +1531,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + [[package]] name = "simd-adler32" version = "0.3.7" @@ -1453,6 +1648,25 @@ dependencies = [ "libc", ] +[[package]] +name = "system-deps" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69758bda2e78f098e4ccb393021a0963bb3442eac05f135c30f61b7370bbafae" + [[package]] name = "termcolor" version = "1.4.0" @@ -1809,6 +2023,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + [[package]] name = "version_check" version = "0.9.4" @@ -1931,6 +2151,15 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/deckster/Cargo.toml b/deckster/Cargo.toml index 713e51c..7f6abde 100644 --- a/deckster/Cargo.toml +++ b/deckster/Cargo.toml @@ -28,4 +28,5 @@ 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" -once_cell = "1.19.0" \ No newline at end of file +once_cell = "1.19.0" +pipewire = "0.7.2" \ No newline at end of file diff --git a/deckster/examples/full/knob-pages/default.toml b/deckster/examples/full/knob-pages/default.toml index fe82365..bebc1a2 100644 --- a/deckster/examples/full/knob-pages/default.toml +++ b/deckster/examples/full/knob-pages/default.toml @@ -1,32 +1,32 @@ -[knobs.left-top] -icon = "@ph/microphone-light" +[knobs.right-top] +icon = "@ph/microphone-light[scale=0.9]" indicator.circle.color = "#ffffff" indicator.circle.width = 2 indicator.circle.radius = 40 mode.audio_volume.direction = "input" mode.audio_volume.regex = "Microphone" -mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@ph/microphone-slash-light[alpha=0.9|color=#fc4646]" mode.audio_volume.disable_press_to_unmute = true mode.audio_volume.muted_turn_action = "unmute-at-zero" +mode.audio_volume.style.muted.label = "Muted" +mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[alpha=0.9|color=#fc4646]" -[knobs.left-middle] -icon = "@apps/discord" +[knobs.right-middle] +icon = "@apps/discord[scale=0.25]" indicator.bar.color = "#ffffff" mode.audio_volume.regex = "Discord" -mode.audio_volume.label.inactive = "" -mode.audio_volume.label.active = "{percentage}%" -mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@apps/discord[grayscale|alpha=0.9]" +mode.audio_volume.style.active.label = "{percentage}%" +mode.audio_volume.style.muted.label = "Muted" +mode.audio_volume.style.inactive.label = "" +mode.audio_volume.style.inactive.icon = "@apps/discord[grayscale|alpha=0.9]" -[knobs.left-bottom] -icon = "@apps/spotify" +[knobs.right-bottom] +icon = "@apps/spotify[scale=1.1]" indicator.bar.color = "#ffffff" mode.audio_volume.regex = "Spotify" -mode.audio_volume.label.inactive = "" -mode.audio_volume.label.active = "{percentage}%" -mode.audio_volume.label.muted = "Muted" -mode.audio_volume.icon.inactive = "@apps/spotify[grayscale|alpha=0.9]" \ No newline at end of file +mode.audio_volume.style.active.label = "{percentage}%" +mode.audio_volume.style.muted.label = "Muted" +mode.audio_volume.style.inactive.label = "" +mode.audio_volume.style.inactive.icon = "@apps/spotify[grayscale|alpha=0.9]" \ No newline at end of file diff --git a/deckster/src/icons/mod.rs b/deckster/src/icons/mod.rs index a867999..fdd000c 100644 --- a/deckster/src/icons/mod.rs +++ b/deckster/src/icons/mod.rs @@ -4,12 +4,13 @@ use std::path::Path; use color_eyre::eyre::{eyre, ContextCompat, WrapErr}; use color_eyre::Result; +use resvg::usvg::tiny_skia_path::IntSize; use resvg::usvg::{TextRendering, TreeParsing, TreeTextToPath}; -use tiny_skia::{Pixmap, Transform}; +use tiny_skia::{BlendMode, FilterQuality, Pixmap, PixmapPaint, Transform}; use crate::model::config::{Config, IconFormat, IconPack}; use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource}; -use crate::model::image_filter::ImageFilterDestructive; +use crate::model::image_filter::{ImageFilter, ImageFilterDestructive}; use crate::model::{key_page, knob_page}; mod destructive_filter; @@ -225,3 +226,37 @@ fn read_image_from_svg(path: &Path, dpi: f32, font_db: &resvg::usvg::fontdb::Dat Ok(pixmap) } + +pub fn render_icon_in(pixmap: &mut Pixmap, global_icon_filter_by_pack_id: &HashMap, loaded_icons: &LoadedIconsMap, icon: &IconDescriptor) { + let filter = if let Some(global_filter) = icon.source.pack_id().and_then(|i| global_icon_filter_by_pack_id.get(i)) { + icon.filter.merge_over(global_filter) + } else { + icon.filter.clone() + }; + + let loaded_icon = &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(); + + pixmap.draw_pixmap( + (((pixmap.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32, + (((pixmap.height() as i32 - scaled_size.height() as i32) / 2) as f32 / scale).round() as i32, + loaded_icon.pixmap.as_ref(), + &PixmapPaint { + opacity: filter.transform.alpha, + blend_mode: BlendMode::SourceOver, + quality: FilterQuality::Bicubic, + }, + Transform::from_scale(scale, scale).post_rotate_at( + (filter.transform.clockwise_quarter_rotations as f32) * 90.0, + pixmap.width() as f32 / 2.0, + pixmap.height() as f32 / 2.0, + ), + None, + ); +} diff --git a/deckster/src/main.rs b/deckster/src/main.rs index fd5bb89..f83e0b9 100644 --- a/deckster/src/main.rs +++ b/deckster/src/main.rs @@ -57,7 +57,7 @@ pub async fn main() -> Result<()> { .into_iter() .map(|p| model::knob_page::Page { id: p.inner.id.clone().unwrap_or(p.fallback_id), - knobs: p.inner.knobs.into_iter().collect(), + knobs: p.inner.knobs.into_iter().map(|(p, k)| (p, Arc::new(k))).collect(), }) .map(|p| (p.id.clone(), p)) .collect(); diff --git a/deckster/src/model/key_page.rs b/deckster/src/model/key_page.rs index 9767fa8..6851f56 100644 --- a/deckster/src/model/key_page.rs +++ b/deckster/src/model/key_page.rs @@ -13,14 +13,14 @@ use crate::modes; pub struct File { pub id: Option, pub scrolling: Option, - pub keys: HashMap, + pub keys: HashMap, } #[derive(Debug)] pub struct Page { pub id: String, pub scrolling: Option, - pub keys: HashMap>, + pub keys: HashMap>, } #[derive(Debug, Deserialize)] @@ -64,7 +64,7 @@ impl KeyStyle { } #[derive(Debug, Deserialize)] -pub struct KeyConfig { +pub struct Key { #[serde(default, flatten)] pub base_style: KeyStyle, diff --git a/deckster/src/model/knob_page.rs b/deckster/src/model/knob_page.rs index b7eb924..0992bc6 100644 --- a/deckster/src/model/knob_page.rs +++ b/deckster/src/model/knob_page.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; +use std::sync::Arc; use enum_map::EnumMap; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::model::icon_descriptor::IconDescriptor; use crate::model::position::KnobPosition; @@ -17,7 +18,7 @@ pub struct File { #[derive(Debug)] pub struct Page { pub id: String, - pub knobs: EnumMap, + pub knobs: EnumMap>, } #[derive(Debug, Default, Deserialize)] @@ -28,34 +29,89 @@ pub struct Knob { pub mode: KnobModes, } -#[derive(Debug, Default, Clone, Deserialize)] +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct KnobIndicators { pub bar: Option, pub circle: Option, } -#[derive(Debug, Clone, Deserialize)] +impl KnobIndicators { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + bar: self + .bar + .as_ref() + .zip(base.bar.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.bar.clone()) + .or_else(|| base.bar.clone()), + circle: self + .circle + .as_ref() + .zip(base.circle.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.circle.clone()) + .or_else(|| base.circle.clone()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KnobIndicatorBarConfig { pub color: Option, } -#[derive(Debug, Clone, Deserialize)] +impl KnobIndicatorBarConfig { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + color: self.color.as_ref().or(base.color.as_ref()).cloned(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct KnobIndicatorCircleConfig { pub color: Option, pub width: Option, pub radius: Option, } -#[derive(Debug, Default, Clone, Deserialize)] +impl KnobIndicatorCircleConfig { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + color: self.color.as_ref().or(base.color.as_ref()).cloned(), + width: self.width.as_ref().or(base.width.as_ref()).cloned(), + radius: self.radius.as_ref().or(base.radius.as_ref()).cloned(), + } + } +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] pub struct KnobStyle { pub label: Option, pub icon: Option, pub indicators: Option, } +impl KnobStyle { + pub fn merge_over(&self, base: &Self) -> Self { + Self { + label: self.label.as_ref().or(base.label.as_ref()).cloned(), + icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(), + indicators: self + .indicators + .as_ref() + .zip(base.indicators.as_ref()) + .map(|(a, b)| a.merge_over(b)) + .or_else(|| self.indicators.clone()) + .or_else(|| base.indicators.clone()), + } + } +} + #[derive(Debug, Default, Deserialize)] pub struct KnobModes { - pub audio_volume: Option, + pub audio_volume: Option>, } pub type StyleByStateMap = HashMap; diff --git a/deckster/src/model/position.rs b/deckster/src/model/position.rs index cafdc67..2e11108 100644 --- a/deckster/src/model/position.rs +++ b/deckster/src/model/position.rs @@ -2,6 +2,7 @@ use std::fmt::{Display, Formatter}; use std::str::FromStr; use enum_map::Enum; +use enum_ordinalize::Ordinalize; use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use thiserror::Error; @@ -55,7 +56,7 @@ impl Display for KeyPath { } } -#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)] +#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum, Ordinalize)] #[serde(rename_all = "kebab-case")] pub enum KnobPosition { LeftTop, diff --git a/deckster/src/modes/key/mod.rs b/deckster/src/modes/key/mod.rs index 24eb533..a709464 100644 --- a/deckster/src/modes/key/mod.rs +++ b/deckster/src/modes/key/mod.rs @@ -28,7 +28,7 @@ pub enum KeyEvent { } pub fn start_handlers( - keys: impl Iterator)>, + keys: impl Iterator)>, events: broadcast::Sender<(KeyPath, KeyEvent)>, commands: flume::Sender, ) { diff --git a/deckster/src/modes/knob/audio_volume.rs b/deckster/src/modes/knob/audio_volume.rs index 4ab592e..bc7d8ea 100644 --- a/deckster/src/modes/knob/audio_volume.rs +++ b/deckster/src/modes/knob/audio_volume.rs @@ -1,9 +1,14 @@ use std::collections::HashMap; +use std::sync::Arc; use regex::Regex; use serde::Deserialize; +use tokio::sync::broadcast; use crate::model::knob_page::StyleByStateMap; +use crate::model::position::KnobPath; +use crate::modes::knob::KnobEvent; +use crate::runner::command::IoWorkerCommand; #[derive(Debug, Deserialize)] pub struct Config { @@ -47,3 +52,13 @@ pub enum State { Active, Muted, } + +pub async fn handle(path: KnobPath, config: Arc, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender) { + while let Ok((event_path, event)) = events.recv().await { + if event_path != path { + continue; + } + + dbg!(path.clone(), event); + } +} diff --git a/deckster/src/modes/knob/mod.rs b/deckster/src/modes/knob/mod.rs index 322ca72..c332cae 100644 --- a/deckster/src/modes/knob/mod.rs +++ b/deckster/src/modes/knob/mod.rs @@ -1 +1,37 @@ +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tokio::sync::broadcast; + +use crate::model; +use crate::model::position::KnobPath; +use crate::runner::command::IoWorkerCommand; + pub mod audio_volume; + +#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum RotationDirection { + Clockwise, + Counterclockwise, +} + +#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +pub enum KnobEvent { + Press, + ButtonDown, + ButtonUp, + Rotate { direction: RotationDirection }, + VisibilityChange { is_visible: bool }, +} + +pub fn start_handlers( + knobs: impl Iterator)>, + events: broadcast::Sender<(KnobPath, KnobEvent)>, + commands: flume::Sender, +) { + for (path, config) in knobs { + if let Some(c) = &config.mode.audio_volume { + tokio::spawn(audio_volume::handle(path.clone(), Arc::clone(c), events.subscribe(), commands.clone())); + } + } +} diff --git a/deckster/src/runner/command.rs b/deckster/src/runner/command.rs index 31e284b..283b044 100644 --- a/deckster/src/runner/command.rs +++ b/deckster/src/runner/command.rs @@ -3,11 +3,13 @@ use serde::{Deserialize, Serialize}; use loupedeck_serial::commands::VibrationPattern; use crate::model::key_page::KeyStyle; -use crate::model::position::KeyPath; +use crate::model::knob_page::KnobStyle; +use crate::model::position::{KeyPath, KnobPath}; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub enum IoWorkerCommand { Vibrate { pattern: VibrationPattern }, SetActivePages { key_page_id: String, knob_page_id: String }, SetKeyStyle { path: KeyPath, value: Option }, + SetKnobStyle { path: KnobPath, value: Option }, } diff --git a/deckster/src/runner/graphics.rs b/deckster/src/runner/graphics.rs index 76f6ce1..71df457 100644 --- a/deckster/src/runner/graphics.rs +++ b/deckster/src/runner/graphics.rs @@ -3,16 +3,14 @@ use std::collections::HashMap; use bytes::{BufMut, Bytes, BytesMut}; use resvg::usvg::tiny_skia_path::PathBuilder; -use tiny_skia::{ - BlendMode, Color, FilterQuality, IntSize, LineCap, LineJoin, Paint, Pixmap, PixmapPaint, PremultipliedColorU8, Rect, Shader, Stroke, Transform, -}; +use tiny_skia::{Color, IntSize, LineCap, LineJoin, Paint, Pixmap, PremultipliedColorU8, Rect, Shader, Stroke, Transform}; use loupedeck_serial::util::Endianness; -use crate::icons::LoadedIconsMap; +use crate::icons::{render_icon_in, LoadedIconsMap}; use crate::model::image_filter::ImageFilter; use crate::runner::graphics::labels::LabelRenderer; -use crate::runner::state::Key; +use crate::runner::state::{Key, Knob}; #[derive(Debug)] pub struct GraphicsContext { @@ -30,42 +28,12 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K let style = style.as_ref().unwrap_or(&state.base_style); 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 { - icon.filter.clone() - }; - - 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(); - - 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(), - &PixmapPaint { - opacity: filter.transform.alpha, - blend_mode: BlendMode::SourceOver, - quality: FilterQuality::Bicubic, - }, - Transform::from_scale(scale, scale).post_rotate_at( - (filter.transform.clockwise_quarter_rotations as f32) * 90.0, - key_size.width() as f32 / 2.0, - key_size.height() as f32 / 2.0, - ), - None, - ); + render_icon_in(&mut pixmap, &context.global_icon_filter_by_pack_id, &context.loaded_icons, icon); } if let Some(label) = &style.label { if !label.is_empty() { - context.label_renderer.borrow_mut().render(&mut pixmap, &label); + context.label_renderer.borrow_mut().render(&mut pixmap, label); } } @@ -92,6 +60,27 @@ pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&K convert_pixels_to_rgb565(pixmap.pixels(), context.buffer_endianness).freeze() } +pub fn render_knob(context: &GraphicsContext, screen_size: IntSize, state: Option<&Knob>) -> Bytes { + let mut pixmap = Pixmap::new(screen_size.width(), screen_size.height()).unwrap(); + + if let Some(state) = state { + let style = state.style.as_ref().map(|s| s.merge_over(&state.base_style)); + let style = style.as_ref().unwrap_or(&state.base_style); + + if let Some(icon) = &style.icon { + render_icon_in(&mut pixmap, &context.global_icon_filter_by_pack_id, &context.loaded_icons, icon); + } + + if let Some(label) = &style.label { + if !label.is_empty() { + context.label_renderer.borrow_mut().render(&mut pixmap, label); + } + } + } + + convert_pixels_to_rgb565(pixmap.pixels(), context.buffer_endianness).freeze() +} + fn convert_pixels_to_rgb565(pixels: &[PremultipliedColorU8], endianness: Endianness) -> BytesMut { let pixel_count = pixels.len(); diff --git a/deckster/src/runner/mod.rs b/deckster/src/runner/mod.rs index 99519bc..0b7d243 100644 --- a/deckster/src/runner/mod.rs +++ b/deckster/src/runner/mod.rs @@ -1,5 +1,4 @@ use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; use std::path::Path; use std::sync::Arc; use std::thread; @@ -7,7 +6,6 @@ use std::time::Instant; use color_eyre::eyre::{ContextCompat, WrapErr}; use color_eyre::Result; -use enum_map::EnumMap; use enum_ordinalize::Ordinalize; use log::{info, trace}; use rgb::RGB8; @@ -15,19 +13,19 @@ use tiny_skia::IntSize; use tokio::sync::broadcast; use command::IoWorkerCommand; -use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics}; +use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::device::LoupedeckDevice; -use loupedeck_serial::events::LoupedeckEvent; +use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap}; -use crate::model; -use crate::model::knob_page::KnobStyle; -use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath}; -use crate::modes::key::{start_handlers, KeyEvent, KeyTouchEventKind}; +use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath, KnobPosition}; +use crate::modes::key::{KeyEvent, KeyTouchEventKind}; +use crate::modes::knob::KnobEvent; use crate::runner::graphics::labels::LabelRenderer; -use crate::runner::graphics::{render_key, GraphicsContext}; -use crate::runner::state::{Key, State}; +use crate::runner::graphics::{render_key, render_knob, GraphicsContext}; +use crate::runner::state::{Key, Knob, State}; +use crate::{model, modes}; pub mod command; mod graphics; @@ -55,8 +53,9 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re device.set_brightness(0.5); device.vibrate(VibrationPattern::RiseFall); - let (commands_sender, commands_receiver) = flume::bounded::(20); - let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20); + let (commands_sender, commands_receiver) = flume::bounded::(5); + let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(5); + let knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)> = broadcast::Sender::new(5); commands_sender .send(IoWorkerCommand::SetActivePages { @@ -65,24 +64,23 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re }) .unwrap(); - let cloned_config = Arc::clone(&config); - let cloned_commands_sender = commands_sender.clone(); - let cloned_key_events_sender = key_events_sender.clone(); + let io_worker_context = IoWorkerContext::create( + Arc::clone(&config), + icons, + device, + commands_sender.clone(), + key_events_sender.clone(), + knob_events_sender.clone(), + ); + let io_worker_thread = thread::Builder::new() .name("deckster IO worker".to_owned()) .spawn(move || { - do_io_work( - cloned_config, - icons, - device, - cloned_key_events_sender, - cloned_commands_sender, - commands_receiver, - ) + do_io_work(io_worker_context, commands_receiver); }) .wrap_err("Could not spawn the worker thread")?; - start_handlers( + modes::key::start_handlers( config.key_pages_by_id.iter().flat_map(|(page_id, page)| { page.keys.iter().map(|(position, key)| { ( @@ -95,6 +93,22 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re }) }), key_events_sender, + commands_sender.clone(), + ); + + modes::knob::start_handlers( + config.knob_pages_by_id.iter().flat_map(|(page_id, page)| { + page.knobs.iter().map(|(position, knob)| { + ( + KnobPath { + page_id: page_id.clone(), + position, + }, + Arc::clone(knob), + ) + }) + }), + knob_events_sender, commands_sender, ); @@ -104,59 +118,6 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re Ok(()) } -fn create_state(config: &model::config::Config) -> State { - let key_pages_by_id: HashMap<_, _> = config - .key_pages_by_id - .iter() - .map(|(id, p)| state::KeyPage { - id: id.clone(), - keys_by_position: p - .keys - .iter() - .map(|(position, k)| Key { - path: KeyPath { - page_id: p.id.clone(), - position: *position, - }, - base_style: k.base_style.clone(), - style: None, - }) - .map(|k| (k.path.position, k)) - .collect(), - }) - .map(|p| (p.id.clone(), p)) - .collect(); - - let knob_pages_by_id: HashMap<_, _> = config - .knob_pages_by_id - .iter() - .map(|(id, p)| state::KnobPage { - id: id.clone(), - knobs_by_position: EnumMap::from_fn(|position| { - let knob_config = &p.knobs[position]; - - state::Knob { - path: KnobPath { - page_id: p.id.clone(), - position, - }, - base_style: knob_config.base_style.clone(), - style: KnobStyle::default(), - value: 0.0, - } - }), - }) - .map(|p| (p.id.clone(), p)) - .collect(); - - State { - active_key_page_id: config.initial.key_page.clone(), - active_knob_page_id: config.initial.knob_page.clone(), - key_pages_by_id, - knob_pages_by_id, - } -} - enum IoWork { Event(LoupedeckEvent), Command(IoWorkerCommand), @@ -165,43 +126,49 @@ enum IoWork { struct IoWorkerContext { config: Arc, device: LoupedeckDevice, - state: State, + commands_sender: flume::Sender, + key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, + knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, graphics: GraphicsContext, - active_touch_ids: HashSet, } -fn do_io_work( - config: Arc, - icons: LoadedIconsMap, - device: LoupedeckDevice, - key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, - commands_sender: flume::Sender, - commands_receiver: flume::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(); +impl IoWorkerContext { + pub fn create( + config: Arc, + icons: LoadedIconsMap, + device: LoupedeckDevice, + commands_sender: flume::Sender, + key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, + knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, + ) -> Self { + 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 label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref())); - let device_events_receiver = device.events(); + IoWorkerContext { + config, + device, + commands_sender, + key_events_sender, + knob_events_sender, + graphics: GraphicsContext { + loaded_icons: icons, + buffer_endianness, + label_renderer, + global_icon_filter_by_pack_id, + }, + } + } +} - let mut context = IoWorkerContext { - config, - device, - state, - graphics: GraphicsContext { - loaded_icons: icons, - buffer_endianness, - label_renderer, - global_icon_filter_by_pack_id, - }, - active_touch_ids: HashSet::new(), - }; +fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver) { + let mut state = State::create(&context.config); + let device_events_receiver = context.device.events(); loop { let a = flume::Selector::new() @@ -211,26 +178,26 @@ fn do_io_work( match a { IoWork::Event(event) => { - if !handle_event(&mut context, &commands_sender, &key_events_sender, event) { + if !handle_event(&context, &mut state, event) { break; } } - IoWork::Command(command) => handle_command(&mut context, command), + IoWork::Command(command) => handle_command(&context, &mut state, command), } } } -fn handle_event( - context: &mut IoWorkerContext, - commands_sender: &flume::Sender, - key_events_sender: &broadcast::Sender<(KeyPath, KeyEvent)>, - event: LoupedeckEvent, -) -> bool { +fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool { trace!("Handling event: {:?}", &event); let send_key_event = |path: KeyPath, event: KeyEvent| { trace!("Sending key event ({}): {:?}", &path, &event); - key_events_sender.send((path, event)).unwrap(); + context.key_events_sender.send((path, event)).unwrap(); + }; + + let send_knob_event = |path: KnobPath, event: KnobEvent| { + trace!("Sending knob event ({:?}): {:?}", &path, &event); + context.knob_events_sender.send((path, event)).unwrap(); }; match event { @@ -239,10 +206,11 @@ fn handle_event( let position = ButtonPosition::of(&button); let button_config = &context.config.buttons[position]; - commands_sender + context + .commands_sender .send(IoWorkerCommand::SetActivePages { - key_page_id: button_config.key_page.as_ref().unwrap_or(&context.state.active_key_page_id).clone(), - knob_page_id: button_config.knob_page.as_ref().unwrap_or(&context.state.active_knob_page_id).clone(), + key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(), + knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(), }) .unwrap() } @@ -251,7 +219,7 @@ fn handle_event( let display = characteristics.get_display_at_coordinates(x, y); if let Some(display) = display { - if display.name == characteristics.key_grid.display.name { + if display == &characteristics.key_grid.display { let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y); if let Some(key_index) = key_index { let position = KeyPosition { @@ -260,17 +228,19 @@ fn handle_event( }; let path = KeyPath { - page_id: context.state.active_key_page_id.clone(), + page_id: state.active_key_page_id.clone(), position, }; - let (top_left_x, top_left_y, _, _) = characteristics.key_grid.get_local_key_rect_xywh(key_index).unwrap(); + let LoupedeckDisplayRect { + x: top_left_x, y: top_left_y, .. + } = characteristics.key_grid.get_local_key_rect(key_index).unwrap(); let kind = if is_end { - context.active_touch_ids.remove(&touch_id); + state.active_touch_ids.remove(&touch_id); KeyTouchEventKind::End } else { - let is_new = context.active_touch_ids.insert(touch_id); + let is_new = state.active_touch_ids.insert(touch_id); if is_new { KeyTouchEventKind::Start } else { @@ -295,13 +265,36 @@ fn handle_event( } } } + LoupedeckEvent::KnobRotate { knob, direction } => { + let position = match knob { + LoupedeckKnob::LeftTop => KnobPosition::LeftTop, + LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle, + LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom, + LoupedeckKnob::RightTop => KnobPosition::RightTop, + LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle, + LoupedeckKnob::RightBottom => KnobPosition::RightBottom, + }; + + send_knob_event( + KnobPath { + page_id: state.active_knob_page_id.clone(), + position, + }, + KnobEvent::Rotate { + direction: match direction { + RotationDirection::Clockwise => modes::knob::RotationDirection::Clockwise, + RotationDirection::Counterclockwise => modes::knob::RotationDirection::Counterclockwise, + }, + }, + ) + } _ => {} } true } -fn handle_command(context: &mut IoWorkerContext, command: IoWorkerCommand) { +fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorkerCommand) { trace!("Handling command: {:?}", &command); match command { @@ -309,28 +302,43 @@ fn handle_command(context: &mut IoWorkerContext, command: IoWorkerCommand) { context.device.vibrate(pattern); } IoWorkerCommand::SetActivePages { key_page_id, knob_page_id } => { - context.state.active_key_page_id = key_page_id; - context.state.active_knob_page_id = knob_page_id; + state.active_key_page_id = key_page_id; + state.active_knob_page_id = knob_page_id; for button in LoupedeckButton::VARIANTS { let position = ButtonPosition::of(button); - context.device.set_button_color(*button, get_correct_button_color(context, position)).unwrap(); + context + .device + .set_button_color(*button, get_correct_button_color(context, state, position)) + .unwrap(); } let key_grid = &context.device.characteristics().key_grid; for index in 0..(key_grid.rows * key_grid.columns) { - draw_key_at_index(context, index); + draw_key_at_index(context, state, index); + } + + for position in KnobPosition::VARIANTS { + draw_knob_at_position(context, state, *position); } context.device.refresh_display(&key_grid.display).unwrap(); } IoWorkerCommand::SetKeyStyle { path, value } => { - context.state.mutate_key_for_command("SetKeyStyle", &path, |k| { + state.mutate_key_for_command("SetKeyStyle", &path, |k| { k.style = value; }); - draw_key_at_path_if_visible(context, path); + draw_key_at_path_if_visible(context, state, path); + context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); + } + IoWorkerCommand::SetKnobStyle { path, value } => { + state.mutate_knob_for_command("SetKnobStyle", &path, |k| { + k.style = value; + }); + + draw_knob_at_path_if_visible(context, state, path); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); } } @@ -339,13 +347,13 @@ fn handle_command(context: &mut IoWorkerContext, command: IoWorkerCommand) { // active -> config.active_button_color // no actions defined -> #000000 // inactive -> config.inactive_button_color -fn get_correct_button_color(context: &IoWorkerContext, button_position: ButtonPosition) -> RGB8 { +fn get_correct_button_color(context: &IoWorkerContext, state: &State, button_position: ButtonPosition) -> RGB8 { let button_config = &context.config.buttons[button_position]; if let Some(key_page) = &button_config.key_page { - if key_page == &context.state.active_key_page_id { + if key_page == &state.active_key_page_id { if let Some(knob_page) = &button_config.knob_page { - if knob_page == &context.state.active_knob_page_id { + if knob_page == &state.active_knob_page_id { return context.config.active_button_color.into(); } } @@ -379,29 +387,58 @@ fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics, 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 rect = key_grid.get_local_key_rect(index).unwrap(); - 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(); + let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key); + context + .device + .replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer) + .unwrap(); } -fn draw_key_at_index(context: &IoWorkerContext, index: u8) { +fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) { let position = get_key_position_for_index(&context.device.characteristics().key_grid, index); - draw_key(context, index, context.state.active_key_page().keys_by_position.get(&position)); + draw_key(context, index, state.active_key_page().keys_by_position.get(&position)); } -fn draw_key_at_position_if_visible(context: &IoWorkerContext, position: KeyPosition) { +fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) { let index = get_key_index_for_position(&context.device.characteristics().key_grid, position); if let Some(index) = index { - draw_key(context, index, context.state.active_key_page().keys_by_position.get(&position)); + draw_key(context, index, state.active_key_page().keys_by_position.get(&position)); } } -fn draw_key_at_path_if_visible(context: &IoWorkerContext, path: KeyPath) { - if context.state.active_key_page_id == path.page_id { - draw_key_at_position_if_visible(context, path.position); +fn draw_key_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KeyPath) { + if state.active_key_page_id == path.page_id { + draw_key_at_position_if_visible(context, state, path.position); + } +} + +fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Knob>) { + if let Some((display, rect)) = context.device.characteristics().get_display_and_rect_for_knob(match position { + KnobPosition::LeftTop => LoupedeckKnob::LeftTop, + KnobPosition::LeftMiddle => LoupedeckKnob::LeftMiddle, + KnobPosition::LeftBottom => LoupedeckKnob::LeftBottom, + KnobPosition::RightTop => LoupedeckKnob::RightTop, + KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle, + KnobPosition::RightBottom => LoupedeckKnob::RightBottom, + }) { + let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob); + context + .device + .replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer) + .unwrap(); + } +} + +fn draw_knob_at_position(context: &IoWorkerContext, state: &State, position: KnobPosition) { + draw_knob(context, position, Some(&state.active_knob_page().knobs_by_position[position])); +} + +fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KnobPath) { + if state.active_knob_page_id == path.page_id { + draw_knob_at_position(context, state, path.position); } } diff --git a/deckster/src/runner/state.rs b/deckster/src/runner/state.rs index 35febfc..4fb2fc4 100644 --- a/deckster/src/runner/state.rs +++ b/deckster/src/runner/state.rs @@ -1,21 +1,78 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use enum_map::EnumMap; use log::error; +use crate::model; use crate::model::key_page::KeyStyle; use crate::model::knob_page::KnobStyle; use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition}; +use crate::runner::state; #[derive(Debug)] pub struct State { pub active_key_page_id: String, pub active_knob_page_id: String, + pub active_touch_ids: HashSet, pub key_pages_by_id: HashMap, pub knob_pages_by_id: HashMap, } impl State { + pub fn create(config: &model::config::Config) -> Self { + let key_pages_by_id: HashMap<_, _> = config + .key_pages_by_id + .iter() + .map(|(id, p)| state::KeyPage { + id: id.clone(), + keys_by_position: p + .keys + .iter() + .map(|(position, k)| Key { + path: KeyPath { + page_id: p.id.clone(), + position: *position, + }, + base_style: k.base_style.clone(), + style: None, + }) + .map(|k| (k.path.position, k)) + .collect(), + }) + .map(|p| (p.id.clone(), p)) + .collect(); + + let knob_pages_by_id: HashMap<_, _> = config + .knob_pages_by_id + .iter() + .map(|(id, p)| KnobPage { + id: id.clone(), + knobs_by_position: EnumMap::from_fn(|position| { + let knob_config = &p.knobs[position]; + + Knob { + path: KnobPath { + page_id: p.id.clone(), + position, + }, + base_style: knob_config.base_style.clone(), + style: None, + value: 0.0, + } + }), + }) + .map(|p| (p.id.clone(), p)) + .collect(); + + State { + active_key_page_id: config.initial.key_page.clone(), + active_knob_page_id: config.initial.knob_page.clone(), + active_touch_ids: HashSet::new(), + key_pages_by_id, + knob_pages_by_id, + } + } + 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), @@ -28,6 +85,15 @@ impl State { None } + pub fn mutate_knob_for_command(&mut self, command_name: &'static str, path: &KnobPath, mutator: impl FnOnce(&mut Knob) -> R) -> Option { + match self.knob_pages_by_id.get_mut(&path.page_id) { + None => error!("Received {} command with invalid path.page_id: {}", command_name, &path.page_id), + Some(knob_page) => return Some(mutator(&mut knob_page.knobs_by_position[path.position])), + } + + None + } + pub fn active_key_page(&self) -> &KeyPage { &self.key_pages_by_id[&self.active_key_page_id] } @@ -60,6 +126,6 @@ pub struct Key { pub struct Knob { pub path: KnobPath, pub base_style: KnobStyle, - pub style: KnobStyle, + pub style: Option, pub value: f32, } diff --git a/loupedeck_serial/src/characteristics.rs b/loupedeck_serial/src/characteristics.rs index 8e9585a..9624311 100644 --- a/loupedeck_serial/src/characteristics.rs +++ b/loupedeck_serial/src/characteristics.rs @@ -6,12 +6,26 @@ use crate::util::Endianness; #[derive(Debug, Ordinalize, EnumSetType)] #[repr(u8)] pub enum LoupedeckKnob { - KnobLeftTop = 0x01, - KnobLeftMiddle = 0x02, - KnobLeftBottom = 0x03, - KnobRightTop = 0x04, - KnobRightMiddle = 0x05, - KnobRightBottom = 0x06, + LeftTop = 0x01, + LeftMiddle = 0x02, + LeftBottom = 0x03, + RightTop = 0x04, + RightMiddle = 0x05, + RightBottom = 0x06, +} + +impl LoupedeckKnob { + fn is_left(&self) -> bool { + matches!(self, LoupedeckKnob::LeftTop | LoupedeckKnob::LeftMiddle | LoupedeckKnob::LeftBottom) + } + + fn row(&self) -> u8 { + match self { + LoupedeckKnob::LeftTop | LoupedeckKnob::RightTop => 0, + LoupedeckKnob::LeftMiddle | LoupedeckKnob::RightMiddle => 1, + LoupedeckKnob::LeftBottom | LoupedeckKnob::RightBottom => 2, + } + } } #[derive(Debug, Ordinalize, EnumSetType)] @@ -31,7 +45,6 @@ pub enum LoupedeckButton { #[non_exhaustive] pub struct LoupedeckDeviceDisplayConfiguration { pub id: u8, - pub name: &'static str, pub dpi: f32, pub width: u16, pub height: u16, @@ -42,6 +55,16 @@ pub struct LoupedeckDeviceDisplayConfiguration { pub endianness: Endianness, } +impl Eq for LoupedeckDeviceDisplayConfiguration { + fn assert_receiver_is_total_eq(&self) {} +} + +impl PartialEq for LoupedeckDeviceDisplayConfiguration { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self, other) + } +} + #[derive(Debug)] #[non_exhaustive] pub struct LoupedeckDeviceKeyGridCharacteristics { @@ -50,6 +73,14 @@ pub struct LoupedeckDeviceKeyGridCharacteristics { pub display: LoupedeckDeviceDisplayConfiguration, } +#[derive(Debug)] +pub struct LoupedeckDisplayRect { + pub x: u16, + pub y: u16, + pub w: u16, + pub h: u16, +} + impl LoupedeckDeviceKeyGridCharacteristics { pub fn key_size(&self) -> (u16, u16) { // Assuming the sizes are integers @@ -80,7 +111,7 @@ impl LoupedeckDeviceKeyGridCharacteristics { self.get_key_at_local_coordinates(local_x, local_y) } - pub fn get_local_key_rect_xywh(&self, key_index: u8) -> Option<(u16, u16, u16, u16)> { + pub fn get_local_key_rect(&self, key_index: u8) -> Option { if key_index >= self.rows * self.columns { return None; } @@ -89,7 +120,12 @@ impl LoupedeckDeviceKeyGridCharacteristics { let row = (key_index / self.columns) as u16; let column = (key_index % self.columns) as u16; - Some((column * column_width, row * row_height, column_width, row_height)) + Some(LoupedeckDisplayRect { + x: column * column_width, + y: row * row_height, + w: column_width, + h: row_height, + }) } } @@ -102,22 +138,69 @@ pub struct LoupedeckDeviceCharacteristics { pub available_knobs: EnumSet, pub available_buttons: EnumSet, pub key_grid: LoupedeckDeviceKeyGridCharacteristics, - pub additional_displays: &'static [LoupedeckDeviceDisplayConfiguration], + pub knob_displays: Option<(LoupedeckDeviceDisplayConfiguration, LoupedeckDeviceDisplayConfiguration)>, +} + +impl Eq for LoupedeckDeviceCharacteristics { + fn assert_receiver_is_total_eq(&self) {} +} + +impl PartialEq for LoupedeckDeviceCharacteristics { + fn eq(&self, other: &Self) -> bool { + std::ptr::eq(self, other) + } } impl LoupedeckDeviceCharacteristics { + pub fn knob_rows(&self) -> u8 { + self.available_knobs.iter().map(|k| k.row()).max().map(|v| v + 1).unwrap_or(0) + } + pub fn get_display_at_coordinates(&self, x: u16, y: u16) -> Option<&LoupedeckDeviceDisplayConfiguration> { - let check = |display: &&LoupedeckDeviceDisplayConfiguration| { + let check = |display: &LoupedeckDeviceDisplayConfiguration| { x >= display.global_offset_x && x <= display.global_offset_x + display.width && y >= display.global_offset_y && y <= display.global_offset_y + display.height }; - if check(&&self.key_grid.display) { + if check(&self.key_grid.display) { Some(&self.key_grid.display) + } else if let Some(knob_displays) = &self.knob_displays { + if check(&knob_displays.0) { + Some(&knob_displays.0) + } else if check(&knob_displays.1) { + Some(&knob_displays.1) + } else { + None + } } else { - self.additional_displays.iter().find(check) + None + } + } + + pub fn get_display_and_rect_for_knob(&self, knob: LoupedeckKnob) -> Option<(&LoupedeckDeviceDisplayConfiguration, LoupedeckDisplayRect)> { + if !self.available_knobs.contains(knob) { + return None; + } + + if let Some(knob_displays) = &self.knob_displays { + let display = if knob.is_left() { &knob_displays.0 } else { &knob_displays.1 }; + let row = knob.row() as u16; + let rows = self.knob_rows() as u16; + + let row_height = display.height / rows; + + let rect = LoupedeckDisplayRect { + x: 0, + y: row_height * row, + w: display.width, + h: row_height, + }; + + Some((display, rect)) + } else { + None } } } @@ -127,12 +210,12 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck product_id: 0x0004, name: "Loupedeck Live", available_knobs: enum_set!( - LoupedeckKnob::KnobLeftTop - | LoupedeckKnob::KnobLeftMiddle - | LoupedeckKnob::KnobLeftBottom - | LoupedeckKnob::KnobRightTop - | LoupedeckKnob::KnobRightMiddle - | LoupedeckKnob::KnobRightBottom + LoupedeckKnob::LeftTop + | LoupedeckKnob::LeftMiddle + | LoupedeckKnob::LeftBottom + | LoupedeckKnob::RightTop + | LoupedeckKnob::RightMiddle + | LoupedeckKnob::RightBottom ), available_buttons: enum_set!( LoupedeckButton::N0 @@ -149,7 +232,6 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck columns: 4, display: LoupedeckDeviceDisplayConfiguration { id: 0x4d, - name: "center", dpi: 142.875, width: 360, height: 270, @@ -160,10 +242,9 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck endianness: Endianness::LittleEndian, }, }, - additional_displays: &[ + knob_displays: Some(( LoupedeckDeviceDisplayConfiguration { id: 0x4d, - name: "left", dpi: 142.875, width: 60, height: 270, @@ -175,7 +256,6 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck }, LoupedeckDeviceDisplayConfiguration { id: 0x4d, - name: "right", dpi: 142.875, width: 60, height: 270, @@ -185,7 +265,7 @@ static LOUPEDECK_LIVE_CHARACTERISTIC: LoupedeckDeviceCharacteristics = Loupedeck global_offset_y: 0, endianness: Endianness::LittleEndian, }, - ], + )), }; pub static CHARACTERISTICS: [&LoupedeckDeviceCharacteristics; 1] = [&LOUPEDECK_LIVE_CHARACTERISTIC]; diff --git a/loupedeck_serial/src/device.rs b/loupedeck_serial/src/device.rs index 101fc12..0a56467 100644 --- a/loupedeck_serial/src/device.rs +++ b/loupedeck_serial/src/device.rs @@ -196,7 +196,13 @@ impl LoupedeckDevice { width: u16, height: u16, ) -> Result<(), ReplaceFramebufferAreaError> { - if display.name != self.characteristics.key_grid.display.name && !self.characteristics.additional_displays.iter().any(|d| display.name == d.name) { + if !(display.id == self.characteristics.key_grid.display.id + || self + .characteristics + .knob_displays + .as_ref() + .is_some_and(|d| d.0.id == display.id || d.1.id == display.id)) + { return Err(ReplaceFramebufferAreaError::UnknownDisplay); } @@ -224,7 +230,13 @@ impl LoupedeckDevice { } pub fn refresh_display(&self, display: &LoupedeckDeviceDisplayConfiguration) -> Result<(), RefreshDisplayError> { - if display.name != self.characteristics.key_grid.display.name && !self.characteristics.additional_displays.iter().any(|d| display.name == d.name) { + if !(display.id == self.characteristics.key_grid.display.id + || self + .characteristics + .knob_displays + .as_ref() + .is_some_and(|d| d.0.id == display.id || d.1.id == display.id)) + { return Err(RefreshDisplayError::UnknownDisplay); } diff --git a/loupedeck_serial/src/events.rs b/loupedeck_serial/src/events.rs index b81af99..2666a09 100644 --- a/loupedeck_serial/src/events.rs +++ b/loupedeck_serial/src/events.rs @@ -1,6 +1,6 @@ use crate::characteristics::{LoupedeckButton, LoupedeckKnob}; -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq, Copy, Clone)] pub enum RotationDirection { Clockwise, Counterclockwise, diff --git a/loupedeck_serial/src/messages.rs b/loupedeck_serial/src/messages.rs index 68e024e..1b2aa32 100644 --- a/loupedeck_serial/src/messages.rs +++ b/loupedeck_serial/src/messages.rs @@ -131,23 +131,19 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { match command { 0x00 => match message[1] { 0x00 => match message[0] { - 0x01 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobLeftTop, - }, + 0x01 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::LeftTop }, 0x02 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobLeftMiddle, + knob: LoupedeckKnob::LeftMiddle, }, 0x03 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobLeftBottom, - }, - 0x04 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobRightTop, + knob: LoupedeckKnob::LeftBottom, }, + 0x04 => LoupedeckEvent::KnobDown { knob: LoupedeckKnob::RightTop }, 0x05 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobRightMiddle, + knob: LoupedeckKnob::RightMiddle, }, 0x06 => LoupedeckEvent::KnobDown { - knob: LoupedeckKnob::KnobRightBottom, + knob: LoupedeckKnob::RightBottom, }, 0x07 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N0 }, 0x08 => LoupedeckEvent::ButtonDown { button: LoupedeckButton::N1 }, @@ -160,23 +156,19 @@ fn parse_message(command: u8, mut message: Bytes) -> ParseMessageResult { _ => panic!("Illegal button id: {}", message[1]), }, _ => match message[0] { - 0x01 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobLeftTop, - }, + 0x01 => LoupedeckEvent::KnobUp { knob: LoupedeckKnob::LeftTop }, 0x02 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobLeftMiddle, + knob: LoupedeckKnob::LeftMiddle, }, 0x03 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobLeftBottom, - }, - 0x04 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobRightTop, + knob: LoupedeckKnob::LeftBottom, }, + 0x04 => LoupedeckEvent::KnobUp { knob: LoupedeckKnob::RightTop }, 0x05 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobRightMiddle, + knob: LoupedeckKnob::RightMiddle, }, 0x06 => LoupedeckEvent::KnobUp { - knob: LoupedeckKnob::KnobRightBottom, + knob: LoupedeckKnob::RightBottom, }, 0x07 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N0 }, 0x08 => LoupedeckEvent::ButtonUp { button: LoupedeckButton::N1 },