use std::sync::Arc;
use fontdue::Font;
use crate::theme::FontStyle;
use crate::types::{ Color, Length, Rect };
use crate::render::Canvas;
use super::{ Element, MapFn };
#[ cfg( test ) ]
mod tests;
pub struct LinkSpan<Msg>
{
pub( crate ) start: usize,
pub( crate ) end: usize,
pub( crate ) msg: Msg,
}
pub struct RichText<Msg: Clone>
{
pub( crate ) content: String,
pub( crate ) size: Length,
pub( crate ) color: Color,
pub( crate ) link_color: Color,
pub( crate ) font: Option<( String, u16, FontStyle )>,
pub( crate ) links: Vec<LinkSpan<Msg>>,
}
impl<Msg: Clone> RichText<Msg>
{
pub fn new( content: impl Into<String> ) -> Self
{
Self
{
content: content.into(),
size: Length::px( 16.0 ),
color: Color::WHITE,
link_color: Color::rgb( 0.20, 0.50, 0.95 ),
font: None,
links: Vec::new(),
}
}
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 link_color( mut self, c: Color ) -> Self
{
self.link_color = c;
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 link( mut self, start: usize, end: usize, msg: Msg ) -> Self
{
self.links.push( LinkSpan { start, end, msg } );
self
}
#[ inline ]
fn resolved_size( &self, canvas: &Canvas ) -> f32
{
self.size.resolve( canvas.viewport_logical(), Length::EM_BASE_DEFAULT )
}
fn resolve_font( &self, canvas: &Canvas ) -> Option<Arc<Font>>
{
self.font.as_ref().map( |( family, weight, style )| canvas.font_for( family, *weight, *style ) )
}
fn measure( &self, text: &str, canvas: &Canvas, font: Option<&Arc<Font>> ) -> f32
{
let size = self.resolved_size( canvas );
match font
{
Some( f ) => canvas.measure_text_with_font( text, size, f ),
None => canvas.measure_text( text, size ),
}
}
pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> ( f32, f32 )
{
let size = self.resolved_size( canvas );
let line_h = canvas.font_line_metrics( size ).map( |m| m.new_line_size ).unwrap_or( size );
let font = self.resolve_font( canvas );
let lines = wrap_tracked( &self.content, size, max_width, canvas, font.as_ref() );
( max_width, line_h * lines.len().max( 1 ) as f32 )
}
pub fn draw( &self, canvas: &mut Canvas, rect: Rect )
{
let size = self.resolved_size( canvas );
let ascent = canvas.font_line_metrics( size ).map( |m| m.ascent ).unwrap_or( size * 0.8 );
let line_h = canvas.font_line_metrics( size ).map( |m| m.new_line_size ).unwrap_or( size );
let font = self.resolve_font( canvas );
let lines = wrap_tracked( &self.content, size, rect.width, canvas, font.as_ref() );
for ( i, line ) in lines.iter().enumerate()
{
let ty = rect.y + ascent + line_h * i as f32;
match font.as_ref()
{
Some( f ) => canvas.draw_text_with_font( &line.text, rect.x, ty, size, self.color, f ),
None => canvas.draw_text( &line.text, rect.x, ty, size, self.color ),
}
for link in &self.links
{
let Some( ( cx, _cw, sub ) ) = line.segment( link.start, link.end ) else { continue; };
let dx = self.measure( &line.text[..cx], canvas, font.as_ref() );
let tx = rect.x + dx;
match font.as_ref()
{
Some( f ) => canvas.draw_text_with_font( &sub, tx, ty, size, self.link_color, f ),
None => canvas.draw_text( &sub, tx, ty, size, self.link_color ),
}
let sw = self.measure( &sub, canvas, font.as_ref() );
canvas.draw_line( tx, ty + 2.0, tx + sw, ty + 2.0, self.link_color, 1.0 );
}
}
}
pub fn link_rects( &self, rect: Rect, canvas: &Canvas ) -> Vec<( Rect, Msg )>
{
let size = self.resolved_size( canvas );
let ascent = canvas.font_line_metrics( size ).map( |m| m.ascent ).unwrap_or( size * 0.8 );
let line_h = canvas.font_line_metrics( size ).map( |m| m.new_line_size ).unwrap_or( size );
let font = self.resolve_font( canvas );
let lines = wrap_tracked( &self.content, size, rect.width, canvas, font.as_ref() );
let mut out = Vec::new();
for ( i, line ) in lines.iter().enumerate()
{
for link in &self.links
{
let Some( ( cx, _cw, sub ) ) = line.segment( link.start, link.end ) else { continue; };
let dx = self.measure( &line.text[..cx], canvas, font.as_ref() );
let sw = self.measure( &sub, canvas, font.as_ref() );
let y = rect.y + line_h * i as f32;
out.push( ( Rect { x: rect.x + dx, y, width: sw, height: ascent.max( line_h ) }, link.msg.clone() ) );
}
}
out
}
pub( crate ) fn map_msg<U: Clone>( self, f: &MapFn<Msg, U> ) -> RichText<U>
{
RichText
{
content: self.content,
size: self.size,
color: self.color,
link_color: self.link_color,
font: self.font,
links: self.links.into_iter().map( |l| LinkSpan { start: l.start, end: l.end, msg: f( l.msg ) } ).collect(),
}
}
}
struct VisualLine
{
text: String,
offsets: Vec<usize>,
}
impl VisualLine
{
fn segment( &self, start: usize, end: usize ) -> Option<( usize, usize, String )>
{
let mut byte = 0;
let mut first: Option<usize> = None;
let mut last_byte = 0;
for ( ci, ch ) in self.text.chars().enumerate()
{
let src = self.offsets.get( ci ).copied().unwrap_or( usize::MAX );
if src >= start && src < end
{
if first.is_none() { first = Some( byte ); }
last_byte = byte + ch.len_utf8();
}
byte += ch.len_utf8();
}
let f = first?;
Some( ( f, last_byte - f, self.text[f..last_byte].to_string() ) )
}
}
fn wrap_tracked( content: &str, size: f32, max_width: f32, canvas: &Canvas, font: Option<&Arc<Font>> ) -> Vec<VisualLine>
{
let measure = |s: &str| -> f32
{
match font
{
Some( f ) => canvas.measure_text_with_font( s, size, f ),
None => canvas.measure_text( s, size ),
}
};
let mut words: Vec<( usize, usize )> = Vec::new();
let mut breaks: Vec<usize> = Vec::new(); let mut start: Option<usize> = None;
for ( b, ch ) in content.char_indices()
{
if ch == '\n'
{
if let Some( s ) = start.take() { words.push( ( s, b ) ); }
breaks.push( words.len() );
}
else if ch.is_whitespace()
{
if let Some( s ) = start.take() { words.push( ( s, b ) ); }
}
else if start.is_none()
{
start = Some( b );
}
}
if let Some( s ) = start { words.push( ( s, content.len() ) ); }
let space_w = measure( " " );
let mut lines: Vec<VisualLine> = Vec::new();
let mut cur: Vec<( usize, usize )> = Vec::new();
let mut cur_w = 0.0_f32;
for ( wi, &( ws, we ) ) in words.iter().enumerate()
{
let word_w = measure( &content[ws..we] );
if cur.is_empty()
{
cur.push( ( ws, we ) );
cur_w = word_w;
}
else if max_width > 0.0 && cur_w + space_w + word_w > max_width
{
flush_line( content, &mut cur, &mut lines );
cur.push( ( ws, we ) );
cur_w = word_w;
}
else
{
cur.push( ( ws, we ) );
cur_w += space_w + word_w;
}
if breaks.contains( &( wi + 1 ) )
{
flush_line( content, &mut cur, &mut lines );
cur_w = 0.0;
}
}
if !cur.is_empty() { flush_line( content, &mut cur, &mut lines ); }
if lines.is_empty() { lines.push( VisualLine { text: String::new(), offsets: vec![ 0 ] } ); }
lines
}
fn flush_line( content: &str, cur: &mut Vec<( usize, usize )>, lines: &mut Vec<VisualLine> )
{
let mut text = String::new();
let mut offsets = Vec::new();
for ( wi, &( ws, we ) ) in cur.iter().enumerate()
{
if wi > 0
{
offsets.push( ws );
text.push( ' ' );
}
for ( b, ch ) in content[ws..we].char_indices()
{
offsets.push( ws + b );
text.push( ch );
}
}
offsets.push( cur.last().map( |&( _, we )| we ).unwrap_or( 0 ) );
lines.push( VisualLine { text, offsets } );
cur.clear();
}
impl<Msg: Clone + 'static> From<RichText<Msg>> for Element<Msg>
{
fn from( t: RichText<Msg> ) -> Self
{
Element::RichText( t )
}
}
pub fn rich_text<Msg: Clone>( content: impl Into<String> ) -> RichText<Msg>
{
RichText::new( content )
}