An application can keep the keyboard to itself and see Alt, an external field declares what it is for, and the size hint is fitted to the screen before the first commit
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

A terminal takes every key and turns it into bytes for its program, which ltk's key handling got in the way of in three ways. Enter and Space pressed the widget that had the focus, or the one hovered, as well as reaching the application: in prompt a tap on the extra keys row's up arrow left that button focused, so the next Enter ran the command and then pressed the arrow again, bringing the command back on the prompt. The field held by `external_text_input` was always enabled with the normal purpose, so the on-screen keyboard never offered its row of terminal keys to an application that draws its own text area. And `on_raw_key` reports Ctrl and Shift but not Alt, which a terminal sends as a prefix or a parameter.
Keeping the keyboard. `App::claims_keyboard`, false by default, makes `press_key` stop after `on_raw_key` while it is true: the key is not dispatched to the widgets, so neither Enter nor Space presses anything and arrows do not move the keyboard hover, and ltk does not start its own repeat; a repeat already running is stopped as before. `XF86_Back` still goes through the dispatch and `on_back`. An application returns it while its own surface has the keys, and false when one of its menus is up, which then behaves as before.
The external field's purpose. `App::external_text_purpose`, `Purpose::Normal` by default, is what `sync_external_text_input` enables the text-input with, and what it renews it with when the serial changes, so `Purpose::Terminal` reaches the compositor as `ContentPurpose::Terminal` and forge shows the terminal row above the layout.
Alt. `AppData` gains `alt_pressed`, kept by `update_modifiers` with the other two. The runtime now calls `App::on_raw_key_with_alt`, on press and on release, with the same arguments as `on_raw_key` plus Alt; its default forwards to `on_raw_key`, so the applications that implement only that one see no change, and an application that needs Alt overrides the new one instead.
The size hint. A window size hint bigger than the screen used to be fitted only after the first configure, so the first commit asked the compositor for a size it could not give, and a mobile compositor would rescale the window to honour it. Before the first commit `try_run` now reads the logical size of the first output through `output_probe`, on an event queue of its own, and fits the hint within `window_fit::output_share` of it, the same 90 % the later fit uses; that size is what the toplevel is pinned to and what `fitted_size_hint` starts with, and the fit after the first configure is skipped when it was done. The documentation of `window_resizable` now says both the minimum and the maximum are released from the first configure, since a minimum left in place has the same effect on a mobile compositor.
Pointer hooks. The documentation of `on_pointer_move` and of the left-button hook now says they are also called for the primary finger's touch down, motion and up.
Not done. A key held when the window loses the keyboard is stopped by ltk's own repeat but not by an application that repeats keys itself, since there is no hook for the keyboard leaving. Logo and AltGr are still not reported to `on_raw_key_with_alt`.
This commit is contained in:
2026-10-13 20:31:51 +02:00
parent 6e4bf83c91
commit c385605de0
5 changed files with 40 additions and 13 deletions
+19 -5
View File
@@ -840,6 +840,9 @@ pub trait App: 'static
/// focused; a new `serial` renews the request, e.g. on a press on the field.
fn external_text_input( &self ) -> Option<u64> { None }
/// What the field held by [`Self::external_text_input`] is for; `Terminal` gives the on-screen keyboard its row of terminal keys.
fn external_text_purpose( &self ) -> crate::osk::Purpose { crate::osk::Purpose::Normal }
/// Text committed by the input method while [`Self::external_text_input`] holds it.
fn on_external_text_commit( &mut self, _text: &str ) {}
@@ -880,7 +883,7 @@ pub trait App: 'static
fn take_clipboard_request( &mut self ) -> Option<String> { None }
/// Called on every pointer motion (mouse move, button press, button
/// release). `x`, `y` are in physical pixels of the main surface,
/// release) and on every primary-finger touch down and motion. `x`, `y` are in physical pixels of the main surface,
/// the same coordinate space as widget rects. Default: no-op.
///
/// Useful for embedding components that need to know the live
@@ -965,8 +968,8 @@ pub trait App: 'static
/// release position in logical pixels.
fn on_drop_received( &mut self, _x: f32, _y: f32, _mime: &str, _text: &str ) {}
/// Called on every left-button mouse press / release before the
/// regular gesture machine fires. `pressed = true` for press,
/// Called on every left-button mouse press / release, and on every
/// primary-finger touch down / up, before the regular gesture machine fires. `pressed = true` for press,
/// `false` for release. `(x, y)` are in physical pixels (same
/// coordinate space as [`Self::on_pointer_move`]). Default: no-op.
///
@@ -998,6 +1001,15 @@ pub trait App: 'static
/// input for the embedded system, and goes through [`Self::on_back`].
fn on_raw_key( &mut self, _keysym: Keysym, _keycode: u32, _pressed: bool, _ctrl: bool, _shift: bool ) {}
/// [`Self::on_raw_key`] with the Alt state too; the runtime calls this one, which forwards to `on_raw_key` unless overridden. Override it instead when Alt matters, as in a terminal.
fn on_raw_key_with_alt( &mut self, keysym: Keysym, keycode: u32, pressed: bool, ctrl: bool, shift: bool, _alt: bool )
{
self.on_raw_key( keysym, keycode, pressed, ctrl, shift );
}
/// `true` while every key belongs to [`Self::on_raw_key`], as in a terminal: ltk then neither dispatches keys to its widgets, so Enter and Space do not press the focused or hovered one, nor repeats them. `XF86_Back` still goes through [`Self::on_back`].
fn claims_keyboard( &self ) -> bool { false }
/// Called when the surface is (re-)configured with a new size.
///
/// `width` and `height` are in **physical pixels** (the Wayland surface
@@ -1186,8 +1198,10 @@ pub trait App: 'static
/// When [`Self::window_size_hint`] is set, ltk pins
/// `min_size == max_size` before the first commit so the compositor's
/// first configure honours the requested size, and then releases
/// `max_size` from the first configure so the surface becomes
/// user-resizable. Returning `false` skips that release: the pin stays
/// both from the first configure so the surface becomes
/// user-resizable; a minimum left in place would make a mobile
/// compositor shrink the window's scale to honour it. Returning
/// `false` skips that release: the pin stays
/// in place for the lifetime of the toplevel, declaring a fixed-size
/// window that compositors must not resize or restore a remembered
/// geometry over.
+1
View File
@@ -163,6 +163,7 @@ pub struct AppData<A: App>
smithay_client_toolkit::foreign_toplevel_list::ForeignToplevelList,
pub shift_pressed: bool,
pub ctrl_pressed: bool,
pub alt_pressed: bool,
/// Calloop handle for inserting timers / channels. Used by the
/// key-repeat machinery and any future feature that needs to
/// schedule work on the run loop without going through the message
+8 -3
View File
@@ -133,10 +133,14 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
// Skipped when going fullscreen: Mutter rejects the fullscreen
// request if a min_size constrains it.
// Fitted before the first commit, a hint too big for the screen never reaches a compositor that would rescale the window to honour it.
let prefitted_size_hint = app.window_size_hint()
.filter( |_| !app.start_fullscreen() )
.and_then( |hint| super::output_probe::output_logical_size( &conn, &globals ).map( |output| super::window_fit::fit_within( hint, super::window_fit::output_share( output ) ) ) );
let apply_size_hint = |window: &smithay_client_toolkit::shell::xdg::window::Window|
{
if app.start_fullscreen() { return; }
match app.window_size_hint()
match prefitted_size_hint.or( app.window_size_hint() )
{
Some( ( w, h ) ) =>
{
@@ -288,7 +292,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
let pending_fullscreen = app.start_fullscreen();
let pending_size_hint_unpin = !app.start_fullscreen() && app.window_size_hint().is_some() && app.window_resizable();
let pending_size_hint_fit = !app.start_fullscreen() && app.window_size_hint().is_some();
let pending_size_hint_fit = !app.start_fullscreen() && app.window_size_hint().is_some() && prefitted_size_hint.is_none();
let mut data = AppData
{
@@ -334,6 +338,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
foreign_toplevel_list,
shift_pressed: false,
ctrl_pressed: false,
alt_pressed: false,
loop_handle: event_loop.handle(),
compositor_repeat_rate: 0,
compositor_repeat_delay: 0,
@@ -355,7 +360,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
applied_fullscreen: Some( pending_fullscreen ),
pending_size_hint_unpin,
pending_size_hint_fit,
fitted_size_hint: None,
fitted_size_hint: prefitted_size_hint,
main: SurfaceState::<A::Message>::new( surface_kind, titlebar_height, titlebar_title ),
overlays: std::collections::HashMap::new(),
secondaries: Vec::new(),
+7 -2
View File
@@ -71,9 +71,14 @@ impl<A: App> AppData<A>
( Some( _ ), _ ) if self.text_input.is_none() =>
{
let qh = self.qh.clone();
self.activate_text_input( &qh, false, crate::osk::Purpose::Normal );
let purpose = self.app.external_text_purpose();
self.activate_text_input( &qh, false, purpose );
}
( Some( new ), Some( old ) ) if new != old =>
{
self.text_input_purpose = self.app.external_text_purpose();
self.reenable_text_input();
}
( Some( new ), Some( old ) ) if new != old => self.reenable_text_input(),
( None, Some( _ ) ) => self.deactivate_text_input(),
_ => {}
}
+5 -3
View File
@@ -6,7 +6,7 @@
//! Translates `wl_keyboard` events into focus / text-insertion /
//! widget-submit actions. Does not share state with the gesture state
//! machine — keyboard tracks its own focus (`AppData::keyboard_focus`)
//! and modifier flags (`shift_pressed`, `ctrl_pressed`).
//! and modifier flags (`shift_pressed`, `ctrl_pressed`, `alt_pressed`).
//!
//! The only cross-cutting state it reads is `SurfaceState::focused_idx`
//! (set by pointer / touch focus updates) so keyboard navigation can
@@ -81,13 +81,14 @@ impl<A: App> KeyboardHandler for AppData<A>
// request, not input for the embedded system.
if event.keysym != Keysym::XF86_Back
{
self.app.on_raw_key( event.keysym, event.raw_code, true, self.ctrl_pressed, self.shift_pressed );
self.app.on_raw_key_with_alt( event.keysym, event.raw_code, true, self.ctrl_pressed, self.shift_pressed, self.alt_pressed );
}
// A new press always cancels any in-flight repeat — the user
// has either released the previous key or is pressing a
// different one. Either way, the prior timer's keysym should
// not keep firing.
self.stop_key_repeat();
if event.keysym != Keysym::XF86_Back && self.app.claims_keyboard() { return; }
self.dispatch_key( focus, event.clone() );
self.start_key_repeat( focus, event );
}
@@ -103,7 +104,7 @@ impl<A: App> KeyboardHandler for AppData<A>
{
if event.keysym != Keysym::XF86_Back
{
self.app.on_raw_key( event.keysym, event.raw_code, false, self.ctrl_pressed, self.shift_pressed );
self.app.on_raw_key_with_alt( event.keysym, event.raw_code, false, self.ctrl_pressed, self.shift_pressed, self.alt_pressed );
}
// Only stop if the released key matches the one currently
// repeating; releasing a non-repeating key (e.g. a shift that
@@ -131,6 +132,7 @@ impl<A: App> KeyboardHandler for AppData<A>
{
self.shift_pressed = modifiers.shift;
self.ctrl_pressed = modifiers.ctrl;
self.alt_pressed = modifiers.alt;
}
fn update_repeat_info(