use core::{ iter::{Enumerate, Peekable}, slice, }; use heapless::LinearMap; use crate::ui::{ component::{Component, Event, EventCtx, Never}, display::{Color, Font}, geometry::Rect, }; use super::layout::{ DefaultTextTheme, LayoutFit, LayoutSink, LineBreaking, Op, PageBreaking, TextLayout, TextRenderer, }; pub const MAX_ARGUMENTS: usize = 6; pub struct FormattedText { layout: TextLayout, format: F, args: LinearMap<&'static [u8], T, MAX_ARGUMENTS>, char_offset: usize, } impl FormattedText { pub fn new(format: F) -> Self { Self { format, layout: TextLayout::new::(), args: LinearMap::new(), char_offset: 0, } } pub fn with(mut self, key: &'static [u8], value: T) -> Self { if self.args.insert(key, value).is_err() { #[cfg(feature = "ui_debug")] panic!("text args map is full"); } self } pub fn with_format(mut self, format: F) -> Self { self.format = format; self } pub fn with_text_font(mut self, text_font: Font) -> Self { self.layout.text_font = text_font; self } pub fn with_text_color(mut self, text_color: Color) -> Self { self.layout.text_color = text_color; self } pub fn with_line_breaking(mut self, line_breaking: LineBreaking) -> Self { self.layout.line_breaking = line_breaking; self } pub fn with_page_breaking(mut self, page_breaking: PageBreaking) -> Self { self.layout.page_breaking = page_breaking; self } pub fn set_char_offset(&mut self, char_offset: usize) { self.char_offset = char_offset; } pub fn char_offset(&mut self) -> usize { self.char_offset } pub fn layout_mut(&mut self) -> &mut TextLayout { &mut self.layout } } impl FormattedText where F: AsRef<[u8]>, T: AsRef<[u8]>, { pub fn layout_content(&self, sink: &mut dyn LayoutSink) -> LayoutFit { let mut cursor = self.layout.initial_cursor(); let mut ops = Op::skip_n_text_bytes( Tokenizer::new(self.format.as_ref()).flat_map(|arg| match arg { Token::Literal(literal) => Some(Op::Text(literal)), Token::Argument(b"mono") => Some(Op::Font(self.layout.mono_font)), Token::Argument(b"bold") => Some(Op::Font(self.layout.bold_font)), Token::Argument(b"normal") => Some(Op::Font(self.layout.normal_font)), Token::Argument(b"medium") => Some(Op::Font(self.layout.medium_font)), Token::Argument(argument) => self .args .get(argument) .map(|value| Op::Text(value.as_ref())), }), self.char_offset, ); self.layout.layout_ops(&mut ops, &mut cursor, sink) } } impl Component for FormattedText where F: AsRef<[u8]>, T: AsRef<[u8]>, { type Msg = Never; fn place(&mut self, bounds: Rect) -> Rect { self.layout.bounds = bounds; self.layout.bounds } fn event(&mut self, _ctx: &mut EventCtx, _event: Event) -> Option { None } fn paint(&mut self) { self.layout_content(&mut TextRenderer); } fn bounds(&self, sink: &mut dyn FnMut(Rect)) { sink(self.layout.bounds) } } #[cfg(feature = "ui_debug")] pub mod trace { use crate::ui::component::text::layout::trace::TraceSink; use super::*; pub struct TraceText<'a, F, T>(pub &'a FormattedText); impl<'a, F, T> crate::trace::Trace for TraceText<'a, F, T> where F: AsRef<[u8]>, T: AsRef<[u8]>, { fn trace(&self, d: &mut dyn crate::trace::Tracer) { self.0.layout_content(&mut TraceSink(d)); } } } #[cfg(feature = "ui_debug")] impl crate::trace::Trace for FormattedText where F: AsRef<[u8]>, T: AsRef<[u8]>, { fn trace(&self, t: &mut dyn crate::trace::Tracer) { t.open("Text"); t.field("content", &trace::TraceText(self)); t.close(); } } #[derive(Copy, Clone, PartialEq, Eq)] pub enum Token<'a> { /// Process literal text content. Literal(&'a [u8]), /// Process argument with specified descriptor. Argument(&'a [u8]), } /// Processes a format string into an iterator of `Token`s. /// /// # Example /// /// ``` /// let parser = Tokenizer::new("Nice to meet {you}, where you been?"); /// assert!(matches!(parser.next(), Some(Token::Literal("Nice to meet ")))); /// assert!(matches!(parser.next(), Some(Token::Argument("you")))); /// assert!(matches!(parser.next(), Some(Token::Literal(", where you been?")))); /// ``` pub struct Tokenizer<'a> { input: &'a [u8], inner: Peekable>>, } impl<'a> Tokenizer<'a> { /// Create a new tokenizer for bytes of a formatting string `input`, /// returning an iterator. pub fn new(input: &'a [u8]) -> Self { Self { input, inner: input.iter().enumerate().peekable(), } } } impl<'a> Iterator for Tokenizer<'a> { type Item = Token<'a>; fn next(&mut self) -> Option { const ASCII_OPEN_BRACE: u8 = b'{'; const ASCII_CLOSED_BRACE: u8 = b'}'; match self.inner.next() { // Argument token is starting. Read until we find '}', then parse the content between // the braces and return the token. If we encounter the end of string before the closing // brace, quit. Some((open, &ASCII_OPEN_BRACE)) => loop { match self.inner.next() { Some((close, &ASCII_CLOSED_BRACE)) => { break Some(Token::Argument(&self.input[open + 1..close])); } None => { break None; } _ => {} } }, // Literal token is starting. Read until we find '{' or the end of string, and return // the token. Use `peek()` for matching the opening brace, se we can keep it // in the iterator for the above code. Some((start, _)) => loop { match self.inner.peek() { Some(&(open, &ASCII_OPEN_BRACE)) => { break Some(Token::Literal(&self.input[start..open])); } None => { break Some(Token::Literal(&self.input[start..])); } _ => { self.inner.next(); } } }, None => None, } } } #[cfg(test)] mod tests { use super::*; #[test] fn tokenizer_yields_expected_tokens() { assert!(Tokenizer::new(b"").eq([])); assert!(Tokenizer::new(b"x").eq([Token::Literal(b"x")])); assert!(Tokenizer::new(b"x\0y").eq([Token::Literal("x\0y".as_bytes())])); assert!(Tokenizer::new(b"{").eq([])); assert!(Tokenizer::new(b"x{").eq([Token::Literal(b"x")])); assert!(Tokenizer::new(b"x{y").eq([Token::Literal(b"x")])); assert!(Tokenizer::new(b"{}").eq([Token::Argument(b"")])); assert!(Tokenizer::new(b"x{}y{").eq([ Token::Literal(b"x"), Token::Argument(b""), Token::Literal(b"y"), ])); assert!(Tokenizer::new(b"{\0}").eq([Token::Argument("\0".as_bytes()),])); assert!(Tokenizer::new(b"{{y}").eq([Token::Argument(b"{y"),])); assert!(Tokenizer::new(b"{{{{xyz").eq([])); assert!(Tokenizer::new(b"x{}{{}}}}").eq([ Token::Literal(b"x"), Token::Argument(b""), Token::Argument(b"{"), Token::Literal(b"}}}"), ])); } }