Compare commits

..

1 commit

Author SHA1 Message Date
e74cd39047 Measure width of characters 2022-05-21 13:26:02 +02:00
36 changed files with 77 additions and 4675 deletions

View file

@ -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,
}

View file

@ -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

View file

@ -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"

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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))
}
}

View file

@ -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)
}
}

View file

@ -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());
}
}

View file

@ -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
View 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})");
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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(())
}
}

View file

@ -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 {}

View file

@ -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::*;

View file

@ -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
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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
}
}

View file

@ -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(())
}
}

View file

@ -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)
}
}

View file

@ -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(())
}
}

View file

@ -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),
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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],
}
}

View file

@ -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,
}
);

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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
}