TEXT SELECTION

This commit is contained in:
2025-11-21 23:56:31 -05:00
parent 246caffb34
commit 1cec56e847
10 changed files with 332 additions and 69 deletions

View File

@@ -10,7 +10,7 @@ use winit::{
pub struct TextEdit {
pub(super) view: TextView,
pub(super) cursor: Option<Cursor>,
pub(super) selection: TextSelection,
}
impl TextEdit {
@@ -21,28 +21,86 @@ impl TextEdit {
.align(self.align)
}
pub fn content(&self) -> String {
self.buf
.lines
.iter()
.map(|l| l.text())
.collect::<Vec<_>>()
.join("\n")
pub fn select_content(&self, start: Cursor, end: Cursor) -> String {
let (start, end) = sort_cursors(start, end);
let mut iter = self.buf.lines.iter().skip(start.line);
let first = iter.next().unwrap();
if start.line == end.line {
first.text()[start.index..end.index].to_string()
} else {
let mut str = first.text()[start.index..].to_string();
for _ in (start.line + 1)..end.line {
str = str + "\n" + iter.next().unwrap().text();
}
let last = iter.next().unwrap();
str = str + "\n" + &last.text()[..end.index];
str
}
}
}
impl Widget for TextEdit {
fn draw(&mut self, painter: &mut Painter) {
let base = painter.layer;
painter.child_layer();
let region = self.view.draw(painter);
painter.layer = base;
if let Some(cursor) = &self.cursor
&& let Some(offset) = cursor_pos(cursor, &self.buf)
{
let size = vec2(1, self.attrs.line_height);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(offset).within(&region),
);
match &self.selection {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
if let Some(offset) = cursor_pos(cursor, &self.buf) {
let size = vec2(1, self.attrs.line_height);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT).offset(offset).within(&region),
);
}
}
TextSelection::Span { start, end } => {
let (start, end) = sort_cursors(*start, *end);
let line_height = self.attrs.line_height;
let mut highlight = |offset, width: f32| {
painter.primitive_within(
RectPrimitive::color(Color::SKY),
vec2(width, line_height)
.align(Align::TOP_LEFT)
.offset(offset)
.within(&region),
);
};
if let (Some(start_offset), Some(end_offset)) =
(cursor_pos(&start, &self.buf), cursor_pos(&end, &self.buf))
{
let mut iter = self.buf.layout_runs().skip(start.line);
if start.line == end.line {
highlight(start_offset, end_offset.x - start_offset.x);
} else {
let first = iter.next().unwrap();
highlight(start_offset, first.line_w - start_offset.x);
for i in (start.line + 1)..end.line {
let line = iter.next().unwrap();
let offset = vec2(0.0, i as f32 * line_height);
highlight(offset, line.line_w);
}
let offset = vec2(0.0, end.line as f32 * line_height);
highlight(offset, end_offset.x);
}
// painter.primitive_within(
// RectPrimitive::color(Color::WHITE),
// size.align(Align::TOP_LEFT)
// .offset(end_offset)
// .within(&region),
// );
let size = vec2(1, self.attrs.line_height);
painter.primitive_within(
RectPrimitive::color(Color::WHITE),
size.align(Align::TOP_LEFT)
.offset(end_offset)
.within(&region),
);
}
}
}
}
@@ -56,17 +114,17 @@ impl Widget for TextEdit {
}
/// copied & modified from fn found in Editor in cosmic_text
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<Vec2> {
let mut prev = None;
for run in buf
.layout_runs()
.skip_while(|r| r.line_i < cursor.line)
.take_while(|r| r.line_i == cursor.line)
{
prev = Some((run.line_w, run.line_top));
prev = Some(vec2(run.line_w, run.line_top));
for glyph in run.glyphs.iter() {
if cursor.index == glyph.start {
return Some((glyph.x, run.line_top));
return Some(vec2(glyph.x, run.line_top));
} else if cursor.index > glyph.start && cursor.index < glyph.end {
// Guess x offset based on characters
let mut before = 0;
@@ -81,7 +139,7 @@ fn cursor_pos(cursor: &Cursor, buf: &TextBuffer) -> Option<(f32, f32)> {
}
let offset = glyph.w * (before as f32) / (total as f32);
return Some((glyph.x + offset, run.line_top));
return Some(vec2(glyph.x + offset, run.line_top));
}
}
}
@@ -106,22 +164,18 @@ impl<'a> TextEditCtx<'a> {
self.text
.buf
.set_text(self.font_system, "", &Attrs::new(), SHAPING, None);
if let Some(cursor) = &mut self.text.cursor {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
self.text.selection.clear();
text
}
pub fn motion(&mut self, motion: Motion) {
if let Some(cursor) = self.text.cursor
if let TextSelection::Pos(cursor) = self.text.selection
&& let Some((cursor, _)) =
self.text
.buf
.cursor_motion(self.font_system, cursor, None, motion)
{
self.text.cursor = Some(cursor);
self.text.selection = TextSelection::Pos(cursor);
}
}
@@ -144,12 +198,41 @@ impl<'a> TextEditCtx<'a> {
}
}
pub fn clear_span(&mut self) -> bool {
if let TextSelection::Span { start, end } = self.text.selection {
let lines = &mut self.text.view.buf.lines;
let (start, end) = sort_cursors(start, end);
if start.line == end.line {
let line = &mut lines[start.line];
let text = line.text();
let text = text[..start.index].to_string() + &text[end.index..];
edit_line(line, text);
} else {
// start
let start_text = lines[start.line].text()[..start.index].to_string();
let end_text = &lines[end.line].text()[end.index..];
let text = start_text + end_text;
edit_line(&mut lines[start.line], text);
}
// between
let range = (start.line + 1)..=end.line;
if !range.is_empty() {
lines.splice(range, None);
}
self.text.selection = TextSelection::Pos(start);
true
} else {
false
}
}
fn insert_inner(&mut self, text: &str, mov: bool) {
if let Some(cursor) = &mut self.text.cursor {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let line = &mut self.text.view.buf.lines[cursor.line];
let mut line_text = line.text().to_string();
line_text.insert_str(cursor.index, text);
line.set_text(line_text, line.ending(), line.attrs_list().clone());
edit_line(line, line_text);
if mov {
for _ in 0..text.chars().count() {
self.motion(Motion::Right);
@@ -159,7 +242,8 @@ impl<'a> TextEditCtx<'a> {
}
pub fn newline(&mut self) {
if let Some(cursor) = &mut self.text.cursor {
self.clear_span();
if let TextSelection::Pos(cursor) = &mut self.text.selection {
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
let new = line.split_off(cursor.index);
@@ -170,7 +254,8 @@ impl<'a> TextEditCtx<'a> {
}
pub fn backspace(&mut self) {
if let Some(cursor) = &mut self.text.cursor
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
&& (cursor.index != 0 || cursor.line != 0)
{
self.motion(Motion::Left);
@@ -179,7 +264,9 @@ impl<'a> TextEditCtx<'a> {
}
pub fn delete(&mut self) {
if let Some(cursor) = &mut self.text.cursor {
if !self.clear_span()
&& let TextSelection::Pos(cursor) = &mut self.text.selection
{
let lines = &mut self.text.view.buf.lines;
let line = &mut lines[cursor.line];
if cursor.index == line.text().len() {
@@ -190,22 +277,47 @@ impl<'a> TextEditCtx<'a> {
let line = &mut lines[cursor.line];
let mut cur = line.text().to_string();
cur.push_str(&add);
line.set_text(cur, line.ending(), line.attrs_list().clone());
edit_line(line, cur);
} else {
let mut text = line.text().to_string();
text.remove(cursor.index);
line.set_text(text, line.ending(), line.attrs_list().clone());
edit_line(line, text);
}
}
}
pub fn select(&mut self, pos: Vec2, size: Vec2) {
pub fn select(&mut self, pos: Vec2, size: Vec2, drag: bool) {
let pos = pos - self.text.region().top_left().to_abs(size);
self.text.cursor = self.text.buf.hit(pos.x, pos.y);
let hit = self.text.buf.hit(pos.x, pos.y);
let sel = &mut self.text.selection;
match sel {
TextSelection::None => {
if !drag && let Some(hit) = hit {
*sel = TextSelection::Pos(hit)
}
}
TextSelection::Pos(pos) => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => (),
(Some(hit), false) => *pos = hit,
(Some(end), true) => *sel = TextSelection::Span { start: *pos, end },
},
TextSelection::Span { start, end } => match (hit, drag) {
(None, false) => *sel = TextSelection::None,
(None, true) => *sel = TextSelection::Pos(*start),
(Some(hit), false) => *sel = TextSelection::Pos(hit),
(Some(hit), true) => *end = hit,
},
}
if let TextSelection::Span { start, end } = sel
&& start == end
{
*sel = TextSelection::Pos(*start);
}
}
pub fn deselect(&mut self) {
self.text.cursor = None;
self.text.selection = TextSelection::None;
}
pub fn apply_event(&mut self, event: &KeyEvent, modifiers: &Modifiers) -> TextInputResult {
@@ -232,11 +344,20 @@ impl<'a> TextEditCtx<'a> {
_ => return TextInputResult::Unused,
},
Key::Character(text) => {
if modifiers.control && text == "v" {
return TextInputResult::Paste;
} else {
self.insert(text)
if modifiers.control {
match text.as_str() {
"v" => return TextInputResult::Paste,
"c" => {
if let TextSelection::Span { start, end } = self.text.selection {
let content = self.text.select_content(start, end);
return TextInputResult::Copy(content);
}
return TextInputResult::Used;
}
_ => (),
}
}
self.insert(text)
}
_ => return TextInputResult::Unused,
}
@@ -262,9 +383,37 @@ pub enum TextInputResult {
Unused,
Unfocus,
Submit,
Copy(String),
Paste,
}
#[derive(Default)]
pub enum TextSelection {
#[default]
None,
Pos(Cursor),
Span {
start: Cursor,
end: Cursor,
},
}
impl TextSelection {
pub fn clear(&mut self) {
match self {
TextSelection::None => (),
TextSelection::Pos(cursor) => {
cursor.line = 0;
cursor.index = 0;
cursor.affinity = Affinity::default();
}
TextSelection::Span { start: _, end: _ } => {
*self = TextSelection::None;
}
}
}
}
impl TextInputResult {
pub fn unfocus(&self) -> bool {
matches!(self, TextInputResult::Unfocus)