diff --git a/masonry/src/text/backspace.rs b/masonry/src/text/backspace.rs index 11219dfad..287022f4a 100644 --- a/masonry/src/text/backspace.rs +++ b/masonry/src/text/backspace.rs @@ -5,14 +5,14 @@ use xi_unicode::*; -use super::{EditableTextCursor, Selectable}; +use crate::text::StringCursor; /// Logic adapted from Android and /// /// See links present in that PR for upstream Android Source /// Matches Android Logic as at 2024-05-10 #[allow(clippy::cognitive_complexity)] -fn backspace_offset(text: &impl Selectable, start: usize) -> usize { +fn backspace_offset(text: &str, start: usize) -> usize { #[derive(PartialEq)] enum State { Start, @@ -34,9 +34,15 @@ fn backspace_offset(text: &impl Selectable, start: usize) -> usize { let mut delete_code_point_count = 0; let mut last_seen_vs_code_point_count = 0; - let mut cursor = text - .cursor(start) - .expect("Backspace must begin at a valid codepoint boundary."); + + let mut cursor = StringCursor { + text, + position: start, + }; + assert!( + cursor.is_boundary(), + "Backspace must begin at a valid codepoint boundary." + ); while state != State::Finished && cursor.pos() > 0 { let code_point = cursor.prev_codepoint().unwrap_or('0'); @@ -190,8 +196,8 @@ fn backspace_offset(text: &impl Selectable, start: usize) -> usize { /// This involves complicated logic to handle various special cases that /// are unique to backspace. #[allow(clippy::trivially_copy_pass_by_ref)] -pub fn offset_for_delete_backwards(caret_position: usize, text: &impl Selectable) -> usize { - backspace_offset(text, caret_position) +pub fn offset_for_delete_backwards(caret_position: usize, text: &impl AsRef) -> usize { + backspace_offset(text.as_ref(), caret_position) } #[cfg(test)] diff --git a/masonry/src/text/edit.rs b/masonry/src/text/edit.rs index 373b3d9a1..89b9e5592 100644 --- a/masonry/src/text/edit.rs +++ b/masonry/src/text/edit.rs @@ -23,47 +23,15 @@ use super::{ Selectable, TextBrush, TextWithSelection, }; -/// Text which can be edited -pub trait EditableText: Selectable { - /// Replace range with new text. - /// Can panic if supplied an invalid range. - // TODO: make this generic over Self - fn edit(&mut self, range: Range, new: impl Into); - /// Create a value of this struct - fn from_str(s: &str) -> Self; -} - -impl EditableText for String { - fn edit(&mut self, range: Range, new: impl Into) { - self.replace_range(range, &new.into()); - } - fn from_str(s: &str) -> Self { - s.to_string() - } -} - -// TODO: What advantage does this actually have? -// impl EditableText for Arc { -// fn edit(&mut self, range: Range, new: impl Into) { -// let new = new.into(); -// if !range.is_empty() || !new.is_empty() { -// Arc::make_mut(self).edit(range, new) -// } -// } -// fn from_str(s: &str) -> Self { -// Arc::new(s.to_owned()) -// } -// } - /// A region of text which can support editing operations -pub struct TextEditor { - inner: TextWithSelection, +pub struct TextEditor { + inner: TextWithSelection, /// The range of the preedit region in the text preedit_range: Option>, } -impl TextEditor { - pub fn new(text: T, text_size: f32) -> Self { +impl TextEditor { + pub fn new(text: String, text_size: f32) -> Self { Self { inner: TextWithSelection::new(text, text_size), preedit_range: None, @@ -122,11 +90,11 @@ impl TextEditor { Key::Named(NamedKey::Backspace) => { if let Some(selection) = self.inner.selection { if !selection.is_caret() { - self.text_mut().edit(selection.range(), ""); + self.text_mut().replace_range(selection.range(), ""); self.inner.selection = Some(Selection::caret(selection.min(), Affinity::Upstream)); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); } else { // TODO: more specific behavior may sometimes be warranted here @@ -136,11 +104,11 @@ impl TextEditor { let text = self.text_mut(); let offset = offset_for_delete_backwards(selection.active, text); - self.text_mut().edit(offset..selection.active, ""); + self.text_mut().replace_range(offset..selection.active, ""); self.inner.selection = Some(Selection::caret(offset, selection.active_affinity)); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); } Handled::Yes @@ -151,24 +119,24 @@ impl TextEditor { Key::Named(NamedKey::Delete) => { if let Some(selection) = self.inner.selection { if !selection.is_caret() { - self.text_mut().edit(selection.range(), ""); + self.text_mut().replace_range(selection.range(), ""); self.inner.selection = Some(Selection::caret( selection.min(), Affinity::Downstream, )); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); } else if let Some(offset) = self.text().next_grapheme_offset(selection.active) { - self.text_mut().edit(selection.min()..offset, ""); + self.text_mut().replace_range(selection.min()..offset, ""); self.inner.selection = Some(Selection::caret( selection.min(), selection.active_affinity, )); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); } Handled::Yes @@ -184,18 +152,19 @@ impl TextEditor { h_pos: None, }); let c = ' '; - self.text_mut().edit(selection.range(), c); + self.text_mut() + .replace_range(selection.range(), &c.to_string()); self.inner.selection = Some(Selection::caret( selection.min() + c.len_utf8(), // We have just added this character, so we are "affined" with it Affinity::Downstream, )); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); Handled::Yes } Key::Named(NamedKey::Enter) => { - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextEntered(contents)); Handled::Yes } @@ -219,17 +188,17 @@ impl TextEditor { Key::Named(NamedKey::Backspace) => { if let Some(selection) = self.inner.selection { if !selection.is_caret() { - self.text_mut().edit(selection.range(), ""); + self.text_mut().replace_range(selection.range(), ""); self.inner.selection = Some(Selection::caret(selection.min(), Affinity::Upstream)); } let offset = self.text().prev_word_offset(selection.active).unwrap_or(0); - self.text_mut().edit(offset..selection.active, ""); + self.text_mut().replace_range(offset..selection.active, ""); self.inner.selection = Some(Selection::caret(offset, Affinity::Upstream)); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); Handled::Yes } else { @@ -239,7 +208,7 @@ impl TextEditor { Key::Named(NamedKey::Delete) => { if let Some(selection) = self.inner.selection { if !selection.is_caret() { - self.text_mut().edit(selection.range(), ""); + self.text_mut().replace_range(selection.range(), ""); self.inner.selection = Some(Selection::caret( selection.min(), Affinity::Downstream, @@ -247,11 +216,11 @@ impl TextEditor { } else if let Some(offset) = self.text().next_word_offset(selection.active) { - self.text_mut().edit(selection.active..offset, ""); + self.text_mut().replace_range(selection.active..offset, ""); self.inner.selection = Some(Selection::caret(selection.min(), Affinity::Upstream)); } - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); Handled::Yes } else { @@ -268,20 +237,21 @@ impl TextEditor { TextEvent::Ime(ime) => match ime { Ime::Commit(text) => { if let Some(selection_range) = self.selection.map(|x| x.range()) { - self.text_mut().edit(selection_range.clone(), text); + self.text_mut().replace_range(selection_range.clone(), text); self.selection = Some(Selection::caret( selection_range.start + text.len(), Affinity::Upstream, )); } - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); Handled::Yes } Ime::Preedit(preedit_string, preedit_sel) => { if let Some(preedit) = self.preedit_range.clone() { // TODO: Handle the case where this is the same value, to avoid some potential infinite loops - self.text_mut().edit(preedit.clone(), preedit_string); + self.text_mut() + .replace_range(preedit.clone(), preedit_string); let np = preedit.start..(preedit.start + preedit_string.len()); self.preedit_range = if preedit_string.is_empty() { None @@ -308,7 +278,7 @@ impl TextEditor { return Handled::Yes; } let sr = self.selection.map(|x| x.range()).unwrap_or(0..0); - self.text_mut().edit(sr.clone(), preedit_string); + self.text_mut().replace_range(sr.clone(), preedit_string); let np = sr.start..(sr.start + preedit_string.len()); self.preedit_range = if preedit_string.is_empty() { None @@ -330,7 +300,7 @@ impl TextEditor { Ime::Enabled => { // Generally this shouldn't happen, but I can't prove it won't. if let Some(preedit) = self.preedit_range.clone() { - self.text_mut().edit(preedit.clone(), ""); + self.text_mut().replace_range(preedit.clone(), ""); self.selection = Some( self.selection .unwrap_or(Selection::caret(0, Affinity::Upstream)), @@ -341,7 +311,7 @@ impl TextEditor { } Ime::Disabled => { if let Some(preedit) = self.preedit_range.clone() { - self.text_mut().edit(preedit.clone(), ""); + self.text_mut().replace_range(preedit.clone(), ""); self.preedit_range = None; let sm = self.selection.map(|x| x.min()).unwrap_or(0); if preedit.contains(&sm) { @@ -364,20 +334,20 @@ impl TextEditor { active_affinity: Affinity::Downstream, h_pos: None, }); - self.text_mut().edit(selection.range(), &**c); + self.text_mut().replace_range(selection.range(), c); self.inner.selection = Some(Selection::caret( selection.min() + c.len(), // We have just added this character, so we are "affined" with it Affinity::Downstream, )); - let contents = self.text().as_str().to_string(); + let contents = self.text().clone(); ctx.submit_action(Action::TextChanged(contents)); Handled::Yes } } -impl Deref for TextEditor { - type Target = TextWithSelection; +impl Deref for TextEditor { + type Target = TextWithSelection; fn deref(&self) -> &Self::Target { &self.inner @@ -385,7 +355,7 @@ impl Deref for TextEditor { } // TODO: Being able to call `Self::Target::rebuild` (and `draw`) isn't great. -impl DerefMut for TextEditor { +impl DerefMut for TextEditor { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } @@ -393,20 +363,10 @@ impl DerefMut for TextEditor { #[cfg(test)] mod tests { - use super::EditableText; - - // #[test] - // fn arcstring_empty_edit() { - // let a = Arc::new("hello".to_owned()); - // let mut b = a.clone(); - // b.edit(5..5, ""); - // assert!(Arc::ptr_eq(&a, &b)); - // } - #[test] fn replace() { let mut a = String::from("hello world"); - a.edit(1..9, "era"); + a.replace_range(1..9, "era"); assert_eq!("herald", a); } } diff --git a/masonry/src/text/layout.rs b/masonry/src/text/layout.rs index 0c2e9cabe..9355a9c15 100644 --- a/masonry/src/text/layout.rs +++ b/masonry/src/text/layout.rs @@ -14,8 +14,6 @@ use vello::kurbo::{Affine, Line, Point, Rect, Size}; use vello::peniko::{self, Color, Gradient}; use vello::Scene; -use super::{Link, TextStorage}; - /// A component for displaying text on screen. /// /// This is a type intended to be used by other widgets that display text. @@ -276,31 +274,31 @@ impl TextLayout { // } } -impl TextLayout { +impl + Eq> TextLayout { #[track_caller] fn assert_rebuilt(&self, method: &str) { if self.needs_layout || self.needs_line_breaks { debug_panic!( "TextLayout::{method} called without rebuilding layout object. Text was '{}'", - self.text.as_str().chars().take(250).collect::() + self.text.as_ref().chars().take(250).collect::() ); } } /// Set the text to display. pub fn set_text(&mut self, text: T) { - if !self.text.maybe_eq(&text) { + if self.text != text { self.text = text; self.invalidate(); } } - /// Returns the [`TextStorage`] backing this layout, if it exists. + /// Returns the string backing this layout, if it exists. pub fn text(&self) -> &T { &self.text } - /// Returns the [`TextStorage`] backing this layout, if it exists. + /// Returns the string backing this layout, if it exists. /// /// Invalidates the layout and so should only be used when definitely applying an edit pub fn text_mut(&mut self) -> &mut T { @@ -428,22 +426,6 @@ impl TextLayout { Line::new(p1, p2) } - /// Returns the [`Link`] at the provided point (relative to the layout's origin) if one exists. - /// - /// This can be used both for hit-testing (deciding whether to change the mouse cursor, - /// or performing some other action when hovering) as well as for retrieving a [`Link`] - /// on click. - /// - /// [`Link`]: super::attribute::Link - pub fn link_for_pos(&self, pos: Point) -> Option<&Link> { - let (_, i) = self - .links - .iter() - .rfind(|(hit_box, _)| hit_box.contains(pos))?; - - self.text.links().get(*i) - } - /// Rebuild the inner layout as needed. /// /// This `TextLayout` object manages a lower-level layout object that may @@ -475,15 +457,16 @@ impl TextLayout { if self.needs_layout { self.needs_layout = false; - let mut builder = layout_ctx.ranged_builder(font_ctx, self.text.as_str(), self.scale); + let mut builder = layout_ctx.ranged_builder(font_ctx, self.text.as_ref(), self.scale); builder.push_default(&StyleProperty::Brush(self.brush.clone())); builder.push_default(&StyleProperty::FontSize(self.text_size)); builder.push_default(&StyleProperty::FontStack(self.font)); builder.push_default(&StyleProperty::FontWeight(self.weight)); builder.push_default(&StyleProperty::FontStyle(self.style)); - // For more advanced features (e.g. variable font axes), these can be set in add_attributes - let builder = self.text.add_attributes(builder); + // Currently, this is used for: + // - underlining IME suggestions + // - applying a brush to selected text. let mut builder = attributes(builder); builder.build_into(&mut self.layout); @@ -520,10 +503,10 @@ impl TextLayout { } } -impl std::fmt::Debug for TextLayout { +impl + Eq> std::fmt::Debug for TextLayout { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("TextLayout") - .field("text", &self.text.as_str().len()) + .field("text", &self.text.as_ref()) .field("scale", &self.scale) .field("brush", &self.brush) .field("font", &self.font) @@ -540,7 +523,7 @@ impl std::fmt::Debug for TextLayout { } } -impl Default for TextLayout { +impl + Eq + Default> Default for TextLayout { fn default() -> Self { Self::new(Default::default(), crate::theme::TEXT_SIZE_NORMAL as f32) } diff --git a/masonry/src/text/mod.rs b/masonry/src/text/mod.rs index 0a9bbe70e..129a196eb 100644 --- a/masonry/src/text/mod.rs +++ b/masonry/src/text/mod.rs @@ -10,21 +10,16 @@ //! //! All of these have the same set of global styling options, and can contain rich text -mod store; -pub use store::{Link, TextStorage}; - mod layout; pub use layout::{Hinting, LayoutMetrics, TextBrush, TextLayout}; mod selection; -pub use selection::{ - len_utf8_from_first_byte, EditableTextCursor, Selectable, StringCursor, TextWithSelection, -}; +pub use selection::{len_utf8_from_first_byte, Selectable, StringCursor, TextWithSelection}; // mod movement; mod edit; -pub use edit::{EditableText, TextEditor}; +pub use edit::TextEditor; mod backspace; pub use backspace::offset_for_delete_backwards; diff --git a/masonry/src/text/movement.rs b/masonry/src/text/movement.rs deleted file mode 100644 index 753e997c8..000000000 --- a/masonry/src/text/movement.rs +++ /dev/null @@ -1,357 +0,0 @@ -// Copyright 2018 the Xilem Authors and the Druid Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Text editing movements. - -use std::ops::Range; - -use unicode_segmentation::UnicodeSegmentation; - -use crate::kurbo::Point; - -use super::{layout::TextLayout, Selectable, TextStorage}; - -/// Compute the result of a [`Movement`] on a [`Selection`]. -/// -/// returns a new selection representing the state after the movement. -/// -/// If `modify` is true, only the 'active' edge (the `end`) of the selection -/// should be changed; this is the case when the user moves with the shift -/// key pressed. -pub fn movement( - m: Movement, - s: Selection, - layout: &TextLayout, - modify: bool, -) -> Selection { - if layout.needs_rebuild() { - debug_panic!("movement() called before layout rebuild"); - return s; - } - let text = layout.text(); - let parley_layout = layout.layout(); - - let writing_direction = || { - if layout - .cursor_for_text_position(s.active, s.active_affinity) - .is_rtl - { - WritingDirection::RightToLeft - } else { - WritingDirection::LeftToRight - } - }; - - let (offset, h_pos) = match m { - Movement::Grapheme(d) => { - let direction = writing_direction(); - if d.is_upstream_for_direction(direction) { - if s.is_caret() || modify { - text.prev_grapheme_offset(s.active) - .map(|off| (off, None)) - .unwrap_or((0, s.h_pos)) - } else { - (s.min(), None) - } - } else { - if s.is_caret() || modify { - text.next_grapheme_offset(s.active) - .map(|off| (off, None)) - .unwrap_or((s.active, s.h_pos)) - } else { - (s.max(), None) - } - } - } - Movement::Vertical(VerticalMovement::LineUp) => { - let cur_pos = layout.cursor_for_text_position(s.active, s.active_affinity); - let h_pos = s.h_pos.unwrap_or(cur_pos.advance); - if cur_pos.path.line_index == 0 { - (0, Some(h_pos)) - } else { - let lm = cur_pos.path.line(&parley_layout).unwrap(); - let point_above = Point::new(h_pos, cur_pos.point.y - lm.height); - let up_pos = layout.hit_test_point(point_above); - if up_pos.is_inside { - (up_pos.idx, Some(h_pos)) - } else { - // because we can't specify affinity, moving up when h_pos - // is wider than both the current line and the previous line - // can result in a cursor position at the visual start of the - // current line; so we handle this as a special-case. - let lm_prev = layout.line_metric(cur_pos.line.saturating_sub(1)).unwrap(); - let up_pos = lm_prev.end_offset - lm_prev.trailing_whitespace; - (up_pos, Some(h_pos)) - } - } - } - Movement::Vertical(VerticalMovement::LineDown) => { - let cur_pos = layout.hit_test_text_position(s.active); - let h_pos = s.h_pos.unwrap_or(cur_pos.point.x); - if cur_pos.line == layout.line_count() - 1 { - (text.len(), Some(h_pos)) - } else { - let lm = layout.line_metric(cur_pos.line).unwrap(); - // may not work correctly for point sizes below 1.0 - let y_below = lm.y_offset + lm.height + 1.0; - let point_below = Point::new(h_pos, y_below); - let up_pos = layout.hit_test_point(point_below); - (up_pos.idx, Some(point_below.x)) - } - } - Movement::Vertical(VerticalMovement::DocumentStart) => (0, None), - Movement::Vertical(VerticalMovement::DocumentEnd) => (text.len(), None), - - Movement::ParagraphStart => (text.preceding_line_break(s.active), None), - Movement::ParagraphEnd => (text.next_line_break(s.active), None), - - Movement::Line(d) => { - let hit = layout.hit_test_text_position(s.active); - let lm = layout.line_metric(hit.line).unwrap(); - let offset = if d.is_upstream_for_direction(writing_direction) { - lm.start_offset - } else { - lm.end_offset - lm.trailing_whitespace - }; - (offset, None) - } - Movement::Word(d) => { - if d.is_upstream_for_direction(writing_direction()) { - let offset = if s.is_caret() || modify { - text.prev_word_offset(s.active).unwrap_or(0) - } else { - s.min() - }; - (offset, None) - } else { - let offset = if s.is_caret() || modify { - text.next_word_offset(s.active).unwrap_or(s.active) - } else { - s.max() - }; - (offset, None) - } - } - - // These two are not handled; they require knowledge of the size - // of the viewport. - Movement::Vertical(VerticalMovement::PageDown) - | Movement::Vertical(VerticalMovement::PageUp) => (s.active, s.h_pos), - other => { - tracing::warn!("unhandled movement {:?}", other); - (s.anchor, s.h_pos) - } - }; - - let start = if modify { s.anchor } else { offset }; - Selection::new(start, offset).with_h_pos(h_pos) -} - -/// Indicates a movement that transforms a particular text position in a -/// document. -/// -/// These movements transform only single indices — not selections. -/// -/// You'll note that a lot of these operations are idempotent, but you can get -/// around this by first sending a `Grapheme` movement. If for instance, you -/// want a `ParagraphStart` that is not idempotent, you can first send -/// `Movement::Grapheme(Direction::Upstream)`, and then follow it with -/// `ParagraphStart`. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Movement { - /// A movement that stops when it reaches an extended grapheme cluster boundary. - /// - /// This movement is achieved on most systems by pressing the left and right - /// arrow keys. For more information on grapheme clusters, see - /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries). - Grapheme(Direction), - /// A movement that stops when it reaches a word boundary. - /// - /// This movement is achieved on most systems by pressing the left and right - /// arrow keys while holding control. For more information on words, see - /// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries). - Word(Direction), - /// A movement that stops when it reaches a soft line break. - /// - /// This movement is achieved on macOS by pressing the left and right arrow - /// keys while holding command. `Line` should be idempotent: if the - /// position is already at the end of a soft-wrapped line, this movement - /// should never push it onto another soft-wrapped line. - /// - /// In order to implement this properly, your text positions should remember - /// their affinity. - Line(Direction), - /// An upstream movement that stops when it reaches a hard line break. - /// - /// `ParagraphStart` should be idempotent: if the position is already at the - /// start of a hard-wrapped line, this movement should never push it onto - /// the previous line. - ParagraphStart, - /// A downstream movement that stops when it reaches a hard line break. - /// - /// `ParagraphEnd` should be idempotent: if the position is already at the - /// end of a hard-wrapped line, this movement should never push it onto the - /// next line. - ParagraphEnd, - /// A vertical movement, see `VerticalMovement` for more details. - Vertical(VerticalMovement), -} - -/// Indicates a horizontal direction in the text. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum Direction { - /// The direction visually to the left. - /// - /// This may be byte-wise forwards or backwards in the document, depending - /// on the text direction around the position being moved. - Left, - /// The direction visually to the right. - /// - /// This may be byte-wise forwards or backwards in the document, depending - /// on the text direction around the position being moved. - Right, - /// Byte-wise backwards in the document. - /// - /// In a left-to-right context, this value is the same as `Left`. - Upstream, - /// Byte-wise forwards in the document. - /// - /// In a left-to-right context, this value is the same as `Right`. - Downstream, -} - -impl Direction { - /// Returns `true` if this direction is byte-wise backwards for - /// the provided [`WritingDirection`]. - /// - /// The provided direction *must not be* `WritingDirection::Natural`. - pub fn is_upstream_for_direction(self, direction: WritingDirection) -> bool { - // assert!( - // !matches!(direction, WritingDirection::Natural), - // "writing direction must be resolved" - // ); - match self { - Direction::Upstream => true, - Direction::Downstream => false, - Direction::Left => matches!(direction, WritingDirection::LeftToRight), - Direction::Right => matches!(direction, WritingDirection::RightToLeft), - } - } -} - -/// Distinguishes between two visually distinct locations with the same byte -/// index. -/// -/// Sometimes, a byte location in a document has two visual locations. For -/// example, the end of a soft-wrapped line and the start of the subsequent line -/// have different visual locations (and we want to be able to place an input -/// caret in either place!) but the same byte-wise location. This also shows up -/// in bidirectional text contexts. Affinity allows us to disambiguate between -/// these two visual locations. -/// -/// Note that in scenarios where soft line breaks interact with bidi text, this gets -/// more complicated. -#[derive(Copy, Clone, Debug, Hash, PartialEq)] -pub enum Affinity { - /// The position which has an apparent position "earlier" in the text. - /// For soft line breaks, this is the position at the end of the first line. - /// - /// For positions in-between bidi contexts, this is the position which is - /// related to the "outgoing" text section. E.g. for the string "abcDEF" (rendered `abcFED`), - /// with the cursor at "abc|DEF" with upstream affinity, the cursor would be rendered at the - /// position `abc|DEF` - Upstream, - /// The position which has a higher apparent position in the text. - /// For soft line breaks, this is the position at the beginning of the second line. - /// - /// For positions in-between bidi contexts, this is the position which is - /// related to the "incoming" text section. E.g. for the string "abcDEF" (rendered `abcFED`), - /// with the cursor at "abc|DEF" with downstream affinity, the cursor would be rendered at the - /// position `abcDEF|` - Downstream, -} - -impl Affinity { - /// Convert into the `parley` form of "leading" - pub fn is_leading(&self) -> bool { - match self { - Affinity::Upstream => false, - Affinity::Downstream => true, - } - } -} - -/// Indicates a horizontal direction for writing text. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum WritingDirection { - LeftToRight, - RightToLeft, - // /// Indicates writing direction should be automatically detected based on - // /// the text contents. - // See also `is_upstream_for_direction` if adding back in - // Natural, -} - -/// Indicates a vertical movement in a text document. -#[non_exhaustive] -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub enum VerticalMovement { - LineUp, - LineDown, - PageUp, - PageDown, - DocumentStart, - DocumentEnd, -} - -/// Given a position in some text, return the containing word boundaries. -/// -/// The returned range may not necessary be a 'word'; for instance it could be -/// the sequence of whitespace between two words. -/// -/// If the position is on a word boundary, that will be considered the start -/// of the range. -/// -/// This uses Unicode word boundaries, as defined in [UAX#29]. -/// -/// [UAX#29]: http://www.unicode.org/reports/tr29/ -pub(crate) fn word_range_for_pos(text: &str, pos: usize) -> Range { - text.split_word_bound_indices() - .map(|(ix, word)| ix..(ix + word.len())) - .find(|range| range.contains(&pos)) - .unwrap_or(pos..pos) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn word_range_simple() { - assert_eq!(word_range_for_pos("hello world", 3), 0..5); - assert_eq!(word_range_for_pos("hello world", 8), 6..11); - } - - #[test] - fn word_range_whitespace() { - assert_eq!(word_range_for_pos("hello world", 5), 5..6); - } - - #[test] - fn word_range_rtl() { - let rtl = "مرحبا بالعالم"; - assert_eq!(word_range_for_pos(rtl, 5), 0..10); - assert_eq!(word_range_for_pos(rtl, 16), 11..25); - assert_eq!(word_range_for_pos(rtl, 10), 10..11); - } - - #[test] - fn word_range_mixed() { - let mixed = "hello مرحبا بالعالم world"; - assert_eq!(word_range_for_pos(mixed, 3), 0..5); - assert_eq!(word_range_for_pos(mixed, 8), 6..16); - assert_eq!(word_range_for_pos(mixed, 19), 17..31); - assert_eq!(word_range_for_pos(mixed, 36), 32..37); - } -} diff --git a/masonry/src/text/selection.rs b/masonry/src/text/selection.rs index a449fe2ed..1915ea521 100644 --- a/masonry/src/text/selection.rs +++ b/masonry/src/text/selection.rs @@ -18,7 +18,7 @@ use winit::keyboard::NamedKey; use crate::event::{PointerButton, PointerState}; use crate::{Handled, TextEvent}; -use super::{TextBrush, TextLayout, TextStorage}; +use super::{TextBrush, TextLayout}; pub struct TextWithSelection { pub layout: TextLayout, @@ -498,15 +498,7 @@ pub enum Affinity { } /// Text which can have internal selections -pub trait Selectable: Sized + TextStorage { - type Cursor<'a>: EditableTextCursor - where - Self: 'a; - - /// Create a cursor with a reference to the text and a offset position. - /// - /// Returns None if the position isn't a codepoint boundary. - fn cursor(&self, position: usize) -> Option>; +pub trait Selectable: Sized + AsRef + Eq { /// Get slice of text at range. fn slice(&self, range: Range) -> Option>; @@ -541,83 +533,32 @@ pub trait Selectable: Sized + TextStorage { fn is_empty(&self) -> bool; } -/// A cursor with convenience functions for moving through `EditableText`. -pub trait EditableTextCursor { - /// Set cursor position. - fn set(&mut self, position: usize); - - /// Get cursor position. - fn pos(&self) -> usize; - - /// Check if cursor position is at a codepoint boundary. - fn is_boundary(&self) -> bool; - - /// Move cursor to previous codepoint boundary, if it exists. - /// Returns previous codepoint as usize offset. - fn prev(&mut self) -> Option; - - /// Move cursor to next codepoint boundary, if it exists. - /// Returns current codepoint as usize offset. - fn next(&mut self) -> Option; - - /// Get the next codepoint after the cursor position, without advancing - /// the cursor. - fn peek_next_codepoint(&self) -> Option; - - /// Return codepoint preceding cursor offset and move cursor backward. - fn prev_codepoint(&mut self) -> Option; - - /// Return codepoint at cursor offset and move cursor forward. - fn next_codepoint(&mut self) -> Option; - - /// Return current offset if it's a boundary, else next. - fn at_or_next(&mut self) -> Option; - - /// Return current offset if it's a boundary, else previous. - fn at_or_prev(&mut self) -> Option; -} - -impl + TextStorage> Selectable for Str { - type Cursor<'a> = StringCursor<'a> where Self: 'a; - - fn cursor(&self, position: usize) -> Option { - let new_cursor = StringCursor { - text: self, - position, - }; - - if new_cursor.is_boundary() { - Some(new_cursor) - } else { - None - } - } - +impl + Eq> Selectable for Str { fn slice(&self, range: Range) -> Option> { - self.get(range).map(Cow::from) + self.as_ref().get(range).map(Cow::from) } fn len(&self) -> usize { - self.deref().len() + self.as_ref().len() } fn prev_grapheme_offset(&self, from: usize) -> Option { let mut c = GraphemeCursor::new(from, self.len(), true); - c.prev_boundary(self, 0).unwrap() + c.prev_boundary(self.as_ref(), 0).unwrap() } fn next_grapheme_offset(&self, from: usize) -> Option { let mut c = GraphemeCursor::new(from, self.len(), true); - c.next_boundary(self, 0).unwrap() + c.next_boundary(self.as_ref(), 0).unwrap() } fn prev_codepoint_offset(&self, from: usize) -> Option { - let mut c = self.cursor(from).unwrap(); + let mut c = StringCursor::new(self.as_ref(), from).unwrap(); c.prev() } fn next_codepoint_offset(&self, from: usize) -> Option { - let mut c = self.cursor(from).unwrap(); + let mut c = StringCursor::new(self.as_ref(), from).unwrap(); if c.next().is_some() { Some(c.pos()) } else { @@ -628,7 +569,7 @@ impl + TextStorage> Selectable for Str { fn prev_word_offset(&self, from: usize) -> Option { let mut offset = from; let mut passed_alphanumeric = false; - for prev_grapheme in self.get(0..from)?.graphemes(true).rev() { + for prev_grapheme in self.as_ref().get(0..from)?.graphemes(true).rev() { let is_alphanumeric = prev_grapheme.chars().next()?.is_alphanumeric(); if is_alphanumeric { passed_alphanumeric = true; @@ -643,7 +584,7 @@ impl + TextStorage> Selectable for Str { fn next_word_offset(&self, from: usize) -> Option { let mut offset = from; let mut passed_alphanumeric = false; - for next_grapheme in self.get(from..)?.graphemes(true) { + for next_grapheme in self.as_ref().get(from..)?.graphemes(true) { let is_alphanumeric = next_grapheme.chars().next()?.is_alphanumeric(); if is_alphanumeric { passed_alphanumeric = true; @@ -656,13 +597,13 @@ impl + TextStorage> Selectable for Str { } fn is_empty(&self) -> bool { - self.deref().is_empty() + self.as_ref().is_empty() } fn preceding_line_break(&self, from: usize) -> usize { let mut offset = from; - for byte in self.get(0..from).unwrap_or("").bytes().rev() { + for byte in self.as_ref().get(0..from).unwrap_or("").bytes().rev() { if byte == 0x0a { return offset; } @@ -675,7 +616,7 @@ impl + TextStorage> Selectable for Str { fn next_line_break(&self, from: usize) -> usize { let mut offset = from; - for char in self.get(from..).unwrap_or("").bytes() { + for char in self.as_ref().get(from..).unwrap_or("").bytes() { if char == 0x0a { return offset; } @@ -686,27 +627,43 @@ impl + TextStorage> Selectable for Str { } } -/// A cursor type that implements `EditableTextCursor` for string types +/// A cursor type with helper methods for moving through strings. #[derive(Debug)] pub struct StringCursor<'a> { - text: &'a str, - position: usize, + pub(crate) text: &'a str, + pub(crate) position: usize, +} + +impl<'a> StringCursor<'a> { + pub fn new(text: &'a str, position: usize) -> Option { + let res = Self { text, position }; + if res.is_boundary() { + Some(res) + } else { + None + } + } } -impl<'a> EditableTextCursor for StringCursor<'a> { - fn set(&mut self, position: usize) { +impl<'a> StringCursor<'a> { + /// Set cursor position. + pub(crate) fn set(&mut self, position: usize) { self.position = position; } - fn pos(&self) -> usize { + /// Get cursor position. + pub(crate) fn pos(&self) -> usize { self.position } - fn is_boundary(&self) -> bool { + /// Check if cursor position is at a codepoint boundary. + pub(crate) fn is_boundary(&self) -> bool { self.text.is_char_boundary(self.position) } - fn prev(&mut self) -> Option { + /// Move cursor to previous codepoint boundary, if it exists. + /// Returns previous codepoint as usize offset, or `None` if this cursor was already at the first boundary. + pub(crate) fn prev(&mut self) -> Option { let current_pos = self.pos(); if current_pos == 0 { @@ -721,7 +678,9 @@ impl<'a> EditableTextCursor for StringCursor<'a> { } } - fn next(&mut self) -> Option { + /// Move cursor to next codepoint boundary, if it exists. + /// Returns current codepoint as usize offset. + pub(crate) fn next(&mut self) -> Option { let current_pos = self.pos(); if current_pos == self.text.len() { @@ -733,42 +692,14 @@ impl<'a> EditableTextCursor for StringCursor<'a> { } } - fn peek_next_codepoint(&self) -> Option { - self.text[self.pos()..].chars().next() - } - - fn prev_codepoint(&mut self) -> Option { + /// Return codepoint preceding cursor offset and move cursor backward. + pub(crate) fn prev_codepoint(&mut self) -> Option { if let Some(prev) = self.prev() { self.text[prev..].chars().next() } else { None } } - - fn next_codepoint(&mut self) -> Option { - let current_index = self.pos(); - if self.next().is_some() { - self.text[current_index..].chars().next() - } else { - None - } - } - - fn at_or_next(&mut self) -> Option { - if self.is_boundary() { - Some(self.pos()) - } else { - self.next() - } - } - - fn at_or_prev(&mut self) -> Option { - if self.is_boundary() { - Some(self.pos()) - } else { - self.prev() - } - } } pub fn len_utf8_from_first_byte(b: u8) -> usize { @@ -780,6 +711,7 @@ pub fn len_utf8_from_first_byte(b: u8) -> usize { } } +// --- MARK: TESTS --- #[cfg(test)] mod tests { use super::*; @@ -817,7 +749,7 @@ mod tests { #[test] fn prev_next() { let input = String::from("abc"); - let mut cursor = input.cursor(0).unwrap(); + let mut cursor = StringCursor::new(&input, 0).unwrap(); assert_eq!(cursor.next(), Some(0)); assert_eq!(cursor.next(), Some(1)); assert_eq!(cursor.prev(), Some(1)); @@ -825,29 +757,6 @@ mod tests { assert_eq!(cursor.next(), Some(2)); } - #[test] - fn peek_next_codepoint() { - let inp = String::from("$¢€£💶"); - let mut cursor = inp.cursor(0).unwrap(); - assert_eq!(cursor.peek_next_codepoint(), Some('$')); - assert_eq!(cursor.peek_next_codepoint(), Some('$')); - assert_eq!(cursor.next_codepoint(), Some('$')); - assert_eq!(cursor.peek_next_codepoint(), Some('¢')); - assert_eq!(cursor.prev_codepoint(), Some('$')); - assert_eq!(cursor.peek_next_codepoint(), Some('$')); - assert_eq!(cursor.next_codepoint(), Some('$')); - assert_eq!(cursor.next_codepoint(), Some('¢')); - assert_eq!(cursor.peek_next_codepoint(), Some('€')); - assert_eq!(cursor.next_codepoint(), Some('€')); - assert_eq!(cursor.peek_next_codepoint(), Some('£')); - assert_eq!(cursor.next_codepoint(), Some('£')); - assert_eq!(cursor.peek_next_codepoint(), Some('💶')); - assert_eq!(cursor.next_codepoint(), Some('💶')); - assert_eq!(cursor.peek_next_codepoint(), None); - assert_eq!(cursor.next_codepoint(), None); - assert_eq!(cursor.peek_next_codepoint(), None); - } - #[test] fn prev_grapheme_offset() { // A with ring, hangul, regional indicator "US" diff --git a/masonry/src/text/store.rs b/masonry/src/text/store.rs deleted file mode 100644 index 1d13cfa14..000000000 --- a/masonry/src/text/store.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2018 the Xilem Authors and the Druid Authors -// SPDX-License-Identifier: Apache-2.0 - -//! Storing text. - -use std::{ops::Deref, sync::Arc}; - -use parley::context::RangedBuilder; - -use crate::ArcStr; - -use super::layout::TextBrush; - -#[derive(Copy, Clone)] -// TODO: Implement links -pub struct Link; - -/// Text which can be displayed. -pub trait TextStorage: 'static { - fn as_str(&self) -> &str; - /// If this `TextStorage` object manages style spans, it should implement - /// this method and update the provided builder with its spans, as required. - /// - /// This takes `&self`, as we needed to call `Self::as_str` to get the value stored in - /// the `RangedBuilder` - #[allow(unused_variables)] - fn add_attributes<'b>( - &self, - builder: RangedBuilder<'b, TextBrush, &'b str>, - ) -> RangedBuilder<'b, TextBrush, &'b str> { - builder - } - - /// Any additional [`Link`] attributes on this text. - /// - /// If this `TextStorage` object manages link attributes, it should implement this - /// method and return any attached [`Link`]s. - /// - /// Unlike other attributes, links are managed in Masonry, not in [`piet`]; as such they - /// require a separate API. - /// - /// [`Link`]: super::attribute::Link - /// [`piet`]: https://docs.rs/piet - fn links(&self) -> &[Link] { - &[] - } - - /// Determines quickly whether two text objects have the same content. - /// - /// To allow for faster checks, this method is allowed to return false negatives. - fn maybe_eq(&self, other: &Self) -> bool; -} - -impl TextStorage for &'static str { - fn as_str(&self) -> &str { - self - } - fn maybe_eq(&self, other: &Self) -> bool { - self == other - } -} - -impl TextStorage for ArcStr { - fn as_str(&self) -> &str { - self.deref() - } - fn maybe_eq(&self, other: &Self) -> bool { - self == other - } -} - -impl TextStorage for String { - fn as_str(&self) -> &str { - self.deref() - } - fn maybe_eq(&self, other: &Self) -> bool { - self == other - } -} - -impl TextStorage for Arc { - fn as_str(&self) -> &str { - self.deref() - } - fn maybe_eq(&self, other: &Self) -> bool { - self == other - } -} diff --git a/masonry/src/widget/button.rs b/masonry/src/widget/button.rs index ecf9c0231..3b5960d60 100644 --- a/masonry/src/widget/button.rs +++ b/masonry/src/widget/button.rs @@ -11,7 +11,6 @@ use vello::Scene; use crate::action::Action; use crate::event::PointerButton; use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint}; -use crate::text::TextStorage; use crate::widget::{Label, WidgetMut, WidgetPod}; use crate::{ theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, Insets, LayoutCtx, LifeCycle, @@ -189,7 +188,7 @@ impl Widget for Button { // This is more of a proof of concept of `get_raw_ref()`. if false { let label = ctx.get_raw_ref(&self.label); - let name = label.widget().text().as_str().to_string(); + let name = label.widget().text().as_ref().to_string(); ctx.current_node().set_name(name); } ctx.current_node() @@ -207,7 +206,7 @@ impl Widget for Button { // FIXME #[cfg(FALSE)] fn get_debug_text(&self) -> Option { - Some(self.label.as_ref().text().as_str().to_string()) + Some(self.label.as_ref().text().as_ref().to_string()) } } diff --git a/masonry/src/widget/checkbox.rs b/masonry/src/widget/checkbox.rs index 51266606e..26fe6d7a4 100644 --- a/masonry/src/widget/checkbox.rs +++ b/masonry/src/widget/checkbox.rs @@ -11,7 +11,6 @@ use vello::Scene; use crate::action::Action; use crate::paint_scene_helpers::{fill_lin_gradient, stroke, UnitPoint}; -use crate::text::TextStorage; use crate::widget::{Label, WidgetMut}; use crate::{ theme, AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, @@ -191,7 +190,7 @@ impl Widget for Checkbox { // This is more of a proof of concept of `get_raw_ref()`. if false { let label = ctx.get_raw_ref(&self.label); - let name = label.widget().text().as_str().to_string(); + let name = label.widget().text().as_ref().to_string(); ctx.current_node().set_name(name); } if self.checked { diff --git a/masonry/src/widget/label.rs b/masonry/src/widget/label.rs index f0c5d5779..19d268e8e 100644 --- a/masonry/src/widget/label.rs +++ b/masonry/src/widget/label.rs @@ -12,7 +12,7 @@ use vello::kurbo::{Affine, Point, Size}; use vello::peniko::BlendMode; use vello::Scene; -use crate::text::{TextBrush, TextLayout, TextStorage}; +use crate::text::{TextBrush, TextLayout}; use crate::widget::WidgetMut; use crate::{ AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, @@ -202,11 +202,7 @@ impl Widget for Label { // TODO: Parley seems to require a relayout when colours change ctx.request_layout(); } - LifeCycle::BuildFocusChain => { - if !self.text_layout.text().links().is_empty() { - tracing::warn!("Links present in text, but not yet integrated"); - } - } + LifeCycle::BuildFocusChain => {} _ => {} } } @@ -265,7 +261,7 @@ impl Widget for Label { fn accessibility(&mut self, ctx: &mut AccessCtx) { ctx.current_node() - .set_name(self.text().as_str().to_string()); + .set_name(self.text().as_ref().to_string()); } fn skip_pointer(&self) -> bool { @@ -281,7 +277,7 @@ impl Widget for Label { } fn get_debug_text(&self) -> Option { - Some(self.text_layout.text().as_str().to_string()) + Some(self.text_layout.text().as_ref().to_string()) } } diff --git a/masonry/src/widget/prose.rs b/masonry/src/widget/prose.rs index 732a34bf3..9db68bc5f 100644 --- a/masonry/src/widget/prose.rs +++ b/masonry/src/widget/prose.rs @@ -16,7 +16,7 @@ use vello::{ use crate::widget::{LineBreaking, WidgetMut}; use crate::{ - text::{TextBrush, TextStorage, TextWithSelection}, + text::{TextBrush, TextWithSelection}, widget::label::LABEL_X_PADDING, AccessCtx, AccessEvent, ArcStr, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId, @@ -223,10 +223,7 @@ impl Widget for Prose { ctx.request_layout(); } LifeCycle::BuildFocusChain => { - // TODO: This is *definitely* empty - if !self.text_layout.text().links().is_empty() { - tracing::warn!("Links present in text, but not yet integrated"); - } + // When we add links to `Prose`, they will probably need to be handled here. } _ => {} } @@ -287,7 +284,7 @@ impl Widget for Prose { fn accessibility(&mut self, ctx: &mut AccessCtx) { ctx.current_node() - .set_name(self.text().as_str().to_string()); + .set_name(self.text().as_ref().to_string()); } fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { @@ -299,6 +296,6 @@ impl Widget for Prose { } fn get_debug_text(&self) -> Option { - Some(self.text_layout.text().as_str().chars().take(100).collect()) + Some(self.text_layout.text().as_ref().chars().take(100).collect()) } } diff --git a/masonry/src/widget/textbox.rs b/masonry/src/widget/textbox.rs index 65022b2ca..50da2164f 100644 --- a/masonry/src/widget/textbox.rs +++ b/masonry/src/widget/textbox.rs @@ -18,7 +18,7 @@ use winit::event::Ime; use crate::widget::{LineBreaking, WidgetMut}; use crate::{ dpi::{LogicalPosition, LogicalSize}, - text::{TextBrush, TextEditor, TextStorage, TextWithSelection}, + text::{TextBrush, TextEditor, TextWithSelection}, AccessCtx, AccessEvent, BoxConstraints, CursorIcon, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, PointerEvent, StatusChange, TextEvent, Widget, WidgetId, }; @@ -45,7 +45,7 @@ pub struct Textbox { // We might change this to a rope based structure at some point. // If you need a text box which uses a different text type, you should // create a custom widget - editor: TextEditor, + editor: TextEditor, line_break_mode: LineBreaking, show_disabled: bool, brush: TextBrush, @@ -232,11 +232,9 @@ impl Widget for Textbox { StatusChange::FocusChanged(false) => { self.editor.focus_lost(); ctx.request_layout(); - // TODO: Stop focusing on any links } StatusChange::FocusChanged(true) => { self.editor.focus_gained(); - // TODO: Focus on first link ctx.request_layout(); } _ => {} @@ -258,10 +256,6 @@ impl Widget for Textbox { } LifeCycle::BuildFocusChain => { ctx.register_for_focus(); - // TODO: This will always be empty - if !self.editor.text().links().is_empty() { - tracing::warn!("Links present in text, but not yet integrated"); - } } _ => {} } @@ -367,6 +361,6 @@ impl Widget for Textbox { } fn get_debug_text(&self) -> Option { - Some(self.editor.text().as_str().chars().take(100).collect()) + Some(self.editor.text().chars().take(100).collect()) } } diff --git a/masonry/src/widget/variable_label.rs b/masonry/src/widget/variable_label.rs index 20a8569f1..23f919726 100644 --- a/masonry/src/widget/variable_label.rs +++ b/masonry/src/widget/variable_label.rs @@ -15,7 +15,7 @@ use vello::kurbo::{Affine, Point, Size}; use vello::peniko::BlendMode; use vello::Scene; -use crate::text::{Hinting, TextBrush, TextLayout, TextStorage}; +use crate::text::{Hinting, TextBrush, TextLayout}; use crate::widget::WidgetMut; use crate::{ AccessCtx, AccessEvent, ArcStr, BoxConstraints, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, @@ -327,11 +327,6 @@ impl Widget for VariableLabel { // TODO: Parley seems to require a relayout when colours change ctx.request_layout(); } - LifeCycle::BuildFocusChain => { - if !self.text_layout.text().links().is_empty() { - tracing::warn!("Links present in text, but not yet integrated"); - } - } LifeCycle::AnimFrame(time) => { let millis = (*time as f64 / 1_000_000.) as f32; let result = self.weight.advance(millis); @@ -410,7 +405,7 @@ impl Widget for VariableLabel { fn accessibility(&mut self, ctx: &mut AccessCtx) { ctx.current_node() - .set_name(self.text().as_str().to_string()); + .set_name(self.text().as_ref().to_string()); } fn children_ids(&self) -> SmallVec<[WidgetId; 16]> { @@ -422,7 +417,7 @@ impl Widget for VariableLabel { } fn get_debug_text(&self) -> Option { - Some(self.text_layout.text().as_str().to_string()) + Some(self.text_layout.text().as_ref().to_string()) } }