From eaf88754f34fda25415cfb798472e0dc804af1a9 Mon Sep 17 00:00:00 2001 From: "Pedro M. de Echanove Pasquin" Date: Sat, 19 Sep 2026 16:14:21 +0200 Subject: [PATCH] =?UTF-8?q?text=5Fedit:=20scale=20the=20single-line=20hori?= =?UTF-8?q?zontal=20padding=20with=20the=20field=20height=20The=20previous?= =?UTF-8?q?=20commit=20put=20the=20password=20eye,=20the=20caret=20and=20t?= =?UTF-8?q?he=20whole=20multiline=20layout=20in=20physical=20pixels,=20but?= =?UTF-8?q?=20the=20single-line=20variant=20still=20inset=20its=20text=20b?= =?UTF-8?q?y=20the=20raw=20`theme::PAD=5FH`:=2020=20physical=20px=20whatev?= =?UTF-8?q?er=20the=20scale.=20On=20a=20scale-2=20panel=20a=20field=2088?= =?UTF-8?q?=20px=20tall,=20drawn=20as=20a=20pill=20of=20radius=2044,=20kep?= =?UTF-8?q?t=20a=2020=20px=20inset,=20so=20the=20first=20glyph=20sat=20ins?= =?UTF-8?q?ide=20the=20rounded=20end=20while=20everything=20else=20in=20th?= =?UTF-8?q?e=20field=20had=20grown.=20The=20padding=20now=20follows=20the?= =?UTF-8?q?=20field=20height,=20like=20the=20eye=20already=20did:=20`metri?= =?UTF-8?q?cs::single=5Fline=5Fpad=5Fh(=20rect=20)`=20is=20`PAD=5FH=20?= =?UTF-8?q?=C3=97=20rect.height=20/=20theme::HEIGHT`,=20built=20on=20`sing?= =?UTF-8?q?le=5Fline=5Fscale`,=20which=20replaces=20the=20private=20`passw?= =?UTF-8?q?ord=5Ftoggle=5Fscale`=20of=20`draw.rs`.=20The=20height=20is=20t?= =?UTF-8?q?he=20right=20reference=20rather=20than=20`canvas.geom=5Fpx(=20P?= =?UTF-8?q?AD=5FH=20)`:=20the=20inset=20exists=20to=20clear=20the=20pill's?= =?UTF-8?q?=20rounded=20end,=20whose=20radius=20is=20half=20the=20height.?= =?UTF-8?q?=20With=20the=20default=20height=20the=20two=20agree,=20since?= =?UTF-8?q?=20that=20height=20is=20itself=20`geom=5Fpx(=20HEIGHT=20)`;=20w?= =?UTF-8?q?hen=20the=20app=20pins=20the=20height=20the=20padding=20follows?= =?UTF-8?q?=20that=20choice=20instead=20of=20the=20fluid=20mode,=20which?= =?UTF-8?q?=20on=20a=20large=20surface=20would=20push=20it=20to=201.5=20ti?= =?UTF-8?q?mes=20the=20design=20value=20next=20to=20a=20field=20the=20app?= =?UTF-8?q?=20deliberately=20kept=20smaller.=20It=20also=20needs=20no=20ca?= =?UTF-8?q?nvas,=20so=20`password=5Ftoggle=5Fhit=5Fzone(=20rect=20)`=20kee?= =?UTF-8?q?ps=20its=20signature.=20`draw`=20resolves=20the=20padding=20onc?= =?UTF-8?q?e=20and=20uses=20it=20for=20the=20narrowed=20text=20rect,=20the?= =?UTF-8?q?=20inner=20clip,=20the=20selection=20band,=20the=20placeholder,?= =?UTF-8?q?=20the=20text=20and=20the=20caret;=20`password=5Ftoggle=5Fhit?= =?UTF-8?q?=5Fzone`=20uses=20it=20for=20the=20half=20padding=20beside=20th?= =?UTF-8?q?e=20eye.=20`single=5Fline=5Fscroll=5Fx`,=20`single=5Fline=5Fali?= =?UTF-8?q?gn=5Foffset`=20and=20the=20single-line=20branch=20of=20`byte=5F?= =?UTF-8?q?offset=5Fat`=20use=20the=20same=20helper,=20so=20a=20click=20st?= =?UTF-8?q?ill=20maps=20to=20the=20glyph=20that=20was=20drawn.=20`hit=5Fte?= =?UTF-8?q?st.rs`=20no=20longer=20references=20`theme`=20and=20drops=20the?= =?UTF-8?q?=20import.=20A=20field=20`HEIGHT`=20tall=20at=20scale=201=20kee?= =?UTF-8?q?ps=20exactly=20the=20previous=20geometry.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/widget/text_edit/draw.rs | 29 ++++++++++++----------------- src/widget/text_edit/hit_test.rs | 9 ++++----- src/widget/text_edit/metrics.rs | 11 +++++++++++ 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/widget/text_edit/draw.rs b/src/widget/text_edit/draw.rs index c31ade8..27e352c 100644 --- a/src/widget/text_edit/draw.rs +++ b/src/widget/text_edit/draw.rs @@ -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 } } -// 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 TextEdit { 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 TextEdit 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 TextEdit } 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 TextEdit ); 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 TextEdit } 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 TextEdit } 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 TextEdit 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() ); diff --git a/src/widget/text_edit/hit_test.rs b/src/widget/text_edit/hit_test.rs index de705ea..e5ea0d8 100644 --- a/src/widget/text_edit/hit_test.rs +++ b/src/widget/text_edit/hit_test.rs @@ -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() ) diff --git a/src/widget/text_edit/metrics.rs b/src/widget/text_edit/metrics.rs index 0b611ee..8a9ff68 100644 --- a/src/widget/text_edit/metrics.rs +++ b/src/widget/text_edit/metrics.rs @@ -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.