use crate::{paint::*, util::undoer::Undoer, *};
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) struct State {
cursorp: Option<CursorPair>,
#[cfg_attr(feature = "serde", serde(skip))]
undoer: Undoer<(CCursorPair, String)>,
}
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct CursorPair {
pub primary: Cursor,
pub secondary: Cursor,
}
impl CursorPair {
fn one(cursor: Cursor) -> Self {
Self {
primary: cursor,
secondary: cursor,
}
}
fn two(min: Cursor, max: Cursor) -> Self {
Self {
primary: max,
secondary: min,
}
}
fn as_ccursorp(&self) -> CCursorPair {
CCursorPair {
primary: self.primary.ccursor,
secondary: self.secondary.ccursor,
}
}
fn is_empty(&self) -> bool {
self.primary.ccursor == self.secondary.ccursor
}
fn single(&self) -> Option<Cursor> {
if self.is_empty() {
Some(self.primary)
} else {
None
}
}
fn primary_is_first(&self) -> bool {
let p = self.primary.ccursor;
let s = self.secondary.ccursor;
(p.index, p.prefer_next_row) <= (s.index, s.prefer_next_row)
}
fn sorted(&self) -> [Cursor; 2] {
if self.primary_is_first() {
[self.primary, self.secondary]
} else {
[self.secondary, self.primary]
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
struct CCursorPair {
pub primary: CCursor,
pub secondary: CCursor,
}
impl CCursorPair {
fn one(ccursor: CCursor) -> Self {
Self {
primary: ccursor,
secondary: ccursor,
}
}
fn two(min: CCursor, max: CCursor) -> Self {
Self {
primary: max,
secondary: min,
}
}
}
#[derive(Debug)]
pub struct TextEdit<'t> {
text: &'t mut String,
id: Option<Id>,
id_source: Option<Id>,
text_style: Option<TextStyle>,
text_color: Option<Color32>,
multiline: bool,
enabled: bool,
desired_width: Option<f32>,
desired_height_rows: usize,
}
impl<'t> TextEdit<'t> {
#[deprecated = "Use `TextEdit::singleline` or `TextEdit::multiline` (or the helper `ui.text_edit_singleline`, `ui.text_edit_multiline`) instead"]
pub fn new(text: &'t mut String) -> Self {
Self::multiline(text)
}
pub fn singleline(text: &'t mut String) -> Self {
TextEdit {
text,
id: None,
id_source: None,
text_style: None,
text_color: None,
multiline: false,
enabled: true,
desired_width: None,
desired_height_rows: 1,
}
}
pub fn multiline(text: &'t mut String) -> Self {
TextEdit {
text,
id: None,
id_source: None,
text_style: None,
text_color: None,
multiline: true,
enabled: true,
desired_width: None,
desired_height_rows: 4,
}
}
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
pub fn id_source(mut self, id_source: impl std::hash::Hash) -> Self {
self.id_source = Some(Id::new(id_source));
self
}
pub fn text_style(mut self, text_style: TextStyle) -> Self {
self.text_style = Some(text_style);
self
}
pub fn text_color(mut self, text_color: Color32) -> Self {
self.text_color = Some(text_color);
self
}
pub fn text_color_opt(mut self, text_color: Option<Color32>) -> Self {
self.text_color = text_color;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn desired_width(mut self, desired_width: f32) -> Self {
self.desired_width = Some(desired_width);
self
}
pub fn desired_rows(mut self, desired_height_rows: usize) -> Self {
self.desired_height_rows = desired_height_rows;
self
}
}
impl<'t> Widget for TextEdit<'t> {
fn ui(self, ui: &mut Ui) -> Response {
let margin = Vec2::splat(2.0);
let frame_rect = ui.available_rect_before_wrap();
let content_rect = frame_rect.shrink2(margin);
let where_to_put_background = ui.painter().add(PaintCmd::Noop);
let mut content_ui = ui.child_ui(content_rect, *ui.layout());
let response = self.content_ui(&mut content_ui);
let frame_rect = Rect::from_min_max(frame_rect.min, content_ui.min_rect().max + margin);
let response = response | ui.allocate_response(frame_rect.size(), Sense::click());
let visuals = ui.style().interact(&response);
let frame_rect = response.rect;
ui.painter().set(
where_to_put_background,
PaintCmd::Rect {
rect: frame_rect,
corner_radius: visuals.corner_radius,
fill: ui.style().visuals.dark_bg_color,
stroke: visuals.bg_stroke,
},
);
response
}
}
impl<'t> TextEdit<'t> {
fn content_ui(self, ui: &mut Ui) -> Response {
let TextEdit {
text,
id,
id_source,
text_style,
text_color,
multiline,
enabled,
desired_width,
desired_height_rows,
} = self;
let text_style = text_style.unwrap_or_else(|| ui.style().body_text_style);
let font = &ui.fonts()[text_style];
let line_spacing = font.row_height();
let available_width = ui.available_width();
let mut galley = if multiline {
font.layout_multiline(text.clone(), available_width)
} else {
font.layout_single_line(text.clone())
};
let desired_width = desired_width.unwrap_or_else(|| ui.style().spacing.text_edit_width);
let desired_height = (desired_height_rows.at_least(1) as f32) * line_spacing;
let desired_size = vec2(
galley.size.x.max(desired_width.min(available_width)),
galley.size.y.max(desired_height),
);
let (auto_id, rect) = ui.allocate_space(desired_size);
let id = id.unwrap_or_else(|| {
if let Some(id_source) = id_source {
ui.make_persistent_id(id_source)
} else {
auto_id
}
});
let mut state = ui.memory().text_edit.get(&id).cloned().unwrap_or_default();
let sense = if enabled {
Sense::click_and_drag()
} else {
Sense::hover()
};
let response = ui.interact(rect, id, sense);
if enabled {
ui.memory().interested_in_kb_focus(id);
}
if enabled {
if let Some(mouse_pos) = ui.input().mouse.pos {
let cursor_at_mouse = galley.cursor_from_pos(mouse_pos - response.rect.min);
if response.hovered {
paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse);
}
if response.hovered && response.double_clicked {
let center = cursor_at_mouse;
let ccursorp = select_word_at(text, center.ccursor);
state.cursorp = Some(CursorPair {
primary: galley.from_ccursor(ccursorp.primary),
secondary: galley.from_ccursor(ccursorp.secondary),
});
} else if response.hovered && ui.input().mouse.pressed {
ui.memory().request_kb_focus(id);
if ui.input().modifiers.shift {
if let Some(cursorp) = &mut state.cursorp {
cursorp.primary = cursor_at_mouse;
} else {
state.cursorp = Some(CursorPair::one(cursor_at_mouse));
}
} else {
state.cursorp = Some(CursorPair::one(cursor_at_mouse));
}
} else if ui.input().mouse.down && response.active {
if let Some(cursorp) = &mut state.cursorp {
cursorp.primary = cursor_at_mouse;
}
}
}
}
if ui.input().mouse.pressed && !response.hovered {
ui.memory().surrender_kb_focus(id);
}
if !enabled {
ui.memory().surrender_kb_focus(id);
}
if response.hovered && enabled {
ui.output().cursor_icon = CursorIcon::Text;
}
if ui.memory().has_kb_focus(id) && enabled {
let mut cursorp = state
.cursorp
.map(|cursorp| {
CursorPair {
primary: galley.from_pcursor(cursorp.primary.pcursor),
secondary: galley.from_pcursor(cursorp.secondary.pcursor),
}
})
.unwrap_or_else(|| CursorPair::one(galley.end()));
state
.undoer
.feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone()));
for event in &ui.input().events {
let did_mutate_text = match event {
Event::Copy => {
if cursorp.is_empty() {
ui.ctx().output().copied_text = text.clone();
} else {
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
}
None
}
Event::Cut => {
if cursorp.is_empty() {
ui.ctx().output().copied_text = std::mem::take(text);
Some(CCursorPair::default())
} else {
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
Some(CCursorPair::one(delete_selected(text, &cursorp)))
}
}
Event::Text(text_to_insert) => {
if !text_to_insert.is_empty()
&& text_to_insert != "\n"
&& text_to_insert != "\r"
{
let mut ccursor = delete_selected(text, &cursorp);
insert_text(&mut ccursor, text, text_to_insert);
Some(CCursorPair::one(ccursor))
} else {
None
}
}
Event::Key {
key: Key::Enter,
pressed: true,
..
} => {
if multiline {
let mut ccursor = delete_selected(text, &cursorp);
insert_text(&mut ccursor, text, "\n");
Some(CCursorPair::one(ccursor))
} else {
ui.memory().surrender_kb_focus(id);
break;
}
}
Event::Key {
key: Key::Escape,
pressed: true,
..
} => {
ui.memory().surrender_kb_focus(id);
break;
}
Event::Key {
key: Key::Z,
pressed: true,
modifiers,
} if modifiers.command && !modifiers.shift => {
if let Some((undo_ccursorp, undo_txt)) =
state.undoer.undo(&(cursorp.as_ccursorp(), text.clone()))
{
*text = undo_txt.clone();
Some(*undo_ccursorp)
} else {
None
}
}
Event::Key {
key,
pressed: true,
modifiers,
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
Event::Key { .. } => None,
};
if let Some(new_ccursorp) = did_mutate_text {
let font = &ui.fonts()[text_style];
galley = if multiline {
font.layout_multiline(text.clone(), available_width)
} else {
font.layout_single_line(text.clone())
};
cursorp = CursorPair {
primary: galley.from_ccursor(new_ccursorp.primary),
secondary: galley.from_ccursor(new_ccursorp.secondary),
};
}
}
state.cursorp = Some(cursorp);
state
.undoer
.feed_state(ui.input().time, &(cursorp.as_ccursorp(), text.clone()));
}
if ui.memory().has_kb_focus(id) {
if let Some(cursorp) = state.cursorp {
paint_cursor_selection(ui, response.rect.min, &galley, &cursorp);
paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary);
}
}
let text_color = text_color
.or(ui.style().visuals.override_text_color)
.unwrap_or_else(|| ui.style().visuals.widgets.inactive.text_color());
ui.painter()
.galley(response.rect.min, galley, text_style, text_color);
ui.memory().text_edit.insert(id, state);
Response {
lost_kb_focus: ui.memory().lost_kb_focus(id),
..response
}
}
}
fn paint_cursor_selection(ui: &mut Ui, pos: Pos2, galley: &Galley, cursorp: &CursorPair) {
let color = ui.style().visuals.selection.bg_fill;
if cursorp.is_empty() {
return;
}
let [min, max] = cursorp.sorted();
let min = min.rcursor;
let max = max.rcursor;
for ri in min.row..=max.row {
let row = &galley.rows[ri];
let left = if ri == min.row {
row.x_offset(min.column)
} else {
row.min_x()
};
let right = if ri == max.row {
row.x_offset(max.column)
} else {
let newline_size = if row.ends_with_newline {
row.height() / 2.0
} else {
0.0
};
row.max_x() + newline_size
};
let rect = Rect::from_min_max(pos + vec2(left, row.y_min), pos + vec2(right, row.y_max));
ui.painter().rect_filled(rect, 0.0, color);
}
}
fn paint_cursor_end(ui: &mut Ui, pos: Pos2, galley: &Galley, cursor: &Cursor) {
let stroke = ui.style().visuals.selection.stroke;
let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
let cursor_pos = cursor_pos.expand(1.5);
let top = cursor_pos.center_top();
let bottom = cursor_pos.center_bottom();
ui.painter().line_segment(
[top, bottom],
(ui.style().visuals.text_cursor_width, stroke.color),
);
if false {
let extrusion = 3.0;
let width = 1.0;
ui.painter().line_segment(
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
(width, stroke.color),
);
ui.painter().line_segment(
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
(width, stroke.color),
);
}
}
fn selected_str<'s>(text: &'s str, cursorp: &CursorPair) -> &'s str {
let [min, max] = cursorp.sorted();
let byte_begin = byte_index_from_char_index(text, min.ccursor.index);
let byte_end = byte_index_from_char_index(text, max.ccursor.index);
&text[byte_begin..byte_end]
}
fn byte_index_from_char_index(s: &str, char_index: usize) -> usize {
for (ci, (bi, _)) in s.char_indices().enumerate() {
if ci == char_index {
return bi;
}
}
s.len()
}
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
let mut char_it = text.chars();
let mut new_text = String::with_capacity(text.len() + text_to_insert.len());
for _ in 0..ccursor.index {
let c = char_it.next().unwrap();
new_text.push(c);
}
ccursor.index += text_to_insert.chars().count();
new_text += text_to_insert;
new_text.extend(char_it);
*text = new_text;
}
fn delete_selected(text: &mut String, cursorp: &CursorPair) -> CCursor {
let [min, max] = cursorp.sorted();
delete_selected_ccursor_range(text, [min.ccursor, max.ccursor])
}
fn delete_selected_ccursor_range(text: &mut String, [min, max]: [CCursor; 2]) -> CCursor {
let [min, max] = [min.index, max.index];
assert!(min <= max);
if min < max {
let mut char_it = text.chars();
let mut new_text = String::with_capacity(text.len());
for _ in 0..min {
new_text.push(char_it.next().unwrap())
}
new_text.extend(char_it.skip(max - min));
*text = new_text;
}
CCursor {
index: min,
prefer_next_row: true,
}
}
fn delete_previous_char(text: &mut String, ccursor: CCursor) -> CCursor {
if ccursor.index > 0 {
let max_ccursor = ccursor;
let min_ccursor = max_ccursor - 1;
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
} else {
ccursor
}
}
fn delete_next_char(text: &mut String, ccursor: CCursor) -> CCursor {
delete_selected_ccursor_range(text, [ccursor, ccursor + 1])
}
fn delete_previous_word(text: &mut String, max_ccursor: CCursor) -> CCursor {
let min_ccursor = ccursor_previous_word(text, max_ccursor);
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
}
fn delete_next_word(text: &mut String, min_ccursor: CCursor) -> CCursor {
let max_ccursor = ccursor_next_word(text, min_ccursor);
delete_selected_ccursor_range(text, [min_ccursor, max_ccursor])
}
fn delete_paragraph_before_cursor(
text: &mut String,
galley: &Galley,
cursorp: &CursorPair,
) -> CCursor {
let [min, max] = cursorp.sorted();
let min = galley.from_pcursor(PCursor {
paragraph: min.pcursor.paragraph,
offset: 0,
prefer_next_row: true,
});
if min.ccursor == max.ccursor {
delete_previous_char(text, min.ccursor)
} else {
delete_selected(text, &CursorPair::two(min, max))
}
}
fn delete_paragraph_after_cursor(
text: &mut String,
galley: &Galley,
cursorp: &CursorPair,
) -> CCursor {
let [min, max] = cursorp.sorted();
let max = galley.from_pcursor(PCursor {
paragraph: max.pcursor.paragraph,
offset: usize::MAX,
prefer_next_row: false,
});
if min.ccursor == max.ccursor {
delete_next_char(text, min.ccursor)
} else {
delete_selected(text, &CursorPair::two(min, max))
}
}
fn on_key_press(
cursorp: &mut CursorPair,
text: &mut String,
galley: &Galley,
key: Key,
modifiers: &Modifiers,
) -> Option<CCursorPair> {
match key {
Key::Backspace => {
let ccursor = if modifiers.mac_cmd {
delete_paragraph_before_cursor(text, galley, cursorp)
} else if let Some(cursor) = cursorp.single() {
if modifiers.alt || modifiers.ctrl {
delete_previous_word(text, cursor.ccursor)
} else {
delete_previous_char(text, cursor.ccursor)
}
} else {
delete_selected(text, cursorp)
};
Some(CCursorPair::one(ccursor))
}
Key::Delete => {
let ccursor = if modifiers.mac_cmd {
delete_paragraph_after_cursor(text, galley, cursorp)
} else if let Some(cursor) = cursorp.single() {
if modifiers.alt || modifiers.ctrl {
delete_next_word(text, cursor.ccursor)
} else {
delete_next_char(text, cursor.ccursor)
}
} else {
delete_selected(text, cursorp)
};
let ccursor = CCursor {
prefer_next_row: true,
..ccursor
};
Some(CCursorPair::one(ccursor))
}
Key::A if modifiers.command => {
*cursorp = CursorPair::two(Cursor::default(), galley.end());
None
}
Key::K if modifiers.ctrl => {
let ccursor = delete_paragraph_after_cursor(text, galley, cursorp);
Some(CCursorPair::one(ccursor))
}
Key::U if modifiers.ctrl => {
let ccursor = delete_paragraph_before_cursor(text, galley, cursorp);
Some(CCursorPair::one(ccursor))
}
Key::W if modifiers.ctrl => {
let ccursor = if let Some(cursor) = cursorp.single() {
delete_previous_word(text, cursor.ccursor)
} else {
delete_selected(text, cursorp)
};
Some(CCursorPair::one(ccursor))
}
Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End => {
move_single_cursor(&mut cursorp.primary, galley, key, modifiers);
if !modifiers.shift {
cursorp.secondary = cursorp.primary;
}
None
}
_ => None,
}
}
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
match key {
Key::ArrowLeft => {
if modifiers.alt || modifiers.ctrl {
*cursor = galley.from_ccursor(ccursor_previous_word(&galley.text, cursor.ccursor));
} else if modifiers.mac_cmd {
*cursor = galley.cursor_begin_of_row(cursor);
} else {
*cursor = galley.cursor_left_one_character(cursor);
}
}
Key::ArrowRight => {
if modifiers.alt || modifiers.ctrl {
*cursor = galley.from_ccursor(ccursor_next_word(&galley.text, cursor.ccursor));
} else if modifiers.mac_cmd {
*cursor = galley.cursor_end_of_row(cursor);
} else {
*cursor = galley.cursor_right_one_character(cursor);
}
}
Key::ArrowUp => {
if modifiers.command {
*cursor = Cursor::default();
} else {
*cursor = galley.cursor_up_one_row(cursor);
}
}
Key::ArrowDown => {
if modifiers.command {
*cursor = galley.end();
} else {
*cursor = galley.cursor_down_one_row(cursor);
}
}
Key::Home => {
if modifiers.ctrl {
*cursor = Cursor::default();
} else {
*cursor = galley.cursor_begin_of_row(cursor);
}
}
Key::End => {
if modifiers.ctrl {
*cursor = galley.end();
} else {
*cursor = galley.cursor_end_of_row(cursor);
}
}
_ => unreachable!(),
}
}
fn select_word_at(text: &str, ccursor: CCursor) -> CCursorPair {
if ccursor.index == 0 {
CCursorPair::two(ccursor, ccursor_next_word(text, ccursor))
} else {
let it = text.chars();
let mut it = it.skip(ccursor.index - 1);
if let Some(char_before_cursor) = it.next() {
if let Some(char_after_cursor) = it.next() {
if is_word_char(char_before_cursor) && is_word_char(char_after_cursor) {
let min = ccursor_previous_word(text, ccursor + 1);
let max = ccursor_next_word(text, min);
CCursorPair::two(min, max)
} else if is_word_char(char_before_cursor) {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, min);
CCursorPair::two(min, max)
} else if is_word_char(char_after_cursor) {
let max = ccursor_next_word(text, ccursor);
CCursorPair::two(ccursor, max)
} else {
let min = ccursor_previous_word(text, ccursor);
let max = ccursor_next_word(text, ccursor);
CCursorPair::two(min, max)
}
} else {
let min = ccursor_previous_word(text, ccursor);
CCursorPair::two(min, ccursor)
}
} else {
let max = ccursor_next_word(text, ccursor);
CCursorPair::two(ccursor, max)
}
}
}
fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
CCursor {
index: next_word_boundary_char_index(text.chars(), ccursor.index),
prefer_next_row: false,
}
}
fn ccursor_previous_word(text: &str, ccursor: CCursor) -> CCursor {
let num_chars = text.chars().count();
CCursor {
index: num_chars
- next_word_boundary_char_index(text.chars().rev(), num_chars - ccursor.index),
prefer_next_row: true,
}
}
fn next_word_boundary_char_index(it: impl Iterator<Item = char>, mut index: usize) -> usize {
let mut it = it.skip(index);
if let Some(_first) = it.next() {
index += 1;
if let Some(second) = it.next() {
index += 1;
for next in it {
if is_word_char(next) != is_word_char(second) {
break;
}
index += 1;
}
}
}
index
}
fn is_word_char(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}