Implement text selection
This commit is contained in:
parent
96befb0736
commit
13666755e1
2 changed files with 475 additions and 177 deletions
|
@ -115,7 +115,9 @@ impl PartialEq for PCursor {
|
||||||
|
|
||||||
/// All different types of cursors together.
|
/// All different types of cursors together.
|
||||||
/// They all point to the same place, but in their own different ways.
|
/// They all point to the same place, but in their own different ways.
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
/// pcursor/rcursor can also point to after the end of the paragraph/row.
|
||||||
|
/// Does not implement `PartialEq` because you must think which cursor should be equivalent.
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
pub struct Cursor {
|
pub struct Cursor {
|
||||||
pub ccursor: CCursor,
|
pub ccursor: CCursor,
|
||||||
|
@ -201,6 +203,10 @@ impl Row {
|
||||||
}
|
}
|
||||||
self.char_count_excluding_newline()
|
self.char_count_excluding_newline()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn x_offset(&self, column: usize) -> f32 {
|
||||||
|
self.x_offsets[column.min(self.x_offsets.len() - 1)]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Galley {
|
impl Galley {
|
||||||
|
@ -224,7 +230,7 @@ impl Galley {
|
||||||
|
|
||||||
/// ## Physical positions
|
/// ## Physical positions
|
||||||
impl Galley {
|
impl Galley {
|
||||||
fn last_pos(&self) -> Rect {
|
fn end_pos(&self) -> Rect {
|
||||||
if let Some(row) = self.rows.last() {
|
if let Some(row) = self.rows.last() {
|
||||||
let x = row.max_x();
|
let x = row.max_x();
|
||||||
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
||||||
|
@ -247,13 +253,12 @@ impl Galley {
|
||||||
|| row.ends_with_newline)
|
|| row.ends_with_newline)
|
||||||
{
|
{
|
||||||
let column = pcursor.offset - it.offset;
|
let column = pcursor.offset - it.offset;
|
||||||
let column = column.at_most(row.char_count_excluding_newline());
|
|
||||||
|
|
||||||
let select_next_row_instead = pcursor.prefer_next_row
|
let select_next_row_instead = pcursor.prefer_next_row
|
||||||
&& !row.ends_with_newline
|
&& !row.ends_with_newline
|
||||||
&& column >= row.char_count_excluding_newline();
|
&& column >= row.char_count_excluding_newline();
|
||||||
if !select_next_row_instead {
|
if !select_next_row_instead {
|
||||||
let x = row.x_offsets[column];
|
let x = row.x_offset(column);
|
||||||
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
return Rect::from_min_max(pos2(x, row.y_min), pos2(x, row.y_max));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -267,7 +272,7 @@ impl Galley {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.last_pos()
|
self.end_pos()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a 0-width Rect.
|
/// Returns a 0-width Rect.
|
||||||
|
@ -625,39 +630,6 @@ impl Galley {
|
||||||
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
|
column: self.rows[cursor.rcursor.row].char_count_excluding_newline(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor_next_word(&self, cursor: &Cursor) -> Cursor {
|
|
||||||
self.from_ccursor(CCursor {
|
|
||||||
index: next_word(self.text.chars(), cursor.ccursor.index),
|
|
||||||
prefer_next_row: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cursor_previous_word(&self, cursor: &Cursor) -> Cursor {
|
|
||||||
let num_chars = self.text.chars().count();
|
|
||||||
self.from_ccursor(CCursor {
|
|
||||||
index: num_chars - next_word(self.text.chars().rev(), num_chars - cursor.ccursor.index),
|
|
||||||
prefer_next_row: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_word(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 next.is_alphabetic() != second.is_alphabetic() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
index
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
|
|
|
@ -4,23 +4,87 @@ use crate::{paint::*, *};
|
||||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
#[cfg_attr(feature = "serde", serde(default))]
|
#[cfg_attr(feature = "serde", serde(default))]
|
||||||
pub(crate) struct State {
|
pub(crate) struct State {
|
||||||
/// We store as PCursor (paragraph number, and character offset within that paragraph).
|
cursorp: Option<CursorPair>,
|
||||||
/// This is so what if we resize the `TextEdit` region, and text wrapping changes,
|
|
||||||
/// we keep the same byte character offset from the beginning of the text,
|
|
||||||
/// even though the number of rows changes
|
|
||||||
/// (each paragraph can be several rows, due to word wrapping).
|
|
||||||
/// The column (character offset) should be able to extend beyond the last word so that we can
|
|
||||||
/// go down and still end up on the same column when we return.
|
|
||||||
pcursor: Option<PCursor>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// struct PCursorPair {
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
// /// Where the selection started (e.g. a drag started).
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
// begin: PCursor,
|
struct CursorPair {
|
||||||
// /// The end of the selection. When moving with e.g. shift+arrows, this is what moves.
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
// /// Note that this may be BEFORE the `begin`.
|
/// When moving with e.g. shift+arrows, this is what moves.
|
||||||
// end: PCursor,
|
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||||
// }
|
pub primary: Cursor,
|
||||||
|
|
||||||
|
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||||
|
/// This part of the cursor does not move when shift is down.
|
||||||
|
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 is_empty(&self) -> bool {
|
||||||
|
self.primary.ccursor == self.secondary.ccursor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If there is a selection, None is returned.
|
||||||
|
/// If the two ends is the same, that is returned.
|
||||||
|
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)]
|
||||||
|
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||||
|
struct CCursorPair {
|
||||||
|
/// When selecting with a mouse, this is where the mouse was released.
|
||||||
|
/// When moving with e.g. shift+arrows, this is what moves.
|
||||||
|
/// Note that the two ends can come in any order, and also be equal (no selection).
|
||||||
|
pub primary: CCursor,
|
||||||
|
|
||||||
|
/// When selecting with a mouse, this is where the mouse was first pressed.
|
||||||
|
/// This part of the cursor does not move when shift is down.
|
||||||
|
pub secondary: CCursor,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CCursorPair {
|
||||||
|
fn one(ccursor: CCursor) -> Self {
|
||||||
|
Self {
|
||||||
|
primary: ccursor,
|
||||||
|
secondary: ccursor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A text region that the user can edit the contents of.
|
/// A text region that the user can edit the contents of.
|
||||||
///
|
///
|
||||||
|
@ -178,19 +242,45 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
};
|
};
|
||||||
let response = ui.interact(rect, id, sense);
|
let response = ui.interact(rect, id, sense);
|
||||||
|
|
||||||
if response.clicked && enabled {
|
if enabled && response.hovered {
|
||||||
ui.memory().request_kb_focus(id);
|
|
||||||
if let Some(mouse_pos) = ui.input().mouse.pos {
|
if let Some(mouse_pos) = ui.input().mouse.pos {
|
||||||
state.pcursor = Some(
|
// TODO: triple-click to select whole paragraph
|
||||||
galley
|
// TODO: drag selected text to either move or clone (ctrl on windows, alt on mac)
|
||||||
.cursor_from_pos(mouse_pos - response.rect.min)
|
|
||||||
.pcursor,
|
let cursor_at_mouse = galley.cursor_from_pos(mouse_pos - response.rect.min);
|
||||||
);
|
|
||||||
|
{
|
||||||
|
// preview:
|
||||||
|
let end_color = Rgba::new(0.1, 0.6, 1.0, 1.0).multiply(0.5).into(); // TODO: from style
|
||||||
|
paint_cursor_end(ui, response.rect.min, &galley, &cursor_at_mouse, end_color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.double_clicked {
|
||||||
|
// Select word:
|
||||||
|
let center = cursor_at_mouse;
|
||||||
|
let primary =
|
||||||
|
galley.from_ccursor(ccursor_next_word(&galley.text, center.ccursor));
|
||||||
|
state.cursorp = Some(CursorPair {
|
||||||
|
secondary: galley
|
||||||
|
.from_ccursor(ccursor_previous_word(&galley.text, primary.ccursor)),
|
||||||
|
primary,
|
||||||
|
});
|
||||||
|
} else if ui.input().mouse.pressed {
|
||||||
|
ui.memory().request_kb_focus(id);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if ui.input().mouse.click || (ui.input().mouse.pressed && !response.hovered) {
|
}
|
||||||
|
|
||||||
|
if ui.input().mouse.pressed && !response.hovered {
|
||||||
// User clicked somewhere else
|
// User clicked somewhere else
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
}
|
}
|
||||||
|
@ -200,27 +290,51 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_kb_focus(id) && enabled {
|
if ui.memory().has_kb_focus(id) && enabled {
|
||||||
let mut cursor = state
|
let mut cursorp = state
|
||||||
.pcursor
|
.cursorp
|
||||||
.map(|pcursor| galley.from_pcursor(pcursor))
|
.map(|cursorp| {
|
||||||
.unwrap_or_else(|| galley.end());
|
// We only keep the PCursor (paragraph number, and character offset within that paragraph).
|
||||||
|
// This is so what if we resize the `TextEdit` region, and text wrapping changes,
|
||||||
|
// we keep the same byte character offset from the beginning of the text,
|
||||||
|
// even though the number of rows changes
|
||||||
|
// (each paragraph can be several rows, due to word wrapping).
|
||||||
|
// The column (character offset) should be able to extend beyond the last word so that we can
|
||||||
|
// go down and still end up on the same column when we return.
|
||||||
|
CursorPair {
|
||||||
|
primary: galley.from_pcursor(cursorp.primary.pcursor),
|
||||||
|
secondary: galley.from_pcursor(cursorp.secondary.pcursor),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| CursorPair::one(galley.end()));
|
||||||
|
|
||||||
for event in &ui.input().events {
|
for event in &ui.input().events {
|
||||||
let did_mutate_text = match event {
|
let did_mutate_text = match event {
|
||||||
Event::Copy | Event::Cut => {
|
Event::Copy => {
|
||||||
// TODO: cut
|
if cursorp.is_empty() {
|
||||||
ui.ctx().output().copied_text = text.clone();
|
ui.ctx().output().copied_text = text.clone();
|
||||||
|
} else {
|
||||||
|
ui.ctx().output().copied_text = selected_str(text, &cursorp).to_owned();
|
||||||
|
}
|
||||||
None
|
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) => {
|
Event::Text(text_to_insert) => {
|
||||||
// Newlines are handled by `Key::Enter`.
|
// Newlines are handled by `Key::Enter`.
|
||||||
if !text_to_insert.is_empty()
|
if !text_to_insert.is_empty()
|
||||||
&& text_to_insert != "\n"
|
&& text_to_insert != "\n"
|
||||||
&& text_to_insert != "\r"
|
&& text_to_insert != "\r"
|
||||||
{
|
{
|
||||||
let mut ccursor = cursor.ccursor;
|
let mut ccursor = delete_selected(text, &cursorp).into();
|
||||||
insert_text(&mut ccursor, text, text_to_insert);
|
insert_text(&mut ccursor, text, text_to_insert);
|
||||||
Some(ccursor)
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -231,9 +345,9 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if multiline {
|
if multiline {
|
||||||
let mut ccursor = cursor.ccursor;
|
let mut ccursor = delete_selected(text, &cursorp).into();
|
||||||
insert_text(&mut ccursor, text, "\n");
|
insert_text(&mut ccursor, text, "\n");
|
||||||
Some(ccursor)
|
Some(CCursorPair::one(ccursor))
|
||||||
} else {
|
} else {
|
||||||
// Common to end input with enter
|
// Common to end input with enter
|
||||||
ui.memory().surrender_kb_focus(id);
|
ui.memory().surrender_kb_focus(id);
|
||||||
|
@ -252,11 +366,11 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
key,
|
key,
|
||||||
pressed: true,
|
pressed: true,
|
||||||
modifiers,
|
modifiers,
|
||||||
} => on_key_press(&mut cursor, text, &galley, *key, modifiers),
|
} => on_key_press(&mut cursorp, text, &galley, *key, modifiers),
|
||||||
Event::Key { .. } => None,
|
Event::Key { .. } => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(new_ccursor) = did_mutate_text {
|
if let Some(new_ccursorp) = did_mutate_text {
|
||||||
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
// Layout again to avoid frame delay, and to keep `text` and `galley` in sync.
|
||||||
let font = &ui.fonts()[text_style];
|
let font = &ui.fonts()[text_style];
|
||||||
galley = if multiline {
|
galley = if multiline {
|
||||||
|
@ -265,19 +379,20 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
font.layout_single_line(text.clone())
|
font.layout_single_line(text.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set cursor using new galley:
|
// Set cursorp using new galley:
|
||||||
cursor = galley.from_ccursor(new_ccursor);
|
cursorp = CursorPair {
|
||||||
|
primary: galley.from_ccursor(new_ccursorp.primary),
|
||||||
|
secondary: galley.from_ccursor(new_ccursorp.secondary),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.pcursor = Some(cursor.pcursor);
|
state.cursorp = Some(cursorp);
|
||||||
}
|
}
|
||||||
|
|
||||||
let painter = ui.painter();
|
|
||||||
let visuals = ui.style().interact(&response);
|
|
||||||
|
|
||||||
{
|
{
|
||||||
|
let visuals = ui.style().interact(&response);
|
||||||
let bg_rect = response.rect.expand(2.0); // breathing room for content
|
let bg_rect = response.rect.expand(2.0); // breathing room for content
|
||||||
painter.add(PaintCmd::Rect {
|
ui.painter().add(PaintCmd::Rect {
|
||||||
rect: bg_rect,
|
rect: bg_rect,
|
||||||
corner_radius: visuals.corner_radius,
|
corner_radius: visuals.corner_radius,
|
||||||
fill: ui.style().visuals.dark_bg_color,
|
fill: ui.style().visuals.dark_bg_color,
|
||||||
|
@ -287,21 +402,21 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ui.memory().has_kb_focus(id) {
|
if ui.memory().has_kb_focus(id) {
|
||||||
if let Some(pcursor) = state.pcursor {
|
if let Some(cursorp) = state.cursorp {
|
||||||
let cursor_pos = galley
|
// TODO: color from Style
|
||||||
.pos_from_pcursor(pcursor)
|
let selection_color = Rgba::new(0.0, 0.5, 1.0, 0.0).multiply(0.15).into(); // additive!
|
||||||
.translate(response.rect.min.to_vec2());
|
let end_color = Rgba::new(0.3, 0.6, 1.0, 1.0).into();
|
||||||
painter.line_segment(
|
paint_cursor_selection(ui, response.rect.min, &galley, &cursorp, selection_color);
|
||||||
[cursor_pos.center_top(), cursor_pos.center_bottom()],
|
paint_cursor_end(ui, response.rect.min, &galley, &cursorp.primary, end_color);
|
||||||
(ui.style().visuals.text_cursor_width, color::WHITE),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let text_color = text_color
|
let text_color = text_color
|
||||||
.or(ui.style().visuals.override_text_color)
|
.or(ui.style().visuals.override_text_color)
|
||||||
.unwrap_or_else(|| visuals.text_color());
|
// .unwrap_or_else(|| ui.style().interact(&response).text_color()); // too bright
|
||||||
painter.galley(response.rect.min, galley, text_style, 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);
|
ui.memory().text_edit.insert(id, state);
|
||||||
|
|
||||||
Response {
|
Response {
|
||||||
|
@ -311,9 +426,85 @@ impl<'t> Widget for TextEdit<'t> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn paint_cursor_selection(
|
||||||
|
ui: &mut Ui,
|
||||||
|
pos: Pos2,
|
||||||
|
galley: &Galley,
|
||||||
|
cursorp: &CursorPair,
|
||||||
|
color: Srgba,
|
||||||
|
) {
|
||||||
|
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 {
|
||||||
|
row.max_x()
|
||||||
|
};
|
||||||
|
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, color: Srgba) {
|
||||||
|
let cursor_pos = galley.pos_from_cursor(cursor).translate(pos.to_vec2());
|
||||||
|
let cursor_pos = cursor_pos.expand(1.5); // slightly above/below row
|
||||||
|
|
||||||
|
let top = cursor_pos.center_top();
|
||||||
|
let bottom = cursor_pos.center_bottom();
|
||||||
|
|
||||||
|
ui.painter()
|
||||||
|
.line_segment([top, bottom], (ui.style().visuals.text_cursor_width, color));
|
||||||
|
|
||||||
|
if false {
|
||||||
|
// Roof/floor:
|
||||||
|
let extrusion = 3.0;
|
||||||
|
let width = 1.0;
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[top - vec2(extrusion, 0.0), top + vec2(extrusion, 0.0)],
|
||||||
|
(width, color),
|
||||||
|
);
|
||||||
|
ui.painter().line_segment(
|
||||||
|
[bottom - vec2(extrusion, 0.0), bottom + vec2(extrusion, 0.0)],
|
||||||
|
(width, 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.len();
|
||||||
|
}
|
||||||
|
|
||||||
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
|
fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
|
||||||
let mut char_it = text.chars();
|
let mut char_it = text.chars();
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
let mut new_text = String::with_capacity(text.len() + text_to_insert.len());
|
||||||
for _ in 0..ccursor.index {
|
for _ in 0..ccursor.index {
|
||||||
let c = char_it.next().unwrap();
|
let c = char_it.next().unwrap();
|
||||||
new_text.push(c);
|
new_text.push(c);
|
||||||
|
@ -324,109 +515,145 @@ fn insert_text(ccursor: &mut CCursor, text: &mut String, text_to_insert: &str) {
|
||||||
*text = new_text;
|
*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, // end of paragraph
|
||||||
|
prefer_next_row: false,
|
||||||
|
});
|
||||||
|
if min.ccursor == max.ccursor {
|
||||||
|
delete_next_char(text, min.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_selected(text, &CursorPair::two(min, max))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
/// Returns `Some(new_cursor)` if we did mutate `text`.
|
||||||
fn on_key_press(
|
fn on_key_press(
|
||||||
cursor: &mut Cursor,
|
cursorp: &mut CursorPair,
|
||||||
text: &mut String,
|
text: &mut String,
|
||||||
galley: &Galley,
|
galley: &Galley,
|
||||||
key: Key,
|
key: Key,
|
||||||
modifiers: &Modifiers,
|
modifiers: &Modifiers,
|
||||||
) -> Option<CCursor> {
|
) -> Option<CCursorPair> {
|
||||||
// TODO: cursor position preview on mouse hover
|
|
||||||
// TODO: drag-select
|
|
||||||
// TODO: double-click to select whole word
|
|
||||||
// TODO: triple-click to select whole paragraph
|
|
||||||
// TODO: drag selected text to either move or clone (ctrl on windows, alt on mac)
|
|
||||||
// TODO: ctrl-U to clear paragraph before the cursor
|
// TODO: ctrl-U to clear paragraph before the cursor
|
||||||
// TODO: ctrl-W to delete previous word
|
// TODO: ctrl-W to delete previous word
|
||||||
// TODO: alt/ctrl + backspace to delete previous word (alt on mac, ctrl on windows)
|
|
||||||
// TODO: alt/ctrl + delete to delete next word (alt on mac, ctrl on windows)
|
|
||||||
// TODO: cmd-A to select all
|
// TODO: cmd-A to select all
|
||||||
// TODO: shift modifier to only move half of the cursor to select things
|
|
||||||
|
|
||||||
match key {
|
match key {
|
||||||
Key::Backspace => {
|
Key::Backspace => {
|
||||||
if cursor.ccursor.index > 0 {
|
let ccursor = if modifiers.mac_cmd {
|
||||||
*cursor = galley.from_ccursor(cursor.ccursor - 1);
|
delete_paragraph_before_cursor(text, galley, cursorp)
|
||||||
let mut char_it = text.chars();
|
} else if let Some(cursor) = cursorp.single() {
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
if modifiers.alt || modifiers.ctrl {
|
||||||
for _ in 0..cursor.ccursor.index {
|
// alt on mac, ctrl on windows
|
||||||
new_text.push(char_it.next().unwrap())
|
delete_previous_word(text, cursor.ccursor)
|
||||||
|
} else {
|
||||||
|
delete_previous_char(text, cursor.ccursor)
|
||||||
}
|
}
|
||||||
new_text.extend(char_it.skip(1));
|
|
||||||
*text = new_text;
|
|
||||||
Some(cursor.ccursor)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
delete_selected(text, cursorp)
|
||||||
}
|
};
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
}
|
}
|
||||||
Key::Delete => {
|
Key::Delete => {
|
||||||
let mut char_it = text.chars();
|
let ccursor = if modifiers.mac_cmd {
|
||||||
let mut new_text = String::with_capacity(text.capacity());
|
delete_paragraph_after_cursor(text, galley, cursorp)
|
||||||
for _ in 0..cursor.ccursor.index {
|
} else if let Some(cursor) = cursorp.single() {
|
||||||
new_text.push(char_it.next().unwrap())
|
if modifiers.alt || modifiers.ctrl {
|
||||||
}
|
// alt on mac, ctrl on windows
|
||||||
new_text.extend(char_it.skip(1));
|
delete_next_word(text, cursor.ccursor)
|
||||||
*text = new_text;
|
} else {
|
||||||
Some(cursor.ccursor)
|
delete_next_char(text, cursor.ccursor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete_selected(text, cursorp)
|
||||||
|
};
|
||||||
|
let ccursor = CCursor {
|
||||||
|
prefer_next_row: true,
|
||||||
|
..ccursor
|
||||||
|
};
|
||||||
|
Some(CCursorPair::one(ccursor))
|
||||||
}
|
}
|
||||||
|
|
||||||
Key::ArrowLeft => {
|
Key::ArrowLeft | Key::ArrowRight | Key::ArrowUp | Key::ArrowDown | Key::Home | Key::End => {
|
||||||
if modifiers.alt || modifiers.ctrl {
|
move_single_cursor(&mut cursorp.primary, galley, key, modifiers);
|
||||||
// alt on mac, ctrl on windows
|
if !modifiers.shift {
|
||||||
*cursor = galley.cursor_previous_word(cursor);
|
cursorp.secondary = cursorp.primary;
|
||||||
} else if modifiers.mac_cmd {
|
|
||||||
*cursor = galley.cursor_begin_of_row(cursor);
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_left_one_character(cursor);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Key::ArrowRight => {
|
|
||||||
if modifiers.alt || modifiers.ctrl {
|
|
||||||
// alt on mac, ctrl on windows
|
|
||||||
*cursor = galley.cursor_next_word(cursor);
|
|
||||||
} else if modifiers.mac_cmd {
|
|
||||||
*cursor = galley.cursor_end_of_row(cursor);
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_right_one_character(cursor);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Key::ArrowUp => {
|
|
||||||
if modifiers.command {
|
|
||||||
// mac and windows behavior
|
|
||||||
*cursor = Cursor::default();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_up_one_row(cursor);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Key::ArrowDown => {
|
|
||||||
if modifiers.command {
|
|
||||||
// mac and windows behavior
|
|
||||||
*cursor = galley.end();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_down_one_row(cursor);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
Key::Home => {
|
|
||||||
if modifiers.ctrl {
|
|
||||||
// windows behavior
|
|
||||||
*cursor = Cursor::default();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_begin_of_row(cursor);
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Key::End => {
|
|
||||||
if modifiers.ctrl {
|
|
||||||
// windows behavior
|
|
||||||
*cursor = galley.end();
|
|
||||||
} else {
|
|
||||||
*cursor = galley.cursor_end_of_row(cursor);
|
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -436,3 +663,102 @@ fn on_key_press(
|
||||||
Key::Insert | Key::PageDown | Key::PageUp | Key::Tab => None,
|
Key::Insert | Key::PageDown | Key::PageUp | Key::Tab => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn move_single_cursor(cursor: &mut Cursor, galley: &Galley, key: Key, modifiers: &Modifiers) {
|
||||||
|
match key {
|
||||||
|
Key::ArrowLeft => {
|
||||||
|
if modifiers.alt || modifiers.ctrl {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*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 {
|
||||||
|
// alt on mac, ctrl on windows
|
||||||
|
*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 {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_up_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::ArrowDown => {
|
||||||
|
if modifiers.command {
|
||||||
|
// mac and windows behavior
|
||||||
|
*cursor = galley.end();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_down_one_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Key::Home => {
|
||||||
|
if modifiers.ctrl {
|
||||||
|
// windows behavior
|
||||||
|
*cursor = Cursor::default();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_begin_of_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Key::End => {
|
||||||
|
if modifiers.ctrl {
|
||||||
|
// windows behavior
|
||||||
|
*cursor = galley.end();
|
||||||
|
} else {
|
||||||
|
*cursor = galley.cursor_end_of_row(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn ccursor_next_word(text: &str, ccursor: CCursor) -> CCursor {
|
||||||
|
CCursor {
|
||||||
|
index: next_word_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_char_index(text.chars().rev(), num_chars - ccursor.index),
|
||||||
|
prefer_next_row: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_word_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 == '_'
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue