ltk/widget/text_edit/
hit_test.rsuse crate::render::Canvas;
use crate::types::{ Point, Rect };
use super::theme;
use super::wrapping::compute_visual_lines;
pub( crate ) fn byte_offset_at(
canvas: &Canvas,
rect: Rect,
pos: Point,
value: &str,
multiline: bool,
secure: bool,
cursor_pos: usize,
align: crate::widget::text::TextAlign,
font_size: f32,
) -> usize
{
if value.is_empty() { return 0; }
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 visual_lines = compute_visual_lines( canvas, value, inner_width, theme::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 visual_idx = ( ( pos.y - rect.y - theme::PAD_V_MULTI ) / 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 )
} else {
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 );
byte_offset_in_line(
canvas, value, 0, value.len(),
pos.x - rect.x - theme::PAD_H - align_x + scroll_x,
secure,
font_size,
)
}
}
pub( crate ) fn single_line_scroll_x(
canvas: &Canvas,
rect: Rect,
value: &str,
cursor: usize,
secure: bool,
font_size: f32,
) -> f32
{
let inner_width = ( rect.width - theme::PAD_H * 2.0 ).max( font_size );
let safe_cursor = cursor.min( value.len() );
let display_value: String = if secure
{
"\u{2022}".repeat( value.chars().count() )
} else {
value.to_string()
};
let prefix_text: String = if secure
{
"\u{2022}".repeat( value[..safe_cursor].chars().count() )
} else {
value[..safe_cursor].to_string()
};
let total_w = canvas.measure_text( &display_value, font_size );
if total_w <= inner_width { return 0.0; }
let prefix_w = canvas.measure_text( &prefix_text, font_size );
let suffix_w = ( total_w - prefix_w ).max( 0.0 );
const MARGIN: f32 = 4.0;
if prefix_w <= inner_width / 2.0
{
0.0
} else if suffix_w <= inner_width / 2.0 {
( total_w - inner_width + MARGIN ).max( 0.0 )
} else {
prefix_w - inner_width / 2.0
}
}
pub( crate ) fn single_line_align_offset(
canvas: &Canvas,
rect: Rect,
value: &str,
secure: bool,
align: crate::widget::text::TextAlign,
font_size: f32,
) -> f32
{
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 display_value: String = if secure
{
"\u{2022}".repeat( value.chars().count() )
} else {
value.to_string()
};
let total_w = canvas.measure_text( &display_value, font_size );
if total_w >= inner_width { return 0.0; }
match align
{
TextAlign::Left => 0.0,
TextAlign::Center => ( inner_width - total_w ) * 0.5,
TextAlign::Right => inner_width - total_w,
}
}
pub( super ) fn byte_offset_in_line(
canvas: &Canvas,
value: &str,
line_start: usize,
line_end: usize,
target_x: f32,
secure: bool,
font_size: f32,
) -> usize
{
let line = &value[line_start..line_end];
if target_x <= 0.0 { return line_start; }
let mut acc_w = 0.0f32;
let mut last_byte = line_start;
for ( ofs, ch ) in line.char_indices()
{
let glyph_str = if secure { "\u{2022}".to_string() } else { ch.to_string() };
let w = canvas.measure_text( &glyph_str, font_size );
if target_x < acc_w + w * 0.5
{
return line_start + ofs;
}
acc_w += w;
last_byte = line_start + ofs + ch.len_utf8();
}
last_byte.min( line_end )
}