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()
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
use crate::{layout::Direction, GuiResponse, *};
|
||||
|
||||
mod slider;
|
||||
mod text_edit;
|
||||
pub mod text_edit;
|
||||
|
||||
pub use {slider::*, text_edit::*};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue