use crate::render::Canvas;
use crate::render::clip::interior_clip;
use crate::types::{ Color, Rect };
use super::TextEdit;
use super::hit_test::{ single_line_align_offset, single_line_scroll_x };
use super::metrics::{ single_line_pad_h, single_line_scale };
use super::scroll::{ TextScroll, Window };
use super::gutter;
use super::tabs::{ line_x, next_stop, tab_width };
use super::TextFont;
use super::theme;
fn password_toggle_reserve( rect: Rect ) -> f32
{
( theme::PASSWORD_TOGGLE_SIZE + theme::PASSWORD_TOGGLE_SLOP * 2.0 ) * single_line_scale( rect )
}
pub fn password_toggle_hit_zone( rect: Rect ) -> Rect
{
let zone_w = ( password_toggle_reserve( rect )
+ single_line_pad_h( rect, None ) * 0.5 ).min( rect.width );
Rect
{
x: rect.x + rect.width - zone_w,
y: rect.y,
width: zone_w,
height: rect.height,
}
}
impl<Msg: Clone> TextEdit<Msg>
{
pub fn draw(
&self,
canvas: &mut Canvas,
rect: Rect,
focused: bool,
cursor_pos: usize,
selection_anchor: usize,
scroll: TextScroll,
) -> Option<TextScroll>
{
if self.is_multiline()
{
return Some( self.draw_multiline( canvas, rect, focused, cursor_pos, selection_anchor, scroll ) );
}
if !self.borderless
{
let border_c = if focused { theme::focus_border() } else { theme::border() };
let border_w = if focused { theme::FOCUS_BORDER_W } else { theme::BORDER_W };
canvas.fill_rect( rect, theme::bg(), theme::RADIUS );
canvas.stroke_rect( rect, border_c, border_w, theme::RADIUS );
}
let font_size = self.effective_font_size( canvas );
let text_y = rect.y + (rect.height + font_size) / 2.0 - 2.0;
let text = self.display_text();
let secure = self.effective_secure();
let toggle_reserve = if self.password_toggle.is_some()
{
password_toggle_reserve( rect )
}
else
{
0.0
};
let pad_h = single_line_pad_h( rect, self.pad_h );
let text_rect = Rect
{
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, self.pad_h );
let align_x = single_line_align_offset( canvas, text_rect, &self.value, secure, self.align, font_size, self.pad_h );
let outer_clip = canvas.clip_bounds();
let inner_rect = Rect
{
x: rect.x + pad_h * 0.5,
y: rect.y,
width: ( rect.width - pad_h - toggle_reserve ).max( 0.0 ),
height: rect.height,
};
canvas.set_clip_rects( &interior_clip( &outer_clip, inner_rect ) );
if focused && selection_anchor != cursor_pos
{
let ( s, e ) = (
cursor_pos.min( selection_anchor ).min( self.value.len() ),
cursor_pos.max( selection_anchor ).min( self.value.len() ),
);
let prefix_text = if secure
{
"\u{2022}".repeat( self.value[..s].chars().count() )
} else {
self.value[..s].to_string()
};
let span_text = if secure
{
"\u{2022}".repeat( self.value[s..e].chars().count() )
} else {
self.value[s..e].to_string()
};
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
{
x: x0, y: rect.y + 6.0,
width: w, height: rect.height - 12.0,
};
canvas.fill_rect( sel_rect, theme::selection(), 2.0 );
}
if text.is_empty()
{
let placeholder_align_x = single_line_align_offset(
canvas, rect, &self.placeholder, false, self.align, font_size, self.pad_h,
);
canvas.draw_text(
&self.placeholder,
rect.x + pad_h + placeholder_align_x,
text_y,
font_size,
theme::placeholder(),
);
} else {
canvas.draw_text(
&text,
rect.x + pad_h + align_x - scroll_x,
text_y,
font_size,
self.color.unwrap_or_else( theme::text ),
);
}
if focused
{
let safe_cursor = cursor_pos.min( self.value.len() );
let cursor_text = if secure
{
"\u{2022}".repeat( self.value[..safe_cursor].chars().count() )
} else {
self.value[..safe_cursor].to_string()
};
let cursor_x = rect.x + pad_h + align_x - scroll_x
+ canvas.measure_text( &cursor_text, font_size );
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: ( theme::CARET_W * dpi ).round(),
height: cursor_h,
};
canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
}
if outer_clip.is_empty()
{
canvas.clear_clip();
} else {
canvas.set_clip_rects( &outer_clip );
}
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 * 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() );
let zone = password_toggle_hit_zone( rect );
let dest = Rect
{
x: ( zone.x + ( zone.width - iw as f32 ) / 2.0 ).round(),
y: ( rect.y + ( rect.height - ih as f32 ) / 2.0 ).round(),
width: iw as f32,
height: ih as f32,
};
canvas.draw_image_data( &tinted, iw, ih, dest, 1.0 );
}
}
None
}
#[ allow( clippy::too_many_arguments ) ]
fn draw_runs( &self, canvas: &mut Canvas, font: &TextFont, start: usize, end: usize, x: f32, y: f32, font_size: f32 )
{
let plain = self.color.unwrap_or_else( theme::text );
let runs = self.highlights.as_deref().map( Vec::as_slice ).unwrap_or( &[] );
let origin = x;
let tab_w = tab_width( canvas, font, font_size );
let mut x = x;
let mut at = start;
let draw = |canvas: &mut Canvas, from: usize, to: usize, color: Color, x: &mut f32|
{
for ( i, piece ) in self.value[ from..to ].split( '\t' ).enumerate()
{
if i > 0 { *x = origin + next_stop( *x - origin, tab_w ); }
if piece.is_empty() { continue; }
font.draw( canvas, piece, *x, y, font_size, color );
*x += font.measure( canvas, piece, font_size );
}
};
let first = runs.partition_point( |run| run.range.end <= start );
for run in &runs[ first.. ]
{
if run.range.start >= end { break; }
let from = run.range.start.max( at );
let to = run.range.end.min( end );
if to <= from || !self.value.is_char_boundary( from ) || !self.value.is_char_boundary( to ) { continue; }
if from > at { draw( canvas, at, from, plain, &mut x ); }
draw( canvas, from, to, run.color, &mut x );
at = to;
}
if at < end { draw( canvas, at, end, plain, &mut x ); }
}
fn draw_multiline(
&self,
canvas: &mut Canvas,
rect: Rect,
focused: bool,
cursor_pos: usize,
selection_anchor: usize,
scroll: TextScroll,
) -> TextScroll
{
if !self.borderless
{
let border_c = if focused { theme::focus_border() } else { theme::border() };
let border_w = if focused { theme::FOCUS_BORDER_W } else { theme::BORDER_W };
canvas.fill_rect( rect, theme::bg(), theme::RADIUS_MULTI );
canvas.stroke_rect( rect, border_c, border_w, theme::RADIUS_MULTI );
}
let field = rect;
let font = self.text_font();
let rect = gutter::text_rect( canvas, field, &self.value, &font, self.line_numbers );
let window = Window::resolve( canvas, rect, &self.value, &font, cursor_pos, scroll );
let m = window.metrics;
let line_h = m.line_h;
let visual_lines = &window.lines;
let safe_cursor = cursor_pos.min( self.value.len() );
let cursor_visual_idx = window.cursor_line;
let first_line = window.first_line;
let baseline_0 = rect.y + m.pad_v + m.glyph_h;
let outer_clip = canvas.clip_bounds();
let inner_rect = Rect
{
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 ) );
if selection_anchor != cursor_pos
{
let s = cursor_pos.min( selection_anchor ).min( self.value.len() );
let e = cursor_pos.max( selection_anchor ).min( self.value.len() );
let color = if focused { theme::selection() } else { theme::selection_inactive() };
for ( i, vl ) in visual_lines.iter().enumerate().skip( first_line )
{
let visual_idx = i - first_line;
let y = rect.y + m.pad_v + visual_idx as f32 * line_h;
if y > rect.y + rect.height - m.pad_v { break; }
let span_start = s.max( vl.start );
let span_end = e.min( vl.end );
if span_start >= span_end { continue; }
let from = line_x( canvas, &font, &self.value, vl.start, span_start, m.font_size );
let to = line_x( canvas, &font, &self.value, vl.start, span_end, m.font_size );
let x0 = rect.x + m.pad_h + from;
let w = ( to - from ).max( m.caret_w * 2.0 );
let sel_rect = Rect
{
x: x0, y,
width: w, height: line_h,
};
canvas.fill_rect( sel_rect, color, 2.0 );
}
}
if self.value.is_empty()
{
canvas.draw_text(
&self.placeholder,
rect.x + m.pad_h,
baseline_0,
m.font_size,
theme::placeholder(),
);
} else {
for ( i, vl ) in visual_lines.iter().enumerate().skip( first_line )
{
let visual_idx = i - first_line;
let y = baseline_0 + visual_idx as f32 * line_h;
if y > rect.y + rect.height - m.pad_v + line_h
{
break;
}
self.draw_runs( canvas, &font, vl.start, vl.end, rect.x + m.pad_h, y, m.font_size );
}
}
if focused
{
let vl = visual_lines[ cursor_visual_idx ];
let cursor_x = rect.x + m.pad_h
+ line_x( canvas, &font, &self.value, vl.start, safe_cursor, m.font_size );
let visual_y_idx = cursor_visual_idx.saturating_sub( first_line );
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 + ( line_h - m.caret_h ) / 2.0,
width: m.caret_w,
height: m.caret_h,
};
canvas.fill_rect( cursor_rect, theme::cursor(), 0.0 );
}
if self.line_numbers
{
let band = Rect { x: field.x, width: rect.x - field.x, ..inner_rect };
canvas.set_clip_rects( &interior_clip( &outer_clip, band ) );
self.draw_line_numbers( canvas, rect.x, baseline_0, &window, focused.then_some( safe_cursor ) );
}
if outer_clip.is_empty()
{
canvas.clear_clip();
} else {
canvas.set_clip_rects( &outer_clip );
}
if let Some( bar ) = window.scrollbar( canvas, rect )
{
let radius = bar.track.width / 2.0;
canvas.fill_rect( bar.track, theme::scrollbar_track(), radius );
canvas.fill_rect( bar.thumb, theme::scrollbar_thumb(), radius );
}
TextScroll { first_line, cursor_pos: safe_cursor, remainder: scroll.remainder }
}
fn draw_line_numbers( &self, canvas: &mut Canvas, right: f32, baseline_0: f32, window: &Window, cursor: Option<usize> )
{
let m = window.metrics;
let bytes = self.value.as_bytes();
let lines = &window.lines;
let Some( first ) = lines.get( window.first_line ) else { return };
let newlines = |end: usize| bytes[ ..end ].iter().filter( |&&b| b == b'\n' ).count();
let current = cursor.map( |at| newlines( at ) + 1 );
let mut number = newlines( first.start ) + 1;
let mono = crate::system_fonts::monospace();
for ( k, vl ) in lines.iter().enumerate().skip( window.first_line )
{
let row = k - window.first_line;
if row > window.visible { break; }
let y = baseline_0 + row as f32 * m.line_h;
let starts = vl.start == 0 || bytes[ vl.start - 1 ] == b'\n';
if !starts { continue; }
if k != window.first_line { number += 1; }
let label = number.to_string();
let color = if current == Some( number ) { self.color.unwrap_or_else( theme::text ) } else { theme::placeholder() };
let x = right - gutter::measure( canvas, &label, m.font_size );
match &mono
{
Some( mono ) => canvas.draw_text_with_font( &label, x, y, m.font_size, color, mono ),
None => canvas.draw_text( &label, x, y, m.font_size, color ),
}
}
}
}