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 }