This commit is contained in:
Moritz Ruth 2024-01-29 21:55:58 +01:00
parent eb0983e79d
commit 1904e3e96a
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
38 changed files with 555 additions and 569 deletions

125
Cargo.lock generated
View file

@ -43,9 +43,9 @@ dependencies = [
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.5" version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"anstyle-parse", "anstyle-parse",
@ -197,9 +197,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.4.13" version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52bdc885e4cacc7f7c9eedc1ef6da641603180c783c41a15c264944deeaab642" checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -207,9 +207,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.4.12" version = "4.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb7fb5e4e979aec3be7791562fcba452f94ad85e954da024396433e0e25a79e9" checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -365,6 +365,7 @@ dependencies = [
"clap", "clap",
"color-eyre", "color-eyre",
"cosmic-text", "cosmic-text",
"deckster_shared",
"derive_more", "derive_more",
"encode_unicode", "encode_unicode",
"enum-map", "enum-map",
@ -390,6 +391,32 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "deckster_mode"
version = "0.1.0"
dependencies = [
"deckster_shared",
"either",
"im",
"thiserror",
"toml",
]
[[package]]
name = "deckster_shared"
version = "0.1.0"
dependencies = [
"derive_more",
"enum-map",
"enum-ordinalize",
"im",
"rgb",
"serde",
"serde_with",
"thiserror",
"toml",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@ -413,6 +440,12 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "either"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]] [[package]]
name = "encode_unicode" name = "encode_unicode"
version = "1.0.0" version = "1.0.0"
@ -481,16 +514,26 @@ dependencies = [
] ]
[[package]] [[package]]
name = "env_logger" name = "env_filter"
version = "0.10.1" version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea"
dependencies = [ dependencies = [
"humantime",
"is-terminal",
"log", "log",
"regex", "regex",
"termcolor", ]
[[package]]
name = "env_logger"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eeb342678d785662fd2514be38c459bb925f02b68dd2a3e0f21d7ef82d979dd"
dependencies = [
"anstream",
"anstyle",
"env_filter",
"humantime",
"log",
] ]
[[package]] [[package]]
@ -499,16 +542,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys",
]
[[package]] [[package]]
name = "eyre" name = "eyre"
version = "0.6.11" version = "0.6.11"
@ -724,6 +757,7 @@ dependencies = [
"bitmaps", "bitmaps",
"rand_core", "rand_core",
"rand_xoshiro", "rand_xoshiro",
"serde",
"sized-chunks", "sized-chunks",
"typenum", "typenum",
"version_check", "version_check",
@ -773,17 +807,6 @@ dependencies = [
"mach2", "mach2",
] ]
[[package]]
name = "is-terminal"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455"
dependencies = [
"hermit-abi",
"rustix",
"windows-sys",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.10" version = "1.0.10"
@ -879,12 +902,6 @@ dependencies = [
"pkg-config", "pkg-config",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.11" version = "0.4.11"
@ -1276,19 +1293,6 @@ dependencies = [
"semver", "semver",
] ]
[[package]]
name = "rustix"
version = "0.38.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316"
dependencies = [
"bitflags 2.4.1",
"errno",
"libc",
"linux-raw-sys",
"windows-sys",
]
[[package]] [[package]]
name = "rustybuzz" name = "rustybuzz"
version = "0.11.0" version = "0.11.0"
@ -1606,29 +1610,20 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "termcolor"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449"
dependencies = [
"winapi-util",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.52" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a48fd946b02c0a526b2e9481c8e2a17755e47039164a86c4070446e3a4614d" checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.52" version = "1.0.56"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7fbe9b594d6568a6a1443250a7e67d80b74e1e96f6d1715e1e21cc1888291d3" checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -1,6 +1,8 @@
[workspace] [workspace]
members = [ members = [
"deckster", "deckster",
"deckster_mode",
"deckster_shared",
"loupedeck_serial", "loupedeck_serial",
"pa-volume-interface" "pa-volume-interface"
] ]

View file

@ -12,12 +12,13 @@ derive_more = "0.99.17"
encode_unicode = "1.0.0" encode_unicode = "1.0.0"
enum-map = "3.0.0-beta.2" enum-map = "3.0.0-beta.2"
enum-ordinalize = "4.3.0" enum-ordinalize = "4.3.0"
env_logger = "0.10.1" env_logger = "0.11.0"
flume = "0.11.0" flume = "0.11.0"
humantime-serde = "1.1.1" humantime-serde = "1.1.1"
log = "0.4.20" log = "0.4.20"
loupedeck_serial = { path = "../loupedeck_serial" } loupedeck_serial = { path = "../loupedeck_serial" }
pa-volume-interface = { path = "../pa-volume-interface" } pa-volume-interface = { path = "../pa-volume-interface" }
deckster_shared = { path = "../deckster_shared" }
regex = "1.10.2" regex = "1.10.2"
resvg = "0.37.0" resvg = "0.37.0"
rgb = "0.8.37" rgb = "0.8.37"

View file

@ -1,36 +1,48 @@
[keys.1x2] [keys.1x2]
icon = "@ph/skip-back" icon = "@ph/skip-back"
mode.playerctl__button.command = "previous"
mode.playerctl__button.style.inactive.icon = "@ph/skip-back[alpha=0.4]" handler = "playerctl previous"
config.mode = "previous"
config.style.inactive.icon = "@ph/skip-back[alpha=0.4]"
[keys.2x2] [keys.2x2]
icon = "@ph/play-pause[alpha=0.4]" icon = "@ph/play-pause[alpha=0.4]"
mode.playerctl__button.command = "play-pause"
mode.playerctl__button.style.paused.icon = "@ph/play" handler = "playerctl play-pause"
mode.playerctl__button.style.playing.icon = "@ph/pause" config.style.paused.icon = "@ph/play"
config.style.playing.icon = "@ph/pause"
[keys.3x2] [keys.3x2]
icon = "@ph/skip-forward" icon = "@ph/skip-forward"
mode.playerctl__button.command = "next"
mode.playerctl__button.style.inactive.icon = "@ph/skip-forward[alpha=0.4]" handler = "playerctl next"
config.style.inactive.icon = "@ph/skip-forward[alpha=0.4]"
[keys.1x3] [keys.1x3]
icon = "@fad/shuffle[alpha=0.4]" icon = "@fad/shuffle[alpha=0.4]"
mode.playerctl__shuffle.style.on.icon = "@fad/shuffle[color=#58fc11]"
handler = "playerctl shuffle"
config.style.on.icon = "@fad/shuffle[color=#58fc11]"
[keys.2x3] [keys.2x3]
icon = "@fad/repeat[alpha=0.4]" icon = "@fad/repeat[alpha=0.4]"
mode.playerctl__loop.style.single.icon = "@fad/repeat-one[color=#58fc11]"
mode.playerctl__loop.style.all.icon = "@fad/repeat[color=#58fc11]" handler = "playerctl loop"
config.style.single.icon = "@fad/repeat-one[color=#58fc11]"
config.style.all.icon = "@fad/repeat[color=#58fc11]"
[keys.3x3] [keys.3x3]
icon = "@ph/timer[color=#ff0000]" icon = "@ph/timer[color=#ff0000]"
mode.timer.durations = ["60s", "5m", "10m", "15m", "30m"]
mode.timer.vibrate_when_finished = true handler = "timer"
mode.timer.needy = true config.durations = ["60s", "5m", "10m", "15m", "30m"]
config.vibrate_when_finished = true
config.needy = true
[keys.4x3] [keys.4x3]
icon = "@ph/computer-tower" icon = "@ph/computer-tower"
label = "Gaming PC" label = "Gaming PC"
mode.home_assistant__switch.name = "switch.mwin"
mode.home_assistant__switch.icon.on = "@ph/computer-tower[color=#58fc11]" handler = "home-assistant switch"
config.name = "switch.mwin"
config.style.on.icon = "@ph/computer-tower[color=#58fc11]"

View file

@ -1,28 +0,0 @@
[scrolling]
knob = "right-bottom"
axis = "vertical"
delta = 1
[keys.4x1]
label = "😀"
mode.command.command = "wtype 😀"
[keys.4x2]
label = "😄"
mode.command.command = "wtype 😄"
[keys.4x3]
label = "😅"
mode.command.command = "wtype 😅"
[keys.4x4]
label = "😂"
mode.command.command = "wtype 😂"
[keys.4x5]
label = "😁"
mode.command.command = "wtype 😁"
[keys.4x6]
label = "😇"
mode.command.command = "wtype 😇"

View file

@ -1,35 +0,0 @@
[keys.4x1]
label = "9"
mode.command.command = "wtype 9"
[keys.3x1]
label = "8"
mode.command.command = "wtype 8"
[keys.2x1]
label = "7"
mode.command.command = "wtype 7"
[keys.4x2]
label = "6"
mode.command.command = "wtype 6"
[keys.3x2]
label = "5"
mode.command.command = "wtype 5"
[keys.2x2]
label = "4"
mode.command.command = "wtype 4"
[keys.4x3]
label = "3"
mode.command.command = "wtype 3"
[keys.3x3]
label = "2"
mode.command.command = "wtype 2"
[keys.2x3]
label = "1"
mode.command.command = "wtype 1"

View file

@ -1,29 +0,0 @@
[scrolling]
button.previous = "3x3"
button.next = "4x3"
axis = "horizontal"
delta = 4
[keys.1x1]
label = ""
mode.command.command = "wtype "
[keys.2x1]
label = "…"
mode.command.command = "wtype …"
[keys.3x1]
label = "—"
mode.command.command = "wtype —"
[keys.4x1]
label = ""
mode.command.command = "wtype "
[keys.5x1]
label = "·"
mode.command.command = "wtype ·"
[keys.6x1]
label = "→"
mode.command.command = "wtype →"

View file

@ -2,49 +2,53 @@
icon = "@ph/microphone-light[scale=0.9]" icon = "@ph/microphone-light[scale=0.9]"
indicators.bar.color = "#ffffff50" indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05 handler = "audio_volume"
mode.audio_volume.target.type = "input" config.delta = 0.05
mode.audio_volume.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }] config.target.type = "input"
mode.audio_volume.muted_turn_action = "normal" config.target.predicates = [{ property = "description", value = "SC425 USB Microphone Analog Stereo" }]
config.muted_turn_action = "normal"
mode.audio_volume.style.active.label = "{percentage}%" config.style.active.label = "{percentage}%"
mode.audio_volume.style.muted.label = "Muted" config.style.muted.label = "Muted"
mode.audio_volume.style.muted.icon = "@ph/microphone-slash-light[scale=0.9|color=#fc4646]" config.style.muted.icon = "@ph/microphone-slash-light[scale=0.9|color=#fc4646]"
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" config.style.muted.indicators.bar.color = "#fc464690"
mode.audio_volume.style.inactive.label = "N/A" config.style.inactive.label = "N/A"
mode.audio_volume.style.inactive.icon = "@ph/microphone-slash-light[scale=0.9|alpha=0.8|color=#fc4646]" config.style.inactive.icon = "@ph/microphone-slash-light[scale=0.9|alpha=0.8|color=#fc4646]"
[knobs.left-top] [knobs.left-top]
icon = "@apps/discord[scale=0.25]" icon = "@apps/discord[scale=0.25]"
indicators.bar.color = "#ffffff50" indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05 handler = "audio_volume"
mode.audio_volume.target.type = "application" config.delta = 0.05
mode.audio_volume.target.predicates = [{ property = "binary-name", value = "Discord" }, { property = "description", value = "playStream" }] config.target.type = "application"
config.target.predicates = [{ property = "binary-name", value = "Discord" }, { property = "description", value = "playStream" }]
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" config.style.muted.indicators.bar.color = "#fc464690"
mode.audio_volume.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]" config.style.inactive.icon = "@apps/discord[scale=0.25|grayscale|alpha=0.8]"
[knobs.left-middle] [knobs.left-middle]
icon = "@apps/youtube[scale=1.3]" icon = "@apps/youtube[scale=1.3]"
indicators.bar.color = "#ffffff50" indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05 handler = "audio_volume"
mode.audio_volume.muted_turn_action = "unmute" config.delta = 0.05
mode.audio_volume.target.type = "application" config.muted_turn_action = "unmute"
mode.audio_volume.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }] config.target.type = "application"
config.target.predicates = [{ property = "binary-name", value = "librewolf" }, { property = "description", regex = "\\- Piped$" }]
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" config.style.muted.indicators.bar.color = "#fc464690"
mode.audio_volume.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale]" config.style.inactive.icon = "@apps/youtube[scale=1.3|grayscale]"
[knobs.left-bottom] [knobs.left-bottom]
icon = "@apps/spotify[scale=1.2]" icon = "@apps/spotify[scale=1.2]"
indicators.bar.color = "#ffffff50" indicators.bar.color = "#ffffff50"
mode.audio_volume.delta = 0.05 handler = "audio_volume"
mode.audio_volume.muted_turn_action = "unmute-at-zero" config.delta = 0.05
mode.audio_volume.target.type = "application" config.muted_turn_action = "unmute-at-zero"
mode.audio_volume.target.predicates = [{ property = "application-name", value = "spotify" }] config.target.type = "application"
config.target.predicates = [{ property = "application-name", value = "spotify" }]
mode.audio_volume.style.muted.indicators.bar.color = "#fc464690" config.style.muted.indicators.bar.color = "#fc464690"
mode.audio_volume.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]" config.style.inactive.icon = "@apps/spotify[scale=1.2|grayscale|alpha=0.6]"

View file

@ -1,7 +1,7 @@
use color_eyre::Result; use color_eyre::Result;
use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8}; use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8};
use crate::model::image_filter::ImageFilterDestructive; use deckster_shared::image_filter::ImageFilterDestructive;
pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result<Pixmap> { pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result<Pixmap> {
let mut result = original.clone(); let mut result = original.clone();

View file

@ -8,10 +8,11 @@ use resvg::usvg::tiny_skia_path::IntSize;
use resvg::usvg::{TextRendering, TreeParsing, TreeTextToPath}; use resvg::usvg::{TextRendering, TreeParsing, TreeTextToPath};
use tiny_skia::{BlendMode, FilterQuality, Pixmap, PixmapPaint, Transform}; use tiny_skia::{BlendMode, FilterQuality, Pixmap, PixmapPaint, Transform};
use deckster_shared::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use deckster_shared::image_filter::{ImageFilter, ImageFilterDestructive};
use deckster_shared::state::{KeyStyleByStateMap, KnobStyleByStateMap};
use crate::model::config::{Config, IconFormat, IconPack}; use crate::model::config::{Config, IconFormat, IconPack};
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use crate::model::image_filter::{ImageFilter, ImageFilterDestructive};
use crate::model::{key_page, knob_page};
mod destructive_filter; mod destructive_filter;
@ -26,7 +27,7 @@ pub struct LoadedIcon {
pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> { pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
let mut result: HashSet<IconDescriptor> = HashSet::new(); let mut result: HashSet<IconDescriptor> = HashSet::new();
fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &key_page::StyleByStateMap<T>) { fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &KeyStyleByStateMap<T>) {
map.values().for_each(|v| { map.values().for_each(|v| {
if let Some(icon) = &v.icon { if let Some(icon) = &v.icon {
result.insert(icon.clone()); result.insert(icon.clone());
@ -34,7 +35,7 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
}); });
} }
fn insert_all_from_knob_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &knob_page::StyleByStateMap<T>) { fn insert_all_from_knob_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &KnobStyleByStateMap<T>) {
map.values().for_each(|v| { map.values().for_each(|v| {
if let Some(icon) = &v.icon { if let Some(icon) = &v.icon {
result.insert(icon.clone()); result.insert(icon.clone());

View file

@ -6,6 +6,7 @@ use std::sync::Arc;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use color_eyre::eyre::WrapErr; use color_eyre::eyre::WrapErr;
use color_eyre::Result; use color_eyre::Result;
use log::LevelFilter;
use walkdir::WalkDir; use walkdir::WalkDir;
use crate::model::config::WithFallbackId; use crate::model::config::WithFallbackId;
@ -33,7 +34,8 @@ enum Command {
#[tokio::main] #[tokio::main]
pub async fn main() -> Result<()> { pub async fn main() -> Result<()> {
env_logger::init(); env_logger::builder().filter_module("deckster", LevelFilter::Info).parse_env("RUST_LOG").init();
let cli = Cli::parse(); let cli = Cli::parse();
match cli.command { match cli.command {

View file

@ -6,10 +6,11 @@ use enum_map::EnumMap;
use rgb::RGB8; use rgb::RGB8;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use deckster_shared::image_filter::ImageFilter;
use deckster_shared::rgb::RGB8Wrapper;
use crate::model; use crate::model;
use crate::model::image_filter::ImageFilter;
use crate::model::position::ButtonPosition; use crate::model::position::ButtonPosition;
use crate::model::rgb::RGB8Wrapper;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct File { pub struct File {

View file

@ -1,12 +1,12 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use deckster_shared::path::{KeyPosition, KnobPosition};
use deckster_shared::style::KeyStyle;
use crate::model::geometry::UIntVec2; use crate::model::geometry::UIntVec2;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::position::{KeyPosition, KnobPosition};
use crate::model::rgb::RGB8WithOptionalA;
use crate::modes; use crate::modes;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -46,23 +46,6 @@ pub enum ScrollingConfigAxis {
Horizontal, Horizontal,
} }
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyStyle {
pub label: Option<String>,
pub icon: Option<IconDescriptor>,
pub border: Option<RGB8WithOptionalA>,
}
impl KeyStyle {
pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle {
KeyStyle {
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
border: self.border.or(base.border),
}
}
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Key { pub struct Key {
#[serde(default, flatten)] #[serde(default, flatten)]
@ -81,7 +64,4 @@ pub struct KeyModes {
pub playerctl__button: Option<Arc<modes::key::playerctl::ButtonConfig>>, pub playerctl__button: Option<Arc<modes::key::playerctl::ButtonConfig>>,
pub playerctl__shuffle: Option<Arc<modes::key::playerctl::ShuffleConfig>>, pub playerctl__shuffle: Option<Arc<modes::key::playerctl::ShuffleConfig>>,
pub playerctl__loop: Option<Arc<modes::key::playerctl::LoopConfig>>, pub playerctl__loop: Option<Arc<modes::key::playerctl::LoopConfig>>,
pub vibrate: Option<Arc<modes::key::vibrate::Config>>,
} }
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;

View file

@ -2,11 +2,11 @@ use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use enum_map::EnumMap; use enum_map::EnumMap;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use deckster_shared::path::KnobPosition;
use deckster_shared::style::KnobStyle;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::position::KnobPosition;
use crate::model::rgb::RGB8WithOptionalA;
use crate::modes; use crate::modes;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -29,89 +29,7 @@ pub struct Knob {
pub mode: KnobModes, pub mode: KnobModes,
} }
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobIndicators {
pub bar: Option<KnobIndicatorBarConfig>,
pub circle: Option<KnobIndicatorCircleConfig>,
}
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>,
}
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>,
}
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)] #[derive(Debug, Default, Deserialize)]
pub struct KnobModes { pub struct KnobModes {
pub audio_volume: Option<Arc<modes::knob::audio_volume::Config>>, pub audio_volume: Option<Arc<modes::knob::audio_volume::Config>>,
} }
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -1,8 +1,5 @@
pub mod config; pub mod config;
pub mod geometry; pub mod geometry;
pub mod icon_descriptor;
pub mod image_filter;
pub mod key_page; pub mod key_page;
pub mod knob_page; pub mod knob_page;
pub mod position; pub mod position;
pub mod rgb;

View file

@ -1,96 +1,9 @@
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr;
use enum_map::Enum; use enum_map::Enum;
use enum_ordinalize::Ordinalize; use serde::Deserialize;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckKnob}; use loupedeck_serial::characteristics::LoupedeckButton;
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)]
/// One-based coordinates of a specific virtual (not physical) key.
pub struct KeyPosition {
pub x: u16,
pub y: u16,
}
#[derive(Debug, Error)]
#[error("The input value does not match the required format of <x> and <y> separated by an 'x'")]
pub struct KeyPositionFromStrError {}
impl FromStr for KeyPosition {
type Err = KeyPositionFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values = s.split_once('x');
if let Some((x, y)) = values {
if let Ok(x) = u16::from_str(x) {
if let Ok(y) = u16::from_str(y) {
return Ok(KeyPosition { x, y });
}
}
}
Err(KeyPositionFromStrError {})
}
}
impl Display for KeyPosition {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}x{}", self.x, self.y))
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub struct KeyPath {
pub page_id: String,
pub position: KeyPosition,
}
impl Display for KeyPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}/{}", &self.page_id, &self.position))
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum, Ordinalize)]
#[serde(rename_all = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
}
impl KnobPosition {
pub fn is_left(&self) -> bool {
matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop)
}
}
impl From<LoupedeckKnob> for KnobPosition {
fn from(value: LoupedeckKnob) -> Self {
match value {
LoupedeckKnob::LeftTop => KnobPosition::LeftTop,
LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle,
LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom,
LoupedeckKnob::RightTop => KnobPosition::RightTop,
LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle,
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
}
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub struct KnobPath {
pub page_id: String,
pub position: KnobPosition,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)] #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize, Enum)]
pub enum ButtonPosition { pub enum ButtonPosition {

View file

@ -5,8 +5,6 @@ use log::{error, trace, warn};
use serde::Deserialize; use serde::Deserialize;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use crate::modes::key::KeyEvent;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub command: String, pub command: String,

View file

@ -1,19 +1,19 @@
use serde::Deserialize; use serde::Deserialize;
use crate::model::key_page::StyleByStateMap; use deckster_shared::state::KeyStyleByStateMap;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct SwitchConfig { pub struct SwitchConfig {
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<SwitchState>, pub style: KeyStyleByStateMap<SwitchState>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ButtonConfig { pub struct ButtonConfig {
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<ButtonState>, pub style: KeyStyleByStateMap<ButtonState>,
} }
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)] #[derive(Debug, Eq, PartialEq, Hash, Deserialize)]

View file

@ -3,34 +3,20 @@ use std::sync::Arc;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use deckster_shared::handler_communication::HandlerCommand;
use deckster_shared::path::KeyPath;
use crate::model; use crate::model;
use crate::model::position::KeyPath;
use crate::runner::command::IoWorkerCommand;
pub mod command; pub mod command;
pub mod home_assistant; pub mod home_assistant;
pub mod playerctl; pub mod playerctl;
pub mod timer; pub mod timer;
pub mod vibrate;
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum KeyTouchEventKind {
Start,
Move,
End,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KeyEvent {
Press,
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
VisibilityChange { is_visible: bool },
}
pub fn start_handlers( pub fn start_handlers(
keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::Key>)>, keys: impl Iterator<Item = (KeyPath, Arc<model::key_page::Key>)>,
events: broadcast::Sender<(KeyPath, KeyEvent)>, events: broadcast::Sender<(KeyPath, KeyEvent)>,
commands: flume::Sender<IoWorkerCommand>, commands: flume::Sender<HandlerCommand>,
) { ) {
for (path, config) in keys { for (path, config) in keys {
let mut events = events.subscribe(); let mut events = events.subscribe();
@ -52,10 +38,6 @@ pub fn start_handlers(
tokio::spawn(playerctl::handle_loop(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone())); tokio::spawn(playerctl::handle_loop(path.clone(), Arc::clone(c), own_events.subscribe(), commands.clone()));
} }
if let Some(c) = &config.mode.vibrate {
tokio::spawn(vibrate::handle(Arc::clone(c), own_events.subscribe(), commands.clone()));
}
tokio::spawn(async move { tokio::spawn(async move {
while let Ok((p, e)) = events.recv().await { while let Ok((p, e)) = events.recv().await {
#[allow(clippy::collapsible_if)] #[allow(clippy::collapsible_if)]

View file

@ -14,15 +14,16 @@ use tokio::select;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use tokio::sync::broadcast::error::RecvError; use tokio::sync::broadcast::error::RecvError;
use crate::model::key_page::StyleByStateMap; use deckster_shared::handler_communication::HandlerCommand;
use crate::model::position::KeyPath; use deckster_shared::path::KeyPath;
use deckster_shared::state::KeyStyleByStateMap;
use crate::modes::key::KeyEvent; use crate::modes::key::KeyEvent;
use crate::runner::command::IoWorkerCommand;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ButtonConfig { pub struct ButtonConfig {
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<ButtonState>, pub style: KeyStyleByStateMap<ButtonState>,
pub command: ButtonCommand, pub command: ButtonCommand,
} }
@ -47,7 +48,7 @@ pub enum ButtonState {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ShuffleConfig { pub struct ShuffleConfig {
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<ShuffleState>, pub style: KeyStyleByStateMap<ShuffleState>,
} }
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)] #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
@ -61,7 +62,7 @@ pub enum ShuffleState {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct LoopConfig { pub struct LoopConfig {
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<LoopState>, pub style: KeyStyleByStateMap<LoopState>,
} }
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)] #[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Deserialize)]
@ -164,7 +165,7 @@ static STATE_WATCHER_LOOP: Lazy<PlayerctlStateWatcher<LoopState>> = Lazy::new(||
}) })
}); });
pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) { pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
let mut is_active = false; let mut is_active = false;
let mut state = STATE_WATCHER_PLAYING.subscribe_to_state(); let mut state = STATE_WATCHER_PLAYING.subscribe_to_state();
@ -185,7 +186,7 @@ pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events:
Ok(state) => { Ok(state) => {
is_active = state != ButtonState::Inactive; is_active = state != ButtonState::Inactive;
commands.send(IoWorkerCommand::SetKeyStyle { commands.send(HandlerCommand::SetKeyStyle {
path: path.clone(), path: path.clone(),
value: config.style.get(&state).cloned() value: config.style.get(&state).cloned()
}).unwrap(); }).unwrap();
@ -206,7 +207,7 @@ pub async fn handle_button(path: KeyPath, config: Arc<ButtonConfig>, mut events:
} }
} }
pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) { pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state(); let mut state = STATE_WATCHER_SHUFFLE.subscribe_to_state();
loop { loop {
@ -216,7 +217,7 @@ pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut event
Err(RecvError::Closed) => { result.unwrap(); }, Err(RecvError::Closed) => { result.unwrap(); },
Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ }, Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
Ok(state) => { Ok(state) => {
commands.send(IoWorkerCommand::SetKeyStyle { commands.send(HandlerCommand::SetKeyStyle {
path: path.clone(), path: path.clone(),
value: config.style.get(&state).cloned() value: config.style.get(&state).cloned()
}).unwrap(); }).unwrap();
@ -243,7 +244,7 @@ pub async fn handle_shuffle(path: KeyPath, config: Arc<ShuffleConfig>, mut event
} }
} }
pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) { pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<HandlerCommand>) {
let mut state = STATE_WATCHER_LOOP.subscribe_to_state(); let mut state = STATE_WATCHER_LOOP.subscribe_to_state();
loop { loop {
@ -253,7 +254,7 @@ pub async fn handle_loop(path: KeyPath, config: Arc<LoopConfig>, mut events: bro
Err(RecvError::Closed) => { result.unwrap(); }, Err(RecvError::Closed) => { result.unwrap(); },
Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ }, Err(RecvError::Lagged(_)) => { /* mission failed, we'll get 'em next time */ },
Ok(state) => { Ok(state) => {
commands.send(IoWorkerCommand::SetKeyStyle { commands.send(HandlerCommand::SetKeyStyle {
path: path.clone(), path: path.clone(),
value: config.style.get(&state).cloned() value: config.style.get(&state).cloned()
}).unwrap(); }).unwrap();

View file

@ -1,24 +0,0 @@
use std::sync::Arc;
use serde::Deserialize;
use tokio::sync::broadcast;
use loupedeck_serial::commands::VibrationPattern;
use crate::modes::key::{KeyEvent, KeyTouchEventKind};
use crate::runner::command::IoWorkerCommand;
#[derive(Debug, Deserialize)]
pub struct Config {
pub pattern: VibrationPattern,
}
pub async fn handle(config: Arc<Config>, mut events: broadcast::Receiver<KeyEvent>, commands: flume::Sender<IoWorkerCommand>) {
while let Ok(event) = events.recv().await {
if let KeyEvent::Touch { kind, .. } = event {
if kind == KeyTouchEventKind::Start {
commands.send(IoWorkerCommand::Vibrate { pattern: config.pattern }).unwrap();
}
}
}
}

View file

@ -9,13 +9,12 @@ use serde::Deserialize;
use tokio::select; use tokio::select;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use deckster_shared::handler_communication::HandlerCommand;
use deckster_shared::handler_communication::{KnobEvent, RotationDirection};
use deckster_shared::path::KnobPath;
use deckster_shared::state::KnobStyleByStateMap;
use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface}; use pa_volume_interface::{PaEntityKind, PaEntityMetadata, PaEntityState, PaVolumeInterface};
use crate::model::knob_page::StyleByStateMap;
use crate::model::position::KnobPath;
use crate::modes::knob::{KnobEvent, RotationDirection};
use crate::runner::command::IoWorkerCommand;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
pub target: Target, pub target: Target,
@ -27,7 +26,7 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub muted_turn_action: MutedTurnAction, pub muted_turn_action: MutedTurnAction,
#[serde(default)] #[serde(default)]
pub style: StyleByStateMap<State>, pub style: KnobStyleByStateMap<State>,
} }
#[derive(Debug, Eq, PartialEq, Deserialize, Display)] #[derive(Debug, Eq, PartialEq, Deserialize, Display)]
@ -149,7 +148,7 @@ fn state_matches(target: &Target, state: &PaEntityState) -> bool {
}); });
} }
pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<IoWorkerCommand>) { pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::Receiver<(KnobPath, KnobEvent)>, commands: flume::Sender<HandlerCommand>) {
let mut entity_state: Option<Arc<PaEntityState>> = None; let mut entity_state: Option<Arc<PaEntityState>> = None;
let pa_volume_interface = &PA_VOLUME_INTERFACE; let pa_volume_interface = &PA_VOLUME_INTERFACE;
@ -162,7 +161,7 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
move |entity_state: &Option<Arc<PaEntityState>>| { move |entity_state: &Option<Arc<PaEntityState>>| {
commands commands
.send(IoWorkerCommand::SetKnobValue { .send(HandlerCommand::SetKnobValue {
path: path.clone(), path: path.clone(),
value: entity_state.as_ref().map(|s| { value: entity_state.as_ref().map(|s| {
if s.is_muted() && config.muted_turn_action == MutedTurnAction::UnmuteAtZero { if s.is_muted() && config.muted_turn_action == MutedTurnAction::UnmuteAtZero {
@ -202,7 +201,7 @@ pub async fn handle(path: KnobPath, config: Arc<Config>, mut events: broadcast::
} }
commands commands
.send(IoWorkerCommand::SetKnobStyle { .send(HandlerCommand::SetKnobStyle {
path: path.clone(), path: path.clone(),
value: style, value: style,
}) })

View file

@ -1,33 +1,19 @@
use std::sync::Arc; use std::sync::Arc;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast; use tokio::sync::broadcast;
use deckster_shared::handler_communication::HandlerCommand;
use deckster_shared::handler_communication::KnobEvent;
use deckster_shared::path::KnobPath;
use crate::model; use crate::model;
use crate::model::position::KnobPath;
use crate::runner::command::IoWorkerCommand;
pub mod audio_volume; 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( pub fn start_handlers(
knobs: impl Iterator<Item = (KnobPath, Arc<model::knob_page::Knob>)>, knobs: impl Iterator<Item = (KnobPath, Arc<model::knob_page::Knob>)>,
events: broadcast::Sender<(KnobPath, KnobEvent)>, events: broadcast::Sender<(KnobPath, KnobEvent)>,
commands: flume::Sender<IoWorkerCommand>, commands: flume::Sender<HandlerCommand>,
) { ) {
for (path, config) in knobs { for (path, config) in knobs {
if let Some(c) = &config.mode.audio_volume { if let Some(c) = &config.mode.audio_volume {

View file

@ -1,16 +0,0 @@
use serde::{Deserialize, Serialize};
use loupedeck_serial::commands::VibrationPattern;
use crate::model::key_page::KeyStyle;
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> },
SetKnobValue { path: KnobPath, value: Option<f32> },
}

View file

@ -6,12 +6,12 @@ use resvg::usvg::tiny_skia_path::PathBuilder;
use rgb::RGBA; use rgb::RGBA;
use tiny_skia::{Color, IntSize, LineCap, LineJoin, Paint, Pixmap, PremultipliedColorU8, Rect, Shader, Stroke, Transform}; use tiny_skia::{Color, IntSize, LineCap, LineJoin, Paint, Pixmap, PremultipliedColorU8, Rect, Shader, Stroke, Transform};
use deckster_shared::image_filter::ImageFilter;
use deckster_shared::state::{Key, Knob};
use loupedeck_serial::util::Endianness; use loupedeck_serial::util::Endianness;
use crate::icons::{render_icon_in, LoadedIconsMap}; use crate::icons::{render_icon_in, LoadedIconsMap};
use crate::model::image_filter::ImageFilter;
use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::state::{Key, Knob};
#[derive(Debug)] #[derive(Debug)]
pub struct GraphicsContext { pub struct GraphicsContext {

View file

@ -12,22 +12,23 @@ use rgb::RGB8;
use tiny_skia::IntSize; use tiny_skia::IntSize;
use tokio::sync::broadcast; use tokio::sync::broadcast;
use command::IoWorkerCommand; use deckster_shared::handler_communication::HandlerCommand;
use deckster_shared::handler_communication::KnobEvent;
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
use deckster_shared::state::{Key, Knob};
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob}; use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
use loupedeck_serial::commands::VibrationPattern; use loupedeck_serial::commands::VibrationPattern;
use loupedeck_serial::device::LoupedeckDevice; use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection}; use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap}; use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIconsMap};
use crate::model::position::{ButtonPosition, KeyPath, KeyPosition, KnobPath, KnobPosition}; use crate::model::position::ButtonPosition;
use crate::modes::key::{KeyEvent, KeyTouchEventKind}; use crate::modes::key::{KeyEvent, KeyTouchEventKind};
use crate::modes::knob::KnobEvent;
use crate::runner::graphics::labels::LabelRenderer; use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::graphics::{render_key, render_knob, GraphicsContext}; use crate::runner::graphics::{render_key, render_knob, GraphicsContext};
use crate::runner::state::{Key, Knob, State}; use crate::runner::state::State;
use crate::{model, modes}; use crate::{model, modes};
pub mod command;
mod graphics; mod graphics;
pub mod state; pub mod state;
@ -40,7 +41,7 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re
info!("Connecting to the device…"); info!("Connecting to the device…");
let device = available_device.connect().wrap_err("Connecting to the device failed.")?; let device = available_device.connect().wrap_err("Connecting to the device failed.")?;
info!("Connected"); info!("Connected.");
let key_grid = &device.characteristics().key_grid; let key_grid = &device.characteristics().key_grid;
let used_icon_descriptors = get_used_icon_descriptors(&config); let used_icon_descriptors = get_used_icon_descriptors(&config);
@ -50,15 +51,15 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re
let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?; let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?;
info!("Finished loading {} icon(s) in {}ms", icons.len(), start_time.elapsed().as_millis()); info!("Finished loading {} icon(s) in {}ms", icons.len(), start_time.elapsed().as_millis());
device.set_brightness(0.5); device.set_brightness(0.1);
device.vibrate(VibrationPattern::RiseFall); device.vibrate(VibrationPattern::RiseFall);
let (commands_sender, commands_receiver) = flume::bounded::<IoWorkerCommand>(5); let (commands_sender, commands_receiver) = flume::bounded::<HandlerCommand>(5);
let key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)> = broadcast::Sender::new(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); let knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)> = broadcast::Sender::new(5);
commands_sender commands_sender
.send(IoWorkerCommand::SetActivePages { .send(HandlerCommand::SetActivePages {
knob_page_id: config.initial.knob_page.clone(), knob_page_id: config.initial.knob_page.clone(),
key_page_id: config.initial.key_page.clone(), key_page_id: config.initial.key_page.clone(),
}) })
@ -120,13 +121,13 @@ pub async fn start(config_directory: &Path, config: model::config::Config) -> Re
enum IoWork { enum IoWork {
Event(LoupedeckEvent), Event(LoupedeckEvent),
Command(IoWorkerCommand), Command(HandlerCommand),
} }
struct IoWorkerContext { struct IoWorkerContext {
config: Arc<model::config::Config>, config: Arc<model::config::Config>,
device: LoupedeckDevice, device: LoupedeckDevice,
commands_sender: flume::Sender<IoWorkerCommand>, commands_sender: flume::Sender<HandlerCommand>,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>,
graphics: GraphicsContext, graphics: GraphicsContext,
@ -137,7 +138,7 @@ impl IoWorkerContext {
config: Arc<model::config::Config>, config: Arc<model::config::Config>,
icons: LoadedIconsMap, icons: LoadedIconsMap,
device: LoupedeckDevice, device: LoupedeckDevice,
commands_sender: flume::Sender<IoWorkerCommand>, commands_sender: flume::Sender<HandlerCommand>,
key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>, key_events_sender: broadcast::Sender<(KeyPath, KeyEvent)>,
knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>, knob_events_sender: broadcast::Sender<(KnobPath, KnobEvent)>,
) -> Self { ) -> Self {
@ -166,7 +167,7 @@ impl IoWorkerContext {
} }
} }
fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<IoWorkerCommand>) { fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
let mut state = State::create(&context.config); let mut state = State::create(&context.config);
let device_events_receiver = context.device.events(); let device_events_receiver = context.device.events();
@ -208,7 +209,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
context context
.commands_sender .commands_sender
.send(IoWorkerCommand::SetActivePages { .send(HandlerCommand::SetActivePages {
key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_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(), knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(),
}) })
@ -266,7 +267,7 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
} }
} }
LoupedeckEvent::KnobRotate { knob, direction } => { LoupedeckEvent::KnobRotate { knob, direction } => {
let position: KnobPosition = knob.into(); let position: KnobPosition = get_position_of_loupedeck_knob(knob);
send_knob_event( send_knob_event(
KnobPath { KnobPath {
@ -275,14 +276,14 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
}, },
KnobEvent::Rotate { KnobEvent::Rotate {
direction: match direction { direction: match direction {
RotationDirection::Clockwise => modes::knob::RotationDirection::Clockwise, RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise,
RotationDirection::Counterclockwise => modes::knob::RotationDirection::Counterclockwise, RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise,
}, },
}, },
) )
} }
LoupedeckEvent::KnobDown { knob } => { LoupedeckEvent::KnobDown { knob } => {
let position: KnobPosition = knob.into(); let position: KnobPosition = get_position_of_loupedeck_knob(knob);
send_knob_event( send_knob_event(
KnobPath { KnobPath {
@ -298,14 +299,11 @@ fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEv
true true
} }
fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorkerCommand) { fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) {
trace!("Handling command: {:?}", &command); trace!("Handling command: {:?}", &command);
match command { match command {
IoWorkerCommand::Vibrate { pattern } => { HandlerCommand::SetActivePages { key_page_id, knob_page_id } => {
context.device.vibrate(pattern);
}
IoWorkerCommand::SetActivePages { key_page_id, knob_page_id } => {
state.active_key_page_id = key_page_id; state.active_key_page_id = key_page_id;
state.active_knob_page_id = knob_page_id; state.active_knob_page_id = knob_page_id;
@ -329,7 +327,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke
context.device.refresh_display(&key_grid.display).unwrap(); context.device.refresh_display(&key_grid.display).unwrap();
} }
IoWorkerCommand::SetKeyStyle { path, value } => { HandlerCommand::SetKeyStyle { path, value } => {
state.mutate_key_for_command("SetKeyStyle", &path, |k| { state.mutate_key_for_command("SetKeyStyle", &path, |k| {
k.style = value; k.style = value;
}); });
@ -337,7 +335,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke
draw_key_at_path_if_visible(context, state, path); draw_key_at_path_if_visible(context, state, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
} }
IoWorkerCommand::SetKnobStyle { path, value } => { HandlerCommand::SetKnobStyle { path, value } => {
state.mutate_knob_for_command("SetKnobStyle", &path, |k| { state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
k.style = value; k.style = value;
}); });
@ -345,7 +343,7 @@ fn handle_command(context: &IoWorkerContext, state: &mut State, command: IoWorke
draw_knob_at_path_if_visible(context, state, path); draw_knob_at_path_if_visible(context, state, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap(); context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
} }
IoWorkerCommand::SetKnobValue { path, value } => { HandlerCommand::SetKnobValue { path, value } => {
if let Some(v) = value { if let Some(v) = value {
if !(0.0..=1.0).contains(&v) { if !(0.0..=1.0).contains(&v) {
error!("Received SetKnobValue with an out-of-range value: {}", v); error!("Received SetKnobValue with an out-of-range value: {}", v);
@ -460,3 +458,14 @@ fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path:
draw_knob_at_position(context, state, path.position); draw_knob_at_position(context, state, path.position);
} }
} }
fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition {
match value {
LoupedeckKnob::LeftTop => KnobPosition::LeftTop,
LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle,
LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom,
LoupedeckKnob::RightTop => KnobPosition::RightTop,
LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle,
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
}
}

View file

@ -3,10 +3,10 @@ use std::collections::{HashMap, HashSet};
use enum_map::EnumMap; use enum_map::EnumMap;
use log::error; use log::error;
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
use deckster_shared::state::{Key, Knob};
use crate::model; 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; use crate::runner::state;
#[derive(Debug)] #[derive(Debug)]
@ -114,18 +114,3 @@ pub struct KnobPage {
pub id: String, pub id: String,
pub knobs_by_position: EnumMap<KnobPosition, Knob>, pub knobs_by_position: EnumMap<KnobPosition, Knob>,
} }
#[derive(Debug)]
pub struct Key {
pub path: KeyPath,
pub base_style: KeyStyle,
pub style: Option<KeyStyle>,
}
#[derive(Debug)]
pub struct Knob {
pub path: KnobPath,
pub base_style: KnobStyle,
pub style: Option<KnobStyle>,
pub value: Option<f32>,
}

11
deckster_mode/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "deckster_mode"
version = "0.1.0"
edition = "2021"
[dependencies]
deckster_shared = { path = "../deckster_shared" }
thiserror = "1.0.56"
im = "15.1.0"
toml = "0.8.8"
either = "1.9.0"

44
deckster_mode/src/lib.rs Normal file
View file

@ -0,0 +1,44 @@
use std::io;
use either::Either;
use thiserror::Error;
use deckster_shared::handler_communication::HandlerEvent;
use deckster_shared::path::{KeyPath, KnobPath};
#[derive(Debug, Error)]
pub enum RunError {
#[error("A stdin line could not be read.")]
LineIo(#[from] io::Error),
#[error("A stdin line could not be deserialized.")]
LineDeserialization { line: String, source: toml::de::Error },
}
pub trait DecksterHandler {
fn handle(&mut self, event: HandlerEvent);
}
pub fn run<H: DecksterHandler, I: FnOnce(im::HashMap<KeyPath, (String, toml::Value)>, im::HashMap<KnobPath, (String, toml::Value)>) -> H>(
init_handler: I,
) -> Result<(), RunError> {
let mut handler: Either<H, I> = Either::Right(init_handler);
for line in io::stdin().lines() {
let line = line?;
match handler {
Either::Left(mut h) => {
let event: HandlerEvent = toml::from_str(&line).map_err(|e| RunError::LineDeserialization { line, source: e.clone() })?;
h.handle(event);
handler = Either::Left(h);
}
Either::Right(init_handler) => {
handler = Either::Left(init_handler(im::HashMap::new(), im::HashMap::new()));
}
}
}
Ok(())
}

View file

@ -0,0 +1,15 @@
[package]
name = "deckster_shared"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.195", features = ["derive", "rc"] }
serde_with = "3.4.0"
thiserror = "1.0.56"
derive_more = "0.99.17"
rgb = "0.8.37"
enum-ordinalize = "4.3.0"
enum-map = "3.0.0-beta.2"
im = { version = "15.1.0", features = ["serde"] }
toml = { version = "0.8.8", default-features = false }

View file

@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use crate::path::{KeyPath, KnobPath};
use crate::style::{KeyStyle, KnobStyle};
#[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 },
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum KeyTouchEventKind {
Start,
Move,
End,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub enum KeyEvent {
Press,
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
VisibilityChange { is_visible: bool },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum HandlerEvent {
Knob { path: KnobPath, event: KnobEvent },
Key { path: KeyPath, event: KeyEvent },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(tag = "command", rename_all = "kebab-case")]
pub enum HandlerCommand {
SetActivePages { key_page_id: String, knob_page_id: String },
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
SetKnobValue { path: KnobPath, value: Option<f32> },
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct InitialHandlerMessage<KeyConfig: Clone, KnobConfig: Clone> {
key_configs: im::HashMap<KeyPath, (String, KeyConfig)>,
knob_configs: im::HashMap<KnobPath, (String, KnobConfig)>,
}

View file

@ -4,9 +4,8 @@ use std::str::FromStr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError}; use crate::image_filter::{ImageFilter, ImageFilterFromStringError};
#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)] #[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
pub struct IconDescriptor { pub struct IconDescriptor {
@ -14,7 +13,7 @@ pub struct IconDescriptor {
pub filter: ImageFilter, pub filter: ImageFilter,
} }
#[derive(Debug, Error)] #[derive(Debug, thiserror::Error)]
pub enum IconDescriptorFromStrError { pub enum IconDescriptorFromStrError {
#[error("Not a valid icon identifier: {0}")] #[error("Not a valid icon identifier: {0}")]
InvalidIconPackSource(String), InvalidIconPackSource(String),

View file

@ -5,12 +5,12 @@ use std::str::FromStr;
use serde_with::{DeserializeFromStr, SerializeDisplay}; use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error; use thiserror::Error;
use crate::model::rgb::RGB8Wrapper; use crate::rgb::RGB8Wrapper;
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub struct ImageFilterTransform { pub struct ImageFilterTransform {
pub scale: f32, pub scale: f32,
// Must be in 0..=3 /// Must be in 0..=3
pub clockwise_quarter_rotations: u8, pub clockwise_quarter_rotations: u8,
pub alpha: f32, pub alpha: f32,
} }

View file

@ -0,0 +1,7 @@
pub mod handler_communication;
pub mod icon_descriptor;
pub mod image_filter;
pub mod path;
pub mod rgb;
pub mod state;
pub mod style;

View file

@ -0,0 +1,78 @@
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;
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)]
/// One-based coordinates of a specific virtual (not physical) key.
pub struct KeyPosition {
pub x: u16,
pub y: u16,
}
#[derive(Debug, Error)]
#[error("The input value does not match the required format of <x> and <y> separated by an 'x'")]
pub struct KeyPositionFromStrError {}
impl FromStr for KeyPosition {
type Err = KeyPositionFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let values = s.split_once('x');
if let Some((x, y)) = values {
if let Ok(x) = u16::from_str(x) {
if let Ok(y) = u16::from_str(y) {
return Ok(KeyPosition { x, y });
}
}
}
Err(KeyPositionFromStrError {})
}
}
impl Display for KeyPosition {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}x{}", self.x, self.y))
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub struct KeyPath {
pub page_id: String,
pub position: KeyPosition,
}
impl Display for KeyPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("{}/{}", &self.page_id, &self.position))
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum, Ordinalize)]
#[serde(rename_all = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
RightBottom,
}
impl KnobPosition {
pub fn is_left(&self) -> bool {
matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop)
}
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub struct KnobPath {
pub page_id: String,
pub position: KnobPosition,
}

View file

@ -0,0 +1,22 @@
use std::collections::HashMap;
use crate::path::{KeyPath, KnobPath};
use crate::style::{KeyStyle, KnobStyle};
#[derive(Debug)]
pub struct Key {
pub path: KeyPath,
pub base_style: KeyStyle,
pub style: Option<KeyStyle>,
}
#[derive(Debug)]
pub struct Knob {
pub path: KnobPath,
pub base_style: KnobStyle,
pub style: Option<KnobStyle>,
pub value: Option<f32>,
}
pub type KeyStyleByStateMap<State> = HashMap<State, KeyStyle>;
pub type KnobStyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -0,0 +1,101 @@
use serde::{Deserialize, Serialize};
use crate::icon_descriptor::IconDescriptor;
use crate::rgb::RGB8WithOptionalA;
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyStyle {
pub label: Option<String>,
pub icon: Option<IconDescriptor>,
pub border: Option<RGB8WithOptionalA>,
}
impl KeyStyle {
pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle {
KeyStyle {
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
border: self.border.or(base.border),
}
}
}
#[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, Clone, PartialEq, Serialize, Deserialize)]
pub struct KnobIndicators {
pub bar: Option<KnobIndicatorBarConfig>,
pub circle: Option<KnobIndicatorCircleConfig>,
}
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>,
}
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>,
}
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(),
}
}
}