Better wide char support

This commit is contained in:
Alexandre Bury 2016-07-04 16:04:32 -07:00
parent cb7a1e37a4
commit cc72aa4ddc
7 changed files with 135 additions and 59 deletions

View File

@ -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íso­so Valparaíso
Vatican City Vatican City
Victoria Victoria
Vienna Vienna

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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