git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

src/server/osk.rs (5.3K)

  1 //! The on-screen keyboard follows a touched text field.
  2 //!
  3 //! When a client enables (or updates) a text-input-v3 field right after a
  4 //! finger landed on one of its windows, the board is shown with
  5 //! `cce-keyboard show`; when the field lets go it is hidden again with
  6 //! `cce-keyboard hide`, but only if this module showed it — a board summoned
  7 //! by hand (Super+O) stays until it is dismissed by hand.
  8 //!
  9 //! The touch is the whole signal: text-input-v3 says that a field is active,
 10 //! never why, and a field focused from the keyboard or with the pointer must
 11 //! not raise the board. So a touch on a window arms a short window
 12 //! (`TOUCH_WINDOW`), and the first activation inside it spends it. Spending
 13 //! it matters: every key tapped on the board makes the client commit a new
 14 //! caret, and a touch that could be reused would re-launch `show` per key.
 15 //! Touches on the board itself never arm it (they land on a layer surface).
 16 //!
 17 //! The hide waits `HIDE_DELAY_MS`: moving from one field to the next
 18 //! disables one text input and enables the other, and the board should not
 19 //! close and reopen in between.
 20 //!
 21 //! Off with `window_manager { osk_on_touch (bool)false }`.
 22 
 23 use std::time::{Duration, Instant};
 24 
 25 use crate::ffi;
 26 use crate::server::Server;
 27 
 28 /// How long after a touch on a window an activation still counts as the
 29 /// touch's. A tap that the client sees as a button press (no wl_touch) is
 30 /// only delivered at the lift, and the field enables a frame or two after
 31 /// that; a long press must still be inside it.
 32 const TOUCH_WINDOW: Duration = Duration::from_millis(800);
 33 
 34 /// How long a field may be gone before the board follows it.
 35 const HIDE_DELAY_MS: i32 = 250;
 36 
 37 const SHOW_CMD: &str = "cce-keyboard show";
 38 const HIDE_CMD: &str = "cce-keyboard hide";
 39 
 40 pub struct Osk {
 41     server: *mut Server,
 42     /// The last touch on a window, until an activation spends it.
 43     armed_at: Option<Instant>,
 44     /// This module put the board up, so it may take it down.
 45     shown_by_us: bool,
 46     hide_timer: *mut ffi::wl_event_source,
 47 }
 48 
 49 impl Osk {
 50     pub fn new(server: *mut Server) -> Self {
 51         Osk { server, armed_at: None, shown_by_us: false, hide_timer: std::ptr::null_mut() }
 52     }
 53 
 54     unsafe fn enabled(&self) -> bool {
 55         !self.server.is_null() && (*self.server).wm.osk_on_touch
 56     }
 57 
 58     /// A finger landed on (or, for a tap, lifted from) a window.
 59     pub fn note_touch(&mut self) {
 60         self.armed_at = Some(Instant::now());
 61     }
 62 
 63     /// Whether a touch armed the board recently enough to count, disarming
 64     /// it either way.
 65     fn spend_touch(&mut self, now: Instant) -> bool {
 66         self.armed_at.take().is_some_and(|t| now.saturating_duration_since(t) <= TOUCH_WINDOW)
 67     }
 68 
 69     /// The seat's text input was enabled, or committed new state while
 70     /// enabled.
 71     pub unsafe fn field_active(&mut self) {
 72         // Whatever raised the field, a hide queued by the last one is moot.
 73         self.cancel_hide();
 74         if !self.enabled() {
 75             return;
 76         }
 77         if !self.spend_touch(Instant::now()) {
 78             return;
 79         }
 80         log::debug!("osk: a touched field activated; showing the board");
 81         (*self.server).wm.execute_action(&crate::config::Action::Spawn, Some(SHOW_CMD));
 82         self.shown_by_us = true;
 83     }
 84 
 85     /// The seat's text input went away (disabled, destroyed, or focus left).
 86     pub unsafe fn field_gone(&mut self) {
 87         if !self.shown_by_us || self.server.is_null() {
 88             return;
 89         }
 90         if self.hide_timer.is_null() {
 91             let event_loop = ffi::wl_display_get_event_loop((*self.server).wl_server);
 92             self.hide_timer = ffi::wl_event_loop_add_timer(event_loop, Some(handle_hide_timer), self as *mut Osk as *mut _);
 93             if self.hide_timer.is_null() {
 94                 log::error!("osk: failed to create the hide timer; hiding now");
 95                 self.hide_now();
 96                 return;
 97             }
 98         }
 99         ffi::wl_event_source_timer_update(self.hide_timer, HIDE_DELAY_MS);
100     }
101 
102     unsafe fn cancel_hide(&mut self) {
103         if !self.hide_timer.is_null() {
104             ffi::wl_event_source_timer_update(self.hide_timer, 0);
105         }
106     }
107 
108     unsafe fn hide_now(&mut self) {
109         if !self.shown_by_us {
110             return;
111         }
112         self.shown_by_us = false;
113         log::debug!("osk: the field let go; hiding the board");
114         (*self.server).wm.execute_action(&crate::config::Action::Spawn, Some(HIDE_CMD));
115     }
116 }
117 
118 unsafe extern "C" fn handle_hide_timer(data: *mut std::ffi::c_void) -> std::os::raw::c_int {
119     let osk = &mut *(data as *mut Osk);
120     osk.hide_now();
121     0
122 }
123 
124 #[cfg(test)]
125 mod tests {
126     use super::*;
127 
128     #[test]
129     fn a_touch_is_spent_by_one_activation() {
130         let mut osk = Osk::new(std::ptr::null_mut());
131         osk.note_touch();
132         let now = Instant::now();
133         assert!(osk.spend_touch(now));
134         assert!(!osk.spend_touch(now), "the second activation is the caret moving, not a new tap");
135     }
136 
137     #[test]
138     fn a_stale_touch_does_not_count() {
139         let mut osk = Osk::new(std::ptr::null_mut());
140         osk.note_touch();
141         assert!(!osk.spend_touch(Instant::now() + TOUCH_WINDOW + Duration::from_millis(1)));
142         assert!(osk.armed_at.is_none());
143     }
144 
145     #[test]
146     fn without_a_touch_nothing_shows() {
147         let mut osk = Osk::new(std::ptr::null_mut());
148         assert!(!osk.spend_touch(Instant::now()));
149     }
150 }