Use styled chunks of text instead of plain strings
This commit is contained in:
parent
761519c1a7
commit
9b0d80873f
8 changed files with 209 additions and 66 deletions
|
|
@ -6,13 +6,14 @@ use toss::terminal::Terminal;
|
||||||
fn draw(f: &mut Frame) {
|
fn draw(f: &mut Frame) {
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 0),
|
Pos::new(0, 0),
|
||||||
"Hello world!",
|
("Hello world!", ContentStyle::default().green()),
|
||||||
ContentStyle::default().green(),
|
|
||||||
);
|
);
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 1),
|
Pos::new(0, 1),
|
||||||
|
(
|
||||||
"Press any key to exit",
|
"Press any key to exit",
|
||||||
ContentStyle::default().on_dark_blue(),
|
ContentStyle::default().on_dark_blue(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
f.show_cursor(Pos::new(16, 0));
|
f.show_cursor(Pos::new(16, 0));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,53 +7,45 @@ fn draw(f: &mut Frame) {
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 0),
|
Pos::new(0, 0),
|
||||||
"Writing over wide graphemes removes the entire overwritten grapheme.",
|
"Writing over wide graphemes removes the entire overwritten grapheme.",
|
||||||
ContentStyle::default(),
|
|
||||||
);
|
);
|
||||||
let under = ContentStyle::default().white().on_dark_blue();
|
let under = ContentStyle::default().white().on_dark_blue();
|
||||||
let over = ContentStyle::default().black().on_dark_yellow();
|
let over = ContentStyle::default().black().on_dark_yellow();
|
||||||
for i in 0..6 {
|
for i in 0..6 {
|
||||||
let delta = i - 2;
|
let delta = i - 2;
|
||||||
f.write(Pos::new(2 + i * 7, 2), "a😀", under);
|
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, 3), ("a😀", under));
|
||||||
f.write(Pos::new(2 + i * 7, 4), "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, 3), ("b", over));
|
||||||
f.write(Pos::new(2 + i * 7 + delta, 4), "😈", over);
|
f.write(Pos::new(2 + i * 7 + delta, 4), ("😈", over));
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 6),
|
Pos::new(0, 6),
|
||||||
"Wide graphemes at the edges of the screen apply their style, but are not",
|
"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 x1 = -1;
|
||||||
let x2 = f.size().width as i32 / 2 - 3;
|
let x2 = f.size().width as i32 / 2 - 3;
|
||||||
let x3 = f.size().width as i32 - 5;
|
let x3 = f.size().width as i32 - 5;
|
||||||
f.write(Pos::new(x1, 9), "123456", under);
|
f.write(Pos::new(x1, 9), ("123456", under));
|
||||||
f.write(Pos::new(x1, 10), "😀😀😀", under);
|
f.write(Pos::new(x1, 10), ("😀😀😀", under));
|
||||||
f.write(Pos::new(x2, 9), "123456", under);
|
f.write(Pos::new(x2, 9), ("123456", under));
|
||||||
f.write(Pos::new(x2, 10), "😀😀😀", under);
|
f.write(Pos::new(x2, 10), ("😀😀😀", under));
|
||||||
f.write(Pos::new(x3, 9), "123456", under);
|
f.write(Pos::new(x3, 9), ("123456", under));
|
||||||
f.write(Pos::new(x3, 10), "😀😀😀", under);
|
f.write(Pos::new(x3, 10), ("😀😀😀", under));
|
||||||
|
|
||||||
let scientist = "👩🔬";
|
let scientist = "👩🔬";
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 12),
|
Pos::new(0, 12),
|
||||||
"Most terminals ignore the zero width joiner and display this female",
|
"Most terminals ignore the zero width joiner and display this female",
|
||||||
ContentStyle::default(),
|
|
||||||
);
|
);
|
||||||
f.write(
|
f.write(
|
||||||
Pos::new(0, 13),
|
Pos::new(0, 13),
|
||||||
"scientist emoji as a woman and a microscope: 👩🔬",
|
"scientist emoji as a woman and a microscope: 👩🔬",
|
||||||
ContentStyle::default(),
|
|
||||||
);
|
);
|
||||||
for i in 0..(f.width(scientist) + 4) {
|
for i in 0..(f.width(scientist) + 4) {
|
||||||
f.write(Pos::new(2, 15 + i as i32), scientist, under);
|
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(i as i32, 15 + i as i32), ("x", over));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crossterm::event::Event;
|
use crossterm::event::Event;
|
||||||
use crossterm::style::ContentStyle;
|
|
||||||
use toss::frame::{Frame, Pos};
|
use toss::frame::{Frame, Pos};
|
||||||
|
use toss::styled::Styled;
|
||||||
use toss::terminal::Terminal;
|
use toss::terminal::Terminal;
|
||||||
|
|
||||||
fn draw(f: &mut Frame) {
|
fn draw(f: &mut Frame) {
|
||||||
|
|
@ -18,13 +18,10 @@ fn draw(f: &mut Frame) {
|
||||||
);
|
);
|
||||||
|
|
||||||
let breaks = f.wrap(text, f.size().width.into());
|
let breaks = f.wrap(text, f.size().width.into());
|
||||||
let lines = toss::split_at_indices(text, &breaks);
|
let lines = Styled::default().then(text).split_at_indices(&breaks);
|
||||||
for (i, line) in lines.iter().enumerate() {
|
for (i, mut line) in lines.into_iter().enumerate() {
|
||||||
f.write(
|
line.trim_end();
|
||||||
Pos::new(0, i as i32),
|
f.write(Pos::new(0, i as i32), line);
|
||||||
line.trim_end(),
|
|
||||||
ContentStyle::default(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use crossterm::style::ContentStyle;
|
use crossterm::style::ContentStyle;
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::styled::Styled;
|
||||||
use crate::widthdb::WidthDB;
|
use crate::widthdb::WidthDB;
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
@ -127,23 +128,23 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(
|
pub fn write(&mut self, widthdb: &mut WidthDB, mut pos: Pos, styled: &Styled) {
|
||||||
&mut self,
|
|
||||||
widthdb: &mut WidthDB,
|
|
||||||
mut pos: Pos,
|
|
||||||
content: &str,
|
|
||||||
style: ContentStyle,
|
|
||||||
) {
|
|
||||||
// If we're not even visible, there's nothing to do
|
// If we're not even visible, there's nothing to do
|
||||||
if pos.y < 0 || pos.y >= self.size.height as i32 {
|
if pos.y < 0 || pos.y >= self.size.height as i32 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let y = pos.y as u16;
|
let y = pos.y as u16;
|
||||||
|
|
||||||
for grapheme in content.graphemes(true) {
|
for styled_grapheme in styled.styled_graphemes() {
|
||||||
let width = widthdb.grapheme_width(grapheme);
|
let width = widthdb.grapheme_width(styled_grapheme.content());
|
||||||
if width > 0 {
|
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;
|
pos.x += width as i32;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crossterm::style::ContentStyle;
|
||||||
|
|
||||||
use crate::buffer::Buffer;
|
use crate::buffer::Buffer;
|
||||||
pub use crate::buffer::{Pos, Size};
|
pub use crate::buffer::{Pos, Size};
|
||||||
|
use crate::styled::Styled;
|
||||||
use crate::widthdb::WidthDB;
|
use crate::widthdb::WidthDB;
|
||||||
use crate::wrap;
|
use crate::wrap;
|
||||||
|
|
||||||
|
|
@ -60,7 +61,7 @@ impl Frame {
|
||||||
wrap::wrap(text, width, &mut self.widthdb)
|
wrap::wrap(text, width, &mut self.widthdb)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) {
|
pub fn write<S: Into<Styled>>(&mut self, pos: Pos, styled: S) {
|
||||||
self.buffer.write(&mut self.widthdb, pos, content, style);
|
self.buffer.write(&mut self.widthdb, pos, &styled.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
mod buffer;
|
mod buffer;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
|
pub mod styled;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
mod widthdb;
|
mod widthdb;
|
||||||
mod wrap;
|
mod wrap;
|
||||||
|
|
||||||
pub use wrap::split_at_indices;
|
|
||||||
|
|
|
||||||
170
src/styled.rs
Normal file
170
src/styled.rs
Normal file
|
|
@ -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<S: ToString>(string: S, style: ContentStyle) -> Self {
|
||||||
|
Self {
|
||||||
|
string: string.to_string(),
|
||||||
|
style,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn plain<S: ToString>(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<String> for Chunk {
|
||||||
|
fn from(string: String) -> Self {
|
||||||
|
Self::plain(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&String> for Chunk {
|
||||||
|
fn from(string: &String) -> Self {
|
||||||
|
Self::plain(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ToString> From<(S,)> for Chunk {
|
||||||
|
fn from(tuple: (S,)) -> Self {
|
||||||
|
Self::plain(tuple.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: ToString> 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<Chunk>);
|
||||||
|
|
||||||
|
impl Styled {
|
||||||
|
pub fn new<C: Into<Chunk>>(chunk: C) -> Self {
|
||||||
|
Self::default().then(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn then<C: Into<Chunk>>(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<Item = &str> {
|
||||||
|
self.0.iter().flat_map(|c| c.string.graphemes(true))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grapheme_indices(&self) -> impl Iterator<Item = (usize, &str)> {
|
||||||
|
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<Item = StyledContent<&str>> {
|
||||||
|
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<Self> {
|
||||||
|
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<C: Into<Chunk>> From<C> for Styled {
|
||||||
|
fn from(chunk: C) -> Self {
|
||||||
|
Self::new(chunk)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/wrap.rs
18
src/wrap.rs
|
|
@ -75,21 +75,3 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec<usize> {
|
||||||
|
|
||||||
breaks
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue