366 lines
14 KiB
Rust
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,
|
|
}
|
|
}
|