Improved text edit with cursor that can be moved with arrow keys
This commit is contained in:
parent
89aa285255
commit
c0e7f947ff
|
|
@ -52,7 +52,8 @@ pub struct Galley {
|
||||||
/// The number of chars in all lines sum up to text.chars().count()
|
/// The number of chars in all lines sum up to text.chars().count()
|
||||||
pub lines: Vec<Line>,
|
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,
|
pub size: Vec2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,10 +66,40 @@ impl Galley {
|
||||||
}
|
}
|
||||||
assert_eq!(char_count, self.text.chars().count());
|
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)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct UvRect {
|
pub struct UvRect {
|
||||||
/// X/Y offset for nice rendering (unit: points).
|
/// 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) {
|
for c in (FIRST_ASCII..=LAST_ASCII).map(|c| c as u8 as char) {
|
||||||
font.add_char(c);
|
font.add_char(c);
|
||||||
}
|
}
|
||||||
|
font.add_char(REPLACEMENT_CHAR);
|
||||||
|
|
||||||
font
|
font
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -148,11 +181,20 @@ impl Font {
|
||||||
self.glyph_infos.get(&c).and_then(|gi| gi.uv_rect)
|
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)
|
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) {
|
fn add_char(&mut self, c: char) {
|
||||||
|
if self.glyph_infos.contains_key(&c) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let glyph = self.font.glyph(c);
|
let glyph = self.font.glyph(c);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
glyph.id().0,
|
glyph.id().0,
|
||||||
|
|
@ -224,19 +266,18 @@ impl Font {
|
||||||
let mut last_glyph_id = None;
|
let mut last_glyph_id = None;
|
||||||
|
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if let Some(glyph) = self.glyph_info(c) {
|
let glyph = self.glyph_info_or_replacemnet(c);
|
||||||
if let Some(last_glyph_id) = last_glyph_id {
|
|
||||||
cursor_x_in_points +=
|
if let Some(last_glyph_id) = last_glyph_id {
|
||||||
self.font
|
cursor_x_in_points +=
|
||||||
.pair_kerning(scale_in_pixels, last_glyph_id, glyph.id)
|
self.font
|
||||||
/ self.pixels_per_point
|
.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
|
|
||||||
}
|
}
|
||||||
|
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);
|
x_offsets.push(cursor_x_in_points);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -352,16 +393,20 @@ impl Font {
|
||||||
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
|
self.layout_paragraph_max_width(paragraph_text, max_width_in_points);
|
||||||
assert!(!paragraph_lines.is_empty());
|
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 {
|
for line in &mut paragraph_lines {
|
||||||
line.y_offset += cursor_y;
|
line.y_offset += cursor_y;
|
||||||
}
|
}
|
||||||
lines.append(&mut paragraph_lines);
|
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;
|
paragraph_start = paragraph_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if text.ends_with('\n') {
|
||||||
|
cursor_y += line_spacing;
|
||||||
|
}
|
||||||
|
|
||||||
let mut widest_line = 0.0;
|
let mut widest_line = 0.0;
|
||||||
for line in &lines {
|
for line in &lines {
|
||||||
widest_line = line.max_x().max(widest_line);
|
widest_line = line.max_x().max(widest_line);
|
||||||
|
|
|
||||||
|
|
@ -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")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub enum Key {
|
pub enum Key {
|
||||||
Alt,
|
Alt,
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
if x <= *range.start() {
|
||||||
*range.start()
|
*range.start()
|
||||||
} else if *range.end() <= x {
|
} else if *range.end() <= x {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
containers::{area, collapsing_header, menu, resize, scroll_area},
|
containers::{area, collapsing_header, menu, resize, scroll_area},
|
||||||
|
widgets::text_edit,
|
||||||
Id, Layer, Pos2, Rect,
|
Id, Layer, Pos2, Rect,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,8 +20,9 @@ pub struct Memory {
|
||||||
// states of various types of widgets
|
// states of various types of widgets
|
||||||
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
|
pub(crate) collapsing_headers: HashMap<Id, collapsing_header::State>,
|
||||||
pub(crate) menu_bar: HashMap<Id, menu::BarState>,
|
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) 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,
|
pub(crate) areas: Areas,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
use crate::{layout::Direction, GuiResponse, *};
|
use crate::{layout::Direction, GuiResponse, *};
|
||||||
|
|
||||||
mod slider;
|
mod slider;
|
||||||
mod text_edit;
|
pub mod text_edit;
|
||||||
|
|
||||||
pub use {slider::*, text_edit::*};
|
pub use {slider::*, text_edit::*};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
use crate::*;
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct TextEdit<'t> {
|
pub struct TextEdit<'t> {
|
||||||
text: &'t mut String,
|
text: &'t mut String,
|
||||||
|
|
@ -36,12 +42,22 @@ impl<'t> TextEdit<'t> {
|
||||||
|
|
||||||
impl<'t> Widget for TextEdit<'t> {
|
impl<'t> Widget for TextEdit<'t> {
|
||||||
fn ui(self, ui: &mut Ui) -> GuiResponse {
|
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 line_spacing = font.line_spacing();
|
||||||
let galley = font.layout_multiline(self.text.as_str(), ui.available().width());
|
let available_width = ui.available().width();
|
||||||
let desired_size = galley.size.max(vec2(ui.available().width(), line_spacing));
|
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));
|
let interact = ui.reserve_space(desired_size, Some(id));
|
||||||
|
|
||||||
if interact.clicked {
|
if interact.clicked {
|
||||||
|
|
@ -53,27 +69,31 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
let has_kb_focus = ui.has_kb_focus(id);
|
let has_kb_focus = ui.has_kb_focus(id);
|
||||||
|
|
||||||
if has_kb_focus {
|
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 {
|
for event in &ui.input().events {
|
||||||
match event {
|
match event {
|
||||||
Event::Copy | Event::Cut => {
|
Event::Copy | Event::Cut => {
|
||||||
// TODO: cut
|
// TODO: cut
|
||||||
ui.ctx().output().copied_text = self.text.clone();
|
ui.ctx().output().copied_text = text.clone();
|
||||||
}
|
}
|
||||||
Event::Text(text) => {
|
Event::Text(text_to_insert) => {
|
||||||
if text == "\u{7f}" {
|
insert_text(&mut cursor, text, text_to_insert);
|
||||||
// backspace
|
|
||||||
} else {
|
|
||||||
*self.text += text;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Event::Key { key, pressed: true } => {
|
Event::Key { key, pressed: true } => {
|
||||||
if *key == Key::Backspace {
|
on_key_press(&mut cursor, text, *key);
|
||||||
self.text.pop(); // TODO: unicode aware
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
ui.add_paint_cmd(PaintCmd::Rect {
|
||||||
|
|
@ -90,21 +110,77 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
let show_cursor =
|
let show_cursor =
|
||||||
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
|
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
|
||||||
if show_cursor {
|
if show_cursor {
|
||||||
let cursor_pos = if let Some(last) = galley.lines.last() {
|
if let Some(cursor) = state.cursor {
|
||||||
interact.rect.min + vec2(last.max_x(), last.y_offset)
|
let cursor_pos = interact.rect.min + galley.char_start_pos(cursor);
|
||||||
} else {
|
ui.add_paint_cmd(PaintCmd::line_segment(
|
||||||
interact.rect.min
|
[cursor_pos, cursor_pos + vec2(0.0, line_spacing)],
|
||||||
};
|
color::WHITE,
|
||||||
ui.add_paint_cmd(PaintCmd::line_segment(
|
ui.style().text_cursor_width,
|
||||||
[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)
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,13 @@ pub fn input_event(
|
||||||
raw_input.mouse_pos = None;
|
raw_input.mouse_pos = None;
|
||||||
}
|
}
|
||||||
ReceivedCharacter(ch) => {
|
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, .. } => {
|
KeyboardInput { input, .. } => {
|
||||||
if let Some(virtual_keycode) = input.virtual_keycode {
|
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> {
|
pub fn translate_virtual_key_code(key: glutin::VirtualKeyCode) -> Option<emigui::Key> {
|
||||||
use VirtualKeyCode::*;
|
use VirtualKeyCode::*;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue