diff --git a/src/event_loop/text_editing/cursor.rs b/src/event_loop/text_editing/cursor.rs index 8193ba1..a87bfa4 100644 --- a/src/event_loop/text_editing/cursor.rs +++ b/src/event_loop/text_editing/cursor.rs @@ -98,14 +98,14 @@ impl AppData let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false }; let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied() .unwrap_or( value.len() ).min( value.len() ); - let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx ) + let ( rect, _, multiline, secure, _align, font_size ) = match self.text_input_geometry( focus, idx ) { Some( g ) => g, None => return false, }; if !multiline || secure { return false; } let canvas = match self.surface( focus ).canvas.as_ref() { Some( c ) => c, None => return false }; - let new_cursor = match crate::widget::text_edit::cursor_visual_up( canvas, rect, &value, cursor ) + let new_cursor = match crate::widget::text_edit::cursor_visual_up( canvas, rect, &value, cursor, font_size ) { Some( n ) => n, None => return false, @@ -124,14 +124,14 @@ impl AppData let ( idx, value ) = match self.focused_text_value( focus ) { Some( v ) => v, None => return false }; let cursor = self.surface( focus ).frame.cursor_state.get( &idx ).copied() .unwrap_or( value.len() ).min( value.len() ); - let ( rect, _, multiline, secure, _align, _font_size ) = match self.text_input_geometry( focus, idx ) + let ( rect, _, multiline, secure, _align, font_size ) = match self.text_input_geometry( focus, idx ) { Some( g ) => g, None => return false, }; if !multiline || secure { return false; } let canvas = match self.surface( focus ).canvas.as_ref() { Some( c ) => c, None => return false }; - let new_cursor = match crate::widget::text_edit::cursor_visual_down( canvas, rect, &value, cursor ) + let new_cursor = match crate::widget::text_edit::cursor_visual_down( canvas, rect, &value, cursor, font_size ) { Some( n ) => n, None => return false, @@ -155,9 +155,9 @@ impl AppData .unwrap_or( value.len() ).min( value.len() ); let new_cursor = match self.text_input_geometry( focus, idx ) { - Some( ( rect, _, true, false, _, _ ) ) => match self.surface( focus ).canvas.as_ref() + Some( ( rect, _, true, false, _, font_size ) ) => match self.surface( focus ).canvas.as_ref() { - Some( canvas ) => crate::widget::text_edit::cursor_visual_home( canvas, rect, &value, cursor ), + Some( canvas ) => crate::widget::text_edit::cursor_visual_home( canvas, rect, &value, cursor, font_size ), None => value[..cursor].rfind( '\n' ).map( |p| p + 1 ).unwrap_or( 0 ), }, _ => value[..cursor].rfind( '\n' ).map( |p| p + 1 ).unwrap_or( 0 ), @@ -178,9 +178,9 @@ impl AppData .unwrap_or( value.len() ).min( value.len() ); let new_cursor = match self.text_input_geometry( focus, idx ) { - Some( ( rect, _, true, false, _, _ ) ) => match self.surface( focus ).canvas.as_ref() + Some( ( rect, _, true, false, _, font_size ) ) => match self.surface( focus ).canvas.as_ref() { - Some( canvas ) => crate::widget::text_edit::cursor_visual_end( canvas, rect, &value, cursor ), + Some( canvas ) => crate::widget::text_edit::cursor_visual_end( canvas, rect, &value, cursor, font_size ), None => value[cursor..].find( '\n' ).map( |p| cursor + p ).unwrap_or( value.len() ), }, _ => value[cursor..].find( '\n' ).map( |p| cursor + p ).unwrap_or( value.len() ), diff --git a/src/widget/text_edit/cursor.rs b/src/widget/text_edit/cursor.rs index 0aa4603..99d1c28 100644 --- a/src/widget/text_edit/cursor.rs +++ b/src/widget/text_edit/cursor.rs @@ -2,10 +2,10 @@ // Copyright (C) 2026 Liberux Labs, S. L. use crate::render::Canvas; -use crate::types::Rect; +use crate::types::{ Length, Rect }; use super::hit_test::byte_offset_in_line; -use super::theme; +use super::metrics::MultilineMetrics; use super::wrapping::{ compute_visual_lines, VisualLine }; /// Locate the visual line containing `cursor` in the wrap layout for @@ -17,13 +17,13 @@ use super::wrapping::{ compute_visual_lines, VisualLine }; /// edge of the new one. fn current_visual_line( canvas: &Canvas, + m: &MultilineMetrics, rect: Rect, value: &str, cursor: usize, ) -> Option<( Vec, usize )> { - let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( theme::FONT_SIZE ); - let visual_lines = compute_visual_lines( canvas, value, inner_width, theme::FONT_SIZE ); + let visual_lines = compute_visual_lines( canvas, value, m.inner_width( rect ), m.font_size ); if visual_lines.is_empty() { return None; } let safe_cursor = cursor.min( value.len() ); let idx = visual_lines.iter() @@ -43,49 +43,55 @@ fn current_visual_line( /// short → long sequence will drift toward the end of every short /// line — same simplification GTK / Cocoa make for plain controls. pub( crate ) fn cursor_visual_up( - canvas: &Canvas, - rect: Rect, - value: &str, - cursor: usize, + canvas: &Canvas, + rect: Rect, + value: &str, + cursor: usize, + font_size: Option, ) -> Option { - let ( lines, idx ) = current_visual_line( canvas, rect, value, cursor )?; + let m = MultilineMetrics::resolve( canvas, font_size ); + let ( lines, idx ) = current_visual_line( canvas, &m, rect, value, cursor )?; if idx == 0 { return None; } let cur = lines[ idx ]; let safe = cursor.min( value.len() ); - let target_x = canvas.measure_text( &value[cur.start..safe], theme::FONT_SIZE ); + let target_x = canvas.measure_text( &value[cur.start..safe], m.font_size ); let prev = lines[ idx - 1 ]; - Some( byte_offset_in_line( canvas, value, prev.start, prev.end, target_x, false, theme::FONT_SIZE ) ) + Some( byte_offset_in_line( canvas, value, prev.start, prev.end, target_x, false, m.font_size ) ) } /// Mirror of [`cursor_visual_up`] for the Down arrow. pub( crate ) fn cursor_visual_down( - canvas: &Canvas, - rect: Rect, - value: &str, - cursor: usize, + canvas: &Canvas, + rect: Rect, + value: &str, + cursor: usize, + font_size: Option, ) -> Option { - let ( lines, idx ) = current_visual_line( canvas, rect, value, cursor )?; + let m = MultilineMetrics::resolve( canvas, font_size ); + let ( lines, idx ) = current_visual_line( canvas, &m, rect, value, cursor )?; if idx + 1 >= lines.len() { return None; } let cur = lines[ idx ]; let safe = cursor.min( value.len() ); - let target_x = canvas.measure_text( &value[cur.start..safe], theme::FONT_SIZE ); + let target_x = canvas.measure_text( &value[cur.start..safe], m.font_size ); let next = lines[ idx + 1 ]; - Some( byte_offset_in_line( canvas, value, next.start, next.end, target_x, false, theme::FONT_SIZE ) ) + Some( byte_offset_in_line( canvas, value, next.start, next.end, target_x, false, m.font_size ) ) } /// Byte offset of the start of the cursor's current visual line. /// Falls back to `0` when the wrap layout cannot be computed (e.g. /// degenerate rect), so the Home key always has a sensible target. pub( crate ) fn cursor_visual_home( - canvas: &Canvas, - rect: Rect, - value: &str, - cursor: usize, + canvas: &Canvas, + rect: Rect, + value: &str, + cursor: usize, + font_size: Option, ) -> usize { - current_visual_line( canvas, rect, value, cursor ) + let m = MultilineMetrics::resolve( canvas, font_size ); + current_visual_line( canvas, &m, rect, value, cursor ) .map( |( lines, idx )| lines[ idx ].start ) .unwrap_or( 0 ) } @@ -96,13 +102,15 @@ pub( crate ) fn cursor_visual_home( /// start of the next visual row, but the caret renders at the trailing /// edge of *this* row by convention). pub( crate ) fn cursor_visual_end( - canvas: &Canvas, - rect: Rect, - value: &str, - cursor: usize, + canvas: &Canvas, + rect: Rect, + value: &str, + cursor: usize, + font_size: Option, ) -> usize { - current_visual_line( canvas, rect, value, cursor ) + let m = MultilineMetrics::resolve( canvas, font_size ); + current_visual_line( canvas, &m, rect, value, cursor ) .map( |( lines, idx )| lines[ idx ].end ) .unwrap_or( value.len() ) } diff --git a/src/widget/text_edit/draw.rs b/src/widget/text_edit/draw.rs index 655fc48..c31ade8 100644 --- a/src/widget/text_edit/draw.rs +++ b/src/widget/text_edit/draw.rs @@ -6,6 +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::theme; use super::wrapping::compute_visual_lines; @@ -46,6 +47,17 @@ 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 ) +} + /// Hit rect for the eye icon at the right edge of a `TextEdit` /// configured with [`TextEdit::password_toggle`]. Returned in the /// same coordinate space as the field's `rect`. Pointer / touch @@ -54,9 +66,8 @@ fn rect_intersect( a: Rect, b: Rect ) -> Option /// moving the caret. pub fn password_toggle_hit_zone( rect: Rect ) -> Rect { - let zone_w = ( theme::PASSWORD_TOGGLE_SIZE - + theme::PASSWORD_TOGGLE_SLOP * 2.0 - + theme::PAD_H * 0.5 ).min( rect.width ); + let zone_w = ( password_toggle_reserve( rect ) + + theme::PAD_H * 0.5 * password_toggle_scale( rect ) ).min( rect.width ); Rect { x: rect.x + rect.width - zone_w, @@ -110,7 +121,7 @@ impl TextEdit // never slide under the icon. let toggle_reserve = if self.password_toggle.is_some() { - theme::PASSWORD_TOGGLE_SIZE + theme::PASSWORD_TOGGLE_SLOP * 2.0 + password_toggle_reserve( rect ) } else { @@ -209,12 +220,13 @@ impl TextEdit // Caret height follows the text line, not the field box — a // tall field (launcher pill) otherwise grows a caret twice // the glyph height. Matches the multiline caret. - let cursor_h = font_size + 4.0; + let dpi = canvas.dpi_scale().max( 1.0 ); + let cursor_h = ( font_size + theme::CARET_OVERHANG ) * dpi; let cursor_rect = Rect { x: cursor_x, y: rect.y + ( rect.height - cursor_h ) / 2.0, - width: 2.0, + width: ( theme::CARET_W * dpi ).round(), height: cursor_h, }; canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 ); @@ -237,7 +249,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.round() as u32; + let icon_px = ( theme::PASSWORD_TOGGLE_SIZE * password_toggle_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() ); @@ -268,16 +280,14 @@ impl TextEdit canvas.fill_rect( rect, theme::bg(), theme::RADIUS_MULTI ); canvas.stroke_rect( rect, border_c, border_w, theme::RADIUS_MULTI ); - let line_h = theme::FONT_SIZE * theme::LINE_H_MULT; - let inner_h = ( rect.height - theme::PAD_V_MULTI * 2.0 ).max( line_h ); - let visible_lines = ( inner_h / line_h ).floor().max( 1.0 ) as usize; - let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( theme::FONT_SIZE ); + let m = MultilineMetrics::resolve( canvas, self.font_size ); + let line_h = m.line_h; // Compute the visual-line layout once per draw. Iterators // later go through this list; long logical lines are // soft-wrapped at the last whitespace before the row would // overflow without mutating the buffer. - let visual_lines = compute_visual_lines( canvas, &self.value, inner_width, theme::FONT_SIZE ); + let visual_lines = compute_visual_lines( canvas, &self.value, m.inner_width( rect ), m.font_size ); // Vertical auto-scroll: keep the cursor's visual line on // screen. The cursor sits in the *first* visual line whose @@ -289,14 +299,9 @@ impl TextEdit let cursor_visual_idx = visual_lines.iter() .position( |vl| safe_cursor >= vl.start && safe_cursor <= vl.end ) .unwrap_or( visual_lines.len().saturating_sub( 1 ) ); - let total_lines = visual_lines.len(); - let max_first = total_lines.saturating_sub( visible_lines ); - let first_line = if cursor_visual_idx + 1 > visible_lines - { - ( cursor_visual_idx + 1 - visible_lines ).min( max_first ) - } else { 0 }; + let first_line = m.first_visible_line( rect, visual_lines.len(), cursor_visual_idx ); - let baseline_0 = rect.y + theme::PAD_V_MULTI + theme::FONT_SIZE; + let baseline_0 = rect.y + m.pad_v + m.glyph_h; // Clip text to the inner rect so partial lines at the bottom // stay inside the box and do not bleed onto sibling widgets @@ -306,10 +311,10 @@ impl TextEdit let outer_clip = canvas.clip_bounds(); let inner_rect = Rect { - x: rect.x + theme::PAD_H * 0.5, - y: rect.y + theme::PAD_V_MULTI * 0.5, - width: ( rect.width - theme::PAD_H ).max( 0.0 ), - height: ( rect.height - theme::PAD_V_MULTI ).max( 0.0 ), + x: rect.x + m.pad_h * 0.5, + y: rect.y + m.pad_v * 0.5, + width: ( rect.width - m.pad_h ).max( 0.0 ), + height: ( rect.height - m.pad_v ).max( 0.0 ), }; canvas.set_clip_rects( &interior_clip( &outer_clip, inner_rect ) ); @@ -324,8 +329,8 @@ impl TextEdit for ( i, vl ) in visual_lines.iter().enumerate().skip( first_line ) { let visual_idx = i - first_line; - let y = rect.y + theme::PAD_V_MULTI + visual_idx as f32 * line_h; - if y > rect.y + rect.height - theme::PAD_V_MULTI { break; } + let y = rect.y + m.pad_v + visual_idx as f32 * line_h; + if y > rect.y + rect.height - m.pad_v { break; } // Intersection of [s, e] with this visual line's // byte range. Empty / non-overlapping → skip. let span_start = s.max( vl.start ); @@ -333,8 +338,8 @@ impl TextEdit if span_start >= span_end { continue; } let prefix_text = &self.value[ vl.start..span_start ]; let span_text = &self.value[ span_start..span_end ]; - let x0 = rect.x + theme::PAD_H + canvas.measure_text( prefix_text, theme::FONT_SIZE ); - let w = canvas.measure_text( span_text, theme::FONT_SIZE ).max( 4.0 ); + let x0 = rect.x + m.pad_h + canvas.measure_text( prefix_text, m.font_size ); + let w = canvas.measure_text( span_text, m.font_size ).max( m.caret_w * 2.0 ); let sel_rect = Rect { x: x0, y, @@ -348,9 +353,9 @@ impl TextEdit { canvas.draw_text( &self.placeholder, - rect.x + theme::PAD_H, + rect.x + m.pad_h, baseline_0, - theme::FONT_SIZE, + m.font_size, theme::placeholder(), ); } else { @@ -358,16 +363,16 @@ impl TextEdit { let visual_idx = i - first_line; let y = baseline_0 + visual_idx as f32 * line_h; - if y > rect.y + rect.height - theme::PAD_V_MULTI + line_h + if y > rect.y + rect.height - m.pad_v + line_h { break; } let line = &self.value[ vl.start..vl.end ]; canvas.draw_text( line, - rect.x + theme::PAD_H, + rect.x + m.pad_h, y, - theme::FONT_SIZE, + m.font_size, theme::text(), ); } @@ -377,16 +382,16 @@ impl TextEdit { let vl = visual_lines[ cursor_visual_idx ]; let line_prefix = &self.value[ vl.start..safe_cursor ]; - let cursor_x = rect.x + theme::PAD_H - + canvas.measure_text( line_prefix, theme::FONT_SIZE ); + let cursor_x = rect.x + m.pad_h + + canvas.measure_text( line_prefix, m.font_size ); let visual_y_idx = cursor_visual_idx.saturating_sub( first_line ); - let cursor_y = rect.y + theme::PAD_V_MULTI + visual_y_idx as f32 * line_h; + let cursor_y = rect.y + m.pad_v + visual_y_idx as f32 * line_h; let cursor_rect = Rect { x: cursor_x, - y: cursor_y + 2.0, - width: 2.0, - height: theme::FONT_SIZE + 4.0, + y: cursor_y + ( line_h - m.caret_h ) / 2.0, + width: m.caret_w, + height: m.caret_h, }; canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 ); } diff --git a/src/widget/text_edit/hit_test.rs b/src/widget/text_edit/hit_test.rs index 8edfaf3..de705ea 100644 --- a/src/widget/text_edit/hit_test.rs +++ b/src/widget/text_edit/hit_test.rs @@ -4,6 +4,7 @@ use crate::render::Canvas; use crate::types::{ Length, Point, Rect }; +use super::metrics::MultilineMetrics; use super::theme; use super::wrapping::compute_visual_lines; @@ -33,32 +34,25 @@ pub( crate ) fn byte_offset_at( if multiline && !secure { - let line_h = theme::FONT_SIZE * theme::LINE_H_MULT; - let inner_h = ( rect.height - theme::PAD_V_MULTI * 2.0 ).max( line_h ); - let visible_lines = ( inner_h / line_h ).floor().max( 1.0 ) as usize; - let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( theme::FONT_SIZE ); + let m = MultilineMetrics::resolve( canvas, font_size ); // Recompute the visual layout from current state. Same call // as `draw_multiline` makes, so the click maps to the line / // column the user actually sees. - let visual_lines = compute_visual_lines( canvas, value, inner_width, theme::FONT_SIZE ); + let visual_lines = compute_visual_lines( canvas, value, m.inner_width( rect ), m.font_size ); let safe_cursor = cursor_pos.min( value.len() ); let cursor_visual_idx = visual_lines.iter() .position( |vl| safe_cursor >= vl.start && safe_cursor <= vl.end ) .unwrap_or( visual_lines.len().saturating_sub( 1 ) ); let total_lines = visual_lines.len(); - let max_first = total_lines.saturating_sub( visible_lines ); - let first_line = if cursor_visual_idx + 1 > visible_lines - { - ( cursor_visual_idx + 1 - visible_lines ).min( max_first ) - } else { 0 }; + let first_line = m.first_visible_line( rect, total_lines, cursor_visual_idx ); - let visual_idx = ( ( pos.y - rect.y - theme::PAD_V_MULTI ) / line_h ) + let visual_idx = ( ( pos.y - rect.y - m.pad_v ) / m.line_h ) .max( 0.0 ) as usize; let target_idx = ( first_line + visual_idx ).min( total_lines.saturating_sub( 1 ) ); let vl = visual_lines[ target_idx ]; - byte_offset_in_line( canvas, value, vl.start, vl.end, pos.x - rect.x - theme::PAD_H, false, theme::FONT_SIZE ) + byte_offset_in_line( canvas, value, vl.start, vl.end, pos.x - rect.x - m.pad_h, false, m.font_size ) } else { // Resolve the font-size sentinel (0.0 / negative → widget-scaling mode) // so a click maps against the same size the renderer drew with. diff --git a/src/widget/text_edit/metrics.rs b/src/widget/text_edit/metrics.rs new file mode 100644 index 0000000..0b611ee --- /dev/null +++ b/src/widget/text_edit/metrics.rs @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: LGPL-2.1-only +// Copyright (C) 2026 Liberux Labs, S. L. + +use crate::render::Canvas; +use crate::types::{ Length, Rect }; + +use super::theme; + +/// 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. +#[ derive( Clone, Copy, Debug ) ] +pub( crate ) struct MultilineMetrics +{ + pub font_size: f32, + pub glyph_h: f32, + pub line_h: f32, + pub pad_h: f32, + pub pad_v: f32, + pub caret_w: f32, + pub caret_h: f32, +} + +impl MultilineMetrics +{ + pub( crate ) fn resolve( canvas: &Canvas, font_size: Option ) -> Self + { + let font_size = super::resolve_font_size( canvas, font_size ); + let dpi = canvas.dpi_scale().max( 1.0 ); + let glyph_h = font_size * dpi; + Self + { + font_size, + glyph_h, + line_h: glyph_h * theme::LINE_H_MULT, + pad_h: canvas.geom_px( theme::PAD_H ), + pad_v: canvas.geom_px( theme::PAD_V_MULTI ), + caret_w: ( theme::CARET_W * dpi ).round(), + caret_h: glyph_h + theme::CARET_OVERHANG * dpi, + } + } + + pub( crate ) fn height_for_rows( &self, rows: u32 ) -> f32 + { + rows as f32 * self.line_h + self.pad_v * 2.0 + } + + pub( crate ) fn inner_width( &self, rect: Rect ) -> f32 + { + ( rect.width - self.pad_h * 2.0 ).max( self.glyph_h ) + } + + pub( crate ) fn visible_lines( &self, rect: Rect ) -> usize + { + let inner_h = ( rect.height - self.pad_v * 2.0 ).max( self.line_h ); + ( inner_h / self.line_h ).floor().max( 1.0 ) as usize + } + + /// First visual row on screen so that the caret's row stays visible. + pub( crate ) fn first_visible_line( &self, rect: Rect, total_lines: usize, cursor_line: usize ) -> usize + { + let visible = self.visible_lines( rect ); + let max_first = total_lines.saturating_sub( visible ); + if cursor_line + 1 > visible + { + ( cursor_line + 1 - visible ).min( max_first ) + } else { + 0 + } + } +} diff --git a/src/widget/text_edit/mod.rs b/src/widget/text_edit/mod.rs index 8c3aeae..09c6e0a 100644 --- a/src/widget/text_edit/mod.rs +++ b/src/widget/text_edit/mod.rs @@ -18,6 +18,7 @@ pub( crate ) mod theme; pub( crate ) mod wrapping; pub( crate ) mod hit_test; pub( crate ) mod cursor; +pub( crate ) mod metrics; mod draw; #[ cfg( test ) ] mod tests; @@ -439,9 +440,8 @@ impl TextEdit { if self.multiline && !self.effective_secure() { - let line_h = theme::FONT_SIZE * theme::LINE_H_MULT; - let h = self.rows as f32 * line_h + theme::PAD_V_MULTI * 2.0; - ( max_width, h ) + let m = metrics::MultilineMetrics::resolve( canvas, self.font_size ); + ( max_width, m.height_for_rows( self.rows ) ) } else { let w = self.fixed_width .map( |l| canvas.resolve_geom( l ).min( max_width ) ) diff --git a/src/widget/text_edit/theme.rs b/src/widget/text_edit/theme.rs index 09ddfbf..80a77b3 100644 --- a/src/widget/text_edit/theme.rs +++ b/src/widget/text_edit/theme.rs @@ -23,6 +23,9 @@ pub const HEIGHT: f32 = 48.0; pub const RADIUS: f32 = 100.0; pub const FONT_SIZE: f32 = 16.0; pub const PAD_H: f32 = 20.0; +pub const CARET_W: f32 = 2.0; +/// How much taller than the glyph box the caret is drawn. +pub const CARET_OVERHANG: f32 = 4.0; /// Vertical padding inside multiline boxes — the single-line variant /// centres the text vertically in `HEIGHT`, so it doesn't need this. pub const PAD_V_MULTI: f32 = 12.0; @@ -35,8 +38,8 @@ pub const ROWS_DEFAULT: u32 = 5; /// Corner radius applied to multiline boxes — the pill shape used by /// the single-line variant looks wrong at multi-row heights. pub const RADIUS_MULTI: f32 = 12.0; -/// Side length, in logical pixels, of the -/// [`super::TextEdit::password_toggle`] eye icon. +/// Side length of the [`super::TextEdit::password_toggle`] eye icon in a +/// field [`HEIGHT`] tall; it scales with the field height. pub const PASSWORD_TOGGLE_SIZE: f32 = 20.0; /// Extra horizontal slop on each side of the eye icon's hit zone /// to make the toggle finger-friendly even on touch panels.