Implement simple single-line editor
This commit is contained in:
parent
cd320b3678
commit
e188a99f2a
4 changed files with 166 additions and 2 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -208,6 +208,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"toss",
|
"toss",
|
||||||
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1245,7 +1246,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toss"
|
name = "toss"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/Garmelon/toss.git?rev=26bf89023e254778b9dcb826840f677ed7105292#26bf89023e254778b9dcb826840f677ed7105292"
|
source = "git+https://github.com/Garmelon/toss.git?rev=d693712dab61d806c3ac36083d27016e67794154#d693712dab61d806c3ac36083d27016e67794154"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossterm",
|
"crossterm",
|
||||||
"unicode-linebreak",
|
"unicode-linebreak",
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ serde = { version = "1.0.138", features = ["derive"] }
|
||||||
serde_json = "1.0.82"
|
serde_json = "1.0.82"
|
||||||
thiserror = "1.0.31"
|
thiserror = "1.0.31"
|
||||||
tokio = { version = "1.19.2", features = ["full"] }
|
tokio = { version = "1.19.2", features = ["full"] }
|
||||||
|
unicode-segmentation = "1.9.0"
|
||||||
unicode-width = "0.1.9"
|
unicode-width = "0.1.9"
|
||||||
|
|
||||||
[dependencies.tokio-tungstenite]
|
[dependencies.tokio-tungstenite]
|
||||||
|
|
@ -29,4 +30,4 @@ features = ["rustls-tls-native-roots"]
|
||||||
|
|
||||||
[dependencies.toss]
|
[dependencies.toss]
|
||||||
git = "https://github.com/Garmelon/toss.git"
|
git = "https://github.com/Garmelon/toss.git"
|
||||||
rev = "26bf89023e254778b9dcb826840f677ed7105292"
|
rev = "d693712dab61d806c3ac36083d27016e67794154"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
mod chat;
|
mod chat;
|
||||||
|
mod editor;
|
||||||
mod list;
|
mod list;
|
||||||
mod room;
|
mod room;
|
||||||
mod rooms;
|
mod rooms;
|
||||||
|
|
|
||||||
161
src/ui/editor.rs
Normal file
161
src/ui/editor.rs
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
use std::iter;
|
||||||
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crossterm::style::ContentStyle;
|
||||||
|
use toss::frame::{Frame, Pos};
|
||||||
|
use toss::styled::Styled;
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
|
pub struct Editor {
|
||||||
|
text: String,
|
||||||
|
|
||||||
|
/// Index of the cursor in the text.
|
||||||
|
///
|
||||||
|
/// Must point to a valid grapheme boundary.
|
||||||
|
idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Editor {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
text: String::new(),
|
||||||
|
idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
fn move_cursor_to_grapheme_boundary(&mut self) {
|
||||||
|
for i in self.grapheme_boundaries() {
|
||||||
|
#[allow(clippy::comparison_chain)]
|
||||||
|
if i == self.idx {
|
||||||
|
// We're at a valid grapheme boundary already
|
||||||
|
return;
|
||||||
|
} else if i > self.idx {
|
||||||
|
// There was no valid grapheme boundary at our cursor index, so
|
||||||
|
// we'll take the next one we can get.
|
||||||
|
self.idx = i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This loop should always return since the index behind the last
|
||||||
|
// grapheme is included in the grapheme boundary iterator.
|
||||||
|
panic!("cursor index out of bounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a character at the current cursor position and move the cursor
|
||||||
|
/// accordingly.
|
||||||
|
pub fn insert_char(&mut self, ch: char) {
|
||||||
|
self.text.insert(self.idx, ch);
|
||||||
|
self.idx += 1;
|
||||||
|
self.move_cursor_to_grapheme_boundary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete the grapheme before the cursor position.
|
||||||
|
pub fn backspace(&mut self) {
|
||||||
|
let boundaries = self.grapheme_boundaries();
|
||||||
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
|
if *end == self.idx {
|
||||||
|
self.text.replace_range(start..end, "");
|
||||||
|
self.idx = *start;
|
||||||
|
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.idx {
|
||||||
|
self.text.replace_range(start..end, "");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor_left(&mut self) {
|
||||||
|
let boundaries = self.grapheme_boundaries();
|
||||||
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
|
if *end == self.idx {
|
||||||
|
self.idx = *start;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_cursor_right(&mut self) {
|
||||||
|
let boundaries = self.grapheme_boundaries();
|
||||||
|
for (start, end) in boundaries.iter().zip(boundaries.iter().skip(1)) {
|
||||||
|
if *start == self.idx {
|
||||||
|
self.idx = *end;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap(&self, frame: &mut Frame, width: usize) -> Vec<Range<usize>> {
|
||||||
|
let mut rows = vec![];
|
||||||
|
let mut start = 0;
|
||||||
|
let mut col = 0;
|
||||||
|
for (i, g) in self.text.grapheme_indices(true) {
|
||||||
|
let grapheme_width = if g == "\t" {
|
||||||
|
frame.tab_width_at_column(col)
|
||||||
|
} else {
|
||||||
|
frame.grapheme_width(g)
|
||||||
|
} as usize;
|
||||||
|
|
||||||
|
if col + grapheme_width > width {
|
||||||
|
rows.push(start..i);
|
||||||
|
start = i;
|
||||||
|
col = grapheme_width;
|
||||||
|
} else {
|
||||||
|
col += grapheme_width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.push(start..self.text.len());
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_highlighted<F>(&self, frame: &mut Frame, pos: Pos, width: usize, highlight: F)
|
||||||
|
where
|
||||||
|
F: Fn(&str) -> Styled,
|
||||||
|
{
|
||||||
|
let text = highlight(&self.text);
|
||||||
|
let row_ranges = self.wrap(frame, width);
|
||||||
|
let breakpoints = row_ranges
|
||||||
|
.iter()
|
||||||
|
.skip(1)
|
||||||
|
.map(|r| r.start)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let rows = text.split_at_indices(&breakpoints);
|
||||||
|
for (i, row) in rows.into_iter().enumerate() {
|
||||||
|
let pos = pos + Pos::new(0, i as i32);
|
||||||
|
frame.write(pos, row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_with_style(
|
||||||
|
&self,
|
||||||
|
frame: &mut Frame,
|
||||||
|
pos: Pos,
|
||||||
|
width: usize,
|
||||||
|
style: ContentStyle,
|
||||||
|
) {
|
||||||
|
self.render_highlighted(frame, pos, width, |s| Styled::new((s, style)));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, frame: &mut Frame, pos: Pos, width: usize) {
|
||||||
|
self.render_highlighted(frame, pos, width, |s| Styled::new(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue