This commit is contained in:
Moritz Ruth 2024-01-08 20:56:14 +01:00
parent a352885158
commit e7461a07c2
Signed by: moritzruth
GPG key ID: C9BBAB79405EE56D
14 changed files with 378 additions and 344 deletions

View file

@ -1,27 +1,16 @@
use color_eyre::{eyre::ContextCompat, Result};
use color_eyre::Result;
use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8};
use crate::model::image_filter::ImageFilter;
use crate::model::image_filter::ImageFilterDestructive;
pub fn apply(original: &Pixmap, filter: &ImageFilter) -> Result<Pixmap> {
let mut result = if let Some(rect) = filter.crop {
original.clone_rect(*rect).wrap_err_with(|| format!("Invalid crop rect: {}", rect))?
} else {
original.clone()
};
// scale, rotate and border are handled in runner::graphics::render_key
pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result<Pixmap> {
let mut result = original.clone();
for p in result.pixels_mut() {
if filter.invert {
*p = PremultipliedColorU8::from_rgba(p.alpha() - p.red(), p.alpha() - p.green(), p.alpha() - p.blue(), p.alpha()).unwrap();
}
if filter.alpha != 1.0 {
let d = p.demultiply();
*p = ColorU8::from_rgba(d.red(), d.green(), d.blue(), (d.alpha() as f32 * filter.alpha).round() as u8).premultiply();
}
if filter.grayscale {
let a = p.alpha();
if a > 0 {

View file

@ -9,70 +9,82 @@ use tiny_skia::{Pixmap, Transform};
use crate::model::config::{Config, IconFormat, IconPack};
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use crate::model::rgb::RGB8Wrapper;
use crate::model::IconMap;
use crate::model::image_filter::ImageFilterDestructive;
use crate::model::{key_page, knob_page};
mod filter;
mod destructive_filter;
type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>;
#[derive(Debug)]
pub struct LoadedIcon {
pub pixmap: Pixmap,
pub scale: f32,
pub clockwise_quarter_rotations: u8,
pub border: Option<RGB8Wrapper>,
pub pre_scale: f32,
}
pub fn get_used_icon_descriptors(config: &Config) -> HashSet<IconDescriptor> {
let mut result: HashSet<IconDescriptor> = HashSet::new();
fn insert_all_from_map<T>(result: &mut HashSet<IconDescriptor>, map: &IconMap<T>) {
fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &key_page::StyleByStateMap<T>) {
map.values().for_each(|v| {
result.insert(v.clone());
if let Some(icon) = &v.icon {
result.insert(icon.clone());
}
});
}
fn insert_all_from_knob_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &knob_page::StyleByStateMap<T>) {
map.values().for_each(|v| {
if let Some(icon) = &v.icon {
result.insert(icon.clone());
}
});
}
for page in config.key_pages_by_id.values() {
for key in page.keys.values() {
if let Some(d) = &key.icon {
if let Some(d) = &key.base_style.icon {
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__next {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
if let Some(c) = &key.mode.media__play_pause {
insert_all_from_map(&mut result, &c.icon);
}
if let Some(c) = &key.mode.media__play_pause {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
if let Some(c) = &key.mode.media__previous {
insert_all_from_map(&mut result, &c.icon);
}
if let Some(c) = &key.mode.media__previous {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
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__button {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
if let Some(c) = &key.mode.home_assistant__switch {
insert_all_from_map(&mut result, &c.icon);
}
if let Some(c) = &key.mode.home_assistant__switch {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
if let Some(c) = &key.mode.spotify__repeat {
insert_all_from_map(&mut result, &c.icon);
}
if let Some(c) = &key.mode.spotify__repeat {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
if let Some(c) = &key.mode.spotify__shuffle {
insert_all_from_map(&mut result, &c.icon);
}
if let Some(c) = &key.mode.spotify__shuffle {
insert_all_from_key_style_by_state_map(&mut result, &c.style);
}
}
}
for page in config.knob_pages_by_id.values() {
for knob in page.knobs.values() {
result.insert(knob.icon.clone());
if let Some(d) = &knob.base_style.icon {
result.insert(d.clone());
}
if let Some(c) = &knob.mode.audio_volume {
insert_all_from_map(&mut result, &c.icon);
insert_all_from_knob_style_by_state_map(&mut result, &c.style)
}
}
}
@ -85,15 +97,15 @@ pub fn load_icons(
icon_packs_by_id: &HashMap<String, IconPack>,
descriptors: HashSet<IconDescriptor>,
dpi: f32,
) -> Result<HashMap<IconDescriptor, LoadedIcon>> {
) -> Result<LoadedIconsMap> {
let mut highest_scale_by_source: HashMap<IconDescriptorSource, f32> = HashMap::new();
for d in &descriptors {
let mut scale = d.filter.scale;
let mut scale = d.filter.transform.scale;
if let IconDescriptorSource::IconPack { pack_id, .. } = &d.source {
let pack = &icon_packs_by_id[pack_id];
if let Some(filter) = &pack.global_filter {
scale *= filter.scale;
scale *= filter.transform.scale;
}
}
@ -106,17 +118,31 @@ pub fn load_icons(
}
}
let mut unfiltered_scaled_pixmap_by_source: HashMap<IconDescriptorSource, (Pixmap, f32)> = HashMap::new();
let mut icons_by_descriptor: HashMap<IconDescriptor, LoadedIcon> = HashMap::new();
let mut unfiltered_pixmap_and_scale_by_source: HashMap<IconDescriptorSource, (Pixmap, f32)> = HashMap::new();
let mut icons: LoadedIconsMap = HashMap::new();
let mut fonts_db = resvg::usvg::fontdb::Database::new();
fonts_db.load_system_fonts();
for descriptor in descriptors {
if descriptor.source == IconDescriptorSource::None {
let icon_pack = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source {
Some(&icon_packs_by_id[pack_id])
} else {
None
};
let filter = if let Some(global_filter) = icon_pack.map(|p| p.global_filter).flatten() {
descriptor.filter.destructive.combine_after(&global_filter.destructive)
} else {
descriptor.filter.destructive
};
let id = (descriptor.source, filter);
if descriptor.source == IconDescriptorSource::None || icons.contains(&id) {
continue;
}
let (original_image, original_image_scale) = match unfiltered_scaled_pixmap_by_source.entry(descriptor.source.clone()) {
let (original_image, original_image_scale) = match unfiltered_pixmap_and_scale_by_source.entry(descriptor.source.clone()) {
Entry::Occupied(o) => o.into_mut(),
Entry::Vacant(v) => v.insert(read_image_and_get_scale(
config_directory,
@ -128,35 +154,18 @@ pub fn load_icons(
)?),
};
let (pixmap, scale) = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source {
let pack = &icon_packs_by_id[pack_id];
if let Some(global_filter) = &pack.global_filter {
(
filter::apply(&filter::apply(original_image, global_filter)?, &descriptor.filter)?,
descriptor.filter.scale * global_filter.scale,
)
} else {
(filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale)
}
} else {
(filter::apply(original_image, &descriptor.filter)?, descriptor.filter.scale)
};
let pixmap = destructive_filter::apply(original_image, &filter)?;
let scale = scale / *original_image_scale;
let clockwise_quarter_rotations = descriptor.filter.clockwise_quarter_rotations;
let border = descriptor.filter.border;
icons_by_descriptor.insert(
descriptor,
icons.insert(
id,
LoadedIcon {
pixmap,
scale,
clockwise_quarter_rotations,
border,
pre_scale: *original_image_scale,
},
);
}
Ok(icons_by_descriptor)
Ok(icons)
}
fn read_image_and_get_scale(

View file

@ -8,7 +8,7 @@ use thiserror::Error;
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError};
#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
#[derive(Debug, Default, Eq, PartialEq, Hash, Clone, Copy, SerializeDisplay, DeserializeFromStr)]
pub struct IconDescriptor {
pub source: IconDescriptorSource,
pub filter: ImageFilter,
@ -76,6 +76,15 @@ pub enum IconDescriptorSource {
Path(PathBuf),
}
impl IconDescriptorSource {
pub fn pack_id(&self) -> Option<&String> {
match self {
IconDescriptorSource::IconPack { pack_id, .. } => Some(pack_id),
_ => None,
}
}
}
impl Display for IconDescriptorSource {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -8,16 +8,45 @@ use thiserror::Error;
use crate::model::geometry::IntRectWrapper;
use crate::model::rgb::RGB8Wrapper;
#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)]
pub struct ImageFilter {
pub crop: Option<IntRectWrapper>,
#[derive(Debug, PartialEq, Clone)]
pub struct ImageFilterTransform {
pub scale: f32,
// Must be in 0..=3
pub clockwise_quarter_rotations: u8,
pub color: Option<RGB8Wrapper>,
pub alpha: f32,
}
impl ImageFilterTransform {
pub fn combine_after(&self, other: &ImageFilterTransform) -> ImageFilterTransform {
ImageFilterTransform {
scale: self.scale * other.scale,
alpha: self.alpha * other.alpha,
clockwise_quarter_rotations: (self.clockwise_quarter_rotations + other.clockwise_quarter_rotations) % 4,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct ImageFilterDestructive {
pub color: Option<RGB8Wrapper>,
pub grayscale: bool,
pub invert: bool,
pub border: Option<RGB8Wrapper>,
}
impl ImageFilterDestructive {
pub fn combine_after(&self, other: &ImageFilterDestructive) -> ImageFilterDestructive {
ImageFilterDestructive {
color: self.color.or(other.color),
grayscale: self.grayscale || other.grayscale,
invert: self.invert ^ other.invert,
}
}
}
#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)]
pub struct ImageFilter {
pub transform: ImageFilterTransform,
pub destructive: ImageFilterDestructive,
}
impl Eq for ImageFilter {
@ -30,23 +59,25 @@ impl Hash for ImageFilter {
}
}
const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter {
scale: 1.0,
clockwise_quarter_rotations: 0,
crop: None,
color: None,
alpha: 1.0,
grayscale: false,
invert: false,
border: None,
};
impl Default for ImageFilter {
fn default() -> Self {
DEFAULT_IMAGE_FILTER.clone()
}
}
const DEFAULT_IMAGE_FILTER: ImageFilter = ImageFilter {
transform: ImageFilterTransform {
scale: 1.0,
clockwise_quarter_rotations: 0,
alpha: 1.0,
},
destructive: ImageFilterDestructive {
color: None,
grayscale: false,
invert: false,
},
};
#[derive(Debug, Error)]
pub enum ImageFilterFromStringError {
#[error("Unknown filter: {name}")]
@ -141,8 +172,7 @@ impl FromStr for ImageFilter {
};
match filter_name.as_str() {
"crop" => result.crop = Some(parse_rect_filter_value(&filter_name, use_raw_value()?)?),
"scale" => result.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
"scale" => result.transform.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
"rotate" => {
let raw_value = use_raw_value()?;
let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
@ -150,7 +180,7 @@ impl FromStr for ImageFilter {
raw_value,
})?;
result.clockwise_quarter_rotations = match value {
result.transform.clockwise_quarter_rotations = match value {
0 => 0,
90 => 1,
180 => 2,
@ -158,29 +188,19 @@ impl FromStr for ImageFilter {
_ => return Err(ImageFilterFromStringError::RotationNotAllowed),
};
}
"alpha" => result.transform.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?,
"color" => {
let raw_value = use_raw_value()?;
result.color = Some(
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?,
)
}
"alpha" => result.alpha = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..<1", Box::new(|v| (0.0..1.0).contains(v)))?,
"grayscale" => result.grayscale = use_bool_value()?,
"invert" => result.invert = use_bool_value()?,
"border" => {
let raw_value = use_raw_value()?;
result.border = Some(
result.destructive.color = Some(
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
filter_name: filter_name.to_string(),
raw_value,
})?,
)
}
"grayscale" => result.destructive.grayscale = use_bool_value()?,
"invert" => result.destructive.invert = use_bool_value()?,
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
};
}
@ -193,32 +213,32 @@ impl Display for ImageFilter {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut is_first = true;
if let Some(rect) = &self.crop {
// if !is_first {
// f.write_str("|")?
// }
f.write_fmt(format_args!("crop={}", rect))?;
is_first = false;
}
if self.scale != DEFAULT_IMAGE_FILTER.scale {
if self.transform.scale != DEFAULT_IMAGE_FILTER.transform.scale {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("scale={}", self.scale))?;
f.write_fmt(format_args!("scale={}", self.transform.scale))?;
is_first = false;
}
if self.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.clockwise_quarter_rotations {
if self.transform.clockwise_quarter_rotations != DEFAULT_IMAGE_FILTER.transform.clockwise_quarter_rotations {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("rotate={}", self.clockwise_quarter_rotations as u16 * 90))?;
f.write_fmt(format_args!("rotate={}", self.transform.clockwise_quarter_rotations as u16 * 90))?;
is_first = false;
}
if let Some(color) = self.color {
if self.transform.alpha != DEFAULT_IMAGE_FILTER.transform.alpha {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("alpha={}", self.transform.alpha))?;
is_first = false;
}
if let Some(color) = self.destructive.color {
if !is_first {
f.write_str("|")?
}
@ -227,15 +247,7 @@ impl Display for ImageFilter {
is_first = false;
}
if self.alpha != DEFAULT_IMAGE_FILTER.alpha {
if !is_first {
f.write_str("|")?
}
f.write_fmt(format_args!("alpha={}", self.alpha))?;
is_first = false;
}
if self.grayscale {
if self.destructive.grayscale {
if !is_first {
f.write_str("|")?
}
@ -243,7 +255,7 @@ impl Display for ImageFilter {
is_first = false;
}
if self.invert {
if self.destructive.invert {
if !is_first {
f.write_str("|")?
}
@ -251,7 +263,7 @@ impl Display for ImageFilter {
is_first = false;
}
if let Some(color) = self.color {
if let Some(color) = self.destructive.color {
if !is_first {
f.write_str("|")?
}

View file

@ -1,21 +1,19 @@
use std::collections::HashMap;
use serde::Deserialize;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::key_page::StyleByStateMap;
#[derive(Debug, Deserialize)]
pub struct SwitchConfig {
pub name: String,
#[serde(default)]
pub icon: HashMap<SwitchState, IconDescriptor>,
pub style: StyleByStateMap<SwitchState>,
}
#[derive(Debug, Deserialize)]
pub struct ButtonConfig {
pub name: String,
#[serde(default)]
pub icon: HashMap<ButtonState, IconDescriptor>,
pub style: StyleByStateMap<ButtonState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]

View file

@ -1,13 +1,11 @@
use std::collections::HashMap;
use serde::Deserialize;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::key_page::StyleByStateMap;
#[derive(Debug, Deserialize)]
pub struct PlayPauseConfig {
#[serde(default)]
pub icon: HashMap<PlayPauseState, IconDescriptor>,
pub style: StyleByStateMap<PlayPauseState>,
#[serde(default)]
pub action: PlayPauseAction,
}
@ -24,7 +22,7 @@ pub enum PlayPauseAction {
#[derive(Debug, Deserialize)]
pub struct PreviousAndNextConfig {
#[serde(default)]
pub icon: HashMap<PreviousAndNextState, IconDescriptor>,
pub style: StyleByStateMap<PreviousAndNextState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]

View file

@ -1,19 +1,17 @@
use std::collections::HashMap;
use serde::Deserialize;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::key_page::StyleByStateMap;
#[derive(Debug, Deserialize)]
pub struct ShuffleConfig {
#[serde(default)]
pub icon: HashMap<ShuffleState, IconDescriptor>,
pub style: StyleByStateMap<ShuffleState>,
}
#[derive(Debug, Deserialize)]
pub struct RepeatConfig {
#[serde(default)]
pub icon: HashMap<RepeatState, IconDescriptor>,
pub style: StyleByStateMap<RepeatState>,
}
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]

View file

@ -4,7 +4,9 @@ use serde::Deserialize;
use crate::model::geometry::UIntVec2;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::{key_modes, KeyPosition, KnobPosition};
use crate::model::key_modes;
use crate::model::position::{KeyPosition, KnobPosition};
use crate::model::rgb::RGB8WithOptionalA;
#[derive(Debug, Deserialize)]
pub struct File {
@ -44,15 +46,33 @@ pub enum ScrollingConfigAxis {
}
#[derive(Debug, Deserialize)]
pub struct KeyConfig {
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.or_else(|| base.label.clone()),
icon: self.icon.or(base.icon),
border: self.border.or(base.border),
}
}
}
#[derive(Debug, Deserialize)]
pub struct KeyConfig {
#[serde(default, flatten)]
pub base_style: KeyStyle,
#[serde(default)]
pub mode: KeyModes,
}
#[allow(non_snake_case)]
#[derive(Debug, Deserialize, Default)]
#[derive(Debug, Default, Deserialize)]
pub struct KeyModes {
pub vibrate: Option<key_modes::vibrate::Config>,
pub media__play_pause: Option<key_modes::media::PlayPauseConfig>,
@ -63,3 +83,5 @@ pub struct KeyModes {
pub home_assistant__switch: Option<key_modes::home_assistant::SwitchConfig>,
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>,
}
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;

View file

@ -1,11 +1,9 @@
use std::collections::HashMap;
use std::num::NonZeroU8;
use regex::Regex;
use serde::Deserialize;
use crate::model::rgb::RGB8WithOptionalA;
use crate::model::IconMap;
use crate::model::knob_page::StyleByStateMap;
#[derive(Debug, Deserialize)]
pub struct Config {
@ -22,9 +20,7 @@ pub struct Config {
#[serde(default)]
pub label: HashMap<State, String>,
#[serde(default)]
pub icon: IconMap<State>,
pub circle_indicator: Option<CircleIndicatorConfig>,
pub bar_indicator: Option<BarIndicatorConfig>,
pub style: StyleByStateMap<State>,
}
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
@ -51,15 +47,3 @@ pub enum State {
Active,
Muted,
}
#[derive(Debug, Deserialize)]
pub struct CircleIndicatorConfig {
pub color: RGB8WithOptionalA,
pub width: NonZeroU8,
pub radius: u8,
}
#[derive(Debug, Deserialize)]
pub struct BarIndicatorConfig {
pub color: RGB8WithOptionalA,
}

View file

@ -4,8 +4,9 @@ use enum_map::EnumMap;
use serde::Deserialize;
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::knob_modes;
use crate::model::position::KnobPosition;
use crate::model::rgb::RGB8WithOptionalA;
use crate::model::{knob_modes, KnobPosition};
#[derive(Debug, Deserialize)]
pub struct File {
@ -21,12 +22,8 @@ pub struct Page {
#[derive(Debug, Default, Deserialize)]
pub struct Knob {
#[serde(default)]
pub label: String,
#[serde(default)]
pub icon: IconDescriptor,
#[serde(default)]
pub indicator: KnobIndicators,
#[serde(default, flatten)]
pub base_style: KnobStyle,
#[serde(default)]
pub mode: KnobModes,
}
@ -39,17 +36,26 @@ pub struct KnobIndicators {
#[derive(Debug, Deserialize)]
pub struct KnobIndicatorBarConfig {
pub color: RGB8WithOptionalA,
pub color: Option<RGB8WithOptionalA>,
}
#[derive(Debug, Deserialize)]
pub struct KnobIndicatorCircleConfig {
pub color: RGB8WithOptionalA,
pub width: u8,
pub radius: u8,
pub color: Option<RGB8WithOptionalA>,
pub width: Option<u8>,
pub radius: Option<u8>,
}
#[derive(Debug, Default, Deserialize)]
pub struct KnobStyle {
pub label: Option<String>,
pub icon: Option<IconDescriptor>,
pub indicators: Option<KnobIndicators>,
}
#[derive(Debug, Default, Deserialize)]
pub struct KnobModes {
pub audio_volume: Option<knob_modes::audio_volume::Config>,
}
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;

View file

@ -1,16 +1,3 @@
use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use enum_map::Enum;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
use loupedeck_serial::characteristics::LoupedeckButton;
use crate::model::icon_descriptor::IconDescriptor;
pub mod config;
pub mod geometry;
pub mod icon_descriptor;
@ -19,114 +6,5 @@ pub mod key_modes;
pub mod key_page;
pub mod knob_modes;
pub mod knob_page;
pub mod position;
pub mod rgb;
#[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,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)]
#[serde(rename_all = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
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)]
pub enum ButtonPosition {
#[serde(rename = "0")]
N0,
#[serde(rename = "1")]
N1,
#[serde(rename = "2")]
N2,
#[serde(rename = "3")]
N3,
#[serde(rename = "4")]
N4,
#[serde(rename = "5")]
N5,
#[serde(rename = "6")]
N6,
#[serde(rename = "7")]
N7,
}
impl ButtonPosition {
pub fn of(button: &LoupedeckButton) -> Self {
match button {
LoupedeckButton::N0 => Self::N0,
LoupedeckButton::N1 => Self::N1,
LoupedeckButton::N2 => Self::N2,
LoupedeckButton::N3 => Self::N3,
LoupedeckButton::N4 => Self::N4,
LoupedeckButton::N5 => Self::N5,
LoupedeckButton::N6 => Self::N6,
LoupedeckButton::N7 => Self::N7,
}
}
}
impl Display for ButtonPosition {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
ButtonPosition::N0 => "0",
ButtonPosition::N1 => "1",
ButtonPosition::N2 => "2",
ButtonPosition::N3 => "3",
ButtonPosition::N4 => "4",
ButtonPosition::N5 => "5",
ButtonPosition::N6 => "6",
ButtonPosition::N7 => "7",
})
}
}
pub type IconMap<State> = HashMap<State, IconDescriptor>;

View file

@ -0,0 +1,117 @@
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use enum_map::Enum;
use serde::{Deserialize, Serialize};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use thiserror::Error;
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,
}
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Serialize, Deserialize, Enum)]
#[serde(rename_all = "kebab-case")]
pub enum KnobPosition {
LeftTop,
LeftMiddle,
LeftBottom,
RightTop,
RightMiddle,
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)]
pub enum ButtonPosition {
#[serde(rename = "0")]
N0,
#[serde(rename = "1")]
N1,
#[serde(rename = "2")]
N2,
#[serde(rename = "3")]
N3,
#[serde(rename = "4")]
N4,
#[serde(rename = "5")]
N5,
#[serde(rename = "6")]
N6,
#[serde(rename = "7")]
N7,
}
impl ButtonPosition {
pub fn of(button: &LoupedeckButton) -> Self {
match button {
LoupedeckButton::N0 => Self::N0,
LoupedeckButton::N1 => Self::N1,
LoupedeckButton::N2 => Self::N2,
LoupedeckButton::N3 => Self::N3,
LoupedeckButton::N4 => Self::N4,
LoupedeckButton::N5 => Self::N5,
LoupedeckButton::N6 => Self::N6,
LoupedeckButton::N7 => Self::N7,
}
}
}
impl Display for ButtonPosition {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
ButtonPosition::N0 => "0",
ButtonPosition::N1 => "1",
ButtonPosition::N2 => "2",
ButtonPosition::N3 => "3",
ButtonPosition::N4 => "4",
ButtonPosition::N5 => "5",
ButtonPosition::N6 => "6",
ButtonPosition::N7 => "7",
})
}
}

View file

@ -1,3 +1,4 @@
use std::cell::RefCell;
use std::collections::HashMap;
use bytes::{BufMut, Bytes, BytesMut};
@ -10,24 +11,38 @@ use loupedeck_serial::util::Endianness;
use crate::icons::LoadedIcon;
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
use crate::model::image_filter::ImageFilterTransform;
use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::state::Key;
pub fn render_key(
label_renderer: &mut LabelRenderer,
key_size: IntSize,
#[derive(Debug)]
struct GraphicsContext {
label_renderer: RefCell<LabelRenderer>,
buffer_endianness: Endianness,
icons: &HashMap<IconDescriptor, LoadedIcon>,
state: Option<&Key>,
) -> Bytes {
global_icon_filter_by_pack_id: HashMap<String, ImageFilterTransform>,
loaded_icons: HashMap<IconDescriptor, LoadedIcon>,
}
pub fn render_key(context: &GraphicsContext, key_size: IntSize, state: Option<&Key>) -> Bytes {
let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap();
if let Some(state) = state {
if state.icon.source != IconDescriptorSource::None {
let icon = &icons[&state.icon];
let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height())
let style = state.style.merge_over(&state.base_style);
if style.icon.source != IconDescriptorSource::None {
let loaded_icon = &context.loaded_icons[&state.icon];
let filter = if let Some(global_filter) = state.icon.source.pack_id().map(|i| context.global_icon_filter_by_pack_id.get(i)).flatten() {
&state.icon.filter.transform.combine_after(global_filter)
} else {
&state.icon.filter.transform
};
let scale = filter.scale / loaded_icon.pre_scale;
let scaled_size = IntSize::from_wh(loaded_icon.pixmap.width(), loaded_icon.pixmap.height())
.unwrap()
.scale_by(icon.scale)
.scale_by(scale)
.unwrap();
static PAINT: PixmapPaint = PixmapPaint {
@ -37,43 +52,41 @@ pub fn render_key(
};
pixmap.draw_pixmap(
(((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / icon.scale).round() as i32,
(((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / icon.scale).round() as i32,
icon.pixmap.as_ref(),
(((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / scale).round() as i32,
(((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / scale).round() as i32,
loaded_icon.pixmap.as_ref(),
&PAINT,
Transform::from_scale(icon.scale, icon.scale).post_rotate_at(
(icon.clockwise_quarter_rotations as f32) * 90.0,
Transform::from_scale(scale, scale).post_rotate_at(
(filter.clockwise_quarter_rotations as f32) * 90.0,
key_size.width() as f32 / 2.0,
key_size.height() as f32 / 2.0,
),
None,
);
if let Some(color) = icon.border {
let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap());
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, 255)),
..Paint::default()
};
static STROKE: Stroke = Stroke {
width: 15.0,
miter_limit: 4.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Round,
dash: None,
};
pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None);
}
}
if !state.label.is_empty() {
label_renderer.render(&mut pixmap, &state.label);
context.label_renderer.borrow_mut().render(&mut pixmap, &state.label);
}
if let Some(color) = state.st {
let path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap());
let paint = Paint {
shader: Shader::SolidColor(Color::from_rgba8(color.r, color.g, color.b, 255)),
..Paint::default()
};
static STROKE: Stroke = Stroke {
width: 15.0,
miter_limit: 4.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Round,
dash: None,
};
pixmap.stroke_path(&path, &paint, &STROKE, Transform::identity(), None);
}
} else {
pixmap.fill(Color::BLACK);
}
convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze()

View file

@ -5,7 +5,8 @@ use log::error;
use serde::{Deserialize, Serialize};
use crate::model::icon_descriptor::IconDescriptor;
use crate::model::{KeyPath, KeyPosition, KnobPath, KnobPosition};
use crate::model::key_page::KeyStyle;
use crate::model::position::{KeyPath, KeyPosition, KnobPath, KnobPosition};
#[derive(Debug)]
pub struct State {
@ -56,8 +57,8 @@ pub struct KnobPage {
#[derive(Debug)]
pub struct Key {
pub path: KeyPath,
pub icon: IconDescriptor,
pub label: String,
pub base_style: KeyStyle,
pub style: KeyStyle,
}
#[derive(Debug)]