keyboard progress & refactors

This commit is contained in:
galister
2025-06-20 00:48:37 +09:00
parent e0e30dedfb
commit 44a9faac14
37 changed files with 1226 additions and 761 deletions

View File

@@ -0,0 +1,292 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use glam::{Affine2, vec2, vec3a};
use wgui::{
animation::{Animation, AnimationEasing},
event::{self, EventListener},
taffy::{self, prelude::length},
widget::{
div::Div,
rectangle::{Rectangle, RectangleParams},
util::WLength,
},
};
use crate::{
backend::overlay::{OverlayData, OverlayState, Positioning},
gui::{self, panel::GuiPanel},
state::AppState,
subsystem::hid::{ALT, CTRL, META, SHIFT, SUPER, XkbKeymap},
};
use super::{
KEYBOARD_NAME, KeyState, KeyboardBackend, KeyboardState, handle_press, handle_release,
layout::{self, AltModifier, KeyCapType},
};
const BACKGROUND_PADDING: f32 = 4.;
const PIXELS_PER_UNIT: f32 = 80.;
#[allow(clippy::too_many_lines, clippy::significant_drop_tightening)]
pub fn create_keyboard<O>(
app: &AppState,
mut keymap: Option<XkbKeymap>,
) -> anyhow::Result<OverlayData<O>>
where
O: Default,
{
let layout = layout::Layout::load_from_disk();
let state = Rc::new(RefCell::new(KeyboardState {
hid: app.hid_provider.clone(),
audio: app.audio_provider.clone(),
modifiers: 0,
alt_modifier: match layout.alt_modifier {
AltModifier::Shift => SHIFT,
AltModifier::Ctrl => CTRL,
AltModifier::Alt => ALT,
AltModifier::Super => SUPER,
AltModifier::Meta => META,
_ => 0,
},
processes: vec![],
}));
let size = vec2(
layout.row_size * PIXELS_PER_UNIT,
(layout.main_layout.len() as f32) * PIXELS_PER_UNIT,
);
let mut panel = GuiPanel::new_blank(app, 2048)?;
let (background, _) = panel.layout.add_child(
panel.layout.root_widget,
Rectangle::create(RectangleParams {
color: wgui::drawing::Color::new(0., 0., 0., 0.6),
round: WLength::Units(4.0),
..Default::default()
})
.unwrap(),
taffy::Style {
flex_direction: taffy::FlexDirection::Column,
padding: length(BACKGROUND_PADDING),
..Default::default()
},
)?;
let has_altgr = keymap.as_ref().is_some_and(XkbKeymap::has_altgr);
if !layout.auto_labels.unwrap_or(true) {
keymap = None;
}
let (_, mut gui_state_key) =
wgui::parser::new_layout_from_assets(Box::new(gui::asset::GuiAsset {}), "keyboard.xml")?;
for row in 0..layout.key_sizes.len() {
let (div, _) = panel.layout.add_child(
background,
Div::create().unwrap(),
taffy::Style {
flex_direction: taffy::FlexDirection::Row,
..Default::default()
},
)?;
for col in 0..layout.key_sizes[row].len() {
let my_size_f32 = layout.key_sizes[row][col];
let key_width = PIXELS_PER_UNIT * my_size_f32;
let key_height = PIXELS_PER_UNIT;
let taffy_size = taffy::Size {
width: length(key_width),
height: length(PIXELS_PER_UNIT),
};
let Some(key) = layout.get_key_data(keymap.as_ref(), has_altgr, col, row) else {
let _ = panel.layout.add_child(
div,
Div::create()?,
taffy::Style {
size: taffy_size,
min_size: taffy_size,
max_size: taffy_size,
..Default::default()
},
)?;
continue;
};
let my_id: Rc<str> = Rc::from(format!("key-{row}-{col}"));
// todo: make this easier to maintain somehow
let mut params: HashMap<Rc<str>, Rc<str>> = HashMap::new();
params.insert(Rc::from("id"), my_id.clone());
params.insert(Rc::from("width"), Rc::from(key_width.to_string()));
params.insert(Rc::from("height"), Rc::from(key_height.to_string()));
let mut label = key.label.into_iter();
label
.next()
.and_then(|s| params.insert("text".into(), s.into()));
match key.cap_type {
KeyCapType::LetterAltGr => {
label
.next()
.and_then(|s| params.insert("text_altgr".into(), s.into()));
}
KeyCapType::Symbol => {
label
.next()
.and_then(|s| params.insert("text_shift".into(), s.into()));
}
KeyCapType::SymbolAltGr => {
label
.next()
.and_then(|s| params.insert("text_shift".into(), s.into()));
label
.next()
.and_then(|s| params.insert("text_altgr".into(), s.into()));
}
_ => {}
}
let template_key = format!("Key{:?}", key.cap_type);
gui_state_key.process_template(&template_key, &mut panel.layout, div, params)?;
if let Some(widget_id) = gui_state_key.ids.get(&*my_id) {
let key_state = {
let widget = panel
.layout
.widget_states
.get(*widget_id)
.unwrap() // want panic
.lock()
.unwrap(); // want panic
let rect = widget.obj.get_as::<Rectangle>();
Rc::new(KeyState {
button_state: key.button_state,
color: rect.params.color,
color2: rect.params.color2,
border_color: rect.params.border_color,
})
};
panel.layout.add_event_listener(
*widget_id,
EventListener::MouseEnter(Box::new({
let (k, kb) = (key_state.clone(), state.clone());
move |data| {
on_enter_anim(k.clone(), kb.clone(), data);
}
})),
);
panel.layout.add_event_listener(
*widget_id,
EventListener::MouseLeave(Box::new({
let (k, kb) = (key_state.clone(), state.clone());
move |data| {
on_leave_anim(k.clone(), kb.clone(), data);
}
})),
);
panel.layout.add_event_listener(
*widget_id,
EventListener::MousePress(Box::new({
let (k, kb) = (key_state.clone(), state.clone());
move |data, button| {
on_press_anim(k.clone(), data);
handle_press(k.clone(), kb.clone(), button);
}
})),
);
panel.layout.add_event_listener(
*widget_id,
EventListener::MouseRelease(Box::new({
let (k, kb) = (key_state.clone(), state.clone());
move |data, button| {
on_release_anim(k.clone(), data);
handle_release(k.clone(), kb.clone(), button);
}
})),
);
}
}
}
let interaction_transform = Affine2::from_translation(vec2(0.5, 0.5))
* Affine2::from_scale(vec2(1., -size.x as f32 / size.y as f32));
let width = layout.row_size * 0.05 * app.session.config.keyboard_scale;
Ok(OverlayData {
state: OverlayState {
name: KEYBOARD_NAME.into(),
grabbable: true,
recenter: true,
positioning: Positioning::Anchored,
interactable: true,
spawn_scale: width,
spawn_point: vec3a(0., -0.5, 0.),
interaction_transform,
..Default::default()
},
backend: Box::new(KeyboardBackend { panel, state }),
..Default::default()
})
}
fn on_enter_anim(
key_state: Rc<KeyState>,
_keyboard_state: Rc<RefCell<KeyboardState>>,
data: &mut event::CallbackData,
) {
data.animations.push(Animation::new(
data.widget_id,
5,
AnimationEasing::OutQuad,
Box::new(move |data| {
let rect = data.obj.get_as_mut::<Rectangle>();
let brightness = data.pos * 0.5;
rect.params.color.r = key_state.color.r + brightness;
rect.params.color.g = key_state.color.g + brightness;
rect.params.color.b = key_state.color.b + brightness;
data.needs_redraw = true;
}),
));
}
fn on_leave_anim(
key_state: Rc<KeyState>,
_keyboard_state: Rc<RefCell<KeyboardState>>,
data: &mut event::CallbackData,
) {
data.animations.push(Animation::new(
data.widget_id,
5,
AnimationEasing::OutQuad,
Box::new(move |data| {
let rect = data.obj.get_as_mut::<Rectangle>();
let brightness = (1.0 - data.pos) * 0.5;
rect.params.color.r = key_state.color.r + brightness;
rect.params.color.g = key_state.color.g + brightness;
rect.params.color.b = key_state.color.b + brightness;
data.needs_redraw = true;
}),
));
}
fn on_press_anim(key_state: Rc<KeyState>, data: &mut event::CallbackData) {
let rect = data.obj.get_as_mut::<Rectangle>();
rect.params.border_color = key_state.border_color;
data.needs_redraw = true;
}
fn on_release_anim(key_state: Rc<KeyState>, data: &mut event::CallbackData) {
let rect = data.obj.get_as_mut::<Rectangle>();
rect.params.border_color = key_state.border_color;
data.needs_redraw = true;
}

View File

@@ -0,0 +1,261 @@
use std::{collections::HashMap, str::FromStr, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::{
config::{ConfigType, load_known_yaml},
subsystem::hid::{
KEYS_TO_MODS, KeyType, META, NUM_LOCK, SHIFT, VirtualKey, XkbKeymap, get_key_type,
},
};
use super::KeyButtonData;
static MACRO_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^([A-Za-z0-9_-]+)(?: +(UP|DOWN))?$").unwrap()); // want panic
#[derive(Debug, Deserialize, Serialize)]
#[allow(clippy::struct_field_names)]
pub struct Layout {
pub(super) name: String,
pub(super) row_size: f32,
pub(super) key_sizes: Vec<Vec<f32>>,
pub(super) main_layout: Vec<Vec<Option<String>>>,
pub(super) alt_modifier: AltModifier,
pub(super) exec_commands: HashMap<String, Vec<String>>,
pub(super) macros: HashMap<String, Vec<String>>,
pub(super) labels: HashMap<String, Vec<String>>,
pub(super) auto_labels: Option<bool>,
}
impl Layout {
pub(super) fn load_from_disk() -> Self {
let mut layout = load_known_yaml::<Self>(ConfigType::Keyboard);
layout.post_load();
layout
}
fn post_load(&mut self) {
for i in 0..self.key_sizes.len() {
let row = &self.key_sizes[i];
let width: f32 = row.iter().sum();
assert!(
(width - self.row_size).abs() < 0.001,
"Row {} has a width of {}, but the row size is {}",
i,
width,
self.row_size
);
}
for i in 0..self.main_layout.len() {
let row = &self.main_layout[i];
let width = row.len();
assert!(
(width == self.key_sizes[i].len()),
"Row {} has {} keys, needs to have {} according to key_sizes",
i,
width,
self.key_sizes[i].len()
);
}
}
pub(super) fn get_key_data(
&self,
keymap: Option<&XkbKeymap>,
has_altgr: bool,
col: usize,
row: usize,
) -> Option<KeyData> {
let key = self.main_layout[row][col].as_ref()?;
let mut label = Vec::with_capacity(3);
let mut cap_type = KeyCapType::Letter;
let button_state: KeyButtonData;
if let Ok(vk) = VirtualKey::from_str(key) {
if let Some(keymap) = keymap.as_ref() {
match get_key_type(vk) {
KeyType::Symbol => {
let label0 = keymap.label_for_key(vk, 0);
let label1 = keymap.label_for_key(vk, SHIFT);
if label0.chars().next().is_some_and(char::is_alphabetic) {
label.push(label1);
if has_altgr {
cap_type = KeyCapType::LetterAltGr;
label.push(keymap.label_for_key(vk, META));
} else {
cap_type = KeyCapType::Letter;
}
} else {
label.push(label0);
label.push(label1);
if has_altgr {
label.push(keymap.label_for_key(vk, META));
cap_type = KeyCapType::SymbolAltGr;
} else {
cap_type = KeyCapType::Symbol;
}
}
}
KeyType::NumPad => {
label.push(keymap.label_for_key(vk, NUM_LOCK));
}
KeyType::Special => {
cap_type = KeyCapType::Special;
match vk {
VirtualKey::RShift | VirtualKey::LShift => {
label.push("shift".into());
}
VirtualKey::RSuper | VirtualKey::LSuper => {
label.push("super".into());
}
VirtualKey::KP_Enter => {
label.push("return".into());
}
_ => label.push(format!("{vk:?}").to_lowercase()),
}
}
KeyType::Other => {}
}
}
if let Some(mods) = KEYS_TO_MODS.get(vk) {
button_state = KeyButtonData::Modifier {
modifier: *mods,
sticky: false.into(),
};
} else {
button_state = KeyButtonData::Key {
vk,
pressed: false.into(),
};
}
} else if let Some(macro_verbs) = self.macros.get(key) {
button_state = KeyButtonData::Macro {
verbs: key_events_for_macro(macro_verbs),
};
} else if let Some(exec_args) = self.exec_commands.get(key) {
let mut iter = exec_args.iter().cloned();
if let Some(program) = iter.next() {
button_state = KeyButtonData::Exec {
program,
args: iter.by_ref().take_while(|arg| arg[..] != *"null").collect(),
release_program: iter.next(),
release_args: iter.collect(),
};
} else {
log::error!("Keyboard: EXEC args empty for {key}");
return None;
}
} else {
log::error!("Unknown key: {key}");
return None;
}
if label.is_empty() {
label = self.label_for_key(key);
}
Some(KeyData {
label,
button_state,
cap_type,
})
}
fn label_for_key(&self, key: &str) -> Vec<String> {
if let Some(label) = self.labels.get(key) {
return label.clone();
}
if key.is_empty() {
return vec![];
}
if key.len() == 1 {
return vec![key.to_string().to_lowercase()];
}
let mut key = key;
if key.starts_with("KP_") {
key = &key[3..];
}
if key.contains('_') {
key = key.split('_').next().unwrap_or_else(|| {
log::error!("keyboard.yaml: Key '{key}' must not start or end with '_'!");
"???"
});
}
vec![format!(
"{}{}",
key.chars().next().unwrap().to_uppercase(), // safe because we checked is_empty
&key[1..].to_lowercase()
)]
}
}
pub(super) fn key_events_for_macro(macro_verbs: &Vec<String>) -> Vec<(VirtualKey, bool)> {
let mut key_events = vec![];
for verb in macro_verbs {
if let Some(caps) = MACRO_REGEX.captures(verb) {
if let Ok(virtual_key) = VirtualKey::from_str(&caps[1]) {
if let Some(state) = caps.get(2) {
if state.as_str() == "UP" {
key_events.push((virtual_key, false));
} else if state.as_str() == "DOWN" {
key_events.push((virtual_key, true));
} else {
log::error!(
"Unknown key state in macro: {}, looking for UP or DOWN.",
state.as_str()
);
return vec![];
}
} else {
key_events.push((virtual_key, true));
key_events.push((virtual_key, false));
}
} else {
log::error!("Unknown virtual key: {}", &caps[1]);
return vec![];
}
}
}
key_events
}
pub(super) struct KeyData {
pub(super) label: Vec<String>,
pub(super) button_state: KeyButtonData,
pub(super) cap_type: KeyCapType,
}
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize)]
#[repr(usize)]
pub enum AltModifier {
#[default]
None,
Shift,
Ctrl,
Alt,
Super,
Meta,
}
#[derive(Debug)]
pub enum KeyCapType {
/// Label an SVG
Special,
/// Label is in center of keycap
Letter,
/// Label on the top
/// AltGr symbol on bottom
LetterAltGr,
/// Primary symbol on bottom
/// Shift symbol on top
Symbol,
/// Primary symbol on bottom-left
/// Shift symbol on top-left
/// AltGr symbol on bottom-right
SymbolAltGr,
}

View File

@@ -0,0 +1,239 @@
use std::{
cell::{Cell, RefCell},
process::{Child, Command},
rc::Rc,
sync::Arc,
};
use vulkano::image::view::ImageView;
use wgui::{drawing, event::MouseButton};
use crate::{
backend::{
input::InteractionHandler,
overlay::{FrameMeta, OverlayBackend, OverlayRenderer, ShouldRender},
},
graphics::CommandBuffers,
gui::panel::GuiPanel,
state::AppState,
subsystem::{
audio::{AudioOutput, AudioRole},
hid::{ALT, CTRL, KeyModifier, META, SHIFT, SUPER, VirtualKey},
input::HidWrapper,
},
};
pub mod builder;
mod layout;
pub const KEYBOARD_NAME: &str = "kbd";
const AUTO_RELEASE_MODS: [KeyModifier; 5] = [SHIFT, CTRL, ALT, SUPER, META];
struct KeyboardBackend {
panel: GuiPanel,
state: Rc<RefCell<KeyboardState>>,
}
impl OverlayBackend for KeyboardBackend {
fn set_interaction(&mut self, interaction: Box<dyn crate::backend::input::InteractionHandler>) {
self.panel.set_interaction(interaction);
}
fn set_renderer(&mut self, renderer: Box<dyn crate::backend::overlay::OverlayRenderer>) {
self.panel.set_renderer(renderer);
}
}
impl InteractionHandler for KeyboardBackend {
fn on_pointer(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
pressed: bool,
) {
self.panel.on_pointer(app, hit, pressed);
}
fn on_scroll(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
delta_y: f32,
delta_x: f32,
) {
self.panel.on_scroll(app, hit, delta_y, delta_x);
}
fn on_left(&mut self, app: &mut AppState, pointer: usize) {
self.panel.on_left(app, pointer);
}
fn on_hover(
&mut self,
app: &mut AppState,
hit: &crate::backend::input::PointerHit,
) -> Option<crate::backend::input::Haptics> {
self.panel.on_hover(app, hit)
}
}
impl OverlayRenderer for KeyboardBackend {
fn init(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.panel.init(app)
}
fn should_render(&mut self, app: &mut AppState) -> anyhow::Result<ShouldRender> {
self.panel.should_render(app)
}
fn render(
&mut self,
app: &mut AppState,
tgt: Arc<ImageView>,
buf: &mut CommandBuffers,
alpha: f32,
) -> anyhow::Result<bool> {
self.panel.render(app, tgt, buf, alpha)
}
fn frame_meta(&mut self) -> Option<FrameMeta> {
self.panel.frame_meta()
}
fn pause(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.state.borrow_mut().modifiers = 0;
app.hid_provider.borrow_mut().set_modifiers_routed(0);
self.panel.pause(app)
}
fn resume(&mut self, app: &mut AppState) -> anyhow::Result<()> {
self.panel.resume(app)
}
}
struct KeyboardState {
hid: Rc<RefCell<HidWrapper>>,
audio: Rc<RefCell<AudioOutput>>,
modifiers: KeyModifier,
alt_modifier: KeyModifier,
processes: Vec<Child>,
}
const KEY_AUDIO_WAV: &[u8] = include_bytes!("../../res/421581.wav");
struct KeyState {
button_state: KeyButtonData,
color: drawing::Color,
color2: drawing::Color,
border_color: drawing::Color,
}
enum KeyButtonData {
Key {
vk: VirtualKey,
pressed: Cell<bool>,
},
Modifier {
modifier: KeyModifier,
sticky: Cell<bool>,
},
Macro {
verbs: Vec<(VirtualKey, bool)>,
},
Exec {
program: String,
args: Vec<String>,
release_program: Option<String>,
release_args: Vec<String>,
},
}
fn play_key_click(keyboard: &KeyboardState) {
keyboard
.audio
.borrow_mut()
.play(AudioRole::Keyboard, KEY_AUDIO_WAV);
}
fn handle_press(key: Rc<KeyState>, keyboard: Rc<RefCell<KeyboardState>>, button: MouseButton) {
let mut keyboard = keyboard.borrow_mut();
match &key.button_state {
KeyButtonData::Key { vk, pressed } => {
keyboard.modifiers |= match button {
MouseButton::Right => SHIFT,
MouseButton::Middle => keyboard.alt_modifier,
_ => 0,
};
{
let mut hid = keyboard.hid.borrow_mut();
hid.set_modifiers_routed(keyboard.modifiers);
hid.send_key_routed(*vk, true);
}
pressed.set(true);
play_key_click(&keyboard);
}
KeyButtonData::Modifier { modifier, sticky } => {
sticky.set(keyboard.modifiers & *modifier == 0);
keyboard.modifiers |= *modifier;
keyboard
.hid
.borrow_mut()
.set_modifiers_routed(keyboard.modifiers);
play_key_click(&keyboard);
}
KeyButtonData::Macro { verbs } => {
let hid = keyboard.hid.borrow_mut();
for (vk, press) in verbs {
hid.send_key_routed(*vk, *press);
}
play_key_click(&keyboard);
}
KeyButtonData::Exec { program, args, .. } => {
// Reap previous processes
keyboard
.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
if let Ok(child) = Command::new(program).args(args).spawn() {
keyboard.processes.push(child);
}
play_key_click(&keyboard);
}
}
}
fn handle_release(key: Rc<KeyState>, keyboard: Rc<RefCell<KeyboardState>>, _button: MouseButton) {
let mut keyboard = keyboard.borrow_mut();
match &key.button_state {
KeyButtonData::Key { vk, pressed } => {
pressed.set(false);
for m in &AUTO_RELEASE_MODS {
if keyboard.modifiers & *m != 0 {
keyboard.modifiers &= !*m;
}
}
let mut hid = keyboard.hid.borrow_mut();
hid.send_key_routed(*vk, false);
hid.set_modifiers_routed(keyboard.modifiers);
}
KeyButtonData::Modifier { modifier, sticky } => {
if !sticky.get() {
keyboard.modifiers &= !*modifier;
keyboard
.hid
.borrow_mut()
.set_modifiers_routed(keyboard.modifiers);
}
}
KeyButtonData::Exec {
release_program,
release_args,
..
} => {
// Reap previous processes
keyboard
.processes
.retain_mut(|child| !matches!(child.try_wait(), Ok(Some(_))));
if let Some(program) = release_program {
if let Ok(child) = Command::new(program).args(release_args).spawn() {
keyboard.processes.push(child);
}
}
}
_ => {}
}
}