Files
ltk/src/event_loop/osk.rs
T
admin a0ceef51ef
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s
WallpaperBundle::blurred; the on-screen keyboard at the right size on a scaled output, keys as tall as they are wide, a terminal row that fits and a Spanish symbols layer that does not squeeze the rest
The keyboard of the previous commit was drawn and tried on a desktop at scale 1. On the Librem 5, at scale 2 and hosted by forge, it showed what that could not: letters twice their size, a terminal row whose modifier labels did not fit, keys shorter than they were wide, and a Spanish numbers layer with a gap at each side. This fixes those, and adds the wallpaper helper the shell and the compositor now use for their frosted panels.
A frosted wallpaper. Crustace decoded the theme's lockscreen image at full size a second time for the launcher and the quick settings, and forge decoded it again for the app switcher, blurred it with a floating-point Gaussian at 1920 px and composed a copy per panel size; the switcher's decode alone left some 60 MB in a thread arena that glibc never gives back. A backdrop that is blurred needs no resolution. `WallpaperBundle::blurred( divisor, sigma )` returns a bundle `divisor` times smaller on each side, reduced with a triangle filter and blurred with `imageops::fast_blur`, the box approximation that works in place on the 8-bit pixels; the renderer scales it back up with linear filtering, and the blur hides what the reduction lost. It is an ordinary bundle, so `for_size` crops it for portrait as it does the original. Two tests: the size is divided and never reaches zero, and a flat colour stays that colour. `docs/architecture.md` mentions it next to `for_size`.
Text twice its size. `Panel::draw` passed the canvas a font size already multiplied by the output scale, and the canvas multiplies every font size by its `dpi_scale` once more, as it must for widget text, which is sized in font space. At scale 1 the two agree; at 2 the labels came out at four times the area. The keys and the variants popup now pass `TEXT_SIZE` and `ACTION_SIZE` unscaled, and `draw_centred` computes the baseline offset from the size in physical pixels, `size * dpi_scale()`, so the label stays centred on the key. Radii, key geometry and icons were already right: they are physical pixels and never go through `dpi_scale`. The runtime's built-in panel and forge's both draw through `Panel::draw`, so both had it.
Keys as tall as they are wide. Rows were a fixed 46 px high whatever the width, so on a phone the keys came out wide and flat. The new `key_unit( width, scale )` is the width one key unit takes when the widest row of the layer fills the panel, the width of a `q`; `set_rect` uses it as both the width unit and the height of every row, the terminal row included, and the variants popup, which was already as tall as its key, follows. The panel's height therefore depends on its width: `height( scale )` becomes `height( width, scale )`, `place_osk` passes the surface width, and `KEY_HEIGHT` goes. The architecture document says so. In landscape the keys grow with the screen's width; a cap on the height has not been needed yet.
The terminal row. Esc, Tab, Ctrl and Alt were one unit wide and their labels did not fit. They are 1.5 units now, and to keep the row at 13 units `\` and `~` leave it: the row is Esc, Tab, Ctrl, Alt, the four arrows, `-`, `/` and `|`. Neither is lost: `\` is a long press on `/`, next to `?`, and `~` one on `-`, next to `_` and `–`; `|` takes over the variants `\` had, `` ` ``, `^`, `$`, `#` and `*`, beside its own `&` and `!`. The arrows are drawn at 14 instead of the 22 of the other action icons, which filled the narrow key. The built-in bar's test checks `|` in the row and `\` and `~` among the variants.
The Spanish symbols layer. Its third row carried `¡`, `!`, `¿` and `?`, 12 units against the 10 of every other row; since all rows share the key unit of the widest, the numbers and `@ # € %` rows came out narrower and centred, with a gap at each side, and switching to English and back looked right only because that layout's row has 10. `¡` and `¿` leave the row and become long presses on `!` and `?`, as on Android, and stay among the variants of `.` where they already were. The new test `shipped_layers_are_no_wider_than_their_base` reads `en.json` and `es.json` and fails when any row of any layer is wider than the widest row of the base layer, so a layout file cannot bring this back unnoticed.
2026-09-27 21:43:33 +02:00

163 lines
5.9 KiB
Rust

// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! The built-in keyboard under a focused text field, for a session whose compositor offers no input method.
use std::time::{ Duration, Instant };
use smithay_client_toolkit::seat::keyboard::KeyEvent;
use super::app_data::AppData;
use super::surface::SurfaceFocus;
use crate::app::App;
use crate::osk::{ Event, Modifiers, Panel };
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; returns the height it takes.
pub( crate ) fn place_osk( osk: &mut Option<Panel>, pw: u32, ph: u32, sf: f32 ) -> f32
{
let Some( panel ) = osk.as_mut() else { return 0.0 };
let h = panel.height( pw as f32, sf ).min( ph as f32 );
panel.set_rect( Rect { x: 0.0, y: ph as f32 - h, width: pw as f32, height: h }, sf );
h
}
impl<A: App> AppData<A>
{
fn builtin_keyboard( &self ) -> bool
{
self.builtin_osk || self.app.builtin_keyboard()
}
pub( crate ) fn show_osk( &mut self, focus: SurfaceFocus )
{
if !self.builtin_keyboard() { return; }
let ss = self.surface_mut( focus );
let purpose = ss.focused_idx
.and_then( |i| find_handlers( &ss.frame.widget_rects, i ) )
.map( |h| h.input_purpose() )
.unwrap_or_default();
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.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<usize> )
{
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<Duration>
{
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 )
{
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();
}
/// 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 = KeyEvent { time: 0, raw_code: 0, 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 );
}
}
}