use std::sync::Arc;
use crate::font_face::FontFace;
use crate::layout::stack::VAlign;
use crate::theme::FontStyle;
use crate::types::{ Color, Length, Rect };
use crate::render::Canvas;
use super::Element;
#[ cfg( test ) ]
mod tests;
#[ derive( Debug, Clone, Copy, PartialEq ) ]
pub enum TextAlign
{
Left,
Center,
Right,
}
pub struct Text
{
pub( crate ) content: String,
pub( crate ) size: Length,
pub( crate ) color: Color,
pub( crate ) align: TextAlign,
pub( crate ) wrap: bool,
pub( crate ) truncate: bool,
pub( crate ) font: Option<( String, u16, FontStyle )>,
pub( crate ) face: Option<Arc<FontFace>>,
pub( crate ) line_height: f32,
pub( crate ) fit: bool,
pub( crate ) valign: VAlign,
}
impl Text
{
pub fn new( content: impl Into<String> ) -> Self
{
Self
{
content: content.into(),
size: Length::px( 16.0 ),
color: Color::WHITE,
align: TextAlign::Left,
wrap: false,
truncate: true,
font: None,
face: None,
line_height: 1.0,
fit: false,
valign: VAlign::Top,
}
}
#[ inline ]
fn resolved_size( &self, canvas: &Canvas ) -> f32
{
canvas.resolve_font( self.size )
}
pub fn no_truncate( mut self ) -> Self
{
self.truncate = false;
self
}
pub fn font( mut self, family: impl Into<String>, weight: u16, style: FontStyle ) -> Self
{
self.font = Some( ( family.into(), weight, style ) );
self
}
pub fn face( mut self, face: Arc<FontFace> ) -> Self
{
self.face = Some( face );
self
}
pub fn weight( mut self, weight: u16 ) -> Self
{
self.font = Some( ( "sora".to_string(), weight, FontStyle::Normal ) );
self
}
pub fn size( mut self, s: impl Into<Length> ) -> Self
{
self.size = s.into();
self
}
pub fn color( mut self, c: Color ) -> Self
{
self.color = c;
self
}
pub fn align( mut self, a: TextAlign ) -> Self
{
self.align = a;
self
}
pub fn align_center( mut self ) -> Self
{
self.align = TextAlign::Center;
self
}
pub fn wrap( mut self, w: bool ) -> Self
{
self.wrap = w;
self
}
pub fn line_height( mut self, mult: f32 ) -> Self
{
self.line_height = mult.max( 0.5 );
self
}
pub fn fit( mut self ) -> Self
{
self.fit = true;
self
}
pub fn valign( mut self, v: VAlign ) -> Self
{
self.valign = v;
self
}
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<FontFace>>
{
if let Some( face ) = &self.face
{
return Some( face.clone() );
}
self.font.as_ref().map( |( family, weight, style )|
{
canvas.font_for( family, *weight, *style )
} )
}
fn measure( &self, text: &str, size: f32, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
{
match font
{
Some( f ) => canvas.measure_text_with_font( text, size, f ),
None => canvas.measure_text( text, size ),
}
}
fn measure_char( &self, ch: char, size: f32, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
{
match font
{
Some( f ) => f.metrics( ch, size * canvas.dpi_scale() ).advance_width,
None => canvas.font_metrics( ch, size ).advance_width,
}
}
fn paint( &self, canvas: &mut Canvas, text: &str, x: f32, y: f32, size: f32, font: Option<&Arc<FontFace>> )
{
match font
{
Some( f ) => canvas.draw_text_with_font( text, x, y, size, self.color, f ),
None => canvas.draw_text( text, x, y, size, self.color ),
}
}
fn line_height_at( &self, size: f32, canvas: &Canvas ) -> f32
{
canvas.font_line_metrics( size )
.map( |m| m.new_line_size )
.unwrap_or( size ) * self.line_height
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let size = self.resolved_size( canvas );
let line_h = self.line_height_at( size, canvas );
let font = self.resolve_font( canvas );
if self.wrap
{
let lines = wrap_lines( &self.content, size, max_width - WRAP_PAD, canvas, font.as_ref() );
let h = line_h * lines.len().max( 1 ) as f32;
( max_width, h )
}
else
{
let w = ( self.measure( &self.content, size, canvas, font.as_ref() ) + 8.0 ).min( max_width );
( w, line_h )
}
}
fn fitted_size( &self, rect: Rect, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> f32
{
let size = self.resolved_size( canvas );
if !self.fit { return size; }
let fits = |s: f32|
{
let line_h = self.line_height_at( s, canvas );
if self.wrap
{
wrap_lines( &self.content, s, rect.width - WRAP_PAD, canvas, font ).len() as f32 * line_h <= rect.height
} else {
self.measure( &self.content, s, canvas, font ) + 8.0 <= rect.width && line_h <= rect.height
}
};
if fits( size ) { return size; }
let ( mut lo, mut hi ) = ( 1.0_f32.min( size ), size );
for _ in 0..12
{
let mid = ( lo + hi ) / 2.0;
if fits( mid ) { lo = mid; } else { hi = mid; }
}
lo
}
fn top( &self, rect: Rect, block: f32 ) -> f32
{
let slack = ( rect.height - block ).max( 0.0 );
match self.valign
{
VAlign::Top | VAlign::Fill => rect.y,
VAlign::Center => rect.y + slack / 2.0,
VAlign::Bottom => rect.y + slack,
}
}
pub fn draw( &self, canvas: &mut Canvas, rect: Rect, _focused: bool )
{
let font = self.resolve_font( canvas );
let size = self.fitted_size( rect, canvas, font.as_ref() );
let ascent = canvas.font_line_metrics( size )
.map( |m| m.ascent )
.unwrap_or( size * 0.8 );
let line_h = self.line_height_at( size, canvas );
if self.wrap
{
let lines = wrap_lines( &self.content, size, rect.width - WRAP_PAD, canvas, font.as_ref() );
let top = self.top( rect, line_h * lines.len() as f32 );
for ( i, line ) in lines.iter().enumerate()
{
let line_w = self.measure( line, size, canvas, font.as_ref() );
let slack = ( rect.width - line_w ).max( 0.0 );
let pad = WRAP_PAD.min( slack );
let tx = match self.align
{
TextAlign::Left => rect.x + pad,
TextAlign::Center => rect.x + slack / 2.0,
TextAlign::Right => rect.x + rect.width - line_w - pad,
};
let ty = top + ascent + line_h * i as f32;
self.paint( canvas, line, tx, ty, size, font.as_ref() );
}
return;
}
let text_w = self.measure( &self.content, size, canvas, font.as_ref() );
let display = if self.truncate && text_w > rect.width && rect.width > 0.0
{
let ellipsis = "...";
let ell_w = self.measure( ellipsis, size, canvas, font.as_ref() );
let budget = rect.width - ell_w;
if budget <= 0.0
{
ellipsis.to_string()
}
else
{
let mut accum = 0.0_f32;
let truncated: String = self.content.chars().take_while( |ch|
{
let cw = self.measure_char( *ch, size, canvas, font.as_ref() );
accum += cw;
accum <= budget
} ).collect();
format!( "{truncated}{ellipsis}" )
}
}
else
{
self.content.clone()
};
let disp_w = self.measure( &display, size, canvas, font.as_ref() );
let slack = ( rect.width - disp_w ).max( 0.0 );
let pad = 4.0_f32.min( slack );
let tx = match self.align
{
TextAlign::Left => rect.x + pad,
TextAlign::Center => rect.x + slack / 2.0,
TextAlign::Right => rect.x + rect.width - disp_w - pad,
};
let ty = self.top( rect, line_h ) + ascent;
self.paint( canvas, &display, tx, ty, size, font.as_ref() );
}
}
pub const WRAP_PAD: f32 = 4.0;
fn wrap_lines( text: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<FontFace>> ) -> Vec<String>
{
if max_width <= 0.0 || text.is_empty()
{
return vec![ text.to_string() ];
}
let measure = |s: &str| -> f32
{
match font
{
Some( f ) => canvas.measure_text_with_font( s, size, f ),
None => canvas.measure_text( s, size ),
}
};
let space_w = measure( " " );
let mut lines = Vec::new();
let mut current = String::new();
let mut current_w = 0.0_f32;
for word in text.split_whitespace()
{
let word_w = measure( word );
if !current.is_empty() && current_w + space_w + word_w <= max_width
{
current.push( ' ' );
current.push_str( word );
current_w += space_w + word_w;
continue;
}
if !current.is_empty()
{
lines.push( std::mem::take( &mut current ) );
}
if word_w <= max_width
{
current.push_str( word );
current_w = word_w;
continue;
}
let mut start = 0;
for ( i, ch ) in word.char_indices().skip( 1 )
{
if measure( &word[ start..i + ch.len_utf8() ] ) > max_width
{
lines.push( word[ start..i ].to_string() );
start = i;
}
}
current.push_str( &word[ start.. ] );
current_w = measure( ¤t );
}
if !current.is_empty() { lines.push( current ); }
if lines.is_empty() { lines.push( String::new() ); }
lines
}
impl<Msg: Clone + 'static> From<Text> for Element<Msg>
{
fn from( t: Text ) -> Self
{
Element::Text( t )
}
}