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,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();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
mod buffer;
|
mod buffer;
|
||||||
pub mod frame;
|
pub mod frame;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
mod widthdb;
|
||||||
|
|
|
||||||
|
|
@ -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
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