text_edit: scale the single-line horizontal padding with the field height
CI / build + test (push) Canceled after 0s
CI / cargo audit (push) Canceled after 0s

The previous commit put the password eye, the caret and the whole multiline layout in physical pixels, but the single-line variant still inset its text by the raw `theme::PAD_H`: 20 physical px whatever the scale. On a scale-2 panel a field 88 px tall, drawn as a pill of radius 44, kept a 20 px inset, so the first glyph sat inside the rounded end while everything else in the field had grown.
The padding now follows the field height, like the eye already did: `metrics::single_line_pad_h( rect )` is `PAD_H × rect.height / theme::HEIGHT`, built on `single_line_scale`, which replaces the private `password_toggle_scale` of `draw.rs`. The height is the right reference rather than `canvas.geom_px( PAD_H )`: the inset exists to clear the pill's rounded end, whose radius is half the height. With the default height the two agree, since that height is itself `geom_px( HEIGHT )`; when the app pins the height the padding follows that choice instead of the fluid mode, which on a large surface would push it to 1.5 times the design value next to a field the app deliberately kept smaller. It also needs no canvas, so `password_toggle_hit_zone( rect )` keeps its signature.
`draw` resolves the padding once and uses it for the narrowed text rect, the inner clip, the selection band, the placeholder, the text and the caret; `password_toggle_hit_zone` uses it for the half padding beside the eye. `single_line_scroll_x`, `single_line_align_offset` and the single-line branch of `byte_offset_at` use the same helper, so a click still maps to the glyph that was drawn. `hit_test.rs` no longer references `theme` and drops the import. A field `HEIGHT` tall at scale 1 keeps exactly the previous geometry.
This commit is contained in:
2026-09-19 16:14:21 +02:00
parent 495968fca5
commit eaf88754f3
3 changed files with 27 additions and 22 deletions
+12 -17
View File
@@ -6,7 +6,7 @@ use crate::types::Rect;
use super::TextEdit;
use super::hit_test::{ single_line_align_offset, single_line_scroll_x };
use super::metrics::MultilineMetrics;
use super::metrics::{ single_line_pad_h, single_line_scale, MultilineMetrics };
use super::theme;
use super::wrapping::compute_visual_lines;
@@ -47,15 +47,9 @@ fn rect_intersect( a: Rect, b: Rect ) -> Option<Rect>
}
}
// The toggle follows the field height, which is already in physical pixels.
fn password_toggle_scale( rect: Rect ) -> f32
{
rect.height / theme::HEIGHT
}
fn password_toggle_reserve( rect: Rect ) -> f32
{
( theme::PASSWORD_TOGGLE_SIZE + theme::PASSWORD_TOGGLE_SLOP * 2.0 ) * password_toggle_scale( rect )
( theme::PASSWORD_TOGGLE_SIZE + theme::PASSWORD_TOGGLE_SLOP * 2.0 ) * single_line_scale( rect )
}
/// Hit rect for the eye icon at the right edge of a `TextEdit`
@@ -67,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 )
+ theme::PAD_H * 0.5 * password_toggle_scale( rect ) ).min( rect.width );
+ single_line_pad_h( rect ) * 0.5 ).min( rect.width );
Rect
{
x: rect.x + rect.width - zone_w,
@@ -127,9 +121,10 @@ impl<Msg: Clone> TextEdit<Msg>
{
0.0
};
let pad_h = single_line_pad_h( rect );
let text_rect = Rect
{
width: ( rect.width - toggle_reserve ).max( theme::PAD_H * 2.0 ),
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 );
@@ -143,9 +138,9 @@ impl<Msg: Clone> TextEdit<Msg>
let outer_clip = canvas.clip_bounds();
let inner_rect = Rect
{
x: rect.x + theme::PAD_H * 0.5,
x: rect.x + pad_h * 0.5,
y: rect.y,
width: ( rect.width - theme::PAD_H - toggle_reserve ).max( 0.0 ),
width: ( rect.width - pad_h - toggle_reserve ).max( 0.0 ),
height: rect.height,
};
canvas.set_clip_rects( &interior_clip( &outer_clip, inner_rect ) );
@@ -170,7 +165,7 @@ impl<Msg: Clone> TextEdit<Msg>
} else {
self.value[s..e].to_string()
};
let x0 = rect.x + theme::PAD_H + align_x - scroll_x
let x0 = rect.x + pad_h + align_x - scroll_x
+ canvas.measure_text( &prefix_text, font_size );
let w = canvas.measure_text( &span_text, font_size );
let sel_rect = Rect
@@ -191,7 +186,7 @@ impl<Msg: Clone> TextEdit<Msg>
);
canvas.draw_text(
&self.placeholder,
rect.x + theme::PAD_H + placeholder_align_x,
rect.x + pad_h + placeholder_align_x,
text_y,
font_size,
theme::placeholder(),
@@ -199,7 +194,7 @@ impl<Msg: Clone> TextEdit<Msg>
} else {
canvas.draw_text(
&text,
rect.x + theme::PAD_H + align_x - scroll_x,
rect.x + pad_h + align_x - scroll_x,
text_y,
font_size,
theme::text(),
@@ -215,7 +210,7 @@ impl<Msg: Clone> TextEdit<Msg>
} else {
self.value[..safe_cursor].to_string()
};
let cursor_x = rect.x + theme::PAD_H + align_x - scroll_x
let cursor_x = rect.x + pad_h + align_x - scroll_x
+ canvas.measure_text( &cursor_text, font_size );
// Caret height follows the text line, not the field box — a
// tall field (launcher pill) otherwise grows a caret twice
@@ -249,7 +244,7 @@ impl<Msg: Clone> TextEdit<Msg>
if let Some( ( visible, _ ) ) = self.password_toggle.as_ref()
{
let icon_name = if *visible { "actions/invisible" } else { "actions/visible" };
let icon_px = ( theme::PASSWORD_TOGGLE_SIZE * password_toggle_scale( rect ) ).round().max( 1.0 ) as u32;
let icon_px = ( theme::PASSWORD_TOGGLE_SIZE * single_line_scale( rect ) ).round().max( 1.0 ) as u32;
if let Some( ( rgba, iw, ih ) ) = crate::theme::icon_rgba( icon_name, icon_px )
{
let tinted = crate::theme::tint_symbolic( &rgba, theme::placeholder() );
+4 -5
View File
@@ -4,8 +4,7 @@
use crate::render::Canvas;
use crate::types::{ Length, Point, Rect };
use super::metrics::MultilineMetrics;
use super::theme;
use super::metrics::{ single_line_pad_h, MultilineMetrics };
use super::wrapping::compute_visual_lines;
/// Convert a pointer position inside the widget rect to the byte offset
@@ -65,7 +64,7 @@ pub( crate ) fn byte_offset_at(
let align_x = single_line_align_offset( canvas, rect, value, secure, align, font_size );
byte_offset_in_line(
canvas, value, 0, value.len(),
pos.x - rect.x - theme::PAD_H - align_x + scroll_x,
pos.x - rect.x - single_line_pad_h( rect ) - align_x + scroll_x,
secure,
font_size,
)
@@ -98,7 +97,7 @@ pub( crate ) fn single_line_scroll_x(
font_size: f32,
) -> f32
{
let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( font_size );
let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size );
let safe_cursor = cursor.min( value.len() );
let display_value: String = if secure
@@ -147,7 +146,7 @@ pub( crate ) fn single_line_align_offset(
{
use crate::widget::text::TextAlign;
if matches!( align, TextAlign::Left ) { return 0.0; }
let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( font_size );
let inner_width = ( rect.width - single_line_pad_h( rect ) * 2.0 ).max( font_size );
let display_value: String = if secure
{
"\u{2022}".repeat( value.chars().count() )
+11
View File
@@ -6,6 +6,17 @@ use crate::types::{ Length, Rect };
use super::theme;
/// Single-line geometry follows the field height, which is already physical.
pub( crate ) fn single_line_scale( rect: Rect ) -> f32
{
rect.height / theme::HEIGHT
}
pub( crate ) fn single_line_pad_h( rect: Rect ) -> f32
{
theme::PAD_H * single_line_scale( rect )
}
/// Resolved layout of a multiline field, shared by sizing, drawing,
/// hit-testing and caret navigation so they all agree. `font_size` is in
/// font space (pre-`dpi_scale`); every other field is physical px.