A text field can take its horizontal padding from its host, the primary finger reaches the pointer hooks, and the screen probe left out of the previous commit is added
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

The previous commit described the size hint fitted to the screen before the first commit and the pointer hooks being called for the primary finger, but left out the files that do it, so `run.rs` called an `output_probe` module that did not exist. They are here, together with a text field option that a host laying out its own padding needs.
Text field padding. A single-line `TextEdit` insets its text by `PAD_H` scaled with the field height, so a tall and narrow field has almost no room left for its text: rustdroid's Android fields, which already subtract the padding the Android view asked for, showed a verification code digit in a 24 px wide gap minus ltk's own inset, scrolled it as if it overflowed and clipped half of it. `TextEdit::padding_h( px )` sets that inset in physical pixels; `0` lets the text use the whole rect. Without it nothing changes. `single_line_pad_h` takes the override and every single-line consumer passes it: the draw path, `single_line_scroll_x`, `single_line_align_offset` and `byte_offset_at`, so a click still lands on the glyph that is drawn. The runtime sees it through a new `pad_h` field in `WidgetHandlers::TextEdit`, read by `text_input_pad` where a click is turned into a text position (pointer down, word selection, drag selection and the context menu's paste offset). The password toggle zone keeps the theme inset.
The screen probe. `output_probe::output_logical_size` reads the logical size of the first output on an event queue of its own, before any surface exists: `xdg_output`'s logical size when the compositor offers it, else the current mode over the scale; the objects it binds are destroyed or released before it returns. `window_fit::output_share` gives the part of an output a window may take, the same 90 % the fit after the first configure uses, which now goes through it too. When the size hint is unpinned on the first configure the minimum size is released along with the maximum, as the documentation of `window_resizable` says.
Touch as pointer. The primary finger's down, motion and up are also reported through `App::on_pointer_move` and `App::on_pointer_button`, as the left mouse button is, so an application that handles the pointer gets the touch without a separate path. The module documentation says so.
This commit is contained in:
2026-10-13 20:36:29 +02:00
parent c385605de0
commit e3b76423fb
14 changed files with 182 additions and 21 deletions
+1
View File
@@ -325,6 +325,7 @@ impl<Msg: Clone> Element<Msg>
multiline: t.is_multiline(),
align: t.align,
font_size: t.font_size,
pad_h: t.pad_h,
select_on_focus: t.select_on_focus,
password_toggle_msg: t.password_toggle.as_ref().map( |( _, m )| m.clone() ),
}
+4 -1
View File
@@ -61,6 +61,8 @@ pub enum WidgetHandlers<Msg: Clone>
/// drew them. `None` for fields that do not call
/// `.font_size( … )` (they follow the widget-scaling mode).
font_size: Option<Length>,
/// Horizontal inner padding override snapshot, for the same hit-testing.
pad_h: Option<f32>,
/// `true` when the source field opted into select-all-on-
/// focus. The runtime reads this in `set_focus` to decide
/// whether the new selection should anchor at `0` (replace
@@ -142,7 +144,7 @@ impl<Msg: Clone> Clone for WidgetHandlers<Msg>
WidgetHandlers::Radio { on_select, selected } => WidgetHandlers::Radio { on_select: on_select.clone(), selected: *selected },
WidgetHandlers::ListItem { on_press } => WidgetHandlers::ListItem { on_press: on_press.clone() },
WidgetHandlers::WindowButton { on_press } => WidgetHandlers::WindowButton { on_press: on_press.clone() },
WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, select_on_focus, password_toggle_msg } =>
WidgetHandlers::TextEdit { value, on_change, on_submit, on_focus, secure, purpose, multiline, align, font_size, pad_h, select_on_focus, password_toggle_msg } =>
{
WidgetHandlers::TextEdit
{
@@ -155,6 +157,7 @@ impl<Msg: Clone> Clone for WidgetHandlers<Msg>
multiline: *multiline,
align: *align,
font_size: *font_size,
pad_h: *pad_h,
select_on_focus: *select_on_focus,
password_toggle_msg: password_toggle_msg.clone(),
}
+5 -5
View File
@@ -61,7 +61,7 @@ fn password_toggle_reserve( rect: Rect ) -> f32
pub fn password_toggle_hit_zone( rect: Rect ) -> Rect
{
let zone_w = ( password_toggle_reserve( rect )
+ single_line_pad_h( rect ) * 0.5 ).min( rect.width );
+ single_line_pad_h( rect, None ) * 0.5 ).min( rect.width );
Rect
{
x: rect.x + rect.width - zone_w,
@@ -123,14 +123,14 @@ impl<Msg: Clone> TextEdit<Msg>
{
0.0
};
let pad_h = single_line_pad_h( rect );
let pad_h = single_line_pad_h( rect, self.pad_h );
let text_rect = Rect
{
width: ( rect.width - toggle_reserve ).max( pad_h * 2.0 ),
..rect
};
let scroll_x = single_line_scroll_x( canvas, text_rect, &self.value, cursor_pos, secure, font_size );
let align_x = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size );
let scroll_x = single_line_scroll_x( canvas, text_rect, &self.value, cursor_pos, secure, font_size, self.pad_h );
let align_x = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size, self.pad_h );
// Clip text / cursor / selection to the inner band so the
// scrolled-off portion does not bleed past the pill stroke.
@@ -184,7 +184,7 @@ impl<Msg: Clone> TextEdit<Msg>
// would, so an empty centred / right-aligned field still
// reads with the placeholder where the digits will land.
let placeholder_align_x = single_line_align_offset(
canvas, rect, &self.placeholder, false, self.align, font_size,
canvas, rect, &self.placeholder, false, self.align, font_size, self.pad_h,
);
canvas.draw_text(
&self.placeholder,
+8 -5
View File
@@ -28,6 +28,7 @@ pub( crate ) fn byte_offset_at(
scroll: TextScroll,
align: crate::widget::text::TextAlign,
font_size: Option<Length>,
pad_h: Option<f32>,
) -> usize
{
if value.is_empty() { return 0; }
@@ -51,11 +52,11 @@ pub( crate ) fn byte_offset_at(
// applies so a click lands on the glyph the user actually
// sees, not on the byte offset that would correspond to an
// unscrolled, left-aligned layout.
let scroll_x = single_line_scroll_x( canvas, rect, value, cursor_pos, secure, font_size );
let align_x = single_line_align_offset( canvas, rect, value, secure, align, font_size );
let scroll_x = single_line_scroll_x( canvas, rect, value, cursor_pos, secure, font_size, pad_h );
let align_x = single_line_align_offset( canvas, rect, value, secure, align, font_size, pad_h );
byte_offset_in_line(
canvas, value, 0, value.len(),
pos.x - rect.x - single_line_pad_h( rect ) - align_x + scroll_x,
pos.x - rect.x - single_line_pad_h( rect, pad_h ) - align_x + scroll_x,
secure,
font_size,
)
@@ -86,9 +87,10 @@ pub( crate ) fn single_line_scroll_x(
cursor: usize,
secure: bool,
font_size: f32,
pad_h: Option<f32>,
) -> f32
{
let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size );
let inner_width = ( rect.width - single_line_pad_h( rect, pad_h ) * 2.0 ).max( font_size );
let safe_cursor = cursor.min( value.len() );
let display_value: String = if secure
@@ -133,11 +135,12 @@ pub( crate ) fn single_line_align_offset(
secure: bool,
align: crate::widget::text::TextAlign,
font_size: f32,
pad_h: Option<f32>,
) -> f32
{
use crate::widget::text::TextAlign;
if matches!( align, TextAlign::Left ) { return 0.0; }
let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size );
let inner_width = ( rect.width - single_line_pad_h( rect, pad_h ) * 2.0 ).max( font_size );
let display_value: String = if secure
{
"\u{2022}".repeat( value.chars().count() )
+2 -2
View File
@@ -12,9 +12,9 @@ pub( crate ) fn single_line_scale( rect: Rect ) -> f32
rect.height / theme::HEIGHT
}
pub( crate ) fn single_line_pad_h( rect: Rect ) -> f32
pub( crate ) fn single_line_pad_h( rect: Rect, pad_h: Option<f32> ) -> f32
{
theme::PAD_H * single_line_scale( rect )
pad_h.unwrap_or_else( || theme::PAD_H * single_line_scale( rect ) )
}
/// Resolved layout of a multiline field, shared by sizing, drawing,
+14 -1
View File
@@ -153,6 +153,8 @@ pub struct TextEdit<Msg: Clone>
/// to fit a fixed number of glyphs (date / time pickers, inline
/// numeric inputs).
pub( crate ) fixed_width: Option<Length>,
/// Horizontal inner padding of the single-line field in physical pixels; `None` scales the theme padding with the field height.
pub( crate ) pad_h: Option<f32>,
/// Font size in pixels for the single-line draw path. `0.0` (the
/// Label font size. `None` follows the process [`crate::WidgetScaling`]
/// mode at the theme default (via [`crate::Canvas::font_px`]); a
@@ -214,6 +216,7 @@ impl<Msg: Clone> TextEdit<Msg>
align: super::text::TextAlign::Left,
borderless: false,
fixed_width: None,
pad_h: None,
font_size: None,
height: None,
select_on_focus: false,
@@ -310,6 +313,16 @@ impl<Msg: Clone> TextEdit<Msg>
self
}
/// Set the horizontal inner padding of a single-line field, in
/// physical pixels, for a host that lays out its own (`0` lets the
/// text use the whole rect). Without it the padding follows the
/// field height.
pub fn padding_h( mut self, px: f32 ) -> Self
{
self.pad_h = Some( px.max( 0.0 ) );
self
}
/// Override the preferred width reported to the parent layout.
/// Accepts logical `f32` pixels or any [`Length`] — pair a
/// [`Length::fluid`] width with a fluid font size so box and glyphs
@@ -508,7 +521,7 @@ impl<Msg: Clone> TextEdit<Msg>
) -> usize
{
byte_offset_at(
canvas, rect, pos, &self.value, self.is_multiline(), self.effective_secure(), cursor_pos, scroll, self.align, self.font_size,
canvas, rect, pos, &self.value, self.is_multiline(), self.effective_secure(), cursor_pos, scroll, self.align, self.font_size, self.pad_h,
)
}