Improved text edit with cursor that can be moved with arrow keys

This commit is contained in:
Emil Ernerfeldt 2020-05-16 19:38:46 +02:00
parent 89aa285255
commit c0e7f947ff
7 changed files with 199 additions and 47 deletions

View File

@ -52,7 +52,8 @@ pub struct Galley {
/// The number of chars in all lines sum up to text.chars().count()
pub lines: Vec<Line>,
// TODO: remove? Can just calculate on the fly
// We need size here to keep track of extra newline at the end. Hacky. Should fix.
// Newlines should probably be part of the start of the line?
pub size: Vec2,
}
@ -65,10 +66,40 @@ impl Galley {
}
assert_eq!(char_count, self.text.chars().count());
}
/// If given a char index after the first line, the end of the last character is returned instead.
/// Returns a Vec2 rather than a Pos2 as this is an offset into the galley. *shrug*
pub fn char_start_pos(&self, char_idx: usize) -> Vec2 {
let mut char_count = 0;
for line in &self.lines {
let line_char_count = line.char_count();
if char_count <= char_idx && char_idx < char_count + line_char_count {
let line_char_offset = char_idx - char_count;
return vec2(line.x_offsets[line_char_offset], line.y_offset);
}
char_count += line_char_count;
}
if let Some(last) = self.lines.last() {
if self.text.ends_with('\n') {
// The position of the next character will be here:
vec2(0.0, 0.5 * (self.size.y + last.y_offset)) // TODO: fix this hack
} else {
vec2(last.max_x(), last.y_offset)
}
} else {
// Empty galley
vec2(0.0, 0.0)
}
}
}
// ----------------------------------------------------------------------------
// const REPLACEMENT_CHAR: char = '\u{25A1}'; // □ white square Replaces a missing or unsupported Unicode character.
// const REPLACEMENT_CHAR: char = '\u{FFFD}'; // <20> REPLACEMENT CHARACTER
const REPLACEMENT_CHAR: char = '?';
#[derive(Clone, Copy, Debug)]
pub struct UvRect {
/// X/Y offset for nice rendering (unit: points).
@ -128,6 +159,8 @@ impl Font {
for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
font.add_char(c);
}
font.add_char(REPLACEMENT_CHAR);
font
}
@ -148,11 +181,20 @@ impl Font {
self.glyph_infos.get(&c).and_then(|gi| gi.uv_rect)
}
fn glyph_info(&self, c: char) -> Option<&GlyphInfo> {
fn glyph_info_or_none(&self, c: char) -> Option<&GlyphInfo> {
self.glyph_infos.get(&c)
}
fn glyph_info_or_replacemnet(&self, c: char) -> &GlyphInfo {
self.glyph_info_or_none(c)
.unwrap_or_else(|| self.glyph_info_or_none(REPLACEMENT_CHAR).unwrap())
}
fn add_char(&mut self, c: char) {
if self.glyph_infos.contains_key(&c) {
return;
}
let glyph = self.font.glyph(c);
assert_ne!(
glyph.id().0,
@ -224,19 +266,18 @@ impl Font {
let mut last_glyph_id = None;
for c in text.chars() {
if let Some(glyph) = self.glyph_info(c) {
if let Some(last_glyph_id) = last_glyph_id {
cursor_x_in_points +=
self.font
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
/ self.pixels_per_point
}
cursor_x_in_points += glyph.advance_width;
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
last_glyph_id = Some(glyph.id);
} else {
// Ignore unknown glyph
let glyph = self.glyph_info_or_replacemnet(c);
if let Some(last_glyph_id) = last_glyph_id {
cursor_x_in_points +=
self.font
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
/ self.pixels_per_point
}
cursor_x_in_points += glyph.advance_width;
cursor_x_in_points = self.round_to_pixel(cursor_x_in_points);
last_glyph_id = Some(glyph.id);
x_offsets.push(cursor_x_in_points);
}
@ -352,16 +393,20 @@ impl Font {
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
assert!(!paragraph_lines.is_empty());
let line_height = paragraph_lines.last().unwrap().y_offset + line_spacing;
let paragraph_height = paragraph_lines.last().unwrap().y_offset + line_spacing;
for line in &mut paragraph_lines {
line.y_offset += cursor_y;
}
lines.append(&mut paragraph_lines);
cursor_y += line_height; // TODO: add extra spacing between paragraphs
cursor_y += paragraph_height; // TODO: add extra spacing between paragraphs
paragraph_start = paragraph_end;
}
if text.ends_with('\n') {
cursor_y += line_spacing;
}
let mut widest_line = 0.0;
for line in &lines {
widest_line = line.max_x().max(widest_line);

View File

@ -120,7 +120,7 @@ pub enum Event {
},
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Key {
Alt,

View File

@ -547,7 +547,10 @@ pub fn remap_clamp(x: f32, from: RangeInclusive<f32>, to: RangeInclusive<f32>) -
}
}
pub fn clamp(x: f32, range: RangeInclusive<f32>) -> f32 {
pub fn clamp<T>(x: T, range: RangeInclusive<T>) -> T
where
T: Copy + PartialOrd,
{
if x <= *range.start() {
*range.start()
} else if *range.end() <= x {

View File

@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
use crate::{
containers::{area, collapsing_header, menu, resize, scroll_area},
widgets::text_edit,
Id, Layer, Pos2, Rect,
};
@ -19,8 +20,9 @@ pub struct Memory {
// states of various types of widgets
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
pub(crate) menu_bar: HashMap<Id, menu::BarState>,
pub(crate) scroll_areas: HashMap<Id, scroll_area::State>,
pub(crate) resize: HashMap<Id, resize::State>,
pub(crate) scroll_areas: HashMap<Id, scroll_area::State>,
pub(crate) text_edit: HashMap<Id, text_edit::State>,
pub(crate) areas: Areas,
}

View File

@ -3,7 +3,7 @@
use crate::{layout::Direction, GuiResponse, *};
mod slider;
mod text_edit;
pub mod text_edit;
pub use {slider::*, text_edit::*};

View File

@ -1,5 +1,11 @@
use crate::*;
#[derive(Clone, Copy, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
pub(crate) struct State {
/// Charctaer based, NOT bytes
pub cursor: Option<usize>,
}
#[derive(Debug)]
pub struct TextEdit<'t> {
text: &'t mut String,
@ -36,12 +42,22 @@ impl<'t> TextEdit<'t> {
impl<'t> Widget for TextEdit<'t> {
fn ui(self, ui: &mut Ui) -> GuiResponse {
let id = ui.make_child_id(self.id);
let TextEdit {
text,
id,
text_style,
text_color,
} = self;
let font = &ui.fonts()[self.text_style];
let id = ui.make_child_id(id);
let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default();
let font = &ui.fonts()[text_style];
let line_spacing = font.line_spacing();
let galley = font.layout_multiline(self.text.as_str(), ui.available().width());
let desired_size = galley.size.max(vec2(ui.available().width(), line_spacing));
let available_width = ui.available().width();
let mut galley = font.layout_multiline(text.as_str(), available_width);
let desired_size = galley.size.max(vec2(available_width, line_spacing));
let interact = ui.reserve_space(desired_size, Some(id));
if interact.clicked {
@ -53,27 +69,31 @@ impl<'t> Widget for TextEdit<'t> {
let has_kb_focus = ui.has_kb_focus(id);
if has_kb_focus {
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
cursor = clamp(cursor, 0..=text.chars().count());
for event in &ui.input().events {
match event {
Event::Copy | Event::Cut => {
// TODO: cut
ui.ctx().output().copied_text = self.text.clone();
ui.ctx().output().copied_text = text.clone();
}
Event::Text(text) => {
if text == "\u{7f}" {
// backspace
} else {
*self.text += text;
}
Event::Text(text_to_insert) => {
insert_text(&mut cursor, text, text_to_insert);
}
Event::Key { key, pressed: true } => {
if *key == Key::Backspace {
self.text.pop(); // TODO: unicode aware
}
on_key_press(&mut cursor, text, *key);
}
_ => {}
}
}
state.cursor = Some(cursor);
// layout again to avoid frame delay:
let font = &ui.fonts()[text_style];
galley = font.layout_multiline(text.as_str(), available_width);
// dbg!(&galley);
}
ui.add_paint_cmd(PaintCmd::Rect {
@ -90,21 +110,77 @@ impl<'t> Widget for TextEdit<'t> {
let show_cursor =
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
if show_cursor {
let cursor_pos = if let Some(last) = galley.lines.last() {
interact.rect.min + vec2(last.max_x(), last.y_offset)
} else {
interact.rect.min
};
ui.add_paint_cmd(PaintCmd::line_segment(
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
color::WHITE,
ui.style().text_cursor_width,
));
if let Some(cursor) = state.cursor {
let cursor_pos = interact.rect.min + galley.char_start_pos(cursor);
ui.add_paint_cmd(PaintCmd::line_segment(
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
color::WHITE,
ui.style().text_cursor_width,
));
}
}
}
ui.add_galley(interact.rect.min, galley, self.text_style, self.text_color);
ui.add_galley(interact.rect.min, galley, text_style, text_color);
ui.memory().text_edit.insert(id, state);
ui.response(interact)
}
}
fn insert_text(cursor: &mut usize, text: &mut String, text_to_insert: &str) {
// eprintln!("insert_text before: '{}', cursor at {}", text, cursor);
let mut char_it = text.chars();
let mut new_text = String::with_capacity(text.capacity());
for _ in 0..*cursor {
let c = char_it.next().unwrap();
new_text.push(c);
}
*cursor += text_to_insert.chars().count();
new_text += text_to_insert;
new_text.extend(char_it);
*text = new_text;
// eprintln!("insert_text after: '{}', cursor at {}\n", text, cursor);
}
fn on_key_press(cursor: &mut usize, text: &mut String, key: Key) {
// eprintln!("on_key_press before: '{}', cursor at {}", text, cursor);
match key {
Key::Backspace if *cursor > 0 => {
*cursor -= 1;
let mut char_it = text.chars();
let mut new_text = String::with_capacity(text.capacity());
for _ in 0..*cursor {
new_text.push(char_it.next().unwrap())
}
new_text.extend(char_it.skip(1));
*text = new_text;
}
Key::Delete => {
let mut char_it = text.chars();
let mut new_text = String::with_capacity(text.capacity());
for _ in 0..*cursor {
new_text.push(char_it.next().unwrap())
}
new_text.extend(char_it.skip(1));
*text = new_text;
}
Key::Home => {
*cursor = 0; // TODO: start of line
}
Key::End => {
*cursor = text.chars().count(); // TODO: end of line
}
Key::Left if *cursor > 0 => {
*cursor -= 1;
}
Key::Right => {
*cursor = (*cursor + 1).min(text.chars().count());
}
_ => {}
}
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
}

View File

@ -48,7 +48,13 @@ pub fn input_event(
raw_input.mouse_pos = None;
}
ReceivedCharacter(ch) => {
raw_input.events.push(Event::Text(ch.to_string()));
if !should_ignore_char(ch) {
if ch == '\r' {
raw_input.events.push(Event::Text("\n".to_owned()));
} else {
raw_input.events.push(Event::Text(ch.to_string()));
}
}
}
KeyboardInput { input, .. } => {
if let Some(virtual_keycode) = input.virtual_keycode {
@ -103,6 +109,26 @@ pub fn input_event(
}
}
fn should_ignore_char(chr: char) -> bool {
// Glium sends some keys as chars:
match chr {
'\u{7f}' | // backspace
'\u{f728}' | // delete
'\u{f700}' | // up
'\u{f701}' | // down
'\u{f702}' | // left
'\u{f703}' | // right
'\u{f729}' | // home
'\u{f72b}' | // end
'\u{f72c}' | // page up
'\u{f72d}' | // page down
'\u{f710}' | // print screen
'\u{f704}' | '\u{f705}' // F1, F2, ...
=> true,
_ => false,
}
}
pub fn translate_virtual_key_code(key: glutin::VirtualKeyCode) -> Option<emigui::Key> {
use VirtualKeyCode::*;