diff --git a/examples/hello_world.rs b/examples/hello_world.rs index fbad68c..b7b670d 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -6,13 +6,14 @@ use toss::terminal::Terminal; fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), - "Hello world!", - ContentStyle::default().green(), + ("Hello world!", ContentStyle::default().green()), ); f.write( Pos::new(0, 1), - "Press any key to exit", - ContentStyle::default().on_dark_blue(), + ( + "Press any key to exit", + ContentStyle::default().on_dark_blue(), + ), ); f.show_cursor(Pos::new(16, 0)); } diff --git a/examples/overlapping_graphemes.rs b/examples/overlapping_graphemes.rs index e5277b1..adf610c 100644 --- a/examples/overlapping_graphemes.rs +++ b/examples/overlapping_graphemes.rs @@ -7,53 +7,45 @@ fn draw(f: &mut Frame) { f.write( Pos::new(0, 0), "Writing over wide graphemes removes the entire overwritten grapheme.", - ContentStyle::default(), ); let under = ContentStyle::default().white().on_dark_blue(); let over = ContentStyle::default().black().on_dark_yellow(); for i in 0..6 { let delta = i - 2; - f.write(Pos::new(2 + i * 7, 2), "a😀", under); - f.write(Pos::new(2 + i * 7, 3), "a😀", under); - f.write(Pos::new(2 + i * 7, 4), "a😀", under); - f.write(Pos::new(2 + i * 7 + delta, 3), "b", over); - f.write(Pos::new(2 + i * 7 + delta, 4), "😈", over); + f.write(Pos::new(2 + i * 7, 2), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 3), ("a😀", under)); + f.write(Pos::new(2 + i * 7, 4), ("a😀", under)); + f.write(Pos::new(2 + i * 7 + delta, 3), ("b", over)); + f.write(Pos::new(2 + i * 7 + delta, 4), ("😈", over)); } f.write( Pos::new(0, 6), "Wide graphemes at the edges of the screen apply their style, but are not", - ContentStyle::default(), - ); - f.write( - Pos::new(0, 7), - "actually rendered.", - ContentStyle::default(), ); + f.write(Pos::new(0, 7), "actually rendered."); let x1 = -1; let x2 = f.size().width as i32 / 2 - 3; let x3 = f.size().width as i32 - 5; - f.write(Pos::new(x1, 9), "123456", under); - f.write(Pos::new(x1, 10), "😀😀😀", under); - f.write(Pos::new(x2, 9), "123456", under); - f.write(Pos::new(x2, 10), "😀😀😀", under); - f.write(Pos::new(x3, 9), "123456", under); - f.write(Pos::new(x3, 10), "😀😀😀", under); + f.write(Pos::new(x1, 9), ("123456", under)); + f.write(Pos::new(x1, 10), ("😀😀😀", under)); + f.write(Pos::new(x2, 9), ("123456", under)); + f.write(Pos::new(x2, 10), ("😀😀😀", under)); + f.write(Pos::new(x3, 9), ("123456", under)); + f.write(Pos::new(x3, 10), ("😀😀😀", under)); let scientist = "👩‍🔬"; f.write( Pos::new(0, 12), "Most terminals ignore the zero width joiner and display this female", - ContentStyle::default(), ); f.write( Pos::new(0, 13), "scientist emoji as a woman and a microscope: 👩‍🔬", - ContentStyle::default(), ); for i in 0..(f.width(scientist) + 4) { - f.write(Pos::new(2, 15 + i as i32), scientist, under); - f.write(Pos::new(i as i32, 15 + i as i32), "x", over); + f.write(Pos::new(2, 15 + i as i32), (scientist, under)); + f.write(Pos::new(i as i32, 15 + i as i32), ("x", over)); } } diff --git a/examples/text_wrapping.rs b/examples/text_wrapping.rs index ee15dec..c82022e 100644 --- a/examples/text_wrapping.rs +++ b/examples/text_wrapping.rs @@ -1,6 +1,6 @@ use crossterm::event::Event; -use crossterm::style::ContentStyle; use toss::frame::{Frame, Pos}; +use toss::styled::Styled; use toss::terminal::Terminal; fn draw(f: &mut Frame) { @@ -18,13 +18,10 @@ fn draw(f: &mut Frame) { ); let breaks = f.wrap(text, f.size().width.into()); - let lines = toss::split_at_indices(text, &breaks); - for (i, line) in lines.iter().enumerate() { - f.write( - Pos::new(0, i as i32), - line.trim_end(), - ContentStyle::default(), - ); + let lines = Styled::default().then(text).split_at_indices(&breaks); + for (i, mut line) in lines.into_iter().enumerate() { + line.trim_end(); + f.write(Pos::new(0, i as i32), line); } } diff --git a/src/buffer.rs b/src/buffer.rs index 4707b53..f71c86e 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -1,6 +1,7 @@ use crossterm::style::ContentStyle; use unicode_segmentation::UnicodeSegmentation; +use crate::styled::Styled; use crate::widthdb::WidthDB; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -127,23 +128,23 @@ impl Buffer { } } - pub fn write( - &mut self, - widthdb: &mut WidthDB, - mut pos: Pos, - content: &str, - style: ContentStyle, - ) { + pub fn write(&mut self, widthdb: &mut WidthDB, mut pos: Pos, styled: &Styled) { // If we're not even visible, there's nothing to do if pos.y < 0 || pos.y >= self.size.height as i32 { return; } let y = pos.y as u16; - for grapheme in content.graphemes(true) { - let width = widthdb.grapheme_width(grapheme); + for styled_grapheme in styled.styled_graphemes() { + let width = widthdb.grapheme_width(styled_grapheme.content()); if width > 0 { - self.write_grapheme(pos.x, y, width, grapheme, style); + self.write_grapheme( + pos.x, + y, + width, + styled_grapheme.content(), + *styled_grapheme.style(), + ); } pos.x += width as i32; } diff --git a/src/frame.rs b/src/frame.rs index f8c46af..1566fac 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -4,6 +4,7 @@ use crossterm::style::ContentStyle; use crate::buffer::Buffer; pub use crate::buffer::{Pos, Size}; +use crate::styled::Styled; use crate::widthdb::WidthDB; use crate::wrap; @@ -60,7 +61,7 @@ impl Frame { wrap::wrap(text, width, &mut self.widthdb) } - pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { - self.buffer.write(&mut self.widthdb, pos, content, style); + pub fn write>(&mut self, pos: Pos, styled: S) { + self.buffer.write(&mut self.widthdb, pos, &styled.into()); } } diff --git a/src/lib.rs b/src/lib.rs index 9cbdd9f..d73aeed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ mod buffer; pub mod frame; +pub mod styled; pub mod terminal; mod widthdb; mod wrap; - -pub use wrap::split_at_indices; diff --git a/src/styled.rs b/src/styled.rs new file mode 100644 index 0000000..2ee4efb --- /dev/null +++ b/src/styled.rs @@ -0,0 +1,170 @@ +use crossterm::style::{ContentStyle, StyledContent}; +use unicode_segmentation::UnicodeSegmentation; + +#[derive(Debug, Clone)] +pub struct Chunk { + string: String, + style: ContentStyle, +} + +impl Chunk { + pub fn new(string: S, style: ContentStyle) -> Self { + Self { + string: string.to_string(), + style, + } + } + + pub fn plain(string: S) -> Self { + Self::new(string, ContentStyle::default()) + } + + pub fn split_at(&self, mid: usize) -> (Self, Self) { + let (lstr, rstr) = self.string.split_at(mid); + let left = Self { + string: lstr.to_string(), + style: self.style, + }; + let right = Self { + string: rstr.to_string(), + style: self.style, + }; + (left, right) + } +} + +impl From<&str> for Chunk { + fn from(str: &str) -> Self { + Self::plain(str) + } +} + +impl From for Chunk { + fn from(string: String) -> Self { + Self::plain(string) + } +} + +impl From<&String> for Chunk { + fn from(string: &String) -> Self { + Self::plain(string) + } +} + +impl From<(S,)> for Chunk { + fn from(tuple: (S,)) -> Self { + Self::plain(tuple.0) + } +} + +impl From<(S, ContentStyle)> for Chunk { + fn from(tuple: (S, ContentStyle)) -> Self { + Self::new(tuple.0, tuple.1) + } +} + +#[derive(Debug, Default, Clone)] +pub struct Styled(Vec); + +impl Styled { + pub fn new>(chunk: C) -> Self { + Self::default().then(chunk) + } + + pub fn then>(mut self, chunk: C) -> Self { + self.0.push(chunk.into()); + self + } + + pub fn and_then(mut self, other: Styled) -> Self { + self.0.extend(other.0); + self + } + + pub fn text(&self) -> String { + self.0.iter().flat_map(|c| c.string.chars()).collect() + } + + pub fn graphemes(&self) -> impl Iterator { + self.0.iter().flat_map(|c| c.string.graphemes(true)) + } + + pub fn grapheme_indices(&self) -> impl Iterator { + self.0 + .iter() + .scan(0, |s, c| { + let offset = *s; + *s += c.string.len(); + Some((offset, c)) + }) + .flat_map(|(o, c)| { + c.string + .grapheme_indices(true) + .map(move |(gi, g)| (o + gi, g)) + }) + } + + pub fn styled_graphemes(&self) -> impl Iterator> { + self.0.iter().flat_map(|c| { + c.string + .graphemes(true) + .map(|g| StyledContent::new(c.style, g)) + }) + } + + pub fn split_at(self, mid: usize) -> (Self, Self) { + let mut left = vec![]; + let mut right = vec![]; + let mut offset = 0; + for chunk in self.0 { + let len = chunk.string.len(); + if offset >= mid { + right.push(chunk); + } else if offset + len > mid { + let (lchunk, rchunk) = chunk.split_at(mid - offset); + left.push(lchunk); + right.push(rchunk); + } else { + left.push(chunk); + } + offset += len; + } + (Self(left), Self(right)) + } + + pub fn split_at_indices(self, indices: &[usize]) -> Vec { + let mut lines = vec![]; + + let mut rest = self; + let mut offset = 0; + + for i in indices { + let (left, right) = rest.split_at(i - offset); + lines.push(left); + rest = right; + offset = *i; + } + + lines.push(rest); + + lines + } + + pub fn trim_end(&mut self) { + while let Some(last) = self.0.last_mut() { + let trimmed = last.string.trim_end(); + if trimmed.is_empty() { + self.0.pop(); + } else { + last.string = trimmed.to_string(); + break; + } + } + } +} + +impl> From for Styled { + fn from(chunk: C) -> Self { + Self::new(chunk) + } +} diff --git a/src/wrap.rs b/src/wrap.rs index 309214a..1159557 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -75,21 +75,3 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec { breaks } - -pub fn split_at_indices<'a>(s: &'a str, indices: &[usize]) -> Vec<&'a str> { - let mut slices = vec![]; - - let mut rest = s; - let mut offset = 0; - - for i in indices { - let (left, right) = rest.split_at(i - offset); - slices.push(left); - rest = right; - offset = *i; - } - - slices.push(rest); - - slices -}