diff --git a/Cargo.lock b/Cargo.lock index 9c08eda57..dbc64a55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1081,7 +1081,7 @@ dependencies = [ [[package]] name = "fontique" version = "0.2.0" -source = "git+https://github.com/linebender/parley?rev=217f243aa61178229da694b1d2b0598afcf29aff#217f243aa61178229da694b1d2b0598afcf29aff" +source = "git+https://github.com/linebender/parley?rev=3c6014072226a76cf171a7d3daca503a37152a9e#3c6014072226a76cf171a7d3daca503a37152a9e" dependencies = [ "core-foundation", "core-text", @@ -2509,7 +2509,7 @@ dependencies = [ [[package]] name = "parley" version = "0.2.0" -source = "git+https://github.com/linebender/parley?rev=217f243aa61178229da694b1d2b0598afcf29aff#217f243aa61178229da694b1d2b0598afcf29aff" +source = "git+https://github.com/linebender/parley?rev=3c6014072226a76cf171a7d3daca503a37152a9e#3c6014072226a76cf171a7d3daca503a37152a9e" dependencies = [ "accesskit", "fontique", diff --git a/Cargo.toml b/Cargo.toml index 35c16c1e4..ef31fcdd6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,7 +108,7 @@ xilem_core = { version = "0.1.0", path = "xilem_core" } vello = "0.3" wgpu = "22.1.0" kurbo = "0.11.1" -parley = { git = "https://github.com/linebender/parley", rev = "217f243aa61178229da694b1d2b0598afcf29aff", features = [ +parley = { git = "https://github.com/linebender/parley", rev = "3c6014072226a76cf171a7d3daca503a37152a9e", features = [ "accesskit", ] } peniko = "0.2.0" diff --git a/masonry/src/text/editor.rs b/masonry/src/text/editor.rs index d72d5fb48..8aec0c93d 100644 --- a/masonry/src/text/editor.rs +++ b/masonry/src/text/editor.rs @@ -11,7 +11,7 @@ use accesskit::{Node, NodeId, TreeUpdate}; use parley::layout::LayoutAccessibility; use parley::{ layout::{ - cursor::{Cursor, Selection, VisualMode}, + cursor::{Cursor, Selection}, Affinity, Alignment, Layout, Line, }, style::Brush, @@ -57,7 +57,6 @@ where layout: Layout, layout_access: LayoutAccessibility, selection: Selection, - cursor_mode: VisualMode, width: Option, scale: f32, // Simple tracking of when the layout needs to be updated @@ -85,7 +84,6 @@ where layout: Default::default(), layout_access: Default::default(), selection: Default::default(), - cursor_mode: Default::default(), width: None, scale: 1.0, layout_dirty: false, @@ -128,13 +126,22 @@ where /// Delete the selection or the next cluster (typical ‘delete’ behavior). pub fn delete(&mut self) { if self.editor.selection.is_collapsed() { - let range = self.editor.selection.focus().text_range(); - if !range.is_empty() { + // Upstream cluster range + if let Some(range) = self + .editor + .selection + .focus() + .logical_clusters(&self.editor.layout)[1] + .as_ref() + .map(|cluster| cluster.text_range()) + .and_then(|range| (!range.is_empty()).then_some(range)) + { let start = range.start; self.editor.buffer.replace_range(range, ""); self.update_layout(); - self.editor - .set_selection(self.editor.cursor_at(start).into()); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Upstream).into(), + ); } } else { self.delete_selection(); @@ -143,18 +150,18 @@ where /// Delete the selection or up to the next word boundary (typical ‘ctrl + delete’ behavior). pub fn delete_word(&mut self) { - let start = self.editor.selection.focus().text_range().start; if self.editor.selection.is_collapsed() { - let end = self - .editor - .cursor_at(start) - .next_word(&self.editor.layout) - .index(); - - self.editor.buffer.replace_range(start..end, ""); - self.update_layout(); - self.editor - .set_selection(self.editor.cursor_at(start).into()); + let focus = self.editor.selection.focus(); + let start = focus.index(); + let end = focus.next_logical_word(&self.editor.layout).index(); + if self.editor.text().get(start..end).is_some() { + self.editor.buffer.replace_range(start..end, ""); + self.update_layout(); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); + } } else { self.delete_selection(); } @@ -162,27 +169,40 @@ where /// Delete the selection or the previous cluster (typical ‘backspace’ behavior). pub fn backdelete(&mut self) { - let end = self.editor.selection.focus().text_range().start; if self.editor.selection.is_collapsed() { - if let Some(start) = self + // Upstream cluster + if let Some(cluster) = self .editor .selection .focus() - .cluster_path() - .cluster(&self.editor.layout) - .map(|x| { - if self.editor.selection.focus().affinity() == Affinity::Upstream { - Some(x) - } else { - x.previous_logical() - } - }) - .and_then(|c| c.map(|x| x.text_range().start)) + .logical_clusters(&self.editor.layout)[0] + .clone() { + let range = cluster.text_range(); + let end = range.end; + let start = if cluster.is_hard_line_break() + /* || cluster.info().is_emoji() */ + { + // For newline sequences and emoji, delete the previous cluster + range.start + } else { + // Otherwise, delete the previous character + let Some((start, _)) = self + .editor + .text() + .get(..end) + .and_then(|str| str.char_indices().next_back()) + else { + return; + }; + start + }; self.editor.buffer.replace_range(start..end, ""); self.update_layout(); - self.editor - .set_selection(self.editor.cursor_at(start).into()); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); } } else { self.delete_selection(); @@ -191,20 +211,18 @@ where /// Delete the selection or back to the previous word boundary (typical ‘ctrl + backspace’ behavior). pub fn backdelete_word(&mut self) { - let end = self.editor.selection.focus().text_range().start; if self.editor.selection.is_collapsed() { - let start = self - .editor - .selection - .focus() - .previous_word(&self.editor.layout) - .text_range() - .start; - - self.editor.buffer.replace_range(start..end, ""); - self.update_layout(); - self.editor - .set_selection(self.editor.cursor_at(start).into()); + let focus = self.editor.selection.focus(); + let end = focus.index(); + let start = focus.previous_logical_word(&self.editor.layout).index(); + if self.editor.text().get(start..end).is_some() { + self.editor.buffer.replace_range(start..end, ""); + self.update_layout(); + self.editor.set_selection( + Cursor::from_byte_index(&self.editor.layout, start, Affinity::Downstream) + .into(), + ); + } } else { self.delete_selection(); } @@ -276,21 +294,20 @@ where /// Move to the next cluster left in visual order. pub fn move_left(&mut self) { - self.editor - .set_selection(self.editor.selection.previous_visual( - &self.editor.layout, - self.editor.cursor_mode, - false, - )); + self.editor.set_selection( + self.editor + .selection + .previous_visual(&self.editor.layout, false), + ); } /// Move to the next cluster right in visual order. pub fn move_right(&mut self) { - self.editor.set_selection(self.editor.selection.next_visual( - &self.editor.layout, - self.editor.cursor_mode, - false, - )); + self.editor.set_selection( + self.editor + .selection + .next_visual(&self.editor.layout, false), + ); } /// Move to the next word boundary left. @@ -298,24 +315,24 @@ where self.editor.set_selection( self.editor .selection - .previous_word(&self.editor.layout, false), + .previous_visual_word(&self.editor.layout, false), ); } /// Move to the next word boundary right. pub fn move_word_right(&mut self) { - self.editor - .set_selection(self.editor.selection.next_word(&self.editor.layout, false)); + self.editor.set_selection( + self.editor + .selection + .next_visual_word(&self.editor.layout, false), + ); } /// Select the whole buffer. pub fn select_all(&mut self) { self.editor.set_selection( - Selection::from_index(&self.editor.layout, 0, Affinity::default()).move_lines( - &self.editor.layout, - isize::MAX, - true, - ), + Selection::from_byte_index(&self.editor.layout, 0_usize, Affinity::default()) + .move_lines(&self.editor.layout, isize::MAX, true), ); } @@ -371,21 +388,17 @@ where /// Move the selection focus point to the next cluster left in visual order. pub fn select_left(&mut self) { - self.editor - .set_selection(self.editor.selection.previous_visual( - &self.editor.layout, - self.editor.cursor_mode, - true, - )); + self.editor.set_selection( + self.editor + .selection + .previous_visual(&self.editor.layout, true), + ); } /// Move the selection focus point to the next cluster right in visual order. pub fn select_right(&mut self) { - self.editor.set_selection(self.editor.selection.next_visual( - &self.editor.layout, - self.editor.cursor_mode, - true, - )); + self.editor + .set_selection(self.editor.selection.next_visual(&self.editor.layout, true)); } /// Move the selection focus point to the next word boundary left. @@ -393,14 +406,17 @@ where self.editor.set_selection( self.editor .selection - .previous_word(&self.editor.layout, true), + .previous_visual_word(&self.editor.layout, true), ); } /// Move the selection focus point to the next word boundary right. pub fn select_word_right(&mut self) { - self.editor - .set_selection(self.editor.selection.next_word(&self.editor.layout, true)); + self.editor.set_selection( + self.editor + .selection + .next_visual_word(&self.editor.layout, true), + ); } /// Select the word at the point. @@ -413,7 +429,7 @@ where /// Select the physical line at the point. pub fn select_line_at_point(&mut self, x: f32, y: f32) { self.refresh_layout(); - let focus = *Selection::from_point(&self.editor.layout, x, y) + let focus = Selection::from_point(&self.editor.layout, x, y) .line_start(&self.editor.layout, true) .focus(); self.editor @@ -437,11 +453,10 @@ where pub fn extend_selection_to_byte(&mut self, index: usize) { if self.editor.buffer.is_char_boundary(index) { self.refresh_layout(); - self.editor.set_selection( - self.editor - .selection - .maybe_extend(self.editor.cursor_at(index), true), - ); + self.editor.set_selection(extend_selection( + &self.editor.selection, + self.editor.cursor_at(index), + )); } } @@ -451,10 +466,10 @@ where pub fn select_byte_range(&mut self, start: usize, end: usize) { if self.editor.buffer.is_char_boundary(start) && self.editor.buffer.is_char_boundary(end) { self.refresh_layout(); - self.editor.set_selection( - Selection::from(self.editor.cursor_at(start)) - .maybe_extend(self.editor.cursor_at(end), true), - ); + self.editor.set_selection(Selection::new( + self.editor.cursor_at(start), + self.editor.cursor_at(end), + )); } } @@ -479,6 +494,10 @@ where } } +fn extend_selection(selection: &Selection, focus: Cursor) -> Selection { + Selection::new(selection.anchor(), focus) +} + impl PlainEditor where T: Brush + Clone + Debug + PartialEq + Default, @@ -518,13 +537,13 @@ where // TODO: Do we need to be non-dirty? // FIXME: `Selection` should make this easier if index >= self.buffer.len() { - Cursor::from_index( + Cursor::from_byte_index( &self.layout, self.buffer.len().saturating_sub(1), Affinity::Upstream, ) } else { - Cursor::from_index(&self.layout, index, Affinity::Downstream) + Cursor::from_byte_index(&self.layout, index, Affinity::Downstream) } } @@ -534,7 +553,6 @@ where layout_cx: &mut LayoutContext, s: &str, ) { - // TODO: Do we need to be non-dirty? let range = self.selection.text_range(); let start = range.start; if self.selection.is_collapsed() { @@ -544,7 +562,13 @@ where } self.update_layout(font_cx, layout_cx); - self.set_selection(self.cursor_at(start.saturating_add(s.len())).into()); + let new_index = start.saturating_add(s.len()); + let affinity = if s.ends_with("\n") { + Affinity::Downstream + } else { + Affinity::Upstream + }; + self.set_selection(Cursor::from_byte_index(&self.layout, new_index, affinity).into()); } /// Update the selection, and nudge the `Generation` if something other than `h_pos` changed. @@ -560,14 +584,14 @@ where /// Get either the contents of the current selection, or the text of the cluster at the caret. pub fn active_text(&self) -> ActiveText { if self.selection.is_collapsed() { - let range = self - .selection - .focus() - .cluster_path() - .cluster(&self.layout) - .map(|c| c.text_range()) - .unwrap_or_default(); - ActiveText::FocusedCluster(self.selection.focus().affinity(), &self.buffer[range]) + // let range = self + // .selection + // .focus() + // .cluster_path() + // .cluster(&self.layout) + // .map(|c| c.text_range()) + // .unwrap_or_default(); + ActiveText::FocusedCluster(self.selection.focus().affinity(), "") } else { ActiveText::Selection(&self.buffer[self.selection.text_range()]) } @@ -579,12 +603,8 @@ where } /// Get a rectangle representing the current caret cursor position. - pub fn selection_strong_geometry(&self, size: f32) -> Option { - self.selection.focus().strong_geometry(&self.layout, size) - } - - pub fn selection_weak_geometry(&self, size: f32) -> Option { - self.selection.focus().weak_geometry(&self.layout, size) + pub fn cursor_geometry(&self, size: f32) -> Option { + Some(self.selection.focus().geometry(&self.layout, size)) } /// Get the lines from the `Layout`. diff --git a/masonry/src/widget/text_area.rs b/masonry/src/widget/text_area.rs index ee521e5d0..6fc1c8814 100644 --- a/masonry/src/widget/text_area.rs +++ b/masonry/src/widget/text_area.rs @@ -129,8 +129,12 @@ impl TextArea { /// // This is written out fully to appease rust-analyzer; StyleProperty is imported but not recognised. /// To change the font size, use `with_style`, setting [`StyleProperty::FontSize`](parley::StyleProperty::FontSize). - pub fn new(text: &str) -> Self { + pub fn new(mut text: &str) -> Self { let mut editor = PlainEditor::new(theme::TEXT_SIZE_NORMAL); + // HACK: Parley crashes if the *initial* text is empty; any subsequent version seems to be fine. + if text.is_empty() { + text = " "; + } editor.set_text(text); TextArea { editor, @@ -808,14 +812,10 @@ impl Widget for TextArea { // TODO: Make configurable scene.fill(Fill::NonZero, transform, Color::STEEL_BLUE, None, &rect); } - if let Some(cursor) = self.editor.selection_strong_geometry(1.5) { + if let Some(cursor) = self.editor.cursor_geometry(1.5) { // TODO: Make configurable scene.fill(Fill::NonZero, transform, Color::WHITE, None, &cursor); }; - if let Some(cursor) = self.editor.selection_weak_geometry(1.5) { - // TODO: Make configurable - scene.fill(Fill::NonZero, transform, Color::LIGHT_GRAY, None, &cursor); - }; } let brush = if ctx.is_disabled() {