commit
This commit is contained in:
parent
1904e3e96a
commit
b5a7ab3c6b
71 changed files with 921 additions and 1297 deletions
15
crates/deckster_shared/Cargo.toml
Normal file
15
crates/deckster_shared/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "deckster_shared"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0.195", features = ["derive", "rc"] }
|
||||
serde_with = "3.4.0"
|
||||
thiserror = "1.0.56"
|
||||
derive_more = "0.99.17"
|
||||
rgb = "0.8.37"
|
||||
enum-ordinalize = "4.3.0"
|
||||
enum-map = "3.0.0-beta.2"
|
||||
im = { version = "15.1.0", features = ["serde"] }
|
||||
parse-display = "0.8.2"
|
77
crates/deckster_shared/src/handler_communication.rs
Normal file
77
crates/deckster_shared/src/handler_communication.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::path::{KeyPath, KnobPath};
|
||||
use crate::style::{KeyStyle, KnobStyle};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
|
||||
pub enum RotationDirection {
|
||||
Clockwise,
|
||||
Counterclockwise,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum KnobEvent {
|
||||
Press,
|
||||
ButtonDown,
|
||||
ButtonUp,
|
||||
Rotate { direction: RotationDirection },
|
||||
VisibilityChange { is_visible: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum KeyTouchEventKind {
|
||||
Start,
|
||||
Move,
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub enum KeyEvent {
|
||||
Press,
|
||||
Touch { touch_id: u8, x: u16, y: u16, kind: KeyTouchEventKind },
|
||||
VisibilityChange { is_visible: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "kebab-case")]
|
||||
pub enum HandlerEvent {
|
||||
Knob { path: KnobPath, event: KnobEvent },
|
||||
Key { path: KeyPath, event: KeyEvent },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "command", rename_all = "kebab-case")]
|
||||
pub enum HandlerCommand {
|
||||
SetActivePages { key_page_id: String, knob_page_id: String },
|
||||
SetKeyStyle { path: KeyPath, value: Option<KeyStyle> },
|
||||
SetKnobStyle { path: KnobPath, value: Option<KnobStyle> },
|
||||
SetKnobValue { path: KnobPath, value: Option<f32> },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct InitialHandlerMessage<KeyConfig: Clone, KnobConfig: Clone> {
|
||||
pub key_configs: im::HashMap<KeyPath, (Box<str>, KeyConfig)>,
|
||||
pub knob_configs: im::HashMap<KnobPath, (Box<str>, KnobConfig)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum HandlerInitializationResultMessage {
|
||||
Ready,
|
||||
Error { error: HandlerInitializationError },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, Error)]
|
||||
pub enum HandlerInitializationError {
|
||||
#[error("The provided mode string is invalid: {message}")]
|
||||
InvalidModeString { message: Box<str> },
|
||||
#[error("The provided handler config is invalid: {message}")]
|
||||
InvalidConfig {
|
||||
supports_keys: bool,
|
||||
supports_knobs: bool,
|
||||
message: Box<str>,
|
||||
},
|
||||
#[error("{message}")]
|
||||
Other { message: Box<str> },
|
||||
}
|
89
crates/deckster_shared/src/icon_descriptor.rs
Normal file
89
crates/deckster_shared/src/icon_descriptor.rs
Normal file
|
@ -0,0 +1,89 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
use crate::image_filter::{ImageFilter, ImageFilterFromStringError};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, SerializeDisplay, DeserializeFromStr)]
|
||||
pub struct IconDescriptor {
|
||||
pub source: IconDescriptorSource,
|
||||
pub filter: ImageFilter,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum IconDescriptorFromStrError {
|
||||
#[error("Not a valid icon identifier: {0}")]
|
||||
InvalidIconPackSource(String),
|
||||
|
||||
#[error("The image filter is invalid: {0}")]
|
||||
InvalidImageFilter(#[source] ImageFilterFromStringError),
|
||||
|
||||
#[error("The image filter is missing the closing ']'")]
|
||||
MissingImageFilterClosingBracket,
|
||||
}
|
||||
|
||||
impl FromStr for IconDescriptor {
|
||||
type Err = IconDescriptorFromStrError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (raw_source, raw_filter) = s.split_once('[').unwrap_or((s, ""));
|
||||
|
||||
let source = if let Some(raw_source) = raw_source.strip_prefix('@') {
|
||||
let (pack_id, icon_id) = raw_source
|
||||
.split_once('/')
|
||||
.ok_or_else(|| IconDescriptorFromStrError::InvalidIconPackSource(raw_source.to_string()))?;
|
||||
IconDescriptorSource::IconPack {
|
||||
pack_id: pack_id.to_owned(),
|
||||
icon_id: icon_id.to_owned(),
|
||||
}
|
||||
} else {
|
||||
IconDescriptorSource::Path(PathBuf::from(raw_source))
|
||||
};
|
||||
|
||||
let filter: ImageFilter = if raw_filter.is_empty() {
|
||||
ImageFilter::default()
|
||||
} else {
|
||||
let mut raw_filter = raw_filter.to_owned();
|
||||
if raw_filter.pop().expect("emptiness was eliminated a few lines earlier") != ']' {
|
||||
return Err(IconDescriptorFromStrError::MissingImageFilterClosingBracket);
|
||||
}
|
||||
|
||||
ImageFilter::from_str(&raw_filter).map_err(IconDescriptorFromStrError::InvalidImageFilter)?
|
||||
};
|
||||
|
||||
Ok(IconDescriptor { source, filter })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IconDescriptor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}[{}]", self.source, self.filter))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub enum IconDescriptorSource {
|
||||
IconPack { pack_id: String, icon_id: String },
|
||||
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 {
|
||||
IconDescriptorSource::IconPack { pack_id, icon_id } => f.write_fmt(format_args!("@{}/{}", pack_id, icon_id)),
|
||||
IconDescriptorSource::Path(path) => f.write_str(&path.to_string_lossy()),
|
||||
}
|
||||
}
|
||||
}
|
282
crates/deckster_shared/src/image_filter.rs
Normal file
282
crates/deckster_shared/src/image_filter.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::str::FromStr;
|
||||
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::rgb::RGB8Wrapper;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub struct ImageFilterTransform {
|
||||
pub scale: f32,
|
||||
/// Must be in 0..=3
|
||||
pub clockwise_quarter_rotations: u8,
|
||||
pub alpha: f32,
|
||||
}
|
||||
|
||||
impl ImageFilterTransform {
|
||||
pub fn merge_over(&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, Eq, PartialEq, Hash, Clone)]
|
||||
pub struct ImageFilterDestructive {
|
||||
pub color: Option<RGB8Wrapper>,
|
||||
pub grayscale: bool,
|
||||
pub invert: bool,
|
||||
}
|
||||
|
||||
impl ImageFilterDestructive {
|
||||
pub fn merge_over(&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 ImageFilter {
|
||||
pub fn merge_over(&self, other: &ImageFilter) -> ImageFilter {
|
||||
ImageFilter {
|
||||
transform: self.transform.merge_over(&other.transform),
|
||||
destructive: self.destructive.merge_over(&other.destructive),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ImageFilter {
|
||||
fn assert_receiver_is_total_eq(&self) {}
|
||||
}
|
||||
|
||||
impl Hash for ImageFilter {
|
||||
fn hash<H: Hasher>(&self, hasher: &mut H) {
|
||||
self.to_string().hash(hasher)
|
||||
}
|
||||
}
|
||||
|
||||
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}")]
|
||||
UnknownFilter { name: String },
|
||||
|
||||
#[error("Filter {name} can only be used once.")]
|
||||
FilterUsedMoreThanOnce { name: String },
|
||||
|
||||
#[error("Only the following values are allowed for rotate: -90, 90, 180, 270")]
|
||||
RotationNotAllowed,
|
||||
|
||||
#[error("Filter {filter_name} requires a value ({filter_name}=<value>).")]
|
||||
FilterValueMissing { filter_name: String },
|
||||
|
||||
#[error("Filter {filter_name} does not require or allow a value.")]
|
||||
FilterValueIgnored { filter_name: String },
|
||||
|
||||
#[error("The following value supplied to {filter_name} could not be parsed: {raw_value}")]
|
||||
FilterValueNotParsable { filter_name: String, raw_value: String },
|
||||
|
||||
#[error("{value} is not in the range ({range}) required by {filter_name}.")]
|
||||
FilterValueNotInRange { filter_name: String, value: String, range: String },
|
||||
}
|
||||
|
||||
impl FromStr for ImageFilter {
|
||||
type Err = ImageFilterFromStringError;
|
||||
|
||||
fn from_str<'a>(s: &str) -> Result<Self, Self::Err> {
|
||||
fn parse_f32_filter_value(
|
||||
filter_name: &str,
|
||||
raw_value: String,
|
||||
range_string: &str,
|
||||
check_range: Box<dyn Fn(&f32) -> bool>,
|
||||
) -> Result<f32, ImageFilterFromStringError> {
|
||||
let value = f32::from_str(&raw_value).map_err(|_| ImageFilterFromStringError::FilterValueNotParsable {
|
||||
filter_name: filter_name.to_string(),
|
||||
raw_value,
|
||||
})?;
|
||||
|
||||
if !check_range(&value) {
|
||||
return Err(ImageFilterFromStringError::FilterValueNotInRange {
|
||||
filter_name: filter_name.to_string(),
|
||||
range: range_string.to_string(),
|
||||
value: value.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
let mut result = ImageFilter::default();
|
||||
let mut previous_filter_names: Vec<String> = Vec::new();
|
||||
|
||||
let filters: Vec<&str> = s.split('|').map(|f| f.trim()).filter(|s| !s.is_empty()).collect();
|
||||
|
||||
if filters.is_empty() {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
for filter in filters {
|
||||
let split_filter = filter.split_once('=');
|
||||
let (filter_name, optional_raw_value) = if let Some((filter_name, raw_value)) = split_filter {
|
||||
(filter_name.to_owned(), Some(raw_value.to_owned()))
|
||||
} else {
|
||||
(filter.to_owned(), None)
|
||||
};
|
||||
|
||||
if previous_filter_names.contains(&filter_name) {
|
||||
return Err(ImageFilterFromStringError::FilterUsedMoreThanOnce { name: filter_name });
|
||||
} else {
|
||||
previous_filter_names.push(filter_name.clone());
|
||||
}
|
||||
|
||||
let value_is_present = optional_raw_value.is_some();
|
||||
let use_bool_value = || -> Result<bool, ImageFilterFromStringError> {
|
||||
if value_is_present {
|
||||
Err(ImageFilterFromStringError::FilterValueIgnored {
|
||||
filter_name: filter_name.clone(),
|
||||
})
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
};
|
||||
|
||||
let use_raw_value = || -> Result<String, ImageFilterFromStringError> {
|
||||
optional_raw_value.ok_or_else(|| ImageFilterFromStringError::FilterValueMissing {
|
||||
filter_name: filter_name.clone(),
|
||||
})
|
||||
};
|
||||
|
||||
match filter_name.as_str() {
|
||||
"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 {
|
||||
filter_name: filter_name.to_string(),
|
||||
raw_value,
|
||||
})?;
|
||||
|
||||
result.transform.clockwise_quarter_rotations = match value {
|
||||
0 => 0,
|
||||
90 => 1,
|
||||
180 => 2,
|
||||
270 | -90 => 3,
|
||||
_ => 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.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 }),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImageFilter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let mut is_first = true;
|
||||
|
||||
if self.transform.scale != DEFAULT_IMAGE_FILTER.transform.scale {
|
||||
if !is_first {
|
||||
f.write_str("|")?
|
||||
}
|
||||
f.write_fmt(format_args!("scale={}", self.transform.scale))?;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
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.transform.clockwise_quarter_rotations as u16 * 90))?;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
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("|")?
|
||||
}
|
||||
|
||||
f.write_fmt(format_args!("color={}", color))?;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
if self.destructive.grayscale {
|
||||
if !is_first {
|
||||
f.write_str("|")?
|
||||
}
|
||||
f.write_str("grayscale")?;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
if self.destructive.invert {
|
||||
if !is_first {
|
||||
f.write_str("|")?
|
||||
}
|
||||
f.write_str("invert")?;
|
||||
is_first = false;
|
||||
}
|
||||
|
||||
if let Some(color) = self.destructive.color {
|
||||
if !is_first {
|
||||
f.write_str("|")?
|
||||
}
|
||||
|
||||
f.write_fmt(format_args!("color={}", color))?;
|
||||
// is_first = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
7
crates/deckster_shared/src/lib.rs
Normal file
7
crates/deckster_shared/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod handler_communication;
|
||||
pub mod icon_descriptor;
|
||||
pub mod image_filter;
|
||||
pub mod path;
|
||||
pub mod rgb;
|
||||
pub mod state;
|
||||
pub mod style;
|
43
crates/deckster_shared/src/path.rs
Normal file
43
crates/deckster_shared/src/path.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use enum_map::Enum;
|
||||
use enum_ordinalize::Ordinalize;
|
||||
use parse_display::{Display, FromStr};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
|
||||
/// One-based coordinates of a specific virtual (not physical) key.
|
||||
#[display("{x}x{y}")]
|
||||
pub struct KeyPosition {
|
||||
pub x: u16,
|
||||
pub y: u16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
|
||||
#[display("{page_id}/{position}")]
|
||||
pub struct KeyPath {
|
||||
pub page_id: String,
|
||||
pub position: KeyPosition,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, Enum, Ordinalize, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
|
||||
#[display(style = "kebab-case")]
|
||||
pub enum KnobPosition {
|
||||
LeftTop,
|
||||
LeftMiddle,
|
||||
LeftBottom,
|
||||
RightTop,
|
||||
RightMiddle,
|
||||
RightBottom,
|
||||
}
|
||||
|
||||
impl KnobPosition {
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, KnobPosition::LeftBottom | KnobPosition::LeftMiddle | KnobPosition::LeftTop)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Display, FromStr, SerializeDisplay, DeserializeFromStr)]
|
||||
#[display("{page_id}/{position}")]
|
||||
pub struct KnobPath {
|
||||
pub page_id: String,
|
||||
pub position: KnobPosition,
|
||||
}
|
108
crates/deckster_shared/src/rgb.rs
Normal file
108
crates/deckster_shared/src/rgb.rs
Normal file
|
@ -0,0 +1,108 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::{Deref, From, Into};
|
||||
use rgb::{RGB8, RGBA8};
|
||||
use serde_with::{DeserializeFromStr, SerializeDisplay};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("The input value does not match the required format.")]
|
||||
pub struct ParsingError {}
|
||||
|
||||
fn parse_rgb8_from_hex_str(s: &str) -> Result<RGB8, ParsingError> {
|
||||
let first_index = if s.starts_with('#') { 1 } else { 0 };
|
||||
if s.len() - first_index == 6 {
|
||||
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ParsingError {})?;
|
||||
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ParsingError {})?;
|
||||
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ParsingError {})?;
|
||||
|
||||
return Ok(RGB8::new(r, g, b));
|
||||
}
|
||||
|
||||
Err(ParsingError {})
|
||||
}
|
||||
|
||||
fn fmt_rgb8_as_hex_string(v: &RGB8, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{:#04x}{:#04x}{:#04x}", v.r, v.g, v.b))
|
||||
}
|
||||
|
||||
fn parse_rgba8_from_hex_str(s: &str) -> Result<RGBA8, ParsingError> {
|
||||
let first_index = if s.starts_with('#') { 1 } else { 0 };
|
||||
if s.len() - first_index == 8 {
|
||||
let r = u8::from_str_radix(&s[first_index..(first_index + 2)], 16).map_err(|_| ParsingError {})?;
|
||||
let g = u8::from_str_radix(&s[(first_index + 2)..(first_index + 4)], 16).map_err(|_| ParsingError {})?;
|
||||
let b = u8::from_str_radix(&s[(first_index + 4)..(first_index + 6)], 16).map_err(|_| ParsingError {})?;
|
||||
let a = u8::from_str_radix(&s[(first_index + 6)..(first_index + 8)], 16).map_err(|_| ParsingError {})?;
|
||||
|
||||
return Ok(RGBA8::new(r, g, b, a));
|
||||
}
|
||||
|
||||
Err(ParsingError {})
|
||||
}
|
||||
|
||||
fn fmt_rgba8_as_hex_string(v: &RGBA8, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{:#04x}{:#04x}{:#04x}{:#04x}", v.r, v.g, v.b, v.a))
|
||||
}
|
||||
|
||||
fn parse_rgb8_with_optional_alpha_from_hex_str(s: &str, fallback_alpha: u8) -> Result<RGBA8, ParsingError> {
|
||||
// optionally +1 for the '#'
|
||||
match s.len() {
|
||||
6 | 7 => Ok(parse_rgb8_from_hex_str(s)?.alpha(fallback_alpha)),
|
||||
8 | 9 => Ok(parse_rgba8_from_hex_str(s)?),
|
||||
_ => Err(ParsingError {}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
|
||||
pub struct RGB8Wrapper(RGB8);
|
||||
|
||||
impl FromStr for RGB8Wrapper {
|
||||
type Err = ParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_rgb8_from_hex_str(s).map(RGB8Wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RGB8Wrapper {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
fmt_rgb8_as_hex_string(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
|
||||
#[repr(transparent)]
|
||||
pub struct RGBA8Wrapper(pub RGBA8);
|
||||
|
||||
impl FromStr for RGBA8Wrapper {
|
||||
type Err = ParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_rgba8_from_hex_str(s).map(RGBA8Wrapper)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RGBA8Wrapper {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
fmt_rgba8_as_hex_string(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deref, From, Into, SerializeDisplay, DeserializeFromStr)]
|
||||
#[repr(transparent)]
|
||||
pub struct RGB8WithOptionalA(RGBA8);
|
||||
|
||||
impl FromStr for RGB8WithOptionalA {
|
||||
type Err = ParsingError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
parse_rgb8_with_optional_alpha_from_hex_str(s, 0xff).map(RGB8WithOptionalA)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RGB8WithOptionalA {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
fmt_rgba8_as_hex_string(&self.0, f)
|
||||
}
|
||||
}
|
22
crates/deckster_shared/src/state.rs
Normal file
22
crates/deckster_shared/src/state.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::path::{KeyPath, KnobPath};
|
||||
use crate::style::{KeyStyle, KnobStyle};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Key {
|
||||
pub path: KeyPath,
|
||||
pub base_style: KeyStyle,
|
||||
pub style: Option<KeyStyle>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Knob {
|
||||
pub path: KnobPath,
|
||||
pub base_style: KnobStyle,
|
||||
pub style: Option<KnobStyle>,
|
||||
pub value: Option<f32>,
|
||||
}
|
||||
|
||||
pub type KeyStyleByStateMap<State> = HashMap<State, KeyStyle>;
|
||||
pub type KnobStyleByStateMap<State> = HashMap<State, KnobStyle>;
|
101
crates/deckster_shared/src/style.rs
Normal file
101
crates/deckster_shared/src/style.rs
Normal file
|
@ -0,0 +1,101 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::icon_descriptor::IconDescriptor;
|
||||
use crate::rgb::RGB8WithOptionalA;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KeyStyle {
|
||||
pub label: Option<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub border: Option<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
impl KeyStyle {
|
||||
pub fn merge_over(&self, base: &KeyStyle) -> KeyStyle {
|
||||
KeyStyle {
|
||||
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
|
||||
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
|
||||
border: self.border.or(base.border),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KnobStyle {
|
||||
pub label: Option<String>,
|
||||
pub icon: Option<IconDescriptor>,
|
||||
pub indicators: Option<KnobIndicators>,
|
||||
}
|
||||
|
||||
impl KnobStyle {
|
||||
pub fn merge_over(&self, base: &Self) -> Self {
|
||||
Self {
|
||||
label: self.label.as_ref().or(base.label.as_ref()).cloned(),
|
||||
icon: self.icon.as_ref().or(base.icon.as_ref()).cloned(),
|
||||
indicators: self
|
||||
.indicators
|
||||
.as_ref()
|
||||
.zip(base.indicators.as_ref())
|
||||
.map(|(a, b)| a.merge_over(b))
|
||||
.or_else(|| self.indicators.clone())
|
||||
.or_else(|| base.indicators.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KnobIndicators {
|
||||
pub bar: Option<KnobIndicatorBarConfig>,
|
||||
pub circle: Option<KnobIndicatorCircleConfig>,
|
||||
}
|
||||
|
||||
impl KnobIndicators {
|
||||
pub fn merge_over(&self, base: &Self) -> Self {
|
||||
Self {
|
||||
bar: self
|
||||
.bar
|
||||
.as_ref()
|
||||
.zip(base.bar.as_ref())
|
||||
.map(|(a, b)| a.merge_over(b))
|
||||
.or_else(|| self.bar.clone())
|
||||
.or_else(|| base.bar.clone()),
|
||||
circle: self
|
||||
.circle
|
||||
.as_ref()
|
||||
.zip(base.circle.as_ref())
|
||||
.map(|(a, b)| a.merge_over(b))
|
||||
.or_else(|| self.circle.clone())
|
||||
.or_else(|| base.circle.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KnobIndicatorBarConfig {
|
||||
pub color: Option<RGB8WithOptionalA>,
|
||||
}
|
||||
|
||||
impl KnobIndicatorBarConfig {
|
||||
pub fn merge_over(&self, base: &Self) -> Self {
|
||||
Self {
|
||||
color: self.color.as_ref().or(base.color.as_ref()).cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct KnobIndicatorCircleConfig {
|
||||
pub color: Option<RGB8WithOptionalA>,
|
||||
pub width: Option<u8>,
|
||||
pub radius: Option<u8>,
|
||||
}
|
||||
|
||||
impl KnobIndicatorCircleConfig {
|
||||
pub fn merge_over(&self, base: &Self) -> Self {
|
||||
Self {
|
||||
color: self.color.as_ref().or(base.color.as_ref()).cloned(),
|
||||
width: self.width.as_ref().or(base.width.as_ref()).cloned(),
|
||||
radius: self.radius.as_ref().or(base.radius.as_ref()).cloned(),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue