// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! The built-in keyboard under a focused text field, for a session whose compositor offers no input method. use std::time::{ Duration, Instant }; use super::app_data::AppData; use super::surface::{ Insets, SurfaceFocus }; use crate::app::App; use crate::input::event::KeyInput; use crate::osk::{ Event, Modifiers, Panel }; use crate::platform::Platform; use crate::Keysym; use crate::tree::find_handlers; use crate::types::{ Point, Rect }; /// Fit a surface's panel to the bottom of its `pw` by `ph` pixels, inside the `insets` the system covers; returns the height it takes. pub( crate ) fn place_osk( osk: &mut Option, pw: u32, ph: u32, sf: f32, insets: Insets ) -> f32 { let Some( panel ) = osk.as_mut() else { return 0.0 }; let width = ( pw as f32 - ( insets.left + insets.right ) * sf ).max( 0.0 ); let bottom = ph as f32 - insets.bottom * sf; let h = panel.height_within( width, sf, ph as f32 ); panel.set_rect( Rect { x: insets.left * sf, y: bottom - h, width, height: h }, sf ); h } impl AppData { fn builtin_keyboard( &self ) -> bool { self.builtin_osk || self.app.builtin_keyboard() } /// The panel for the focused text field, laid out for its purpose. pub( crate ) fn show_osk( &mut self, focus: SurfaceFocus ) { let ss = self.surface( focus ); let purpose = ss.focused_idx .and_then( |i| find_handlers( &ss.frame.widget_rects, i ) ) .map( |h| h.input_purpose() ) .unwrap_or_default(); self.show_osk_for( focus, purpose ); } /// The panel on `focus` for text of `purpose`, also for an app's own text input ([`App::external_text_input`]). pub( crate ) fn show_osk_for( &mut self, focus: SurfaceFocus, purpose: crate::osk::Purpose ) { if !self.builtin_keyboard() { return; } let ss = self.surface_mut( focus ); if ss.osk.is_none() { let lang = std::env::var( "LANG" ).unwrap_or_default(); ss.osk = Some( Panel::for_language( &lang ) ); } if let Some( panel ) = ss.osk.as_mut() { panel.set_purpose( purpose ); } let sf = ss.scale_factor.max( 1 ) as f32; let ( pw, ph ) = ( ss.width * ss.scale_factor.max( 1 ) as u32, ss.height * ss.scale_factor.max( 1 ) as u32 ); place_osk( &mut ss.osk, pw, ph, sf, ss.insets ); ss.request_redraw(); } pub( crate ) fn hide_osk( &mut self, focus: SurfaceFocus ) { let ss = self.surface_mut( focus ); if ss.osk.take().is_some() { ss.request_redraw(); } } /// A press on the text field that already had the focus brings a hidden keyboard back. pub( crate ) fn osk_after_press( &mut self, focus: SurfaceFocus, hit: Option ) { let Some( idx ) = hit else { return }; let ss = self.surface( focus ); let is_text = find_handlers( &ss.frame.widget_rects, idx ).map( |h| h.is_text_input() ).unwrap_or( false ); if is_text && ss.osk.is_none() && self.press_on_focused { self.show_osk( focus ); } } /// `true` when the press landed on the panel and was consumed. pub( crate ) fn osk_press( &mut self, focus: SurfaceFocus, pos: Point ) -> bool { let now = Instant::now(); let Some( event ) = self.surface_mut( focus ).osk.as_mut().and_then( |p| p.press_down( pos, now ) ) else { return false }; self.apply_osk_event( focus, event ); true } /// `true` while a key is held, so the motion is the panel's and not a drag. pub( crate ) fn osk_move( &mut self, focus: SurfaceFocus, pos: Point ) -> bool { let ss = self.surface_mut( focus ); let Some( panel ) = ss.osk.as_mut().filter( |p| p.pressed().is_some() ) else { return false }; if panel.press_move( pos ) { ss.request_redraw(); } true } /// `true` when the release ended a key press. pub( crate ) fn osk_release( &mut self, focus: SurfaceFocus, pos: Point ) -> bool { let now = Instant::now(); let Some( event ) = self.surface_mut( focus ).osk.as_mut().and_then( |p| p.press_up( pos, now ) ) else { return false }; self.apply_osk_event( focus, event ); true } pub( crate ) fn cancel_osk_presses( &mut self ) { for ss in std::iter::once( &mut self.main ).chain( self.overlays.values_mut() ) { if let Some( panel ) = ss.osk.as_mut() { panel.cancel_press(); } } } pub( crate ) fn next_osk_wakeup( &self ) -> Option { let now = Instant::now(); std::iter::once( &self.main ).chain( self.overlays.values() ) .filter_map( |ss| ss.osk.as_ref().and_then( Panel::next_deadline ) ) .map( |deadline| deadline.saturating_duration_since( now ) ) .min() } pub( crate ) fn check_osk_deadlines( &mut self ) { let now = Instant::now(); let mut due: Vec<( SurfaceFocus, Event )> = Vec::new(); if let Some( event ) = self.main.osk.as_mut().and_then( |p| p.tick( now ) ) { due.push( ( SurfaceFocus::Main, event ) ); } for ( id, ss ) in self.overlays.iter_mut() { if let Some( event ) = ss.osk.as_mut().and_then( |p| p.tick( now ) ) { due.push( ( SurfaceFocus::Overlay( *id ), event ) ); } } for ( focus, event ) in due { self.apply_osk_event( focus, event ); } } fn apply_osk_event( &mut self, focus: SurfaceFocus, event: Event ) { if self.external_text_input.is_some() && !self.focused_text_input( focus ) { self.apply_osk_event_to_app( focus, event ); return; } match event { Event::Text( text ) => self.handle_text_insert( focus, &text ), Event::Backspace => self.handle_backspace( focus ), Event::Enter => self.osk_enter( focus ), Event::Key( keysym, mods ) => self.osk_key( focus, keysym, mods ), Event::Hide => self.hide_osk( focus ), Event::Changed => {} } self.surface_mut( focus ).request_redraw(); } /// For an app's own text input every key reaches the app as a physical one would, through [`App::on_raw_key_event`]. fn apply_osk_event_to_app( &mut self, focus: SurfaceFocus, event: Event ) { match event { Event::Text( text ) => { let keysym = text.chars().next().map_or( Keysym::NoSymbol, Keysym::from_char ); self.osk_app_key( keysym, Some( text ), Modifiers::default() ); } Event::Backspace => self.osk_app_key( Keysym::BackSpace, None, Modifiers::default() ), Event::Enter => self.osk_app_key( Keysym::Return, None, Modifiers::default() ), Event::Key( keysym, mods ) => self.osk_app_key( keysym, None, mods ), Event::Hide => self.hide_osk( focus ), Event::Changed => {} } self.surface_mut( focus ).request_redraw(); } /// Press and release `keysym` with the panel's modifiers held for it. Without modifiers a key that has a character types it, as on a physical keyboard. fn osk_app_key( &mut self, keysym: Keysym, text: Option, mods: Modifiers ) { let text = text.or_else( || ( !mods.any() ).then( || keysym.key_char() ).flatten().map( String::from ) ); let held = ( self.ctrl_pressed, self.shift_pressed, self.alt_pressed ); self.modifiers_changed( mods.ctrl, held.1, mods.alt ); let key = KeyInput::synthetic( keysym, text ); self.key_press( key.clone() ); self.key_release( &key ); self.modifiers_changed( held.0, held.1, held.2 ); } /// A key the panel cannot type as text goes through the same dispatch as a physical one. fn osk_key( &mut self, focus: SurfaceFocus, keysym: Keysym, mods: Modifiers ) { let utf8 = ( !mods.any() ).then( || keysym.key_char() ).flatten().map( String::from ); let event = KeyInput::synthetic( keysym, utf8 ); let ctrl = std::mem::replace( &mut self.ctrl_pressed, mods.ctrl ); self.dispatch_key( focus, event ); self.ctrl_pressed = ctrl; } /// Enter as the physical key does it: a newline in a multiline field, otherwise the field's submit. fn osk_enter( &mut self, focus: SurfaceFocus ) { let ss = self.surface( focus ); let Some( idx ) = ss.focused_idx else { return }; let Some( handlers ) = find_handlers( &ss.frame.widget_rects, idx ) else { return }; if handlers.is_multiline_text_input() { self.handle_text_insert( focus, "\n" ); } else if let Some( msg ) = handlers.submit_msg().or_else( || handlers.press_msg() ) { self.pending_msgs.push( msg ); } } }