// SPDX-License-Identifier: MIT OR Apache-2.0 #[cfg(not(feature = "std"))] use alloc::string::{String, ToString}; use core::{cmp, iter::once}; use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "swash")] use crate::Color; use crate::{ Action, AttrsList, BorrowedWithFontSystem, BufferLine, BufferRef, Change, ChangeItem, Cursor, Edit, FontSystem, Selection, Shaping, }; /// A wrapper of [`Buffer`] for easy editing #[derive(Debug)] pub struct Editor<'buffer> { buffer_ref: BufferRef<'buffer>, cursor: Cursor, cursor_x_opt: Option, selection: Selection, cursor_moved: bool, auto_indent: bool, tab_width: u16, change: Option, } impl<'buffer> Editor<'buffer> { /// Create a new [`Editor`] with the provided [`Buffer`] pub fn new(buffer: impl Into>) -> Self { Self { buffer_ref: buffer.into(), cursor: Cursor::default(), cursor_x_opt: None, selection: Selection::None, cursor_moved: false, auto_indent: false, tab_width: 4, change: None, } } /// Draw the editor #[cfg(feature = "swash")] pub fn draw( &self, font_system: &mut FontSystem, cache: &mut crate::SwashCache, text_color: Color, cursor_color: Color, selection_color: Color, mut f: F, ) where F: FnMut(i32, i32, u32, u32, Color), { self.with_buffer(|buffer| { let line_height = buffer.metrics().line_height; for run in buffer.layout_runs() { let line_i = run.line_i; let line_y = run.line_y; let line_top = run.line_top; let cursor_glyph_opt = |cursor: &Cursor| -> Option<(usize, f32)> { if cursor.line == line_i { for (glyph_i, glyph) in run.glyphs.iter().enumerate() { if cursor.index == glyph.start { return Some((glyph_i, 0.0)); } else if cursor.index > glyph.start && cursor.index < glyph.end { // Guess x offset based on characters let mut before = 0; let mut total = 0; let cluster = &run.text[glyph.start..glyph.end]; for (i, _) in cluster.grapheme_indices(true) { if glyph.start + i < cursor.index { before += 1; } total += 1; } let offset = glyph.w * (before as f32) / (total as f32); return Some((glyph_i, offset)); } } match run.glyphs.last() { Some(glyph) => { if cursor.index == glyph.end { return Some((run.glyphs.len(), 0.0)); } } None => { return Some((0, 0.0)); } } } None }; // Highlight selection (TODO: HIGHLIGHT COLOR!) if let Some((start, end)) = self.selection_bounds() { if line_i >= start.line && line_i <= end.line { let mut range_opt = None; for glyph in run.glyphs.iter() { // Guess x offset based on characters let cluster = &run.text[glyph.start..glyph.end]; let total = cluster.grapheme_indices(true).count(); let mut c_x = glyph.x; let c_w = glyph.w / total as f32; for (i, c) in cluster.grapheme_indices(true) { let c_start = glyph.start + i; let c_end = glyph.start + i + c.len(); if (start.line != line_i || c_end > start.index) && (end.line != line_i || c_start < end.index) { range_opt = match range_opt.take() { Some((min, max)) => Some(( cmp::min(min, c_x as i32), cmp::max(max, (c_x + c_w) as i32), )), None => Some((c_x as i32, (c_x + c_w) as i32)), }; } else if let Some((min, max)) = range_opt.take() { f( min, line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, selection_color, ); } c_x += c_w; } } if run.glyphs.is_empty() && end.line > line_i { // Highlight all of internal empty lines range_opt = Some((0, buffer.size().0 as i32)); } if let Some((mut min, mut max)) = range_opt.take() { if end.line > line_i { // Draw to end of line if run.rtl { min = 0; } else { max = buffer.size().0 as i32; } } f( min, line_top as i32, cmp::max(0, max - min) as u32, line_height as u32, selection_color, ); } } } // Draw cursor if let Some((cursor_glyph, cursor_glyph_offset)) = cursor_glyph_opt(&self.cursor) { let x = match run.glyphs.get(cursor_glyph) { Some(glyph) => { // Start of detected glyph if glyph.level.is_rtl() { (glyph.x + glyph.w - cursor_glyph_offset) as i32 } else { (glyph.x + cursor_glyph_offset) as i32 } } None => match run.glyphs.last() { Some(glyph) => { // End of last glyph if glyph.level.is_rtl() { glyph.x as i32 } else { (glyph.x + glyph.w) as i32 } } None => { // Start of empty line 0 } }, }; f(x, line_top as i32, 1, line_height as u32, cursor_color); } for glyph in run.glyphs.iter() { let physical_glyph = glyph.physical((0., 0.), 1.0); let glyph_color = match glyph.color_opt { Some(some) => some, None => text_color, }; cache.with_pixels( font_system, physical_glyph.cache_key, glyph_color, |x, y, color| { f( physical_glyph.x + x, line_y as i32 + physical_glyph.y + y, 1, 1, color, ); }, ); } } }); } } impl<'buffer> Edit<'buffer> for Editor<'buffer> { fn buffer_ref(&self) -> &BufferRef<'buffer> { &self.buffer_ref } fn buffer_ref_mut(&mut self) -> &mut BufferRef<'buffer> { &mut self.buffer_ref } fn cursor(&self) -> Cursor { self.cursor } fn set_cursor(&mut self, cursor: Cursor) { if self.cursor != cursor { self.cursor = cursor; self.cursor_moved = true; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } fn selection(&self) -> Selection { self.selection } fn set_selection(&mut self, selection: Selection) { if self.selection != selection { self.selection = selection; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } fn auto_indent(&self) -> bool { self.auto_indent } fn set_auto_indent(&mut self, auto_indent: bool) { self.auto_indent = auto_indent; } fn tab_width(&self) -> u16 { self.tab_width } fn set_tab_width(&mut self, tab_width: u16) { // A tab width of 0 is not allowed if tab_width == 0 { return; } if self.tab_width != tab_width { self.tab_width = tab_width; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } fn shape_as_needed(&mut self, font_system: &mut FontSystem, prune: bool) { if self.cursor_moved { let cursor = self.cursor; self.with_buffer_mut(|buffer| buffer.shape_until_cursor(font_system, cursor, prune)); self.cursor_moved = false; } else { self.with_buffer_mut(|buffer| buffer.shape_until_scroll(font_system, prune)); } } fn delete_range(&mut self, start: Cursor, end: Cursor) { let change_item = self.with_buffer_mut(|buffer| { // Collect removed data for change tracking let mut change_text = String::new(); // Delete the selection from the last line let end_line_opt = if end.line > start.line { // Get part of line after selection let after = buffer.lines[end.line].split_off(end.index); // Remove end line let removed = buffer.lines.remove(end.line); change_text.insert_str(0, removed.text()); Some(after) } else { None }; // Delete interior lines (in reverse for safety) for line_i in (start.line + 1..end.line).rev() { let removed = buffer.lines.remove(line_i); change_text.insert_str(0, removed.text()); } // Delete the selection from the first line { // Get part after selection if start line is also end line let after_opt = if start.line == end.line { Some(buffer.lines[start.line].split_off(end.index)) } else { None }; // Delete selected part of line let removed = buffer.lines[start.line].split_off(start.index); change_text.insert_str(0, removed.text()); // Re-add part of line after selection if let Some(after) = after_opt { buffer.lines[start.line].append(after); } // Re-add valid parts of end line if let Some(end_line) = end_line_opt { buffer.lines[start.line].append(end_line); } } ChangeItem { start, end, text: change_text, insert: false, } }); if let Some(ref mut change) = self.change { change.items.push(change_item); } } fn insert_at( &mut self, mut cursor: Cursor, data: &str, attrs_list: Option, ) -> Cursor { let mut remaining_split_len = data.len(); if remaining_split_len == 0 { return cursor; } let change_item = self.with_buffer_mut(|buffer| { // Save cursor for change tracking let start = cursor; let line: &mut BufferLine = &mut buffer.lines[cursor.line]; let insert_line = cursor.line + 1; // Collect text after insertion as a line let after: BufferLine = line.split_off(cursor.index); let after_len = after.text().len(); // Collect attributes let mut final_attrs = attrs_list.unwrap_or_else(|| { AttrsList::new(line.attrs_list().get_span(cursor.index.saturating_sub(1))) }); // Append the inserted text, line by line // we want to see a blank entry if the string ends with a newline let addendum = once("").filter(|_| data.ends_with('\n')); let mut lines_iter = data.split_inclusive('\n').chain(addendum); if let Some(data_line) = lines_iter.next() { let mut these_attrs = final_attrs.split_off(data_line.len()); remaining_split_len -= data_line.len(); core::mem::swap(&mut these_attrs, &mut final_attrs); line.append(BufferLine::new( data_line .strip_suffix(char::is_control) .unwrap_or(data_line), these_attrs, Shaping::Advanced, )); } else { panic!("str::lines() did not yield any elements"); } if let Some(data_line) = lines_iter.next_back() { remaining_split_len -= data_line.len(); let mut tmp = BufferLine::new( data_line .strip_suffix(char::is_control) .unwrap_or(data_line), final_attrs.split_off(remaining_split_len), Shaping::Advanced, ); tmp.append(after); buffer.lines.insert(insert_line, tmp); cursor.line += 1; } else { line.append(after); } for data_line in lines_iter.rev() { remaining_split_len -= data_line.len(); let tmp = BufferLine::new( data_line .strip_suffix(char::is_control) .unwrap_or(data_line), final_attrs.split_off(remaining_split_len), Shaping::Advanced, ); buffer.lines.insert(insert_line, tmp); cursor.line += 1; } assert_eq!(remaining_split_len, 0); // Append the text after insertion cursor.index = buffer.lines[cursor.line].text().len() - after_len; ChangeItem { start, end: cursor, text: data.to_string(), insert: true, } }); if let Some(ref mut change) = self.change { change.items.push(change_item); } cursor } fn copy_selection(&self) -> Option { let (start, end) = self.selection_bounds()?; self.with_buffer(|buffer| { let mut selection = String::new(); // Take the selection from the first line { // Add selected part of line to string if start.line == end.line { selection.push_str(&buffer.lines[start.line].text()[start.index..end.index]); } else { selection.push_str(&buffer.lines[start.line].text()[start.index..]); selection.push('\n'); } } // Take the selection from all interior lines (if they exist) for line_i in start.line + 1..end.line { selection.push_str(buffer.lines[line_i].text()); selection.push('\n'); } // Take the selection from the last line if end.line > start.line { // Add selected part of line to string selection.push_str(&buffer.lines[end.line].text()[..end.index]); } Some(selection) }) } fn delete_selection(&mut self) -> bool { let (start, end) = match self.selection_bounds() { Some(some) => some, None => return false, }; // Reset cursor to start of selection self.cursor = start; // Reset selection to None self.selection = Selection::None; // Delete from start to end of selection self.delete_range(start, end); true } fn apply_change(&mut self, change: &Change) -> bool { // Cannot apply changes if there is a pending change match self.change.take() { Some(pending) => { if !pending.items.is_empty() { //TODO: is this a good idea? log::warn!("pending change caused apply_change to be ignored!"); self.change = Some(pending); return false; } } None => {} } for item in change.items.iter() { //TODO: edit cursor if needed? if item.insert { self.cursor = self.insert_at(item.start, &item.text, None); } else { self.cursor = item.start; self.delete_range(item.start, item.end); } } true } fn start_change(&mut self) { if self.change.is_none() { self.change = Some(Change::default()); } } fn finish_change(&mut self) -> Option { self.change.take() } fn action(&mut self, font_system: &mut FontSystem, action: Action) { let old_cursor = self.cursor; match action { Action::Motion(motion) => { let cursor = self.cursor; let cursor_x_opt = self.cursor_x_opt; if let Some((new_cursor, new_cursor_x_opt)) = self.with_buffer_mut(|buffer| { buffer.cursor_motion(font_system, cursor, cursor_x_opt, motion) }) { self.cursor = new_cursor; self.cursor_x_opt = new_cursor_x_opt; } } Action::Escape => { match self.selection { Selection::None => {} _ => self.with_buffer_mut(|buffer| buffer.set_redraw(true)), } self.selection = Selection::None; } Action::Insert(character) => { if character.is_control() && !['\t', '\n', '\u{92}'].contains(&character) { // Filter out special chars (except for tab), use Action instead log::debug!("Refusing to insert control character {:?}", character); } else if character == '\n' { self.action(font_system, Action::Enter); } else { let mut str_buf = [0u8; 8]; let str_ref = character.encode_utf8(&mut str_buf); self.insert_string(str_ref, None); } } Action::Enter => { //TODO: what about indenting more after opening brackets or parentheses? if self.auto_indent { let mut string = String::from("\n"); self.with_buffer(|buffer| { let line = &buffer.lines[self.cursor.line]; let text = line.text(); for c in text.chars() { if c.is_whitespace() { string.push(c); } else { break; } } }); self.insert_string(&string, None); } else { self.insert_string("\n", None); } // Ensure line is properly shaped and laid out (for potential immediate commands) let line_i = self.cursor.line; self.with_buffer_mut(|buffer| { buffer.line_layout(font_system, line_i); }); } Action::Backspace => { if self.delete_selection() { // Deleted selection } else { // Save current cursor as end let end = self.cursor; if self.cursor.index > 0 { // Move cursor to previous character index self.cursor.index = self.with_buffer(|buffer| { buffer.lines[self.cursor.line].text()[..self.cursor.index] .char_indices() .next_back() .map_or(0, |(i, _)| i) }); } else if self.cursor.line > 0 { // Move cursor to previous line self.cursor.line -= 1; self.cursor.index = self.with_buffer(|buffer| buffer.lines[self.cursor.line].text().len()); } if self.cursor != end { // Delete range self.delete_range(self.cursor, end); } } } Action::Delete => { if self.delete_selection() { // Deleted selection } else { // Save current cursor as start and end let mut start = self.cursor; let mut end = self.cursor; self.with_buffer(|buffer| { if start.index < buffer.lines[start.line].text().len() { let line = &buffer.lines[start.line]; let range_opt = line .text() .grapheme_indices(true) .take_while(|(i, _)| *i <= start.index) .last() .map(|(i, c)| i..(i + c.len())); if let Some(range) = range_opt { start.index = range.start; end.index = range.end; } } else if start.line + 1 < buffer.lines.len() { end.line += 1; end.index = 0; } }); if start != end { self.cursor = start; self.delete_range(start, end); } } } Action::Indent => { // Get start and end of selection let (start, end) = match self.selection_bounds() { Some(some) => some, None => (self.cursor, self.cursor), }; // For every line in selection let tab_width: usize = self.tab_width.into(); for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace let mut after_whitespace = 0; let mut required_indent = 0; self.with_buffer(|buffer| { let line = &buffer.lines[line_i]; let text = line.text(); // Default to end of line if no non-whitespace found after_whitespace = text.len(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { after_whitespace = index; required_indent = tab_width - (count % tab_width); break; } } }); // No indent required (not possible?) if required_indent == 0 { required_indent = tab_width; } self.insert_at( Cursor::new(line_i, after_whitespace), &" ".repeat(required_indent), None, ); // Adjust cursor if self.cursor.line == line_i { //TODO: should we be forcing cursor index to current indent location? if self.cursor.index < after_whitespace { self.cursor.index = after_whitespace; } self.cursor.index += required_indent; } // Adjust selection match self.selection { Selection::None => {} Selection::Normal(ref mut select) | Selection::Line(ref mut select) | Selection::Word(ref mut select) => { if select.line == line_i && select.index >= after_whitespace { select.index += required_indent; } } } // Request redraw self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } Action::Unindent => { // Get start and end of selection let (start, end) = match self.selection_bounds() { Some(some) => some, None => (self.cursor, self.cursor), }; // For every line in selection let tab_width: usize = self.tab_width.into(); for line_i in start.line..=end.line { // Determine indexes of last indent and first character after whitespace let mut last_indent = 0; let mut after_whitespace = 0; self.with_buffer(|buffer| { let line = &buffer.lines[line_i]; let text = line.text(); // Default to end of line if no non-whitespace found after_whitespace = text.len(); for (count, (index, c)) in text.char_indices().enumerate() { if !c.is_whitespace() { after_whitespace = index; break; } if count % tab_width == 0 { last_indent = index; } } }); // No de-indent required if last_indent == after_whitespace { continue; } // Delete one indent self.delete_range( Cursor::new(line_i, last_indent), Cursor::new(line_i, after_whitespace), ); // Adjust cursor if self.cursor.line == line_i && self.cursor.index > last_indent { self.cursor.index -= after_whitespace - last_indent; } // Adjust selection match self.selection { Selection::None => {} Selection::Normal(ref mut select) | Selection::Line(ref mut select) | Selection::Word(ref mut select) => { if select.line == line_i && select.index > last_indent { select.index -= after_whitespace - last_indent; } } } // Request redraw self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } Action::Click { x, y } => { self.set_selection(Selection::None); if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } } Action::DoubleClick { x, y } => { self.set_selection(Selection::None); if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } self.selection = Selection::Word(self.cursor); self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } Action::TripleClick { x, y } => { self.set_selection(Selection::None); if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) { if new_cursor != self.cursor { self.cursor = new_cursor; } self.selection = Selection::Line(self.cursor); self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } Action::Drag { x, y } => { if self.selection == Selection::None { self.selection = Selection::Normal(self.cursor); self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } if let Some(new_cursor) = self.with_buffer(|buffer| buffer.hit(x as f32, y as f32)) { if new_cursor != self.cursor { self.cursor = new_cursor; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); } } } Action::Scroll { lines } => { self.with_buffer_mut(|buffer| { let mut scroll = buffer.scroll(); scroll.layout += lines; buffer.set_scroll(scroll); }); } } if old_cursor != self.cursor { self.cursor_moved = true; self.with_buffer_mut(|buffer| buffer.set_redraw(true)); /*TODO if let Some(glyph) = run.glyphs.get(new_cursor_glyph) { let font_opt = self.buffer.font_system().get_font(glyph.cache_key.font_id); let text_glyph = &run.text[glyph.start..glyph.end]; log::debug!( "{}, {}: '{}' ('{}'): '{}' ({:?})", self.cursor.line, self.cursor.index, font_opt.as_ref().map_or("?", |font| font.info.family.as_str()), font_opt.as_ref().map_or("?", |font| font.info.post_script_name.as_str()), text_glyph, text_glyph ); } */ } } } impl<'font_system, 'buffer> BorrowedWithFontSystem<'font_system, Editor<'buffer>> { #[cfg(feature = "swash")] pub fn draw( &mut self, cache: &mut crate::SwashCache, text_color: Color, cursor_color: Color, selection_color: Color, f: F, ) where F: FnMut(i32, i32, u32, u32, Color), { self.inner.draw( self.font_system, cache, text_color, cursor_color, selection_color, f, ); } }