use crate::core::{ObjectId, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyboardState {
Hidden,
Showing,
Visible,
Hiding,
}
impl KeyboardState {
pub fn is_active(self) -> bool {
matches!(self, Self::Showing | Self::Visible | Self::Hiding)
}
pub fn is_visible(self) -> bool {
self == Self::Visible
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct KeyboardNotch {
pub height: u32,
pub animation_ms: u32,
}
impl Default for KeyboardNotch {
fn default() -> Self {
Self { height: 0, animation_ms: 300 }
}
}
impl KeyboardNotch {
pub const fn new(height: u32) -> Self {
Self { height, animation_ms: 300 }
}
pub fn is_present(self) -> bool {
self.height > 0
}
}
#[derive(Debug)]
pub struct VirtualKeyboard {
state: KeyboardState,
notch: KeyboardNotch,
focused_widget: Option<ObjectId>,
original_offset_y: i32,
shift_y: i32,
}
impl Default for VirtualKeyboard {
fn default() -> Self {
Self {
state: KeyboardState::Hidden,
notch: KeyboardNotch::default(),
focused_widget: None,
original_offset_y: 0,
shift_y: 0,
}
}
}
impl VirtualKeyboard {
pub fn new() -> Self {
Self::default()
}
pub fn state(&self) -> KeyboardState {
self.state
}
pub fn notch(&self) -> KeyboardNotch {
self.notch
}
pub fn shift_y(&self) -> i32 {
self.shift_y
}
pub fn focused_widget(&self) -> Option<ObjectId> {
self.focused_widget
}
pub fn is_keyboard_active(&self) -> bool {
self.state.is_active()
}
pub fn request_show(
&mut self,
widget_id: ObjectId,
widget_rect: Rect,
screen_height: u32,
notch: KeyboardNotch,
) {
self.focused_widget = Some(widget_id);
self.notch = notch;
self.state = KeyboardState::Showing;
let widget_bottom = widget_rect.y + widget_rect.height as i32;
let available_height = screen_height.saturating_sub(self.notch.height) as i32;
if widget_bottom > available_height {
self.original_offset_y = self.shift_y;
self.shift_y = available_height - widget_bottom;
} else {
self.original_offset_y = 0;
self.shift_y = 0;
}
}
pub fn on_shown(&mut self) {
self.state = KeyboardState::Visible;
}
pub fn request_hide(&mut self) {
self.state = KeyboardState::Hiding;
self.focused_widget = None;
}
pub fn on_hidden(&mut self) {
self.state = KeyboardState::Hidden;
self.notch = KeyboardNotch::default();
self.shift_y = self.original_offset_y;
self.original_offset_y = 0;
}
pub fn apply_layout_shift(&self, widget_rect: &mut Rect) {
if self.shift_y != 0 {
widget_rect.y += self.shift_y;
}
}
pub fn reset(&mut self) {
self.state = KeyboardState::Hidden;
self.notch = KeyboardNotch::default();
self.focused_widget = None;
self.shift_y = 0;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_hidden() {
let kb = VirtualKeyboard::new();
assert_eq!(kb.state(), KeyboardState::Hidden);
assert!(!kb.is_keyboard_active());
assert_eq!(kb.shift_y(), 0);
assert!(kb.focused_widget().is_none());
}
#[test]
fn request_show_transitions_to_showing() {
let mut kb = VirtualKeyboard::new();
let rect = Rect::new(0, 100, 200, 40);
kb.request_show(ObjectId::from(42u64), rect, 800, KeyboardNotch::new(300));
assert_eq!(kb.state(), KeyboardState::Showing);
assert!(kb.is_keyboard_active());
assert_eq!(kb.focused_widget(), Some(ObjectId::from(42u64)));
}
#[test]
fn on_shown_transitions_to_visible() {
let mut kb = VirtualKeyboard::new();
kb.request_show(
ObjectId::from(1u64),
Rect::new(0, 100, 200, 40),
800,
KeyboardNotch::new(300),
);
kb.on_shown();
assert_eq!(kb.state(), KeyboardState::Visible);
}
#[test]
fn request_hide_transitions_to_hiding() {
let mut kb = VirtualKeyboard::new();
kb.request_show(
ObjectId::from(1u64),
Rect::new(0, 100, 200, 40),
800,
KeyboardNotch::new(300),
);
kb.on_shown();
kb.request_hide();
assert_eq!(kb.state(), KeyboardState::Hiding);
}
#[test]
fn on_hidden_resets_state() {
let mut kb = VirtualKeyboard::new();
kb.request_show(
ObjectId::from(1u64),
Rect::new(0, 100, 200, 40),
800,
KeyboardNotch::new(300),
);
kb.on_shown();
kb.request_hide();
kb.on_hidden();
assert_eq!(kb.state(), KeyboardState::Hidden);
assert_eq!(kb.shift_y(), 0);
assert!(kb.focused_widget().is_none());
}
#[test]
fn shift_applied_when_widget_would_be_covered() {
let mut kb = VirtualKeyboard::new();
let rect = Rect::new(0, 700, 200, 40); kb.request_show(
ObjectId::from(1u64),
rect,
800, KeyboardNotch::new(300), );
assert_eq!(kb.shift_y(), -240);
}
#[test]
fn no_shift_when_widget_above_keyboard() {
let mut kb = VirtualKeyboard::new();
let rect = Rect::new(0, 100, 200, 40); kb.request_show(ObjectId::from(1u64), rect, 800, KeyboardNotch::new(300));
assert_eq!(kb.shift_y(), 0);
}
#[test]
fn apply_layout_shift_modifies_rect() {
let mut kb = VirtualKeyboard::new();
let rect = Rect::new(0, 700, 200, 40);
kb.request_show(ObjectId::from(1u64), rect, 800, KeyboardNotch::new(300));
let mut shifted = Rect::new(10, 200, 100, 30);
kb.apply_layout_shift(&mut shifted);
assert_eq!(shifted.y, 200 + kb.shift_y());
}
#[test]
fn reset_clears_focus_and_shift() {
let mut kb = VirtualKeyboard::new();
kb.request_show(
ObjectId::from(1u64),
Rect::new(0, 700, 200, 40),
800,
KeyboardNotch::new(300),
);
kb.on_shown();
kb.reset();
assert_eq!(kb.state(), KeyboardState::Hidden);
assert_eq!(kb.shift_y(), 0);
assert!(kb.focused_widget().is_none());
}
}