mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-23 17:35:00 +00:00
Better wide char support
This commit is contained in:
parent
cb7a1e37a4
commit
cc72aa4ddc
@ -28,6 +28,7 @@ Bangui
|
|||||||
Banjul
|
Banjul
|
||||||
Basseterre
|
Basseterre
|
||||||
Beijing
|
Beijing
|
||||||
|
北京
|
||||||
Beirut
|
Beirut
|
||||||
Belgrade
|
Belgrade
|
||||||
Belmopan
|
Belmopan
|
||||||
@ -227,6 +228,7 @@ Thimphu
|
|||||||
Tirana
|
Tirana
|
||||||
Tiraspol
|
Tiraspol
|
||||||
Tokyo
|
Tokyo
|
||||||
|
東京
|
||||||
Tórshavn
|
Tórshavn
|
||||||
Tripoli
|
Tripoli
|
||||||
Tskhinvali
|
Tskhinvali
|
||||||
@ -234,7 +236,7 @@ Tunis
|
|||||||
Ulaanbaatar
|
Ulaanbaatar
|
||||||
Vaduz
|
Vaduz
|
||||||
Valletta
|
Valletta
|
||||||
Valparaísoso
|
Valparaíso
|
||||||
Vatican City
|
Vatican City
|
||||||
Victoria
|
Victoria
|
||||||
Vienna
|
Vienna
|
||||||
|
@ -8,6 +8,8 @@ use event::*;
|
|||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
/// Current state of the menubar
|
/// Current state of the menubar
|
||||||
#[derive(PartialEq, Debug)]
|
#[derive(PartialEq, Debug)]
|
||||||
enum State {
|
enum State {
|
||||||
@ -78,7 +80,7 @@ impl Menubar {
|
|||||||
(i == self.focus);
|
(i == self.focus);
|
||||||
printer.with_selection(selected, |printer| {
|
printer.with_selection(selected, |printer| {
|
||||||
printer.print((offset, 0), &format!(" {} ", title));
|
printer.print((offset, 0), &format!(" {} ", title));
|
||||||
offset += title.len() + 2;
|
offset += title.width() + 2;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -108,7 +110,7 @@ impl Menubar {
|
|||||||
self.state = State::Submenu;
|
self.state = State::Submenu;
|
||||||
let offset = (self.menus[..self.focus]
|
let offset = (self.menus[..self.focus]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|&(ref title, _)| title.len() + 2)
|
.map(|&(ref title, _)| title.width() + 2)
|
||||||
.fold(0, |a, b| a + b),
|
.fold(0, |a, b| a + b),
|
||||||
if self.autohide {
|
if self.autohide {
|
||||||
1
|
1
|
||||||
|
@ -6,6 +6,7 @@ use vec::Vec2;
|
|||||||
use view::{View};
|
use view::{View};
|
||||||
use event::*;
|
use event::*;
|
||||||
use printer::Printer;
|
use printer::Printer;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
/// Simple text label with a callback when ENTER is pressed.
|
/// Simple text label with a callback when ENTER is pressed.
|
||||||
/// A button shows its content in a single line and has a fixed size.
|
/// A button shows its content in a single line and has a fixed size.
|
||||||
@ -44,7 +45,7 @@ impl View for Button {
|
|||||||
|
|
||||||
fn get_min_size(&self, _: Vec2) -> Vec2 {
|
fn get_min_size(&self, _: Vec2) -> Vec2 {
|
||||||
// Meh. Fixed size we are.
|
// Meh. Fixed size we are.
|
||||||
Vec2::new(2 + self.label.chars().count(), 1)
|
Vec2::new(2 + self.label.width(), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_event(&mut self, event: Event) -> EventResult {
|
fn on_event(&mut self, event: Event) -> EventResult {
|
||||||
|
@ -10,6 +10,8 @@ use view::{Button, SizedView};
|
|||||||
use vec::{ToVec4, Vec2, Vec4};
|
use vec::{ToVec4, Vec2, Vec4};
|
||||||
use printer::Printer;
|
use printer::Printer;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
enum Focus {
|
enum Focus {
|
||||||
Content,
|
Content,
|
||||||
@ -147,7 +149,7 @@ impl View for Dialog {
|
|||||||
printer.print_box(Vec2::new(0, 0), printer.size);
|
printer.print_box(Vec2::new(0, 0), printer.size);
|
||||||
|
|
||||||
if !self.title.is_empty() {
|
if !self.title.is_empty() {
|
||||||
let len = self.title.chars().count();
|
let len = self.title.width();
|
||||||
let x = (printer.size.x - len) / 2;
|
let x = (printer.size.x - len) / 2;
|
||||||
printer.print((x - 2, 0), "┤ ");
|
printer.print((x - 2, 0), "┤ ");
|
||||||
printer.print((x + len, 0), " ├");
|
printer.print((x + len, 0), " ├");
|
||||||
@ -181,7 +183,7 @@ impl View for Dialog {
|
|||||||
|
|
||||||
if !self.title.is_empty() {
|
if !self.title.is_empty() {
|
||||||
// If we have a title, we have to fit it too!
|
// If we have a title, we have to fit it too!
|
||||||
inner_size.x = max(inner_size.x, self.title.chars().count() + 6);
|
inner_size.x = max(inner_size.x, self.title.width() + 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
inner_size
|
inner_size
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
|
|
||||||
@ -8,16 +9,19 @@ use view::{IdView, View};
|
|||||||
use event::*;
|
use event::*;
|
||||||
use printer::Printer;
|
use printer::Printer;
|
||||||
|
|
||||||
|
|
||||||
/// Input box where the user can enter and edit text.
|
/// Input box where the user can enter and edit text.
|
||||||
pub struct EditView {
|
pub struct EditView {
|
||||||
/// Current content
|
/// Current content.
|
||||||
content: String,
|
content: String,
|
||||||
/// Cursor position in the content
|
/// Cursor position in the content, in bytes.
|
||||||
cursor: usize,
|
cursor: usize,
|
||||||
/// Minimum layout length asked to the parent
|
/// Minimum layout length asked to the parent.
|
||||||
min_length: usize,
|
min_length: usize,
|
||||||
|
|
||||||
/// When the content is too long for the display, offset it
|
/// Number of bytes to skip at the beginning of the content.
|
||||||
|
///
|
||||||
|
/// (When the content is too long for the display, we hide part of it)
|
||||||
offset: usize,
|
offset: usize,
|
||||||
/// Last display length, to know the possible offset range
|
/// Last display length, to know the possible offset range
|
||||||
last_length: usize, /* scrollable: bool,
|
last_length: usize, /* scrollable: bool,
|
||||||
@ -73,48 +77,62 @@ impl EditView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_char(s: &mut String, cursor: usize) {
|
|
||||||
let i = match s.char_indices().nth(cursor) {
|
|
||||||
Some((i, _)) => i,
|
|
||||||
None => return,
|
|
||||||
};
|
|
||||||
s.remove(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl View for EditView {
|
impl View for EditView {
|
||||||
fn draw(&mut self, printer: &Printer) {
|
fn draw(&mut self, printer: &Printer) {
|
||||||
// let style = if focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE };
|
// let style = if focused { color::HIGHLIGHT } else { color::HIGHLIGHT_INACTIVE };
|
||||||
let len = self.content.chars().count();
|
assert!(printer.size.x == self.last_length);
|
||||||
|
|
||||||
|
let width = self.content.width();
|
||||||
printer.with_color(ColorStyle::Secondary, |printer| {
|
printer.with_color(ColorStyle::Secondary, |printer| {
|
||||||
printer.with_effect(Effect::Reverse, |printer| {
|
printer.with_effect(Effect::Reverse, |printer| {
|
||||||
if len < self.last_length {
|
if width < self.last_length {
|
||||||
|
// No problem, everything fits.
|
||||||
printer.print((0, 0), &self.content);
|
printer.print((0, 0), &self.content);
|
||||||
printer.print_hline((len, 0), printer.size.x - len, "_");
|
printer.print_hline((width, 0),
|
||||||
|
printer.size.x - width,
|
||||||
|
"_");
|
||||||
} else {
|
} else {
|
||||||
let visible_end = min(self.content.len(), self.offset + self.last_length);
|
let content = &self.content[self.offset..];
|
||||||
|
let display_bytes = content.graphemes(true)
|
||||||
|
.scan(0, |w, g| {
|
||||||
|
*w += g.width();
|
||||||
|
if *w > self.last_length {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(g)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|g| g.len())
|
||||||
|
.fold(0, |a, b| a + b);
|
||||||
|
|
||||||
|
let content = &content[..display_bytes];
|
||||||
|
|
||||||
let content = &self.content[self.offset..visible_end];
|
|
||||||
printer.print((0, 0), content);
|
printer.print((0, 0), content);
|
||||||
if visible_end - self.offset < printer.size.x {
|
let width = content.width();
|
||||||
printer.print((printer.size.x - 1, 0), "_");
|
|
||||||
|
if width < self.last_length {
|
||||||
|
printer.print_hline((width, 0),
|
||||||
|
self.last_length - width,
|
||||||
|
"_");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Now print cursor
|
// Now print cursor
|
||||||
if printer.focused {
|
if printer.focused {
|
||||||
let c = if self.cursor == len {
|
let c = if self.cursor == self.content.len() {
|
||||||
"_"
|
"_"
|
||||||
} else {
|
} else {
|
||||||
// Get the char from the string... Is it so hard?
|
// Get the char from the string... Is it so hard?
|
||||||
self.content
|
self.content[self.cursor..]
|
||||||
.graphemes(true)
|
.graphemes(true)
|
||||||
.nth(self.cursor)
|
.next()
|
||||||
.expect(&format!("Found no char at cursor {} in {}",
|
.expect(&format!("Found no char at cursor {} in {}",
|
||||||
self.cursor,
|
self.cursor,
|
||||||
self.content))
|
self.content))
|
||||||
};
|
};
|
||||||
printer.print_hline((self.cursor - self.offset, 0), 1, c);
|
let offset = self.content[self.offset..self.cursor].width();
|
||||||
|
printer.print((offset, 0), c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -137,25 +155,41 @@ impl View for EditView {
|
|||||||
Event::Char(ch) => {
|
Event::Char(ch) => {
|
||||||
// Find the byte index of the char at self.cursor
|
// Find the byte index of the char at self.cursor
|
||||||
|
|
||||||
match self.content.char_indices().nth(self.cursor) {
|
self.content.insert(self.cursor, ch);
|
||||||
None => self.content.push(ch),
|
|
||||||
Some((i, _)) => self.content.insert(i, ch),
|
|
||||||
}
|
|
||||||
// TODO: handle wide (CJK) chars
|
// TODO: handle wide (CJK) chars
|
||||||
self.cursor += 1;
|
self.cursor += ch.len_utf8();
|
||||||
}
|
}
|
||||||
Event::Key(key) => {
|
Event::Key(key) => {
|
||||||
match key {
|
match key {
|
||||||
Key::Home => self.cursor = 0,
|
Key::Home => self.cursor = 0,
|
||||||
Key::End => self.cursor = self.content.chars().count(),
|
Key::End => self.cursor = self.content.len(),
|
||||||
Key::Left if self.cursor > 0 => self.cursor -= 1,
|
Key::Left if self.cursor > 0 => {
|
||||||
Key::Right if self.cursor < self.content.chars().count() => self.cursor += 1,
|
let len = self.content[..self.cursor]
|
||||||
Key::Backspace if self.cursor > 0 => {
|
.graphemes(true)
|
||||||
self.cursor -= 1;
|
.last()
|
||||||
remove_char(&mut self.content, self.cursor);
|
.unwrap()
|
||||||
|
.len();
|
||||||
|
self.cursor -= len;
|
||||||
}
|
}
|
||||||
Key::Del if self.cursor < self.content.chars().count() => {
|
Key::Right if self.cursor < self.content.len() => {
|
||||||
remove_char(&mut self.content, self.cursor);
|
let len = self.content[self.cursor..]
|
||||||
|
.graphemes(true)
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.len();
|
||||||
|
self.cursor += len;
|
||||||
|
}
|
||||||
|
Key::Backspace if self.cursor > 0 => {
|
||||||
|
let len = self.content[..self.cursor]
|
||||||
|
.graphemes(true)
|
||||||
|
.last()
|
||||||
|
.unwrap()
|
||||||
|
.len();
|
||||||
|
self.cursor -= len;
|
||||||
|
self.content.remove(self.cursor);
|
||||||
|
}
|
||||||
|
Key::Del if self.cursor < self.content.len() => {
|
||||||
|
self.content.remove(self.cursor);
|
||||||
}
|
}
|
||||||
_ => return EventResult::Ignored,
|
_ => return EventResult::Ignored,
|
||||||
}
|
}
|
||||||
@ -165,20 +199,51 @@ impl View for EditView {
|
|||||||
// Keep cursor in [offset, offset+last_length] by changing offset
|
// Keep cursor in [offset, offset+last_length] by changing offset
|
||||||
// So keep offset in [last_length-cursor,cursor]
|
// So keep offset in [last_length-cursor,cursor]
|
||||||
// Also call this on resize, but right now it is an event like any other
|
// Also call this on resize, but right now it is an event like any other
|
||||||
if self.cursor >= self.offset + self.last_length {
|
if self.cursor < self.offset {
|
||||||
self.offset = self.cursor - self.last_length + 1;
|
|
||||||
} else if self.cursor < self.offset {
|
|
||||||
self.offset = self.cursor;
|
self.offset = self.cursor;
|
||||||
}
|
|
||||||
if self.offset + self.last_length > self.content.len() + 1 {
|
|
||||||
|
|
||||||
self.offset = if self.content.len() > self.last_length {
|
|
||||||
self.content.len() - self.last_length + 1
|
|
||||||
} else {
|
} else {
|
||||||
0
|
// So we're against the right wall.
|
||||||
};
|
// Let's find how much space will be taken by the selection (either a char, or _)
|
||||||
|
let c_len = self.content[self.cursor..]
|
||||||
|
.graphemes(true)
|
||||||
|
.map(|g| g.width())
|
||||||
|
.next()
|
||||||
|
.unwrap_or(1);
|
||||||
|
// Now, we have to fit self.content[..self.cursor] into self.last_length - c_len.
|
||||||
|
let available = self.last_length - c_len;
|
||||||
|
// Look at the content before the cursor (we will print its tail).
|
||||||
|
// From the end, count the length until we reach `available`.
|
||||||
|
// Then sum the byte lengths.
|
||||||
|
let tail_bytes =
|
||||||
|
tail_bytes(&self.content[self.offset..self.cursor], available);
|
||||||
|
self.offset = self.cursor - tail_bytes;
|
||||||
|
assert!(self.cursor >= self.offset);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have too much space
|
||||||
|
if self.content[self.offset..].width() < self.last_length {
|
||||||
|
let tail_bytes = tail_bytes(&self.content, self.last_length - 1);
|
||||||
|
self.offset = self.content.len() - tail_bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
EventResult::Consumed(None)
|
EventResult::Consumed(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the number of bytes, from the end of text,
|
||||||
|
// which constitute the longest tail that fits in the given width.
|
||||||
|
fn tail_bytes(text: &str, width: usize) -> usize {
|
||||||
|
text.graphemes(true)
|
||||||
|
.rev()
|
||||||
|
.scan(0, |w, g| {
|
||||||
|
*w += g.width();
|
||||||
|
if *w > width {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(g)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|g| g.len())
|
||||||
|
.fold(0, |a, b| a + b)
|
||||||
|
}
|
||||||
|
@ -9,6 +9,8 @@ use event::{Event, EventResult, Key};
|
|||||||
use vec::Vec2;
|
use vec::Vec2;
|
||||||
use printer::Printer;
|
use printer::Printer;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
struct Item<T> {
|
struct Item<T> {
|
||||||
label: String,
|
label: String,
|
||||||
value: Rc<T>,
|
value: Rc<T>,
|
||||||
@ -134,7 +136,7 @@ impl<T: 'static> View for SelectView<T> {
|
|||||||
|
|
||||||
self.scrollbase.draw(printer, |printer, i| {
|
self.scrollbase.draw(printer, |printer, i| {
|
||||||
printer.with_selection(i == self.focus, |printer| {
|
printer.with_selection(i == self.focus, |printer| {
|
||||||
let l = self.items[i].label.chars().count();
|
let l = self.items[i].label.width();
|
||||||
let x = self.align.h.get_offset(l, printer.size.x);
|
let x = self.align.h.get_offset(l, printer.size.x);
|
||||||
printer.print_hline((0, 0), x, " ");
|
printer.print_hline((0, 0), x, " ");
|
||||||
printer.print((x, 0), &self.items[i].label);
|
printer.print((x, 0), &self.items[i].label);
|
||||||
@ -149,7 +151,7 @@ impl<T: 'static> View for SelectView<T> {
|
|||||||
// we'll still return our longest item.
|
// we'll still return our longest item.
|
||||||
let w = self.items
|
let w = self.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|item| item.label.len())
|
.map(|item| item.label.width())
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(1);
|
.unwrap_or(1);
|
||||||
let h = self.items.len();
|
let h = self.items.len();
|
||||||
|
@ -7,6 +7,8 @@ use align::*;
|
|||||||
use event::*;
|
use event::*;
|
||||||
use super::scroll::ScrollBase;
|
use super::scroll::ScrollBase;
|
||||||
|
|
||||||
|
use unicode_width::UnicodeWidthStr;
|
||||||
|
|
||||||
/// A simple view showing a fixed text
|
/// A simple view showing a fixed text
|
||||||
pub struct TextView {
|
pub struct TextView {
|
||||||
content: String,
|
content: String,
|
||||||
@ -40,7 +42,7 @@ fn get_line_span(line: &str, max_width: usize) -> usize {
|
|||||||
// (Or use a common function? Better!)
|
// (Or use a common function? Better!)
|
||||||
let mut lines = 1;
|
let mut lines = 1;
|
||||||
let mut length = 0;
|
let mut length = 0;
|
||||||
for l in line.split(' ').map(|word| word.chars().count()) {
|
for l in line.split(' ').map(|word| word.width()) {
|
||||||
length += l;
|
length += l;
|
||||||
if length > max_width {
|
if length > max_width {
|
||||||
length = l;
|
length = l;
|
||||||
@ -111,7 +113,7 @@ impl TextView {
|
|||||||
|
|
||||||
for line in self.content.split('\n') {
|
for line in self.content.split('\n') {
|
||||||
height += 1;
|
height += 1;
|
||||||
max_width = max(max_width, line.chars().count());
|
max_width = max(max_width, line.width());
|
||||||
}
|
}
|
||||||
|
|
||||||
Vec2::new(max_width, height)
|
Vec2::new(max_width, height)
|
||||||
@ -150,7 +152,7 @@ impl<'a> Iterator for LinesIterator<'a> {
|
|||||||
let content = &self.content[self.start..];
|
let content = &self.content[self.start..];
|
||||||
|
|
||||||
if let Some(next) = content.find('\n') {
|
if let Some(next) = content.find('\n') {
|
||||||
if content[..next].chars().count() <= self.width {
|
if content[..next].width() <= self.width {
|
||||||
// We found a newline before the allowed limit.
|
// We found a newline before the allowed limit.
|
||||||
// Break early.
|
// Break early.
|
||||||
self.start += next + 1;
|
self.start += next + 1;
|
||||||
@ -161,7 +163,7 @@ impl<'a> Iterator for LinesIterator<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let content_len = content.chars().count();
|
let content_len = content.width();
|
||||||
if content_len <= self.width {
|
if content_len <= self.width {
|
||||||
// I thought it would be longer! -- that's what she said :(
|
// I thought it would be longer! -- that's what she said :(
|
||||||
self.start += content.len();
|
self.start += content.len();
|
||||||
@ -209,7 +211,7 @@ impl View for TextView {
|
|||||||
self.scrollbase.draw(printer, |printer, i| {
|
self.scrollbase.draw(printer, |printer, i| {
|
||||||
let row = &self.rows[i];
|
let row = &self.rows[i];
|
||||||
let text = &self.content[row.start..row.end];
|
let text = &self.content[row.start..row.end];
|
||||||
let l = text.chars().count();
|
let l = text.width();
|
||||||
let x = self.align.h.get_offset(l, printer.size.x);
|
let x = self.align.h.get_offset(l, printer.size.x);
|
||||||
printer.print((x, 0), text);
|
printer.print((x, 0), text);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user