Compare commits
1 commit
master
...
measure-wi
| Author | SHA1 | Date | |
|---|---|---|---|
| e74cd39047 |
36 changed files with 77 additions and 4675 deletions
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"files.insertFinalNewline": true,
|
||||
"rust-analyzer.cargo.features": "all",
|
||||
"rust-analyzer.imports.granularity.enforce": true,
|
||||
"rust-analyzer.imports.granularity.group": "module",
|
||||
"rust-analyzer.imports.group.enable": true,
|
||||
"evenBetterToml.formatter.columnWidth": 100,
|
||||
}
|
||||
70
CHANGELOG.md
70
CHANGELOG.md
|
|
@ -1,70 +0,0 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
Procedure when bumping the version number:
|
||||
1. Update dependencies in a separate commit
|
||||
2. Set version number in `Cargo.toml`
|
||||
3. Add new section in this changelog
|
||||
4. Commit with message `Bump version to X.Y.Z`
|
||||
5. Create tag named `vX.Y.Z`
|
||||
6. Push `master` and the new tag
|
||||
|
||||
## Unreleased
|
||||
|
||||
## v0.3.4 - 2025-03-8
|
||||
|
||||
### Added
|
||||
- `Frame::set_bell` to print a bell character when the frame is displayed
|
||||
- `widgets::bell`
|
||||
|
||||
## v0.3.3 - 2025-02-28
|
||||
|
||||
### Fixed
|
||||
- Rendering glitches in unicode-based with estimation
|
||||
|
||||
## v0.3.2 - 2025-02-23
|
||||
|
||||
### Added
|
||||
- Unicode-based grapheme width estimation method
|
||||
|
||||
## v0.3.1 - 2025-02-21
|
||||
|
||||
### Fixed
|
||||
- Rendering glitches, mainly related to emoji
|
||||
|
||||
## v0.3.0 - 2024-11-06
|
||||
|
||||
### Added
|
||||
- `Terminal::mark_dirty`
|
||||
|
||||
### Changed
|
||||
- **(breaking)** Updated dependencies
|
||||
|
||||
## v0.2.3 - 2024-04-25
|
||||
|
||||
### Fixed
|
||||
- Width measurements of ASCII control characters
|
||||
- Toss messing up the terminal state
|
||||
|
||||
## v0.2.2 - 2024-01-14
|
||||
|
||||
### Fixed
|
||||
- Crash when drawing `widgets::Predrawn` with width 0
|
||||
|
||||
## v0.2.1 - 2024-01-05
|
||||
|
||||
### Added
|
||||
- `Frame::set_title`
|
||||
- `WidgetExt::title`
|
||||
- `widgets::title`
|
||||
|
||||
## v0.2.0 - 2023-08-31
|
||||
|
||||
### Changed
|
||||
- **(breaking)** Updated dependencies
|
||||
|
||||
## v0.1.0 - 2023-05-14
|
||||
|
||||
Initial release
|
||||
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,11 +1,11 @@
|
|||
[package]
|
||||
name = "toss"
|
||||
version = "0.3.4"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.83"
|
||||
crossterm = "0.28.1"
|
||||
unicode-linebreak = "0.1.5"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.2.0"
|
||||
anyhow = "1.0.57"
|
||||
crossterm = "0.23.2"
|
||||
unicode-blocks = "0.1.4"
|
||||
unicode-segmentation = "1.9.0"
|
||||
unicode-width = "0.1.9"
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
use crossterm::event::Event;
|
||||
use crossterm::style::Stylize;
|
||||
use toss::{Frame, Pos, Style, Terminal};
|
||||
|
||||
fn draw(f: &mut Frame) {
|
||||
f.write(Pos::new(0, 0), ("Hello world!", Style::new().green()));
|
||||
f.write(
|
||||
Pos::new(0, 1),
|
||||
("Press any key to exit", Style::new().on_dark_blue()),
|
||||
);
|
||||
f.show_cursor(Pos::new(16, 0));
|
||||
}
|
||||
|
||||
fn render_frame(term: &mut Terminal) {
|
||||
let mut dirty = true;
|
||||
while dirty {
|
||||
term.autoresize().unwrap();
|
||||
draw(term.frame());
|
||||
term.present().unwrap();
|
||||
dirty = term.measure_widths().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Automatically enters alternate screen and enables raw mode
|
||||
let mut term = Terminal::new().unwrap();
|
||||
term.set_measuring(true);
|
||||
|
||||
loop {
|
||||
// Render and display a frame. A full frame is displayed on the terminal
|
||||
// once this function exits.
|
||||
render_frame(&mut term);
|
||||
|
||||
// Exit if the user presses any buttons
|
||||
if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
use std::io;
|
||||
|
||||
use crossterm::event::Event;
|
||||
use crossterm::style::Stylize;
|
||||
use toss::widgets::{BorderLook, Text};
|
||||
use toss::{Style, Styled, Terminal, Widget, WidgetExt};
|
||||
|
||||
fn widget() -> impl Widget<io::Error> {
|
||||
let styled = Styled::new("Hello world!", Style::new().dark_green())
|
||||
.then_plain("\n")
|
||||
.then("Press any key to exit", Style::new().on_dark_blue());
|
||||
Text::new(styled)
|
||||
.padding()
|
||||
.with_horizontal(1)
|
||||
.border()
|
||||
.with_look(BorderLook::LINE_DOUBLE)
|
||||
.with_style(Style::new().dark_red())
|
||||
.background()
|
||||
.with_style(Style::new().on_yellow().opaque())
|
||||
.float()
|
||||
.with_all(0.5)
|
||||
}
|
||||
|
||||
fn render_frame(term: &mut Terminal) {
|
||||
let mut dirty = true;
|
||||
while dirty {
|
||||
term.present_widget(widget()).unwrap();
|
||||
dirty = term.measure_widths().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Automatically enters alternate screen and enables raw mode
|
||||
let mut term = Terminal::new().unwrap();
|
||||
term.set_measuring(true);
|
||||
|
||||
loop {
|
||||
// Render and display a frame. A full frame is displayed on the terminal
|
||||
// once this function exits.
|
||||
render_frame(&mut term);
|
||||
|
||||
// Exit if the user presses any buttons
|
||||
if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
use crossterm::event::Event;
|
||||
use crossterm::style::Stylize;
|
||||
use toss::{Frame, Pos, Style, Terminal};
|
||||
|
||||
fn draw(f: &mut Frame) {
|
||||
f.write(
|
||||
Pos::new(0, 0),
|
||||
"Writing over wide graphemes removes the entire overwritten grapheme.",
|
||||
);
|
||||
let under = Style::new().white().on_dark_blue();
|
||||
let over = Style::new().black().on_dark_yellow();
|
||||
for i in 0..6 {
|
||||
let delta = i - 2;
|
||||
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, 4), ("a😀", under));
|
||||
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(0, 6),
|
||||
"Wide graphemes at the edges of the screen apply their style, but are not",
|
||||
);
|
||||
f.write(Pos::new(0, 7), "actually rendered.");
|
||||
let x1 = -1;
|
||||
let x2 = f.size().width as i32 / 2 - 3;
|
||||
let x3 = f.size().width as i32 - 5;
|
||||
f.write(Pos::new(x1, 9), ("123456", under));
|
||||
f.write(Pos::new(x1, 10), ("😀😀😀", under));
|
||||
f.write(Pos::new(x2, 9), ("123456", under));
|
||||
f.write(Pos::new(x2, 10), ("😀😀😀", under));
|
||||
f.write(Pos::new(x3, 9), ("123456", under));
|
||||
f.write(Pos::new(x3, 10), ("😀😀😀", under));
|
||||
|
||||
let scientist = "👩🔬";
|
||||
f.write(
|
||||
Pos::new(0, 12),
|
||||
"Most terminals ignore the zero width joiner and display this female",
|
||||
);
|
||||
f.write(
|
||||
Pos::new(0, 13),
|
||||
"scientist emoji as a woman and a microscope: 👩🔬",
|
||||
);
|
||||
for i in 0..(f.widthdb().width(scientist) + 4) {
|
||||
f.write(Pos::new(2, 15 + i as i32), (scientist, under));
|
||||
f.write(Pos::new(i as i32, 15 + i as i32), ("x", over));
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frame(term: &mut Terminal) {
|
||||
let mut dirty = true;
|
||||
while dirty {
|
||||
term.autoresize().unwrap();
|
||||
draw(term.frame());
|
||||
term.present().unwrap();
|
||||
dirty = term.measure_widths().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Automatically enters alternate screen and enables raw mode
|
||||
let mut term = Terminal::new().unwrap();
|
||||
term.set_measuring(true);
|
||||
|
||||
loop {
|
||||
// Render and display a frame. A full frame is displayed on the terminal
|
||||
// once this function exits.
|
||||
render_frame(&mut term);
|
||||
|
||||
// Exit if the user presses any buttons
|
||||
if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
use crossterm::event::Event;
|
||||
use toss::{Frame, Pos, Styled, Terminal};
|
||||
|
||||
fn draw(f: &mut Frame) {
|
||||
let text = concat!(
|
||||
"This is a short paragraph in order to demonstrate unicode-aware word wrapping. ",
|
||||
"Resize your terminal to different widths to try it out. ",
|
||||
"After this sentence come two newlines, so it should always break here.\n",
|
||||
"\n",
|
||||
"Since the wrapping algorithm is aware of the Unicode Standard Annex #14, ",
|
||||
"it understands things like non-breaking spaces and word joiners: ",
|
||||
"This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n",
|
||||
"\n",
|
||||
"It can also properly handle wide graphemes (like emoji 🤔), ",
|
||||
"including ones usually displayed incorrectly by terminal emulators, like 👩🔬 (a female scientist emoji).\n",
|
||||
"\n",
|
||||
"Finally, tabs are supported as well. ",
|
||||
"The following text is rendered with a tab width of 4:\n",
|
||||
"\tx\n",
|
||||
"1\tx\n",
|
||||
"12\tx\n",
|
||||
"123\tx\n",
|
||||
"1234\tx\n",
|
||||
"12345\tx\n",
|
||||
"123456\tx\n",
|
||||
"1234567\tx\n",
|
||||
"12345678\tx\n",
|
||||
"123456789\tx\n",
|
||||
);
|
||||
|
||||
let width = f.size().width.into();
|
||||
let breaks = f.widthdb().wrap(text, width);
|
||||
let lines = Styled::new_plain(text).split_at_indices(&breaks);
|
||||
for (i, mut line) in lines.into_iter().enumerate() {
|
||||
line.trim_end();
|
||||
f.write(Pos::new(0, i as i32), line);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_frame(term: &mut Terminal) {
|
||||
let mut dirty = true;
|
||||
while dirty {
|
||||
term.autoresize().unwrap();
|
||||
draw(term.frame());
|
||||
term.present().unwrap();
|
||||
dirty = term.measure_widths().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Automatically enters alternate screen and enables raw mode
|
||||
let mut term = Terminal::new().unwrap();
|
||||
term.set_measuring(true);
|
||||
term.set_tab_width(4);
|
||||
|
||||
loop {
|
||||
// Render and display a frame. A full frame is displayed on the terminal
|
||||
// once this function exits.
|
||||
render_frame(&mut term);
|
||||
|
||||
// Exit if the user presses any buttons
|
||||
if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
354
src/buffer.rs
354
src/buffer.rs
|
|
@ -1,354 +0,0 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use crossterm::style::ContentStyle;
|
||||
|
||||
use crate::{Pos, Size, Style, Styled, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
pub content: Box<str>,
|
||||
pub style: ContentStyle,
|
||||
pub width: u8,
|
||||
pub offset: u8,
|
||||
}
|
||||
|
||||
impl Default for Cell {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content: " ".to_string().into_boxed_str(),
|
||||
style: ContentStyle::default(),
|
||||
width: 1,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct StackFrame {
|
||||
pub pos: Pos,
|
||||
pub size: Size,
|
||||
pub drawable_area: Option<(Pos, Size)>,
|
||||
}
|
||||
|
||||
impl StackFrame {
|
||||
fn intersect_areas(
|
||||
a_start: Pos,
|
||||
a_size: Size,
|
||||
b_start: Pos,
|
||||
b_size: Size,
|
||||
) -> Option<(Pos, Size)> {
|
||||
// The first row/column that is not part of the area any more
|
||||
let a_end = a_start + a_size;
|
||||
let b_end = b_start + b_size;
|
||||
|
||||
let x_start = a_start.x.max(b_start.x);
|
||||
let x_end = a_end.x.min(b_end.x);
|
||||
let y_start = a_start.y.max(b_start.y);
|
||||
let y_end = a_end.y.min(b_end.y);
|
||||
|
||||
if x_start < x_end && y_start < y_end {
|
||||
let start = Pos::new(x_start, y_start);
|
||||
let size = Size::new((x_end - x_start) as u16, (y_end - y_start) as u16);
|
||||
Some((start, size))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn then(&self, pos: Pos, size: Size) -> Self {
|
||||
let pos = self.local_to_global(pos);
|
||||
|
||||
let drawable_area = self
|
||||
.drawable_area
|
||||
.and_then(|(da_pos, da_size)| Self::intersect_areas(da_pos, da_size, pos, size));
|
||||
|
||||
Self {
|
||||
pos,
|
||||
size,
|
||||
drawable_area,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_to_global(&self, local_pos: Pos) -> Pos {
|
||||
local_pos + self.pos
|
||||
}
|
||||
|
||||
fn global_to_local(&self, global_pos: Pos) -> Pos {
|
||||
global_pos - self.pos
|
||||
}
|
||||
|
||||
/// Ranges along the x and y axis where drawing is allowed, in global
|
||||
/// coordinates.
|
||||
fn legal_ranges(&self) -> Option<(Range<i32>, Range<i32>)> {
|
||||
if let Some((pos, size)) = self.drawable_area {
|
||||
let xrange = pos.x..pos.x + size.width as i32;
|
||||
let yrange = pos.y..pos.y + size.height as i32;
|
||||
Some((xrange, yrange))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Buffer {
|
||||
size: Size,
|
||||
data: Vec<Cell>,
|
||||
cursor: Option<Pos>,
|
||||
|
||||
/// A stack of rectangular drawing areas.
|
||||
///
|
||||
/// When rendering to the buffer with a nonempty stack, it behaves as if it
|
||||
/// was the size of the topmost stack element, and characters are translated
|
||||
/// by the position of the topmost stack element. No characters can be
|
||||
/// placed outside the area described by the topmost stack element.
|
||||
stack: Vec<StackFrame>,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Index in `data` of the cell at the given position. The position must
|
||||
/// be inside the buffer.
|
||||
///
|
||||
/// Ignores the stack.
|
||||
fn index(&self, x: u16, y: u16) -> usize {
|
||||
assert!(x < self.size.width);
|
||||
assert!(y < self.size.height);
|
||||
|
||||
let x: usize = x.into();
|
||||
let y: usize = y.into();
|
||||
let width: usize = self.size.width.into();
|
||||
|
||||
y * width + x
|
||||
}
|
||||
|
||||
/// A reference to the cell at the given position. The position must be
|
||||
/// inside the buffer.
|
||||
///
|
||||
/// Ignores the stack.
|
||||
pub fn at(&self, x: u16, y: u16) -> &Cell {
|
||||
assert!(x < self.size.width);
|
||||
assert!(y < self.size.height);
|
||||
|
||||
let i = self.index(x, y);
|
||||
&self.data[i]
|
||||
}
|
||||
|
||||
/// A mutable reference to the cell at the given position. The position must
|
||||
/// be inside the buffer.
|
||||
///
|
||||
/// Ignores the stack.
|
||||
fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||
assert!(x < self.size.width);
|
||||
assert!(y < self.size.height);
|
||||
|
||||
let i = self.index(x, y);
|
||||
&mut self.data[i]
|
||||
}
|
||||
|
||||
fn current_frame(&self) -> StackFrame {
|
||||
self.stack.last().copied().unwrap_or(StackFrame {
|
||||
pos: Pos::ZERO,
|
||||
size: self.size,
|
||||
drawable_area: Some((Pos::ZERO, self.size)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn push(&mut self, pos: Pos, size: Size) {
|
||||
self.stack.push(self.current_frame().then(pos, size));
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) {
|
||||
self.stack.pop();
|
||||
}
|
||||
|
||||
/// Size of the current drawable area, respecting the stack.
|
||||
pub fn size(&self) -> Size {
|
||||
self.current_frame().size
|
||||
}
|
||||
|
||||
pub fn cursor(&self) -> Option<Pos> {
|
||||
self.cursor.map(|p| self.current_frame().global_to_local(p))
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, pos: Option<Pos>) {
|
||||
self.cursor = pos.map(|p| self.current_frame().local_to_global(p));
|
||||
}
|
||||
|
||||
/// Resize the buffer and reset its contents.
|
||||
///
|
||||
/// The buffer's contents are reset even if the buffer is already the
|
||||
/// correct size. The stack is reset as well.
|
||||
pub fn resize(&mut self, size: Size) {
|
||||
if size == self.size {
|
||||
self.data.fill_with(Cell::default);
|
||||
} else {
|
||||
let width: usize = size.width.into();
|
||||
let height: usize = size.height.into();
|
||||
let len = width * height;
|
||||
|
||||
self.size = size;
|
||||
self.data.clear();
|
||||
self.data.resize_with(len, Cell::default);
|
||||
}
|
||||
|
||||
self.cursor = None;
|
||||
|
||||
self.stack.clear();
|
||||
}
|
||||
|
||||
/// Reset the contents and stack of the buffer.
|
||||
///
|
||||
/// `buf.reset()` is equivalent to `buf.resize(buf.size())`.
|
||||
pub fn reset(&mut self) {
|
||||
self.resize(self.size);
|
||||
}
|
||||
|
||||
/// Remove the grapheme at the specified coordinates from the buffer.
|
||||
///
|
||||
/// Removes the entire grapheme, not just the cell at the coordinates.
|
||||
/// Preserves the style of the affected cells. Preserves the cursor. Works
|
||||
/// even if the coordinates don't point to the beginning of the grapheme.
|
||||
///
|
||||
/// Ignores the stack.
|
||||
fn erase(&mut self, x: u16, y: u16) {
|
||||
let cell = self.at(x, y);
|
||||
let width: u16 = cell.width.into();
|
||||
let offset: u16 = cell.offset.into();
|
||||
|
||||
for x in (x - offset)..(x - offset + width) {
|
||||
let cell = self.at_mut(x, y);
|
||||
let style = cell.style;
|
||||
|
||||
*cell = Cell::default();
|
||||
cell.style = style;
|
||||
}
|
||||
}
|
||||
|
||||
/// Write styled text to the buffer, respecting the width of individual
|
||||
/// graphemes.
|
||||
///
|
||||
/// The initial x position is considered the first column for tab width
|
||||
/// calculations.
|
||||
pub fn write(&mut self, widthdb: &mut WidthDb, pos: Pos, styled: &Styled) {
|
||||
let frame = self.current_frame();
|
||||
let (xrange, yrange) = match frame.legal_ranges() {
|
||||
Some(ranges) => ranges,
|
||||
None => return, // No drawable area
|
||||
};
|
||||
let pos = frame.local_to_global(pos);
|
||||
if !yrange.contains(&pos.y) {
|
||||
return; // Outside of drawable area
|
||||
}
|
||||
let y = pos.y as u16;
|
||||
|
||||
let mut col: usize = 0;
|
||||
for (_, style, grapheme) in styled.styled_grapheme_indices() {
|
||||
let x = pos.x + col as i32;
|
||||
let width = widthdb.grapheme_width(grapheme, col);
|
||||
col += width as usize;
|
||||
if grapheme == "\t" {
|
||||
for dx in 0..width {
|
||||
self.write_grapheme(&xrange, x + dx as i32, y, 1, " ", style);
|
||||
}
|
||||
} else if width > 0 {
|
||||
self.write_grapheme(&xrange, x, y, width, grapheme, style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a single grapheme to the buffer, respecting its width.
|
||||
///
|
||||
/// Assumes that `pos.y` is in range.
|
||||
fn write_grapheme(
|
||||
&mut self,
|
||||
xrange: &Range<i32>,
|
||||
x: i32,
|
||||
y: u16,
|
||||
width: u8,
|
||||
grapheme: &str,
|
||||
style: Style,
|
||||
) {
|
||||
let min_x = xrange.start;
|
||||
let max_x = xrange.end - 1; // Last possible cell
|
||||
|
||||
let start_x = x;
|
||||
let end_x = x + width as i32 - 1; // Coordinate of last cell
|
||||
|
||||
if start_x > max_x || end_x < min_x {
|
||||
return; // Not visible
|
||||
}
|
||||
|
||||
if start_x >= min_x && end_x <= max_x {
|
||||
// Fully visible, write actual grapheme
|
||||
let base_style = self.at(start_x as u16, y).style;
|
||||
for offset in 0..width {
|
||||
let x = start_x as u16 + offset as u16;
|
||||
self.erase(x, y);
|
||||
*self.at_mut(x, y) = Cell {
|
||||
content: grapheme.to_string().into_boxed_str(),
|
||||
style: style.cover(base_style),
|
||||
width,
|
||||
offset,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Partially visible, write empty cells with correct style
|
||||
let start_x = start_x.max(0) as u16;
|
||||
let end_x = end_x.min(max_x) as u16;
|
||||
for x in start_x..=end_x {
|
||||
let base_style = self.at(x, y).style;
|
||||
self.erase(x, y);
|
||||
*self.at_mut(x, y) = Cell {
|
||||
style: style.cover(base_style),
|
||||
..Default::default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pos) = self.cursor {
|
||||
if pos.y == y as i32 && start_x <= pos.x && pos.x <= end_x {
|
||||
// The cursor lies within the bounds of the current grapheme and
|
||||
self.cursor = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cells(&self) -> Cells<'_> {
|
||||
Cells {
|
||||
buffer: self,
|
||||
x: 0,
|
||||
y: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Cells<'a> {
|
||||
buffer: &'a Buffer,
|
||||
x: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Cells<'a> {
|
||||
type Item = (u16, u16, &'a Cell);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.x >= self.buffer.size.width {
|
||||
return None;
|
||||
}
|
||||
if self.y >= self.buffer.size.height {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (x, y) = (self.x, self.y);
|
||||
let cell = self.buffer.at(self.x, self.y);
|
||||
assert!(cell.offset == 0);
|
||||
|
||||
self.x += cell.width as u16;
|
||||
if self.x >= self.buffer.size.width {
|
||||
self.x = 0;
|
||||
self.y += 1;
|
||||
}
|
||||
|
||||
Some((x, y, cell))
|
||||
}
|
||||
}
|
||||
153
src/coords.rs
153
src/coords.rs
|
|
@ -1,153 +0,0 @@
|
|||
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||
|
||||
/// Size in screen cells.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Size {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
impl Size {
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
pub const fn new(width: u16, height: u16) -> Self {
|
||||
Self { width, height }
|
||||
}
|
||||
|
||||
/// Add two [`Size`]s using [`u16::saturating_add`].
|
||||
pub const fn saturating_add(self, rhs: Self) -> Self {
|
||||
Self::new(
|
||||
self.width.saturating_add(rhs.width),
|
||||
self.height.saturating_add(rhs.height),
|
||||
)
|
||||
}
|
||||
|
||||
/// Subtract two [`Size`]s using [`u16::saturating_sub`].
|
||||
pub const fn saturating_sub(self, rhs: Self) -> Self {
|
||||
Self::new(
|
||||
self.width.saturating_sub(rhs.width),
|
||||
self.height.saturating_sub(rhs.height),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Size {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Self::new(self.width + rhs.width, self.height + rhs.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Size {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.width += rhs.width;
|
||||
self.height += rhs.height;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Size {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self {
|
||||
Self::new(self.width - rhs.width, self.height - rhs.height)
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for Size {
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
self.width -= rhs.width;
|
||||
self.height -= rhs.height;
|
||||
}
|
||||
}
|
||||
|
||||
/// Position in screen cell coordinates.
|
||||
///
|
||||
/// The x axis points to the right. The y axis points down.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Pos {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Pos {
|
||||
pub const ZERO: Self = Self::new(0, 0);
|
||||
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Size> for Pos {
|
||||
fn from(s: Size) -> Self {
|
||||
Self::new(s.width.into(), s.height.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Add for Pos {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self {
|
||||
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Size> for Pos {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Size) -> Self {
|
||||
Self::new(self.x + rhs.width as i32, self.y + rhs.height as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Pos {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.x += rhs.x;
|
||||
self.y += rhs.y;
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign<Size> for Pos {
|
||||
fn add_assign(&mut self, rhs: Size) {
|
||||
self.x += rhs.width as i32;
|
||||
self.y += rhs.height as i32;
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for Pos {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self {
|
||||
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Size> for Pos {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Size) -> Self {
|
||||
Self::new(self.x - rhs.width as i32, self.y - rhs.height as i32)
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign for Pos {
|
||||
fn sub_assign(&mut self, rhs: Self) {
|
||||
self.x -= rhs.x;
|
||||
self.y -= rhs.y;
|
||||
}
|
||||
}
|
||||
|
||||
impl SubAssign<Size> for Pos {
|
||||
fn sub_assign(&mut self, rhs: Size) {
|
||||
self.x -= rhs.width as i32;
|
||||
self.y -= rhs.height as i32;
|
||||
}
|
||||
}
|
||||
|
||||
impl Neg for Pos {
|
||||
type Output = Self;
|
||||
|
||||
fn neg(self) -> Self {
|
||||
Self::new(-self.x, -self.y)
|
||||
}
|
||||
}
|
||||
63
src/frame.rs
63
src/frame.rs
|
|
@ -1,63 +0,0 @@
|
|||
//! Rendering the next frame.
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::{Pos, Size, Styled, WidthDb};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Frame {
|
||||
pub(crate) widthdb: WidthDb,
|
||||
pub(crate) buffer: Buffer,
|
||||
pub(crate) title: Option<String>,
|
||||
pub(crate) bell: bool,
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn push(&mut self, pos: Pos, size: Size) {
|
||||
self.buffer.push(pos, size);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) {
|
||||
self.buffer.pop();
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Size {
|
||||
self.buffer.size()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.buffer.reset();
|
||||
self.title = None;
|
||||
}
|
||||
|
||||
pub fn cursor(&self) -> Option<Pos> {
|
||||
self.buffer.cursor()
|
||||
}
|
||||
|
||||
pub fn set_cursor(&mut self, pos: Option<Pos>) {
|
||||
self.buffer.set_cursor(pos);
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self, pos: Pos) {
|
||||
self.set_cursor(Some(pos));
|
||||
}
|
||||
|
||||
pub fn hide_cursor(&mut self) {
|
||||
self.set_cursor(None);
|
||||
}
|
||||
|
||||
pub fn set_title(&mut self, title: Option<String>) {
|
||||
self.title = title;
|
||||
}
|
||||
|
||||
pub fn set_bell(&mut self, bell: bool) {
|
||||
self.bell = bell;
|
||||
}
|
||||
|
||||
pub fn widthdb(&mut self) -> &mut WidthDb {
|
||||
&mut self.widthdb
|
||||
}
|
||||
|
||||
pub fn write<S: Into<Styled>>(&mut self, pos: Pos, styled: S) {
|
||||
self.buffer.write(&mut self.widthdb, pos, &styled.into());
|
||||
}
|
||||
}
|
||||
37
src/lib.rs
37
src/lib.rs
|
|
@ -1,29 +1,8 @@
|
|||
#![forbid(unsafe_code)]
|
||||
// Rustc lint groups
|
||||
#![warn(future_incompatible)]
|
||||
#![warn(rust_2018_idioms)]
|
||||
#![warn(unused)]
|
||||
// Rustc lints
|
||||
#![warn(noop_method_call)]
|
||||
#![warn(single_use_lifetimes)]
|
||||
// Clippy lints
|
||||
#![warn(clippy::use_self)]
|
||||
|
||||
mod buffer;
|
||||
mod coords;
|
||||
mod frame;
|
||||
mod style;
|
||||
mod styled;
|
||||
mod terminal;
|
||||
mod widget;
|
||||
pub mod widgets;
|
||||
mod widthdb;
|
||||
mod wrap;
|
||||
|
||||
pub use coords::*;
|
||||
pub use frame::*;
|
||||
pub use style::*;
|
||||
pub use styled::*;
|
||||
pub use terminal::*;
|
||||
pub use widget::*;
|
||||
pub use widthdb::*;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = 2 + 2;
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
src/main.rs
Normal file
63
src/main.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{io, slice};
|
||||
|
||||
use crossterm::cursor::{self, MoveTo};
|
||||
use crossterm::execute;
|
||||
use crossterm::style::Print;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use unicode_blocks::UnicodeBlock;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
struct WidthDB(HashMap<String, u8>);
|
||||
|
||||
impl WidthDB {
|
||||
fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
fn measure(&mut self, s: &str) -> anyhow::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
for grapheme in s.graphemes(true) {
|
||||
execute!(stdout, EnterAlternateScreen, MoveTo(0, 0), Print(grapheme))?;
|
||||
let width = cursor::position()?.0 as u8;
|
||||
if width != grapheme.width() as u8 {
|
||||
self.0.insert(grapheme.to_string(), width);
|
||||
}
|
||||
execute!(stdout, LeaveAlternateScreen)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn measure_block(&mut self, block: UnicodeBlock) -> anyhow::Result<()> {
|
||||
for c in block.start()..=block.end() {
|
||||
let c = char::from_u32(c).unwrap();
|
||||
let s = c.to_string();
|
||||
self.measure(&s)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut widthdb = WidthDB::new();
|
||||
|
||||
// widthdb.measure_block(unicode_blocks::BASIC_LATIN);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_1_SUPPLEMENT);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_A);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_ADDITIONAL);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_B);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_C);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_D);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_E);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_F);
|
||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_G);
|
||||
// widthdb.measure_block(unicode_blocks::EMOTICONS);
|
||||
widthdb.measure_block(unicode_blocks::BOX_DRAWING);
|
||||
|
||||
println!();
|
||||
for (grapheme, width) in widthdb.0 {
|
||||
let expected = grapheme.width();
|
||||
println!("{grapheme} = {width} (expected: {expected})");
|
||||
}
|
||||
}
|
||||
60
src/style.rs
60
src/style.rs
|
|
@ -1,60 +0,0 @@
|
|||
use crossterm::style::{ContentStyle, Stylize};
|
||||
|
||||
fn merge_cs(base: ContentStyle, cover: ContentStyle) -> ContentStyle {
|
||||
ContentStyle {
|
||||
foreground_color: cover.foreground_color.or(base.foreground_color),
|
||||
background_color: cover.background_color.or(base.background_color),
|
||||
underline_color: cover.underline_color.or(base.underline_color),
|
||||
attributes: cover.attributes,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Style {
|
||||
pub content_style: ContentStyle,
|
||||
pub opaque: bool,
|
||||
}
|
||||
|
||||
impl Style {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn transparent(mut self) -> Self {
|
||||
self.opaque = false;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn opaque(mut self) -> Self {
|
||||
self.opaque = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cover(self, base: ContentStyle) -> ContentStyle {
|
||||
if self.opaque {
|
||||
return self.content_style;
|
||||
}
|
||||
|
||||
merge_cs(base, self.content_style)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ContentStyle> for Style {
|
||||
fn as_ref(&self) -> &ContentStyle {
|
||||
&self.content_style
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<ContentStyle> for Style {
|
||||
fn as_mut(&mut self) -> &mut ContentStyle {
|
||||
&mut self.content_style
|
||||
}
|
||||
}
|
||||
|
||||
impl Stylize for Style {
|
||||
type Styled = Self;
|
||||
|
||||
fn stylize(self) -> Self::Styled {
|
||||
self
|
||||
}
|
||||
}
|
||||
195
src/styled.rs
195
src/styled.rs
|
|
@ -1,195 +0,0 @@
|
|||
use std::iter::Peekable;
|
||||
use std::slice;
|
||||
|
||||
use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation};
|
||||
|
||||
use crate::Style;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Styled {
|
||||
text: String,
|
||||
/// List of `(style, until)` tuples. The style should be applied to all
|
||||
/// chars in the range `prev_until..until`.
|
||||
styles: Vec<(Style, usize)>,
|
||||
}
|
||||
|
||||
impl Styled {
|
||||
pub fn new<S: AsRef<str>>(text: S, style: Style) -> Self {
|
||||
Self::default().then(text, style)
|
||||
}
|
||||
|
||||
pub fn new_plain<S: AsRef<str>>(text: S) -> Self {
|
||||
Self::default().then_plain(text)
|
||||
}
|
||||
|
||||
pub fn then<S: AsRef<str>>(mut self, text: S, style: Style) -> Self {
|
||||
let text = text.as_ref();
|
||||
if !text.is_empty() {
|
||||
self.text.push_str(text);
|
||||
self.styles.push((style, self.text.len()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn then_plain<S: AsRef<str>>(self, text: S) -> Self {
|
||||
self.then(text, Style::new())
|
||||
}
|
||||
|
||||
pub fn and_then(mut self, mut other: Self) -> Self {
|
||||
let delta = self.text.len();
|
||||
for (_, until) in &mut other.styles {
|
||||
*until += delta;
|
||||
}
|
||||
|
||||
self.text.push_str(&other.text);
|
||||
self.styles.extend(other.styles);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn split_at(self, mid: usize) -> (Self, Self) {
|
||||
let (left_text, right_text) = self.text.split_at(mid);
|
||||
|
||||
let mut left_styles = vec![];
|
||||
let mut right_styles = vec![];
|
||||
let mut from = 0;
|
||||
for (style, until) in self.styles {
|
||||
if from < mid {
|
||||
left_styles.push((style, until.min(mid)));
|
||||
}
|
||||
if mid < until {
|
||||
right_styles.push((style, until.saturating_sub(mid)));
|
||||
}
|
||||
from = until;
|
||||
}
|
||||
|
||||
let left = Self {
|
||||
text: left_text.to_string(),
|
||||
styles: left_styles,
|
||||
};
|
||||
|
||||
let right = Self {
|
||||
text: right_text.to_string(),
|
||||
styles: right_styles,
|
||||
};
|
||||
|
||||
(left, 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) {
|
||||
self.text = self.text.trim_end().to_string();
|
||||
|
||||
let text_len = self.text.len();
|
||||
let mut styles_len = 0;
|
||||
for (_, until) in &mut self.styles {
|
||||
styles_len += 1;
|
||||
if *until >= text_len {
|
||||
*until = text_len;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while self.styles.len() > styles_len {
|
||||
self.styles.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// Iterating over graphemes //
|
||||
//////////////////////////////
|
||||
|
||||
pub struct StyledGraphemeIndices<'a> {
|
||||
text: GraphemeIndices<'a>,
|
||||
styles: Peekable<slice::Iter<'a, (Style, usize)>>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for StyledGraphemeIndices<'a> {
|
||||
type Item = (usize, Style, &'a str);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let (gi, grapheme) = self.text.next()?;
|
||||
let (mut style, mut until) = **self.styles.peek().expect("styles cover entire text");
|
||||
while gi >= until {
|
||||
self.styles.next();
|
||||
(style, until) = **self.styles.peek().expect("styles cover entire text");
|
||||
}
|
||||
Some((gi, style, grapheme))
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled {
|
||||
pub fn graphemes(&self) -> Graphemes<'_> {
|
||||
self.text.graphemes(true)
|
||||
}
|
||||
|
||||
pub fn grapheme_indices(&self) -> GraphemeIndices<'_> {
|
||||
self.text.grapheme_indices(true)
|
||||
}
|
||||
|
||||
pub fn styled_grapheme_indices(&self) -> StyledGraphemeIndices<'_> {
|
||||
StyledGraphemeIndices {
|
||||
text: self.grapheme_indices(),
|
||||
styles: self.styles.iter().peekable(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
// Converting to Styled //
|
||||
//////////////////////////
|
||||
|
||||
impl From<&str> for Styled {
|
||||
fn from(text: &str) -> Self {
|
||||
Self::new_plain(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Styled {
|
||||
fn from(text: String) -> Self {
|
||||
Self::new_plain(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<(S,)> for Styled {
|
||||
fn from((text,): (S,)) -> Self {
|
||||
Self::new_plain(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<(S, Style)> for Styled {
|
||||
fn from((text, style): (S, Style)) -> Self {
|
||||
Self::new(text, style)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsRef<str>> From<&[(S, Style)]> for Styled {
|
||||
fn from(segments: &[(S, Style)]) -> Self {
|
||||
let mut result = Self::default();
|
||||
for (text, style) in segments {
|
||||
result = result.then(text, *style);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
327
src/terminal.rs
327
src/terminal.rs
|
|
@ -1,327 +0,0 @@
|
|||
//! Displaying frames on a terminal.
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::mem;
|
||||
|
||||
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||
use crossterm::event::{
|
||||
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
|
||||
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::style::{Print, PrintStyledContent, StyledContent};
|
||||
use crossterm::terminal::{
|
||||
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
|
||||
LeaveAlternateScreen, SetTitle,
|
||||
};
|
||||
use crossterm::{ExecutableCommand, QueueableCommand};
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod};
|
||||
|
||||
/// Wrapper that manages terminal output.
|
||||
///
|
||||
/// This struct (usually) wraps around stdout and handles showing things on the
|
||||
/// terminal. It cleans up after itself when droppped, so it shouldn't leave the
|
||||
/// terminal in a weird state even if your program crashes.
|
||||
pub struct Terminal {
|
||||
/// Render target.
|
||||
out: Box<dyn Write>,
|
||||
/// The frame being currently rendered.
|
||||
frame: Frame,
|
||||
/// Buffer from the previous frame.
|
||||
prev_frame_buffer: Buffer,
|
||||
/// When the screen is updated next, it must be cleared and redrawn fully
|
||||
/// instead of performing an incremental update.
|
||||
full_redraw: bool,
|
||||
}
|
||||
|
||||
impl Drop for Terminal {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.suspend();
|
||||
}
|
||||
}
|
||||
|
||||
impl Terminal {
|
||||
/// Create a new [`Terminal`] that wraps stdout.
|
||||
pub fn new() -> io::Result<Self> {
|
||||
Self::with_target(Box::new(io::stdout()))
|
||||
}
|
||||
|
||||
/// Create a new terminal wrapping a custom output.
|
||||
pub fn with_target(out: Box<dyn Write>) -> io::Result<Self> {
|
||||
let mut result = Self {
|
||||
out,
|
||||
frame: Frame::default(),
|
||||
prev_frame_buffer: Buffer::default(),
|
||||
full_redraw: true,
|
||||
};
|
||||
result.unsuspend()?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Temporarily restore the terminal state to normal.
|
||||
///
|
||||
/// This is useful when running external programs the user should interact
|
||||
/// with directly, for example a text editor.
|
||||
///
|
||||
/// Call [`Self::unsuspend`] to return the terminal state before drawing and
|
||||
/// presenting the next frame.
|
||||
pub fn suspend(&mut self) -> io::Result<()> {
|
||||
crossterm::terminal::disable_raw_mode()?;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
self.out.execute(PopKeyboardEnhancementFlags)?;
|
||||
self.out.execute(DisableBracketedPaste)?;
|
||||
}
|
||||
self.out.execute(LeaveAlternateScreen)?;
|
||||
self.out.execute(Show)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Restore the terminal state after calling [`Self::suspend`].
|
||||
///
|
||||
/// After calling this function, a new frame needs to be drawn and presented
|
||||
/// by the application. The previous screen contents are **not** restored.
|
||||
pub fn unsuspend(&mut self) -> io::Result<()> {
|
||||
crossterm::terminal::enable_raw_mode()?;
|
||||
self.out.execute(EnterAlternateScreen)?;
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
self.out.execute(EnableBracketedPaste)?;
|
||||
self.out.execute(PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
|
||||
))?;
|
||||
}
|
||||
self.full_redraw = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the tab width in columns.
|
||||
///
|
||||
/// For more details, see [`Self::tab_width`].
|
||||
pub fn set_tab_width(&mut self, tab_width: u8) {
|
||||
self.frame.widthdb.tab_width = tab_width;
|
||||
}
|
||||
|
||||
/// The tab width in columns.
|
||||
///
|
||||
/// For accurate width calculations and consistency across terminals, tabs
|
||||
/// are not printed to the terminal directly, but instead converted into
|
||||
/// spaces.
|
||||
pub fn tab_width(&self) -> u8 {
|
||||
self.frame.widthdb.tab_width
|
||||
}
|
||||
|
||||
/// Set the grapheme width estimation method.
|
||||
///
|
||||
/// For more details, see [`WidthEstimationMethod`].
|
||||
pub fn set_width_estimation_method(&mut self, method: WidthEstimationMethod) {
|
||||
self.frame.widthdb.estimate = method;
|
||||
}
|
||||
|
||||
/// The grapheme width estimation method.
|
||||
///
|
||||
/// For more details, see [`WidthEstimationMethod`].
|
||||
pub fn width_estimation_method(&mut self) -> WidthEstimationMethod {
|
||||
self.frame.widthdb.estimate
|
||||
}
|
||||
|
||||
/// Enable or disable grapheme width measurements.
|
||||
///
|
||||
/// For more details, see [`Self::measuring`].
|
||||
pub fn set_measuring(&mut self, active: bool) {
|
||||
self.frame.widthdb.measure = active;
|
||||
}
|
||||
|
||||
/// Whether grapheme widths should be measured or estimated.
|
||||
///
|
||||
/// Handling of wide characters is inconsistent from terminal emulator to
|
||||
/// terminal emulator, and may even depend on the font the user is using.
|
||||
///
|
||||
/// When enabled, any newly encountered graphemes are measured whenever
|
||||
/// [`Self::measure_widths`] is called. This is done by clearing the screen,
|
||||
/// printing the grapheme and measuring the resulting cursor position.
|
||||
/// Because of this, the screen will flicker occasionally. However, grapheme
|
||||
/// widths will always be accurate independent of the terminal
|
||||
/// configuration.
|
||||
///
|
||||
/// When disabled, the width of graphemes is estimated using the Unicode
|
||||
/// Standard Annex #11. This usually works fine, but may break on some emoji
|
||||
/// or other less commonly used character sequences.
|
||||
pub fn measuring(&self) -> bool {
|
||||
self.frame.widthdb.measure
|
||||
}
|
||||
|
||||
/// Whether any unmeasured graphemes were seen since the last call to
|
||||
/// [`Self::measure_widths`].
|
||||
///
|
||||
/// Returns `true` whenever [`Self::measure_widths`] would return `true`.
|
||||
pub fn measuring_required(&self) -> bool {
|
||||
self.frame.widthdb.measuring_required()
|
||||
}
|
||||
|
||||
/// Measure widths of all unmeasured graphemes.
|
||||
///
|
||||
/// If width measurements are disabled, this function does nothing. For more
|
||||
/// info, see [`Self::measuring`].
|
||||
///
|
||||
/// Returns `true` if any new graphemes were measured and the screen must be
|
||||
/// redrawn. Keep in mind that after redrawing the screen, graphemes may
|
||||
/// have become visible that have not yet been measured. You should keep
|
||||
/// re-measuring and re-drawing until this function returns `false`.
|
||||
pub fn measure_widths(&mut self) -> io::Result<bool> {
|
||||
if self.frame.widthdb.measuring_required() {
|
||||
self.full_redraw = true;
|
||||
self.frame.widthdb.measure_widths(&mut self.out)?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resize the frame and other internal buffers if the terminal size has
|
||||
/// changed.
|
||||
///
|
||||
/// Should be called before drawing a frame and presenting it with
|
||||
/// [`Self::present`]. It is not necessary to call this when using
|
||||
/// [`Self::present_widget`] or [`Self::present_async_widget`].
|
||||
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||
let (width, height) = crossterm::terminal::size()?;
|
||||
let size = Size { width, height };
|
||||
if size != self.frame.size() {
|
||||
self.frame.buffer.resize(size);
|
||||
self.prev_frame_buffer.resize(size);
|
||||
self.full_redraw = true;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The current frame.
|
||||
pub fn frame(&mut self) -> &mut Frame {
|
||||
&mut self.frame
|
||||
}
|
||||
|
||||
/// A database of grapheme widths.
|
||||
pub fn widthdb(&mut self) -> &mut WidthDb {
|
||||
&mut self.frame.widthdb
|
||||
}
|
||||
|
||||
/// Mark the terminal as dirty, forcing a full redraw whenever any variant
|
||||
/// of [`Self::present`] is called.
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.full_redraw = true;
|
||||
}
|
||||
|
||||
/// Display the current frame on the screen and prepare the next frame.
|
||||
///
|
||||
/// Before drawing and presenting a frame, [`Self::measure_widths`] and
|
||||
/// [`Self::autoresize`] should be called.
|
||||
///
|
||||
/// 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<()> {
|
||||
self.out.queue(BeginSynchronizedUpdate)?;
|
||||
let result = self.draw_to_screen();
|
||||
self.out.queue(EndSynchronizedUpdate)?;
|
||||
result?;
|
||||
|
||||
self.out.flush()?;
|
||||
|
||||
mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer);
|
||||
self.frame.reset();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display a [`Widget`] on the screen.
|
||||
///
|
||||
/// Before creating and presenting a widget, [`Self::measure_widths`] should
|
||||
/// be called. There is no need to call [`Self::autoresize`].
|
||||
pub fn present_widget<E, W>(&mut self, widget: W) -> Result<(), E>
|
||||
where
|
||||
E: From<io::Error>,
|
||||
W: Widget<E>,
|
||||
{
|
||||
self.autoresize()?;
|
||||
widget.draw(self.frame())?;
|
||||
self.present()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Display an [`AsyncWidget`] on the screen.
|
||||
///
|
||||
/// Before creating and presenting a widget, [`Self::measure_widths`] should
|
||||
/// be called. There is no need to call [`Self::autoresize`].
|
||||
pub async fn present_async_widget<E, W>(&mut self, widget: W) -> Result<(), E>
|
||||
where
|
||||
E: From<io::Error>,
|
||||
W: AsyncWidget<E>,
|
||||
{
|
||||
self.autoresize()?;
|
||||
widget.draw(self.frame()).await?;
|
||||
self.present()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_to_screen(&mut self) -> io::Result<()> {
|
||||
if self.full_redraw {
|
||||
self.out.queue(Clear(ClearType::All))?;
|
||||
self.prev_frame_buffer.reset(); // Because the screen is now empty
|
||||
self.full_redraw = false;
|
||||
}
|
||||
|
||||
self.draw_differences()?;
|
||||
self.update_cursor()?;
|
||||
self.update_title()?;
|
||||
self.ring_bell()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_differences(&mut self) -> io::Result<()> {
|
||||
for (x, y, cell) in self.frame.buffer.cells() {
|
||||
if self.prev_frame_buffer.at(x, y) == cell {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = StyledContent::new(cell.style, &cell.content as &str);
|
||||
self.out
|
||||
.queue(MoveTo(x, y))?
|
||||
.queue(PrintStyledContent(content))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_cursor(&mut self) -> io::Result<()> {
|
||||
if let Some(pos) = self.frame.cursor() {
|
||||
let size = self.frame.size();
|
||||
let x_in_bounds = 0 <= pos.x && pos.x < size.width as i32;
|
||||
let y_in_bounds = 0 <= pos.y && pos.y < size.height as i32;
|
||||
if x_in_bounds && y_in_bounds {
|
||||
self.out
|
||||
.queue(Show)?
|
||||
.queue(MoveTo(pos.x as u16, pos.y as u16))?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
self.out.queue(Hide)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_title(&mut self) -> io::Result<()> {
|
||||
if let Some(title) = &self.frame.title {
|
||||
self.out.queue(SetTitle(title.clone()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ring_bell(&mut self) -> io::Result<()> {
|
||||
if self.frame.bell {
|
||||
self.out.queue(Print('\x07'))?;
|
||||
}
|
||||
self.frame.bell = false;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
127
src/widget.rs
127
src/widget.rs
|
|
@ -1,127 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::widgets::{
|
||||
Background, Border, Boxed, BoxedAsync, BoxedSendSync, Desync, Either2, Either3, Float,
|
||||
JoinSegment, Layer2, Padding, Resize, Title,
|
||||
};
|
||||
use crate::{Frame, Size, WidthDb};
|
||||
|
||||
// TODO Feature-gate these traits
|
||||
|
||||
pub trait Widget<E> {
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E>;
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait AsyncWidget<E> {
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E>;
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E>;
|
||||
}
|
||||
|
||||
pub trait WidgetExt: Sized {
|
||||
fn background(self) -> Background<Self> {
|
||||
Background::new(self)
|
||||
}
|
||||
|
||||
fn border(self) -> Border<Self> {
|
||||
Border::new(self)
|
||||
}
|
||||
|
||||
fn boxed<'a, E>(self) -> Boxed<'a, E>
|
||||
where
|
||||
Self: Widget<E> + 'a,
|
||||
{
|
||||
Boxed::new(self)
|
||||
}
|
||||
|
||||
fn boxed_send_sync<'a, E>(self) -> BoxedSendSync<'a, E>
|
||||
where
|
||||
Self: Widget<E> + Send + Sync + 'a,
|
||||
{
|
||||
BoxedSendSync::new(self)
|
||||
}
|
||||
|
||||
fn boxed_async<'a, E>(self) -> BoxedAsync<'a, E>
|
||||
where
|
||||
Self: AsyncWidget<E> + Send + Sync + 'a,
|
||||
{
|
||||
BoxedAsync::new(self)
|
||||
}
|
||||
|
||||
fn desync(self) -> Desync<Self> {
|
||||
Desync(self)
|
||||
}
|
||||
|
||||
fn first2<W2>(self) -> Either2<Self, W2> {
|
||||
Either2::First(self)
|
||||
}
|
||||
|
||||
fn second2<W1>(self) -> Either2<W1, Self> {
|
||||
Either2::Second(self)
|
||||
}
|
||||
|
||||
fn first3<W2, W3>(self) -> Either3<Self, W2, W3> {
|
||||
Either3::First(self)
|
||||
}
|
||||
|
||||
fn second3<W1, W3>(self) -> Either3<W1, Self, W3> {
|
||||
Either3::Second(self)
|
||||
}
|
||||
|
||||
fn third3<W1, W2>(self) -> Either3<W1, W2, Self> {
|
||||
Either3::Third(self)
|
||||
}
|
||||
|
||||
fn float(self) -> Float<Self> {
|
||||
Float::new(self)
|
||||
}
|
||||
|
||||
fn segment(self) -> JoinSegment<Self> {
|
||||
JoinSegment::new(self)
|
||||
}
|
||||
|
||||
fn below<W>(self, above: W) -> Layer2<Self, W> {
|
||||
Layer2::new(self, above)
|
||||
}
|
||||
|
||||
fn above<W>(self, below: W) -> Layer2<W, Self> {
|
||||
Layer2::new(below, self)
|
||||
}
|
||||
|
||||
fn padding(self) -> Padding<Self> {
|
||||
Padding::new(self)
|
||||
}
|
||||
|
||||
fn resize(self) -> Resize<Self> {
|
||||
Resize::new(self)
|
||||
}
|
||||
|
||||
fn title<S: ToString>(self, title: S) -> Title<Self> {
|
||||
Title::new(self, title)
|
||||
}
|
||||
}
|
||||
|
||||
// It would be nice if this could be restricted to types implementing Widget.
|
||||
// However, Widget (and AsyncWidget) have the E type parameter, which WidgetExt
|
||||
// doesn't have. We sadly can't have unconstrained type parameters like that in
|
||||
// impl blocks.
|
||||
//
|
||||
// If WidgetExt had a type parameter E, we'd need to specify that parameter
|
||||
// everywhere we use the trait. This is less ergonomic than just constructing
|
||||
// the types manually.
|
||||
//
|
||||
// Blanket-implementing this trait is not great, but usually works fine.
|
||||
impl<W> WidgetExt for W {}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
pub mod background;
|
||||
pub mod bell;
|
||||
pub mod border;
|
||||
pub mod boxed;
|
||||
pub mod cursor;
|
||||
pub mod desync;
|
||||
pub mod editor;
|
||||
pub mod either;
|
||||
pub mod empty;
|
||||
pub mod float;
|
||||
pub mod join;
|
||||
pub mod layer;
|
||||
pub mod padding;
|
||||
pub mod predrawn;
|
||||
pub mod resize;
|
||||
pub mod text;
|
||||
pub mod title;
|
||||
|
||||
pub use background::*;
|
||||
pub use bell::*;
|
||||
pub use border::*;
|
||||
pub use boxed::*;
|
||||
pub use cursor::*;
|
||||
pub use desync::*;
|
||||
pub use editor::*;
|
||||
pub use either::*;
|
||||
pub use empty::*;
|
||||
pub use float::*;
|
||||
pub use join::*;
|
||||
pub use layer::*;
|
||||
pub use padding::*;
|
||||
pub use predrawn::*;
|
||||
pub use resize::*;
|
||||
pub use text::*;
|
||||
pub use title::*;
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Background<I> {
|
||||
pub inner: I,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<I> Background<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
style: Style::new().opaque(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn fill(&self, frame: &mut Frame) {
|
||||
let size = frame.size();
|
||||
for dy in 0..size.height {
|
||||
for dx in 0..size.width {
|
||||
frame.write(Pos::new(dx.into(), dy.into()), (" ", self.style));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Background<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.fill(frame);
|
||||
self.inner.draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Background<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.fill(frame);
|
||||
self.inner.draw(frame).await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
use crate::{Frame, Size, Widget, WidthDb};
|
||||
|
||||
///////////
|
||||
// State //
|
||||
///////////
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct BellState {
|
||||
// Whether the bell should be rung the next time the widget is displayed.
|
||||
pub ring: bool,
|
||||
}
|
||||
|
||||
impl BellState {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn widget(&mut self) -> Bell<'_> {
|
||||
Bell { state: self }
|
||||
}
|
||||
}
|
||||
|
||||
////////////
|
||||
// Widget //
|
||||
////////////
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Bell<'a> {
|
||||
state: &'a mut BellState,
|
||||
}
|
||||
|
||||
impl Bell<'_> {
|
||||
pub fn state(&mut self) -> &mut BellState {
|
||||
self.state
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Bell<'_> {
|
||||
fn size(
|
||||
&self,
|
||||
_widthdb: &mut WidthDb,
|
||||
_max_width: Option<u16>,
|
||||
_max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
Ok(Size::ZERO)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
if self.state.ring {
|
||||
frame.set_bell(true);
|
||||
self.state.ring = false
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Style, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BorderLook {
|
||||
pub top_left: &'static str,
|
||||
pub top_right: &'static str,
|
||||
pub bottom_left: &'static str,
|
||||
pub bottom_right: &'static str,
|
||||
pub top: &'static str,
|
||||
pub bottom: &'static str,
|
||||
pub left: &'static str,
|
||||
pub right: &'static str,
|
||||
}
|
||||
|
||||
impl BorderLook {
|
||||
/// ```text
|
||||
/// +-------+
|
||||
/// | Hello |
|
||||
/// +-------+
|
||||
/// ```
|
||||
pub const ASCII: Self = Self {
|
||||
top_left: "+",
|
||||
top_right: "+",
|
||||
bottom_left: "+",
|
||||
bottom_right: "+",
|
||||
top: "-",
|
||||
bottom: "-",
|
||||
left: "|",
|
||||
right: "|",
|
||||
};
|
||||
|
||||
/// ```text
|
||||
/// ┌───────┐
|
||||
/// │ Hello │
|
||||
/// └───────┘
|
||||
/// ```
|
||||
pub const LINE: Self = Self {
|
||||
top_left: "┌",
|
||||
top_right: "┐",
|
||||
bottom_left: "└",
|
||||
bottom_right: "┘",
|
||||
top: "─",
|
||||
bottom: "─",
|
||||
left: "│",
|
||||
right: "│",
|
||||
};
|
||||
|
||||
/// ```text
|
||||
/// ┏━━━━━━━┓
|
||||
/// ┃ Hello ┃
|
||||
/// ┗━━━━━━━┛
|
||||
/// ```
|
||||
pub const LINE_HEAVY: Self = Self {
|
||||
top_left: "┏",
|
||||
top_right: "┓",
|
||||
bottom_left: "┗",
|
||||
bottom_right: "┛",
|
||||
top: "━",
|
||||
bottom: "━",
|
||||
left: "┃",
|
||||
right: "┃",
|
||||
};
|
||||
|
||||
/// ```text
|
||||
/// ╔═══════╗
|
||||
/// ║ Hello ║
|
||||
/// ╚═══════╝
|
||||
/// ```
|
||||
pub const LINE_DOUBLE: Self = Self {
|
||||
top_left: "╔",
|
||||
top_right: "╗",
|
||||
bottom_left: "╚",
|
||||
bottom_right: "╝",
|
||||
top: "═",
|
||||
bottom: "═",
|
||||
left: "║",
|
||||
right: "║",
|
||||
};
|
||||
}
|
||||
|
||||
impl Default for BorderLook {
|
||||
fn default() -> Self {
|
||||
Self::LINE
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Border<I> {
|
||||
pub inner: I,
|
||||
pub look: BorderLook,
|
||||
pub style: Style,
|
||||
}
|
||||
|
||||
impl<I> Border<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
look: BorderLook::default(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_look(mut self, look: BorderLook) -> Self {
|
||||
self.look = look;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_style(mut self, style: Style) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
|
||||
fn draw_border(&self, frame: &mut Frame) {
|
||||
let size = frame.size();
|
||||
let right = size.width.saturating_sub(1).into();
|
||||
let bottom = size.height.saturating_sub(1).into();
|
||||
|
||||
for y in 1..bottom {
|
||||
frame.write(Pos::new(right, y), (self.look.right, self.style));
|
||||
frame.write(Pos::new(0, y), (self.look.left, self.style));
|
||||
}
|
||||
|
||||
for x in 1..right {
|
||||
frame.write(Pos::new(x, bottom), (self.look.bottom, self.style));
|
||||
frame.write(Pos::new(x, 0), (self.look.top, self.style));
|
||||
}
|
||||
|
||||
frame.write(
|
||||
Pos::new(right, bottom),
|
||||
(self.look.bottom_right, self.style),
|
||||
);
|
||||
frame.write(Pos::new(0, bottom), (self.look.bottom_left, self.style));
|
||||
frame.write(Pos::new(right, 0), (self.look.top_right, self.style));
|
||||
frame.write(Pos::new(0, 0), (self.look.top_left, self.style));
|
||||
}
|
||||
|
||||
fn push_inner(&self, frame: &mut Frame) {
|
||||
let mut size = frame.size();
|
||||
size.width = size.width.saturating_sub(2);
|
||||
size.height = size.height.saturating_sub(2);
|
||||
|
||||
frame.push(Pos::new(1, 1), size);
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Border<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let max_width = max_width.map(|w| w.saturating_sub(2));
|
||||
let max_height = max_height.map(|h| h.saturating_sub(2));
|
||||
let size = self.inner.size(widthdb, max_width, max_height)?;
|
||||
Ok(size + Size::new(2, 2))
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.draw_border(frame);
|
||||
|
||||
self.push_inner(frame);
|
||||
self.inner.draw(frame)?;
|
||||
frame.pop();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Border<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let max_width = max_width.map(|w| w.saturating_sub(2));
|
||||
let max_height = max_height.map(|h| h.saturating_sub(2));
|
||||
let size = self.inner.size(widthdb, max_width, max_height).await?;
|
||||
Ok(size + Size::new(2, 2))
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.draw_border(frame);
|
||||
|
||||
self.push_inner(frame);
|
||||
self.inner.draw(frame).await?;
|
||||
frame.pop();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
pub struct Boxed<'a, E>(Box<dyn WidgetWrapper<E> + 'a>);
|
||||
|
||||
impl<'a, E> Boxed<'a, E> {
|
||||
pub fn new<I>(inner: I) -> Self
|
||||
where
|
||||
I: Widget<E> + 'a,
|
||||
{
|
||||
Self(Box::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Boxed<'_, E> {
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.0.wrap_size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.0.wrap_draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedSendSync<'a, E>(Box<dyn WidgetWrapper<E> + Send + Sync + 'a>);
|
||||
|
||||
impl<'a, E> BoxedSendSync<'a, E> {
|
||||
pub fn new<I>(inner: I) -> Self
|
||||
where
|
||||
I: Widget<E> + Send + Sync + 'a,
|
||||
{
|
||||
Self(Box::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for BoxedSendSync<'_, E> {
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.0.wrap_size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.0.wrap_draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BoxedAsync<'a, E>(Box<dyn AsyncWidgetWrapper<E> + Send + Sync + 'a>);
|
||||
|
||||
impl<'a, E> BoxedAsync<'a, E> {
|
||||
pub fn new<I>(inner: I) -> Self
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync + 'a,
|
||||
{
|
||||
Self(Box::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E> AsyncWidget<E> for BoxedAsync<'_, E> {
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.0.wrap_size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.0.wrap_draw(frame).await
|
||||
}
|
||||
}
|
||||
|
||||
trait WidgetWrapper<E> {
|
||||
fn wrap_size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E>;
|
||||
|
||||
fn wrap_draw(self: Box<Self>, frame: &mut Frame) -> Result<(), E>;
|
||||
}
|
||||
|
||||
impl<E, W> WidgetWrapper<E> for W
|
||||
where
|
||||
W: Widget<E>,
|
||||
{
|
||||
fn wrap_size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn wrap_draw(self: Box<Self>, frame: &mut Frame) -> Result<(), E> {
|
||||
(*self).draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
trait AsyncWidgetWrapper<E> {
|
||||
async fn wrap_size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E>;
|
||||
|
||||
async fn wrap_draw(self: Box<Self>, frame: &mut Frame) -> Result<(), E>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, W> AsyncWidgetWrapper<E> for W
|
||||
where
|
||||
W: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn wrap_size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn wrap_draw(self: Box<Self>, frame: &mut Frame) -> Result<(), E> {
|
||||
(*self).draw(frame).await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Cursor<I> {
|
||||
pub inner: I,
|
||||
pub position: Pos,
|
||||
}
|
||||
|
||||
impl<I> Cursor<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
position: Pos::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_position(mut self, position: Pos) -> Self {
|
||||
self.position = position;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_position_xy(self, x: i32, y: i32) -> Self {
|
||||
self.with_position(Pos::new(x, y))
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Cursor<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame)?;
|
||||
frame.show_cursor(self.position);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Cursor<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame).await?;
|
||||
frame.show_cursor(self.position);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Widget};
|
||||
|
||||
pub struct Desync<I>(pub I);
|
||||
|
||||
impl<E, I> Widget<E> for Desync<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut crate::WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<crate::Size, E> {
|
||||
self.0.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut crate::Frame) -> Result<(), E> {
|
||||
self.0.draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Desync<I>
|
||||
where
|
||||
I: Widget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut crate::WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<crate::Size, E> {
|
||||
self.0.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut crate::Frame) -> Result<(), E> {
|
||||
self.0.draw(frame)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,485 +0,0 @@
|
|||
use std::iter;
|
||||
|
||||
use crossterm::style::Stylize;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::{Frame, Pos, Size, Style, Styled, Widget, WidthDb};
|
||||
|
||||
/// Like [`WidthDb::wrap`] but includes a final break index if the text ends
|
||||
/// with a newline.
|
||||
fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec<usize> {
|
||||
let mut breaks = widthdb.wrap(text, width);
|
||||
if text.ends_with('\n') {
|
||||
breaks.push(text.len())
|
||||
}
|
||||
breaks
|
||||
}
|
||||
|
||||
///////////
|
||||
// State //
|
||||
///////////
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditorState {
|
||||
text: String,
|
||||
|
||||
/// Index of the cursor in the text.
|
||||
///
|
||||
/// Must point to a valid grapheme boundary.
|
||||
cursor_idx: usize,
|
||||
|
||||
/// Column of the cursor on the screen just after it was last moved
|
||||
/// horizontally.
|
||||
cursor_col: usize,
|
||||
|
||||
/// Position of the cursor when the editor was last rendered.
|
||||
last_cursor_pos: Pos,
|
||||
}
|
||||
|
||||
impl EditorState {
|
||||
pub fn new() -> Self {
|
||||
Self::with_initial_text(String::new())
|
||||
}
|
||||
|
||||
pub fn with_initial_text(text: String) -> Self {
|
||||
Self {
|
||||
cursor_idx: text.len(),
|
||||
cursor_col: 0,
|
||||
last_cursor_pos: Pos::ZERO,
|
||||
text,
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
// Grapheme helper functions //
|
||||
///////////////////////////////
|
||||
|
||||
fn grapheme_boundaries(&self) -> Vec<usize> {
|
||||
self.text
|
||||
.grapheme_indices(true)
|
||||
.map(|(i, _)| i)
|
||||
.chain(iter::once(self.text.len()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Ensure the cursor index lies on a grapheme boundary. If it doesn't, it
|
||||
/// is moved to the next grapheme boundary.
|
||||
///
|
||||
/// Can handle arbitrary cursor index.
|
||||
fn move_cursor_to_grapheme_boundary(&mut self) {
|
||||
for i in self.grapheme_boundaries() {
|
||||
#[allow(clippy::comparison_chain)]
|
||||
if i == self.cursor_idx {
|
||||
// We're at a valid grapheme boundary already
|
||||
return;
|
||||
} else if i > self.cursor_idx {
|
||||
// There was no valid grapheme boundary at our cursor index, so
|
||||
// we'll take the next one we can get.
|
||||
self.cursor_idx = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The cursor was out of bounds, so move it to the last valid index.
|
||||
self.cursor_idx = self.text.len();
|
||||
}
|
||||
|
||||
///////////////////////////////
|
||||
// Line/col helper functions //
|
||||
///////////////////////////////
|
||||
|
||||
/// Like [`Self::grapheme_boundaries`] but for lines.
|
||||
///
|
||||
/// Note that the last line can have a length of 0 if the text ends with a
|
||||
/// newline.
|
||||
fn line_boundaries(&self) -> Vec<usize> {
|
||||
let newlines = self
|
||||
.text
|
||||
.char_indices()
|
||||
.filter(|(_, c)| *c == '\n')
|
||||
.map(|(i, _)| i + 1); // utf-8 encodes '\n' as a single byte
|
||||
iter::once(0)
|
||||
.chain(newlines)
|
||||
.chain(iter::once(self.text.len()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Find the cursor's current line.
|
||||
///
|
||||
/// Returns `(line_nr, start_idx, end_idx)`.
|
||||
fn cursor_line(&self, boundaries: &[usize]) -> (usize, usize, usize) {
|
||||
let mut result = (0, 0, 0);
|
||||
for (i, (start, end)) in boundaries.iter().zip(boundaries.iter().skip(1)).enumerate() {
|
||||
if self.cursor_idx >= *start {
|
||||
result = (i, *start, *end);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn cursor_col(&self, widthdb: &mut WidthDb, line_start: usize) -> usize {
|
||||
widthdb.width(&self.text[line_start..self.cursor_idx])
|
||||
}
|
||||
|
||||
fn line(&self, line: usize) -> (usize, usize) {
|
||||
let boundaries = self.line_boundaries();
|
||||
boundaries
|
||||
.iter()
|
||||
.copied()
|
||||
.zip(boundaries.iter().copied().skip(1))
|
||||
.nth(line)
|
||||
.expect("line exists")
|
||||
}
|
||||
|
||||
fn move_cursor_to_line_col(&mut self, widthdb: &mut WidthDb, line: usize, col: usize) {
|
||||
let (start, end) = self.line(line);
|
||||
let line = &self.text[start..end];
|
||||
|
||||
let mut width = 0;
|
||||
for (gi, g) in line.grapheme_indices(true) {
|
||||
self.cursor_idx = start + gi;
|
||||
if col > width {
|
||||
width += widthdb.grapheme_width(g, width) as usize;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !line.ends_with('\n') {
|
||||
self.cursor_idx = end;
|
||||
}
|
||||
}
|
||||
|
||||
fn record_cursor_col(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.line_boundaries();
|
||||
let (_, start, _) = self.cursor_line(&boundaries);
|
||||
self.cursor_col = self.cursor_col(widthdb, start);
|
||||
}
|
||||
|
||||
/////////////
|
||||
// Editing //
|
||||
/////////////
|
||||
|
||||
pub fn text(&self) -> &str {
|
||||
&self.text
|
||||
}
|
||||
|
||||
pub fn set_text(&mut self, widthdb: &mut WidthDb, text: String) {
|
||||
self.text = text;
|
||||
self.move_cursor_to_grapheme_boundary();
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.text = String::new();
|
||||
self.cursor_idx = 0;
|
||||
self.cursor_col = 0;
|
||||
}
|
||||
|
||||
/// Insert a character at the current cursor position and move the cursor
|
||||
/// accordingly.
|
||||
pub fn insert_char(&mut self, widthdb: &mut WidthDb, ch: char) {
|
||||
self.text.insert(self.cursor_idx, ch);
|
||||
self.cursor_idx += ch.len_utf8();
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
/// Insert a string at the current cursor position and move the cursor
|
||||
/// accordingly.
|
||||
pub fn insert_str(&mut self, widthdb: &mut WidthDb, str: &str) {
|
||||
self.text.insert_str(self.cursor_idx, str);
|
||||
self.cursor_idx += str.len();
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
/// Delete the grapheme before the cursor position.
|
||||
pub fn backspace(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||
if *end == self.cursor_idx {
|
||||
self.text.replace_range(start..end, "");
|
||||
self.cursor_idx = *start;
|
||||
self.record_cursor_col(widthdb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the grapheme after the cursor position.
|
||||
pub fn delete(&mut self) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||
if *start == self.cursor_idx {
|
||||
self.text.replace_range(start..end, "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// Cursor movement //
|
||||
/////////////////////
|
||||
|
||||
pub fn move_cursor_left(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||
if *end == self.cursor_idx {
|
||||
self.cursor_idx = *start;
|
||||
self.record_cursor_col(widthdb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_cursor_right(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||
if *start == self.cursor_idx {
|
||||
self.cursor_idx = *end;
|
||||
self.record_cursor_col(widthdb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_cursor_left_a_word(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
let mut encountered_word = false;
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)).rev() {
|
||||
if *end == self.cursor_idx {
|
||||
let g = &self.text[*start..*end];
|
||||
let whitespace = g.chars().all(|c| c.is_whitespace());
|
||||
if encountered_word && whitespace {
|
||||
break;
|
||||
} else if !whitespace {
|
||||
encountered_word = true;
|
||||
}
|
||||
self.cursor_idx = *start;
|
||||
}
|
||||
}
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
pub fn move_cursor_right_a_word(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.grapheme_boundaries();
|
||||
let mut encountered_word = false;
|
||||
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||
if *start == self.cursor_idx {
|
||||
let g = &self.text[*start..*end];
|
||||
let whitespace = g.chars().all(|c| c.is_whitespace());
|
||||
if encountered_word && whitespace {
|
||||
break;
|
||||
} else if !whitespace {
|
||||
encountered_word = true;
|
||||
}
|
||||
self.cursor_idx = *end;
|
||||
}
|
||||
}
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
pub fn move_cursor_to_start_of_line(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.line_boundaries();
|
||||
let (line, _, _) = self.cursor_line(&boundaries);
|
||||
self.move_cursor_to_line_col(widthdb, line, 0);
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
pub fn move_cursor_to_end_of_line(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.line_boundaries();
|
||||
let (line, _, _) = self.cursor_line(&boundaries);
|
||||
self.move_cursor_to_line_col(widthdb, line, usize::MAX);
|
||||
self.record_cursor_col(widthdb);
|
||||
}
|
||||
|
||||
pub fn move_cursor_up(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.line_boundaries();
|
||||
let (line, _, _) = self.cursor_line(&boundaries);
|
||||
if line > 0 {
|
||||
self.move_cursor_to_line_col(widthdb, line - 1, self.cursor_col);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_cursor_down(&mut self, widthdb: &mut WidthDb) {
|
||||
let boundaries = self.line_boundaries();
|
||||
|
||||
// There's always at least one line, and always at least two line
|
||||
// boundaries at 0 and self.text.len().
|
||||
let amount_of_lines = boundaries.len() - 1;
|
||||
|
||||
let (line, _, _) = self.cursor_line(&boundaries);
|
||||
if line + 1 < amount_of_lines {
|
||||
self.move_cursor_to_line_col(widthdb, line + 1, self.cursor_col);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_cursor_pos(&self) -> Pos {
|
||||
self.last_cursor_pos
|
||||
}
|
||||
|
||||
pub fn widget(&mut self) -> Editor<'_> {
|
||||
Editor {
|
||||
highlighted: Styled::new_plain(&self.text),
|
||||
hidden: None,
|
||||
focus: true,
|
||||
state: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EditorState {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
////////////
|
||||
// Widget //
|
||||
////////////
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor<'a> {
|
||||
state: &'a mut EditorState,
|
||||
highlighted: Styled,
|
||||
pub hidden: Option<Styled>,
|
||||
pub focus: bool,
|
||||
}
|
||||
|
||||
impl Editor<'_> {
|
||||
pub fn state(&mut self) -> &mut EditorState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &Styled {
|
||||
&self.highlighted
|
||||
}
|
||||
|
||||
pub fn highlight<F>(&mut self, highlight: F)
|
||||
where
|
||||
F: FnOnce(&str) -> Styled,
|
||||
{
|
||||
self.highlighted = highlight(&self.state.text);
|
||||
assert_eq!(self.state.text, self.highlighted.text());
|
||||
}
|
||||
|
||||
pub fn with_highlight<F>(mut self, highlight: F) -> Self
|
||||
where
|
||||
F: FnOnce(&str) -> Styled,
|
||||
{
|
||||
self.highlight(highlight);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_visible(mut self) -> Self {
|
||||
self.hidden = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hidden<S: Into<Styled>>(mut self, placeholder: S) -> Self {
|
||||
self.hidden = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_hidden_default_placeholder(self) -> Self {
|
||||
self.with_hidden(("<hidden>", Style::new().grey().italic()))
|
||||
}
|
||||
|
||||
pub fn with_focus(mut self, active: bool) -> Self {
|
||||
self.focus = active;
|
||||
self
|
||||
}
|
||||
|
||||
fn wrapped_cursor(cursor_idx: usize, break_indices: &[usize]) -> (usize, usize) {
|
||||
let mut row = 0;
|
||||
let mut line_idx = cursor_idx;
|
||||
|
||||
for break_idx in break_indices {
|
||||
if cursor_idx < *break_idx {
|
||||
break;
|
||||
} else {
|
||||
row += 1;
|
||||
line_idx = cursor_idx - break_idx;
|
||||
}
|
||||
}
|
||||
|
||||
(row, line_idx)
|
||||
}
|
||||
|
||||
fn indices(&self, widthdb: &mut WidthDb, max_width: Option<u16>) -> Vec<usize> {
|
||||
let max_width = max_width
|
||||
// One extra column for cursor
|
||||
.map(|w| w.saturating_sub(1) as usize)
|
||||
.unwrap_or(usize::MAX);
|
||||
let text = self.hidden.as_ref().unwrap_or(&self.highlighted);
|
||||
wrap(widthdb, text.text(), max_width)
|
||||
}
|
||||
|
||||
fn rows(&self, indices: &[usize]) -> Vec<Styled> {
|
||||
let text = match self.hidden.as_ref() {
|
||||
Some(hidden) if !self.highlighted.text().is_empty() => hidden,
|
||||
_ => &self.highlighted,
|
||||
};
|
||||
text.clone().split_at_indices(indices)
|
||||
}
|
||||
|
||||
fn cursor(&self, widthdb: &mut WidthDb, width: u16, indices: &[usize], rows: &[Styled]) -> Pos {
|
||||
if self.hidden.is_some() {
|
||||
return Pos::new(0, 0);
|
||||
}
|
||||
|
||||
let (cursor_row, cursor_line_idx) = Self::wrapped_cursor(self.state.cursor_idx, indices);
|
||||
let cursor_col = widthdb.width(rows[cursor_row].text().split_at(cursor_line_idx).0);
|
||||
|
||||
// Ensure the cursor is always visible
|
||||
let cursor_col = cursor_col.min(width.saturating_sub(1).into());
|
||||
|
||||
let cursor_row: i32 = cursor_row.try_into().unwrap_or(i32::MAX);
|
||||
let cursor_col: i32 = cursor_col.try_into().unwrap_or(i32::MAX);
|
||||
Pos::new(cursor_col, cursor_row)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Editor<'_> {
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
_max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let indices = self.indices(widthdb, max_width);
|
||||
let rows = self.rows(&indices);
|
||||
|
||||
let width = rows
|
||||
.iter()
|
||||
.map(|row| widthdb.width(row.text()))
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
// One extra column for cursor
|
||||
.saturating_add(1);
|
||||
let height = rows.len();
|
||||
|
||||
let width: u16 = width.try_into().unwrap_or(u16::MAX);
|
||||
let height: u16 = height.try_into().unwrap_or(u16::MAX);
|
||||
Ok(Size::new(width, height))
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let size = frame.size();
|
||||
let indices = self.indices(frame.widthdb(), Some(size.width));
|
||||
let rows = self.rows(&indices);
|
||||
let cursor = self.cursor(frame.widthdb(), size.width, &indices, &rows);
|
||||
|
||||
for (i, row) in rows.into_iter().enumerate() {
|
||||
frame.write(Pos::new(0, i as i32), row);
|
||||
}
|
||||
|
||||
if self.focus {
|
||||
frame.set_cursor(Some(cursor));
|
||||
}
|
||||
self.state.last_cursor_pos = cursor;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
macro_rules! mk_either {
|
||||
(
|
||||
pub enum $name:ident {
|
||||
$( $constr:ident($ty:ident), )+
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum $name< $( $ty ),+ > {
|
||||
$( $constr($ty), )+
|
||||
}
|
||||
|
||||
impl<E, $( $ty ),+> Widget<E> for $name< $( $ty ),+ >
|
||||
where
|
||||
$( $ty: Widget<E>, )+
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
match self {
|
||||
$( Self::$constr(w) => w.size(widthdb, max_width, max_height), )+
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
match self {
|
||||
$( Self::$constr(w) => w.draw(frame), )+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, $( $ty ),+> AsyncWidget<E> for $name< $( $ty ),+ >
|
||||
where
|
||||
$( $ty: AsyncWidget<E> + Send + Sync, )+
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
match self {
|
||||
$( Self::$constr(w) => w.size(widthdb, max_width, max_height).await, )+
|
||||
}
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
match self {
|
||||
$( Self::$constr(w) => w.draw(frame).await, )+
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either2 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
}
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either3 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
Third(I3),
|
||||
}
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either4 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
Third(I3),
|
||||
Fourth(I4),
|
||||
}
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either5 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
Third(I3),
|
||||
Fourth(I4),
|
||||
Fifth(I5),
|
||||
}
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either6 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
Third(I3),
|
||||
Fourth(I4),
|
||||
Fifth(I5),
|
||||
Sixth(I6),
|
||||
}
|
||||
}
|
||||
|
||||
mk_either! {
|
||||
pub enum Either7 {
|
||||
First(I1),
|
||||
Second(I2),
|
||||
Third(I3),
|
||||
Fourth(I4),
|
||||
Fifth(I5),
|
||||
Sixth(I6),
|
||||
Seventh(I7),
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
use crate::{Frame, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Empty {
|
||||
pub size: Size,
|
||||
}
|
||||
|
||||
impl Empty {
|
||||
pub fn new() -> Self {
|
||||
Self { size: Size::ZERO }
|
||||
}
|
||||
|
||||
pub fn with_width(mut self, width: u16) -> Self {
|
||||
self.size.width = width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_height(mut self, height: u16) -> Self {
|
||||
self.size.height = height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_size(mut self, size: Size) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Empty {
|
||||
fn size(
|
||||
&self,
|
||||
_widthdb: &mut WidthDb,
|
||||
_max_width: Option<u16>,
|
||||
_max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
Ok(self.size)
|
||||
}
|
||||
|
||||
fn draw(self, _frame: &mut Frame) -> Result<(), E> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Float<I> {
|
||||
pub inner: I,
|
||||
horizontal: Option<f32>,
|
||||
vertical: Option<f32>,
|
||||
}
|
||||
|
||||
impl<I> Float<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
horizontal: None,
|
||||
vertical: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(&self) -> Option<f32> {
|
||||
self.horizontal
|
||||
}
|
||||
|
||||
pub fn set_horizontal(&mut self, position: Option<f32>) {
|
||||
if let Some(position) = position {
|
||||
assert!((0.0..=1.0).contains(&position));
|
||||
}
|
||||
self.horizontal = position;
|
||||
}
|
||||
|
||||
pub fn vertical(&self) -> Option<f32> {
|
||||
self.vertical
|
||||
}
|
||||
|
||||
pub fn set_vertical(&mut self, position: Option<f32>) {
|
||||
if let Some(position) = position {
|
||||
assert!((0.0..=1.0).contains(&position));
|
||||
}
|
||||
self.vertical = position;
|
||||
}
|
||||
|
||||
pub fn with_horizontal(mut self, position: f32) -> Self {
|
||||
self.set_horizontal(Some(position));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_vertical(mut self, position: f32) -> Self {
|
||||
self.set_vertical(Some(position));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_all(self, position: f32) -> Self {
|
||||
self.with_horizontal(position).with_vertical(position)
|
||||
}
|
||||
|
||||
pub fn with_left(self) -> Self {
|
||||
self.with_horizontal(0.0)
|
||||
}
|
||||
|
||||
pub fn with_right(self) -> Self {
|
||||
self.with_horizontal(1.0)
|
||||
}
|
||||
|
||||
pub fn with_top(self) -> Self {
|
||||
self.with_vertical(0.0)
|
||||
}
|
||||
|
||||
pub fn with_bottom(self) -> Self {
|
||||
self.with_vertical(1.0)
|
||||
}
|
||||
|
||||
pub fn with_center_h(self) -> Self {
|
||||
self.with_horizontal(0.5)
|
||||
}
|
||||
|
||||
pub fn with_center_v(self) -> Self {
|
||||
self.with_vertical(0.5)
|
||||
}
|
||||
|
||||
pub fn with_center(self) -> Self {
|
||||
self.with_all(0.5)
|
||||
}
|
||||
|
||||
fn push_inner(&self, frame: &mut Frame, size: Size, mut inner_size: Size) {
|
||||
let mut inner_pos = Pos::ZERO;
|
||||
|
||||
if let Some(horizontal) = self.horizontal {
|
||||
let available = size.width.saturating_sub(inner_size.width) as f32;
|
||||
// Biased towards the left if horizontal lands exactly on the
|
||||
// boundary between two cells
|
||||
inner_pos.x = (horizontal * available).floor().min(available) as i32;
|
||||
inner_size.width = inner_size.width.min(size.width);
|
||||
} else {
|
||||
inner_size.width = size.width;
|
||||
}
|
||||
|
||||
if let Some(vertical) = self.vertical {
|
||||
let available = size.height.saturating_sub(inner_size.height) as f32;
|
||||
// Biased towards the top if vertical lands exactly on the boundary
|
||||
// between two cells
|
||||
inner_pos.y = (vertical * available).floor().min(available) as i32;
|
||||
inner_size.height = inner_size.height.min(size.height);
|
||||
} else {
|
||||
inner_size.height = size.height;
|
||||
}
|
||||
|
||||
frame.push(inner_pos, inner_size);
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Float<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let size = frame.size();
|
||||
let inner_size = self
|
||||
.inner
|
||||
.size(frame.widthdb(), Some(size.width), Some(size.height))?;
|
||||
|
||||
self.push_inner(frame, size, inner_size);
|
||||
self.inner.draw(frame)?;
|
||||
frame.pop();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Float<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let size = frame.size();
|
||||
let inner_size = self
|
||||
.inner
|
||||
.size(frame.widthdb(), Some(size.width), Some(size.height))
|
||||
.await?;
|
||||
|
||||
self.push_inner(frame, size, inner_size);
|
||||
self.inner.draw(frame).await?;
|
||||
frame.pop();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,721 +0,0 @@
|
|||
use std::cmp::Ordering;
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb};
|
||||
|
||||
// The following algorithm has three goals, listed in order of importance:
|
||||
//
|
||||
// 1. Use the available space
|
||||
// 2. Avoid shrinking segments where possible
|
||||
// 3. Match the given weights as closely as possible
|
||||
//
|
||||
// Its input is a list of weighted segments where each segment wants to use a
|
||||
// certain amount of space. The weights signify how the available space would be
|
||||
// assigned if goal 2 was irrelevant.
|
||||
//
|
||||
// First, the algorithm must decide whether it must grow or shrink segments.
|
||||
// Because goal 2 has a higher priority than goal 3, it never makes sense to
|
||||
// shrink a segment in order to make another larger. In both cases, a segment's
|
||||
// actual size is compared to its allotment, i. e. what size it should be based
|
||||
// on its weight.
|
||||
//
|
||||
// Growth
|
||||
// ======
|
||||
//
|
||||
// If segments must be grown, an important observation can be made: If all
|
||||
// segments are smaller than their allotment, then each segment can be assigned
|
||||
// its allotment without violating goal 2, thereby fulfilling goal 3.
|
||||
//
|
||||
// Another important observation can be made: If a segment is at least as large
|
||||
// as its allotment, it must never be grown as that would violate goal 3.
|
||||
//
|
||||
// Based on these two observations, the growth algorithm first repeatedly
|
||||
// removes all segments that are at least as large as their allotment. It then
|
||||
// resizes the remaining segments to their allotments.
|
||||
//
|
||||
// Shrinkage
|
||||
// =========
|
||||
//
|
||||
// If segments must be shrunk, an important observation can be made: If all
|
||||
// segments are larger than their allotment, then each segment can be assigned
|
||||
// its allotment, thereby fulfilling goal 3. Since goal 1 is more important than
|
||||
// goal 2, we know that some elements must be shrunk.
|
||||
//
|
||||
// Another important observation can be made: If a segment is at least as small
|
||||
// as its allotment, it must never be shrunk as that would violate goal 3.
|
||||
//
|
||||
// Based on these two observations, the shrinkage algorithm first repeatedly
|
||||
// removes all segments that are at least as small as their allotment. It then
|
||||
// resizes the remaining segments to their allotments.
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Segment {
|
||||
major: u16,
|
||||
minor: u16,
|
||||
weight: f32,
|
||||
growing: bool,
|
||||
shrinking: bool,
|
||||
}
|
||||
|
||||
impl Segment {
|
||||
fn new<I>(major_minor: (u16, u16), segment: &JoinSegment<I>) -> Self {
|
||||
Self {
|
||||
major: major_minor.0,
|
||||
minor: major_minor.1,
|
||||
weight: segment.weight,
|
||||
growing: segment.growing,
|
||||
shrinking: segment.shrinking,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn total_size(segments: &[&mut Segment]) -> u16 {
|
||||
let mut total = 0_u16;
|
||||
for segment in segments {
|
||||
total = total.saturating_add(segment.major);
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn total_weight(segments: &[&mut Segment]) -> f32 {
|
||||
segments.iter().map(|s| s.weight).sum()
|
||||
}
|
||||
|
||||
fn balance(segments: &mut [Segment], available: u16) {
|
||||
let segments = segments.iter_mut().collect::<Vec<_>>();
|
||||
match total_size(&segments).cmp(&available) {
|
||||
Ordering::Less => grow(segments, available),
|
||||
Ordering::Greater => shrink(segments, available),
|
||||
Ordering::Equal => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn grow(mut segments: Vec<&mut Segment>, mut available: u16) {
|
||||
assert!(available >= total_size(&segments));
|
||||
|
||||
// Only grow segments that can be grown.
|
||||
segments.retain(|s| {
|
||||
if s.growing {
|
||||
return true;
|
||||
}
|
||||
available = available.saturating_sub(s.major);
|
||||
false
|
||||
});
|
||||
|
||||
// Repeatedly remove all segments that do not need to grow, i. e. that are
|
||||
// at least as large as their allotment.
|
||||
loop {
|
||||
let mut total_weight = total_weight(&segments);
|
||||
|
||||
// If there are no segments with a weight > 0, space is distributed
|
||||
// evenly among all remaining segments.
|
||||
if total_weight <= 0.0 {
|
||||
for segment in &mut segments {
|
||||
segment.weight = 1.0;
|
||||
}
|
||||
total_weight = segments.len() as f32;
|
||||
}
|
||||
|
||||
let mut removed = 0;
|
||||
segments.retain(|s| {
|
||||
let allotment = s.weight / total_weight * available as f32;
|
||||
if (s.major as f32) < allotment {
|
||||
return true; // May need to grow
|
||||
}
|
||||
removed += s.major;
|
||||
false
|
||||
});
|
||||
available -= removed;
|
||||
|
||||
if removed == 0 {
|
||||
break; // All remaining segments are smaller than their allotments
|
||||
}
|
||||
}
|
||||
|
||||
let total_weight = segments.iter().map(|s| s.weight).sum::<f32>();
|
||||
if total_weight <= 0.0 {
|
||||
return; // No more segments left
|
||||
}
|
||||
|
||||
// Size each remaining segment according to its allotment.
|
||||
let mut used = 0;
|
||||
for segment in &mut segments {
|
||||
let allotment = segment.weight / total_weight * available as f32;
|
||||
segment.major = allotment.floor() as u16;
|
||||
used += segment.major;
|
||||
}
|
||||
|
||||
// Distribute remaining unused space from left to right.
|
||||
//
|
||||
// The rounding error on each segment is at most 1, so we only need to loop
|
||||
// over the segments once.
|
||||
let remaining = available - used;
|
||||
assert!(remaining as usize <= segments.len());
|
||||
for segment in segments.into_iter().take(remaining.into()) {
|
||||
segment.major += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn shrink(mut segments: Vec<&mut Segment>, mut available: u16) {
|
||||
assert!(available <= total_size(&segments));
|
||||
|
||||
// Only shrink segments that can be shrunk.
|
||||
segments.retain(|s| {
|
||||
if s.shrinking {
|
||||
return true;
|
||||
}
|
||||
available = available.saturating_sub(s.major);
|
||||
false
|
||||
});
|
||||
|
||||
// Repeatedly remove all segments that do not need to shrink, i. e. that are
|
||||
// at least as small as their allotment.
|
||||
loop {
|
||||
let mut total_weight = total_weight(&segments);
|
||||
|
||||
// If there are no segments with a weight > 0, space is distributed
|
||||
// evenly among all remaining segments.
|
||||
if total_weight <= 0.0 {
|
||||
for segment in &mut segments {
|
||||
segment.weight = 1.0;
|
||||
}
|
||||
total_weight = segments.len() as f32;
|
||||
}
|
||||
|
||||
let mut removed = 0;
|
||||
segments.retain(|s| {
|
||||
let allotment = s.weight / total_weight * available as f32;
|
||||
if (s.major as f32) > allotment {
|
||||
return true; // May need to shrink
|
||||
}
|
||||
|
||||
// The segment size subtracted from `available` is always smaller
|
||||
// than or equal to its allotment. Since `available` is the sum of
|
||||
// all allotments, it can never go below 0.
|
||||
assert!(s.major <= available);
|
||||
|
||||
removed += s.major;
|
||||
false
|
||||
});
|
||||
available -= removed;
|
||||
|
||||
if removed == 0 {
|
||||
break; // All segments want more than their weight allows.
|
||||
}
|
||||
}
|
||||
|
||||
let total_weight = segments.iter().map(|s| s.weight).sum::<f32>();
|
||||
if total_weight <= 0.0 {
|
||||
return; // No more segments left
|
||||
}
|
||||
|
||||
// Size each remaining segment according to its allotment.
|
||||
let mut used = 0;
|
||||
for segment in &mut segments {
|
||||
let allotment = segment.weight / total_weight * available as f32;
|
||||
segment.major = allotment.floor() as u16;
|
||||
used += segment.major;
|
||||
}
|
||||
|
||||
// Distribute remaining unused space from left to right.
|
||||
//
|
||||
// The rounding error on each segment is at most 1, so we only need to loop
|
||||
// over the segments once.
|
||||
let remaining = available - used;
|
||||
assert!(remaining as usize <= segments.len());
|
||||
for segment in segments.into_iter().take(remaining.into()) {
|
||||
segment.major += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct JoinSegment<I> {
|
||||
pub inner: I,
|
||||
weight: f32,
|
||||
pub growing: bool,
|
||||
pub shrinking: bool,
|
||||
}
|
||||
|
||||
impl<I> JoinSegment<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
weight: 1.0,
|
||||
growing: true,
|
||||
shrinking: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weight(&self) -> f32 {
|
||||
self.weight
|
||||
}
|
||||
|
||||
pub fn set_weight(&mut self, weight: f32) {
|
||||
assert!(weight >= 0.0);
|
||||
self.weight = weight;
|
||||
}
|
||||
|
||||
pub fn with_weight(mut self, weight: f32) -> Self {
|
||||
self.set_weight(weight);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_growing(mut self, enabled: bool) -> Self {
|
||||
self.growing = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_shrinking(mut self, enabled: bool) -> Self {
|
||||
self.shrinking = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_fixed(self, fixed: bool) -> Self {
|
||||
self.with_growing(!fixed).with_shrinking(!fixed)
|
||||
}
|
||||
}
|
||||
|
||||
fn to_mm<T>(horizontal: bool, w: T, h: T) -> (T, T) {
|
||||
if horizontal {
|
||||
(w, h)
|
||||
} else {
|
||||
(h, w)
|
||||
}
|
||||
}
|
||||
|
||||
fn from_mm<T>(horizontal: bool, major: T, minor: T) -> (T, T) {
|
||||
if horizontal {
|
||||
(major, minor)
|
||||
} else {
|
||||
(minor, major)
|
||||
}
|
||||
}
|
||||
|
||||
fn size<E, I: Widget<E>>(
|
||||
horizontal: bool,
|
||||
widthdb: &mut WidthDb,
|
||||
segment: &JoinSegment<I>,
|
||||
major: Option<u16>,
|
||||
minor: Option<u16>,
|
||||
) -> Result<(u16, u16), E> {
|
||||
if horizontal {
|
||||
let size = segment.inner.size(widthdb, major, minor)?;
|
||||
Ok((size.width, size.height))
|
||||
} else {
|
||||
let size = segment.inner.size(widthdb, minor, major)?;
|
||||
Ok((size.height, size.width))
|
||||
}
|
||||
}
|
||||
|
||||
fn size_with_balanced<E, I: Widget<E>>(
|
||||
horizontal: bool,
|
||||
widthdb: &mut WidthDb,
|
||||
segment: &JoinSegment<I>,
|
||||
balanced: &Segment,
|
||||
minor: Option<u16>,
|
||||
) -> Result<(u16, u16), E> {
|
||||
size(horizontal, widthdb, segment, Some(balanced.major), minor)
|
||||
}
|
||||
|
||||
async fn size_async<E, I: AsyncWidget<E>>(
|
||||
horizontal: bool,
|
||||
widthdb: &mut WidthDb,
|
||||
segment: &JoinSegment<I>,
|
||||
major: Option<u16>,
|
||||
minor: Option<u16>,
|
||||
) -> Result<(u16, u16), E> {
|
||||
if horizontal {
|
||||
let size = segment.inner.size(widthdb, major, minor).await?;
|
||||
Ok((size.width, size.height))
|
||||
} else {
|
||||
let size = segment.inner.size(widthdb, minor, major).await?;
|
||||
Ok((size.height, size.width))
|
||||
}
|
||||
}
|
||||
|
||||
async fn size_async_with_balanced<E, I: AsyncWidget<E>>(
|
||||
horizontal: bool,
|
||||
widthdb: &mut WidthDb,
|
||||
segment: &JoinSegment<I>,
|
||||
balanced: &Segment,
|
||||
minor: Option<u16>,
|
||||
) -> Result<(u16, u16), E> {
|
||||
size_async(horizontal, widthdb, segment, Some(balanced.major), minor).await
|
||||
}
|
||||
|
||||
fn sum_major_max_minor(segments: &[Segment]) -> (u16, u16) {
|
||||
let mut major = 0_u16;
|
||||
let mut minor = 0_u16;
|
||||
for segment in segments {
|
||||
major = major.saturating_add(segment.major);
|
||||
minor = minor.max(segment.minor);
|
||||
}
|
||||
(major, minor)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Join<I> {
|
||||
horizontal: bool,
|
||||
segments: Vec<JoinSegment<I>>,
|
||||
}
|
||||
|
||||
impl<I> Join<I> {
|
||||
pub fn horizontal(segments: Vec<JoinSegment<I>>) -> Self {
|
||||
Self {
|
||||
horizontal: true,
|
||||
segments,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(segments: Vec<JoinSegment<I>>) -> Self {
|
||||
Self {
|
||||
horizontal: false,
|
||||
segments,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Join<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height);
|
||||
|
||||
let mut segments = Vec::with_capacity(self.segments.len());
|
||||
for segment in &self.segments {
|
||||
let major_minor = size(self.horizontal, widthdb, segment, None, max_minor)?;
|
||||
segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
|
||||
if let Some(available) = max_major {
|
||||
balance(&mut segments, available);
|
||||
|
||||
let mut new_segments = Vec::with_capacity(self.segments.len());
|
||||
for (segment, balanced) in self.segments.iter().zip(segments.into_iter()) {
|
||||
let major_minor =
|
||||
size_with_balanced(self.horizontal, widthdb, segment, &balanced, max_minor)?;
|
||||
new_segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
segments = new_segments;
|
||||
}
|
||||
|
||||
let (major, minor) = sum_major_max_minor(&segments);
|
||||
let (width, height) = from_mm(self.horizontal, major, minor);
|
||||
Ok(Size::new(width, height))
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let frame_size = frame.size();
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height);
|
||||
|
||||
let widthdb = frame.widthdb();
|
||||
let mut segments = Vec::with_capacity(self.segments.len());
|
||||
for segment in &self.segments {
|
||||
let major_minor = size(self.horizontal, widthdb, segment, None, Some(max_minor))?;
|
||||
segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
balance(&mut segments, max_major);
|
||||
|
||||
let mut major = 0_i32;
|
||||
for (segment, balanced) in self.segments.into_iter().zip(segments.into_iter()) {
|
||||
let (x, y) = from_mm(self.horizontal, major, 0);
|
||||
let (w, h) = from_mm(self.horizontal, balanced.major, max_minor);
|
||||
frame.push(Pos::new(x, y), Size::new(w, h));
|
||||
segment.inner.draw(frame)?;
|
||||
frame.pop();
|
||||
major += balanced.major as i32;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Join<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height);
|
||||
|
||||
let mut segments = Vec::with_capacity(self.segments.len());
|
||||
for segment in &self.segments {
|
||||
let major_minor =
|
||||
size_async(self.horizontal, widthdb, segment, None, max_minor).await?;
|
||||
segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
|
||||
if let Some(available) = max_major {
|
||||
balance(&mut segments, available);
|
||||
|
||||
let mut new_segments = Vec::with_capacity(self.segments.len());
|
||||
for (segment, balanced) in self.segments.iter().zip(segments.into_iter()) {
|
||||
let major_minor = size_async_with_balanced(
|
||||
self.horizontal,
|
||||
widthdb,
|
||||
segment,
|
||||
&balanced,
|
||||
max_minor,
|
||||
)
|
||||
.await?;
|
||||
new_segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
segments = new_segments;
|
||||
}
|
||||
|
||||
let (major, minor) = sum_major_max_minor(&segments);
|
||||
let (width, height) = from_mm(self.horizontal, major, minor);
|
||||
Ok(Size::new(width, height))
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let frame_size = frame.size();
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height);
|
||||
|
||||
let widthdb = frame.widthdb();
|
||||
let mut segments = Vec::with_capacity(self.segments.len());
|
||||
for segment in &self.segments {
|
||||
let major_minor =
|
||||
size_async(self.horizontal, widthdb, segment, None, Some(max_minor)).await?;
|
||||
segments.push(Segment::new(major_minor, segment));
|
||||
}
|
||||
balance(&mut segments, max_major);
|
||||
|
||||
let mut major = 0_i32;
|
||||
for (segment, balanced) in self.segments.into_iter().zip(segments.into_iter()) {
|
||||
let (x, y) = from_mm(self.horizontal, major, 0);
|
||||
let (w, h) = from_mm(self.horizontal, balanced.major, max_minor);
|
||||
frame.push(Pos::new(x, y), Size::new(w, h));
|
||||
segment.inner.draw(frame).await?;
|
||||
frame.pop();
|
||||
major += balanced.major as i32;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mk_join {
|
||||
(
|
||||
pub struct $name:ident {
|
||||
$( pub $arg:ident: $type:ident [$n:expr], )+
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct $name< $($type),+ >{
|
||||
horizontal: bool,
|
||||
$( pub $arg: JoinSegment<$type>, )+
|
||||
}
|
||||
|
||||
impl< $($type),+ > $name< $($type),+ >{
|
||||
pub fn horizontal( $($arg: JoinSegment<$type>),+ ) -> Self {
|
||||
Self { horizontal: true, $( $arg, )+ }
|
||||
}
|
||||
|
||||
pub fn vertical( $($arg: JoinSegment<$type>),+ ) -> Self {
|
||||
Self { horizontal: false, $( $arg, )+ }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, $($type),+ > Widget<E> for $name< $($type),+ >
|
||||
where
|
||||
$( $type: Widget<E>, )+
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height);
|
||||
|
||||
let mut segments = [ $(
|
||||
Segment::new(
|
||||
size(self.horizontal, widthdb, &self.$arg, None, max_minor)?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
|
||||
if let Some(available) = max_major {
|
||||
balance(&mut segments, available);
|
||||
|
||||
let new_segments = [ $(
|
||||
Segment::new(
|
||||
size_with_balanced(self.horizontal, widthdb, &self.$arg, &segments[$n], max_minor)?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
segments = new_segments;
|
||||
}
|
||||
|
||||
let (major, minor) = sum_major_max_minor(&segments);
|
||||
let (width, height) = from_mm(self.horizontal, major, minor);
|
||||
Ok(Size::new(width, height))
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let frame_size = frame.size();
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height);
|
||||
|
||||
let widthdb = frame.widthdb();
|
||||
let mut segments = [ $(
|
||||
Segment::new(
|
||||
size(self.horizontal, widthdb, &self.$arg, None, Some(max_minor))?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
balance(&mut segments, max_major);
|
||||
|
||||
let mut major = 0_i32;
|
||||
$( {
|
||||
let balanced = &segments[$n];
|
||||
let (x, y) = from_mm(self.horizontal, major, 0);
|
||||
let (w, h) = from_mm(self.horizontal, balanced.major, max_minor);
|
||||
frame.push(Pos::new(x, y), Size::new(w, h));
|
||||
self.$arg.inner.draw(frame)?;
|
||||
frame.pop();
|
||||
major += balanced.major as i32;
|
||||
} )*
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, $($type),+ > AsyncWidget<E> for $name< $($type),+ >
|
||||
where
|
||||
E: Send,
|
||||
$( $type: AsyncWidget<E> + Send + Sync, )+
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, max_width, max_height);
|
||||
|
||||
let mut segments = [ $(
|
||||
Segment::new(
|
||||
size_async(self.horizontal, widthdb, &self.$arg, None, max_minor).await?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
|
||||
if let Some(available) = max_major {
|
||||
balance(&mut segments, available);
|
||||
|
||||
let new_segments = [ $(
|
||||
Segment::new(
|
||||
size_async_with_balanced(self.horizontal, widthdb, &self.$arg, &segments[$n], max_minor).await?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
segments = new_segments;
|
||||
}
|
||||
|
||||
let (major, minor) = sum_major_max_minor(&segments);
|
||||
let (width, height) = from_mm(self.horizontal, major, minor);
|
||||
Ok(Size::new(width, height))
|
||||
}
|
||||
|
||||
#[allow(unused_assignments)]
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let frame_size = frame.size();
|
||||
let (max_major, max_minor) = to_mm(self.horizontal, frame_size.width, frame_size.height);
|
||||
|
||||
let widthdb = frame.widthdb();
|
||||
let mut segments = [ $(
|
||||
Segment::new(
|
||||
size_async(self.horizontal, widthdb, &self.$arg, None, Some(max_minor)).await?,
|
||||
&self.$arg,
|
||||
),
|
||||
)+ ];
|
||||
balance(&mut segments, max_major);
|
||||
|
||||
let mut major = 0_i32;
|
||||
$( {
|
||||
let balanced = &segments[$n];
|
||||
let (x, y) = from_mm(self.horizontal, major, 0);
|
||||
let (w, h) = from_mm(self.horizontal, balanced.major, max_minor);
|
||||
frame.push(Pos::new(x, y), Size::new(w, h));
|
||||
self.$arg.inner.draw(frame).await?;
|
||||
frame.pop();
|
||||
major += balanced.major as i32;
|
||||
} )*
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join2 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
}
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join3 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
pub third: I3 [2],
|
||||
}
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join4 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
pub third: I3 [2],
|
||||
pub fourth: I4 [3],
|
||||
}
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join5 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
pub third: I3 [2],
|
||||
pub fourth: I4 [3],
|
||||
pub fifth: I5 [4],
|
||||
}
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join6 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
pub third: I3 [2],
|
||||
pub fourth: I4 [3],
|
||||
pub fifth: I5 [4],
|
||||
pub sixth: I6 [5],
|
||||
}
|
||||
}
|
||||
|
||||
mk_join! {
|
||||
pub struct Join7 {
|
||||
pub first: I1 [0],
|
||||
pub second: I2 [1],
|
||||
pub third: I3 [2],
|
||||
pub fourth: I4 [3],
|
||||
pub fifth: I5 [4],
|
||||
pub sixth: I6 [5],
|
||||
pub seventh: I7 [6],
|
||||
}
|
||||
}
|
||||
|
|
@ -1,201 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Layer<I> {
|
||||
layers: Vec<I>,
|
||||
}
|
||||
|
||||
impl<I> Layer<I> {
|
||||
pub fn new(layers: Vec<I>) -> Self {
|
||||
Self { layers }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Layer<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let mut size = Size::ZERO;
|
||||
for layer in &self.layers {
|
||||
let lsize = layer.size(widthdb, max_width, max_height)?;
|
||||
size.width = size.width.max(lsize.width);
|
||||
size.height = size.height.max(lsize.height);
|
||||
}
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
for layer in self.layers {
|
||||
layer.draw(frame)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Layer<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let mut size = Size::ZERO;
|
||||
for layer in &self.layers {
|
||||
let lsize = layer.size(widthdb, max_width, max_height).await?;
|
||||
size.width = size.width.max(lsize.width);
|
||||
size.height = size.height.max(lsize.height);
|
||||
}
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
for layer in self.layers {
|
||||
layer.draw(frame).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mk_layer {
|
||||
(
|
||||
pub struct $name:ident {
|
||||
$( pub $arg:ident: $type:ident, )+
|
||||
}
|
||||
) => {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct $name< $($type),+ >{
|
||||
$( pub $arg: $type, )+
|
||||
}
|
||||
|
||||
impl< $($type),+ > $name< $($type),+ >{
|
||||
pub fn new( $($arg: $type),+ ) -> Self {
|
||||
Self { $( $arg, )+ }
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, $($type),+ > Widget<E> for $name< $($type),+ >
|
||||
where
|
||||
$( $type: Widget<E>, )+
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let mut size = Size::ZERO;
|
||||
|
||||
$({
|
||||
let lsize = self.$arg.size(widthdb, max_width, max_height)?;
|
||||
size.width = size.width.max(lsize.width);
|
||||
size.height = size.height.max(lsize.height);
|
||||
})+
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
$( self.$arg.draw(frame)?; )+
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, $($type),+ > AsyncWidget<E> for $name< $($type),+ >
|
||||
where
|
||||
E: Send,
|
||||
$( $type: AsyncWidget<E> + Send + Sync, )+
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let mut size = Size::ZERO;
|
||||
|
||||
$({
|
||||
let lsize = self.$arg.size(widthdb, max_width, max_height).await?;
|
||||
size.width = size.width.max(lsize.width);
|
||||
size.height = size.height.max(lsize.height);
|
||||
})+
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
$( self.$arg.draw(frame).await?; )+
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer2 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
}
|
||||
);
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer3 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
pub third: I3,
|
||||
}
|
||||
);
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer4 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
pub third: I3,
|
||||
pub fourth: I4,
|
||||
}
|
||||
);
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer5 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
pub third: I3,
|
||||
pub fourth: I4,
|
||||
pub fifth: I5,
|
||||
}
|
||||
);
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer6 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
pub third: I3,
|
||||
pub fourth: I4,
|
||||
pub fifth: I5,
|
||||
pub sixth: I6,
|
||||
}
|
||||
);
|
||||
|
||||
mk_layer!(
|
||||
pub struct Layer7 {
|
||||
pub first: I1,
|
||||
pub second: I2,
|
||||
pub third: I3,
|
||||
pub fourth: I4,
|
||||
pub fifth: I5,
|
||||
pub sixth: I6,
|
||||
pub seventh: I7,
|
||||
}
|
||||
);
|
||||
|
|
@ -1,133 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Padding<I> {
|
||||
pub inner: I,
|
||||
pub left: u16,
|
||||
pub right: u16,
|
||||
pub top: u16,
|
||||
pub bottom: u16,
|
||||
pub stretch: bool,
|
||||
}
|
||||
|
||||
impl<I> Padding<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
stretch: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_left(mut self, amount: u16) -> Self {
|
||||
self.left = amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_right(mut self, amount: u16) -> Self {
|
||||
self.right = amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_top(mut self, amount: u16) -> Self {
|
||||
self.top = amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_bottom(mut self, amount: u16) -> Self {
|
||||
self.bottom = amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_horizontal(self, amount: u16) -> Self {
|
||||
self.with_left(amount).with_right(amount)
|
||||
}
|
||||
|
||||
pub fn with_vertical(self, amount: u16) -> Self {
|
||||
self.with_top(amount).with_bottom(amount)
|
||||
}
|
||||
|
||||
pub fn with_all(self, amount: u16) -> Self {
|
||||
self.with_horizontal(amount).with_vertical(amount)
|
||||
}
|
||||
|
||||
pub fn with_stretch(mut self, stretch: bool) -> Self {
|
||||
self.stretch = stretch;
|
||||
self
|
||||
}
|
||||
|
||||
fn pad_size(&self) -> Size {
|
||||
Size::new(self.left + self.right, self.top + self.bottom)
|
||||
}
|
||||
|
||||
fn push_inner(&self, frame: &mut Frame) {
|
||||
let size = frame.size();
|
||||
let pad_size = self.pad_size();
|
||||
let inner_size = size.saturating_sub(pad_size);
|
||||
frame.push(Pos::new(self.left.into(), self.top.into()), inner_size);
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Padding<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let pad_size = self.pad_size();
|
||||
let max_width = max_width.map(|w| w.saturating_sub(pad_size.width));
|
||||
let max_height = max_height.map(|h| h.saturating_sub(pad_size.height));
|
||||
let size = self.inner.size(widthdb, max_width, max_height)?;
|
||||
Ok(size + pad_size)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
if self.stretch {
|
||||
self.inner.draw(frame)?;
|
||||
} else {
|
||||
self.push_inner(frame);
|
||||
self.inner.draw(frame)?;
|
||||
frame.pop();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Padding<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let pad_size = self.pad_size();
|
||||
let max_width = max_width.map(|w| w.saturating_sub(pad_size.width));
|
||||
let max_height = max_height.map(|h| h.saturating_sub(pad_size.height));
|
||||
let size = self.inner.size(widthdb, max_width, max_height).await?;
|
||||
Ok(size + pad_size)
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
if self.stretch {
|
||||
self.inner.draw(frame).await?;
|
||||
} else {
|
||||
self.push_inner(frame);
|
||||
self.inner.draw(frame).await?;
|
||||
frame.pop();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
use std::mem;
|
||||
|
||||
use crate::buffer::Buffer;
|
||||
use crate::{AsyncWidget, Frame, Pos, Size, Style, Styled, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Predrawn {
|
||||
buffer: Buffer,
|
||||
}
|
||||
|
||||
impl Predrawn {
|
||||
pub fn new<E, W: Widget<E>>(inner: W, widthdb: &mut WidthDb) -> Result<Self, E> {
|
||||
let mut tmp_frame = Frame::default();
|
||||
|
||||
let size = inner.size(widthdb, None, None)?;
|
||||
tmp_frame.buffer.resize(size);
|
||||
|
||||
mem::swap(widthdb, &mut tmp_frame.widthdb);
|
||||
inner.draw(&mut tmp_frame)?;
|
||||
mem::swap(widthdb, &mut tmp_frame.widthdb);
|
||||
|
||||
let buffer = tmp_frame.buffer;
|
||||
Ok(Self { buffer })
|
||||
}
|
||||
|
||||
pub async fn new_async<E, W: AsyncWidget<E>>(
|
||||
inner: W,
|
||||
widthdb: &mut WidthDb,
|
||||
) -> Result<Self, E> {
|
||||
let mut tmp_frame = Frame::default();
|
||||
|
||||
let size = inner.size(widthdb, None, None).await?;
|
||||
tmp_frame.buffer.resize(size);
|
||||
|
||||
mem::swap(widthdb, &mut tmp_frame.widthdb);
|
||||
inner.draw(&mut tmp_frame).await?;
|
||||
mem::swap(widthdb, &mut tmp_frame.widthdb);
|
||||
|
||||
let buffer = tmp_frame.buffer;
|
||||
Ok(Self { buffer })
|
||||
}
|
||||
|
||||
pub fn size(&self) -> Size {
|
||||
self.buffer.size()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Predrawn {
|
||||
fn size(
|
||||
&self,
|
||||
_widthdb: &mut WidthDb,
|
||||
_max_width: Option<u16>,
|
||||
_max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
Ok(self.buffer.size())
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
for (x, y, cell) in self.buffer.cells() {
|
||||
let pos = Pos::new(x.into(), y.into());
|
||||
let style = Style {
|
||||
content_style: cell.style,
|
||||
opaque: true,
|
||||
};
|
||||
frame.write(pos, Styled::new(&cell.content, style));
|
||||
}
|
||||
|
||||
if let Some(cursor) = self.buffer.cursor() {
|
||||
frame.set_cursor(Some(cursor));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Resize<I> {
|
||||
pub inner: I,
|
||||
pub min_width: Option<u16>,
|
||||
pub min_height: Option<u16>,
|
||||
pub max_width: Option<u16>,
|
||||
pub max_height: Option<u16>,
|
||||
}
|
||||
|
||||
impl<I> Resize<I> {
|
||||
pub fn new(inner: I) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
min_width: None,
|
||||
min_height: None,
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_min_width(mut self, width: u16) -> Self {
|
||||
self.min_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_min_height(mut self, height: u16) -> Self {
|
||||
self.min_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_width(mut self, width: u16) -> Self {
|
||||
self.max_width = Some(width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_max_height(mut self, height: u16) -> Self {
|
||||
self.max_height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
fn presize(
|
||||
&self,
|
||||
mut width: Option<u16>,
|
||||
mut height: Option<u16>,
|
||||
) -> (Option<u16>, Option<u16>) {
|
||||
if let Some(mw) = self.max_width {
|
||||
width = Some(width.unwrap_or(mw).min(mw));
|
||||
}
|
||||
if let Some(mh) = self.max_height {
|
||||
height = Some(height.unwrap_or(mh).max(mh));
|
||||
}
|
||||
(width, height)
|
||||
}
|
||||
|
||||
fn resize(&self, size: Size) -> Size {
|
||||
let mut width = size.width;
|
||||
let mut height = size.height;
|
||||
|
||||
if let Some(min_width) = self.min_width {
|
||||
width = width.max(min_width);
|
||||
}
|
||||
if let Some(min_height) = self.min_height {
|
||||
height = height.max(min_height);
|
||||
}
|
||||
|
||||
if let Some(max_width) = self.max_width {
|
||||
width = width.min(max_width);
|
||||
}
|
||||
if let Some(max_height) = self.max_height {
|
||||
height = height.min(max_height);
|
||||
}
|
||||
|
||||
Size::new(width, height)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Resize<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_width, max_height) = self.presize(max_width, max_height);
|
||||
let size = self.inner.size(widthdb, max_width, max_height)?;
|
||||
Ok(self.resize(size))
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Resize<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let (max_width, max_height) = self.presize(max_width, max_height);
|
||||
let size = self.inner.size(widthdb, max_width, max_height).await?;
|
||||
Ok(self.resize(size))
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame).await
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
use crate::{Frame, Pos, Size, Styled, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Text {
|
||||
pub styled: Styled,
|
||||
pub wrap: bool,
|
||||
}
|
||||
|
||||
impl Text {
|
||||
pub fn new<S: Into<Styled>>(styled: S) -> Self {
|
||||
Self {
|
||||
styled: styled.into(),
|
||||
wrap: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_wrap(mut self, active: bool) -> Self {
|
||||
self.wrap = active;
|
||||
self
|
||||
}
|
||||
|
||||
fn wrapped(&self, widthdb: &mut WidthDb, max_width: Option<u16>) -> Vec<Styled> {
|
||||
let max_width = max_width
|
||||
.filter(|_| self.wrap)
|
||||
.map(|w| w as usize)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let indices = widthdb.wrap(self.styled.text(), max_width);
|
||||
self.styled.clone().split_at_indices(&indices)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Widget<E> for Text {
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
_max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
let lines = self.wrapped(widthdb, max_width);
|
||||
|
||||
let min_width = lines
|
||||
.iter()
|
||||
.map(|l| widthdb.width(l.text().trim_end()))
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let min_height = lines.len();
|
||||
|
||||
let min_width: u16 = min_width.try_into().unwrap_or(u16::MAX);
|
||||
let min_height: u16 = min_height.try_into().unwrap_or(u16::MAX);
|
||||
Ok(Size::new(min_width, min_height))
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
let size = frame.size();
|
||||
|
||||
for (i, line) in self
|
||||
.wrapped(frame.widthdb(), Some(size.width))
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
let i: i32 = i.try_into().unwrap_or(i32::MAX);
|
||||
frame.write(Pos::new(0, i), line);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
use async_trait::async_trait;
|
||||
|
||||
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Title<I> {
|
||||
pub inner: I,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl<I> Title<I> {
|
||||
pub fn new<S: ToString>(inner: I, title: S) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
title: title.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E, I> Widget<E> for Title<I>
|
||||
where
|
||||
I: Widget<E>,
|
||||
{
|
||||
fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height)
|
||||
}
|
||||
|
||||
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame)?;
|
||||
frame.set_title(Some(self.title));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<E, I> AsyncWidget<E> for Title<I>
|
||||
where
|
||||
I: AsyncWidget<E> + Send + Sync,
|
||||
{
|
||||
async fn size(
|
||||
&self,
|
||||
widthdb: &mut WidthDb,
|
||||
max_width: Option<u16>,
|
||||
max_height: Option<u16>,
|
||||
) -> Result<Size, E> {
|
||||
self.inner.size(widthdb, max_width, max_height).await
|
||||
}
|
||||
|
||||
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||
self.inner.draw(frame).await?;
|
||||
frame.set_title(Some(self.title));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
163
src/widthdb.rs
163
src/widthdb.rs
|
|
@ -1,163 +0,0 @@
|
|||
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::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
|
||||
use crate::wrap;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum WidthEstimationMethod {
|
||||
/// Estimate the width of a grapheme using legacy methods.
|
||||
///
|
||||
/// Different terminal emulators all use different approaches to determine
|
||||
/// grapheme widths, so this method will never be able to give a fully
|
||||
/// correct solution. For that, the only possible approach is measuring the
|
||||
/// actual grapheme width.
|
||||
#[default]
|
||||
Legacy,
|
||||
|
||||
/// Estimate the width of a grapheme using the unicode standard in a
|
||||
/// best-effort manner.
|
||||
Unicode,
|
||||
}
|
||||
|
||||
/// Measures and stores the with (in terminal coordinates) of graphemes.
|
||||
#[derive(Debug)]
|
||||
pub struct WidthDb {
|
||||
pub(crate) estimate: WidthEstimationMethod,
|
||||
pub(crate) measure: bool,
|
||||
pub(crate) tab_width: u8,
|
||||
known: HashMap<String, u8>,
|
||||
requested: HashSet<String>,
|
||||
}
|
||||
|
||||
impl Default for WidthDb {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
estimate: WidthEstimationMethod::default(),
|
||||
measure: false,
|
||||
tab_width: 8,
|
||||
known: Default::default(),
|
||||
requested: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WidthDb {
|
||||
/// Determine the width of a tab character starting at the specified column.
|
||||
fn tab_width_at_column(&self, col: usize) -> u8 {
|
||||
self.tab_width - (col % self.tab_width as usize) as u8
|
||||
}
|
||||
|
||||
/// Determine the width of a grapheme.
|
||||
///
|
||||
/// If the grapheme is a tab, the column is used to determine its width.
|
||||
///
|
||||
/// If the width has not been measured yet or measurements are turned off,
|
||||
/// it is estimated using the Unicode Standard Annex #11.
|
||||
pub fn grapheme_width(&mut self, grapheme: &str, col: usize) -> u8 {
|
||||
assert_eq!(Some(grapheme), grapheme.graphemes(true).next());
|
||||
if grapheme == "\t" {
|
||||
return self.tab_width_at_column(col);
|
||||
}
|
||||
|
||||
if self.measure {
|
||||
if let Some(width) = self.known.get(grapheme) {
|
||||
return *width;
|
||||
}
|
||||
self.requested.insert(grapheme.to_string());
|
||||
}
|
||||
|
||||
match self.estimate {
|
||||
// A character-wise width calculation is a simple and obvious
|
||||
// approach to compute character widths. The idea is that dumb
|
||||
// terminal emulators tend to do something roughly like this, and
|
||||
// smart terminal emulators try to emulate dumb ones for
|
||||
// compatibility. In practice, this approach seems to be fairly
|
||||
// robust.
|
||||
WidthEstimationMethod::Legacy => grapheme
|
||||
.chars()
|
||||
.filter(|c| !c.is_ascii_control())
|
||||
.flat_map(|c| c.width())
|
||||
.sum::<usize>()
|
||||
.try_into()
|
||||
.unwrap_or(u8::MAX),
|
||||
|
||||
// The unicode width crate considers control chars to have a width
|
||||
// of 1 even though they usually have a width of 0 when displayed.
|
||||
WidthEstimationMethod::Unicode => grapheme
|
||||
.split(|c: char| c.is_ascii_control())
|
||||
.map(|s| s.width())
|
||||
.sum::<usize>()
|
||||
.try_into()
|
||||
.unwrap_or(u8::MAX),
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine the width of a string based on its graphemes.
|
||||
///
|
||||
/// If a grapheme is a tab, its column is used to determine its width.
|
||||
///
|
||||
/// If the width of a grapheme has not been measured yet or measurements are
|
||||
/// turned off, it is estimated using the Unicode Standard Annex #11.
|
||||
pub fn width(&mut self, s: &str) -> usize {
|
||||
let mut total: usize = 0;
|
||||
for grapheme in s.graphemes(true) {
|
||||
total += self.grapheme_width(grapheme, total) as usize;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
/// Perform primitive word wrapping with the specified maximum width.
|
||||
///
|
||||
/// Returns the byte offsets at which the string should be split into lines.
|
||||
/// An offset of 1 would mean the first line contains only a single byte.
|
||||
/// These offsets lie on grapheme boundaries.
|
||||
///
|
||||
/// This function does not support bidirectional script. It assumes the
|
||||
/// entire text has the same direction.
|
||||
pub fn wrap(&mut self, text: &str, width: usize) -> Vec<usize> {
|
||||
wrap::wrap(self, text, width)
|
||||
}
|
||||
|
||||
/// Whether any new graphemes have been seen since the last time
|
||||
/// [`Self::measure_widths`] was called.
|
||||
pub(crate) fn measuring_required(&self) -> bool {
|
||||
self.measure && !self.requested.is_empty()
|
||||
}
|
||||
|
||||
/// Measure the width of all new graphemes that have been seen since the
|
||||
/// last time this function was called.
|
||||
///
|
||||
/// This function measures the actual width of graphemes by writing them to
|
||||
/// the terminal. After it finishes, the terminal's contents should be
|
||||
/// assumed to be garbage and a full redraw should be performed.
|
||||
pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> {
|
||||
if !self.measure {
|
||||
return Ok(());
|
||||
}
|
||||
for grapheme in self.requested.drain() {
|
||||
if grapheme.chars().any(|c| c.is_ascii_control()) {
|
||||
// ASCII control characters like the escape character or the
|
||||
// bell character tend to be interpreted specially by terminals.
|
||||
// This may break width measurements. To avoid this, we just
|
||||
// assign each control character a with of 0.
|
||||
self.known.insert(grapheme, 0);
|
||||
continue;
|
||||
}
|
||||
|
||||
out.queue(Clear(ClearType::All))?
|
||||
.queue(MoveTo(0, 0))?
|
||||
.queue(Print(&grapheme))?;
|
||||
out.flush()?;
|
||||
let width = crossterm::cursor::position()?.0 as u8;
|
||||
self.known.insert(grapheme, width);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
91
src/wrap.rs
91
src/wrap.rs
|
|
@ -1,91 +0,0 @@
|
|||
//! Word wrapping for text.
|
||||
|
||||
use unicode_linebreak::BreakOpportunity;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use crate::WidthDb;
|
||||
|
||||
pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec<usize> {
|
||||
let mut breaks = vec![];
|
||||
|
||||
let mut break_options = unicode_linebreak::linebreaks(text).peekable();
|
||||
|
||||
// The last valid break point encountered and its width
|
||||
let mut valid_break = None;
|
||||
|
||||
// Starting index and width of the line at the current grapheme (with and
|
||||
// without trailing whitespace)
|
||||
let mut current_start = 0;
|
||||
let mut current_width = 0;
|
||||
let mut current_width_trimmed = 0;
|
||||
|
||||
for (gi, g) in text.grapheme_indices(true) {
|
||||
// Advance break options
|
||||
let (bi, b) = loop {
|
||||
let (bi, b) = break_options.peek().expect("not at end of string yet");
|
||||
if *bi < gi {
|
||||
break_options.next();
|
||||
} else {
|
||||
break (*bi, b);
|
||||
}
|
||||
};
|
||||
|
||||
// Evaluate break options at the current position
|
||||
if bi == gi {
|
||||
match b {
|
||||
BreakOpportunity::Mandatory => {
|
||||
breaks.push(bi);
|
||||
valid_break = None;
|
||||
current_start = bi;
|
||||
current_width = 0;
|
||||
current_width_trimmed = 0;
|
||||
}
|
||||
BreakOpportunity::Allowed => {
|
||||
valid_break = Some(bi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate widths after current grapheme
|
||||
let g_is_whitespace = g.chars().all(|c| c.is_whitespace());
|
||||
let g_width = widthdb.grapheme_width(g, current_width) as usize;
|
||||
current_width += g_width;
|
||||
if !g_is_whitespace {
|
||||
current_width_trimmed = current_width;
|
||||
}
|
||||
|
||||
// Wrap at last break point if necessary
|
||||
if current_width_trimmed > width {
|
||||
if let Some(bi) = valid_break {
|
||||
let new_line = &text[bi..gi + g.len()];
|
||||
|
||||
breaks.push(bi);
|
||||
valid_break = None;
|
||||
current_start = bi;
|
||||
current_width = widthdb.width(new_line);
|
||||
current_width_trimmed = widthdb.width(new_line.trim_end());
|
||||
}
|
||||
}
|
||||
|
||||
// Perform a forced break if still necessary
|
||||
if current_width_trimmed > width {
|
||||
if current_start == gi {
|
||||
// The grapheme is the only thing on the current line and it is
|
||||
// wider than the maximum width, so we'll allow it, thereby
|
||||
// forcing the following grapheme to break no matter what
|
||||
// (either because of a mandatory or allowed break, or via a
|
||||
// forced break).
|
||||
} else {
|
||||
// Forced break in the middle of a normally non-breakable chunk
|
||||
// because there are no valid break points.
|
||||
breaks.push(gi);
|
||||
valid_break = None;
|
||||
current_start = gi;
|
||||
current_width = widthdb.grapheme_width(g, 0).into();
|
||||
current_width_trimmed = if g_is_whitespace { 0 } else { current_width };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
breaks
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue