deckster/src/runner/io_worker.rs
2024-02-05 00:13:03 +01:00

366 lines
14 KiB
Rust

use std::cell::RefCell;
use std::path::Path;
use std::sync::Arc;
use enum_ordinalize::Ordinalize;
use log::{error, trace};
use resvg::usvg::tiny_skia_path::IntSize;
use rgb::RGB8;
use tokio::sync::broadcast;
use deckster_shared::handler_communication::{HandlerCommand, HandlerEvent, KeyEvent, KeyTouchEventKind, KnobEvent};
use deckster_shared::path::{KeyPath, KeyPosition, KnobPath, KnobPosition};
use deckster_shared::state::{Key, Knob};
use loupedeck_serial::characteristics::{LoupedeckButton, LoupedeckDeviceKeyGridCharacteristics, LoupedeckDisplayRect, LoupedeckKnob};
use loupedeck_serial::device::LoupedeckDevice;
use loupedeck_serial::events::{LoupedeckEvent, RotationDirection};
use crate::icons::IconManager;
use crate::model::config::Config;
use crate::model::position::ButtonPosition;
use crate::runner::graphics::labels::LabelRenderer;
use crate::runner::graphics::{render_key, render_knob, GraphicsContext};
use crate::runner::state::State;
enum IoWork {
DeviceEvent(LoupedeckEvent),
Command(HandlerCommand),
}
pub struct IoWorkerContext {
config: Arc<Config>,
device: LoupedeckDevice,
commands_sender: flume::Sender<HandlerCommand>,
events_sender: broadcast::Sender<HandlerEvent>,
graphics: GraphicsContext,
}
impl IoWorkerContext {
pub fn create(
config_directory: &Path,
config: Arc<Config>,
device: LoupedeckDevice,
commands_sender: flume::Sender<HandlerCommand>,
events_sender: broadcast::Sender<HandlerEvent>,
) -> Self {
let buffer_endianness = device.characteristics().key_grid.display.endianness;
let label_renderer = RefCell::new(LabelRenderer::new(config.label_font_family.as_ref()));
let dpi = device.characteristics().key_grid.display.dpi;
let icon_packs = Arc::clone(&config.icon_packs);
IoWorkerContext {
config,
device,
commands_sender,
events_sender,
graphics: GraphicsContext {
buffer_endianness,
label_renderer,
icon_manager: IconManager::new(config_directory.to_path_buf(), icon_packs, dpi),
},
}
}
}
pub fn do_io_work(context: IoWorkerContext, commands_receiver: flume::Receiver<HandlerCommand>) {
let mut state = State::create(&context.config);
let device_events_receiver = context.device.events();
loop {
let a = flume::Selector::new()
.recv(&device_events_receiver, |e| IoWork::DeviceEvent(e.unwrap()))
.recv(&commands_receiver, |c| IoWork::Command(c.unwrap()))
.wait();
match a {
IoWork::DeviceEvent(event) => {
if !handle_event(&context, &mut state, event) {
break;
}
}
IoWork::Command(command) => handle_command(&context, &mut state, command),
}
}
}
fn handle_event(context: &IoWorkerContext, state: &mut State, event: LoupedeckEvent) -> bool {
trace!("Handling event: {:?}", &event);
let send_key_event = |path: KeyPath, event: KeyEvent| {
trace!("Sending key event ({}): {:?}", &path, &event);
context.events_sender.send(HandlerEvent::Key { path, event }).unwrap();
};
let send_knob_event = |path: KnobPath, event: KnobEvent| {
trace!("Sending knob event ({:?}): {:?}", &path, &event);
context.events_sender.send(HandlerEvent::Knob { path, event }).unwrap();
};
match event {
LoupedeckEvent::Disconnected => return false,
LoupedeckEvent::ButtonDown { button } => {
let position = ButtonPosition::of(&button);
let button_config = &context.config.buttons[position];
context
.commands_sender
.send(HandlerCommand::SetActivePages {
key_page_id: button_config.key_page.as_ref().unwrap_or(&state.active_key_page_id).clone(),
knob_page_id: button_config.knob_page.as_ref().unwrap_or(&state.active_knob_page_id).clone(),
})
.unwrap()
}
LoupedeckEvent::Touch { x, y, is_end, touch_id } => {
let characteristics = context.device.characteristics();
let display = characteristics.get_display_at_coordinates(x, y);
if let Some(display) = display {
if display == &characteristics.key_grid.display {
let key_index = characteristics.key_grid.get_key_at_global_coordinates(x, y);
if let Some(key_index) = key_index {
let position = KeyPosition {
x: (key_index % characteristics.key_grid.columns) as u16 + 1,
y: (key_index / characteristics.key_grid.columns) as u16 + 1,
};
let path = KeyPath {
page_id: state.active_key_page_id.clone(),
position,
};
let LoupedeckDisplayRect {
x: top_left_x, y: top_left_y, ..
} = characteristics.key_grid.get_local_key_rect(key_index).unwrap();
let kind = if is_end {
state.active_touch_ids.remove(&touch_id);
KeyTouchEventKind::End
} else {
let is_new = state.active_touch_ids.insert(touch_id);
if is_new {
KeyTouchEventKind::Start
} else {
KeyTouchEventKind::Move
}
};
send_key_event(
path.clone(),
KeyEvent::Touch {
touch_id,
x: x - top_left_x,
y: y - top_left_y,
kind,
},
);
if kind == KeyTouchEventKind::Start {
send_key_event(path.clone(), KeyEvent::Press);
}
}
}
}
}
LoupedeckEvent::KnobRotate { knob, direction } => {
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
send_knob_event(
KnobPath {
page_id: state.active_knob_page_id.clone(),
position,
},
KnobEvent::Rotate {
direction: match direction {
RotationDirection::Clockwise => deckster_shared::handler_communication::RotationDirection::Clockwise,
RotationDirection::Counterclockwise => deckster_shared::handler_communication::RotationDirection::Counterclockwise,
},
},
)
}
LoupedeckEvent::KnobDown { knob } => {
let position: KnobPosition = get_position_of_loupedeck_knob(knob);
send_knob_event(
KnobPath {
page_id: state.active_knob_page_id.clone(),
position,
},
KnobEvent::Press,
)
}
_ => {}
}
true
}
fn handle_command(context: &IoWorkerContext, state: &mut State, command: HandlerCommand) {
trace!("Handling command: {:?}", &command);
match command {
HandlerCommand::SetActivePages { key_page_id, knob_page_id } => {
state.active_key_page_id = key_page_id;
state.active_knob_page_id = knob_page_id;
for button in LoupedeckButton::VARIANTS {
let position = ButtonPosition::of(button);
context
.device
.set_button_color(*button, get_correct_button_color(context, state, position))
.unwrap();
}
let key_grid = &context.device.characteristics().key_grid;
for index in 0..(key_grid.rows * key_grid.columns) {
draw_key_at_index(context, state, index);
}
for position in KnobPosition::VARIANTS {
draw_knob_at_position(context, state, *position);
}
context.device.refresh_display(&key_grid.display).unwrap();
}
HandlerCommand::SetKeyStyle { path, value } => {
state.mutate_key_for_command("SetKeyStyle", &path, |k| {
k.style = value;
});
draw_key_at_path_if_visible(context, state, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
}
HandlerCommand::SetKnobStyle { path, value } => {
state.mutate_knob_for_command("SetKnobStyle", &path, |k| {
k.style = value;
});
draw_knob_at_path_if_visible(context, state, path);
context.device.refresh_display(&context.device.characteristics().key_grid.display).unwrap();
}
HandlerCommand::SetKnobValue { path, value } => {
if let Some(v) = value {
if !(0.0..=1.0).contains(&v) {
error!("Received SetKnobValue with an out-of-range value: {}", v);
return;
}
}
state.mutate_knob_for_command("SetKnobValue", &path, |k| {
k.value = value;
});
draw_knob_at_path_if_visible(context, state, path);
}
}
}
// active -> config.active_button_color
// no actions defined -> #000000
// inactive -> config.inactive_button_color
fn get_correct_button_color(context: &IoWorkerContext, state: &State, button_position: ButtonPosition) -> RGB8 {
let button_config = &context.config.buttons[button_position];
if let Some(key_page) = &button_config.key_page {
if key_page == &state.active_key_page_id {
if let Some(knob_page) = &button_config.knob_page {
if knob_page == &state.active_knob_page_id {
return context.config.active_button_color.into();
}
}
}
} else if button_config.knob_page.is_none() {
return RGB8::new(0, 0, 0);
}
context.config.inactive_button_color.into()
}
fn get_key_index_for_position(key_grid: &LoupedeckDeviceKeyGridCharacteristics, position: KeyPosition) -> Option<u8> {
if (position.x - 1) >= key_grid.columns as u16 || (position.y - 1) >= key_grid.rows as u16 {
None
} else {
let x = (position.x - 1) as u8;
let y = (position.y - 1) as u8;
Some(y * key_grid.columns + x)
}
}
fn get_key_position_for_index(key_grid: &LoupedeckDeviceKeyGridCharacteristics, index: u8) -> KeyPosition {
let x = index % key_grid.columns;
let y = index / key_grid.columns;
KeyPosition {
x: (x + 1) as u16,
y: (y + 1) as u16,
}
}
fn draw_key(context: &IoWorkerContext, index: u8, key: Option<&Key>) {
let key_grid = &context.device.characteristics().key_grid;
let rect = key_grid.get_local_key_rect(index).unwrap();
let buffer = render_key(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), key);
context
.device
.replace_framebuffer_area_raw(&key_grid.display, rect.x, rect.y, rect.w, rect.h, buffer)
.unwrap();
}
fn draw_key_at_index(context: &IoWorkerContext, state: &State, index: u8) {
let position = get_key_position_for_index(&context.device.characteristics().key_grid, index);
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
}
fn draw_key_at_position_if_visible(context: &IoWorkerContext, state: &State, position: KeyPosition) {
let index = get_key_index_for_position(&context.device.characteristics().key_grid, position);
if let Some(index) = index {
draw_key(context, index, state.active_key_page().keys_by_position.get(&position));
}
}
fn draw_key_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KeyPath) {
if state.active_key_page_id == path.page_id {
draw_key_at_position_if_visible(context, state, path.position);
}
}
fn draw_knob(context: &IoWorkerContext, position: KnobPosition, knob: Option<&Knob>) {
if let Some((display, rect)) = context.device.characteristics().get_display_and_rect_for_knob(match position {
KnobPosition::LeftTop => LoupedeckKnob::LeftTop,
KnobPosition::LeftMiddle => LoupedeckKnob::LeftMiddle,
KnobPosition::LeftBottom => LoupedeckKnob::LeftBottom,
KnobPosition::RightTop => LoupedeckKnob::RightTop,
KnobPosition::RightMiddle => LoupedeckKnob::RightMiddle,
KnobPosition::RightBottom => LoupedeckKnob::RightBottom,
}) {
let buffer = render_knob(&context.graphics, IntSize::from_wh(rect.w as u32, rect.h as u32).unwrap(), knob);
context
.device
.replace_framebuffer_area_raw(display, rect.x, rect.y, rect.w, rect.h, buffer)
.unwrap();
}
}
fn draw_knob_at_position(context: &IoWorkerContext, state: &State, position: KnobPosition) {
draw_knob(context, position, Some(&state.active_knob_page().knobs_by_position[position]));
}
fn draw_knob_at_path_if_visible(context: &IoWorkerContext, state: &State, path: KnobPath) {
if state.active_knob_page_id == path.page_id {
draw_knob_at_position(context, state, path.position);
}
}
fn get_position_of_loupedeck_knob(value: LoupedeckKnob) -> KnobPosition {
match value {
LoupedeckKnob::LeftTop => KnobPosition::LeftTop,
LoupedeckKnob::LeftMiddle => KnobPosition::LeftMiddle,
LoupedeckKnob::LeftBottom => KnobPosition::LeftBottom,
LoupedeckKnob::RightTop => KnobPosition::RightTop,
LoupedeckKnob::RightMiddle => KnobPosition::RightMiddle,
LoupedeckKnob::RightBottom => KnobPosition::RightBottom,
}
}