ltk/widget/text_edit/
wrapping.rsuse std::hash::{ Hash, Hasher };
use std::rc::Rc;
use crate::render::Canvas;
use super::tabs::{ next_stop, tab_width };
use super::TextFont;
#[ derive( Clone, Copy, Debug ) ]
pub( crate ) struct VisualLine
{
pub start: usize,
pub end: usize,
}
#[ derive( Clone, Copy, PartialEq ) ]
struct LayoutKey
{
hash: u64,
len: usize,
width: u32,
px: u32,
font: usize,
}
const LAYOUT_CACHE_CAP: usize = 4;
thread_local!
{
static LAYOUTS: std::cell::RefCell<Vec<( LayoutKey, Rc<Vec<VisualLine>> )>> = const { std::cell::RefCell::new( Vec::new() ) };
}
pub( crate ) fn compute_visual_lines(
canvas: &Canvas,
font: &TextFont,
value: &str,
inner_width: f32,
font_size: f32,
) -> Rc<Vec<VisualLine>>
{
let mut hasher = std::collections::hash_map::DefaultHasher::new();
value.hash( &mut hasher );
let key = LayoutKey
{
hash: hasher.finish(),
len: value.len(),
width: inner_width.to_bits(),
px: ( font_size * canvas.dpi_scale() ).to_bits(),
font: font.face_key( canvas ),
};
if let Some( lines ) = LAYOUTS.with( |c| c.borrow().iter().find( |( k, _ )| *k == key ).map( |( _, l )| Rc::clone( l ) ) )
{
return lines;
}
let lines = Rc::new( wrap( canvas, font, value, inner_width, font_size ) );
LAYOUTS.with( |c|
{
let mut c = c.borrow_mut();
if c.len() >= LAYOUT_CACHE_CAP { c.remove( 0 ); }
c.push( ( key, Rc::clone( &lines ) ) );
} );
lines
}
fn wrap( canvas: &Canvas, font: &TextFont, value: &str, inner_width: f32, font_size: f32 ) -> Vec<VisualLine>
{
let mut out = Vec::new();
if value.is_empty()
{
out.push( VisualLine { start: 0, end: 0 } );
return out;
}
let max_w = inner_width.max( 1.0 );
let tab_w = tab_width( canvas, font, font_size );
let mut byte_pos = 0usize;
for logical_line in value.split( '\n' )
{
let line_start = byte_pos;
let line_end = byte_pos + logical_line.len();
if line_start == line_end
{
out.push( VisualLine { start: line_start, end: line_end } );
} else {
wrap_logical_line( canvas, font, value, line_start, line_end, max_w, font_size, tab_w, &mut out );
}
byte_pos = line_end + 1;
}
out
}
#[ allow( clippy::too_many_arguments ) ]
fn wrap_logical_line(
canvas: &Canvas,
font: &TextFont,
value: &str,
start: usize,
end: usize,
max_width: f32,
font_size: f32,
tab_w: f32,
out: &mut Vec<VisualLine>,
)
{
let mut cur = start;
while cur < end
{
let segment = &value[cur..end];
let break_at = find_wrap_offset( canvas, font, segment, max_width, font_size, tab_w );
let raw_next = cur + break_at;
let mut next = raw_next.max( cur + 1 ).min( end );
while !value.is_char_boundary( next ) { next += 1; }
out.push( VisualLine { start: cur, end: next } );
cur = next;
}
}
fn find_wrap_offset(
canvas: &Canvas,
font: &TextFont,
segment: &str,
max_width: f32,
font_size: f32,
tab_w: f32,
) -> usize
{
let mut acc_w = 0.0f32;
let mut last_break_after: Option<usize> = None;
for ( i, ch ) in segment.char_indices()
{
let w = if ch == '\t'
{
next_stop( acc_w, tab_w ) - acc_w
} else {
font.measure_char( canvas, ch, font_size )
};
if acc_w + w > max_width && i > 0
{
return last_break_after.unwrap_or( i );
}
acc_w += w;
if ch == ' ' || ch == '\t'
{
last_break_after = Some( i + ch.len_utf8() );
}
}
segment.len()
}