2020-05-19 20:28:57 +00:00
|
|
|
use crate::{paint::*, *};
|
2020-04-29 19:25:49 +00:00
|
|
|
|
2020-05-16 17:38:46 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, serde_derive::Deserialize, serde_derive::Serialize)]
|
|
|
|
pub(crate) struct State {
|
2020-05-16 18:54:01 +00:00
|
|
|
/// Charctaer based, NOT bytes.
|
|
|
|
/// TODO: store as line + row
|
2020-05-16 17:38:46 +00:00
|
|
|
pub cursor: Option<usize>,
|
|
|
|
}
|
|
|
|
|
2020-04-29 19:25:49 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct TextEdit<'t> {
|
|
|
|
text: &'t mut String,
|
|
|
|
id: Option<Id>,
|
2020-05-08 20:42:31 +00:00
|
|
|
text_style: TextStyle, // TODO: Option<TextStyle>, where None means "use the default for the current Ui"
|
2020-04-29 19:25:49 +00:00
|
|
|
text_color: Option<Color>,
|
2020-05-17 07:44:09 +00:00
|
|
|
multiline: bool,
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'t> TextEdit<'t> {
|
|
|
|
pub fn new(text: &'t mut String) -> Self {
|
|
|
|
TextEdit {
|
|
|
|
text,
|
|
|
|
id: None,
|
|
|
|
text_style: TextStyle::Body,
|
|
|
|
text_color: Default::default(),
|
2020-05-17 07:44:09 +00:00
|
|
|
multiline: true,
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn id(mut self, id_source: impl std::hash::Hash) -> Self {
|
|
|
|
self.id = Some(Id::new(id_source));
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn text_style(mut self, text_style: TextStyle) -> Self {
|
|
|
|
self.text_style = text_style;
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn text_color(mut self, text_color: Color) -> Self {
|
|
|
|
self.text_color = Some(text_color);
|
|
|
|
self
|
|
|
|
}
|
2020-05-17 07:44:09 +00:00
|
|
|
|
|
|
|
pub fn multiline(mut self, multiline: bool) -> Self {
|
|
|
|
self.multiline = multiline;
|
|
|
|
self
|
|
|
|
}
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl<'t> Widget for TextEdit<'t> {
|
2020-05-08 20:42:31 +00:00
|
|
|
fn ui(self, ui: &mut Ui) -> GuiResponse {
|
2020-05-16 17:38:46 +00:00
|
|
|
let TextEdit {
|
|
|
|
text,
|
|
|
|
id,
|
|
|
|
text_style,
|
|
|
|
text_color,
|
2020-05-17 07:44:09 +00:00
|
|
|
multiline,
|
2020-05-16 17:38:46 +00:00
|
|
|
} = self;
|
|
|
|
|
|
|
|
let id = ui.make_child_id(id);
|
2020-04-29 19:25:49 +00:00
|
|
|
|
2020-05-16 17:38:46 +00:00
|
|
|
let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default();
|
|
|
|
|
|
|
|
let font = &ui.fonts()[text_style];
|
2020-04-29 19:25:49 +00:00
|
|
|
let line_spacing = font.line_spacing();
|
2020-05-16 17:38:46 +00:00
|
|
|
let available_width = ui.available().width();
|
2020-05-17 07:44:09 +00:00
|
|
|
let mut galley = if multiline {
|
2020-05-20 19:22:42 +00:00
|
|
|
font.layout_multiline(text.clone(), available_width)
|
2020-05-17 07:44:09 +00:00
|
|
|
} else {
|
2020-05-20 19:22:42 +00:00
|
|
|
font.layout_single_line(text.clone())
|
2020-05-17 07:44:09 +00:00
|
|
|
};
|
2020-05-16 17:38:46 +00:00
|
|
|
let desired_size = galley.size.max(vec2(available_width, line_spacing));
|
2020-05-08 20:42:31 +00:00
|
|
|
let interact = ui.reserve_space(desired_size, Some(id));
|
2020-04-29 19:25:49 +00:00
|
|
|
|
|
|
|
if interact.clicked {
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.request_kb_focus(id);
|
2020-05-16 18:54:01 +00:00
|
|
|
if let Some(mouse_pos) = ui.input().mouse_pos {
|
2020-05-17 20:32:04 +00:00
|
|
|
state.cursor = Some(galley.char_at(mouse_pos - interact.rect.min).char_idx);
|
2020-05-16 18:54:01 +00:00
|
|
|
}
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
if interact.hovered {
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.output().cursor_icon = CursorIcon::Text;
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
2020-05-08 20:42:31 +00:00
|
|
|
let has_kb_focus = ui.has_kb_focus(id);
|
2020-04-29 19:25:49 +00:00
|
|
|
|
|
|
|
if has_kb_focus {
|
2020-05-16 17:38:46 +00:00
|
|
|
let mut cursor = state.cursor.unwrap_or_else(|| text.chars().count());
|
|
|
|
cursor = clamp(cursor, 0..=text.chars().count());
|
|
|
|
|
2020-05-08 20:42:31 +00:00
|
|
|
for event in &ui.input().events {
|
2020-04-29 19:25:49 +00:00
|
|
|
match event {
|
|
|
|
Event::Copy | Event::Cut => {
|
|
|
|
// TODO: cut
|
2020-05-16 17:38:46 +00:00
|
|
|
ui.ctx().output().copied_text = text.clone();
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
2020-05-16 17:38:46 +00:00
|
|
|
Event::Text(text_to_insert) => {
|
|
|
|
insert_text(&mut cursor, text, text_to_insert);
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
Event::Key { key, pressed: true } => {
|
2020-05-16 17:38:46 +00:00
|
|
|
on_key_press(&mut cursor, text, *key);
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
}
|
2020-05-16 17:38:46 +00:00
|
|
|
state.cursor = Some(cursor);
|
|
|
|
|
|
|
|
// layout again to avoid frame delay:
|
|
|
|
let font = &ui.fonts()[text_style];
|
2020-05-17 07:44:09 +00:00
|
|
|
galley = if multiline {
|
2020-05-20 19:22:42 +00:00
|
|
|
font.layout_multiline(text.clone(), available_width)
|
2020-05-17 07:44:09 +00:00
|
|
|
} else {
|
2020-05-20 19:22:42 +00:00
|
|
|
font.layout_single_line(text.clone())
|
2020-05-17 07:44:09 +00:00
|
|
|
};
|
2020-05-16 17:38:46 +00:00
|
|
|
|
|
|
|
// dbg!(&galley);
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
|
2020-05-17 07:44:09 +00:00
|
|
|
{
|
|
|
|
let bg_rect = interact.rect.expand(2.0); // breathing room for content
|
|
|
|
ui.add_paint_cmd(PaintCmd::Rect {
|
|
|
|
rect: bg_rect,
|
|
|
|
corner_radius: ui.style().interact.style(&interact).corner_radius,
|
|
|
|
fill_color: Some(ui.style().dark_bg_color),
|
|
|
|
outline: ui.style().interact.style(&interact).rect_outline,
|
|
|
|
});
|
|
|
|
}
|
2020-04-29 19:25:49 +00:00
|
|
|
|
|
|
|
if has_kb_focus {
|
2020-05-08 20:42:31 +00:00
|
|
|
let cursor_blink_hz = ui.style().cursor_blink_hz;
|
2020-04-29 19:25:49 +00:00
|
|
|
let show_cursor =
|
2020-05-08 20:42:31 +00:00
|
|
|
(ui.input().time * cursor_blink_hz as f64 * 3.0).floor() as i64 % 3 != 0;
|
2020-04-29 19:25:49 +00:00
|
|
|
if show_cursor {
|
2020-05-16 17:38:46 +00:00
|
|
|
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,
|
|
|
|
));
|
|
|
|
}
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-16 17:38:46 +00:00
|
|
|
ui.add_galley(interact.rect.min, galley, text_style, text_color);
|
|
|
|
ui.memory().text_edit.insert(id, state);
|
2020-05-08 20:42:31 +00:00
|
|
|
ui.response(interact)
|
2020-04-29 19:25:49 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-16 17:38:46 +00:00
|
|
|
|
|
|
|
fn insert_text(cursor: &mut usize, text: &mut String, text_to_insert: &str) {
|
2020-05-17 20:32:04 +00:00
|
|
|
eprintln!("insert_text {:?}", text_to_insert);
|
2020-05-16 17:38:46 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2020-05-17 20:32:04 +00:00
|
|
|
|
2020-05-16 17:38:46 +00:00
|
|
|
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 => {
|
2020-05-17 20:32:04 +00:00
|
|
|
// To start of paragraph:
|
|
|
|
let pos = line_col_from_char_idx(text, *cursor);
|
|
|
|
*cursor = char_idx_from_line_col(text, (pos.0, 0));
|
2020-05-16 17:38:46 +00:00
|
|
|
}
|
|
|
|
Key::End => {
|
2020-05-17 20:32:04 +00:00
|
|
|
// To end of paragraph:
|
|
|
|
let pos = line_col_from_char_idx(text, *cursor);
|
|
|
|
let line = line_from_number(text, pos.0);
|
|
|
|
*cursor = char_idx_from_line_col(text, (pos.0, line.chars().count()));
|
2020-05-16 17:38:46 +00:00
|
|
|
}
|
|
|
|
Key::Left if *cursor > 0 => {
|
|
|
|
*cursor -= 1;
|
|
|
|
}
|
|
|
|
Key::Right => {
|
|
|
|
*cursor = (*cursor + 1).min(text.chars().count());
|
|
|
|
}
|
2020-05-17 20:32:04 +00:00
|
|
|
Key::Up => {
|
|
|
|
let mut pos = line_col_from_char_idx(text, *cursor);
|
|
|
|
pos.0 = pos.0.saturating_sub(1);
|
|
|
|
*cursor = char_idx_from_line_col(text, pos);
|
|
|
|
}
|
|
|
|
Key::Down => {
|
|
|
|
let mut pos = line_col_from_char_idx(text, *cursor);
|
|
|
|
pos.0 += 1;
|
|
|
|
*cursor = char_idx_from_line_col(text, pos);
|
|
|
|
}
|
2020-05-16 17:38:46 +00:00
|
|
|
_ => {}
|
|
|
|
}
|
|
|
|
|
|
|
|
// eprintln!("on_key_press after: '{}', cursor at {}\n", text, cursor);
|
|
|
|
}
|
2020-05-17 20:32:04 +00:00
|
|
|
|
|
|
|
fn line_col_from_char_idx(s: &str, char_idx: usize) -> (usize, usize) {
|
|
|
|
let mut char_count = 0;
|
|
|
|
|
|
|
|
let mut last_line_nr = 0;
|
|
|
|
let mut last_line = s;
|
|
|
|
for (line_nr, line) in s.split('\n').enumerate() {
|
|
|
|
let line_width = line.chars().count();
|
|
|
|
if char_idx <= char_count + line_width {
|
|
|
|
return (line_nr, char_idx - char_count);
|
|
|
|
}
|
|
|
|
char_count += line_width + 1;
|
|
|
|
last_line_nr = line_nr;
|
|
|
|
last_line = line;
|
|
|
|
}
|
|
|
|
|
|
|
|
// safe fallback:
|
|
|
|
(last_line_nr, last_line.chars().count())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn char_idx_from_line_col(s: &str, pos: (usize, usize)) -> usize {
|
|
|
|
let mut char_count = 0;
|
|
|
|
for (line_nr, line) in s.split('\n').enumerate() {
|
|
|
|
if line_nr == pos.0 {
|
|
|
|
return char_count + pos.1.min(line.chars().count());
|
|
|
|
}
|
|
|
|
char_count += line.chars().count() + 1;
|
|
|
|
}
|
|
|
|
char_count
|
|
|
|
}
|
|
|
|
|
|
|
|
fn line_from_number(s: &str, desired_line_number: usize) -> &str {
|
|
|
|
for (line_nr, line) in s.split('\n').enumerate() {
|
|
|
|
if line_nr == desired_line_number {
|
|
|
|
return line;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|