use denise::{ElementState, InputEvent, KeyCode, Modifiers, Point, Rect, Role};
use denise_layout::{Composer, Layout, LayoutSource, Output};
use denise_text::TextStyle;
use denise_ui::widgets::{Button, Panel, TextInput};
use denise_ui::{NodeId, Side, Ui};
mod grid;
pub mod icons;
pub use grid::{Key, ROWS, Row};
const LEVEL3_LEGEND: &str = "alt";
pub(crate) const LAYOUT_KEY: KeyCode = KeyCode::Unidentified(u32::MAX);
pub const KEY_HEIGHT: i32 = 48;
pub const KEY_GAP: i32 = 6;
const KEY_TEXT: u16 = 16;
pub const REPEAT_DELAY_MS: u64 = 450;
const STRIP_PAD: i32 = 8;
const STRIP_BORDER: i32 = 2;
pub const HOLD_MS: u64 = 500;
pub const REPEAT_INTERVAL_MS: u64 = 65;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Shift {
#[default]
Off,
Once,
}
impl Shift {
#[inline]
pub const fn legend(self) -> &'static str {
match self {
Shift::Off => "shift",
Shift::Once => "SHIFT",
}
}
#[inline]
const fn next(self) -> Self {
match self {
Shift::Off => Shift::Once,
Shift::Once => Shift::Off,
}
}
#[inline]
const fn holds_shift(self) -> bool {
matches!(self, Shift::Once)
}
}
pub struct Keyboard {
layout: &'static Layout,
composer: Composer,
modifiers: Modifiers,
shift: Shift,
level3: bool,
ctrl: bool,
offering: Option<Offering>,
shelf: Option<NodeId>,
keys: Vec<(KeyCode, NodeId)>,
scale: f32,
style: TextStyle,
}
impl Keyboard {
pub fn new(layout: &'static Layout) -> Self {
Self {
layout,
composer: Composer::new(layout),
modifiers: Modifiers::NONE,
shift: Shift::Off,
level3: false,
ctrl: false,
offering: None,
shelf: None,
keys: Vec::new(),
scale: 1.0,
style: TextStyle::built_in(KEY_TEXT),
}
}
#[must_use]
pub fn with_scale(mut self, scale: f32) -> Self {
self.scale = scale;
self
}
pub fn set_style<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, style: TextStyle) {
self.style = style;
for &(_, node) in &self.keys {
if let Some(button) = ui.widget_mut::<Button<M>>(node) {
button.set_style(style);
}
}
}
#[must_use]
pub fn with_style(mut self, style: TextStyle) -> Self {
self.style = style;
self
}
pub fn from_system() -> (Self, LayoutSource) {
let (layout, source) = denise_layout::from_system();
(Self::new(layout), source)
}
pub fn set_layout<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, layout: &'static Layout) {
if core::ptr::eq(self.layout, layout) {
return;
}
self.layout = layout;
self.composer.set_layout(layout);
self.relabel(ui);
}
pub fn cycle_layout<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) -> &'static Layout {
let next = denise_layout::BUILT_IN
.iter()
.position(|l| core::ptr::eq(*l, self.layout))
.map_or(0, |i| (i + 1) % denise_layout::BUILT_IN.len());
let layout = denise_layout::BUILT_IN[next];
self.set_layout(ui, layout);
layout
}
#[inline]
pub const fn layout(&self) -> &'static Layout {
self.layout
}
#[inline]
pub fn keys(&self) -> &[(KeyCode, NodeId)] {
&self.keys
}
#[inline]
pub const fn is_open(&self) -> bool {
self.shelf.is_some()
}
pub const LOGICAL_HEIGHT: i32 = ROWS.len() as i32 * (KEY_HEIGHT + KEY_GAP) + KEY_GAP;
#[inline]
pub fn height(&self) -> i32 {
self.scaled(Self::LOGICAL_HEIGHT)
}
#[inline]
fn scaled(&self, logical: i32) -> i32 {
Rect::new(0, 0, 0, logical).scaled(self.scale).height
}
pub fn open<M: Clone + 'static>(
&mut self,
ui: &mut Ui<M>,
on_key: fn(KeyCode) -> M,
) -> Option<NodeId> {
if self.shelf.is_some() {
return None;
}
let shelf = ui.push_shelf(Side::Below, self.height())?;
let width = ui.size().width as i32;
self.build(ui, shelf, width, on_key);
self.shelf = Some(shelf);
Some(shelf)
}
pub fn close<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) {
if self.shelf.take().is_some() {
self.keys.clear();
self.composer.set_layout(self.layout);
ui.close_shelf();
}
}
pub fn follow_focus<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, on_key: fn(KeyCode) -> M) {
let Some(focus) = ui.focus_changed() else {
return;
};
let wants = focus.is_some_and(|id| ui.widget::<TextInput<M>>(id).is_some());
if wants {
self.open(ui, on_key);
} else {
self.close(ui);
}
}
pub fn press_key<M: Clone + 'static>(
&mut self,
ui: &mut Ui<M>,
code: KeyCode,
) -> Vec<InputEvent> {
match code {
KeyCode::ShiftLeft => {
self.tap_shift();
self.relabel(ui);
Vec::new()
}
KeyCode::CapsLock => {
self.tap_caps();
self.relabel(ui);
Vec::new()
}
KeyCode::ControlLeft => {
self.tap_ctrl();
self.relabel(ui);
Vec::new()
}
KeyCode::AltRight => {
self.tap_level3();
self.relabel(ui);
Vec::new()
}
LAYOUT_KEY => {
self.cycle_layout(ui);
Vec::new()
}
_ => {
let spent = self.shift == Shift::Once || self.ctrl;
let events = self.press(code);
if spent {
self.relabel(ui);
}
events
}
}
}
pub fn tick<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, now_ms: u64) -> Vec<InputEvent> {
let _ = now_ms;
let mut out = Vec::new();
let owed: Vec<(KeyCode, NodeId)> = self
.keys
.iter()
.copied()
.filter(|&(_, node)| {
ui.widget::<Button<M>>(node)
.is_some_and(|button| button.repeats_pending() > 0)
})
.collect();
let held: Vec<(KeyCode, u32)> = owed
.into_iter()
.filter_map(|(code, node)| {
let repeats = ui.widget_mut::<Button<M>>(node)?.take_repeats();
(repeats > 0).then_some((code, repeats))
})
.collect();
for (code, repeats) in held {
for _ in 0..repeats {
out.extend(self.press_repeat(code));
}
}
self.offer_alternates(ui);
out
}
fn offer_alternates<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) {
if self.offering.is_some() || self.shelf.is_none() {
return;
}
let ready = self.keys.iter().copied().find(|&(_, node)| {
ui.widget::<Button<M>>(node)
.and_then(Button::held_ms)
.is_some_and(|held| held >= HOLD_MS)
});
let Some((code, key)) = ready else {
return;
};
self.open_alternates(ui, code, key);
}
fn open_alternates<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, code: KeyCode, key: NodeId) {
if self.offering.is_some() {
return;
}
let Some(shelf) = self.shelf else {
return;
};
let Some(base) = self.legend(code) else {
return;
};
let choices: Vec<char> = self.layout.alternates_for(base).collect();
if choices.is_empty() {
return;
}
let Some(bounds) = ui.layout(key) else {
return;
};
let cell = bounds.height;
let pad = self.scaled(STRIP_PAD);
let width = cell * choices.len() as i32 + pad * 2;
let height = cell + pad * 2;
let x = (bounds.x + bounds.width / 2 - width / 2)
.max(0)
.min((ui.size().width as i32 - width).max(0));
let y = (bounds.y - height - self.scaled(KEY_GAP) * 2).max(0);
let Some(strip) = ui.add(
shelf,
Panel::filled(Role::Base300)
.backdrop()
.with_radius(denise::theme::Radius::Box)
.with_border(Role::Primary, self.scaled(STRIP_BORDER).max(1)),
Rect::new(x, y, width, height),
) else {
return;
};
let mut nodes = Vec::with_capacity(choices.len());
for (i, ch) in choices.iter().enumerate() {
let at = Rect::new(pad + cell * i as i32, pad, cell, cell);
if let Some(node) = ui.add(
strip,
Button::<M>::inert(ch.to_string())
.no_focus()
.with_role(Role::Base100)
.with_style(self.style),
at,
) {
nodes.push((*ch, node));
}
}
self.offering = Some(Offering {
strip,
choices: nodes,
over: None,
});
}
pub fn handle<M: Clone + 'static>(
&mut self,
ui: &mut Ui<M>,
events: &[InputEvent],
) -> Vec<InputEvent> {
let mut out = Vec::new();
for event in events {
let Some(offering) = self.offering.as_ref() else {
return out;
};
match event {
InputEvent::PointerMoved { position } | InputEvent::TouchMoved { position, .. } => {
let at = self.choice_at(ui, *position);
if let Some(offering) = self.offering.as_mut()
&& offering.over != at
{
offering.over = at;
self.highlight(ui);
}
}
InputEvent::PointerButton {
state: ElementState::Up,
position,
..
}
| InputEvent::TouchUp {
position,
cancelled: false,
..
} => {
let chosen = self
.choice_at(ui, *position)
.and_then(|i| offering.choices.get(i))
.map(|(ch, _)| *ch);
self.close_offering(ui);
if let Some(ch) = chosen {
out.push(InputEvent::Text { ch });
}
}
InputEvent::TouchUp {
cancelled: true, ..
} => self.close_offering(ui),
_ => {}
}
}
out
}
fn choice_at<M: Clone + 'static>(&self, ui: &Ui<M>, at: Point) -> Option<usize> {
let offering = self.offering.as_ref()?;
offering
.choices
.iter()
.position(|&(_, node)| ui.bounds(node).is_some_and(|b| b.contains(at)))
}
fn highlight<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) {
let Some(offering) = self.offering.as_ref() else {
return;
};
let over = offering.over;
let nodes: Vec<(usize, NodeId)> = offering
.choices
.iter()
.enumerate()
.map(|(i, &(_, node))| (i, node))
.collect();
for (i, node) in nodes {
let role = if Some(i) == over {
Role::Primary
} else {
Role::Base100
};
let same = ui
.widget::<Button<M>>(node)
.is_some_and(|button| button.role() == role);
if !same && let Some(button) = ui.widget_mut::<Button<M>>(node) {
button.set_role(role);
}
}
}
fn close_offering<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) {
if let Some(offering) = self.offering.take() {
ui.remove(offering.strip);
}
}
pub fn offer_for_test<M: Clone + 'static>(&mut self, ui: &mut Ui<M>, code: KeyCode) {
let Some(&(_, key)) = self.keys.iter().find(|&&(c, _)| c == code) else {
return;
};
self.open_alternates(ui, code, key);
}
pub fn choices(&self) -> &[(char, NodeId)] {
self.offering.as_ref().map_or(&[], |o| &o.choices)
}
#[inline]
pub const fn offering(&self) -> bool {
self.offering.is_some()
}
pub fn press_repeat(&mut self, code: KeyCode) -> Vec<InputEvent> {
let mut out = Vec::with_capacity(3);
let modifiers = self.modifiers();
for state in [ElementState::Down, ElementState::Up] {
out.push(InputEvent::Key {
code,
state,
repeat: true,
modifiers,
});
let composed = self.composer.feed(code, state, modifiers);
for &ch in composed.as_slice() {
out.push(InputEvent::Text { ch });
}
}
out
}
pub fn press(&mut self, code: KeyCode) -> Vec<InputEvent> {
let mut out = Vec::with_capacity(3);
let modifiers = self.modifiers();
for state in [ElementState::Down, ElementState::Up] {
out.push(InputEvent::Key {
code,
state,
repeat: false,
modifiers,
});
let composed = self.composer.feed(code, state, modifiers);
for &ch in composed.as_slice() {
out.push(InputEvent::Text { ch });
}
}
if self.shift == Shift::Once {
self.shift = Shift::Off;
}
self.ctrl = false;
out
}
pub fn tap_shift(&mut self) -> Shift {
self.shift = self.shift.next();
self.shift
}
pub fn tap_caps(&mut self) -> bool {
let modifiers = self.modifiers();
self.composer
.feed(KeyCode::CapsLock, ElementState::Down, modifiers);
self.composer
.feed(KeyCode::CapsLock, ElementState::Up, modifiers);
self.composer.caps_lock()
}
#[inline]
pub fn caps(&self) -> bool {
self.composer.caps_lock()
}
pub fn tap_ctrl(&mut self) -> bool {
self.ctrl = !self.ctrl;
self.ctrl
}
#[inline]
pub const fn ctrl(&self) -> bool {
self.ctrl
}
pub fn tap_level3(&mut self) -> bool {
self.level3 = !self.level3;
let state = if self.level3 {
ElementState::Down
} else {
ElementState::Up
};
self.composer
.feed(KeyCode::AltRight, state, self.modifiers());
self.level3
}
#[inline]
pub fn occluded<M: Clone + 'static>(&self, ui: &Ui<M>) -> Option<Rect> {
self.shelf.and(ui.occluded())
}
#[inline]
pub const fn shift(&self) -> Shift {
self.shift
}
#[inline]
pub const fn level3(&self) -> bool {
self.level3
}
fn modifiers(&self) -> Modifiers {
let mut modifiers = self.modifiers;
if self.shift.holds_shift() {
modifiers |= Modifiers::SHIFT;
}
if self.ctrl {
modifiers |= Modifiers::CTRL;
}
modifiers
}
fn legend(&self, code: KeyCode) -> Option<char> {
match self.composer.output_for(code, self.shift.holds_shift()) {
Output::Char(ch) | Output::Dead(ch) => Some(ch),
Output::None => None,
}
}
fn label_for(&self, code: KeyCode) -> String {
match code {
KeyCode::ShiftLeft => self.shift.legend().to_string(),
KeyCode::CapsLock => if self.caps() { "CAPS" } else { "caps" }.to_string(),
KeyCode::ControlLeft => if self.ctrl { "CTRL" } else { "ctrl" }.to_string(),
KeyCode::AltRight => LEVEL3_LEGEND.to_string(),
LAYOUT_KEY => self.layout.name.to_string(),
_ => grid::legend_of(code)
.map(str::to_string)
.or_else(|| self.legend(code).map(|ch| ch.to_string()))
.unwrap_or_default(),
}
}
fn corner_for(&self, code: KeyCode) -> String {
if code == LAYOUT_KEY {
return self.layout.name.to_string();
}
if grid::legend_of(code).is_some() {
return String::new();
}
let Some(base) = self.legend(code) else {
return String::new();
};
if base.is_alphabetic() {
return String::new();
}
let shifted = match self.composer.output_for(code, true) {
Output::Char(ch) | Output::Dead(ch) => ch,
Output::None => return String::new(),
};
if shifted == base {
return String::new();
}
shifted.to_string()
}
pub fn relabel<M: Clone + 'static>(&mut self, ui: &mut Ui<M>) {
for &(code, node) in &self.keys {
let label = self.label_for(code);
let corner = self.corner_for(code);
if let Some(button) = ui.widget_mut::<Button<M>>(node) {
button.set_label(label);
button.set_corner(corner);
}
}
}
fn build<M: Clone + 'static>(
&mut self,
ui: &mut Ui<M>,
shelf: NodeId,
width: i32,
on_key: fn(KeyCode) -> M,
) {
ui.add(
shelf,
Panel::filled(Role::Base300).backdrop(),
Rect::new(0, 0, width, self.height()),
);
let width = if self.scale > 0.0 {
Rect::new(0, 0, width, 0).scaled(1.0 / self.scale).width
} else {
width
};
for (r, row) in ROWS.iter().enumerate() {
let y = KEY_GAP + r as i32 * (KEY_HEIGHT + KEY_GAP);
let count = row.keys.len() as i32;
let units: i32 = row.keys.iter().map(|k| k.units).sum::<i32>().max(1);
let leftover = (width - KEY_GAP * (count + 1)).max(count);
let mut done = 0;
for (i, key) in row.keys.iter().enumerate() {
let gaps = KEY_GAP * (i as i32 + 1);
let x = gaps + leftover * done / units;
done += key.units;
let w = gaps + leftover * done / units - x;
let mut button = Button::new(self.label_for(key.code), on_key(key.code))
.no_focus()
.with_role(role_of(key.code))
.with_style(self.style)
.with_corner(self.corner_for(key.code));
if let Some(icon) = grid::icon_of(key.code) {
button = button.with_icon(icon);
}
if key.repeats {
button = button.with_repeat(REPEAT_DELAY_MS, REPEAT_INTERVAL_MS);
} else if grid::legend_of(key.code).is_none() {
button = button.watching_hold();
}
if let Some(node) = ui.add(
shelf,
button,
Rect::new(x, y, w, KEY_HEIGHT).scaled(self.scale),
) {
self.keys.push((key.code, node));
}
}
}
}
}
fn role_of(code: KeyCode) -> Role {
match code {
KeyCode::ShiftLeft
| KeyCode::CapsLock
| KeyCode::ControlLeft
| KeyCode::AltRight
| KeyCode::Backspace
| KeyCode::Tab
| KeyCode::Escape
| KeyCode::ArrowLeft
| KeyCode::ArrowRight
| LAYOUT_KEY => Role::Neutral,
KeyCode::Enter => Role::Primary,
_ => Role::Base100,
}
}
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct Readme;
struct Offering {
strip: NodeId,
choices: Vec<(char, NodeId)>,
over: Option<usize>,
}