Compare commits
144 commits
measure-wi
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 57aa8c5930 | |||
| e3af509358 | |||
| 89b4595ed9 | |||
| 96b2e13c4a | |||
| 712c1537ad | |||
| d28ce90ec7 | |||
| 423dd100c1 | |||
| be7eff0979 | |||
| 77a02116a6 | |||
| 1618264cb7 | |||
| 73a0268dfd | |||
| 65f31a2697 | |||
| 3a5ce3832b | |||
| b1d7221bae | |||
| 0f7505ebb4 | |||
| ef6d75c23a | |||
| 8556fd8176 | |||
| 94052c5a65 | |||
| 761e8baeba | |||
| 2d604d606c | |||
| b01ee297d5 | |||
| 44512f1088 | |||
| b757f1be03 | |||
| 2714deeafb | |||
| 77b4f825c9 | |||
| 2c7888fa41 | |||
| f6cbba5231 | |||
| 87723840df | |||
| a4ec64aa57 | |||
| 6eb853e313 | |||
| 3b9ffe8715 | |||
| f005ec10fe | |||
| 8bfb4b2dc3 | |||
| f414db40d5 | |||
| 968dbe501f | |||
| 4179e7f56c | |||
| 57788a9dd9 | |||
| 59710c8162 | |||
| 242a1aed29 | |||
| d0b3b9edd4 | |||
| 810524325e | |||
| 35aa70de4b | |||
| 7c6e651f88 | |||
| 007493f136 | |||
| 77e72de9ad | |||
| 88e66e17ec | |||
| ea6be7bf32 | |||
| 542ea7bc66 | |||
| 0573fcec77 | |||
| 417f33cc24 | |||
| 607c11fea4 | |||
| cb483431cc | |||
| 397d3a6eac | |||
| 783e57a9ab | |||
| 8f155dc6a2 | |||
| b1c276ec38 | |||
| a8876e94f3 | |||
| bdc1549268 | |||
| 204540f375 | |||
| b27cb81642 | |||
| ba716dd089 | |||
| 3fb3a7b92b | |||
| 42d22e2a49 | |||
| d449c61f27 | |||
| f581fa6c47 | |||
| 828bba464a | |||
| 15e30dfdb2 | |||
| e666d5c092 | |||
| caca3b6ef1 | |||
| f25ce49e77 | |||
| 95a01d5fc8 | |||
| 8834bb6d9d | |||
| 7c3277a822 | |||
| 72b44fb3fc | |||
| ba6ee45110 | |||
| fae12a4b9f | |||
| b2d87543d7 | |||
| ac2546ba97 | |||
| ed14ea9023 | |||
| e3365fdc02 | |||
| 2dee39c03c | |||
| 5a15838989 | |||
| 845d88c93f | |||
| c689d97974 | |||
| 9ff8007cae | |||
| 4c304ffe79 | |||
| 67f703cf68 | |||
| eb36bfa2ea | |||
| 3f7e985b3f | |||
| b327dee3c3 | |||
| 47df35d9db | |||
| 575faf9bbf | |||
| bcc07dc9ba | |||
| dbafc40700 | |||
| 964f3bf011 | |||
| 6a0c0474ec | |||
| f793ec79ac | |||
| 70d33d4d5d | |||
| 904f5c16fa | |||
| 4ffaae067e | |||
| 0d59116012 | |||
| 0a3b193f79 | |||
| 8942b381f5 | |||
| 06aefd562b | |||
| f48901f543 | |||
| 6ed47ad916 | |||
| f258c84094 | |||
| 24fd0050fb | |||
| 45ece466c2 | |||
| 7e42913245 | |||
| fbe9e065fc | |||
| 5957e8e550 | |||
| 3b2a2105fe | |||
| 31bb2de87b | |||
| d186291ef7 | |||
| dfc10f9d09 | |||
| 26a8936cf5 | |||
| 464aefa6d7 | |||
| c1907bb8ee | |||
| 53b2728c82 | |||
| 14aedaf252 | |||
| e4e1454e80 | |||
| d693712dab | |||
| f0af4ddc40 | |||
| 26bf89023e | |||
| ee9d6018c0 | |||
| 11b2211fad | |||
| 9b0d80873f | |||
| 761519c1a7 | |||
| a0602a941c | |||
| 333cf74fba | |||
| 33264b4aec | |||
| 6b9e4cbc63 | |||
| 37634139b0 | |||
| 8fae7d2bf1 | |||
| 3b2ea37ba5 | |||
| 833defd1ce | |||
| 79e8813884 | |||
| fe424b3376 | |||
| 67f8919630 | |||
| 9512ddaa3b | |||
| add2f25aba | |||
| cc2f102141 | |||
| bbaea3b5bf |
36 changed files with 4675 additions and 77 deletions
8
.vscode/settings.json
vendored
Normal file
8
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"files.insertFinalNewline": true,
|
||||||
|
"rust-analyzer.cargo.features": "all",
|
||||||
|
"rust-analyzer.imports.granularity.enforce": true,
|
||||||
|
"rust-analyzer.imports.granularity.group": "module",
|
||||||
|
"rust-analyzer.imports.group.enable": true,
|
||||||
|
"evenBetterToml.formatter.columnWidth": 100,
|
||||||
|
}
|
||||||
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
|
|
||||||
|
Procedure when bumping the version number:
|
||||||
|
1. Update dependencies in a separate commit
|
||||||
|
2. Set version number in `Cargo.toml`
|
||||||
|
3. Add new section in this changelog
|
||||||
|
4. Commit with message `Bump version to X.Y.Z`
|
||||||
|
5. Create tag named `vX.Y.Z`
|
||||||
|
6. Push `master` and the new tag
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
## v0.3.4 - 2025-03-8
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Frame::set_bell` to print a bell character when the frame is displayed
|
||||||
|
- `widgets::bell`
|
||||||
|
|
||||||
|
## v0.3.3 - 2025-02-28
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Rendering glitches in unicode-based with estimation
|
||||||
|
|
||||||
|
## v0.3.2 - 2025-02-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Unicode-based grapheme width estimation method
|
||||||
|
|
||||||
|
## v0.3.1 - 2025-02-21
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Rendering glitches, mainly related to emoji
|
||||||
|
|
||||||
|
## v0.3.0 - 2024-11-06
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Terminal::mark_dirty`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **(breaking)** Updated dependencies
|
||||||
|
|
||||||
|
## v0.2.3 - 2024-04-25
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Width measurements of ASCII control characters
|
||||||
|
- Toss messing up the terminal state
|
||||||
|
|
||||||
|
## v0.2.2 - 2024-01-14
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Crash when drawing `widgets::Predrawn` with width 0
|
||||||
|
|
||||||
|
## v0.2.1 - 2024-01-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `Frame::set_title`
|
||||||
|
- `WidgetExt::title`
|
||||||
|
- `widgets::title`
|
||||||
|
|
||||||
|
## v0.2.0 - 2023-08-31
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **(breaking)** Updated dependencies
|
||||||
|
|
||||||
|
## v0.1.0 - 2023-05-14
|
||||||
|
|
||||||
|
Initial release
|
||||||
12
Cargo.toml
12
Cargo.toml
|
|
@ -1,11 +1,11 @@
|
||||||
[package]
|
[package]
|
||||||
name = "toss"
|
name = "toss"
|
||||||
version = "0.1.0"
|
version = "0.3.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.57"
|
async-trait = "0.1.83"
|
||||||
crossterm = "0.23.2"
|
crossterm = "0.28.1"
|
||||||
unicode-blocks = "0.1.4"
|
unicode-linebreak = "0.1.5"
|
||||||
unicode-segmentation = "1.9.0"
|
unicode-segmentation = "1.12.0"
|
||||||
unicode-width = "0.1.9"
|
unicode-width = "0.2.0"
|
||||||
|
|
|
||||||
39
examples/hello_world.rs
Normal file
39
examples/hello_world.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
examples/hello_world_widgets.rs
Normal file
47
examples/hello_world_widgets.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
76
examples/overlapping_graphemes.rs
Normal file
76
examples/overlapping_graphemes.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
examples/text_wrapping.rs
Normal file
66
examples/text_wrapping.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
use crossterm::event::Event;
|
||||||
|
use toss::{Frame, Pos, Styled, Terminal};
|
||||||
|
|
||||||
|
fn draw(f: &mut Frame) {
|
||||||
|
let text = concat!(
|
||||||
|
"This is a short paragraph in order to demonstrate unicode-aware word wrapping. ",
|
||||||
|
"Resize your terminal to different widths to try it out. ",
|
||||||
|
"After this sentence come two newlines, so it should always break here.\n",
|
||||||
|
"\n",
|
||||||
|
"Since the wrapping algorithm is aware of the Unicode Standard Annex #14, ",
|
||||||
|
"it understands things like non-breaking spaces and word joiners: ",
|
||||||
|
"This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n",
|
||||||
|
"\n",
|
||||||
|
"It can also properly handle wide graphemes (like emoji 🤔), ",
|
||||||
|
"including ones usually displayed incorrectly by terminal emulators, like 👩🔬 (a female scientist emoji).\n",
|
||||||
|
"\n",
|
||||||
|
"Finally, tabs are supported as well. ",
|
||||||
|
"The following text is rendered with a tab width of 4:\n",
|
||||||
|
"\tx\n",
|
||||||
|
"1\tx\n",
|
||||||
|
"12\tx\n",
|
||||||
|
"123\tx\n",
|
||||||
|
"1234\tx\n",
|
||||||
|
"12345\tx\n",
|
||||||
|
"123456\tx\n",
|
||||||
|
"1234567\tx\n",
|
||||||
|
"12345678\tx\n",
|
||||||
|
"123456789\tx\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let width = f.size().width.into();
|
||||||
|
let breaks = f.widthdb().wrap(text, width);
|
||||||
|
let lines = Styled::new_plain(text).split_at_indices(&breaks);
|
||||||
|
for (i, mut line) in lines.into_iter().enumerate() {
|
||||||
|
line.trim_end();
|
||||||
|
f.write(Pos::new(0, i as i32), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_frame(term: &mut Terminal) {
|
||||||
|
let mut dirty = true;
|
||||||
|
while dirty {
|
||||||
|
term.autoresize().unwrap();
|
||||||
|
draw(term.frame());
|
||||||
|
term.present().unwrap();
|
||||||
|
dirty = term.measure_widths().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// Automatically enters alternate screen and enables raw mode
|
||||||
|
let mut term = Terminal::new().unwrap();
|
||||||
|
term.set_measuring(true);
|
||||||
|
term.set_tab_width(4);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Render and display a frame. A full frame is displayed on the terminal
|
||||||
|
// once this function exits.
|
||||||
|
render_frame(&mut term);
|
||||||
|
|
||||||
|
// Exit if the user presses any buttons
|
||||||
|
if !matches!(crossterm::event::read().unwrap(), Event::Resize(_, _)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
354
src/buffer.rs
Normal file
354
src/buffer.rs
Normal file
|
|
@ -0,0 +1,354 @@
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crossterm::style::ContentStyle;
|
||||||
|
|
||||||
|
use crate::{Pos, Size, Style, Styled, WidthDb};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct Cell {
|
||||||
|
pub content: Box<str>,
|
||||||
|
pub style: ContentStyle,
|
||||||
|
pub width: u8,
|
||||||
|
pub offset: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Cell {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
content: " ".to_string().into_boxed_str(),
|
||||||
|
style: ContentStyle::default(),
|
||||||
|
width: 1,
|
||||||
|
offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct StackFrame {
|
||||||
|
pub pos: Pos,
|
||||||
|
pub size: Size,
|
||||||
|
pub drawable_area: Option<(Pos, Size)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StackFrame {
|
||||||
|
fn intersect_areas(
|
||||||
|
a_start: Pos,
|
||||||
|
a_size: Size,
|
||||||
|
b_start: Pos,
|
||||||
|
b_size: Size,
|
||||||
|
) -> Option<(Pos, Size)> {
|
||||||
|
// The first row/column that is not part of the area any more
|
||||||
|
let a_end = a_start + a_size;
|
||||||
|
let b_end = b_start + b_size;
|
||||||
|
|
||||||
|
let x_start = a_start.x.max(b_start.x);
|
||||||
|
let x_end = a_end.x.min(b_end.x);
|
||||||
|
let y_start = a_start.y.max(b_start.y);
|
||||||
|
let y_end = a_end.y.min(b_end.y);
|
||||||
|
|
||||||
|
if x_start < x_end && y_start < y_end {
|
||||||
|
let start = Pos::new(x_start, y_start);
|
||||||
|
let size = Size::new((x_end - x_start) as u16, (y_end - y_start) as u16);
|
||||||
|
Some((start, size))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn then(&self, pos: Pos, size: Size) -> Self {
|
||||||
|
let pos = self.local_to_global(pos);
|
||||||
|
|
||||||
|
let drawable_area = self
|
||||||
|
.drawable_area
|
||||||
|
.and_then(|(da_pos, da_size)| Self::intersect_areas(da_pos, da_size, pos, size));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
pos,
|
||||||
|
size,
|
||||||
|
drawable_area,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_to_global(&self, local_pos: Pos) -> Pos {
|
||||||
|
local_pos + self.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
fn global_to_local(&self, global_pos: Pos) -> Pos {
|
||||||
|
global_pos - self.pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ranges along the x and y axis where drawing is allowed, in global
|
||||||
|
/// coordinates.
|
||||||
|
fn legal_ranges(&self) -> Option<(Range<i32>, Range<i32>)> {
|
||||||
|
if let Some((pos, size)) = self.drawable_area {
|
||||||
|
let xrange = pos.x..pos.x + size.width as i32;
|
||||||
|
let yrange = pos.y..pos.y + size.height as i32;
|
||||||
|
Some((xrange, yrange))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Buffer {
|
||||||
|
size: Size,
|
||||||
|
data: Vec<Cell>,
|
||||||
|
cursor: Option<Pos>,
|
||||||
|
|
||||||
|
/// A stack of rectangular drawing areas.
|
||||||
|
///
|
||||||
|
/// When rendering to the buffer with a nonempty stack, it behaves as if it
|
||||||
|
/// was the size of the topmost stack element, and characters are translated
|
||||||
|
/// by the position of the topmost stack element. No characters can be
|
||||||
|
/// placed outside the area described by the topmost stack element.
|
||||||
|
stack: Vec<StackFrame>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Buffer {
|
||||||
|
/// Index in `data` of the cell at the given position. The position must
|
||||||
|
/// be inside the buffer.
|
||||||
|
///
|
||||||
|
/// Ignores the stack.
|
||||||
|
fn index(&self, x: u16, y: u16) -> usize {
|
||||||
|
assert!(x < self.size.width);
|
||||||
|
assert!(y < self.size.height);
|
||||||
|
|
||||||
|
let x: usize = x.into();
|
||||||
|
let y: usize = y.into();
|
||||||
|
let width: usize = self.size.width.into();
|
||||||
|
|
||||||
|
y * width + x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reference to the cell at the given position. The position must be
|
||||||
|
/// inside the buffer.
|
||||||
|
///
|
||||||
|
/// Ignores the stack.
|
||||||
|
pub fn at(&self, x: u16, y: u16) -> &Cell {
|
||||||
|
assert!(x < self.size.width);
|
||||||
|
assert!(y < self.size.height);
|
||||||
|
|
||||||
|
let i = self.index(x, y);
|
||||||
|
&self.data[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mutable reference to the cell at the given position. The position must
|
||||||
|
/// be inside the buffer.
|
||||||
|
///
|
||||||
|
/// Ignores the stack.
|
||||||
|
fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell {
|
||||||
|
assert!(x < self.size.width);
|
||||||
|
assert!(y < self.size.height);
|
||||||
|
|
||||||
|
let i = self.index(x, y);
|
||||||
|
&mut self.data[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_frame(&self) -> StackFrame {
|
||||||
|
self.stack.last().copied().unwrap_or(StackFrame {
|
||||||
|
pos: Pos::ZERO,
|
||||||
|
size: self.size,
|
||||||
|
drawable_area: Some((Pos::ZERO, self.size)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, pos: Pos, size: Size) {
|
||||||
|
self.stack.push(self.current_frame().then(pos, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
self.stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Size of the current drawable area, respecting the stack.
|
||||||
|
pub fn size(&self) -> Size {
|
||||||
|
self.current_frame().size
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor(&self) -> Option<Pos> {
|
||||||
|
self.cursor.map(|p| self.current_frame().global_to_local(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor(&mut self, pos: Option<Pos>) {
|
||||||
|
self.cursor = pos.map(|p| self.current_frame().local_to_global(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the buffer and reset its contents.
|
||||||
|
///
|
||||||
|
/// The buffer's contents are reset even if the buffer is already the
|
||||||
|
/// correct size. The stack is reset as well.
|
||||||
|
pub fn resize(&mut self, size: Size) {
|
||||||
|
if size == self.size {
|
||||||
|
self.data.fill_with(Cell::default);
|
||||||
|
} else {
|
||||||
|
let width: usize = size.width.into();
|
||||||
|
let height: usize = size.height.into();
|
||||||
|
let len = width * height;
|
||||||
|
|
||||||
|
self.size = size;
|
||||||
|
self.data.clear();
|
||||||
|
self.data.resize_with(len, Cell::default);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor = None;
|
||||||
|
|
||||||
|
self.stack.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the contents and stack of the buffer.
|
||||||
|
///
|
||||||
|
/// `buf.reset()` is equivalent to `buf.resize(buf.size())`.
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.resize(self.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove the grapheme at the specified coordinates from the buffer.
|
||||||
|
///
|
||||||
|
/// Removes the entire grapheme, not just the cell at the coordinates.
|
||||||
|
/// Preserves the style of the affected cells. Preserves the cursor. Works
|
||||||
|
/// even if the coordinates don't point to the beginning of the grapheme.
|
||||||
|
///
|
||||||
|
/// Ignores the stack.
|
||||||
|
fn erase(&mut self, x: u16, y: u16) {
|
||||||
|
let cell = self.at(x, y);
|
||||||
|
let width: u16 = cell.width.into();
|
||||||
|
let offset: u16 = cell.offset.into();
|
||||||
|
|
||||||
|
for x in (x - offset)..(x - offset + width) {
|
||||||
|
let cell = self.at_mut(x, y);
|
||||||
|
let style = cell.style;
|
||||||
|
|
||||||
|
*cell = Cell::default();
|
||||||
|
cell.style = style;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write styled text to the buffer, respecting the width of individual
|
||||||
|
/// graphemes.
|
||||||
|
///
|
||||||
|
/// The initial x position is considered the first column for tab width
|
||||||
|
/// calculations.
|
||||||
|
pub fn write(&mut self, widthdb: &mut WidthDb, pos: Pos, styled: &Styled) {
|
||||||
|
let frame = self.current_frame();
|
||||||
|
let (xrange, yrange) = match frame.legal_ranges() {
|
||||||
|
Some(ranges) => ranges,
|
||||||
|
None => return, // No drawable area
|
||||||
|
};
|
||||||
|
let pos = frame.local_to_global(pos);
|
||||||
|
if !yrange.contains(&pos.y) {
|
||||||
|
return; // Outside of drawable area
|
||||||
|
}
|
||||||
|
let y = pos.y as u16;
|
||||||
|
|
||||||
|
let mut col: usize = 0;
|
||||||
|
for (_, style, grapheme) in styled.styled_grapheme_indices() {
|
||||||
|
let x = pos.x + col as i32;
|
||||||
|
let width = widthdb.grapheme_width(grapheme, col);
|
||||||
|
col += width as usize;
|
||||||
|
if grapheme == "\t" {
|
||||||
|
for dx in 0..width {
|
||||||
|
self.write_grapheme(&xrange, x + dx as i32, y, 1, " ", style);
|
||||||
|
}
|
||||||
|
} else if width > 0 {
|
||||||
|
self.write_grapheme(&xrange, x, y, width, grapheme, style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write a single grapheme to the buffer, respecting its width.
|
||||||
|
///
|
||||||
|
/// Assumes that `pos.y` is in range.
|
||||||
|
fn write_grapheme(
|
||||||
|
&mut self,
|
||||||
|
xrange: &Range<i32>,
|
||||||
|
x: i32,
|
||||||
|
y: u16,
|
||||||
|
width: u8,
|
||||||
|
grapheme: &str,
|
||||||
|
style: Style,
|
||||||
|
) {
|
||||||
|
let min_x = xrange.start;
|
||||||
|
let max_x = xrange.end - 1; // Last possible cell
|
||||||
|
|
||||||
|
let start_x = x;
|
||||||
|
let end_x = x + width as i32 - 1; // Coordinate of last cell
|
||||||
|
|
||||||
|
if start_x > max_x || end_x < min_x {
|
||||||
|
return; // Not visible
|
||||||
|
}
|
||||||
|
|
||||||
|
if start_x >= min_x && end_x <= max_x {
|
||||||
|
// Fully visible, write actual grapheme
|
||||||
|
let base_style = self.at(start_x as u16, y).style;
|
||||||
|
for offset in 0..width {
|
||||||
|
let x = start_x as u16 + offset as u16;
|
||||||
|
self.erase(x, y);
|
||||||
|
*self.at_mut(x, y) = Cell {
|
||||||
|
content: grapheme.to_string().into_boxed_str(),
|
||||||
|
style: style.cover(base_style),
|
||||||
|
width,
|
||||||
|
offset,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Partially visible, write empty cells with correct style
|
||||||
|
let start_x = start_x.max(0) as u16;
|
||||||
|
let end_x = end_x.min(max_x) as u16;
|
||||||
|
for x in start_x..=end_x {
|
||||||
|
let base_style = self.at(x, y).style;
|
||||||
|
self.erase(x, y);
|
||||||
|
*self.at_mut(x, y) = Cell {
|
||||||
|
style: style.cover(base_style),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(pos) = self.cursor {
|
||||||
|
if pos.y == y as i32 && start_x <= pos.x && pos.x <= end_x {
|
||||||
|
// The cursor lies within the bounds of the current grapheme and
|
||||||
|
self.cursor = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cells(&self) -> Cells<'_> {
|
||||||
|
Cells {
|
||||||
|
buffer: self,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Cells<'a> {
|
||||||
|
buffer: &'a Buffer,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for Cells<'a> {
|
||||||
|
type Item = (u16, u16, &'a Cell);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.x >= self.buffer.size.width {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.y >= self.buffer.size.height {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = (self.x, self.y);
|
||||||
|
let cell = self.buffer.at(self.x, self.y);
|
||||||
|
assert!(cell.offset == 0);
|
||||||
|
|
||||||
|
self.x += cell.width as u16;
|
||||||
|
if self.x >= self.buffer.size.width {
|
||||||
|
self.x = 0;
|
||||||
|
self.y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((x, y, cell))
|
||||||
|
}
|
||||||
|
}
|
||||||
153
src/coords.rs
Normal file
153
src/coords.rs
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
use std::ops::{Add, AddAssign, Neg, Sub, SubAssign};
|
||||||
|
|
||||||
|
/// Size in screen cells.
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Size {
|
||||||
|
pub width: u16,
|
||||||
|
pub height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
pub const ZERO: Self = Self::new(0, 0);
|
||||||
|
|
||||||
|
pub const fn new(width: u16, height: u16) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add two [`Size`]s using [`u16::saturating_add`].
|
||||||
|
pub const fn saturating_add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.width.saturating_add(rhs.width),
|
||||||
|
self.height.saturating_add(rhs.height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subtract two [`Size`]s using [`u16::saturating_sub`].
|
||||||
|
pub const fn saturating_sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(
|
||||||
|
self.width.saturating_sub(rhs.width),
|
||||||
|
self.height.saturating_sub(rhs.height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Size {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.width + rhs.width, self.height + rhs.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign for Size {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.width += rhs.width;
|
||||||
|
self.height += rhs.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Size {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.width - rhs.width, self.height - rhs.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubAssign for Size {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
self.width -= rhs.width;
|
||||||
|
self.height -= rhs.height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Position in screen cell coordinates.
|
||||||
|
///
|
||||||
|
/// The x axis points to the right. The y axis points down.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct Pos {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub const ZERO: Self = Self::new(0, 0);
|
||||||
|
|
||||||
|
pub const fn new(x: i32, y: i32) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Size> for Pos {
|
||||||
|
fn from(s: Size) -> Self {
|
||||||
|
Self::new(s.width.into(), s.height.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Pos {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x + rhs.x, self.y + rhs.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add<Size> for Pos {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(self, rhs: Size) -> Self {
|
||||||
|
Self::new(self.x + rhs.width as i32, self.y + rhs.height as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign for Pos {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.x += rhs.x;
|
||||||
|
self.y += rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign<Size> for Pos {
|
||||||
|
fn add_assign(&mut self, rhs: Size) {
|
||||||
|
self.x += rhs.width as i32;
|
||||||
|
self.y += rhs.height as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Pos {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Self) -> Self {
|
||||||
|
Self::new(self.x - rhs.x, self.y - rhs.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub<Size> for Pos {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn sub(self, rhs: Size) -> Self {
|
||||||
|
Self::new(self.x - rhs.width as i32, self.y - rhs.height as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubAssign for Pos {
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
self.x -= rhs.x;
|
||||||
|
self.y -= rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubAssign<Size> for Pos {
|
||||||
|
fn sub_assign(&mut self, rhs: Size) {
|
||||||
|
self.x -= rhs.width as i32;
|
||||||
|
self.y -= rhs.height as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Neg for Pos {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn neg(self) -> Self {
|
||||||
|
Self::new(-self.x, -self.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/frame.rs
Normal file
63
src/frame.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
//! Rendering the next frame.
|
||||||
|
|
||||||
|
use crate::buffer::Buffer;
|
||||||
|
use crate::{Pos, Size, Styled, WidthDb};
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Frame {
|
||||||
|
pub(crate) widthdb: WidthDb,
|
||||||
|
pub(crate) buffer: Buffer,
|
||||||
|
pub(crate) title: Option<String>,
|
||||||
|
pub(crate) bell: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frame {
|
||||||
|
pub fn push(&mut self, pos: Pos, size: Size) {
|
||||||
|
self.buffer.push(pos, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&mut self) {
|
||||||
|
self.buffer.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(&self) -> Size {
|
||||||
|
self.buffer.size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.buffer.reset();
|
||||||
|
self.title = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cursor(&self) -> Option<Pos> {
|
||||||
|
self.buffer.cursor()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cursor(&mut self, pos: Option<Pos>) {
|
||||||
|
self.buffer.set_cursor(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_cursor(&mut self, pos: Pos) {
|
||||||
|
self.set_cursor(Some(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hide_cursor(&mut self) {
|
||||||
|
self.set_cursor(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_title(&mut self, title: Option<String>) {
|
||||||
|
self.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_bell(&mut self, bell: bool) {
|
||||||
|
self.bell = bell;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widthdb(&mut self) -> &mut WidthDb {
|
||||||
|
&mut self.widthdb
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<S: Into<Styled>>(&mut self, pos: Pos, styled: S) {
|
||||||
|
self.buffer.write(&mut self.widthdb, pos, &styled.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/lib.rs
37
src/lib.rs
|
|
@ -1,8 +1,29 @@
|
||||||
#[cfg(test)]
|
#![forbid(unsafe_code)]
|
||||||
mod tests {
|
// Rustc lint groups
|
||||||
#[test]
|
#![warn(future_incompatible)]
|
||||||
fn it_works() {
|
#![warn(rust_2018_idioms)]
|
||||||
let result = 2 + 2;
|
#![warn(unused)]
|
||||||
assert_eq!(result, 4);
|
// 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::*;
|
||||||
|
|
|
||||||
63
src/main.rs
63
src/main.rs
|
|
@ -1,63 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::{io, slice};
|
|
||||||
|
|
||||||
use crossterm::cursor::{self, MoveTo};
|
|
||||||
use crossterm::execute;
|
|
||||||
use crossterm::style::Print;
|
|
||||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
|
||||||
use unicode_blocks::UnicodeBlock;
|
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
|
||||||
use unicode_width::UnicodeWidthStr;
|
|
||||||
|
|
||||||
struct WidthDB(HashMap<String, u8>);
|
|
||||||
|
|
||||||
impl WidthDB {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self(HashMap::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn measure(&mut self, s: &str) -> anyhow::Result<()> {
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
for grapheme in s.graphemes(true) {
|
|
||||||
execute!(stdout, EnterAlternateScreen, MoveTo(0, 0), Print(grapheme))?;
|
|
||||||
let width = cursor::position()?.0 as u8;
|
|
||||||
if width != grapheme.width() as u8 {
|
|
||||||
self.0.insert(grapheme.to_string(), width);
|
|
||||||
}
|
|
||||||
execute!(stdout, LeaveAlternateScreen)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn measure_block(&mut self, block: UnicodeBlock) -> anyhow::Result<()> {
|
|
||||||
for c in block.start()..=block.end() {
|
|
||||||
let c = char::from_u32(c).unwrap();
|
|
||||||
let s = c.to_string();
|
|
||||||
self.measure(&s)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut widthdb = WidthDB::new();
|
|
||||||
|
|
||||||
// widthdb.measure_block(unicode_blocks::BASIC_LATIN);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_1_SUPPLEMENT);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_A);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_ADDITIONAL);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_B);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_C);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_D);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_E);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_F);
|
|
||||||
// widthdb.measure_block(unicode_blocks::LATIN_EXTENDED_G);
|
|
||||||
// widthdb.measure_block(unicode_blocks::EMOTICONS);
|
|
||||||
widthdb.measure_block(unicode_blocks::BOX_DRAWING);
|
|
||||||
|
|
||||||
println!();
|
|
||||||
for (grapheme, width) in widthdb.0 {
|
|
||||||
let expected = grapheme.width();
|
|
||||||
println!("{grapheme} = {width} (expected: {expected})");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
60
src/style.rs
Normal file
60
src/style.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
use crossterm::style::{ContentStyle, Stylize};
|
||||||
|
|
||||||
|
fn merge_cs(base: ContentStyle, cover: ContentStyle) -> ContentStyle {
|
||||||
|
ContentStyle {
|
||||||
|
foreground_color: cover.foreground_color.or(base.foreground_color),
|
||||||
|
background_color: cover.background_color.or(base.background_color),
|
||||||
|
underline_color: cover.underline_color.or(base.underline_color),
|
||||||
|
attributes: cover.attributes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
pub struct Style {
|
||||||
|
pub content_style: ContentStyle,
|
||||||
|
pub opaque: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Style {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transparent(mut self) -> Self {
|
||||||
|
self.opaque = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn opaque(mut self) -> Self {
|
||||||
|
self.opaque = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cover(self, base: ContentStyle) -> ContentStyle {
|
||||||
|
if self.opaque {
|
||||||
|
return self.content_style;
|
||||||
|
}
|
||||||
|
|
||||||
|
merge_cs(base, self.content_style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<ContentStyle> for Style {
|
||||||
|
fn as_ref(&self) -> &ContentStyle {
|
||||||
|
&self.content_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsMut<ContentStyle> for Style {
|
||||||
|
fn as_mut(&mut self) -> &mut ContentStyle {
|
||||||
|
&mut self.content_style
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stylize for Style {
|
||||||
|
type Styled = Self;
|
||||||
|
|
||||||
|
fn stylize(self) -> Self::Styled {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
195
src/styled.rs
Normal file
195
src/styled.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
use std::iter::Peekable;
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
|
use unicode_segmentation::{GraphemeIndices, Graphemes, UnicodeSegmentation};
|
||||||
|
|
||||||
|
use crate::Style;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct Styled {
|
||||||
|
text: String,
|
||||||
|
/// List of `(style, until)` tuples. The style should be applied to all
|
||||||
|
/// chars in the range `prev_until..until`.
|
||||||
|
styles: Vec<(Style, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled {
|
||||||
|
pub fn new<S: AsRef<str>>(text: S, style: Style) -> Self {
|
||||||
|
Self::default().then(text, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_plain<S: AsRef<str>>(text: S) -> Self {
|
||||||
|
Self::default().then_plain(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn then<S: AsRef<str>>(mut self, text: S, style: Style) -> Self {
|
||||||
|
let text = text.as_ref();
|
||||||
|
if !text.is_empty() {
|
||||||
|
self.text.push_str(text);
|
||||||
|
self.styles.push((style, self.text.len()));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn then_plain<S: AsRef<str>>(self, text: S) -> Self {
|
||||||
|
self.then(text, Style::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn and_then(mut self, mut other: Self) -> Self {
|
||||||
|
let delta = self.text.len();
|
||||||
|
for (_, until) in &mut other.styles {
|
||||||
|
*until += delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.text.push_str(&other.text);
|
||||||
|
self.styles.extend(other.styles);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self) -> &str {
|
||||||
|
&self.text
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_at(self, mid: usize) -> (Self, Self) {
|
||||||
|
let (left_text, right_text) = self.text.split_at(mid);
|
||||||
|
|
||||||
|
let mut left_styles = vec![];
|
||||||
|
let mut right_styles = vec![];
|
||||||
|
let mut from = 0;
|
||||||
|
for (style, until) in self.styles {
|
||||||
|
if from < mid {
|
||||||
|
left_styles.push((style, until.min(mid)));
|
||||||
|
}
|
||||||
|
if mid < until {
|
||||||
|
right_styles.push((style, until.saturating_sub(mid)));
|
||||||
|
}
|
||||||
|
from = until;
|
||||||
|
}
|
||||||
|
|
||||||
|
let left = Self {
|
||||||
|
text: left_text.to_string(),
|
||||||
|
styles: left_styles,
|
||||||
|
};
|
||||||
|
|
||||||
|
let right = Self {
|
||||||
|
text: right_text.to_string(),
|
||||||
|
styles: right_styles,
|
||||||
|
};
|
||||||
|
|
||||||
|
(left, right)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn split_at_indices(self, indices: &[usize]) -> Vec<Self> {
|
||||||
|
let mut lines = vec![];
|
||||||
|
|
||||||
|
let mut rest = self;
|
||||||
|
let mut offset = 0;
|
||||||
|
|
||||||
|
for i in indices {
|
||||||
|
let (left, right) = rest.split_at(i - offset);
|
||||||
|
lines.push(left);
|
||||||
|
rest = right;
|
||||||
|
offset = *i;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(rest);
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn trim_end(&mut self) {
|
||||||
|
self.text = self.text.trim_end().to_string();
|
||||||
|
|
||||||
|
let text_len = self.text.len();
|
||||||
|
let mut styles_len = 0;
|
||||||
|
for (_, until) in &mut self.styles {
|
||||||
|
styles_len += 1;
|
||||||
|
if *until >= text_len {
|
||||||
|
*until = text_len;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while self.styles.len() > styles_len {
|
||||||
|
self.styles.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////////
|
||||||
|
// Iterating over graphemes //
|
||||||
|
//////////////////////////////
|
||||||
|
|
||||||
|
pub struct StyledGraphemeIndices<'a> {
|
||||||
|
text: GraphemeIndices<'a>,
|
||||||
|
styles: Peekable<slice::Iter<'a, (Style, usize)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for StyledGraphemeIndices<'a> {
|
||||||
|
type Item = (usize, Style, &'a str);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let (gi, grapheme) = self.text.next()?;
|
||||||
|
let (mut style, mut until) = **self.styles.peek().expect("styles cover entire text");
|
||||||
|
while gi >= until {
|
||||||
|
self.styles.next();
|
||||||
|
(style, until) = **self.styles.peek().expect("styles cover entire text");
|
||||||
|
}
|
||||||
|
Some((gi, style, grapheme))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Styled {
|
||||||
|
pub fn graphemes(&self) -> Graphemes<'_> {
|
||||||
|
self.text.graphemes(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn grapheme_indices(&self) -> GraphemeIndices<'_> {
|
||||||
|
self.text.grapheme_indices(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn styled_grapheme_indices(&self) -> StyledGraphemeIndices<'_> {
|
||||||
|
StyledGraphemeIndices {
|
||||||
|
text: self.grapheme_indices(),
|
||||||
|
styles: self.styles.iter().peekable(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////////////////
|
||||||
|
// Converting to Styled //
|
||||||
|
//////////////////////////
|
||||||
|
|
||||||
|
impl From<&str> for Styled {
|
||||||
|
fn from(text: &str) -> Self {
|
||||||
|
Self::new_plain(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for Styled {
|
||||||
|
fn from(text: String) -> Self {
|
||||||
|
Self::new_plain(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AsRef<str>> From<(S,)> for Styled {
|
||||||
|
fn from((text,): (S,)) -> Self {
|
||||||
|
Self::new_plain(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AsRef<str>> From<(S, Style)> for Styled {
|
||||||
|
fn from((text, style): (S, Style)) -> Self {
|
||||||
|
Self::new(text, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S: AsRef<str>> From<&[(S, Style)]> for Styled {
|
||||||
|
fn from(segments: &[(S, Style)]) -> Self {
|
||||||
|
let mut result = Self::default();
|
||||||
|
for (text, style) in segments {
|
||||||
|
result = result.then(text, *style);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
327
src/terminal.rs
Normal file
327
src/terminal.rs
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
//! Displaying frames on a terminal.
|
||||||
|
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
|
use crossterm::cursor::{Hide, MoveTo, Show};
|
||||||
|
use crossterm::event::{
|
||||||
|
DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags,
|
||||||
|
PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||||
|
};
|
||||||
|
use crossterm::style::{Print, PrintStyledContent, StyledContent};
|
||||||
|
use crossterm::terminal::{
|
||||||
|
BeginSynchronizedUpdate, Clear, ClearType, EndSynchronizedUpdate, EnterAlternateScreen,
|
||||||
|
LeaveAlternateScreen, SetTitle,
|
||||||
|
};
|
||||||
|
use crossterm::{ExecutableCommand, QueueableCommand};
|
||||||
|
|
||||||
|
use crate::buffer::Buffer;
|
||||||
|
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod};
|
||||||
|
|
||||||
|
/// Wrapper that manages terminal output.
|
||||||
|
///
|
||||||
|
/// This struct (usually) wraps around stdout and handles showing things on the
|
||||||
|
/// terminal. It cleans up after itself when droppped, so it shouldn't leave the
|
||||||
|
/// terminal in a weird state even if your program crashes.
|
||||||
|
pub struct Terminal {
|
||||||
|
/// Render target.
|
||||||
|
out: Box<dyn Write>,
|
||||||
|
/// The frame being currently rendered.
|
||||||
|
frame: Frame,
|
||||||
|
/// Buffer from the previous frame.
|
||||||
|
prev_frame_buffer: Buffer,
|
||||||
|
/// When the screen is updated next, it must be cleared and redrawn fully
|
||||||
|
/// instead of performing an incremental update.
|
||||||
|
full_redraw: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Terminal {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.suspend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Terminal {
|
||||||
|
/// Create a new [`Terminal`] that wraps stdout.
|
||||||
|
pub fn new() -> io::Result<Self> {
|
||||||
|
Self::with_target(Box::new(io::stdout()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new terminal wrapping a custom output.
|
||||||
|
pub fn with_target(out: Box<dyn Write>) -> io::Result<Self> {
|
||||||
|
let mut result = Self {
|
||||||
|
out,
|
||||||
|
frame: Frame::default(),
|
||||||
|
prev_frame_buffer: Buffer::default(),
|
||||||
|
full_redraw: true,
|
||||||
|
};
|
||||||
|
result.unsuspend()?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Temporarily restore the terminal state to normal.
|
||||||
|
///
|
||||||
|
/// This is useful when running external programs the user should interact
|
||||||
|
/// with directly, for example a text editor.
|
||||||
|
///
|
||||||
|
/// Call [`Self::unsuspend`] to return the terminal state before drawing and
|
||||||
|
/// presenting the next frame.
|
||||||
|
pub fn suspend(&mut self) -> io::Result<()> {
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
self.out.execute(PopKeyboardEnhancementFlags)?;
|
||||||
|
self.out.execute(DisableBracketedPaste)?;
|
||||||
|
}
|
||||||
|
self.out.execute(LeaveAlternateScreen)?;
|
||||||
|
self.out.execute(Show)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the terminal state after calling [`Self::suspend`].
|
||||||
|
///
|
||||||
|
/// After calling this function, a new frame needs to be drawn and presented
|
||||||
|
/// by the application. The previous screen contents are **not** restored.
|
||||||
|
pub fn unsuspend(&mut self) -> io::Result<()> {
|
||||||
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
self.out.execute(EnterAlternateScreen)?;
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
self.out.execute(EnableBracketedPaste)?;
|
||||||
|
self.out.execute(PushKeyboardEnhancementFlags(
|
||||||
|
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
self.full_redraw = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the tab width in columns.
|
||||||
|
///
|
||||||
|
/// For more details, see [`Self::tab_width`].
|
||||||
|
pub fn set_tab_width(&mut self, tab_width: u8) {
|
||||||
|
self.frame.widthdb.tab_width = tab_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tab width in columns.
|
||||||
|
///
|
||||||
|
/// For accurate width calculations and consistency across terminals, tabs
|
||||||
|
/// are not printed to the terminal directly, but instead converted into
|
||||||
|
/// spaces.
|
||||||
|
pub fn tab_width(&self) -> u8 {
|
||||||
|
self.frame.widthdb.tab_width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the grapheme width estimation method.
|
||||||
|
///
|
||||||
|
/// For more details, see [`WidthEstimationMethod`].
|
||||||
|
pub fn set_width_estimation_method(&mut self, method: WidthEstimationMethod) {
|
||||||
|
self.frame.widthdb.estimate = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The grapheme width estimation method.
|
||||||
|
///
|
||||||
|
/// For more details, see [`WidthEstimationMethod`].
|
||||||
|
pub fn width_estimation_method(&mut self) -> WidthEstimationMethod {
|
||||||
|
self.frame.widthdb.estimate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enable or disable grapheme width measurements.
|
||||||
|
///
|
||||||
|
/// For more details, see [`Self::measuring`].
|
||||||
|
pub fn set_measuring(&mut self, active: bool) {
|
||||||
|
self.frame.widthdb.measure = active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether grapheme widths should be measured or estimated.
|
||||||
|
///
|
||||||
|
/// Handling of wide characters is inconsistent from terminal emulator to
|
||||||
|
/// terminal emulator, and may even depend on the font the user is using.
|
||||||
|
///
|
||||||
|
/// When enabled, any newly encountered graphemes are measured whenever
|
||||||
|
/// [`Self::measure_widths`] is called. This is done by clearing the screen,
|
||||||
|
/// printing the grapheme and measuring the resulting cursor position.
|
||||||
|
/// Because of this, the screen will flicker occasionally. However, grapheme
|
||||||
|
/// widths will always be accurate independent of the terminal
|
||||||
|
/// configuration.
|
||||||
|
///
|
||||||
|
/// When disabled, the width of graphemes is estimated using the Unicode
|
||||||
|
/// Standard Annex #11. This usually works fine, but may break on some emoji
|
||||||
|
/// or other less commonly used character sequences.
|
||||||
|
pub fn measuring(&self) -> bool {
|
||||||
|
self.frame.widthdb.measure
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any unmeasured graphemes were seen since the last call to
|
||||||
|
/// [`Self::measure_widths`].
|
||||||
|
///
|
||||||
|
/// Returns `true` whenever [`Self::measure_widths`] would return `true`.
|
||||||
|
pub fn measuring_required(&self) -> bool {
|
||||||
|
self.frame.widthdb.measuring_required()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measure widths of all unmeasured graphemes.
|
||||||
|
///
|
||||||
|
/// If width measurements are disabled, this function does nothing. For more
|
||||||
|
/// info, see [`Self::measuring`].
|
||||||
|
///
|
||||||
|
/// Returns `true` if any new graphemes were measured and the screen must be
|
||||||
|
/// redrawn. Keep in mind that after redrawing the screen, graphemes may
|
||||||
|
/// have become visible that have not yet been measured. You should keep
|
||||||
|
/// re-measuring and re-drawing until this function returns `false`.
|
||||||
|
pub fn measure_widths(&mut self) -> io::Result<bool> {
|
||||||
|
if self.frame.widthdb.measuring_required() {
|
||||||
|
self.full_redraw = true;
|
||||||
|
self.frame.widthdb.measure_widths(&mut self.out)?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resize the frame and other internal buffers if the terminal size has
|
||||||
|
/// changed.
|
||||||
|
///
|
||||||
|
/// Should be called before drawing a frame and presenting it with
|
||||||
|
/// [`Self::present`]. It is not necessary to call this when using
|
||||||
|
/// [`Self::present_widget`] or [`Self::present_async_widget`].
|
||||||
|
pub fn autoresize(&mut self) -> io::Result<()> {
|
||||||
|
let (width, height) = crossterm::terminal::size()?;
|
||||||
|
let size = Size { width, height };
|
||||||
|
if size != self.frame.size() {
|
||||||
|
self.frame.buffer.resize(size);
|
||||||
|
self.prev_frame_buffer.resize(size);
|
||||||
|
self.full_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The current frame.
|
||||||
|
pub fn frame(&mut self) -> &mut Frame {
|
||||||
|
&mut self.frame
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A database of grapheme widths.
|
||||||
|
pub fn widthdb(&mut self) -> &mut WidthDb {
|
||||||
|
&mut self.frame.widthdb
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the terminal as dirty, forcing a full redraw whenever any variant
|
||||||
|
/// of [`Self::present`] is called.
|
||||||
|
pub fn mark_dirty(&mut self) {
|
||||||
|
self.full_redraw = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display the current frame on the screen and prepare the next frame.
|
||||||
|
///
|
||||||
|
/// Before drawing and presenting a frame, [`Self::measure_widths`] and
|
||||||
|
/// [`Self::autoresize`] should be called.
|
||||||
|
///
|
||||||
|
/// After calling this function, the frame returned by [`Self::frame`] will
|
||||||
|
/// be empty again and have no cursor position.
|
||||||
|
pub fn present(&mut self) -> io::Result<()> {
|
||||||
|
self.out.queue(BeginSynchronizedUpdate)?;
|
||||||
|
let result = self.draw_to_screen();
|
||||||
|
self.out.queue(EndSynchronizedUpdate)?;
|
||||||
|
result?;
|
||||||
|
|
||||||
|
self.out.flush()?;
|
||||||
|
|
||||||
|
mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer);
|
||||||
|
self.frame.reset();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display a [`Widget`] on the screen.
|
||||||
|
///
|
||||||
|
/// Before creating and presenting a widget, [`Self::measure_widths`] should
|
||||||
|
/// be called. There is no need to call [`Self::autoresize`].
|
||||||
|
pub fn present_widget<E, W>(&mut self, widget: W) -> Result<(), E>
|
||||||
|
where
|
||||||
|
E: From<io::Error>,
|
||||||
|
W: Widget<E>,
|
||||||
|
{
|
||||||
|
self.autoresize()?;
|
||||||
|
widget.draw(self.frame())?;
|
||||||
|
self.present()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Display an [`AsyncWidget`] on the screen.
|
||||||
|
///
|
||||||
|
/// Before creating and presenting a widget, [`Self::measure_widths`] should
|
||||||
|
/// be called. There is no need to call [`Self::autoresize`].
|
||||||
|
pub async fn present_async_widget<E, W>(&mut self, widget: W) -> Result<(), E>
|
||||||
|
where
|
||||||
|
E: From<io::Error>,
|
||||||
|
W: AsyncWidget<E>,
|
||||||
|
{
|
||||||
|
self.autoresize()?;
|
||||||
|
widget.draw(self.frame()).await?;
|
||||||
|
self.present()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_to_screen(&mut self) -> io::Result<()> {
|
||||||
|
if self.full_redraw {
|
||||||
|
self.out.queue(Clear(ClearType::All))?;
|
||||||
|
self.prev_frame_buffer.reset(); // Because the screen is now empty
|
||||||
|
self.full_redraw = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.draw_differences()?;
|
||||||
|
self.update_cursor()?;
|
||||||
|
self.update_title()?;
|
||||||
|
self.ring_bell()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_differences(&mut self) -> io::Result<()> {
|
||||||
|
for (x, y, cell) in self.frame.buffer.cells() {
|
||||||
|
if self.prev_frame_buffer.at(x, y) == cell {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = StyledContent::new(cell.style, &cell.content as &str);
|
||||||
|
self.out
|
||||||
|
.queue(MoveTo(x, y))?
|
||||||
|
.queue(PrintStyledContent(content))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cursor(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(pos) = self.frame.cursor() {
|
||||||
|
let size = self.frame.size();
|
||||||
|
let x_in_bounds = 0 <= pos.x && pos.x < size.width as i32;
|
||||||
|
let y_in_bounds = 0 <= pos.y && pos.y < size.height as i32;
|
||||||
|
if x_in_bounds && y_in_bounds {
|
||||||
|
self.out
|
||||||
|
.queue(Show)?
|
||||||
|
.queue(MoveTo(pos.x as u16, pos.y as u16))?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.out.queue(Hide)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_title(&mut self) -> io::Result<()> {
|
||||||
|
if let Some(title) = &self.frame.title {
|
||||||
|
self.out.queue(SetTitle(title.clone()))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ring_bell(&mut self) -> io::Result<()> {
|
||||||
|
if self.frame.bell {
|
||||||
|
self.out.queue(Print('\x07'))?;
|
||||||
|
}
|
||||||
|
self.frame.bell = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
127
src/widget.rs
Normal file
127
src/widget.rs
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
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 {}
|
||||||
35
src/widgets.rs
Normal file
35
src/widgets.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
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::*;
|
||||||
71
src/widgets/background.rs
Normal file
71
src/widgets/background.rs
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/widgets/bell.rs
Normal file
55
src/widgets/bell.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/widgets/border.rs
Normal file
201
src/widgets/border.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/widgets/boxed.rs
Normal file
142
src/widgets/boxed.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/widgets/cursor.rs
Normal file
68
src/widgets/cursor.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/widgets/desync.rs
Normal file
42
src/widgets/desync.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
485
src/widgets/editor.rs
Normal file
485
src/widgets/editor.rs
Normal file
|
|
@ -0,0 +1,485 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/widgets/either.rs
Normal file
118
src/widgets/either.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/widgets/empty.rs
Normal file
42
src/widgets/empty.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
166
src/widgets/float.rs
Normal file
166
src/widgets/float.rs
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
721
src/widgets/join.rs
Normal file
721
src/widgets/join.rs
Normal file
|
|
@ -0,0 +1,721 @@
|
||||||
|
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],
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/widgets/layer.rs
Normal file
201
src/widgets/layer.rs
Normal file
|
|
@ -0,0 +1,201 @@
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
133
src/widgets/padding.rs
Normal file
133
src/widgets/padding.rs
Normal file
|
|
@ -0,0 +1,133 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
74
src/widgets/predrawn.rs
Normal file
74
src/widgets/predrawn.rs
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
120
src/widgets/resize.rs
Normal file
120
src/widgets/resize.rs
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/widgets/text.rs
Normal file
68
src/widgets/text.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/widgets/title.rs
Normal file
59
src/widgets/title.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::{AsyncWidget, Frame, Size, Widget, WidthDb};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Title<I> {
|
||||||
|
pub inner: I,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I> Title<I> {
|
||||||
|
pub fn new<S: ToString>(inner: I, title: S) -> Self {
|
||||||
|
Self {
|
||||||
|
inner,
|
||||||
|
title: title.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E, I> Widget<E> for Title<I>
|
||||||
|
where
|
||||||
|
I: Widget<E>,
|
||||||
|
{
|
||||||
|
fn size(
|
||||||
|
&self,
|
||||||
|
widthdb: &mut WidthDb,
|
||||||
|
max_width: Option<u16>,
|
||||||
|
max_height: Option<u16>,
|
||||||
|
) -> Result<Size, E> {
|
||||||
|
self.inner.size(widthdb, max_width, max_height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||||
|
self.inner.draw(frame)?;
|
||||||
|
frame.set_title(Some(self.title));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<E, I> AsyncWidget<E> for Title<I>
|
||||||
|
where
|
||||||
|
I: AsyncWidget<E> + Send + Sync,
|
||||||
|
{
|
||||||
|
async fn size(
|
||||||
|
&self,
|
||||||
|
widthdb: &mut WidthDb,
|
||||||
|
max_width: Option<u16>,
|
||||||
|
max_height: Option<u16>,
|
||||||
|
) -> Result<Size, E> {
|
||||||
|
self.inner.size(widthdb, max_width, max_height).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn draw(self, frame: &mut Frame) -> Result<(), E> {
|
||||||
|
self.inner.draw(frame).await?;
|
||||||
|
frame.set_title(Some(self.title));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
163
src/widthdb.rs
Normal file
163
src/widthdb.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
use crossterm::cursor::MoveTo;
|
||||||
|
use crossterm::style::Print;
|
||||||
|
use crossterm::terminal::{Clear, ClearType};
|
||||||
|
use crossterm::QueueableCommand;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
|
|
||||||
|
use crate::wrap;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WidthEstimationMethod {
|
||||||
|
/// Estimate the width of a grapheme using legacy methods.
|
||||||
|
///
|
||||||
|
/// Different terminal emulators all use different approaches to determine
|
||||||
|
/// grapheme widths, so this method will never be able to give a fully
|
||||||
|
/// correct solution. For that, the only possible approach is measuring the
|
||||||
|
/// actual grapheme width.
|
||||||
|
#[default]
|
||||||
|
Legacy,
|
||||||
|
|
||||||
|
/// Estimate the width of a grapheme using the unicode standard in a
|
||||||
|
/// best-effort manner.
|
||||||
|
Unicode,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measures and stores the with (in terminal coordinates) of graphemes.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WidthDb {
|
||||||
|
pub(crate) estimate: WidthEstimationMethod,
|
||||||
|
pub(crate) measure: bool,
|
||||||
|
pub(crate) tab_width: u8,
|
||||||
|
known: HashMap<String, u8>,
|
||||||
|
requested: HashSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WidthDb {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
estimate: WidthEstimationMethod::default(),
|
||||||
|
measure: false,
|
||||||
|
tab_width: 8,
|
||||||
|
known: Default::default(),
|
||||||
|
requested: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WidthDb {
|
||||||
|
/// Determine the width of a tab character starting at the specified column.
|
||||||
|
fn tab_width_at_column(&self, col: usize) -> u8 {
|
||||||
|
self.tab_width - (col % self.tab_width as usize) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the width of a grapheme.
|
||||||
|
///
|
||||||
|
/// If the grapheme is a tab, the column is used to determine its width.
|
||||||
|
///
|
||||||
|
/// If the width has not been measured yet or measurements are turned off,
|
||||||
|
/// it is estimated using the Unicode Standard Annex #11.
|
||||||
|
pub fn grapheme_width(&mut self, grapheme: &str, col: usize) -> u8 {
|
||||||
|
assert_eq!(Some(grapheme), grapheme.graphemes(true).next());
|
||||||
|
if grapheme == "\t" {
|
||||||
|
return self.tab_width_at_column(col);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.measure {
|
||||||
|
if let Some(width) = self.known.get(grapheme) {
|
||||||
|
return *width;
|
||||||
|
}
|
||||||
|
self.requested.insert(grapheme.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.estimate {
|
||||||
|
// A character-wise width calculation is a simple and obvious
|
||||||
|
// approach to compute character widths. The idea is that dumb
|
||||||
|
// terminal emulators tend to do something roughly like this, and
|
||||||
|
// smart terminal emulators try to emulate dumb ones for
|
||||||
|
// compatibility. In practice, this approach seems to be fairly
|
||||||
|
// robust.
|
||||||
|
WidthEstimationMethod::Legacy => grapheme
|
||||||
|
.chars()
|
||||||
|
.filter(|c| !c.is_ascii_control())
|
||||||
|
.flat_map(|c| c.width())
|
||||||
|
.sum::<usize>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(u8::MAX),
|
||||||
|
|
||||||
|
// The unicode width crate considers control chars to have a width
|
||||||
|
// of 1 even though they usually have a width of 0 when displayed.
|
||||||
|
WidthEstimationMethod::Unicode => grapheme
|
||||||
|
.split(|c: char| c.is_ascii_control())
|
||||||
|
.map(|s| s.width())
|
||||||
|
.sum::<usize>()
|
||||||
|
.try_into()
|
||||||
|
.unwrap_or(u8::MAX),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine the width of a string based on its graphemes.
|
||||||
|
///
|
||||||
|
/// If a grapheme is a tab, its column is used to determine its width.
|
||||||
|
///
|
||||||
|
/// If the width of a grapheme has not been measured yet or measurements are
|
||||||
|
/// turned off, it is estimated using the Unicode Standard Annex #11.
|
||||||
|
pub fn width(&mut self, s: &str) -> usize {
|
||||||
|
let mut total: usize = 0;
|
||||||
|
for grapheme in s.graphemes(true) {
|
||||||
|
total += self.grapheme_width(grapheme, total) as usize;
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform primitive word wrapping with the specified maximum width.
|
||||||
|
///
|
||||||
|
/// Returns the byte offsets at which the string should be split into lines.
|
||||||
|
/// An offset of 1 would mean the first line contains only a single byte.
|
||||||
|
/// These offsets lie on grapheme boundaries.
|
||||||
|
///
|
||||||
|
/// This function does not support bidirectional script. It assumes the
|
||||||
|
/// entire text has the same direction.
|
||||||
|
pub fn wrap(&mut self, text: &str, width: usize) -> Vec<usize> {
|
||||||
|
wrap::wrap(self, text, width)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether any new graphemes have been seen since the last time
|
||||||
|
/// [`Self::measure_widths`] was called.
|
||||||
|
pub(crate) fn measuring_required(&self) -> bool {
|
||||||
|
self.measure && !self.requested.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Measure the width of all new graphemes that have been seen since the
|
||||||
|
/// last time this function was called.
|
||||||
|
///
|
||||||
|
/// This function measures the actual width of graphemes by writing them to
|
||||||
|
/// the terminal. After it finishes, the terminal's contents should be
|
||||||
|
/// assumed to be garbage and a full redraw should be performed.
|
||||||
|
pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> {
|
||||||
|
if !self.measure {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
for grapheme in self.requested.drain() {
|
||||||
|
if grapheme.chars().any(|c| c.is_ascii_control()) {
|
||||||
|
// ASCII control characters like the escape character or the
|
||||||
|
// bell character tend to be interpreted specially by terminals.
|
||||||
|
// This may break width measurements. To avoid this, we just
|
||||||
|
// assign each control character a with of 0.
|
||||||
|
self.known.insert(grapheme, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.queue(Clear(ClearType::All))?
|
||||||
|
.queue(MoveTo(0, 0))?
|
||||||
|
.queue(Print(&grapheme))?;
|
||||||
|
out.flush()?;
|
||||||
|
let width = crossterm::cursor::position()?.0 as u8;
|
||||||
|
self.known.insert(grapheme, width);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
91
src/wrap.rs
Normal file
91
src/wrap.rs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
//! Word wrapping for text.
|
||||||
|
|
||||||
|
use unicode_linebreak::BreakOpportunity;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
use crate::WidthDb;
|
||||||
|
|
||||||
|
pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec<usize> {
|
||||||
|
let mut breaks = vec![];
|
||||||
|
|
||||||
|
let mut break_options = unicode_linebreak::linebreaks(text).peekable();
|
||||||
|
|
||||||
|
// The last valid break point encountered and its width
|
||||||
|
let mut valid_break = None;
|
||||||
|
|
||||||
|
// Starting index and width of the line at the current grapheme (with and
|
||||||
|
// without trailing whitespace)
|
||||||
|
let mut current_start = 0;
|
||||||
|
let mut current_width = 0;
|
||||||
|
let mut current_width_trimmed = 0;
|
||||||
|
|
||||||
|
for (gi, g) in text.grapheme_indices(true) {
|
||||||
|
// Advance break options
|
||||||
|
let (bi, b) = loop {
|
||||||
|
let (bi, b) = break_options.peek().expect("not at end of string yet");
|
||||||
|
if *bi < gi {
|
||||||
|
break_options.next();
|
||||||
|
} else {
|
||||||
|
break (*bi, b);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Evaluate break options at the current position
|
||||||
|
if bi == gi {
|
||||||
|
match b {
|
||||||
|
BreakOpportunity::Mandatory => {
|
||||||
|
breaks.push(bi);
|
||||||
|
valid_break = None;
|
||||||
|
current_start = bi;
|
||||||
|
current_width = 0;
|
||||||
|
current_width_trimmed = 0;
|
||||||
|
}
|
||||||
|
BreakOpportunity::Allowed => {
|
||||||
|
valid_break = Some(bi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate widths after current grapheme
|
||||||
|
let g_is_whitespace = g.chars().all(|c| c.is_whitespace());
|
||||||
|
let g_width = widthdb.grapheme_width(g, current_width) as usize;
|
||||||
|
current_width += g_width;
|
||||||
|
if !g_is_whitespace {
|
||||||
|
current_width_trimmed = current_width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap at last break point if necessary
|
||||||
|
if current_width_trimmed > width {
|
||||||
|
if let Some(bi) = valid_break {
|
||||||
|
let new_line = &text[bi..gi + g.len()];
|
||||||
|
|
||||||
|
breaks.push(bi);
|
||||||
|
valid_break = None;
|
||||||
|
current_start = bi;
|
||||||
|
current_width = widthdb.width(new_line);
|
||||||
|
current_width_trimmed = widthdb.width(new_line.trim_end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform a forced break if still necessary
|
||||||
|
if current_width_trimmed > width {
|
||||||
|
if current_start == gi {
|
||||||
|
// The grapheme is the only thing on the current line and it is
|
||||||
|
// wider than the maximum width, so we'll allow it, thereby
|
||||||
|
// forcing the following grapheme to break no matter what
|
||||||
|
// (either because of a mandatory or allowed break, or via a
|
||||||
|
// forced break).
|
||||||
|
} else {
|
||||||
|
// Forced break in the middle of a normally non-breakable chunk
|
||||||
|
// because there are no valid break points.
|
||||||
|
breaks.push(gi);
|
||||||
|
valid_break = None;
|
||||||
|
current_start = gi;
|
||||||
|
current_width = widthdb.grapheme_width(g, 0).into();
|
||||||
|
current_width_trimmed = if g_is_whitespace { 0 } else { current_width };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
breaks
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue