mod draw;
pub mod layout;
use std::collections::HashMap;
use std::time::Duration;
use crate::time::Instant;
use crate::types::{ Point, Rect };
use crate::Keysym;
pub use layout::{ Action, Key, Layout, Modifier };
pub const LONG_PRESS: Duration = Duration::from_millis( 450 );
const REPEAT_INTERVAL: Duration = Duration::from_millis( 60 );
const SHIFT_DOUBLE_TAP: Duration = Duration::from_millis( 350 );
const GAP: f32 = 6.0;
const ROW_GAP: f32 = 2.0 * GAP;
const PADDING: f32 = 6.0;
const MAX_HEIGHT_FRACTION: f32 = 0.4;
#[ derive( Clone, Copy, Debug, PartialEq, Eq, Default ) ]
pub enum Purpose
{
#[ default ]
Normal,
Terminal,
}
#[ derive( Clone, Copy, Debug, Default, PartialEq, Eq ) ]
pub struct Modifiers
{
pub ctrl: bool,
pub alt: bool,
}
impl Modifiers
{
pub fn any( &self ) -> bool
{
self.ctrl || self.alt
}
fn take( &mut self ) -> Self
{
std::mem::take( self )
}
}
#[ derive( Clone, Debug, PartialEq, Eq ) ]
pub enum Event
{
Text( String ),
Backspace,
Enter,
Key( Keysym, Modifiers ),
Hide,
Changed,
}
#[ derive( Clone, Copy, Debug, PartialEq, Eq ) ]
enum Shift
{
Off,
Once,
Locked,
}
#[ derive( Clone, Debug ) ]
pub struct KeyRect
{
pub rect: Rect,
pub key: Key,
}
#[ derive( Clone, Debug ) ]
struct Press
{
key: usize,
at: Instant,
repeats: Option<u32>,
}
#[ derive( Clone, Debug ) ]
pub struct Popup
{
pub key: usize,
pub variants: Vec<String>,
pub rects: Vec<Rect>,
pub bounds: Rect,
pub selected: Option<usize>,
}
pub struct Panel
{
layouts: Vec<Layout>,
current: usize,
layer: String,
shift: Shift,
last_shift_at: Option<Instant>,
purpose: Purpose,
bar: Option<Layout>,
modifiers: Modifiers,
rect: Rect,
scale: f32,
keys: Vec<KeyRect>,
press: Option<Press>,
popup: Option<Popup>,
icons: HashMap<( &'static str, u32, [ u8; 4 ] ), Option<crate::ImageData>>,
}
impl Panel
{
pub fn new( mut layouts: Vec<Layout>, preferred: &str ) -> Self
{
if layouts.is_empty() { layouts.push( layout::fallback() ); }
let current = layouts.iter().position( |l| l.name == preferred ).unwrap_or( 0 );
Self
{
layouts,
current,
layer: layout::BASE_LAYER.to_string(),
shift: Shift::Off,
last_shift_at: None,
purpose: Purpose::Normal,
bar: None,
modifiers: Modifiers::default(),
rect: Rect::default(),
scale: 1.0,
keys: Vec::new(),
press: None,
popup: None,
icons: HashMap::new(),
}
}
pub fn for_language( lang: &str ) -> Self
{
let preferred = lang.split( [ '-', '_', '.' ] ).next().unwrap_or( lang ).to_lowercase();
let layouts: Vec<Layout> = layout::available().iter().filter_map( |name| layout::load( name ) ).collect();
Self::new( layouts, &preferred )
}
pub fn layout( &self ) -> &Layout
{
&self.layouts[ self.current ]
}
pub fn layer( &self ) -> &str
{
&self.layer
}
pub fn rect( &self ) -> Rect
{
self.rect
}
pub fn keys( &self ) -> &[ KeyRect ]
{
&self.keys
}
pub fn popup( &self ) -> Option<&Popup>
{
self.popup.as_ref()
}
pub fn pressed( &self ) -> Option<usize>
{
self.press.as_ref().map( |p| p.key )
}
pub fn shift_locked( &self ) -> bool
{
self.shift == Shift::Locked
}
pub fn modifiers( &self ) -> Modifiers
{
self.modifiers
}
pub fn purpose( &self ) -> Purpose
{
self.purpose
}
pub fn set_purpose( &mut self, purpose: Purpose )
{
if purpose == self.purpose { return; }
self.purpose = purpose;
self.bar = ( purpose == Purpose::Terminal ).then( layout::terminal_bar );
self.modifiers = Modifiers::default();
self.relayout();
}
fn bar_rows( &self ) -> &[ Vec<Key> ]
{
self.bar.as_ref().map( |b| b.rows( layout::BASE_LAYER ) ).unwrap_or( &[] )
}
pub fn height( &self, width: f32, scale: f32 ) -> f32
{
let rows = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32;
rows * self.key_unit( width, scale ) + ( ( rows - 1.0 ).max( 0.0 ) * ROW_GAP + 2.0 * PADDING ) * scale
}
pub fn height_within( &self, width: f32, scale: f32, screen_height: f32 ) -> f32
{
self.height( width, scale ).min( screen_height * MAX_HEIGHT_FRACTION )
}
fn key_unit( &self, width: f32, scale: f32 ) -> f32
{
let rows = self.layout().rows( &self.layer );
let widest = rows.iter()
.map( |row| row.iter().map( |k| k.width ).sum::<f32>() )
.fold( 0.0_f32, f32::max )
.max( 1.0 );
let most = rows.iter().map( Vec::len ).max().unwrap_or( 1 ).saturating_sub( 1 ) as f32;
( width - 2.0 * PADDING * scale - GAP * scale * most ) / widest
}
pub fn set_rect( &mut self, rect: Rect, scale: f32 )
{
self.rect = rect;
self.scale = scale;
self.keys.clear();
let gap = GAP * scale;
let row_gap = ROW_GAP * scale;
let pad = PADDING * scale;
let unit = self.key_unit( rect.width, scale );
let rows_n = ( self.bar_rows().len() + self.layout().rows( &self.layer ).len() ) as f32;
let fit = ( rect.height - 2.0 * pad - ( rows_n - 1.0 ).max( 0.0 ) * row_gap ) / rows_n.max( 1.0 );
let key_h = unit.min( fit.max( 1.0 ) );
let avail = rect.width - 2.0 * pad;
let mut y = rect.y + pad;
let bar: Vec<Vec<Key>> = self.bar_rows().to_vec();
for row in &bar
{
let units: f32 = row.iter().map( |k| k.width ).sum::<f32>().max( 1.0 );
let unit = ( avail - gap * row.len().saturating_sub( 1 ) as f32 ) / units;
self.push_row( row, rect.x + pad, y, unit, key_h, gap );
y += key_h + row_gap;
}
let rows: Vec<Vec<Key>> = self.layout().rows( &self.layer ).iter()
.map( |row| row.iter().map( |k| self.labelled( k ) ).collect() )
.collect();
for row in &rows
{
let units: f32 = row.iter().map( |k| k.width ).sum();
let row_w = units * unit + ( row.len().saturating_sub( 1 ) ) as f32 * gap;
self.push_row( row, rect.x + pad + ( avail - row_w ) / 2.0, y, unit, key_h, gap );
y += key_h + row_gap;
}
self.popup = None;
}
fn push_row( &mut self, row: &[ Key ], mut x: f32, y: f32, unit: f32, key_h: f32, gap: f32 )
{
for key in row
{
let w = key.width * unit;
self.keys.push( KeyRect { rect: Rect { x, y, width: w, height: key_h }, key: key.clone() } );
x += w + gap;
}
}
fn labelled( &self, key: &Key ) -> Key
{
if key.action == Some( Action::Layout ) && key.label.is_empty()
{
return Key { label: self.layout().label.clone(), ..key.clone() };
}
key.clone()
}
fn key_at( &self, pos: Point ) -> Option<usize>
{
if let Some( idx ) = self.keys.iter().position( |k| k.rect.contains( pos ) )
{
return Some( idx );
}
let reach = ROW_GAP * self.scale;
self.keys.iter()
.enumerate()
.map( |( i, k )|
{
let dx = ( k.rect.x - pos.x ).max( pos.x - ( k.rect.x + k.rect.width ) ).max( 0.0 );
let dy = ( k.rect.y - pos.y ).max( pos.y - ( k.rect.y + k.rect.height ) ).max( 0.0 );
( i, dx.hypot( dy ) )
} )
.filter( |&( _, d )| d <= reach )
.min_by( |a, b| a.1.total_cmp( &b.1 ) )
.map( |( i, _ )| i )
}
pub fn press_down( &mut self, pos: Point, now: Instant ) -> Option<Event>
{
if !self.rect.contains( pos ) { return None; }
let Some( idx ) = self.key_at( pos ) else { return Some( Event::Changed ) };
let backspace = self.keys[ idx ].key.action == Some( Action::Backspace );
self.press = Some( Press { key: idx, at: now, repeats: backspace.then_some( 0 ) } );
self.popup = None;
Some( if backspace { Event::Backspace } else { Event::Changed } )
}
pub fn press_move( &mut self, pos: Point ) -> bool
{
let Some( popup ) = self.popup.as_mut() else { return false };
let selected = popup.rects.iter().position( |r| pos.x >= r.x && pos.x < r.x + r.width );
let changed = selected != popup.selected;
popup.selected = selected.or( popup.selected );
changed
}
pub fn press_up( &mut self, pos: Point, now: Instant ) -> Option<Event>
{
let press = self.press.take()?;
if let Some( popup ) = self.popup.take()
{
let chosen = popup.selected
.or_else( || popup.rects.iter().position( |r| r.contains( pos ) ) )
.and_then( |i| popup.variants.get( i ).cloned() );
return Some( match chosen
{
Some( text ) => self.typed( text ),
None => Event::Changed,
} );
}
if press.repeats.is_some() { return Some( Event::Changed ); }
let key = self.keys.get( press.key )?.key.clone();
Some( self.activate( &key, now ) )
}
pub fn tick( &mut self, now: Instant ) -> Option<Event>
{
let press = self.press.as_mut()?;
let held = now.duration_since( press.at );
if let Some( repeats ) = press.repeats.as_mut()
{
if held < LONG_PRESS { return None; }
let due = ( ( held - LONG_PRESS ).as_millis() / REPEAT_INTERVAL.as_millis() ) as u32 + 1;
if due <= *repeats { return None; }
*repeats = due;
return Some( Event::Backspace );
}
if self.popup.is_some() || held < LONG_PRESS { return None; }
let idx = press.key;
let text = self.keys[ idx ].key.text.clone()?;
let variants = self.layout().variants( &text );
if variants.is_empty() { return None; }
self.popup = Some( self.popup_for( idx, variants ) );
Some( Event::Changed )
}
pub fn next_deadline( &self ) -> Option<Instant>
{
let press = self.press.as_ref()?;
match press.repeats
{
Some( repeats ) => Some( press.at + LONG_PRESS + REPEAT_INTERVAL * repeats ),
None if self.popup.is_none() => Some( press.at + LONG_PRESS ),
None => None,
}
}
pub fn cancel_press( &mut self )
{
self.press = None;
self.popup = None;
}
fn popup_for( &self, idx: usize, variants: Vec<String> ) -> Popup
{
let key = self.keys[ idx ].rect;
let gap = GAP * self.scale;
let w = key.width.max( key.height );
let total = variants.len() as f32 * w + ( variants.len().saturating_sub( 1 ) ) as f32 * gap;
let x0 = ( key.x + key.width / 2.0 - total / 2.0 ).max( self.rect.x ).min( self.rect.x + self.rect.width - total );
let y = key.y - key.height - gap;
let rects: Vec<Rect> = ( 0..variants.len() )
.map( |i| Rect { x: x0 + i as f32 * ( w + gap ), y, width: w, height: key.height } )
.collect();
let bounds = Rect { x: x0 - gap, y: y - gap, width: total + 2.0 * gap, height: key.height + 2.0 * gap };
Popup { key: idx, variants, rects, bounds, selected: None }
}
fn activate( &mut self, key: &Key, now: Instant ) -> Event
{
if let Some( modifier ) = key.modifier
{
match modifier
{
Modifier::Ctrl => self.modifiers.ctrl = !self.modifiers.ctrl,
Modifier::Alt => self.modifiers.alt = !self.modifiers.alt,
}
return Event::Changed;
}
if let Some( keysym ) = key.keysym
{
return Event::Key( keysym, self.modifiers.take() );
}
match key.action
{
None =>
{
match &key.text
{
Some( t ) => self.typed( t.clone() ),
None => Event::Changed,
}
}
Some( Action::Space ) => self.typed( " ".to_string() ),
Some( Action::Backspace ) => Event::Backspace,
Some( Action::Enter ) => Event::Enter,
Some( Action::Hide ) => Event::Hide,
Some( Action::Shift ) =>
{
let double = self.last_shift_at.is_some_and( |t| now.duration_since( t ) < SHIFT_DOUBLE_TAP );
self.last_shift_at = Some( now );
self.shift = match ( self.shift, double )
{
( Shift::Once, true ) => Shift::Locked,
( Shift::Off, _ ) => Shift::Once,
_ => Shift::Off,
};
self.layer = if self.shift == Shift::Off { layout::BASE_LAYER } else { layout::SHIFT_LAYER }.to_string();
self.relayout();
Event::Changed
}
Some( Action::Layer ) =>
{
if let Some( target ) = key.layer.as_deref().filter( |l| self.layout().has_layer( l ) )
{
self.layer = target.to_string();
self.shift = Shift::Off;
self.relayout();
}
Event::Changed
}
Some( Action::Layout ) =>
{
self.current = ( self.current + 1 ) % self.layouts.len();
self.layer = layout::BASE_LAYER.to_string();
self.shift = Shift::Off;
self.relayout();
Event::Changed
}
}
}
fn typed( &mut self, text: String ) -> Event
{
if self.shift == Shift::Once
{
self.shift = Shift::Off;
self.layer = layout::BASE_LAYER.to_string();
self.relayout();
}
if self.modifiers.any()
{
if let Some( ch ) = text.chars().next().filter( |_| text.chars().count() == 1 )
{
return Event::Key( Keysym::from_char( ch ), self.modifiers.take() );
}
}
Event::Text( text )
}
fn relayout( &mut self )
{
let ( rect, scale ) = ( self.rect, self.scale );
self.set_rect( rect, scale );
}
}
#[ cfg( test ) ]
mod tests
{
use super::*;
fn panel() -> Panel
{
let mut p = Panel::new( Vec::new(), "en" );
p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 400.0, 1.0 ) }, 1.0 );
p
}
fn centre( p: &Panel, text: &str ) -> Point
{
let k = p.keys().iter().find( |k| k.key.text.as_deref() == Some( text ) ).expect( "key present" );
Point { x: k.rect.x + k.rect.width / 2.0, y: k.rect.y + k.rect.height / 2.0 }
}
fn action_centre( p: &Panel, action: Action ) -> Point
{
let k = p.keys().iter().find( |k| k.key.action == Some( action.clone() ) ).expect( "action key present" );
Point { x: k.rect.x + k.rect.width / 2.0, y: k.rect.y + k.rect.height / 2.0 }
}
#[ test ]
fn rows_fit_the_width_and_narrow_rows_are_centred()
{
let p = panel();
for k in p.keys() { assert!( k.rect.x >= 0.0 && k.rect.x + k.rect.width <= 400.0 + 0.01 ); }
let first_q = p.keys().iter().find( |k| k.key.text.as_deref() == Some( "q" ) ).unwrap().rect;
let first_a = p.keys().iter().find( |k| k.key.text.as_deref() == Some( "a" ) ).unwrap().rect;
assert!( first_a.x > first_q.x );
}
#[ test ]
fn a_tap_types_the_key_on_release()
{
let mut p = panel();
let now = Instant::now();
let at = centre( &p, "q" );
assert_eq!( p.press_down( at, now ), Some( Event::Changed ) );
assert_eq!( p.press_up( at, now ), Some( Event::Text( "q".into() ) ) );
}
#[ test ]
fn shift_is_one_shot_and_locks_on_a_double_tap()
{
let mut p = panel();
let now = Instant::now();
let shift = action_centre( &p, Action::Shift );
p.press_down( shift, now ); p.press_up( shift, now );
assert_eq!( p.layer(), layout::SHIFT_LAYER );
let q = centre( &p, "Q" );
p.press_down( q, now ); assert_eq!( p.press_up( q, now ), Some( Event::Text( "Q".into() ) ) );
assert_eq!( p.layer(), layout::BASE_LAYER );
let shift = action_centre( &p, Action::Shift );
p.press_down( shift, now ); p.press_up( shift, now );
p.press_down( shift, now + Duration::from_millis( 100 ) ); p.press_up( shift, now + Duration::from_millis( 100 ) );
assert!( p.shift_locked() );
let q = centre( &p, "Q" );
p.press_down( q, now ); p.press_up( q, now );
assert_eq!( p.layer(), layout::SHIFT_LAYER );
}
#[ test ]
fn a_long_press_opens_the_variants_and_the_release_picks_one()
{
let mut p = panel();
let now = Instant::now();
let e = centre( &p, "e" );
p.press_down( e, now );
assert_eq!( p.tick( now + Duration::from_millis( 100 ) ), None );
assert_eq!( p.tick( now + LONG_PRESS ), Some( Event::Changed ) );
let second = p.popup().unwrap().rects[ 1 ];
let inside = Point { x: second.x + 1.0, y: second.y + 1.0 };
assert!( p.press_move( inside ) );
assert_eq!( p.press_up( inside, now + LONG_PRESS ), Some( Event::Text( "è".into() ) ) );
assert!( p.popup().is_none() );
}
#[ test ]
fn backspace_fires_on_press_and_repeats_while_held()
{
let mut p = panel();
let now = Instant::now();
let bs = action_centre( &p, Action::Backspace );
assert_eq!( p.press_down( bs, now ), Some( Event::Backspace ) );
assert_eq!( p.tick( now + Duration::from_millis( 100 ) ), None );
assert_eq!( p.tick( now + LONG_PRESS + Duration::from_millis( 1 ) ), Some( Event::Backspace ) );
assert_eq!( p.tick( now + LONG_PRESS + Duration::from_millis( 1 ) ), None );
assert_eq!( p.tick( now + LONG_PRESS + REPEAT_INTERVAL * 2 ), Some( Event::Backspace ) );
assert_eq!( p.press_up( bs, now + LONG_PRESS * 2 ), Some( Event::Changed ) );
}
#[ test ]
fn layers_and_layouts_switch()
{
let mut p = panel();
let now = Instant::now();
let sym = action_centre( &p, Action::Layer );
p.press_down( sym, now ); p.press_up( sym, now );
assert_eq!( p.layer(), "symbols" );
assert!( p.keys().iter().any( |k| k.key.text.as_deref() == Some( "1" ) ) );
let switch = p.keys().iter().find( |k| k.key.action == Some( Action::Layout ) ).unwrap();
assert_eq!( switch.key.label, "EN" );
}
#[ test ]
fn a_terminal_gets_its_row_and_sticky_modifiers()
{
let mut p = panel();
let now = Instant::now();
let plain = p.height( 400.0, 1.0 );
p.set_purpose( Purpose::Terminal );
assert!( p.height( 400.0, 1.0 ) > plain );
p.set_rect( Rect { x: 0.0, y: 0.0, width: 400.0, height: p.height( 400.0, 1.0 ) }, 1.0 );
let esc = p.keys().iter().find( |k| k.key.keysym == Some( Keysym::Escape ) ).unwrap().rect;
let at = Point { x: esc.x + 1.0, y: esc.y + 1.0 };
p.press_down( at, now );
assert_eq!( p.press_up( at, now ), Some( Event::Key( Keysym::Escape, Modifiers::default() ) ) );
let ctrl = p.keys().iter().find( |k| k.key.modifier == Some( Modifier::Ctrl ) ).unwrap().rect;
let at = Point { x: ctrl.x + 1.0, y: ctrl.y + 1.0 };
p.press_down( at, now ); p.press_up( at, now );
assert!( p.modifiers().ctrl );
let c = centre( &p, "c" );
p.press_down( c, now );
assert_eq!( p.press_up( c, now ), Some( Event::Key( Keysym::from_char( 'c' ), Modifiers { ctrl: true, alt: false } ) ) );
assert!( !p.modifiers().ctrl );
p.set_purpose( Purpose::Normal );
assert_eq!( p.height( 400.0, 1.0 ), plain );
}
#[ test ]
fn a_press_outside_the_panel_is_not_consumed()
{
let mut p = panel();
assert_eq!( p.press_down( Point { x: 10.0, y: -10.0 }, Instant::now() ), None );
}
}