commit
This commit is contained in:
parent
78291b75b7
commit
d97bf24168
14 changed files with 180 additions and 97 deletions
|
@ -22,15 +22,15 @@ key_page = "default"
|
||||||
knob_page = "default"
|
knob_page = "default"
|
||||||
|
|
||||||
[icon_packs.apps]
|
[icon_packs.apps]
|
||||||
path = "./apps"
|
path = "icons/apps"
|
||||||
format = "svg"
|
format = "svg"
|
||||||
|
|
||||||
[icon_packs.fad]
|
[icon_packs.fad]
|
||||||
path = "./fad"
|
path = "icons/fad"
|
||||||
format = "svg"
|
format = "svg"
|
||||||
global_filter = "invert"
|
global_filter = "invert"
|
||||||
|
|
||||||
[icon_packs.ph]
|
[icon_packs.ph]
|
||||||
path = "./ph"
|
path = "icons/ph"
|
||||||
format = "svg"
|
format = "svg"
|
||||||
global_filter = "invert"
|
global_filter = "invert"
|
1
deckster/examples/full/icons/apps/spotify.svg
Normal file
1
deckster/examples/full/icons/apps/spotify.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="#1DB954" d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>
|
After Width: | Height: | Size: 679 B |
|
@ -10,21 +10,67 @@ use tiny_skia::{Pixmap, Transform};
|
||||||
use crate::icons::filter::apply_filter;
|
use crate::icons::filter::apply_filter;
|
||||||
use crate::model::config::{Config, IconFormat, IconPack};
|
use crate::model::config::{Config, IconFormat, IconPack};
|
||||||
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
|
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
|
||||||
|
use crate::model::IconMap;
|
||||||
|
|
||||||
mod filter;
|
mod filter;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct LoadedIcon {
|
pub struct LoadedIcon {
|
||||||
pixmap: Pixmap,
|
pub pixmap: Pixmap,
|
||||||
scale: f32,
|
pub scale: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
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_map<T>(result: &mut HashSet<IconDescriptor>, map: &IconMap<T>) {
|
||||||
|
map.values().for_each(|v| {
|
||||||
|
result.insert(v.clone());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
for page in config.key_pages_by_id.values() {
|
for page in config.key_pages_by_id.values() {
|
||||||
for key in page.keys.values() {
|
for key in page.keys.values() {
|
||||||
if let Some(d) = &key.icon {
|
if let Some(d) = &key.icon {
|
||||||
result.insert(d.0);
|
result.insert(d.clone());
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.media__next {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.media__play_pause {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.media__previous {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.home_assistant__button {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.home_assistant__switch {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.spotify__repeat {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(c) = &key.mode.spotify__shuffle {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for page in config.knob_pages_by_id.values() {
|
||||||
|
for knob in page.knobs.values() {
|
||||||
|
result.insert(knob.icon.clone());
|
||||||
|
|
||||||
|
if let Some(c) = &knob.mode.audio_volume {
|
||||||
|
insert_all_from_map(&mut result, &c.icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,9 +79,9 @@ pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_icons(
|
pub fn load_icons(
|
||||||
icon_packs_by_id: HashMap<String, IconPack>,
|
config_directory: &Path,
|
||||||
|
icon_packs_by_id: &HashMap<String, IconPack>,
|
||||||
descriptors: HashSet<IconDescriptor>,
|
descriptors: HashSet<IconDescriptor>,
|
||||||
key_size: (u16, u16),
|
|
||||||
dpi: f32,
|
dpi: f32,
|
||||||
) -> Result<HashMap<IconDescriptor, LoadedIcon>> {
|
) -> Result<HashMap<IconDescriptor, LoadedIcon>> {
|
||||||
let mut unfiltered_pixmap_by_source: HashMap<IconDescriptorSource, Pixmap> = HashMap::new();
|
let mut unfiltered_pixmap_by_source: HashMap<IconDescriptorSource, Pixmap> = HashMap::new();
|
||||||
|
@ -50,7 +96,7 @@ pub fn load_icons(
|
||||||
|
|
||||||
let original_image = match unfiltered_pixmap_by_source.entry(descriptor.source.clone()) {
|
let original_image = match unfiltered_pixmap_by_source.entry(descriptor.source.clone()) {
|
||||||
Entry::Occupied(o) => o.into_mut(),
|
Entry::Occupied(o) => o.into_mut(),
|
||||||
Entry::Vacant(v) => v.insert(read_image(&icon_packs_by_id, dpi, &fonts_db, descriptor.source.clone())?),
|
Entry::Vacant(v) => v.insert(read_image(config_directory, icon_packs_by_id, dpi, &fonts_db, descriptor.source.clone())?),
|
||||||
};
|
};
|
||||||
|
|
||||||
let pixmap = apply_filter(original_image, &descriptor.filter)?;
|
let pixmap = apply_filter(original_image, &descriptor.filter)?;
|
||||||
|
@ -63,6 +109,7 @@ pub fn load_icons(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_image(
|
fn read_image(
|
||||||
|
config_directory: &Path,
|
||||||
icon_packs_by_id: &HashMap<String, IconPack>,
|
icon_packs_by_id: &HashMap<String, IconPack>,
|
||||||
dpi: f32,
|
dpi: f32,
|
||||||
fonts_db: &resvg::usvg::fontdb::Database,
|
fonts_db: &resvg::usvg::fontdb::Database,
|
||||||
|
@ -79,7 +126,7 @@ fn read_image(
|
||||||
IconFormat::Svg => "svg",
|
IconFormat::Svg => "svg",
|
||||||
};
|
};
|
||||||
|
|
||||||
pack.path.join(icon_id + "." + extension)
|
config_directory.join(&pack.path).join(icon_id + "." + extension)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@ pub async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
.validate()?;
|
.validate()?;
|
||||||
|
|
||||||
runner::start(config).await?
|
runner::start(&config_path, config).await?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -79,7 +79,7 @@ pub async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_and_deserialize<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
|
fn read_and_deserialize<T: serde::de::DeserializeOwned>(path: &Path) -> Result<T> {
|
||||||
let content = fs::read_to_string(&path).wrap_err_with(|| format!("Reading of {} failed.", path.to_string_lossy()))?;
|
let content = fs::read_to_string(path).wrap_err_with(|| format!("Reading of {} failed.", path.to_string_lossy()))?;
|
||||||
|
|
||||||
toml::from_str::<T>(&content).wrap_err_with(|| format!("Parsing of {} failed.", path.to_string_lossy()))
|
toml::from_str::<T>(&content).wrap_err_with(|| format!("Parsing of {} failed.", path.to_string_lossy()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
|
use std::fmt::{Display, Formatter, Write};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_with::DeserializeFromStr;
|
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError};
|
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError};
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Clone, DeserializeFromStr)]
|
#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
|
||||||
pub struct IconDescriptorString(pub IconDescriptor);
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct IconDescriptor {
|
pub struct IconDescriptor {
|
||||||
pub source: IconDescriptorSource,
|
pub source: IconDescriptorSource,
|
||||||
pub filter: ImageFilter,
|
pub filter: ImageFilter,
|
||||||
|
@ -28,7 +26,7 @@ pub enum IconDescriptorFromStrError {
|
||||||
MissingImageFilterClosingBracket,
|
MissingImageFilterClosingBracket,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for IconDescriptorString {
|
impl FromStr for IconDescriptor {
|
||||||
type Err = IconDescriptorFromStrError;
|
type Err = IconDescriptorFromStrError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
@ -57,7 +55,13 @@ impl FromStr for IconDescriptorString {
|
||||||
ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?
|
ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(IconDescriptorString(IconDescriptor { source, filter }))
|
Ok(IconDescriptor { source, filter })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for IconDescriptor {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_fmt(format_args!("{}[{}]", self.source, self.filter))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,3 +75,13 @@ pub enum IconDescriptorSource {
|
||||||
},
|
},
|
||||||
Path(PathBuf),
|
Path(PathBuf),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for IconDescriptorSource {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
IconDescriptorSource::None => f.write_str("<none>"),
|
||||||
|
IconDescriptorSource::IconPack { pack_id, icon_id } => f.write_fmt(format_args!("@{}/{}", pack_id, icon_id)),
|
||||||
|
IconDescriptorSource::Path(path) => f.write_str(&path.to_string_lossy()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,20 +2,20 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct SwitchConfig {
|
pub struct SwitchConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<SwitchState, IconDescriptorString>,
|
pub icon: HashMap<SwitchState, IconDescriptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ButtonConfig {
|
pub struct ButtonConfig {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<ButtonState, IconDescriptorString>,
|
pub icon: HashMap<ButtonState, IconDescriptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -2,12 +2,12 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PlayPauseConfig {
|
pub struct PlayPauseConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PlayPauseState, IconDescriptorString>,
|
pub icon: HashMap<PlayPauseState, IconDescriptor>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub action: PlayPauseAction,
|
pub action: PlayPauseAction,
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ pub enum PlayPauseAction {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PreviousAndNextConfig {
|
pub struct PreviousAndNextConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PreviousAndNextState, IconDescriptorString>,
|
pub icon: HashMap<PreviousAndNextState, IconDescriptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -2,18 +2,18 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ShuffleConfig {
|
pub struct ShuffleConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<ShuffleState, IconDescriptorString>,
|
pub icon: HashMap<ShuffleState, IconDescriptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RepeatConfig {
|
pub struct RepeatConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<RepeatState, IconDescriptorString>,
|
pub icon: HashMap<RepeatState, IconDescriptor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::geometry::UIntVec2;
|
use crate::model::geometry::UIntVec2;
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
use crate::model::{key_modes, KeyPosition, KnobPosition};
|
use crate::model::{key_modes, KeyPosition, KnobPosition};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
@ -46,7 +46,7 @@ pub enum ScrollingConfigAxis {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KeyConfig {
|
pub struct KeyConfig {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub icon: Option<IconDescriptorString>,
|
pub icon: Option<IconDescriptor>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: KeyModes,
|
pub mode: KeyModes,
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ use std::num::NonZeroU8;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
|
||||||
use crate::model::rgb::RGB8WithOptionalA;
|
use crate::model::rgb::RGB8WithOptionalA;
|
||||||
|
use crate::model::IconMap;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -22,7 +22,7 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: HashMap<State, String>,
|
pub label: HashMap<State, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<State, IconDescriptorString>,
|
pub icon: IconMap<State>,
|
||||||
pub circle_indicator: Option<CircleIndicatorConfig>,
|
pub circle_indicator: Option<CircleIndicatorConfig>,
|
||||||
pub bar_indicator: Option<BarIndicatorConfig>,
|
pub bar_indicator: Option<BarIndicatorConfig>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::collections::HashMap;
|
||||||
use enum_map::EnumMap;
|
use enum_map::EnumMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptorString;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
use crate::model::rgb::RGB8WithOptionalA;
|
use crate::model::rgb::RGB8WithOptionalA;
|
||||||
use crate::model::{knob_modes, KnobPosition};
|
use crate::model::{knob_modes, KnobPosition};
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ pub struct Knob {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: String,
|
pub label: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: IconDescriptorString,
|
pub icon: IconDescriptor,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub indicator: KnobIndicators,
|
pub indicator: KnobIndicators,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
@ -8,6 +9,8 @@ use thiserror::Error;
|
||||||
|
|
||||||
use loupedeck_serial::characteristics::LoupedeckButton;
|
use loupedeck_serial::characteristics::LoupedeckButton;
|
||||||
|
|
||||||
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
pub mod icon_descriptor;
|
pub mod icon_descriptor;
|
||||||
|
@ -125,3 +128,5 @@ impl Display for ButtonPosition {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type IconMap<State> = HashMap<State, IconDescriptor>;
|
||||||
|
|
|
@ -1,22 +1,30 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
use tiny_skia::{Color, Pixmap, PremultipliedColorU8};
|
use tiny_skia::{Color, IntSize, Pixmap, PremultipliedColorU8};
|
||||||
|
|
||||||
use loupedeck_serial::util::Endianness;
|
use loupedeck_serial::util::Endianness;
|
||||||
|
|
||||||
|
use crate::icons::LoadedIcon;
|
||||||
|
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
|
||||||
use crate::runner::state::Key;
|
use crate::runner::state::Key;
|
||||||
|
|
||||||
pub fn render_key(width: u16, height: u16, buffer_endianness: Endianness, state: Option<&Key>) -> Bytes {
|
pub fn render_key(key_size: (u16, u16), buffer_endianness: Endianness, icons: &HashMap<IconDescriptor, LoadedIcon>, state: Option<&Key>) -> Bytes {
|
||||||
let mut canvas = Pixmap::new(width as u32, height as u32).unwrap();
|
let mut canvas = Pixmap::new(key_size.0 as u32, key_size.1 as u32).unwrap();
|
||||||
|
|
||||||
if let Some(state) = state {
|
if let Some(state) = state {
|
||||||
canvas.fill(if state.label.is_empty() {
|
if state.icon.source != IconDescriptorSource::None {
|
||||||
Color::WHITE
|
let icon = &icons[&state.icon];
|
||||||
} else {
|
let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height())
|
||||||
Color::from_rgba8(0, 255, 0, 255)
|
.unwrap()
|
||||||
});
|
.scale_by(icon.scale)
|
||||||
} else {
|
.unwrap();
|
||||||
canvas.fill(Color::BLACK);
|
|
||||||
|
//canvas.draw_pixmap(0, 0, icon.pixmap.as_ref(), &PixmapPaint::default(), Transform::from_scale(icon.scale, icon.scale), None);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
}
|
||||||
|
canvas.fill(Color::WHITE);
|
||||||
|
|
||||||
convert_pixels_to_rgb565(canvas.pixels(), buffer_endianness).freeze()
|
convert_pixels_to_rgb565(canvas.pixels(), buffer_endianness).freeze()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
@ -15,6 +16,7 @@ use loupedeck_serial::commands::VibrationPattern;
|
||||||
use loupedeck_serial::device::LoupedeckDevice;
|
use loupedeck_serial::device::LoupedeckDevice;
|
||||||
use loupedeck_serial::events::LoupedeckEvent;
|
use loupedeck_serial::events::LoupedeckEvent;
|
||||||
|
|
||||||
|
use crate::icons::{get_used_icon_descriptors, load_icons, LoadedIcon};
|
||||||
use crate::model;
|
use crate::model;
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
use crate::model::{ButtonPosition, KeyPath, KeyPosition, KnobPath};
|
use crate::model::{ButtonPosition, KeyPath, KeyPosition, KnobPath};
|
||||||
|
@ -24,7 +26,7 @@ use crate::runner::state::{Key, State, StateChangeCommand};
|
||||||
mod graphics;
|
mod graphics;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
pub async fn start(config: model::config::Config) -> Result<()> {
|
pub async fn start(config_directory: &Path, config: model::config::Config) -> Result<()> {
|
||||||
let config = Arc::new(config);
|
let config = Arc::new(config);
|
||||||
let device = LoupedeckDevice::discover()?
|
let device = LoupedeckDevice::discover()?
|
||||||
.first()
|
.first()
|
||||||
|
@ -32,6 +34,11 @@ pub async fn start(config: model::config::Config) -> Result<()> {
|
||||||
.connect()
|
.connect()
|
||||||
.wrap_err("Connecting to the device failed.")?;
|
.wrap_err("Connecting to the device failed.")?;
|
||||||
|
|
||||||
|
let key_grid = &device.characteristics().key_grid;
|
||||||
|
|
||||||
|
let used_icon_descriptors = get_used_icon_descriptors(&config);
|
||||||
|
let icons = load_icons(config_directory, &config.icon_packs, used_icon_descriptors, key_grid.display.dpi)?;
|
||||||
|
|
||||||
device.set_brightness(0.0);
|
device.set_brightness(0.0);
|
||||||
device.vibrate(VibrationPattern::RiseFall);
|
device.vibrate(VibrationPattern::RiseFall);
|
||||||
|
|
||||||
|
@ -49,7 +56,7 @@ pub async fn start(config: model::config::Config) -> Result<()> {
|
||||||
let cloned_commands_sender = commands_sender.clone();
|
let cloned_commands_sender = commands_sender.clone();
|
||||||
let io_worker_thread = thread::Builder::new()
|
let io_worker_thread = thread::Builder::new()
|
||||||
.name("deckster IO worker".to_owned())
|
.name("deckster IO worker".to_owned())
|
||||||
.spawn(move || do_io_work(cloned_config, device, events_receiver, cloned_commands_sender, commands_receiver))
|
.spawn(move || do_io_work(cloned_config, icons, device, events_receiver, cloned_commands_sender, commands_receiver))
|
||||||
.wrap_err("Could not spawn the worker thread")?;
|
.wrap_err("Could not spawn the worker thread")?;
|
||||||
|
|
||||||
io_worker_thread.join().unwrap();
|
io_worker_thread.join().unwrap();
|
||||||
|
@ -72,7 +79,7 @@ fn create_state(config: &model::config::Config) -> State {
|
||||||
position: *position,
|
position: *position,
|
||||||
},
|
},
|
||||||
label: k.label.clone().unwrap_or_default(),
|
label: k.label.clone().unwrap_or_default(),
|
||||||
icon: IconDescriptor::default(),
|
icon: k.icon.clone().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
.map(|k| (k.path.position, k))
|
.map(|k| (k.path.position, k))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
@ -94,7 +101,7 @@ fn create_state(config: &model::config::Config) -> State {
|
||||||
position,
|
position,
|
||||||
},
|
},
|
||||||
label: knob_config.label.clone(),
|
label: knob_config.label.clone(),
|
||||||
icon: knob_config.icon.0.clone(),
|
icon: knob_config.icon.clone(),
|
||||||
value: 0.0,
|
value: 0.0,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
@ -115,14 +122,23 @@ enum IoWork {
|
||||||
Command(StateChangeCommand),
|
Command(StateChangeCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct IoWorkerContext {
|
||||||
|
config: Arc<model::config::Config>,
|
||||||
|
icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||||
|
device: LoupedeckDevice,
|
||||||
|
state: State,
|
||||||
|
}
|
||||||
|
|
||||||
fn do_io_work(
|
fn do_io_work(
|
||||||
config: Arc<model::config::Config>,
|
config: Arc<model::config::Config>,
|
||||||
|
icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||||
device: LoupedeckDevice,
|
device: LoupedeckDevice,
|
||||||
events_receiver: Receiver<LoupedeckEvent>,
|
events_receiver: Receiver<LoupedeckEvent>,
|
||||||
commands_sender: Sender<StateChangeCommand>,
|
commands_sender: Sender<StateChangeCommand>,
|
||||||
commands_receiver: Receiver<StateChangeCommand>,
|
commands_receiver: Receiver<StateChangeCommand>,
|
||||||
) {
|
) {
|
||||||
let mut state = create_state(&config);
|
let state = create_state(&config);
|
||||||
|
let mut context = IoWorkerContext { config, icons, device, state };
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let a = flume::Selector::new()
|
let a = flume::Selector::new()
|
||||||
|
@ -132,40 +148,34 @@ fn do_io_work(
|
||||||
|
|
||||||
match a {
|
match a {
|
||||||
IoWork::Event(event) => {
|
IoWork::Event(event) => {
|
||||||
if !handle_event(&config, &state, &device, &commands_sender, event) {
|
if !handle_event(&context, &commands_sender, event) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IoWork::Command(command) => handle_command(&config, &mut state, &device, command),
|
IoWork::Command(command) => handle_command(&mut context, command),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_event(
|
fn handle_event(context: &IoWorkerContext, commands_sender: &Sender<StateChangeCommand>, event: LoupedeckEvent) -> bool {
|
||||||
config: &model::config::Config,
|
|
||||||
state: &State,
|
|
||||||
device: &LoupedeckDevice,
|
|
||||||
commands_sender: &Sender<StateChangeCommand>,
|
|
||||||
event: LoupedeckEvent,
|
|
||||||
) -> bool {
|
|
||||||
trace!("Handling event: {:?}", &event);
|
trace!("Handling event: {:?}", &event);
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
LoupedeckEvent::Disconnected => return false,
|
LoupedeckEvent::Disconnected => return false,
|
||||||
LoupedeckEvent::ButtonDown { button } => {
|
LoupedeckEvent::ButtonDown { button } => {
|
||||||
let position = ButtonPosition::of(&button);
|
let position = ButtonPosition::of(&button);
|
||||||
let button_config = &config.buttons[position];
|
let button_config = &context.config.buttons[position];
|
||||||
|
|
||||||
commands_sender
|
commands_sender
|
||||||
.send(StateChangeCommand::SetActivePages {
|
.send(StateChangeCommand::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(&context.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(&context.state.active_knob_page_id).clone(),
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
LoupedeckEvent::Touch { x, y, is_end, .. } => {
|
LoupedeckEvent::Touch { x, y, is_end, .. } => {
|
||||||
if is_end {
|
if is_end {
|
||||||
let characteristics = device.characteristics();
|
let characteristics = context.device.characteristics();
|
||||||
let display = characteristics.get_display_at_coordinates(x, y);
|
let display = characteristics.get_display_at_coordinates(x, y);
|
||||||
|
|
||||||
if let Some(display) = display {
|
if let Some(display) = display {
|
||||||
|
@ -178,11 +188,11 @@ fn handle_event(
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = KeyPath {
|
let path = KeyPath {
|
||||||
page_id: state.active_key_page_id.clone(),
|
page_id: context.state.active_key_page_id.clone(),
|
||||||
position,
|
position,
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = if state.get_key(&path).label.is_empty() {
|
let value = if context.state.get_key(&path).label.is_empty() {
|
||||||
"a".to_owned()
|
"a".to_owned()
|
||||||
} else {
|
} else {
|
||||||
String::new()
|
String::new()
|
||||||
|
@ -200,29 +210,29 @@ fn handle_event(
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_command(config: &model::config::Config, state: &mut State, device: &LoupedeckDevice, command: StateChangeCommand) {
|
fn handle_command(context: &mut IoWorkerContext, command: StateChangeCommand) {
|
||||||
debug!("Handling command: {:?}", &command);
|
debug!("Handling command: {:?}", &command);
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
StateChangeCommand::SetActivePages { key_page_id, knob_page_id } => {
|
StateChangeCommand::SetActivePages { key_page_id, knob_page_id } => {
|
||||||
state.active_key_page_id = key_page_id;
|
context.state.active_key_page_id = key_page_id;
|
||||||
state.active_knob_page_id = knob_page_id;
|
context.state.active_knob_page_id = knob_page_id;
|
||||||
|
|
||||||
for button in LoupedeckButton::VARIANTS {
|
for button in LoupedeckButton::VARIANTS {
|
||||||
let position = ButtonPosition::of(button);
|
let position = ButtonPosition::of(button);
|
||||||
|
|
||||||
device.set_button_color(*button, get_correct_button_color(config, state, position)).unwrap();
|
context.device.set_button_color(*button, get_correct_button_color(context, position)).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_grid = &device.characteristics().key_grid;
|
let key_grid = &context.device.characteristics().key_grid;
|
||||||
for index in 0..(key_grid.rows * key_grid.columns) {
|
for index in 0..(key_grid.rows * key_grid.columns) {
|
||||||
draw_key_at_index(device, state, index);
|
draw_key_at_index(context, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
device.refresh_display(&key_grid.display).unwrap();
|
context.device.refresh_display(&key_grid.display).unwrap();
|
||||||
}
|
}
|
||||||
StateChangeCommand::SetKeyLabel { path, value } => {
|
StateChangeCommand::SetKeyLabel { path, value } => {
|
||||||
state.mutate_key_for_command(
|
context.state.mutate_key_for_command(
|
||||||
"SetKeyLabel",
|
"SetKeyLabel",
|
||||||
&path,
|
&path,
|
||||||
Box::new(|k| {
|
Box::new(|k| {
|
||||||
|
@ -230,11 +240,11 @@ fn handle_command(config: &model::config::Config, state: &mut State, device: &Lo
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_key_at_path_if_visible(device, state, path);
|
draw_key_at_path_if_visible(context, path);
|
||||||
device.refresh_display(&device.characteristics().key_grid.display).unwrap();
|
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
||||||
}
|
}
|
||||||
StateChangeCommand::SetKeyIcon { path, value } => {
|
StateChangeCommand::SetKeyIcon { path, value } => {
|
||||||
state.mutate_key_for_command(
|
context.state.mutate_key_for_command(
|
||||||
"SetKeyIcon",
|
"SetKeyIcon",
|
||||||
&path,
|
&path,
|
||||||
Box::new(|k| {
|
Box::new(|k| {
|
||||||
|
@ -242,8 +252,8 @@ fn handle_command(config: &model::config::Config, state: &mut State, device: &Lo
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
draw_key_at_path_if_visible(device, state, path);
|
draw_key_at_path_if_visible(context, path);
|
||||||
device.refresh_display(&device.characteristics().key_grid.display).unwrap();
|
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,14 +261,14 @@ fn handle_command(config: &model::config::Config, state: &mut State, device: &Lo
|
||||||
// active -> config.active_button_color
|
// active -> config.active_button_color
|
||||||
// no actions defined -> #000000
|
// no actions defined -> #000000
|
||||||
// inactive -> config.inactive_button_color
|
// inactive -> config.inactive_button_color
|
||||||
fn get_correct_button_color(config: &model::config::Config, state: &mut State, button_position: ButtonPosition) -> RGB8 {
|
fn get_correct_button_color(context: &IoWorkerContext, button_position: ButtonPosition) -> RGB8 {
|
||||||
let button_config = &config.buttons[button_position];
|
let button_config = &context.config.buttons[button_position];
|
||||||
|
|
||||||
if let Some(key_page) = &button_config.key_page {
|
if let Some(key_page) = &button_config.key_page {
|
||||||
if key_page == &state.active_key_page_id {
|
if key_page == &context.state.active_key_page_id {
|
||||||
if let Some(knob_page) = &button_config.knob_page {
|
if let Some(knob_page) = &button_config.knob_page {
|
||||||
if knob_page == &state.active_knob_page_id {
|
if knob_page == &context.state.active_knob_page_id {
|
||||||
return config.active_button_color.into();
|
return context.config.active_button_color.into();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -266,7 +276,7 @@ fn get_correct_button_color(config: &model::config::Config, state: &mut State, b
|
||||||
return RGB8::new(0, 0, 0);
|
return RGB8::new(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.inactive_button_color.into()
|
context.config.inactive_button_color.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option<u8> {
|
fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option<u8> {
|
||||||
|
@ -289,32 +299,30 @@ fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_key(device: &LoupedeckDevice, key_grid: &LoupedeckDeviceKeyGridCharacteristics, index: u8, key: Option<&Key>) {
|
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 (x, y, w, h) = key_grid.get_local_key_rect_xywh(index).unwrap();
|
||||||
|
|
||||||
device
|
let p = render_key((w, h), key_grid.display.endianness, &context.icons, key);
|
||||||
.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, render_key(w, h, key_grid.display.endianness, key))
|
context.device.replace_framebuffer_area_raw(&key_grid.display, x, y, w, h, p).unwrap();
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_key_at_index(device: &LoupedeckDevice, state: &State, index: u8) {
|
fn draw_key_at_index(context: &IoWorkerContext, index: u8) {
|
||||||
let key_grid = &device.characteristics().key_grid;
|
let position = get_key_position_for_index(&context.device.characteristics().key_grid, index);
|
||||||
let position = get_key_position_for_index(key_grid, index);
|
|
||||||
|
|
||||||
draw_key(device, key_grid, index, state.active_key_page().keys_by_position.get(&position));
|
draw_key(context, index, context.state.active_key_page().keys_by_position.get(&position));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_key_at_position_if_visible(device: &LoupedeckDevice, state: &State, position: KeyPosition) {
|
fn draw_key_at_position_if_visible(context: &IoWorkerContext, position: KeyPosition) {
|
||||||
let key_grid = &device.characteristics().key_grid;
|
let index = get_key_index_for_position(&context.device.characteristics().key_grid, position);
|
||||||
let index = get_key_index_for_position(key_grid, position);
|
|
||||||
|
|
||||||
if let Some(index) = index {
|
if let Some(index) = index {
|
||||||
draw_key(device, key_grid, index, state.active_key_page().keys_by_position.get(&position));
|
draw_key(context, index, context.state.active_key_page().keys_by_position.get(&position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_key_at_path_if_visible(device: &LoupedeckDevice, state: &State, path: KeyPath) {
|
fn draw_key_at_path_if_visible(context: &IoWorkerContext, path: KeyPath) {
|
||||||
if state.active_key_page_id == path.page_id {
|
if context.state.active_key_page_id == path.page_id {
|
||||||
draw_key_at_position_if_visible(device, state, path.position);
|
draw_key_at_position_if_visible(context, path.position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue