commit
This commit is contained in:
parent
a352885158
commit
e7461a07c2
14 changed files with 378 additions and 344 deletions
|
@ -1,27 +1,16 @@
|
||||||
use color_eyre::{eyre::ContextCompat, Result};
|
use color_eyre::Result;
|
||||||
use tiny_skia::{ColorU8, Pixmap, PremultipliedColorU8};
|
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> {
|
pub fn apply(original: &Pixmap, filter: &ImageFilterDestructive) -> Result<Pixmap> {
|
||||||
let mut result = if let Some(rect) = filter.crop {
|
let mut result = original.clone();
|
||||||
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
|
|
||||||
|
|
||||||
for p in result.pixels_mut() {
|
for p in result.pixels_mut() {
|
||||||
if filter.invert {
|
if filter.invert {
|
||||||
*p = PremultipliedColorU8::from_rgba(p.alpha() - p.red(), p.alpha() - p.green(), p.alpha() - p.blue(), p.alpha()).unwrap();
|
*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 {
|
if filter.grayscale {
|
||||||
let a = p.alpha();
|
let a = p.alpha();
|
||||||
if a > 0 {
|
if a > 0 {
|
|
@ -9,70 +9,82 @@ use tiny_skia::{Pixmap, Transform};
|
||||||
|
|
||||||
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::rgb::RGB8Wrapper;
|
use crate::model::image_filter::ImageFilterDestructive;
|
||||||
use crate::model::IconMap;
|
use crate::model::{key_page, knob_page};
|
||||||
|
|
||||||
mod filter;
|
mod destructive_filter;
|
||||||
|
|
||||||
|
type LoadedIconsMap = HashMap<(IconDescriptorSource, ImageFilterDestructive), LoadedIcon>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct LoadedIcon {
|
pub struct LoadedIcon {
|
||||||
pub pixmap: Pixmap,
|
pub pixmap: Pixmap,
|
||||||
pub scale: f32,
|
pub pre_scale: f32,
|
||||||
pub clockwise_quarter_rotations: u8,
|
|
||||||
pub border: Option<RGB8Wrapper>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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>) {
|
fn insert_all_from_key_style_by_state_map<T>(result: &mut HashSet<IconDescriptor>, map: &key_page::StyleByStateMap<T>) {
|
||||||
map.values().for_each(|v| {
|
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 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.base_style.icon {
|
||||||
result.insert(d.clone());
|
result.insert(d.clone());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.media__next {
|
if let Some(c) = &key.mode.media__next {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.media__play_pause {
|
if let Some(c) = &key.mode.media__play_pause {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.media__previous {
|
if let Some(c) = &key.mode.media__previous {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.home_assistant__button {
|
if let Some(c) = &key.mode.home_assistant__button {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.home_assistant__switch {
|
if let Some(c) = &key.mode.home_assistant__switch {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.spotify__repeat {
|
if let Some(c) = &key.mode.spotify__repeat {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(c) = &key.mode.spotify__shuffle {
|
if let Some(c) = &key.mode.spotify__shuffle {
|
||||||
insert_all_from_map(&mut result, &c.icon);
|
insert_all_from_key_style_by_state_map(&mut result, &c.style);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for page in config.knob_pages_by_id.values() {
|
for page in config.knob_pages_by_id.values() {
|
||||||
for knob in page.knobs.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 {
|
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>,
|
icon_packs_by_id: &HashMap<String, IconPack>,
|
||||||
descriptors: HashSet<IconDescriptor>,
|
descriptors: HashSet<IconDescriptor>,
|
||||||
dpi: f32,
|
dpi: f32,
|
||||||
) -> Result<HashMap<IconDescriptor, LoadedIcon>> {
|
) -> Result<LoadedIconsMap> {
|
||||||
let mut highest_scale_by_source: HashMap<IconDescriptorSource, f32> = HashMap::new();
|
let mut highest_scale_by_source: HashMap<IconDescriptorSource, f32> = HashMap::new();
|
||||||
|
|
||||||
for d in &descriptors {
|
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 {
|
if let IconDescriptorSource::IconPack { pack_id, .. } = &d.source {
|
||||||
let pack = &icon_packs_by_id[pack_id];
|
let pack = &icon_packs_by_id[pack_id];
|
||||||
if let Some(filter) = &pack.global_filter {
|
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 unfiltered_pixmap_and_scale_by_source: HashMap<IconDescriptorSource, (Pixmap, f32)> = HashMap::new();
|
||||||
let mut icons_by_descriptor: HashMap<IconDescriptor, LoadedIcon> = HashMap::new();
|
let mut icons: LoadedIconsMap = HashMap::new();
|
||||||
let mut fonts_db = resvg::usvg::fontdb::Database::new();
|
let mut fonts_db = resvg::usvg::fontdb::Database::new();
|
||||||
fonts_db.load_system_fonts();
|
fonts_db.load_system_fonts();
|
||||||
|
|
||||||
for descriptor in descriptors {
|
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;
|
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::Occupied(o) => o.into_mut(),
|
||||||
Entry::Vacant(v) => v.insert(read_image_and_get_scale(
|
Entry::Vacant(v) => v.insert(read_image_and_get_scale(
|
||||||
config_directory,
|
config_directory,
|
||||||
|
@ -128,35 +154,18 @@ pub fn load_icons(
|
||||||
)?),
|
)?),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (pixmap, scale) = if let IconDescriptorSource::IconPack { pack_id, .. } = &descriptor.source {
|
let pixmap = destructive_filter::apply(original_image, &filter)?;
|
||||||
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 scale = scale / *original_image_scale;
|
icons.insert(
|
||||||
let clockwise_quarter_rotations = descriptor.filter.clockwise_quarter_rotations;
|
id,
|
||||||
let border = descriptor.filter.border;
|
|
||||||
icons_by_descriptor.insert(
|
|
||||||
descriptor,
|
|
||||||
LoadedIcon {
|
LoadedIcon {
|
||||||
pixmap,
|
pixmap,
|
||||||
scale,
|
pre_scale: *original_image_scale,
|
||||||
clockwise_quarter_rotations,
|
|
||||||
border,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(icons_by_descriptor)
|
Ok(icons)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_image_and_get_scale(
|
fn read_image_and_get_scale(
|
||||||
|
|
|
@ -8,7 +8,7 @@ use thiserror::Error;
|
||||||
|
|
||||||
use crate::model::image_filter::{ImageFilter, ImageFilterFromStringError};
|
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 struct IconDescriptor {
|
||||||
pub source: IconDescriptorSource,
|
pub source: IconDescriptorSource,
|
||||||
pub filter: ImageFilter,
|
pub filter: ImageFilter,
|
||||||
|
@ -76,6 +76,15 @@ pub enum IconDescriptorSource {
|
||||||
Path(PathBuf),
|
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 {
|
impl Display for IconDescriptorSource {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
@ -8,16 +8,45 @@ use thiserror::Error;
|
||||||
use crate::model::geometry::IntRectWrapper;
|
use crate::model::geometry::IntRectWrapper;
|
||||||
use crate::model::rgb::RGB8Wrapper;
|
use crate::model::rgb::RGB8Wrapper;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, SerializeDisplay, DeserializeFromStr)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub struct ImageFilter {
|
pub struct ImageFilterTransform {
|
||||||
pub crop: Option<IntRectWrapper>,
|
|
||||||
pub scale: f32,
|
pub scale: f32,
|
||||||
|
// Must be in 0..=3
|
||||||
pub clockwise_quarter_rotations: u8,
|
pub clockwise_quarter_rotations: u8,
|
||||||
pub color: Option<RGB8Wrapper>,
|
|
||||||
pub alpha: f32,
|
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 grayscale: bool,
|
||||||
pub invert: 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 {
|
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 {
|
impl Default for ImageFilter {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DEFAULT_IMAGE_FILTER.clone()
|
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)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ImageFilterFromStringError {
|
pub enum ImageFilterFromStringError {
|
||||||
#[error("Unknown filter: {name}")]
|
#[error("Unknown filter: {name}")]
|
||||||
|
@ -141,8 +172,7 @@ impl FromStr for ImageFilter {
|
||||||
};
|
};
|
||||||
|
|
||||||
match filter_name.as_str() {
|
match filter_name.as_str() {
|
||||||
"crop" => result.crop = Some(parse_rect_filter_value(&filter_name, use_raw_value()?)?),
|
"scale" => result.transform.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
|
||||||
"scale" => result.scale = parse_f32_filter_value(&filter_name, use_raw_value()?, "0..=100", Box::new(|v| (0.0..=100.0).contains(v)))?,
|
|
||||||
"rotate" => {
|
"rotate" => {
|
||||||
let raw_value = use_raw_value()?;
|
let raw_value = use_raw_value()?;
|
||||||
let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
|
let value = i32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
|
||||||
|
@ -150,7 +180,7 @@ impl FromStr for ImageFilter {
|
||||||
raw_value,
|
raw_value,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
result.clockwise_quarter_rotations = match value {
|
result.transform.clockwise_quarter_rotations = match value {
|
||||||
0 => 0,
|
0 => 0,
|
||||||
90 => 1,
|
90 => 1,
|
||||||
180 => 2,
|
180 => 2,
|
||||||
|
@ -158,29 +188,19 @@ impl FromStr for ImageFilter {
|
||||||
_ => return Err(ImageFilterFromStringError::RotationNotAllowed),
|
_ => 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" => {
|
"color" => {
|
||||||
let raw_value = use_raw_value()?;
|
let raw_value = use_raw_value()?;
|
||||||
|
|
||||||
result.color = Some(
|
result.destructive.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(
|
|
||||||
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
|
RGB8Wrapper::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
|
||||||
filter_name: filter_name.to_string(),
|
filter_name: filter_name.to_string(),
|
||||||
raw_value,
|
raw_value,
|
||||||
})?,
|
})?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
"grayscale" => result.destructive.grayscale = use_bool_value()?,
|
||||||
|
"invert" => result.destructive.invert = use_bool_value()?,
|
||||||
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
|
_ => return Err(ImageFilterFromStringError::UnknownFilter { name: filter_name }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -193,32 +213,32 @@ impl Display for ImageFilter {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
let mut is_first = true;
|
let mut is_first = true;
|
||||||
|
|
||||||
if let Some(rect) = &self.crop {
|
if self.transform.scale != DEFAULT_IMAGE_FILTER.transform.scale {
|
||||||
// if !is_first {
|
|
||||||
// f.write_str("|")?
|
|
||||||
// }
|
|
||||||
f.write_fmt(format_args!("crop={}", rect))?;
|
|
||||||
is_first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.scale != DEFAULT_IMAGE_FILTER.scale {
|
|
||||||
if !is_first {
|
if !is_first {
|
||||||
f.write_str("|")?
|
f.write_str("|")?
|
||||||
}
|
}
|
||||||
f.write_fmt(format_args!("scale={}", self.scale))?;
|
f.write_fmt(format_args!("scale={}", self.transform.scale))?;
|
||||||
is_first = false;
|
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 {
|
if !is_first {
|
||||||
f.write_str("|")?
|
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;
|
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 {
|
if !is_first {
|
||||||
f.write_str("|")?
|
f.write_str("|")?
|
||||||
}
|
}
|
||||||
|
@ -227,15 +247,7 @@ impl Display for ImageFilter {
|
||||||
is_first = false;
|
is_first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.alpha != DEFAULT_IMAGE_FILTER.alpha {
|
if self.destructive.grayscale {
|
||||||
if !is_first {
|
|
||||||
f.write_str("|")?
|
|
||||||
}
|
|
||||||
f.write_fmt(format_args!("alpha={}", self.alpha))?;
|
|
||||||
is_first = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.grayscale {
|
|
||||||
if !is_first {
|
if !is_first {
|
||||||
f.write_str("|")?
|
f.write_str("|")?
|
||||||
}
|
}
|
||||||
|
@ -243,7 +255,7 @@ impl Display for ImageFilter {
|
||||||
is_first = false;
|
is_first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.invert {
|
if self.destructive.invert {
|
||||||
if !is_first {
|
if !is_first {
|
||||||
f.write_str("|")?
|
f.write_str("|")?
|
||||||
}
|
}
|
||||||
|
@ -251,7 +263,7 @@ impl Display for ImageFilter {
|
||||||
is_first = false;
|
is_first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(color) = self.color {
|
if let Some(color) = self.destructive.color {
|
||||||
if !is_first {
|
if !is_first {
|
||||||
f.write_str("|")?
|
f.write_str("|")?
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::key_page::StyleByStateMap;
|
||||||
|
|
||||||
#[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, IconDescriptor>,
|
pub style: StyleByStateMap<SwitchState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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, IconDescriptor>,
|
pub style: StyleByStateMap<ButtonState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::key_page::StyleByStateMap;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PlayPauseConfig {
|
pub struct PlayPauseConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PlayPauseState, IconDescriptor>,
|
pub style: StyleByStateMap<PlayPauseState>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub action: PlayPauseAction,
|
pub action: PlayPauseAction,
|
||||||
}
|
}
|
||||||
|
@ -24,7 +22,7 @@ pub enum PlayPauseAction {
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct PreviousAndNextConfig {
|
pub struct PreviousAndNextConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<PreviousAndNextState, IconDescriptor>,
|
pub style: StyleByStateMap<PreviousAndNextState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::key_page::StyleByStateMap;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct ShuffleConfig {
|
pub struct ShuffleConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<ShuffleState, IconDescriptor>,
|
pub style: StyleByStateMap<ShuffleState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RepeatConfig {
|
pub struct RepeatConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: HashMap<RepeatState, IconDescriptor>,
|
pub style: StyleByStateMap<RepeatState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
#[derive(Debug, Eq, PartialEq, Hash, Deserialize)]
|
||||||
|
|
|
@ -4,7 +4,9 @@ use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::geometry::UIntVec2;
|
use crate::model::geometry::UIntVec2;
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
|
@ -44,15 +46,33 @@ pub enum ScrollingConfigAxis {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KeyConfig {
|
pub struct KeyStyle {
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub icon: Option<IconDescriptor>,
|
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)]
|
#[serde(default)]
|
||||||
pub mode: KeyModes,
|
pub mode: KeyModes,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Debug, Deserialize, Default)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
pub struct KeyModes {
|
pub struct KeyModes {
|
||||||
pub vibrate: Option<key_modes::vibrate::Config>,
|
pub vibrate: Option<key_modes::vibrate::Config>,
|
||||||
pub media__play_pause: Option<key_modes::media::PlayPauseConfig>,
|
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__switch: Option<key_modes::home_assistant::SwitchConfig>,
|
||||||
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>,
|
pub home_assistant__button: Option<key_modes::home_assistant::ButtonConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type StyleByStateMap<State> = HashMap<State, KeyStyle>;
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::num::NonZeroU8;
|
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::rgb::RGB8WithOptionalA;
|
use crate::model::knob_page::StyleByStateMap;
|
||||||
use crate::model::IconMap;
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -22,9 +20,7 @@ pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub label: HashMap<State, String>,
|
pub label: HashMap<State, String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub icon: IconMap<State>,
|
pub style: StyleByStateMap<State>,
|
||||||
pub circle_indicator: Option<CircleIndicatorConfig>,
|
|
||||||
pub bar_indicator: Option<BarIndicatorConfig>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
|
#[derive(Debug, Default, Eq, PartialEq, Deserialize)]
|
||||||
|
@ -51,15 +47,3 @@ pub enum State {
|
||||||
Active,
|
Active,
|
||||||
Muted,
|
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,
|
|
||||||
}
|
|
||||||
|
|
|
@ -4,8 +4,9 @@ use enum_map::EnumMap;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
use crate::model::icon_descriptor::IconDescriptor;
|
||||||
|
use crate::model::knob_modes;
|
||||||
|
use crate::model::position::KnobPosition;
|
||||||
use crate::model::rgb::RGB8WithOptionalA;
|
use crate::model::rgb::RGB8WithOptionalA;
|
||||||
use crate::model::{knob_modes, KnobPosition};
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct File {
|
pub struct File {
|
||||||
|
@ -21,12 +22,8 @@ pub struct Page {
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
pub struct Knob {
|
pub struct Knob {
|
||||||
#[serde(default)]
|
#[serde(default, flatten)]
|
||||||
pub label: String,
|
pub base_style: KnobStyle,
|
||||||
#[serde(default)]
|
|
||||||
pub icon: IconDescriptor,
|
|
||||||
#[serde(default)]
|
|
||||||
pub indicator: KnobIndicators,
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub mode: KnobModes,
|
pub mode: KnobModes,
|
||||||
}
|
}
|
||||||
|
@ -39,17 +36,26 @@ pub struct KnobIndicators {
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KnobIndicatorBarConfig {
|
pub struct KnobIndicatorBarConfig {
|
||||||
pub color: RGB8WithOptionalA,
|
pub color: Option<RGB8WithOptionalA>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct KnobIndicatorCircleConfig {
|
pub struct KnobIndicatorCircleConfig {
|
||||||
pub color: RGB8WithOptionalA,
|
pub color: Option<RGB8WithOptionalA>,
|
||||||
pub width: u8,
|
pub width: Option<u8>,
|
||||||
pub radius: 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)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
pub struct KnobModes {
|
pub struct KnobModes {
|
||||||
pub audio_volume: Option<knob_modes::audio_volume::Config>,
|
pub audio_volume: Option<knob_modes::audio_volume::Config>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type StyleByStateMap<State> = HashMap<State, KnobStyle>;
|
||||||
|
|
|
@ -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 config;
|
||||||
pub mod geometry;
|
pub mod geometry;
|
||||||
pub mod icon_descriptor;
|
pub mod icon_descriptor;
|
||||||
|
@ -19,114 +6,5 @@ pub mod key_modes;
|
||||||
pub mod key_page;
|
pub mod key_page;
|
||||||
pub mod knob_modes;
|
pub mod knob_modes;
|
||||||
pub mod knob_page;
|
pub mod knob_page;
|
||||||
|
pub mod position;
|
||||||
pub mod rgb;
|
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>;
|
|
||||||
|
|
117
deckster/src/model/position.rs
Normal file
117
deckster/src/model/position.rs
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
|
@ -10,24 +11,38 @@ use loupedeck_serial::util::Endianness;
|
||||||
|
|
||||||
use crate::icons::LoadedIcon;
|
use crate::icons::LoadedIcon;
|
||||||
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
|
use crate::model::icon_descriptor::{IconDescriptor, IconDescriptorSource};
|
||||||
|
use crate::model::image_filter::ImageFilterTransform;
|
||||||
use crate::runner::graphics::labels::LabelRenderer;
|
use crate::runner::graphics::labels::LabelRenderer;
|
||||||
use crate::runner::state::Key;
|
use crate::runner::state::Key;
|
||||||
|
|
||||||
pub fn render_key(
|
#[derive(Debug)]
|
||||||
label_renderer: &mut LabelRenderer,
|
struct GraphicsContext {
|
||||||
key_size: IntSize,
|
label_renderer: RefCell<LabelRenderer>,
|
||||||
buffer_endianness: Endianness,
|
buffer_endianness: Endianness,
|
||||||
icons: &HashMap<IconDescriptor, LoadedIcon>,
|
global_icon_filter_by_pack_id: HashMap<String, ImageFilterTransform>,
|
||||||
state: Option<&Key>,
|
loaded_icons: HashMap<IconDescriptor, LoadedIcon>,
|
||||||
) -> Bytes {
|
}
|
||||||
|
|
||||||
|
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();
|
let mut pixmap = Pixmap::new(key_size.width(), key_size.height()).unwrap();
|
||||||
|
|
||||||
if let Some(state) = state {
|
if let Some(state) = state {
|
||||||
if state.icon.source != IconDescriptorSource::None {
|
let style = state.style.merge_over(&state.base_style);
|
||||||
let icon = &icons[&state.icon];
|
|
||||||
let scaled_size = IntSize::from_wh(icon.pixmap.width(), icon.pixmap.height())
|
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()
|
.unwrap()
|
||||||
.scale_by(icon.scale)
|
.scale_by(scale)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
static PAINT: PixmapPaint = PixmapPaint {
|
static PAINT: PixmapPaint = PixmapPaint {
|
||||||
|
@ -37,19 +52,24 @@ pub fn render_key(
|
||||||
};
|
};
|
||||||
|
|
||||||
pixmap.draw_pixmap(
|
pixmap.draw_pixmap(
|
||||||
(((key_size.width() as i32 - scaled_size.width() as i32) / 2) as f32 / icon.scale).round() as i32,
|
(((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 / icon.scale).round() as i32,
|
(((key_size.height() as i32 - scaled_size.height() as i32) / 2) as f32 / scale).round() as i32,
|
||||||
icon.pixmap.as_ref(),
|
loaded_icon.pixmap.as_ref(),
|
||||||
&PAINT,
|
&PAINT,
|
||||||
Transform::from_scale(icon.scale, icon.scale).post_rotate_at(
|
Transform::from_scale(scale, scale).post_rotate_at(
|
||||||
(icon.clockwise_quarter_rotations as f32) * 90.0,
|
(filter.clockwise_quarter_rotations as f32) * 90.0,
|
||||||
key_size.width() as f32 / 2.0,
|
key_size.width() as f32 / 2.0,
|
||||||
key_size.height() as f32 / 2.0,
|
key_size.height() as f32 / 2.0,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(color) = icon.border {
|
if !state.label.is_empty() {
|
||||||
|
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 path = PathBuilder::from_rect(Rect::from_xywh(-1.0, -2.0, pixmap.width() as f32, pixmap.height() as f32).unwrap());
|
||||||
|
|
||||||
let paint = Paint {
|
let paint = Paint {
|
||||||
|
@ -69,13 +89,6 @@ pub fn render_key(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !state.label.is_empty() {
|
|
||||||
label_renderer.render(&mut pixmap, &state.label);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pixmap.fill(Color::BLACK);
|
|
||||||
}
|
|
||||||
|
|
||||||
convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze()
|
convert_pixels_to_rgb565(pixmap.pixels(), buffer_endianness).freeze()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,8 @@ use log::error;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::model::icon_descriptor::IconDescriptor;
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
|
@ -56,8 +57,8 @@ pub struct KnobPage {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Key {
|
pub struct Key {
|
||||||
pub path: KeyPath,
|
pub path: KeyPath,
|
||||||
pub icon: IconDescriptor,
|
pub base_style: KeyStyle,
|
||||||
pub label: String,
|
pub style: KeyStyle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
Loading…
Add table
Reference in a new issue