Measure actual width of displayed characters
This commit is contained in:
parent
add2f25aba
commit
9512ddaa3b
6 changed files with 97 additions and 19 deletions
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
mod buffer;
|
||||
pub mod frame;
|
||||
pub mod terminal;
|
||||
mod widthdb;
|
||||
|
|
|
|||
|
|
@ -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
46
src/widthdb.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue