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 std::io;
use crossterm::style::{ContentStyle, Stylize}; use crossterm::style::{ContentStyle, Stylize};
use toss::frame::Pos; use toss::frame::{Frame, Pos};
use toss::terminal::Terminal; use toss::terminal::Terminal;
fn main() -> io::Result<()> { fn draw(f: &mut Frame) {
// 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();
f.write( f.write(
Pos::new(0, 0), Pos::new(0, 0),
"Hello world!", "Hello world!",
@ -25,9 +16,23 @@ fn main() -> io::Result<()> {
ContentStyle::default().on_dark_blue(), ContentStyle::default().on_dark_blue(),
); );
f.show_cursor(Pos::new(16, 0)); f.show_cursor(Pos::new(16, 0));
}
// Show the next frame on the screen fn main() -> io::Result<()> {
term.present()?; // 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 // Wait for input before exiting
let _ = crossterm::event::read(); let _ = crossterm::event::read();

View file

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

View file

@ -2,9 +2,11 @@ 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::widthdb::WidthDB;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Frame { pub struct Frame {
pub(crate) widthdb: WidthDB,
pub(crate) buffer: Buffer, pub(crate) buffer: Buffer,
cursor: Option<Pos>, cursor: Option<Pos>,
} }
@ -35,7 +37,11 @@ impl Frame {
self.set_cursor(None); 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) { 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; mod buffer;
pub mod frame; pub mod frame;
pub mod terminal; 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. /// 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 /// After calling this function, the frame returned by [`Self::frame`] will
/// be empty again and have no cursor position. /// 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 { if self.full_redraw {
io::stdout().queue(Clear(ClearType::All))?; io::stdout().queue(Clear(ClearType::All))?;
self.prev_frame_buffer.reset(); // Because the screen is now empty 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); mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer);
self.frame.reset(); self.frame.reset();
Ok(()) Ok(false)
} }
fn draw_differences(&mut self) -> io::Result<()> { 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(())
}
}