Compare commits

...
Sign in to create a new pull request.

131 commits

Author SHA1 Message Date
57aa8c5930 Bump version to 0.3.4 2025-03-08 19:30:44 +01:00
e3af509358 Add bell widget 2025-03-08 19:10:29 +01:00
89b4595ed9 Print bell character 2025-03-08 19:05:38 +01:00
96b2e13c4a Bump version to 0.3.3 2025-02-28 14:30:45 +01:00
712c1537ad Fix incorrect width estimation of ascii control characters 2025-02-28 14:29:53 +01:00
d28ce90ec7 Bump version to 0.3.2 2025-02-23 23:31:25 +01:00
423dd100c1 Add unicode-based grapheme width estimation method 2025-02-23 18:36:42 +01:00
be7eff0979 Bump version to 0.3.1 2025-02-21 00:36:39 +01:00
77a02116a6 Fix grapheme width estimation
I'm pretty sure it still breaks in lots of terminal emulators, but it
works far better than what recent versions of the unicode_width crate
were doing.
2025-02-20 22:10:11 +01:00
1618264cb7 Fix newlines causing bad rendering artifacts
The unicode-width crate has started to consider newlines to have a width
of 1 instead of 0.
2025-02-20 22:08:37 +01:00
73a0268dfd Bump version to 0.3.0 2024-11-06 22:21:20 +01:00
65f31a2697 Update dependencies 2024-11-06 22:17:28 +01:00
3a5ce3832b Add Terminal::mark_dirty 2024-11-06 22:16:48 +01:00
b1d7221bae Bump version to 0.2.3 2024-04-25 20:14:26 +02:00
0f7505ebb4 Update dependencies 2024-04-25 20:13:42 +02:00
ef6d75c23a Fix suspend sequence
In my kitty-based setup, I observed the following bug:

1. Run cove[1], a toss-based application, in a kitty tab
2. Exit cove
3. Start lazygit[2]
4. Stage some files and enter a commit message
5. Try to press enter and observe garbage appearing in the text box

The bug occurred reliably after running cove, but never occurred if cove
was not run in that tab.

This commit fixes the bug by making the suspend sequence undo the
unsuspend sequence's steps in reverse order.
2024-04-25 20:07:48 +02:00
8556fd8176 Fix control character width measurement 2024-04-25 19:46:10 +02:00
94052c5a65 Fix formatting 2024-03-04 00:08:33 +01:00
761e8baeba Bump version to 0.2.2 2024-01-14 12:38:33 +01:00
2d604d606c Fix crash when drawing Predrawn with width 0 2024-01-14 12:32:21 +01:00
b01ee297d5 Bump version to 0.2.1 2024-01-05 13:54:42 +01:00
44512f1088 Update dependencies 2024-01-05 13:53:13 +01:00
b757f1be03 Add Title widget 2024-01-05 13:52:25 +01:00
2714deeafb Add support for setting window title 2024-01-05 13:33:08 +01:00
77b4f825c9 Fix clippy warning 2024-01-05 13:23:18 +01:00
2c7888fa41 Bump version to 0.2.0 2023-08-31 13:23:00 +02:00
f6cbba5231 Update dependencies 2023-08-31 13:20:54 +02:00
87723840df Bump version to 0.1.0 2023-05-14 15:57:25 +02:00
a4ec64aa57 Update dependencies 2023-05-14 15:57:19 +02:00
6eb853e313 Reduce tearing when redrawing screen 2023-05-10 19:35:56 +02:00
3b9ffe8715 Fix full redraws always using stdout 2023-05-10 19:25:46 +02:00
f005ec10fe Fix editor panicking sometimes when hidden 2023-04-29 01:23:50 +02:00
8bfb4b2dc3 Fix Join panicking in some situations 2023-04-28 14:40:53 +02:00
f414db40d5 Add BoxedSencSync which is Send + Sync 2023-04-17 20:30:24 +02:00
968dbe501f Remove AsyncWidget impls replaceable by Desync 2023-04-17 19:46:40 +02:00
4179e7f56c Add Desync widget to turn Widgets into AsyncWidgets 2023-04-17 19:46:40 +02:00
57788a9dd9 Apply Resize's max size to available size too
I'm not sure if the input max size and the output max size should be
separate, and I'm not sure whether the min size should also have an
effect on the input. For now, this works well enough, but I may need to
adjust it in the future as I stumble across new edge cases.

This change was made because I was using Resize as a way to set the size
of widgets containing text that were rendered inside Predraw widgets.
After this change, setting a max_width but no max_height has the desired
effect of making the inner widgets perform word wrapping. The resulting
Predrawn is then as high as it needs to be to contain the wrapped text.
2023-04-17 16:48:13 +02:00
59710c8162 Fix Predrawn not drawing cursor 2023-04-17 09:38:00 +02:00
242a1aed29 Add option to stretch Padding 2023-04-14 13:57:09 +02:00
d0b3b9edd4 Fix Predrawn size calculations
Previously, Predrawn would use its parent frame's size. Now, it uses the
size requested by the widget. Because of this, it no longer requires a
full &mut Frame, but only a &mut WidthDb.

To set a maximum size, the widget can be wrapped inside a Resize.
2023-04-14 01:51:40 +02:00
810524325e Fix hidden placeholder appearing in empty editor 2023-04-13 02:18:43 +02:00
35aa70de4b Fix editor cursor 2023-04-12 19:16:29 +02:00
7c6e651f88 Add Boxed and BoxedAsync widgets 2023-04-08 16:00:04 +02:00
007493f136 Derive more traits for widgets 2023-04-08 15:40:14 +02:00
77e72de9ad Make Layer* more like Join* 2023-04-08 15:21:51 +02:00
88e66e17ec Add Predrawn widget 2023-04-06 03:00:37 +02:00
ea6be7bf32 Simplify editor cursor handling and rendering 2023-04-06 00:18:56 +02:00
542ea7bc66 Simplify Join* implementation 2023-02-26 14:57:34 +01:00
0573fcec77 Only provide WidthDb in [Async]Widget::size 2023-02-26 14:42:48 +01:00
417f33cc24 Differentiate between growing and shrinking segments
Also uses the new with_* naming scheme for segments
2023-02-20 16:57:35 +01:00
607c11fea4 Use new with_* naming scheme
All builder-like functions are now named with_*. There is also now a way
to set each property imperatively with only a mutable reference.

The only widgets I haven't yet converted to this style are the Join*
widgets; they're a bit harder to figure out an appropriate API for.
2023-02-20 15:53:49 +01:00
cb483431cc Make widget submodules public
This will make the generated documentation more readable once I add
module docstrings. I'm still reexporting the module contents to keep
imports nice and tidy.
2023-02-19 14:24:28 +01:00
397d3a6eac Add Editor widget 2023-02-19 14:18:45 +01:00
783e57a9ab Allow join segments to be fixed
I'm not sure if this is a good abstraction, or if I should instead
re-think the algorithm.
2023-02-19 02:31:58 +01:00
8f155dc6a2 Fix join widgets not handling large sizes 2023-02-19 02:09:41 +01:00
b1c276ec38 Fix panic with zero-weighted segments 2023-02-19 02:02:27 +01:00
a8876e94f3 Make default background style opaque 2023-02-18 21:24:51 +01:00
bdc1549268 Create Join[HV]{2,7} via macros 2023-02-18 20:56:36 +01:00
204540f375 Create Either{2,7} via macros 2023-02-18 20:56:10 +01:00
b27cb81642 Add Resize widget 2023-02-18 20:24:56 +01:00
ba716dd089 Fix examples not measuring widths immediately 2023-02-18 20:13:47 +01:00
3fb3a7b92b Fix sizing bug
The available space was updated while removing elements, but the check
for the next element expected the old value.
2023-02-18 19:40:33 +01:00
42d22e2a49 Fix typo 2023-02-18 19:06:22 +01:00
d449c61f27 Add JoinH2, JoinH3, JoinV2, JoinV3 2023-02-18 19:06:22 +01:00
f581fa6c47 Add JoinH and JoinV 2023-02-18 19:06:18 +01:00
828bba464a Add Either3 widget 2023-02-18 19:00:18 +01:00
15e30dfdb2 Implement join widget spacing algorithm 2023-02-18 18:20:55 +01:00
e666d5c092 Ensure Float position is in range 0.0..=1.0 2023-02-18 14:07:25 +01:00
caca3b6ef1 Derive Debug, Clone, Copy for widgets 2023-02-18 14:07:03 +01:00
f25ce49e77 Rename Layer parts 2023-02-18 02:49:52 +01:00
95a01d5fc8 Add Either widget 2023-02-17 21:27:46 +01:00
8834bb6d9d Add more Float functions 2023-02-17 21:26:00 +01:00
7c3277a822 Add Layer widget 2023-02-17 21:26:00 +01:00
72b44fb3fc Add back optional Terminal::measuring_required 2023-02-17 20:25:23 +01:00
ba6ee45110 Don't measure widths while presenting 2023-02-17 18:59:32 +01:00
fae12a4b9f Improve coords documentation 2023-02-17 15:04:56 +01:00
b2d87543d7 Improve Terminal documentation 2023-02-17 14:58:21 +01:00
ac2546ba97 Add Terminal::{present_widget, present_async_widget} 2023-02-17 14:00:46 +01:00
ed14ea9023 Measure automatically in Terminal::present 2023-02-17 13:59:16 +01:00
e3365fdc02 Improve WidthDb documentation 2023-02-17 13:35:28 +01:00
2dee39c03c Add Cursor widget 2023-02-17 12:13:56 +01:00
5a15838989 Fix Float sizing for unset directions 2023-02-17 00:15:46 +01:00
845d88c93f Make widget example look slightly less horrible 2023-02-16 23:37:16 +01:00
c689d97974 Write transparent Style to Buffer correctly 2023-02-16 23:37:16 +01:00
9ff8007cae Switch usages of ContentStyle to Style 2023-02-16 21:24:52 +01:00
4c304ffe79 Add own Style type 2023-02-16 21:11:51 +01:00
67f703cf68 Extract Pos and Size to separate file 2023-02-16 21:10:48 +01:00
eb36bfa2ea Fix code blocks in docstrings 2023-02-16 20:40:20 +01:00
3f7e985b3f Add Background widget 2023-02-16 16:26:47 +01:00
b327dee3c3 Remove unnecessary AsyncWidgetExt trait 2023-02-16 16:12:50 +01:00
47df35d9db Add Empty widget 2023-02-16 16:07:05 +01:00
575faf9bbf Add Float widget 2023-02-16 15:13:23 +01:00
bcc07dc9ba Add WidgetExt and AsyncWidgetExt traits 2023-02-16 15:12:33 +01:00
dbafc40700 Add Padding widget 2023-02-16 14:37:50 +01:00
964f3bf011 Add Border widget 2023-02-16 14:30:06 +01:00
6a0c0474ec Add widget hello world example 2023-02-16 10:08:45 +01:00
f793ec79ac Add Text widget 2023-02-16 09:58:18 +01:00
70d33d4d5d Add vscode settings 2023-02-16 09:37:59 +01:00
904f5c16fa Add Widget and AsyncWidget traits 2023-02-16 09:37:59 +01:00
4ffaae067e Export all types at top level 2023-02-16 09:37:31 +01:00
0d59116012 Update dependencies 2023-02-11 21:21:14 +01:00
0a3b193f79 Fix clippy lint 2023-01-05 17:34:30 +01:00
8942b381f5 Add and fix some lints 2022-12-11 20:44:06 +01:00
06aefd562b Remove wrap method from Frame 2022-09-26 17:34:38 +02:00
f48901f543 Expose WidthDb from Terminal 2022-09-26 17:21:48 +02:00
6ed47ad916 Rename WidthDB to WidthDb 2022-09-26 17:01:49 +02:00
f258c84094 Expose Widthdb directly via Frame 2022-09-26 17:00:46 +02:00
24fd0050fb Fix drawing widgets on cursor not removing cursor 2022-09-08 18:13:13 +02:00
45ece466c2 Support more modifiers on some terminal emulators
If a terminal emulator supports the kitty protocol, this change will
allow cove to take advantage of this. This means that more modifiers
will work on special keys like enter or escape.

See also this section of the kitty protocol docs:
https://sw.kovidgoyal.net/kitty/keyboard-protocol/#disambiguate-escape-codes
2022-08-11 23:15:12 +02:00
7e42913245 Enable bracketed paste mode in Terminal
Only on non-windows platforms though, crossterm doesn't support paste
events on windows.
2022-08-10 23:53:54 +02:00
fbe9e065fc Update dependencies 2022-08-10 22:51:22 +02:00
5957e8e550 Fix tab width calculations when word wrapping 2022-08-04 02:59:00 +02:00
3b2a2105fe Fix incorrect width for tab-replacing spaces 2022-08-04 02:18:18 +02:00
31bb2de87b Make WidthDB tab-width-aware 2022-08-04 02:18:18 +02:00
d186291ef7 Fix word wrapping with successive forced breaks
If there were multiple forced breaks in succession, all except the first
would be a bit too wide since I forgot to include the current grapheme
in the new line's width.
2022-08-03 21:57:15 +02:00
dfc10f9d09 Fix splitting Styleds 2022-08-03 13:22:38 +02:00
26a8936cf5 Store Styled with contiguous string
The previous implementation of Styled used chunks that consisted of a
String and a ContentStyle. The current implementation instead stores a
single String and chunks consisting of a ContentStyle and an ending
index.

This implementation may reduce allocations and makes width-related
operations easier, for example getting the width of a Styled with its
whitespace trimmed.
2022-08-01 19:02:57 +02:00
464aefa6d7 Forbid stack frames from expanding the drawable area 2022-08-01 00:06:36 +02:00
c1907bb8ee Fix frame cursor functions ignoring stack 2022-07-23 22:24:28 +02:00
53b2728c82 Fix Size subtraction 2022-07-21 14:31:23 +02:00
14aedaf252 Add stack of drawable areas
This lets the user restrict the drawable area to a sub-area of the
buffer. This lets the user draw without caring about the absolute
position, and guarantees that no glyphs or glyph parts appear outside of
the drawable area.
2022-07-13 11:19:19 +02:00
e4e1454e80 Calculate width of Styleds directly 2022-07-12 10:22:30 +02:00
d693712dab Expose tab width calculation 2022-07-07 16:21:05 +02:00
f0af4ddc40 Expose chunks and chunk contents 2022-07-07 16:18:51 +02:00
26bf89023e Update dependencies 2022-07-06 11:21:18 +02:00
ee9d6018c0 Add constructors and traits for Size and Pos 2022-07-04 19:38:28 +02:00
11b2211fad Improve word wrapping
Now supports long trailing whitespace as well as tabs.
2022-07-04 19:38:28 +02:00
9b0d80873f Use styled chunks of text instead of plain strings 2022-07-04 19:38:28 +02:00
761519c1a7 Suspend and unsuspend terminal 2022-06-17 20:02:51 +02:00
a0602a941c Fix examples 2022-06-15 14:02:30 +02:00
333cf74fba Make width measuring optional and disabled by default 2022-06-08 17:38:38 +02:00
35 changed files with 4140 additions and 270 deletions

8
.vscode/settings.json vendored Normal file
View 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
View 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

View file

@ -1,10 +1,11 @@
[package] [package]
name = "toss" name = "toss"
version = "0.1.0" version = "0.3.4"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
crossterm = "0.23.2" async-trait = "0.1.83"
unicode-linebreak = "0.1.2" crossterm = "0.28.1"
unicode-segmentation = "1.9.0" unicode-linebreak = "0.1.5"
unicode-width = "0.1.9" unicode-segmentation = "1.12.0"
unicode-width = "0.2.0"

View file

@ -1,39 +1,30 @@
use crossterm::event::Event; use crossterm::event::Event;
use crossterm::style::{ContentStyle, Stylize}; use crossterm::style::Stylize;
use toss::frame::{Frame, Pos}; use toss::{Frame, Pos, Style, Terminal};
use toss::terminal::{Redraw, Terminal};
fn draw(f: &mut Frame) { fn draw(f: &mut Frame) {
f.write( f.write(Pos::new(0, 0), ("Hello world!", Style::new().green()));
Pos::new(0, 0),
"Hello world!",
ContentStyle::default().green(),
);
f.write( f.write(
Pos::new(0, 1), Pos::new(0, 1),
"Press any key to exit", ("Press any key to exit", Style::new().on_dark_blue()),
ContentStyle::default().on_dark_blue(),
); );
f.show_cursor(Pos::new(16, 0)); f.show_cursor(Pos::new(16, 0));
} }
fn render_frame(term: &mut Terminal) { fn render_frame(term: &mut Terminal) {
loop { let mut dirty = true;
// Must be called before rendering, otherwise the terminal has out-of-date while dirty {
// size information and will present garbage.
term.autoresize().unwrap(); term.autoresize().unwrap();
draw(term.frame()); draw(term.frame());
term.present().unwrap();
if term.present().unwrap() == Redraw::NotRequired { dirty = term.measure_widths().unwrap();
break;
}
} }
} }
fn main() { fn main() {
// Automatically enters alternate screen and enables raw mode // Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new().unwrap(); let mut term = Terminal::new().unwrap();
term.set_measuring(true);
loop { loop {
// Render and display a frame. A full frame is displayed on the terminal // Render and display a frame. A full frame is displayed on the terminal

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

View file

@ -1,79 +1,67 @@
use crossterm::event::Event; use crossterm::event::Event;
use crossterm::style::{ContentStyle, Stylize}; use crossterm::style::Stylize;
use toss::frame::{Frame, Pos}; use toss::{Frame, Pos, Style, Terminal};
use toss::terminal::{Redraw, Terminal};
fn draw(f: &mut Frame) { fn draw(f: &mut Frame) {
f.write( f.write(
Pos::new(0, 0), Pos::new(0, 0),
"Writing over wide graphemes removes the entire overwritten grapheme.", "Writing over wide graphemes removes the entire overwritten grapheme.",
ContentStyle::default(),
); );
let under = ContentStyle::default().white().on_dark_blue(); let under = Style::new().white().on_dark_blue();
let over = ContentStyle::default().black().on_dark_yellow(); let over = Style::new().black().on_dark_yellow();
for i in 0..6 { for i in 0..6 {
let delta = i - 2; let delta = i - 2;
f.write(Pos::new(2 + i * 7, 2), "a😀", under); 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, 3), ("a😀", under));
f.write(Pos::new(2 + i * 7, 4), "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, 3), ("b", over));
f.write(Pos::new(2 + i * 7 + delta, 4), "😈", over); f.write(Pos::new(2 + i * 7 + delta, 4), ("😈", over));
} }
f.write( f.write(
Pos::new(0, 6), Pos::new(0, 6),
"Wide graphemes at the edges of the screen apply their style, but are not", "Wide graphemes at the edges of the screen apply their style, but are not",
ContentStyle::default(),
);
f.write(
Pos::new(0, 7),
"actually rendered.",
ContentStyle::default(),
); );
f.write(Pos::new(0, 7), "actually rendered.");
let x1 = -1; let x1 = -1;
let x2 = f.size().width as i32 / 2 - 3; let x2 = f.size().width as i32 / 2 - 3;
let x3 = f.size().width as i32 - 5; let x3 = f.size().width as i32 - 5;
f.write(Pos::new(x1, 9), "123456", under); f.write(Pos::new(x1, 9), ("123456", under));
f.write(Pos::new(x1, 10), "😀😀😀", under); f.write(Pos::new(x1, 10), ("😀😀😀", under));
f.write(Pos::new(x2, 9), "123456", under); f.write(Pos::new(x2, 9), ("123456", under));
f.write(Pos::new(x2, 10), "😀😀😀", under); f.write(Pos::new(x2, 10), ("😀😀😀", under));
f.write(Pos::new(x3, 9), "123456", under); f.write(Pos::new(x3, 9), ("123456", under));
f.write(Pos::new(x3, 10), "😀😀😀", under); f.write(Pos::new(x3, 10), ("😀😀😀", under));
let scientist = "👩‍🔬"; let scientist = "👩‍🔬";
f.write( f.write(
Pos::new(0, 12), Pos::new(0, 12),
"Most terminals ignore the zero width joiner and display this female", "Most terminals ignore the zero width joiner and display this female",
ContentStyle::default(),
); );
f.write( f.write(
Pos::new(0, 13), Pos::new(0, 13),
"scientist emoji as a woman and a microscope: 👩‍🔬", "scientist emoji as a woman and a microscope: 👩‍🔬",
ContentStyle::default(),
); );
for i in 0..(f.width(scientist) + 4) { for i in 0..(f.widthdb().width(scientist) + 4) {
f.write(Pos::new(2, 15 + i as i32), scientist, under); f.write(Pos::new(2, 15 + i as i32), (scientist, under));
f.write(Pos::new(i as i32, 15 + i as i32), "x", over); f.write(Pos::new(i as i32, 15 + i as i32), ("x", over));
} }
} }
fn render_frame(term: &mut Terminal) { fn render_frame(term: &mut Terminal) {
loop { let mut dirty = true;
// Must be called before rendering, otherwise the terminal has out-of-date while dirty {
// size information and will present garbage.
term.autoresize().unwrap(); term.autoresize().unwrap();
draw(term.frame()); draw(term.frame());
term.present().unwrap();
if term.present().unwrap() == Redraw::NotRequired { dirty = term.measure_widths().unwrap();
break;
}
} }
} }
fn main() { fn main() {
// Automatically enters alternate screen and enables raw mode // Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new().unwrap(); let mut term = Terminal::new().unwrap();
term.set_measuring(true);
loop { loop {
// Render and display a frame. A full frame is displayed on the terminal // Render and display a frame. A full frame is displayed on the terminal

View file

@ -1,7 +1,5 @@
use crossterm::event::Event; use crossterm::event::Event;
use crossterm::style::ContentStyle; use toss::{Frame, Pos, Styled, Terminal};
use toss::frame::{Frame, Pos};
use toss::terminal::{Redraw, Terminal};
fn draw(f: &mut Frame) { fn draw(f: &mut Frame) {
let text = concat!( let text = concat!(
@ -14,37 +12,46 @@ fn draw(f: &mut Frame) {
"This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n", "This\u{00a0}sentence\u{00a0}is\u{00a0}separated\u{00a0}by\u{00a0}non-\u{2060}breaking\u{00a0}spaces.\n",
"\n", "\n",
"It can also properly handle wide graphemes (like emoji 🤔), ", "It can also properly handle wide graphemes (like emoji 🤔), ",
"including ones usually displayed incorrectly by terminal emulators, like 👩‍🔬 (a female scientist 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 breaks = f.wrap(text, f.size().width.into()); let width = f.size().width.into();
let lines = toss::split_at_indices(text, &breaks); let breaks = f.widthdb().wrap(text, width);
for (i, line) in lines.iter().enumerate() { let lines = Styled::new_plain(text).split_at_indices(&breaks);
f.write( for (i, mut line) in lines.into_iter().enumerate() {
Pos::new(0, i as i32), line.trim_end();
line.trim_end(), f.write(Pos::new(0, i as i32), line);
ContentStyle::default(),
);
} }
} }
fn render_frame(term: &mut Terminal) { fn render_frame(term: &mut Terminal) {
loop { let mut dirty = true;
// Must be called before rendering, otherwise the terminal has out-of-date while dirty {
// size information and will present garbage.
term.autoresize().unwrap(); term.autoresize().unwrap();
draw(term.frame()); draw(term.frame());
term.present().unwrap();
if term.present().unwrap() == Redraw::NotRequired { dirty = term.measure_widths().unwrap();
break;
}
} }
} }
fn main() { fn main() {
// Automatically enters alternate screen and enables raw mode // Automatically enters alternate screen and enables raw mode
let mut term = Terminal::new().unwrap(); let mut term = Terminal::new().unwrap();
term.set_measuring(true);
term.set_tab_width(4);
loop { loop {
// Render and display a frame. A full frame is displayed on the terminal // Render and display a frame. A full frame is displayed on the terminal

View file

@ -1,34 +1,8 @@
use std::ops::Range;
use crossterm::style::ContentStyle; use crossterm::style::ContentStyle;
use unicode_segmentation::UnicodeSegmentation;
use crate::widthdb::WidthDB; use crate::{Pos, Size, Style, Styled, WidthDb};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Size {
pub width: u16,
pub height: u16,
}
impl Size {
pub const ZERO: Self = Self {
width: 0,
height: 0,
};
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Pos {
pub x: i32,
pub y: i32,
}
impl Pos {
pub const ZERO: Self = Self { x: 0, y: 0 };
pub fn new(x: i32, y: i32) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Cell { pub struct Cell {
@ -49,13 +23,93 @@ impl Default for Cell {
} }
} }
#[derive(Debug, Default)] #[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 { pub struct Buffer {
size: Size, size: Size,
data: Vec<Cell>, 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 { 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 { fn index(&self, x: u16, y: u16) -> usize {
assert!(x < self.size.width); assert!(x < self.size.width);
assert!(y < self.size.height); assert!(y < self.size.height);
@ -67,30 +121,65 @@ impl Buffer {
y * width + x y * width + x
} }
pub(crate) fn at(&self, x: u16, y: u16) -> &Cell { /// 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!(x < self.size.width);
assert!(y < self.size.height); assert!(y < self.size.height);
let i = self.index(x, y); let i = self.index(x, y);
&self.data[i] &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 { fn at_mut(&mut self, x: u16, y: u16) -> &mut Cell {
assert!(x < self.size.width); assert!(x < self.size.width);
assert!(y < self.size.height); assert!(y < self.size.height);
let i = self.index(x, y); let i = self.index(x, y);
&mut self.data[i] &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 { pub fn size(&self) -> 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. /// Resize the buffer and reset its contents.
/// ///
/// The buffer's contents are reset even if the buffer is already the /// The buffer's contents are reset even if the buffer is already the
/// correct size. /// correct size. The stack is reset as well.
pub fn resize(&mut self, size: Size) { pub fn resize(&mut self, size: Size) {
if size == self.size() { if size == self.size {
self.data.fill_with(Cell::default); self.data.fill_with(Cell::default);
} else { } else {
let width: usize = size.width.into(); let width: usize = size.width.into();
@ -101,58 +190,86 @@ impl Buffer {
self.data.clear(); self.data.clear();
self.data.resize_with(len, Cell::default); self.data.resize_with(len, Cell::default);
} }
self.cursor = None;
self.stack.clear();
} }
/// Reset the contents of the buffer. /// Reset the contents and stack of the buffer.
/// ///
/// `buf.reset()` is equivalent to `buf.resize(buf.size())`. /// `buf.reset()` is equivalent to `buf.resize(buf.size())`.
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.data.fill_with(Cell::default); self.resize(self.size);
} }
/// Remove the grapheme at the specified coordinates from the buffer. /// Remove the grapheme at the specified coordinates from the buffer.
/// ///
/// Removes the entire grapheme, not just the cell at the coordinates. /// Removes the entire grapheme, not just the cell at the coordinates.
/// Preserves the style of the affected cells. Works even if the coordinates /// Preserves the style of the affected cells. Preserves the cursor. Works
/// don't point to the beginning of the grapheme. /// even if the coordinates don't point to the beginning of the grapheme.
///
/// Ignores the stack.
fn erase(&mut self, x: u16, y: u16) { fn erase(&mut self, x: u16, y: u16) {
let cell = self.at(x, y); let cell = self.at(x, y);
let width: u16 = cell.width.into(); let width: u16 = cell.width.into();
let offset: u16 = cell.offset.into(); let offset: u16 = cell.offset.into();
for x in (x - offset)..(x - offset + width) { for x in (x - offset)..(x - offset + width) {
let cell = self.at_mut(x, y); let cell = self.at_mut(x, y);
let style = cell.style; let style = cell.style;
*cell = Cell::default(); *cell = Cell::default();
cell.style = style; cell.style = style;
} }
} }
pub fn write( /// Write styled text to the buffer, respecting the width of individual
&mut self, /// graphemes.
widthdb: &mut WidthDB, ///
mut pos: Pos, /// The initial x position is considered the first column for tab width
content: &str, /// calculations.
style: ContentStyle, pub fn write(&mut self, widthdb: &mut WidthDb, pos: Pos, styled: &Styled) {
) { let frame = self.current_frame();
// If we're not even visible, there's nothing to do let (xrange, yrange) = match frame.legal_ranges() {
if pos.y < 0 || pos.y >= self.size.height as i32 { Some(ranges) => ranges,
return; 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 y = pos.y as u16;
for grapheme in content.graphemes(true) { let mut col: usize = 0;
let width = widthdb.grapheme_width(grapheme); for (_, style, grapheme) in styled.styled_grapheme_indices() {
if width > 0 { let x = pos.x + col as i32;
self.write_grapheme(pos.x, y, width, grapheme, style); 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);
} }
pos.x += width as i32;
} }
} }
/// Write a single grapheme to the buffer, respecting its width.
///
/// Assumes that `pos.y` is in range. /// Assumes that `pos.y` is in range.
fn write_grapheme(&mut self, x: i32, y: u16, width: u8, grapheme: &str, style: ContentStyle) { fn write_grapheme(
let min_x = 0; &mut self,
let max_x = self.size.width as i32 - 1; // Last possible cell 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 start_x = x;
let end_x = x + width as i32 - 1; // Coordinate of last cell let end_x = x + width as i32 - 1; // Coordinate of last cell
@ -163,12 +280,13 @@ impl Buffer {
if start_x >= min_x && end_x <= max_x { if start_x >= min_x && end_x <= max_x {
// Fully visible, write actual grapheme // Fully visible, write actual grapheme
let base_style = self.at(start_x as u16, y).style;
for offset in 0..width { for offset in 0..width {
let x = start_x as u16 + offset as u16; let x = start_x as u16 + offset as u16;
self.erase(x, y); self.erase(x, y);
*self.at_mut(x, y) = Cell { *self.at_mut(x, y) = Cell {
content: grapheme.to_string().into_boxed_str(), content: grapheme.to_string().into_boxed_str(),
style, style: style.cover(base_style),
width, width,
offset, offset,
}; };
@ -178,13 +296,21 @@ impl Buffer {
let start_x = start_x.max(0) as u16; let start_x = start_x.max(0) as u16;
let end_x = end_x.min(max_x) as u16; let end_x = end_x.min(max_x) as u16;
for x in start_x..=end_x { for x in start_x..=end_x {
let base_style = self.at(x, y).style;
self.erase(x, y); self.erase(x, y);
*self.at_mut(x, y) = Cell { *self.at_mut(x, y) = Cell {
style, style: style.cover(base_style),
..Default::default() ..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<'_> { pub fn cells(&self) -> Cells<'_> {
@ -206,6 +332,9 @@ impl<'a> Iterator for Cells<'a> {
type Item = (u16, u16, &'a Cell); type Item = (u16, u16, &'a Cell);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
if self.x >= self.buffer.size.width {
return None;
}
if self.y >= self.buffer.size.height { if self.y >= self.buffer.size.height {
return None; return None;
} }

153
src/coords.rs Normal file
View 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)
}
}

View file

@ -1,35 +1,40 @@
//! Rendering the next frame. //! Rendering the next frame.
use crossterm::style::ContentStyle;
use crate::buffer::Buffer; use crate::buffer::Buffer;
pub use crate::buffer::{Pos, Size}; use crate::{Pos, Size, Styled, WidthDb};
use crate::widthdb::WidthDB;
use crate::wrap;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Frame { pub struct Frame {
pub(crate) widthdb: WidthDB, pub(crate) widthdb: WidthDb,
pub(crate) buffer: Buffer, pub(crate) buffer: Buffer,
cursor: Option<Pos>, pub(crate) title: Option<String>,
pub(crate) bell: bool,
} }
impl Frame { 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 { pub fn size(&self) -> Size {
self.buffer.size() self.buffer.size()
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.buffer.reset(); self.buffer.reset();
self.cursor = None; self.title = None;
} }
pub fn cursor(&self) -> Option<Pos> { pub fn cursor(&self) -> Option<Pos> {
self.cursor self.buffer.cursor()
} }
pub fn set_cursor(&mut self, pos: Option<Pos>) { pub fn set_cursor(&mut self, pos: Option<Pos>) {
self.cursor = pos; self.buffer.set_cursor(pos);
} }
pub fn show_cursor(&mut self, pos: Pos) { pub fn show_cursor(&mut self, pos: Pos) {
@ -40,27 +45,19 @@ impl Frame {
self.set_cursor(None); self.set_cursor(None);
} }
/// Determine the width of a grapheme. pub fn set_title(&mut self, title: Option<String>) {
/// self.title = title;
/// If the width has not been measured yet, it is estimated using the
/// Unicode Standard Annex #11.
pub fn grapheme_width(&mut self, grapheme: &str) -> u8 {
self.widthdb.grapheme_width(grapheme)
} }
/// Determine the width of a string based on its graphemes. pub fn set_bell(&mut self, bell: bool) {
/// self.bell = bell;
/// If the width of a grapheme has not been measured yet, it is estimated
/// using the Unicode Standard Annex #11.
pub fn width(&mut self, s: &str) -> usize {
self.widthdb.width(s)
} }
pub fn wrap(&mut self, text: &str, width: usize) -> Vec<usize> { pub fn widthdb(&mut self) -> &mut WidthDb {
wrap::wrap(text, width, &mut self.widthdb) &mut self.widthdb
} }
pub fn write(&mut self, pos: Pos, content: &str, style: ContentStyle) { pub fn write<S: Into<Styled>>(&mut self, pos: Pos, styled: S) {
self.buffer.write(&mut self.widthdb, pos, content, style); self.buffer.write(&mut self.widthdb, pos, &styled.into());
} }
} }

View file

@ -1,7 +1,29 @@
#![forbid(unsafe_code)]
// Rustc lint groups
#![warn(future_incompatible)]
#![warn(rust_2018_idioms)]
#![warn(unused)]
// Rustc lints
#![warn(noop_method_call)]
#![warn(single_use_lifetimes)]
// Clippy lints
#![warn(clippy::use_self)]
mod buffer; mod buffer;
pub mod frame; mod coords;
pub mod terminal; mod frame;
mod style;
mod styled;
mod terminal;
mod widget;
pub mod widgets;
mod widthdb; mod widthdb;
mod wrap; mod wrap;
pub use wrap::split_at_indices; pub use coords::*;
pub use frame::*;
pub use style::*;
pub use styled::*;
pub use terminal::*;
pub use widget::*;
pub use widthdb::*;

60
src/style.rs Normal file
View 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
View 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
}
}

View file

@ -1,22 +1,28 @@
//! Displaying frames on a terminal. //! Displaying frames on a terminal.
use std::io::Write; use std::io::{self, Write};
use std::{io, mem}; use std::mem;
use crossterm::cursor::{Hide, MoveTo, Show}; use crossterm::cursor::{Hide, MoveTo, Show};
use crossterm::style::{PrintStyledContent, StyledContent}; use crossterm::event::{
use crossterm::terminal::{Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}; 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 crossterm::{ExecutableCommand, QueueableCommand};
use crate::buffer::{Buffer, Size}; use crate::buffer::Buffer;
use crate::frame::Frame; use crate::{AsyncWidget, Frame, Size, Widget, WidthDb, WidthEstimationMethod};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Redraw {
Required,
NotRequired,
}
/// 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 { pub struct Terminal {
/// Render target. /// Render target.
out: Box<dyn Write>, out: Box<dyn Write>,
@ -31,17 +37,17 @@ pub struct Terminal {
impl Drop for Terminal { impl Drop for Terminal {
fn drop(&mut self) { fn drop(&mut self) {
let _ = crossterm::terminal::disable_raw_mode(); let _ = self.suspend();
let _ = self.out.execute(LeaveAlternateScreen);
let _ = self.out.execute(Show);
} }
} }
impl Terminal { impl Terminal {
/// Create a new [`Terminal`] that wraps stdout.
pub fn new() -> io::Result<Self> { pub fn new() -> io::Result<Self> {
Self::with_target(Box::new(io::stdout())) 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> { pub fn with_target(out: Box<dyn Write>) -> io::Result<Self> {
let mut result = Self { let mut result = Self {
out, out,
@ -49,13 +55,136 @@ impl Terminal {
prev_frame_buffer: Buffer::default(), prev_frame_buffer: Buffer::default(),
full_redraw: true, full_redraw: true,
}; };
crossterm::terminal::enable_raw_mode()?; result.unsuspend()?;
result.out.execute(EnterAlternateScreen)?;
Ok(result) 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 /// Resize the frame and other internal buffers if the terminal size has
/// changed. /// 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<()> { pub fn autoresize(&mut self) -> io::Result<()> {
let (width, height) = crossterm::terminal::size()?; let (width, height) = crossterm::terminal::size()?;
let size = Size { width, height }; let size = Size { width, height };
@ -68,42 +197,86 @@ impl Terminal {
Ok(()) Ok(())
} }
/// The current frame.
pub fn frame(&mut self) -> &mut Frame { pub fn frame(&mut self) -> &mut Frame {
&mut self.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. /// Display the current frame on the screen and prepare the next frame.
/// Returns `true` if an immediate redraw is required. ///
/// 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 /// After calling this function, the frame returned by [`Self::frame`] will
/// be empty again and have no cursor position. /// be empty again and have no cursor position.
pub fn present(&mut self) -> io::Result<Redraw> { pub fn present(&mut self) -> io::Result<()> {
if self.frame.widthdb.measuring_required() { self.out.queue(BeginSynchronizedUpdate)?;
self.frame.widthdb.measure_widths(&mut self.out)?; let result = self.draw_to_screen();
// Since we messed up the screen by measuring widths, we'll need to self.out.queue(EndSynchronizedUpdate)?;
// do a full redraw the next time around. result?;
self.full_redraw = true;
// Throwing away the current frame because its content were rendered
// with unconfirmed width data. Also, this function guarantees that
// after it is called, the frame is empty.
self.frame.reset();
return Ok(Redraw::Required);
}
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 { if self.full_redraw {
io::stdout().queue(Clear(ClearType::All))?; self.out.queue(Clear(ClearType::All))?;
self.prev_frame_buffer.reset(); // Because the screen is now empty self.prev_frame_buffer.reset(); // Because the screen is now empty
self.full_redraw = false; self.full_redraw = false;
} }
self.draw_differences()?; self.draw_differences()?;
self.update_cursor()?; self.update_cursor()?;
self.out.flush()?; self.update_title()?;
self.ring_bell()?;
mem::swap(&mut self.prev_frame_buffer, &mut self.frame.buffer); Ok(())
self.frame.reset();
Ok(Redraw::NotRequired)
} }
fn draw_differences(&mut self) -> io::Result<()> { fn draw_differences(&mut self) -> io::Result<()> {
@ -136,4 +309,19 @@ impl Terminal {
self.out.queue(Hide)?; self.out.queue(Hide)?;
Ok(()) 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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(())
}
}

View file

@ -6,51 +6,129 @@ use crossterm::style::Print;
use crossterm::terminal::{Clear, ClearType}; use crossterm::terminal::{Clear, ClearType};
use crossterm::QueueableCommand; use crossterm::QueueableCommand;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr; 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. /// Measures and stores the with (in terminal coordinates) of graphemes.
#[derive(Debug, Default)] #[derive(Debug)]
pub struct WidthDB { pub struct WidthDb {
pub(crate) estimate: WidthEstimationMethod,
pub(crate) measure: bool,
pub(crate) tab_width: u8,
known: HashMap<String, u8>, known: HashMap<String, u8>,
requested: HashSet<String>, requested: HashSet<String>,
} }
impl WidthDB { 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. /// Determine the width of a grapheme.
/// ///
/// If the width has not been measured yet, it is estimated using the /// If the grapheme is a tab, the column is used to determine its width.
/// Unicode Standard Annex #11. ///
pub fn grapheme_width(&mut self, grapheme: &str) -> u8 { /// 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()); assert_eq!(Some(grapheme), grapheme.graphemes(true).next());
if let Some(width) = self.known.get(grapheme) { if grapheme == "\t" {
*width return self.tab_width_at_column(col);
} else { }
if self.measure {
if let Some(width) = self.known.get(grapheme) {
return *width;
}
self.requested.insert(grapheme.to_string()); self.requested.insert(grapheme.to_string());
grapheme.width() as u8 }
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. /// Determine the width of a string based on its graphemes.
/// ///
/// If the width of a grapheme has not been measured yet, it is estimated /// If a grapheme is a tab, its column is used to determine its width.
/// using the Unicode Standard Annex #11. ///
/// 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 { pub fn width(&mut self, s: &str) -> usize {
let mut total: usize = 0; let mut total: usize = 0;
for grapheme in s.graphemes(true) { for grapheme in s.graphemes(true) {
total += if let Some(width) = self.known.get(grapheme) { total += self.grapheme_width(grapheme, total) as usize;
(*width).into()
} else {
self.requested.insert(grapheme.to_string());
grapheme.width()
};
} }
total 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 /// Whether any new graphemes have been seen since the last time
/// [`Self::measure_widths`] was called. /// [`Self::measure_widths`] was called.
pub fn measuring_required(&self) -> bool { pub(crate) fn measuring_required(&self) -> bool {
!self.requested.is_empty() self.measure && !self.requested.is_empty()
} }
/// Measure the width of all new graphemes that have been seen since the /// Measure the width of all new graphemes that have been seen since the
@ -59,8 +137,20 @@ impl WidthDB {
/// This function measures the actual width of graphemes by writing them to /// This function measures the actual width of graphemes by writing them to
/// the terminal. After it finishes, the terminal's contents should be /// the terminal. After it finishes, the terminal's contents should be
/// assumed to be garbage and a full redraw should be performed. /// assumed to be garbage and a full redraw should be performed.
pub fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> { pub(crate) fn measure_widths(&mut self, out: &mut impl Write) -> io::Result<()> {
if !self.measure {
return Ok(());
}
for grapheme in self.requested.drain() { 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))? out.queue(Clear(ClearType::All))?
.queue(MoveTo(0, 0))? .queue(MoveTo(0, 0))?
.queue(Print(&grapheme))?; .queue(Print(&grapheme))?;

View file

@ -3,21 +3,21 @@
use unicode_linebreak::BreakOpportunity; use unicode_linebreak::BreakOpportunity;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use crate::widthdb::WidthDB; use crate::WidthDb;
// TODO Handle tabs separately? pub fn wrap(widthdb: &mut WidthDb, text: &str, width: usize) -> Vec<usize> {
// TODO Convert into an iterator?
pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec<usize> {
let mut breaks = vec![]; let mut breaks = vec![];
let mut break_options = unicode_linebreak::linebreaks(text).peekable(); let mut break_options = unicode_linebreak::linebreaks(text).peekable();
// The last valid break point encountered and its width // The last valid break point encountered and its width
let mut valid_break = None; let mut valid_break = None;
let mut valid_break_width = 0;
// Width of the line at the current grapheme // 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 = 0;
let mut current_width_trimmed = 0;
for (gi, g) in text.grapheme_indices(true) { for (gi, g) in text.grapheme_indices(true) {
// Advance break options // Advance break options
@ -36,60 +36,56 @@ pub fn wrap(text: &str, width: usize, widthdb: &mut WidthDB) -> Vec<usize> {
BreakOpportunity::Mandatory => { BreakOpportunity::Mandatory => {
breaks.push(bi); breaks.push(bi);
valid_break = None; valid_break = None;
valid_break_width = 0; current_start = bi;
current_width = 0; current_width = 0;
current_width_trimmed = 0;
} }
BreakOpportunity::Allowed => { BreakOpportunity::Allowed => {
valid_break = Some(bi); valid_break = Some(bi);
valid_break_width = current_width;
} }
} }
} }
let grapheme_width: usize = widthdb.grapheme_width(g).into(); // Calculate widths after current grapheme
if current_width + grapheme_width > width { let g_is_whitespace = g.chars().all(|c| c.is_whitespace());
if current_width == 0 { let g_width = widthdb.grapheme_width(g, current_width) as usize;
// The grapheme is wider than the maximum width, so we'll allow current_width += g_width;
// it, thereby forcing the following grapheme to break no matter if !g_is_whitespace {
// what (either because of a mandatory or allowed break, or via current_width_trimmed = current_width;
// a forced break). }
} else if let Some(bi) = valid_break {
// We can't fit the grapheme onto the current line, so we'll // Wrap at last break point if necessary
// just break at the last valid break point. if current_width_trimmed > width {
if let Some(bi) = valid_break {
let new_line = &text[bi..gi + g.len()];
breaks.push(bi); breaks.push(bi);
current_width -= valid_break_width;
valid_break = None; valid_break = None;
valid_break_width = 0; current_start = bi;
} else { current_width = widthdb.width(new_line);
// Forced break in the midde of a normally non-breakable chunk current_width_trimmed = widthdb.width(new_line.trim_end());
// because there have been no valid break points yet.
breaks.push(gi);
valid_break = None;
valid_break_width = 0;
current_width = 0;
} }
} }
current_width += grapheme_width; // 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 breaks
} }
pub fn split_at_indices<'a>(s: &'a str, indices: &[usize]) -> Vec<&'a str> {
let mut slices = vec![];
let mut rest = s;
let mut offset = 0;
for i in indices {
let (left, right) = rest.split_at(i - offset);
slices.push(left);
rest = right;
offset = *i;
}
slices.push(rest);
slices
}