From 26b4ad03447bd477c83fa919d1f11ce556f7e02f Mon Sep 17 00:00:00 2001 From: Fl1tzi Date: Tue, 31 Oct 2023 22:21:00 +0100 Subject: [PATCH] changed way on how to program modules and refactored other things --- src/config.rs | 100 +++++++++++++++- src/device.rs | 15 ++- src/image_rendering.rs | 252 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 21 +--- src/modules.rs | 171 +++++++--------------------- src/modules/blank.rs | 8 +- src/modules/counter.rs | 94 +++++++++++++-- src/modules/space.rs | 23 +++- src/type_definition.rs | 78 +++++++++++++ 9 files changed, 599 insertions(+), 163 deletions(-) create mode 100644 src/image_rendering.rs create mode 100644 src/type_definition.rs diff --git a/src/config.rs b/src/config.rs index 4be9554..96c2528 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ +use crate::type_definition::PrettyPrint; use dirs::config_dir; use serde::Deserialize; use serde_json; @@ -6,9 +7,9 @@ use std::{ env, fmt::{self, Display}, fs, - hash::Hash, io::ErrorKind, path::PathBuf, + str::FromStr, sync::Arc, }; use tracing::debug; @@ -20,6 +21,10 @@ pub const CONFIG_FILE: &'static str = "config.json"; /// Combination of buttons acting as a folder which a device can switch to pub type Space = Vec>; +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// CONFIGURATION DEFINITION +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + /// CONFIGURATION #[derive(Deserialize, Debug)] pub struct Config { @@ -64,6 +69,94 @@ fn new_hashmap() -> HashMap { HashMap::new() } +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// BUTTON CONFIGURATION ERRORS +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#[derive(Debug)] +pub enum ButtonConfigError { + /// (Key, Expected) + WrongType(String, &'static str), + /// A general error which gets directly outputed to the user + General(String), +} + +impl Display for ButtonConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + ButtonConfigError::WrongType(key, expected) => { + write!( + formatter, + "Expected value of type {expected} in option \"{key}\"." + ) + } + ButtonConfigError::General(message) => { + write!(formatter, "{message}") + } + } + } +} + +/// See [parse_button_config()] +pub enum ParseButtonConfigResult { + Found(T), + NotFound(T), + ParseError(ButtonConfigError), +} + +impl ParseButtonConfigResult { + /// instead of the enum return Result. + /// + /// [ParseButtonConfigResult::Found] || [ParseButtonConfigResult::NotFound] => Ok(value) + /// [ParseButtonConfigResult::ParseError] => Err(e) + pub fn res(self) -> Result { + match self { + ParseButtonConfigResult::Found(v) | ParseButtonConfigResult::NotFound(v) => Ok(v), + ParseButtonConfigResult::ParseError(e) => Err(e), + } + } +} + +impl Button { + /// reads a key from the config and parses the config in the given type + /// + /// # Return + /// + /// - ParseButtonConfigResult::Found(value) -> returns the value found in the configuration; value is + /// parsing result + /// - ParseButtonConfigResult::NotFound(value) -> the key was not found in the configuration; value + /// is 'if_wrong_type' + /// - ParseButtonConfigResult::ParseError(error) -> the value could not be parsed; error is + /// [ButtonConfigError::WrongType] + pub fn parse_module(&self, key: &'static str, if_wrong_type: T) -> ParseButtonConfigResult + where + T: PrettyPrint + FromStr, + { + // try to find value or return None + let parse_result = match self.options.get(key) { + Some(value) => value.parse::(), + _ => return ParseButtonConfigResult::NotFound(if_wrong_type), + }; + // check if value could be parsed + if let Ok(out) = parse_result { + return ParseButtonConfigResult::Found(out); + } + ParseButtonConfigResult::ParseError(ButtonConfigError::WrongType( + key.to_string(), + if_wrong_type.pprint(), + )) + } + + /// Just retrieve the raw string from a key without trying to parse the value + pub fn raw_module(&self, key: &String) -> Option<&String> { + self.options.get(key) + } +} + +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// PARSING CONFIG +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + #[tracing::instrument] pub fn load_config() -> Result { let config_file: PathBuf = match env::var_os("DACH_DECKER_CONFIG") { @@ -100,6 +193,11 @@ pub fn load_config() -> Result { } } +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// OVERALL CONFIGURATION ERRORS +///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/// General error for parsing the configuration #[derive(Debug)] pub enum ConfigError { ButtonDoesNotExist(u8), diff --git a/src/device.rs b/src/device.rs index 1c63c99..c3c1efb 100644 --- a/src/device.rs +++ b/src/device.rs @@ -101,12 +101,15 @@ impl Device { } // TODO: DO THIS WITHOUT CLONING! Currently takes up a big amount of memory. let button_config = match &self.selected_space { - Some(s) => self.spaces.get(s).unwrap_or_else(|| { - warn!("The space \"{}\" was not found", s); - &self.config.buttons - } - ).to_owned(), - None => self.config.buttons.to_owned() + Some(s) => self + .spaces + .get(s) + .unwrap_or_else(|| { + warn!("The space \"{}\" was not found", s); + &self.config.buttons + }) + .to_owned(), + None => self.config.buttons.to_owned(), }; for i in 0..button_config.len() { let button = button_config.get(i).unwrap().to_owned(); diff --git a/src/image_rendering.rs b/src/image_rendering.rs new file mode 100644 index 0000000..6e25c65 --- /dev/null +++ b/src/image_rendering.rs @@ -0,0 +1,252 @@ +// multiple functions for rendering various images/buttons +// +use crate::GLOBAL_FONT; +use image::imageops; +use image::{io::Reader, DynamicImage, ImageBuffer, Rgb, RgbImage}; +use imageproc::drawing::draw_text_mut; +use rusttype::Scale; +use std::io; + +/// Loads an image from a path +#[allow(dead_code)] +pub fn load_image(path: String) -> io::Result { + Ok(Reader::open(path)? + .decode() + .expect("Unable to decode image")) +} + +/// A red image which should represent an missing image or error +#[allow(dead_code)] +pub fn create_error_image() -> DynamicImage { + let mut error_img: RgbImage = ImageBuffer::new(1, 1); + + for pixel in error_img.enumerate_pixels_mut() { + *pixel.2 = image::Rgb([240, 128, 128]); + } + + DynamicImage::ImageRgb8(error_img) +} + +trait Component { + fn render(&self) -> DynamicImage; +} + +/// The ImageBuilder is an easy way to build images. +/// +/// # Example +/// +/// ```rust +/// ImageBuilder::new(20, 20) +/// .set_text("This is a test") +/// .build(); +/// ``` +pub struct ImageBuilder { + height: usize, + width: usize, + scale: f32, + font_size: f32, + text: Option, + image: Option, +} + +impl Default for ImageBuilder { + fn default() -> ImageBuilder { + ImageBuilder { + // will get changed + height: 0, + // will get changed + width: 0, + scale: 60.0, + font_size: 15.0, + text: None, + image: None, + } + } +} + +impl ImageBuilder { + #[allow(dead_code)] + pub fn new(height: usize, width: usize) -> Self { + ImageBuilder { + height, + width, + ..Default::default() + } + } + + #[allow(dead_code)] + pub fn set_image_scale(mut self, scale: f32) -> Self { + self.scale = scale; + self + } + + #[allow(dead_code)] + pub fn set_text(mut self, text: String) -> Self { + self.text = Some(text); + self + } + + #[allow(dead_code)] + pub fn set_font_size(mut self, font_size: f32) -> Self { + self.font_size = font_size; + self + } + + #[allow(dead_code)] + pub fn set_image(mut self, image: DynamicImage) -> Self { + self.image = Some(image); + self + } + + #[allow(dead_code)] + pub fn build(self) -> DynamicImage { + // cannot use "if let" here, because variables would be moved + if self.text.is_some() && self.image.is_some() { + let c = ImageTextComponent { + height: self.height, + width: self.width, + image: self.image.unwrap(), + scale: self.scale, + font_size: self.font_size, + text: self.text.unwrap(), + }; + return c.render(); + } else if let Some(text) = self.text { + let c = TextComponent { + height: self.height, + width: self.width, + font_size: self.font_size, + text, + }; + return c.render(); + } else if let Some(image) = self.image { + let c = ImageComponent { + height: self.height, + width: self.width, + scale: self.scale, + image, + }; + return c.render(); + } else { + return create_error_image(); + } + } +} + +// Component that just displays an image +struct ImageComponent { + height: usize, + width: usize, + scale: f32, + image: DynamicImage, +} + +impl Component for ImageComponent { + fn render(&self) -> DynamicImage { + let new_h = (self.height as f32 * (self.scale * 0.01)) as u32; + let new_w = (self.width as f32 * (self.scale * 0.01)) as u32; + + let image = self + .image + .resize_to_fill(new_w, new_h, image::imageops::FilterType::Nearest); + + let mut base_image = RgbImage::new(self.height as u32, self.width as u32); + + let free_x = self.width - image.width() as usize; + let free_y = self.height - image.height() as usize; + imageops::overlay( + &mut base_image, + &image.to_rgb8(), + (free_x / 2) as i64, + (free_y / 2) as i64, + ); + + image::DynamicImage::ImageRgb8(base_image) + } +} + +// Component that just displays text +struct TextComponent { + height: usize, + width: usize, + font_size: f32, + text: String, +} + +impl Component for TextComponent { + fn render(&self) -> DynamicImage { + let mut image = RgbImage::new(self.width as u32, self.height as u32); + + let scale = Scale::uniform(self.font_size); + let font = &GLOBAL_FONT.get().unwrap(); + + let v_metrics = font.v_metrics(scale); + let height = (v_metrics.ascent - v_metrics.descent + v_metrics.line_gap).round() as i32; + + // start at y = 10 + let mut y_pos = 10; + + for line in self.text.split("\n") { + draw_text_mut( + &mut image, + Rgb([255, 255, 255]), + 10, + y_pos, + scale, + &GLOBAL_FONT.get().unwrap(), + &line, + ); + y_pos += height; + } + + image::DynamicImage::ImageRgb8(image) + } +} + +// Component that displays image and text +struct ImageTextComponent { + height: usize, + width: usize, + image: DynamicImage, + scale: f32, + font_size: f32, + text: String, +} + +impl Component for ImageTextComponent { + fn render(&self) -> DynamicImage { + let new_h = (self.height as f32 * (self.scale * 0.01)) as u32; + let new_w = (self.width as f32 * (self.scale * 0.01)) as u32; + + let image = self + .image + .resize_to_fill(new_w, new_h, image::imageops::FilterType::Nearest); + + let mut base_image = RgbImage::new(self.height as u32, self.width as u32); + + let font = &GLOBAL_FONT.get().unwrap(); + let font_scale = Scale::uniform(self.font_size); + + // TODO: allow new line + draw_text_mut( + &mut base_image, + Rgb([255, 255, 255]), + 0, + 0, + font_scale, + font, + &self.text, + ); + // position at the middle + let free_space = self.width - image.width() as usize; + // TODO: allow padding to be manually set + imageops::overlay( + &mut base_image, + &image.to_rgb8(), + (free_space / 2) as i64, + self.height as i64 - image.height() as i64 - 5, + ); + + image::DynamicImage::ImageRgb8(base_image) + } +} diff --git a/src/main.rs b/src/main.rs index 38af4e9..82b352e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,16 +1,16 @@ -use crate::config::{Config, DeviceConfig, GlobalConfig}; +use crate::config::{Config, DeviceConfig}; use deck_driver as streamdeck; use device::Device; -use font_loader::system_fonts::{FontProperty, FontPropertyBuilder}; +use font_loader::system_fonts::FontPropertyBuilder; use hidapi::HidApi; use rusttype::Font; use std::{ collections::HashMap, process::exit, - sync::{Arc, Mutex, OnceLock}, + sync::{Arc, OnceLock}, time::Duration, }; -use tracing::{debug, error, info, trace, warn}; +use tracing::{debug, error, info, warn}; use tracing_subscriber::{ self, prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, }; @@ -19,7 +19,9 @@ use config::{load_config, Space}; mod config; mod device; +mod image_rendering; mod modules; +mod type_definition; pub static GLOBAL_FONT: OnceLock = OnceLock::new(); @@ -109,17 +111,6 @@ pub async fn start(config: Config, mut hid: HidApi) { let mut ignore_devices: Vec = Vec::new(); loop { - // check for devices that can be removed - /* let mut removable_devices = Vec::new(); - for (key, device) in &devices { - if device.is_dropped() { - removable_devices.push(key.to_owned()); - } - } - for d in removable_devices { - devices.remove(&d); - }*/ - // refresh device list if let Err(e) = streamdeck::refresh_device_list(&mut hid) { warn!("Cannot fetch new devices: {}", e); diff --git a/src/modules.rs b/src/modules.rs index a4db9dd..0e4c31d 100644 --- a/src/modules.rs +++ b/src/modules.rs @@ -7,38 +7,18 @@ use self::counter::Counter; use self::space::Space; // other things -use crate::GLOBAL_FONT; -use crate::config::Button; +use crate::config::{Button, ButtonConfigError}; use async_trait::async_trait; pub use deck_driver as streamdeck; use futures_util::Future; -use image::imageops::{resize, self}; -use image::io::Reader; -use image::{DynamicImage, Rgb, RgbImage, ImageBuffer}; -use imageproc::drawing::draw_text_mut; -use imageproc::filter; -use lazy_static::lazy_static; -use rusttype::Scale; -use std::collections::HashMap; -use std::io::{BufReader, self}; -use std::pin::Pin; -use std::str::FromStr; -use std::{error::Error, sync::Arc}; -pub use streamdeck::info::ImageFormat; +use image::DynamicImage; +use std::{error::Error, pin::Pin, sync::Arc}; +use streamdeck::info::ImageFormat; use streamdeck::info::Kind; use streamdeck::AsyncStreamDeck; -pub use streamdeck::StreamDeckError; +use streamdeck::StreamDeckError; use tokio::sync::mpsc; -use tracing::{debug, error, info, trace}; - -lazy_static! { - static ref MODULE_MAP: HashMap<&'static str, ModuleFunction> = { - let mut m = HashMap::new(); - m.insert("counter", Counter::run as ModuleFunction); - m.insert("space", Space::run as ModuleFunction); - m - }; -} +use tracing::{debug, error}; /// Events that are coming from the host #[derive(Clone, Copy, Debug)] @@ -49,11 +29,16 @@ pub enum HostEvent { ButtonReleased, } -pub type ModuleFuture = Pin> + Send>>; -pub type ModuleFunction = fn(DeviceAccess, ChannelReceiver, Arc