This commit is contained in:
Moritz Ruth 2024-01-11 02:32:42 +01:00
parent 026a524c20
commit 102de5504b
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
19 changed files with 822 additions and 271 deletions

241
Cargo.lock generated
View file

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

View file

@ -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"
once_cell = "1.19.0"
pipewire = "0.7.2"

View file

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

View file

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

View file

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

View file

@ -13,14 +13,14 @@ use crate::modes;
pub struct File {
pub id: Option<String>,
pub scrolling: Option<ScrollingConfig>,
pub keys: HashMap<KeyPosition, KeyConfig>,
pub keys: HashMap<KeyPosition, Key>,
}
#[derive(Debug)]
pub struct Page {
pub id: String,
pub scrolling: Option<ScrollingConfig>,
pub keys: HashMap<KeyPosition, Arc<KeyConfig>>,
pub keys: HashMap<KeyPosition, Arc<Key>>,
}
#[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,

View file

@ -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<KnobPosition, Knob>,
pub knobs: EnumMap<KnobPosition, Arc<Knob>>,
}
#[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<KnobIndicatorBarConfig>,
pub circle: Option<KnobIndicatorCircleConfig>,
}
#[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<RGB8WithOptionalA>,
}
#[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<RGB8WithOptionalA>,
pub width: Option<u8>,
pub radius: Option<u8>,
}
#[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<String>,
pub icon: Option<IconDescriptor>,
pub indicators: Option<KnobIndicators>,
}
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<modes::knob::audio_volume::Config>,
pub audio_volume: Option<Arc<modes::knob::audio_volume::Config>>,
}
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

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

View file

@ -28,7 +28,7 @@ pub enum KeyEvent {
}
pub fn start_handlers(
keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::KeyConfig>)>,
keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::Key>)>,
events: broadcast::Sender<(KeyPath, KeyEvent)>,
commands: flume::Sender<IoWorkerCommand>,
) {

View file

@ -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<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<IoWorkerCommand>) {
while let Ok((event_path, event)) = events.recv().await {
if event_path != path {
continue;
}
dbg!(path.clone(), event);
}
}

View file

@ -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<Item = (KnobPath, Arc<model::knob_page::Knob>)>,
events: broadcast::Sender<(KnobPath, KnobEvent)>,
commands: flume::Sender<IoWorkerCommand>,
) {
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()));
}
}
}

View file

@ -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<KeyStyle> },
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
}

View file

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

View file

@ -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::<IoWorkerCommand>(20);
let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(20);
let (commands_sender, commands_receiver) = flume::bounded::<IoWorkerCommand>(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<model::config::Config>,
device: LoupedeckDevice,
state: State,
commands_sender: flume::Sender<IoWorkerCommand>,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>,
graphics: GraphicsContext,
active_touch_ids: HashSet<u8>,
}
fn do_io_work(
config: Arc<model::config::Config>,
icons: LoadedIconsMap,
device: LoupedeckDevice,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
commands_sender: flume::Sender<IoWorkerCommand>,
commands_receiver: flume::Receiver<IoWorkerCommand>,
) {
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<model::config::Config>,
icons: LoadedIconsMap,
device: LoupedeckDevice,
commands_sender: flume::Sender<IoWorkerCommand>,
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<IoWorkerCommand>) {
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<IoWorkerCommand>,
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);
}
}

View file

@ -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<u8>,
pub key_pages_by_id: HashMap<String, KeyPage>,
pub knob_pages_by_id: HashMap<String, KnobPage>,
}
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<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),
@ -28,6 +85,15 @@ impl State {
None
}
pub fn mutate_knob_for_command<R>(&mut self, command_name: &'static str, path: &KnobPath, mutator: impl FnOnce(&mut Knob) -> R) -> Option<R> {
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<KnobStyle>,
pub value: f32,
}

View file

@ -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<LoupedeckDisplayRect> {
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<LoupedeckKnob>,
pub available_buttons: EnumSet<LoupedeckButton>,
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];

View file

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

View file

@ -1,6 +1,6 @@
use crate::characteristics::{LoupedeckButton, LoupedeckKnob};
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub enum RotationDirection {
Clockwise,
Counterclockwise,

View file

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