use crate::draw_ctx::DrawCtx;
use crate::event::{Key, Modifiers, MouseButton};
use crate::geometry::{Point, Rect};
use crate::input_profile::current_input_profile;
pub mod events;
pub mod key;
pub mod layouts;
pub mod state;
pub mod style;
use events::push_synthetic_key;
use layouts::{Layer, Layout};
use state::{with_state_mut, with_state_ref};
use style::Style;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KeyboardInputMode {
#[default]
Text,
Numeric,
}
pub fn set_enabled(on: bool) {
with_state_mut(|s| s.enabled = on);
}
pub fn is_enabled() -> bool {
with_state_ref(|s| s.enabled)
}
pub fn is_visible() -> bool {
with_state_ref(|s| s.visible_fraction() > 0.001)
}
pub fn occluded_height(viewport_height: f64) -> f64 {
with_state_ref(|s| {
if !s.enabled {
return 0.0;
}
let target_h = s.last_panel_height.unwrap_or(0.0);
target_h * s.visible_fraction()
})
.min(viewport_height)
}
pub fn target_panel_height(viewport_width: f64) -> f64 {
with_state_ref(|s| {
if !s.enabled {
return 0.0;
}
let style = Style::for_profile(current_input_profile());
let layer = s.current_layer;
let layout = Layout::for_layer(layer);
let computed = layout.compute_panel_height(viewport_width, &style);
if computed > 0.0 {
computed
} else {
s.last_panel_height.unwrap_or(0.0)
}
})
}
pub fn set_text_input_focused(focused: bool, existing_text: Option<&str>, mode: KeyboardInputMode) {
with_state_mut(|s| {
if !s.enabled {
return;
}
s.text_input_focused = focused;
let target = if focused { 1.0 } else { 0.0 };
s.slide.set_target(target);
if focused {
match mode {
KeyboardInputMode::Numeric => {
s.current_layer = Layer::Numbers;
s.caps_lock = false;
s.last_shift_tap = None;
}
KeyboardInputMode::Text => {
if let Some(text) = existing_text {
let last_non_space = text.trim_end().chars().last();
let sentence_start = match last_non_space {
None => true, Some(c) if c == '.' || c == '!' || c == '?' || c == '\n' => true,
_ => false,
};
s.current_layer = if sentence_start {
Layer::Shifted
} else {
Layer::Letters
};
}
}
}
}
crate::animation::request_draw();
});
}
pub fn dismiss() {
with_state_mut(|s| {
s.text_input_focused = false;
s.slide.set_target(0.0);
s.dismiss_requested = true;
crate::animation::request_draw();
});
}
pub fn take_dismiss_request() -> bool {
with_state_mut(|s| {
let pending = s.dismiss_requested;
s.dismiss_requested = false;
pending
})
}
pub fn needs_draw() -> bool {
with_state_ref(|s| s.slide.is_animating() || s.key_repeat.is_some())
}
pub fn paint_software_keyboard(ctx: &mut dyn DrawCtx, viewport: crate::geometry::Size) {
tick_key_repeat();
let visible_fraction = with_state_mut(|s| s.slide.tick());
if visible_fraction <= 0.001 {
with_state_mut(|s| s.last_painted_keys.clear());
return;
}
let style = Style::for_profile(current_input_profile());
let layer = with_state_ref(|s| s.current_layer);
let layout = Layout::for_layer(layer);
let panel_height = layout.compute_panel_height(viewport.width, &style);
let panel_width = viewport.width;
with_state_mut(|s| s.last_panel_height = Some(panel_height));
let hidden_offset = panel_height * (1.0 - visible_fraction);
let bottom_y = -hidden_offset;
let panel = Rect::new(0.0, bottom_y, panel_width, panel_height);
paint_panel_background(ctx, panel, &style);
let painted_keys = layout.paint(ctx, panel, &style, layer);
with_state_mut(|s| s.last_painted_keys = painted_keys);
}
fn paint_panel_background(ctx: &mut dyn DrawCtx, panel: Rect, style: &Style) {
ctx.set_fill_color(style.panel_bg);
ctx.begin_path();
ctx.rect(panel.x, panel.y, panel.width, panel.height);
ctx.fill();
ctx.set_stroke_color(style.panel_top_border);
ctx.set_line_width(1.0);
ctx.begin_path();
let top_y = panel.y + panel.height;
ctx.move_to(panel.x, top_y);
ctx.line_to(panel.x + panel.width, top_y);
ctx.stroke();
}
pub fn contains_point(pos: Point) -> bool {
if !is_visible() {
return false;
}
with_state_ref(|s| {
let frac = s.slide.value();
if frac <= 0.001 {
return false;
}
let panel_height = s.last_panel_height.unwrap_or(0.0);
let panel_top = panel_height * frac;
pos.y >= 0.0 && pos.y <= panel_top
})
}
pub fn handle_software_keyboard_mouse_down(
pos: Point,
button: MouseButton,
_modifiers: Modifiers,
) -> bool {
if button != MouseButton::Left {
return contains_point(pos);
}
if !contains_point(pos) {
return false;
}
let hit = find_key_at(pos);
with_state_mut(|s| {
s.pressed_key_index = hit;
s.captured_pointer = true;
s.key_repeat = hit.and_then(|i| {
s.last_painted_keys.get(i).and_then(|k| match k.action {
key::KeyAction::Backspace => Some(state::KeyRepeatState {
key_index: i,
pressed_at: web_time::Instant::now(),
last_fired_at: None,
}),
_ => None,
})
});
});
if hit.is_some() {
crate::animation::request_draw();
}
true
}
pub fn handle_software_keyboard_mouse_move(pos: Point) -> bool {
let (captured, _) = with_state_ref(|s| (s.captured_pointer, s.pressed_key_index));
if !captured {
return false;
}
let new_hit = find_key_at(pos);
with_state_mut(|s| {
if s.pressed_key_index != new_hit {
s.pressed_key_index = new_hit;
crate::animation::request_draw();
}
});
true
}
pub fn handle_software_keyboard_mouse_up(
pos: Point,
button: MouseButton,
modifiers: Modifiers,
) -> bool {
let captured = with_state_ref(|s| s.captured_pointer);
if !captured {
return false;
}
let pressed = with_state_mut(|s| {
let p = s.pressed_key_index.take();
s.captured_pointer = false;
let repeat_fired = s
.key_repeat
.map(|r| r.last_fired_at.is_some())
.unwrap_or(false);
s.key_repeat = None;
(p, repeat_fired)
});
let (pressed_idx, repeat_already_fired) = pressed;
if button != MouseButton::Left {
crate::animation::request_draw();
return true;
}
let on_panel = contains_point(pos);
let final_hit = if on_panel { find_key_at(pos) } else { None };
if let (Some(start), Some(end)) = (pressed_idx, final_hit) {
if start == end && !repeat_already_fired {
commit_key_press(end, modifiers);
}
}
crate::animation::request_draw();
true
}
fn find_key_at(pos: Point) -> Option<usize> {
with_state_ref(|s| {
s.last_painted_keys
.iter()
.enumerate()
.find(|(_, k)| k.rect.contains(pos))
.map(|(i, _)| i)
})
}
fn commit_key_press(index: usize, modifiers: Modifiers) {
let painted = with_state_ref(|s| s.last_painted_keys.get(index).cloned());
let Some(painted) = painted else {
return;
};
let is_shift_action = matches!(painted.action, key::KeyAction::Switch(Layer::Shifted));
if !is_shift_action {
with_state_mut(|s| s.last_shift_tap = None);
}
match painted.action {
key::KeyAction::Char(c) => {
let mut mods = modifiers;
let was_shifted = with_state_ref(|s| s.current_layer == Layer::Shifted);
if was_shifted {
mods.shift = true;
}
push_synthetic_key(Key::Char(c), mods);
with_state_mut(|s| {
if s.current_layer == Layer::Shifted && !s.caps_lock {
s.current_layer = Layer::Letters;
}
});
}
key::KeyAction::Backspace => push_synthetic_key(Key::Backspace, modifiers),
key::KeyAction::Enter => {
push_synthetic_key(Key::Enter, modifiers);
}
key::KeyAction::Space => push_synthetic_key(Key::Char(' '), modifiers),
key::KeyAction::Switch(target) => {
handle_layer_switch(target);
}
key::KeyAction::Dismiss => dismiss(),
}
crate::animation::request_draw();
}
fn handle_layer_switch(target: Layer) {
if target == Layer::Shifted || target == Layer::Letters {
with_state_mut(|s| {
let now = web_time::Instant::now();
let recently_tapped = s
.last_shift_tap
.map(|t| now.duration_since(t) <= state::SHIFT_DOUBLE_TAP_WINDOW)
.unwrap_or(false);
if s.caps_lock {
s.caps_lock = false;
s.current_layer = Layer::Letters;
s.last_shift_tap = None;
} else if recently_tapped {
s.caps_lock = true;
s.current_layer = Layer::Shifted;
s.last_shift_tap = None;
} else {
s.current_layer = match s.current_layer {
Layer::Shifted => Layer::Letters,
_ => Layer::Shifted,
};
s.last_shift_tap = Some(now);
}
});
} else {
with_state_mut(|s| {
s.current_layer = target;
s.last_shift_tap = None;
});
}
}
fn tick_key_repeat() {
let now = web_time::Instant::now();
let action = with_state_mut(|s| {
let Some(repeat) = s.key_repeat.as_mut() else {
return None;
};
if !s.captured_pointer || s.pressed_key_index != Some(repeat.key_index) {
s.key_repeat = None;
return None;
}
let held = now.duration_since(repeat.pressed_at);
let should_fire = match repeat.last_fired_at {
None => held >= state::KeyRepeatState::INITIAL_DELAY,
Some(t) => now.duration_since(t) >= state::KeyRepeatState::REPEAT_PERIOD,
};
if should_fire {
let key = s.last_painted_keys.get(repeat.key_index)?.action;
repeat.last_fired_at = Some(now);
return Some(key);
}
None
});
if let Some(action) = action {
match action {
key::KeyAction::Backspace => {
push_synthetic_key(Key::Backspace, Modifiers::default());
}
_ => {}
}
crate::animation::request_draw();
}
}
pub use events::drain_synthetic_keys;
pub use key::{KeyAction, KeyCap};
pub use layouts::Layer as KeyboardLayer;
#[cfg(test)]
pub(crate) mod test_hook {
use super::*;
use crate::animation::Tween;
use state::KeyboardState;
#[allow(dead_code)]
pub fn force_layer(layer: Layer) {
with_state_mut(|s| s.current_layer = layer);
}
pub fn force_visible() {
with_state_mut(|s| {
s.enabled = true;
s.text_input_focused = true;
s.slide = Tween::new(1.0, 0.0);
s.last_panel_height = Some(240.0);
});
}
pub fn reset() {
with_state_mut(|s| {
*s = KeyboardState::default();
});
}
pub fn simulate_shift_tap() {
super::handle_layer_switch(super::Layer::Shifted);
}
pub fn caps_lock() -> bool {
with_state_ref(|s| s.caps_lock)
}
pub fn current_layer() -> Layer {
with_state_ref(|s| s.current_layer)
}
}