Measure actual width of displayed characters

This commit is contained in:
Joscha 2022-05-21 23:32:15 +02:00
parent add2f25aba
commit 9512ddaa3b
6 changed files with 97 additions and 19 deletions

View file

@ -1,19 +1,10 @@
use std::io;
use crossterm::style::{ContentStyle, Stylize};
use toss::frame::Pos;
use toss::frame::{Frame, Pos};
use toss::terminal::Terminal;
fn main() -> io::Result<()> {
// Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new()?;
// Must be called before rendering, otherwise the terminal has out-of-date
// size information and will present garbage.
term.autoresize()?;
// Render stuff onto the next frame
let f = term.frame();
fn draw(f: &mut Frame) {
f.write(
Pos::new(0, 0),
"Hello world!",
@ -25,9 +16,23 @@ fn main() -> io::Result<()> {
ContentStyle::default().on_dark_blue(),
);
f.show_cursor(Pos::new(16, 0));
}
// Show the next frame on the screen
term.present()?;
fn main() -> io::Result<()> {
// Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new()?;
loop {
// Must be called before rendering, otherwise the terminal has out-of-date
// size information and will present garbage.
term.autoresize()?;
draw(term.frame());
if !term.present()? {
break;
}
}
// Wait for input before exiting
let _ = crossterm::event::read();

View file

@ -1,6 +1,7 @@
use crossterm::style::ContentStyle;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::widthdb::WidthDB;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Size {
@ -95,13 +96,19 @@ impl Buffer {
self.data.fill_with(Cell::empty);
}
pub fn write(&mut self, mut pos: Pos, content: &str, style: ContentStyle) {
pub fn write(
&mut self,
widthdb: &mut WidthDB,
mut pos: Pos,
content: &str,
style: ContentStyle,
) {
if pos.y < 0 || pos.y >= self.size.height as i32 {
return;
}
for grapheme in content.graphemes(true) {
let width = grapheme.width().max(1) as u8; // TODO Use actual width
let width = widthdb.width(grapheme);
if pos.x >= 0 && pos.x + width as i32 <= self.size.width as i32 {
// Grapheme fits on buffer in its entirety
let grapheme = grapheme.to_string().into_boxed_str();

View file

@ -2,9 +2,11 @@ use crossterm::style::ContentStyle;
use crate::buffer::Buffer;
pub use crate::buffer::{Pos, Size};
use crate::widthdb::WidthDB;
#[derive(Debug, Default)]
pub struct Frame {
pub(crate) widthdb: WidthDB,
pub(crate) buffer: Buffer,
cursor: Option<Pos>,
}
@ -35,7 +37,11 @@ impl Frame {
self.set_cursor(None);
}
pub fn width(&mut self, s: &str) -> u8 {
self.widthdb.width(s)
}
pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) {
self.buffer.write(pos, content, style);
self.buffer.write(&mut self.widthdb, pos, content, style);
}
}

View file

@ -1,3 +1,4 @@
mod buffer;
pub mod frame;
pub mod terminal;
mod widthdb;

View file

@ -64,10 +64,23 @@ impl Terminal {
}
/// Display the current frame on the screen and prepare the next frame.
/// Returns `true` if an immediate redraw is required.
///
/// After calling this function, the frame returned by [`Self::frame`] will
/// be empty again and have no cursor position.
pub fn present(&mut self) -> io::Result<()> {
pub fn present(&mut self) -> io::Result<bool> {
if self.frame.widthdb.measuring_required() {
self.frame.widthdb.measure_widths(&mut self.out)?;
// Since we messed up the screen by measuring widths, we'll need to
// do a full redraw the next time around.
self.full_redraw = true;
// Throwing away the current frame because its content were rendered
// with unconfirmed width data. Also, this function guarantees that
// after it is called, the frame is empty.
self.frame.reset();
return Ok(true);
}
if self.full_redraw {
io::stdout().queue(Clear(ClearType::All))?;
self.prev_frame_buffer.reset(); // Because the screen is now empty
@ -81,7 +94,7 @@ impl Terminal {
mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer);
self.frame.reset();
Ok(())
Ok(false)
}
fn draw_differences(&mut self) -> io::Result<()> {

46
src/widthdb.rs Normal file
View file

@ -0,0 +1,46 @@
use std::collections::{HashMap, HashSet};
use std::io::{self, Write};
use crossterm::cursor::MoveTo;
use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType};
use crossterm::QueueableCommand;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Default)]
pub struct WidthDB {
known: HashMap<String, u8>,
requested: HashSet<String>,
}
impl WidthDB {
pub fn width(&mut self, s: &str) -> u8 {
let mut total = 0;
for grapheme in s.graphemes(true) {
total += if let Some(width) = self.known.get(grapheme) {
*width
} else {
self.requested.insert(grapheme.to_string());
grapheme.width() as u8
};
}
total
}
pub fn measuring_required(&self) -> bool {
!self.requested.is_empty()
}
pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> {
for grapheme in self.requested.drain() {
out.queue(Clear(ClearType::All))?
.queue(MoveTo(0, 0))?
.queue(Print(&grapheme))?;
out.flush()?;
let width = crossterm::cursor::position()?.0.max(1) as u8;
self.known.insert(grapheme, width);
}
Ok(())
}
}