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 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 {
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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("|")?
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>;
|
||||
|
|
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 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()
|
||||
|
|
|
@ -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)]
|
||||
|
|
Loading…
Add table
Reference in a new issue