Remove masonry/src/text/reference (#665)

I'm pretty sure no-one has consulted these files in months. They're dead
code, and there's always source control if someone really needs them.
This commit is contained in:
Olivier FAURE 2024-10-09 15:23:27 +02:00 committed by GitHub
parent e9efc8cb18
commit 6589ac343d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 0 additions and 3223 deletions

View File

@ -1,6 +0,0 @@
# Old Text code
This folder contains code which was previously used for handling text in Masonry and Druid.
This is provided for reference.
The expectation is that this will be adapted into the versions in `text2`, then this will be deleted and `text2` will be renamed to text.

View File

@ -1,389 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Text attributes and spans.
use std::ops::Range;
use parley::{
fontique::{Style, Weight},
style::{FontFamily, StyleProperty},
};
use vello::peniko::{Brush, Color};
use super::font_descriptor::FontDescriptor;
// TODO - Should also hold an associated Command, maybe.
/// A clickable range of text
#[derive(Debug, Clone)]
pub struct Link {
/// The range of text for the link.
pub range: Range<usize>,
}
/// A collection of spans of attributes of various kinds.
#[derive(Debug, Clone, Default)]
pub struct AttributeSpans {
family: SpanSet<FontFamily<'static>>,
size: SpanSet<f32>,
weight: SpanSet<Weight>,
fg_color: SpanSet<Color>,
style: SpanSet<Style>,
underline: SpanSet<bool>,
font_descriptor: SpanSet<FontDescriptor>,
}
/// A set of spans for a given attribute.
///
/// Invariant: the spans are sorted and non-overlapping.
#[derive(Debug, Clone)]
struct SpanSet<T> {
spans: Vec<Span<T>>,
}
/// An attribute and a range.
///
/// This is used to represent text attributes of various kinds,
/// with the range representing a region of some text buffer.
#[derive(Debug, Clone, PartialEq)]
struct Span<T> {
range: Range<usize>,
attr: T,
}
/// Attributes that can be applied to text.
///
/// Where possible, attributes are [`KeyOrValue`] types; this means you
/// can use items defined in the [`theme`] *or* concrete types, where appropriate.
///
/// The easiest way to construct these attributes is via the various constructor
/// methods, such as [`Attribute::size`] or [`Attribute::text_color`].
///
/// # Examples
///
/// ```no_run
/// use masonry::text::Attribute;
/// use masonry::{theme, Color};
///
/// let font = Attribute::font_descriptor(theme::UI_FONT);
/// let font_size = Attribute::size(32.0);
/// let explicit_color = Attribute::text_color(Color::BLACK);
/// let theme_color = Attribute::text_color(theme::SELECTION_TEXT_COLOR);
/// ```
///
/// [`KeyOrValue`]: ../enum.KeyOrValue.html
/// [`theme`]: ../theme
/// [`Attribute::size`]: #method.size
/// [`Attribute::text_color`]: #method.text_color
#[derive(Debug, Clone)]
pub enum Attribute {
/// The font family.
FontFamily(FontFamily<'static>),
/// The font size, in points.
FontSize(f32),
/// The [`FontWeight`](struct.FontWeight.html).
Weight(Weight),
/// The foreground color of the text.
TextColor(Color),
/// The [`FontStyle`]; either regular or italic.
///
/// [`FontStyle`]: enum.FontStyle.html
Style(Style),
/// Underline.
Underline(bool),
/// A [`FontDescriptor`](struct.FontDescriptor.html).
Descriptor(FontDescriptor),
}
impl Link {
/// Create a new `Link`.
pub fn new(range: Range<usize>) -> Self {
Self { range }
}
/// Get this `Link`'s range.
pub fn range(&self) -> Range<usize> {
self.range.clone()
}
}
impl AttributeSpans {
/// Create a new, empty `AttributeSpans`.
pub fn new() -> Self {
Default::default()
}
/// Add a new [`Attribute`] over the provided [`Range`].
pub fn add(&mut self, range: Range<usize>, attr: Attribute) {
match attr {
Attribute::FontFamily(attr) => self.family.add(Span::new(range, attr)),
Attribute::FontSize(attr) => self.size.add(Span::new(range, attr)),
Attribute::Weight(attr) => self.weight.add(Span::new(range, attr)),
Attribute::TextColor(attr) => self.fg_color.add(Span::new(range, attr)),
Attribute::Style(attr) => self.style.add(Span::new(range, attr)),
Attribute::Underline(attr) => self.underline.add(Span::new(range, attr)),
Attribute::Descriptor(attr) => self.font_descriptor.add(Span::new(range, attr)),
}
}
pub(crate) fn to_piet_attrs(&self) -> Vec<(Range<usize>, StyleProperty<'static, Brush>)> {
let mut items = Vec::new();
for Span { range, attr } in self.font_descriptor.iter() {
items.push((
range.clone(),
StyleProperty::FontStack(parley::style::FontStack::Single(attr.family.clone())),
));
items.push((range.clone(), StyleProperty::FontSize(attr.size)));
items.push((range.clone(), StyleProperty::FontWeight(attr.weight)));
items.push((range.clone(), StyleProperty::FontStyle(attr.style)));
}
items.extend(self.family.iter().map(|s| {
(
s.range.clone(),
StyleProperty::FontStack(parley::style::FontStack::Single(s.attr.clone())),
)
}));
items.extend(
self.size
.iter()
.map(|s| (s.range.clone(), StyleProperty::FontSize(s.attr))),
);
items.extend(
self.weight
.iter()
.map(|s| (s.range.clone(), StyleProperty::FontWeight(s.attr))),
);
items.extend(
self.fg_color
.iter()
.map(|s| (s.range.clone(), StyleProperty::Brush(Brush::Solid(s.attr)))),
);
items.extend(
self.style
.iter()
.map(|s| (s.range.clone(), StyleProperty::FontStyle(s.attr))),
);
items.extend(
self.underline
.iter()
.map(|s| (s.range.clone(), StyleProperty::Underline(s.attr))),
);
// sort by ascending start order; this is a stable sort
// so items that come from FontDescriptor will stay at the front
items.sort_by(|a, b| a.0.start.cmp(&b.0.start));
items
}
}
impl<T: Clone> SpanSet<T> {
fn iter(&self) -> impl Iterator<Item = &Span<T>> {
self.spans.iter()
}
/// Add a `Span` to this `SpanSet`.
///
/// Spans can be added in any order. existing spans will be updated
/// as required.
fn add(&mut self, span: Span<T>) {
let span_start = span.range.start;
let span_end = span.range.end;
let insert_idx = self
.spans
.iter()
.position(|x| x.range.start >= span.range.start)
.unwrap_or_else(|| self.spans.len());
// if we are inserting into the middle of an existing span we need
// to add the trailing portion back afterwards.
let mut prev_remainder = None;
if insert_idx > 0 {
// truncate the preceding item, if necessary
let before = self.spans.get_mut(insert_idx - 1).unwrap();
if before.range.end > span_end {
let mut remainder = before.clone();
remainder.range.start = span_end;
prev_remainder = Some(remainder);
}
before.range.end = before.range.end.min(span_start);
}
self.spans.insert(insert_idx, span);
if let Some(remainder) = prev_remainder.take() {
self.spans.insert(insert_idx + 1, remainder);
}
// clip any existing spans as needed
for after in self.spans.iter_mut().skip(insert_idx + 1) {
after.range.start = after.range.start.max(span_end);
after.range.end = after.range.end.max(span_end);
}
// remove any spans that have been overwritten
self.spans.retain(|span| !span.is_empty());
}
/// Edit the spans, inserting empty space into the changed region if needed.
///
/// This is used to keep the spans up to date as edits occur in the buffer.
///
/// `changed` is the range of the string that has been replaced; this can
/// be an empty range (eg, 10..10) for the insertion case.
///
/// `new_len` is the length of the inserted text.
//TODO: we could be smarter here about just extending the existing spans
//as required for insertions in the interior of a span.
//TODO: this isn't currently used; it should be used if we use spans with
//some editable type.
// the branches are much more readable without sharing code
#[allow(dead_code, clippy::branches_sharing_code)]
fn edit(&mut self, changed: Range<usize>, new_len: usize) {
let old_len = changed.len();
let mut to_insert = None;
for (idx, Span { range, attr }) in self.spans.iter_mut().enumerate() {
if range.end <= changed.start {
continue;
} else if range.start < changed.start {
// we start before but end inside; truncate end
if range.end <= changed.end {
range.end = changed.start;
// we start before and end after; this is a special case,
// we'll need to add a new span
} else {
let new_start = changed.start + new_len;
let new_end = range.end - old_len + new_len;
let new_span = Span::new(new_start..new_end, attr.clone());
to_insert = Some((idx + 1, new_span));
range.end = changed.start;
}
// start inside
} else if range.start < changed.end {
range.start = changed.start + new_len;
// end inside; collapse
if range.end <= changed.end {
range.end = changed.start + new_len;
// end outside: adjust by length delta
} else {
range.end -= old_len;
range.end += new_len;
}
// whole range is after:
} else {
range.start -= old_len;
range.start += new_len;
range.end -= old_len;
range.end += new_len;
}
}
if let Some((idx, span)) = to_insert.take() {
self.spans.insert(idx, span);
}
self.spans.retain(|span| !span.is_empty());
}
}
impl<T> Span<T> {
fn new(range: Range<usize>, attr: T) -> Self {
Span { range, attr }
}
fn is_empty(&self) -> bool {
self.range.end <= self.range.start
}
}
impl Attribute {
/// Create a new font size attribute.
pub fn size(size: impl Into<f32>) -> Self {
Attribute::FontSize(size.into())
}
/// Create a new foreground color attribute.
pub fn text_color(color: impl Into<Color>) -> Self {
Attribute::TextColor(color.into())
}
/// Create a new font family attribute.
pub fn font_family(family: FontFamily) -> Self {
Attribute::FontFamily(family)
}
/// Create a new `FontWeight` attribute.
pub fn weight(weight: Weight) -> Self {
Attribute::Weight(weight)
}
/// Create a new `FontStyle` attribute.
pub fn style(style: Style) -> Self {
Attribute::Style(style)
}
/// Create a new underline attribute.
pub fn underline(underline: bool) -> Self {
Attribute::Underline(underline)
}
/// Create a new `FontDescriptor` attribute.
pub fn font_descriptor(font: impl Into<FontDescriptor>) -> Self {
Attribute::Descriptor(font.into())
}
}
impl<T> Default for SpanSet<T> {
fn default() -> Self {
SpanSet { spans: Vec::new() }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn smoke_test_spans() {
let mut spans = SpanSet::<u32>::default();
spans.add(Span::new(2..10, 1));
spans.add(Span::new(3..6, 2));
assert_eq!(
&spans.spans,
&vec![Span::new(2..3, 1), Span::new(3..6, 2), Span::new(6..10, 1)]
);
spans.add(Span::new(0..12, 3));
assert_eq!(&spans.spans, &vec![Span::new(0..12, 3)]);
spans.add(Span::new(5..20, 4));
assert_eq!(&spans.spans, &vec![Span::new(0..5, 3), Span::new(5..20, 4)]);
}
#[test]
fn edit_spans() {
let mut spans = SpanSet::<u32>::default();
spans.add(Span::new(0..2, 1));
spans.add(Span::new(8..12, 2));
spans.add(Span::new(13..16, 3));
spans.add(Span::new(20..22, 4));
let mut deletion = spans.clone();
deletion.edit(6..14, 0);
assert_eq!(
&deletion.spans,
&vec![Span::new(0..2, 1), Span::new(6..8, 3), Span::new(12..14, 4)]
);
spans.edit(10..10, 2);
assert_eq!(
&spans.spans,
&vec![
Span::new(0..2, 1),
Span::new(8..10, 2),
Span::new(12..14, 2),
Span::new(15..18, 3),
Span::new(22..24, 4),
]
);
}
}

View File

@ -1,521 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Traits for text editing and a basic String implementation.
use std::borrow::Cow;
use std::ops::{Deref, Range};
use std::sync::Arc;
use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation};
/// An EditableText trait.
pub trait EditableText: Sized {
// TODO: would be nice to have something like
// type Cursor: EditableTextCursor<Self>;
/// 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<StringCursor>;
/// Replace range with new text.
/// Can panic if supplied an invalid range.
// TODO: make this generic over Self
fn edit(&mut self, range: Range<usize>, new: impl Into<String>);
/// Get slice of text at range.
fn slice(&self, range: Range<usize>) -> Option<Cow<str>>;
/// Get length of text (in bytes).
fn len(&self) -> usize;
/// Get the previous word offset from the given offset, if it exists.
fn prev_word_offset(&self, offset: usize) -> Option<usize>;
/// Get the next word offset from the given offset, if it exists.
fn next_word_offset(&self, offset: usize) -> Option<usize>;
/// Get the next grapheme offset from the given offset, if it exists.
fn prev_grapheme_offset(&self, offset: usize) -> Option<usize>;
/// Get the next grapheme offset from the given offset, if it exists.
fn next_grapheme_offset(&self, offset: usize) -> Option<usize>;
/// Get the previous codepoint offset from the given offset, if it exists.
fn prev_codepoint_offset(&self, offset: usize) -> Option<usize>;
/// Get the next codepoint offset from the given offset, if it exists.
fn next_codepoint_offset(&self, offset: usize) -> Option<usize>;
/// Get the preceding line break offset from the given offset
fn preceding_line_break(&self, offset: usize) -> usize;
/// Get the next line break offset from the given offset
fn next_line_break(&self, offset: usize) -> usize;
/// Returns `true` if this text has 0 length.
fn is_empty(&self) -> bool;
/// Construct an instance of this type from a `&str`.
fn from_str(s: &str) -> Self;
}
impl EditableText for String {
fn cursor<'a>(&self, position: usize) -> Option<StringCursor> {
let new_cursor = StringCursor {
text: self,
position,
};
if new_cursor.is_boundary() {
Some(new_cursor)
} else {
None
}
}
fn edit(&mut self, range: Range<usize>, new: impl Into<String>) {
self.replace_range(range, &new.into());
}
fn slice(&self, range: Range<usize>) -> Option<Cow<str>> {
self.get(range).map(Cow::from)
}
fn len(&self) -> usize {
self.len()
}
fn prev_grapheme_offset(&self, from: usize) -> Option<usize> {
let mut c = GraphemeCursor::new(from, self.len(), true);
c.prev_boundary(self, 0).unwrap()
}
fn next_grapheme_offset(&self, from: usize) -> Option<usize> {
let mut c = GraphemeCursor::new(from, self.len(), true);
c.next_boundary(self, 0).unwrap()
}
fn prev_codepoint_offset(&self, from: usize) -> Option<usize> {
let mut c = self.cursor(from).unwrap();
c.prev()
}
fn next_codepoint_offset(&self, from: usize) -> Option<usize> {
let mut c = self.cursor(from).unwrap();
if c.next().is_some() {
Some(c.pos())
} else {
None
}
}
fn prev_word_offset(&self, from: usize) -> Option<usize> {
let mut offset = from;
let mut passed_alphanumeric = false;
for prev_grapheme in self.get(0..from)?.graphemes(true).rev() {
let is_alphanumeric = prev_grapheme.chars().next()?.is_alphanumeric();
if is_alphanumeric {
passed_alphanumeric = true;
} else if passed_alphanumeric {
return Some(offset);
}
offset -= prev_grapheme.len();
}
None
}
fn next_word_offset(&self, from: usize) -> Option<usize> {
let mut offset = from;
let mut passed_alphanumeric = false;
for next_grapheme in self.get(from..)?.graphemes(true) {
let is_alphanumeric = next_grapheme.chars().next()?.is_alphanumeric();
if is_alphanumeric {
passed_alphanumeric = true;
} else if passed_alphanumeric {
return Some(offset);
}
offset += next_grapheme.len();
}
Some(self.len())
}
fn is_empty(&self) -> bool {
self.is_empty()
}
fn from_str(s: &str) -> Self {
s.to_string()
}
fn preceding_line_break(&self, from: usize) -> usize {
let mut offset = from;
for byte in self.get(0..from).unwrap_or("").bytes().rev() {
if byte == 0x0a {
return offset;
}
offset -= 1;
}
0
}
fn next_line_break(&self, from: usize) -> usize {
let mut offset = from;
for char in self.get(from..).unwrap_or("").bytes() {
if char == 0x0a {
return offset;
}
offset += 1;
}
self.len()
}
}
impl EditableText for Arc<String> {
fn cursor(&self, position: usize) -> Option<StringCursor> {
<String as EditableText>::cursor(self, position)
}
fn edit(&mut self, range: Range<usize>, new: impl Into<String>) {
let new = new.into();
if !range.is_empty() || !new.is_empty() {
Arc::make_mut(self).edit(range, new)
}
}
fn slice(&self, range: Range<usize>) -> Option<Cow<str>> {
Some(Cow::Borrowed(&self[range]))
}
fn len(&self) -> usize {
self.deref().len()
}
fn prev_word_offset(&self, offset: usize) -> Option<usize> {
self.deref().prev_word_offset(offset)
}
fn next_word_offset(&self, offset: usize) -> Option<usize> {
self.deref().next_word_offset(offset)
}
fn prev_grapheme_offset(&self, offset: usize) -> Option<usize> {
self.deref().prev_grapheme_offset(offset)
}
fn next_grapheme_offset(&self, offset: usize) -> Option<usize> {
self.deref().next_grapheme_offset(offset)
}
fn prev_codepoint_offset(&self, offset: usize) -> Option<usize> {
self.deref().prev_codepoint_offset(offset)
}
fn next_codepoint_offset(&self, offset: usize) -> Option<usize> {
self.deref().next_codepoint_offset(offset)
}
fn preceding_line_break(&self, offset: usize) -> usize {
self.deref().preceding_line_break(offset)
}
fn next_line_break(&self, offset: usize) -> usize {
self.deref().next_line_break(offset)
}
fn is_empty(&self) -> bool {
self.deref().is_empty()
}
fn from_str(s: &str) -> Self {
Arc::new(s.to_owned())
}
}
/// 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<usize>;
/// Move cursor to next codepoint boundary, if it exists.
/// Returns current codepoint as usize offset.
fn next(&mut self) -> Option<usize>;
/// Get the next codepoint after the cursor position, without advancing
/// the cursor.
fn peek_next_codepoint(&self) -> Option<char>;
/// Return codepoint preceding cursor offset and move cursor backward.
fn prev_codepoint(&mut self) -> Option<char>;
/// Return codepoint at cursor offset and move cursor forward.
fn next_codepoint(&mut self) -> Option<char>;
/// Return current offset if it's a boundary, else next.
fn at_or_next(&mut self) -> Option<usize>;
/// Return current offset if it's a boundary, else previous.
fn at_or_prev(&mut self) -> Option<usize>;
}
/// A cursor type that implements EditableTextCursor for String
#[derive(Debug)]
pub struct StringCursor<'a> {
text: &'a str,
position: usize,
}
impl<'a> EditableTextCursor<&'a String> for StringCursor<'a> {
fn set(&mut self, position: usize) {
self.position = position;
}
fn pos(&self) -> usize {
self.position
}
fn is_boundary(&self) -> bool {
self.text.is_char_boundary(self.position)
}
fn prev(&mut self) -> Option<usize> {
let current_pos = self.pos();
if current_pos == 0 {
None
} else {
let mut len = 1;
while !self.text.is_char_boundary(current_pos - len) {
len += 1;
}
self.set(self.pos() - len);
Some(self.pos())
}
}
fn next(&mut self) -> Option<usize> {
let current_pos = self.pos();
if current_pos == self.text.len() {
None
} else {
let b = self.text.as_bytes()[current_pos];
self.set(current_pos + len_utf8_from_first_byte(b));
Some(current_pos)
}
}
fn peek_next_codepoint(&self) -> Option<char> {
self.text[self.pos()..].chars().next()
}
fn prev_codepoint(&mut self) -> Option<char> {
if let Some(prev) = self.prev() {
self.text[prev..].chars().next()
} else {
None
}
}
fn next_codepoint(&mut self) -> Option<char> {
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<usize> {
if self.is_boundary() {
Some(self.pos())
} else {
self.next()
}
}
fn at_or_prev(&mut self) -> Option<usize> {
if self.is_boundary() {
Some(self.pos())
} else {
self.prev()
}
}
}
pub fn len_utf8_from_first_byte(b: u8) -> usize {
match b {
b if b < 0x80 => 1,
b if b < 0xe0 => 2,
b if b < 0xf0 => 3,
_ => 4,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn replace() {
let mut a = String::from("hello world");
a.edit(1..9, "era");
assert_eq!("herald", a);
}
#[test]
fn prev_codepoint_offset() {
let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}");
assert_eq!(Some(6), a.prev_codepoint_offset(10));
assert_eq!(Some(3), a.prev_codepoint_offset(6));
assert_eq!(Some(1), a.prev_codepoint_offset(3));
assert_eq!(Some(0), a.prev_codepoint_offset(1));
assert_eq!(None, a.prev_codepoint_offset(0));
let b = a.slice(1..10).unwrap().to_string();
assert_eq!(Some(5), b.prev_codepoint_offset(9));
assert_eq!(Some(2), b.prev_codepoint_offset(5));
assert_eq!(Some(0), b.prev_codepoint_offset(2));
assert_eq!(None, b.prev_codepoint_offset(0));
}
#[test]
fn next_codepoint_offset() {
let a = String::from("a\u{00A1}\u{4E00}\u{1F4A9}");
assert_eq!(Some(10), a.next_codepoint_offset(6));
assert_eq!(Some(6), a.next_codepoint_offset(3));
assert_eq!(Some(3), a.next_codepoint_offset(1));
assert_eq!(Some(1), a.next_codepoint_offset(0));
assert_eq!(None, a.next_codepoint_offset(10));
let b = a.slice(1..10).unwrap().to_string();
assert_eq!(Some(9), b.next_codepoint_offset(5));
assert_eq!(Some(5), b.next_codepoint_offset(2));
assert_eq!(Some(2), b.next_codepoint_offset(0));
assert_eq!(None, b.next_codepoint_offset(9));
}
#[test]
fn prev_next() {
let input = String::from("abc");
let mut cursor = input.cursor(0).unwrap();
assert_eq!(cursor.next(), Some(0));
assert_eq!(cursor.next(), Some(1));
assert_eq!(cursor.prev(), Some(1));
assert_eq!(cursor.next(), Some(1));
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"
let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}");
assert_eq!(Some(9), a.prev_grapheme_offset(17));
assert_eq!(Some(3), a.prev_grapheme_offset(9));
assert_eq!(Some(0), a.prev_grapheme_offset(3));
assert_eq!(None, a.prev_grapheme_offset(0));
}
#[test]
fn next_grapheme_offset() {
// A with ring, hangul, regional indicator "US"
let a = String::from("A\u{030a}\u{110b}\u{1161}\u{1f1fa}\u{1f1f8}");
assert_eq!(Some(3), a.next_grapheme_offset(0));
assert_eq!(Some(9), a.next_grapheme_offset(3));
assert_eq!(Some(17), a.next_grapheme_offset(9));
assert_eq!(None, a.next_grapheme_offset(17));
}
#[test]
fn prev_word_offset() {
let a = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}");
assert_eq!(Some(20), a.prev_word_offset(35));
assert_eq!(Some(20), a.prev_word_offset(27));
assert_eq!(Some(20), a.prev_word_offset(23));
assert_eq!(Some(14), a.prev_word_offset(20));
assert_eq!(Some(14), a.prev_word_offset(19));
assert_eq!(Some(12), a.prev_word_offset(13));
assert_eq!(None, a.prev_word_offset(12));
assert_eq!(None, a.prev_word_offset(11));
assert_eq!(None, a.prev_word_offset(0));
}
#[test]
fn next_word_offset() {
let a = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}");
assert_eq!(Some(11), a.next_word_offset(0));
assert_eq!(Some(11), a.next_word_offset(7));
assert_eq!(Some(13), a.next_word_offset(11));
assert_eq!(Some(18), a.next_word_offset(14));
assert_eq!(Some(35), a.next_word_offset(18));
assert_eq!(Some(35), a.next_word_offset(19));
assert_eq!(Some(35), a.next_word_offset(20));
assert_eq!(Some(35), a.next_word_offset(26));
assert_eq!(Some(35), a.next_word_offset(35));
}
#[test]
fn preceding_line_break() {
let a = String::from("Technically\na word:\n ৬藏A\u{030a}\n\u{110b}\u{1161}");
assert_eq!(0, a.preceding_line_break(0));
assert_eq!(0, a.preceding_line_break(11));
assert_eq!(12, a.preceding_line_break(12));
assert_eq!(12, a.preceding_line_break(13));
assert_eq!(20, a.preceding_line_break(21));
assert_eq!(31, a.preceding_line_break(31));
assert_eq!(31, a.preceding_line_break(34));
let b = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}");
assert_eq!(0, b.preceding_line_break(0));
assert_eq!(0, b.preceding_line_break(11));
assert_eq!(0, b.preceding_line_break(13));
assert_eq!(0, b.preceding_line_break(21));
}
#[test]
fn next_line_break() {
let a = String::from("Technically\na word:\n ৬藏A\u{030a}\n\u{110b}\u{1161}");
assert_eq!(11, a.next_line_break(0));
assert_eq!(11, a.next_line_break(11));
assert_eq!(19, a.next_line_break(13));
assert_eq!(30, a.next_line_break(21));
assert_eq!(a.len(), a.next_line_break(31));
let b = String::from("Technically a word: ৬藏A\u{030a}\u{110b}\u{1161}");
assert_eq!(b.len(), b.next_line_break(0));
assert_eq!(b.len(), b.next_line_break(11));
assert_eq!(b.len(), b.next_line_break(13));
assert_eq!(b.len(), b.next_line_break(19));
}
#[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));
}
}

View File

@ -1,72 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Font attributes
use parley::{
fontique::{Style, Weight},
style::FontFamily,
};
/// A collection of attributes that describe a font.
///
/// This is provided as a convenience; library consumers may wish to have
/// a single type that represents a specific font face at a specific size.
#[derive(Debug, Clone, PartialEq)]
pub struct FontDescriptor {
/// The font's [`FontFamily`](struct.FontFamily.html).
pub family: FontFamily<'static>,
/// The font's size.
pub size: f32,
/// The font's [`FontWeight`](struct.FontWeight.html).
pub weight: Weight,
/// The font's [`FontStyle`](struct.FontStyle.html).
pub style: Style,
}
impl FontDescriptor {
/// Create a new descriptor with the provided [`FontFamily`].
///
/// [`FontFamily`]: struct.FontFamily.html
pub const fn new(family: FontFamily<'static>) -> Self {
FontDescriptor {
family,
size: 12.,
weight: Weight::NORMAL,
style: Style::Normal,
}
}
/// Builder-style method to set the descriptor's font size.
pub const fn with_size(mut self, size: f32) -> Self {
self.size = size;
self
}
/// Builder-style method to set the descriptor's [`FontWeight`].
///
/// [`FontWeight`]: struct.FontWeight.html
pub const fn with_weight(mut self, weight: Weight) -> Self {
self.weight = weight;
self
}
/// Builder-style method to set the descriptor's [`FontStyle`].
///
/// [`FontStyle`]: enum.FontStyle.html
pub const fn with_style(mut self, style: Style) -> Self {
self.style = style;
self
}
}
impl Default for FontDescriptor {
fn default() -> Self {
FontDescriptor {
family: FontFamily::Generic(parley::style::GenericFamily::SystemUi),
weight: Default::default(),
style: Default::default(),
size: 12.,
}
}
}

View File

@ -1,750 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! A widget component that integrates with the platform text system.
use std::cell::{Cell, Ref, RefCell, RefMut};
use std::ops::Range;
use std::sync::{Arc, Weak};
use kurbo::{Line, Point, Vec2};
use parley::layout::Alignment;
use winit::event::Modifiers;
use super::backspace::offset_for_delete_backwards;
use super::editable_text::EditableText;
use super::layout::TextLayout;
use super::shell_text::{Action, Direction, Movement, Selection};
use super::storage::TextStorage;
use crate::text;
/// A widget that accepts text input.
///
/// This is intended to be used as a component of other widgets.
///
/// Text input is more complicated than you think, probably. For a good
/// overview, see [`druid_shell::text`].
///
/// This type manages an inner [`EditSession`] that is shared with the platform.
/// Unlike other aspects of Masonry, the platform interacts with this session, not
/// through discrete events.
///
/// This is managed through a simple 'locking' mechanism; the platform asks for
/// a lock on a particular text session that it wishes to interact with, calls
/// methods on the locked session, and then later releases the lock.
///
/// Importantly, *other events may be received while the lock is held*.
///
/// It is the responsibility of the user of this widget to ensure that the
/// session is not locked before it is accessed. This can be done by checking
/// [`TextComponent::can_read`] and [`TextComponent::can_write`];
/// after checking these methods the inner session can be accessed via
/// [`TextComponent::borrow`] and [`TextComponent::borrow_mut`].
///
/// Semantically, this functions like a `RefCell`; attempting to borrow while
/// a lock is held will result in a panic.
#[derive(Debug, Clone)]
pub struct TextComponent<T> {
edit_session: Arc<RefCell<EditSession<T>>>,
lock: Arc<Cell<ImeLock>>,
// HACK: because of the way focus works (it is managed higher up, in
// whatever widget is controlling this) we can't rely on `is_focused` in
// the PaintCtx.
/// A manual flag set by the parent to control drawing behaviour.
///
/// The parent should update this when handling [`StatusChange::FocusChanged`].
pub has_focus: bool,
}
// crate::declare_widget!(
// TextComponentMut,
// TextComponent<T: (TextStorage + EditableText)>
// );
/// Editable text state.
///
/// This is the inner state of a [`TextComponent`]. It should only be accessed
/// through its containing [`TextComponent`], or by the platform through an
/// [`ImeHandlerRef`] created by [`TextComponent::input_handler`].
#[derive(Debug, Clone)]
pub struct EditSession<T> {
/// The inner [`TextLayout`] object.
///
/// This is exposed so that users can do things like set text properties;
/// you should avoid doing things like rebuilding this layout manually, or
/// setting the text directly.
pub layout: TextLayout<T>,
/// If the platform modifies the text, this contains the new text;
/// we update the app `Data` with this text on the next update pass.
external_text_change: Option<T>,
external_selection_change: Option<Selection>,
external_scroll_to: Option<bool>,
// external_action: Option<Action>,
/// A flag set in `update` if the text has changed from a non-IME source.
// pending_ime_invalidation: Option<ImeInvalidation>,
/// If `true`, the component will send the [`TextComponent::RETURN`]
/// notification when the user enters a newline.
pub send_notification_on_return: bool,
/// If `true`, the component will send the [`TextComponent::CANCEL`]
/// notification when the user cancels editing.
pub send_notification_on_cancel: bool,
selection: Selection,
accepts_newlines: bool,
accepts_tabs: bool,
alignment: Alignment,
/// The y-position of the text when it does not fill our width.
alignment_offset: f64,
/// The portion of the text that is currently marked by the IME.
composition_range: Option<Range<usize>>,
drag_granularity: DragGranularity,
/// The origin of the textbox, relative to the origin of the window.
pub origin: Point,
}
/// An object that can be used to acquire an `ImeHandler`.
///
/// This does not own the session; when the widget that owns the session
/// is dropped, this will become invalid.
#[derive(Debug, Clone)]
struct EditSessionRef<T> {
inner: Weak<RefCell<EditSession<T>>>,
lock: Arc<Cell<ImeLock>>,
}
/// A locked handle to an [`EditSession`].
///
/// This type implements [`InputHandler`]; it is the type that we pass to the
/// platform.
struct EditSessionHandle<T> {
text: T,
inner: Arc<RefCell<EditSession<T>>>,
}
/// When a drag follows a double- or triple-click, the behaviour of
/// drag changes to only select whole words or whole paragraphs.
#[derive(Debug, Clone, Copy, PartialEq)]
enum DragGranularity {
Grapheme,
/// Start and end are the start/end bounds of the initial selection.
Word {
start: usize,
end: usize,
},
/// Start and end are the start/end bounds of the initial selection.
Paragraph {
start: usize,
end: usize,
},
}
/// An informal lock.
#[derive(Debug, Clone, Copy, PartialEq)]
enum ImeLock {
None,
ReadWrite,
Read,
}
impl<T> TextComponent<T> {
/// Returns `true` if the inner [`EditSession`] can be read.
pub fn can_read(&self) -> bool {
self.lock.get() != ImeLock::ReadWrite
}
/// Returns `true` if the inner [`EditSession`] can be mutated.
pub fn can_write(&self) -> bool {
self.lock.get() == ImeLock::None
}
/// Returns `true` if the IME is actively composing (or the text is locked.)
///
/// When text is composing, you should avoid doing things like modifying the
/// selection or copy/pasting text.
pub fn is_composing(&self) -> bool {
self.can_read() && self.borrow().composition_range.is_some()
}
/// Attempt to mutably borrow the inner [`EditSession`].
///
/// # Panics
///
/// This method panics if there is an outstanding lock on the session.
pub fn borrow_mut(&self) -> RefMut<'_, EditSession<T>> {
assert!(self.can_write());
self.edit_session.borrow_mut()
}
/// Attempt to borrow the inner [`EditSession`].
///
/// # Panics
///
/// This method panics if there is an outstanding write lock on the session.
pub fn borrow(&self) -> Ref<'_, EditSession<T>> {
assert!(self.can_read());
self.edit_session.borrow()
}
}
impl<T: TextStorage + EditableText> TextComponentMut<'_, T> {
pub fn set_text(&mut self, new_text: impl Into<T>) {
let new_text = new_text.into();
// TODO - use '==' instead
let needs_rebuild = self
.widget
.borrow()
.layout
.text()
.map(|old| !old.maybe_eq(&new_text))
.unwrap_or(true);
if needs_rebuild {
self.widget.borrow_mut().layout.set_text(new_text.clone());
self.ctx.request_layout();
}
}
pub fn set_focused(&mut self, focused: bool) {
self.widget.has_focus = focused;
self.ctx.request_paint();
}
}
// impl<T: TextStorage + EditableText> Widget for TextComponent<T> {
// fn on_event(&mut self, ctx: &mut EventCtx, event: &Event) {
// match event {
// Event::MouseDown(mouse) if self.can_write() && !ctx.is_disabled() => {
// ctx.set_active(true);
// self.borrow_mut()
// .do_mouse_down(mouse.pos, mouse.mods, mouse.count);
// self.borrow_mut()
// .update_pending_invalidation(ImeInvalidation::SelectionChanged);
// ctx.request_layout();
// ctx.request_paint();
// }
// Event::MouseMove(mouse) if self.can_write() => {
// if !ctx.is_disabled() {
// ctx.set_cursor(&Cursor::IBeam);
// if ctx.is_active() {
// let pre_sel = self.borrow().selection();
// self.borrow_mut().do_drag(mouse.pos);
// if self.borrow().selection() != pre_sel {
// self.borrow_mut()
// .update_pending_invalidation(ImeInvalidation::SelectionChanged);
// ctx.request_layout();
// ctx.request_paint();
// }
// }
// } else {
// ctx.set_disabled(false);
// ctx.clear_cursor();
// }
// }
// Event::MouseUp(_) if ctx.is_active() => {
// ctx.set_active(false);
// ctx.request_paint();
// }
// Event::ImeStateChange => {
// assert!(
// self.can_write(),
// "lock release should be cause of ImeStateChange event"
// );
// let scroll_to = self.borrow_mut().take_scroll_to();
// if let Some(scroll_to) = scroll_to {
// ctx.submit_notification(TextComponent::SCROLL_TO.with(scroll_to));
// }
// let text = self.borrow_mut().take_external_text_change();
// if let Some(text) = text {
// self.borrow_mut().layout.set_text(text.clone());
// let new_text = self
// .borrow()
// .layout
// .text()
// .map(|txt| txt.as_str())
// .unwrap_or("")
// .to_string();
// ctx.submit_notification(TextComponent::TEXT_CHANGED.with(new_text));
// }
// let action = self.borrow_mut().take_external_action();
// if let Some(action) = action {
// match action {
// Action::Cancel => ctx.submit_notification(TextComponent::CANCEL),
// Action::InsertNewLine { .. } => {
// let text = self
// .borrow()
// .layout
// .text()
// .map(|txt| txt.as_str())
// .unwrap_or("")
// .to_string();
// ctx.submit_notification(TextComponent::RETURN.with(text));
// }
// Action::InsertTab { .. } => ctx.submit_notification(TextComponent::TAB),
// Action::InsertBacktab => {
// ctx.submit_notification(TextComponent::BACKTAB)
// }
// _ => tracing::warn!("unexpected external action '{:?}'", action),
// };
// }
// let selection = self.borrow_mut().take_external_selection_change();
// if let Some(selection) = selection {
// self.borrow_mut().selection = selection;
// ctx.request_paint();
// }
// ctx.request_layout();
// }
// _ => (),
// }
// }
// fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {}
// fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle) {
// match event {
// LifeCycle::WidgetAdded => {
// assert!(
// self.can_write(),
// "ime should never be locked at WidgetAdded"
// );
// self.borrow_mut().layout.rebuild_if_needed(ctx.text());
// }
// LifeCycle::DisabledChanged(disabled) => {
// if self.can_write() {
// let color = if *disabled {
// theme::DISABLED_TEXT_COLOR
// } else {
// theme::TEXT_COLOR
// };
// self.borrow_mut().layout.set_text_color(color);
// }
// ctx.request_layout();
// }
// //FIXME: this should happen in the parent too?
// LifeCycle::Internal(crate::InternalLifeCycle::ParentWindowOrigin) => {
// if self.can_write() {
// let prev_origin = self.borrow().origin;
// let new_origin = ctx.window_origin();
// if prev_origin != new_origin {
// self.borrow_mut().origin = ctx.window_origin();
// ctx.invalidate_text_input(ImeInvalidation::LayoutChanged);
// }
// }
// }
// _ => (),
// }
// }
// fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
// if !self.can_write() {
// tracing::warn!("Text layout called with IME lock held.");
// return Size::ZERO;
// }
// self.borrow_mut().layout.set_wrap_width(bc.max().width);
// self.borrow_mut().layout.rebuild_if_needed(ctx.text());
// let metrics = self.borrow().layout.layout_metrics();
// let width = if bc.max().width.is_infinite() || bc.max().width < f64::MAX {
// metrics.trailing_whitespace_width
// } else {
// metrics.size.width
// };
// let size = bc.constrain((width, metrics.size.height));
// let extra_width = if self.borrow().accepts_newlines {
// 0.0
// } else {
// (size.width - width).max(0.0)
// };
// self.borrow_mut().update_alignment_offset(extra_width);
// let baseline_off = metrics.size.height - metrics.first_baseline;
// ctx.set_baseline_offset(baseline_off);
// size
// }
// fn paint(&mut self, ctx: &mut PaintCtx) {
// if !self.can_read() {
// tracing::warn!("Text paint called with IME lock held.");
// }
// let selection_color = if self.has_focus {
// theme::SELECTED_TEXT_BACKGROUND_COLOR
// } else {
// theme::SELECTED_TEXT_INACTIVE_BACKGROUND_COLOR
// };
// let cursor_color = theme::CURSOR_COLOR;
// let text_offset = Vec2::new(self.borrow().alignment_offset, 0.0);
// let selection = self.borrow().selection();
// let composition = self.borrow().composition_range();
// let sel_rects = self.borrow().layout.rects_for_range(selection.range());
// if let Some(composition) = composition {
// // I believe selection should always be contained in composition range while composing?
// assert!(composition.start <= selection.anchor && composition.end >= selection.active);
// let comp_rects = self.borrow().layout.rects_for_range(composition);
// for region in comp_rects {
// let y = region.max_y().floor();
// let line = Line::new((region.min_x(), y), (region.max_x(), y)) + text_offset;
// ctx.stroke(line, &cursor_color, 1.0);
// }
// for region in sel_rects {
// let y = region.max_y().floor();
// let line = Line::new((region.min_x(), y), (region.max_x(), y)) + text_offset;
// ctx.stroke(line, &cursor_color, 2.0);
// }
// } else {
// for region in sel_rects {
// let rounded = (region + text_offset).to_rounded_rect(1.0);
// ctx.fill(rounded, &selection_color);
// }
// }
// self.borrow().layout.draw(ctx, text_offset.to_point());
// }
// fn children_ids(&self) -> SmallVec<[WidgetId; 16]> {
// SmallVec::new()
// }
// fn make_trace_span(&self) -> Span {
// trace_span!("TextComponent")
// }
// }
impl<T> EditSession<T> {
/// The current [`Selection`].
pub fn selection(&self) -> Selection {
self.selection
}
/// Manually set the selection.
///
/// If the new selection is different from the current selection, this
/// will return an ime event that the controlling widget should use to
/// invalidate the platform's IME state, by passing it to
/// [`EventCtx::invalidate_text_input`].
#[must_use]
pub fn set_selection(&mut self, selection: Selection) /* -> Option<ImeInvalidation> */
{
if selection != self.selection {
self.selection = selection;
// self.update_pending_invalidation(ImeInvalidation::SelectionChanged);
// Some(ImeInvalidation::SelectionChanged)
}
}
/// The range of text currently being modified by an IME.
pub fn composition_range(&self) -> Option<Range<usize>> {
self.composition_range.clone()
}
/// Sets whether or not this session will allow the insertion of newlines.
pub fn set_accepts_newlines(&mut self, accepts_newlines: bool) {
self.accepts_newlines = accepts_newlines;
}
/// Set the text alignment.
///
/// This is only meaningful for single-line text that does not fill
/// the minimum layout size.
pub fn set_text_alignment(&mut self, alignment: Alignment) {
self.alignment = alignment;
}
/// The text alignment.
pub fn text_alignment(&self) -> Alignment {
self.alignment
}
// /// Returns any invalidation action that should be passed to the platform.
// ///
// /// The user of this component *must* check this after calling `update`.
// pub fn pending_ime_invalidation(&mut self) -> Option<ImeInvalidation> {
// self.pending_ime_invalidation.take()
// }
fn take_external_text_change(&mut self) -> Option<T> {
self.external_text_change.take()
}
fn take_external_selection_change(&mut self) -> Option<Selection> {
self.external_selection_change.take()
}
fn take_scroll_to(&mut self) -> Option<bool> {
self.external_scroll_to.take()
}
// fn take_external_action(&mut self) -> Option<Action> {
// self.external_action.take()
// }
// // we don't want to replace a more aggressive invalidation with a less aggressive one.
// fn update_pending_invalidation(&mut self, new_invalidation: ImeInvalidation) {
// self.pending_ime_invalidation = match self.pending_ime_invalidation.take() {
// None => Some(new_invalidation),
// Some(prev) => match (prev, new_invalidation) {
// (ImeInvalidation::SelectionChanged, ImeInvalidation::SelectionChanged) => {
// ImeInvalidation::SelectionChanged
// }
// (ImeInvalidation::LayoutChanged, ImeInvalidation::LayoutChanged) => {
// ImeInvalidation::LayoutChanged
// }
// _ => ImeInvalidation::Reset,
// }
// .into(),
// }
// }
fn update_alignment_offset(&mut self, extra_width: f64) {
self.alignment_offset = match self.alignment {
Alignment::Start | Alignment::Justified => 0.0,
Alignment::End => extra_width,
Alignment::Center => extra_width / 2.0,
};
}
}
impl<T: TextStorage + EditableText> EditSession<T> {
/// Insert text *not* from the IME, replacing the current selection.
///
/// The caller is responsible for notifying the platform of the change in
/// text state, by calling [`EventCtx::invalidate_text_input`].
#[must_use]
pub fn insert_text(&mut self, data: &mut T, new_text: &str) {
let new_cursor_pos = self.selection.min() + new_text.len();
data.edit(self.selection.range(), new_text);
self.selection = Selection::caret(new_cursor_pos);
self.scroll_to_selection_end(true);
}
/// Sets the clipboard to the contents of the current selection.
///
/// Returns `true` if the clipboard was set, and `false` if not (indicating)
/// that the selection was empty.)
pub fn set_clipboard(&self) -> bool {
// if let Some(text) = self
// .layout
// .text()
// .and_then(|txt| txt.slice(self.selection.range()))
// {
// if !text.is_empty() {
// druid_shell::Application::global()
// .clipboard()
// .put_string(text);
// return true;
// }
// }
false
}
fn scroll_to_selection_end(&mut self, after_edit: bool) {
self.external_scroll_to = Some(after_edit);
}
fn do_action(&mut self, buffer: &mut T, action: Action) {
match action {
Action::Move(movement) => {
let sel = text::movement::movement(movement, self.selection, &self.layout, false);
self.external_selection_change = Some(sel);
self.scroll_to_selection_end(false);
}
Action::MoveSelecting(movement) => {
let sel = text::movement::movement(movement, self.selection, &self.layout, true);
self.external_selection_change = Some(sel);
self.scroll_to_selection_end(false);
}
Action::SelectAll => {
let len = buffer.len();
self.external_selection_change = Some(Selection::new(0, len));
}
Action::SelectWord => {
if self.selection.is_caret() {
let range =
text::movement::word_range_for_pos(buffer.as_str(), self.selection.active);
self.external_selection_change = Some(Selection::new(range.start, range.end));
}
// it is unclear what the behaviour should be if the selection
// is not a caret (and may span multiple words)
}
// This requires us to have access to the layout, which might be stale?
Action::SelectLine => (),
// this assumes our internal selection is consistent with the buffer?
Action::SelectParagraph => {
if !self.selection.is_caret() || buffer.len() < self.selection.active {
return;
}
let prev = buffer.preceding_line_break(self.selection.active);
let next = buffer.next_line_break(self.selection.active);
self.external_selection_change = Some(Selection::new(prev, next));
}
Action::Delete(movement) if self.selection.is_caret() => {
if movement == Movement::Grapheme(Direction::Upstream) {
self.backspace(buffer);
} else {
let to_delete =
text::movement::movement(movement, self.selection, &self.layout, true);
self.selection = to_delete;
self.ime_insert_text(buffer, "")
}
}
Action::Delete(_) => self.ime_insert_text(buffer, ""),
Action::DecomposingBackspace => {
tracing::warn!("Decomposing Backspace is not implemented");
self.backspace(buffer);
}
//Action::UppercaseSelection
//| Action::LowercaseSelection
//| Action::TitlecaseSelection => {
//tracing::warn!("IME transformations are not implemented");
//}
Action::InsertNewLine {
newline_type,
ignore_hotkey,
} => {
if self.send_notification_on_return && !ignore_hotkey {
self.external_action = Some(action);
} else if self.accepts_newlines {
self.ime_insert_text(buffer, &newline_type.to_string());
}
}
Action::InsertTab { ignore_hotkey } => {
if ignore_hotkey || self.accepts_tabs {
self.ime_insert_text(buffer, "\t");
} else if !ignore_hotkey {
self.external_action = Some(action);
}
}
Action::InsertBacktab => {
if !self.accepts_tabs {
self.external_action = Some(action);
}
}
Action::InsertSingleQuoteIgnoringSmartQuotes => self.ime_insert_text(buffer, "'"),
Action::InsertDoubleQuoteIgnoringSmartQuotes => self.ime_insert_text(buffer, "\""),
Action::Cancel if self.send_notification_on_cancel => {
self.external_action = Some(action)
}
other => tracing::warn!("unhandled IME action {:?}", other),
}
}
/// Replace the current selection with `text`, and advance the cursor.
///
/// This should only be called from the IME.
fn ime_insert_text(&mut self, buffer: &mut T, text: &str) {
let new_cursor_pos = self.selection.min() + text.len();
buffer.edit(self.selection.range(), text);
self.external_selection_change = Some(Selection::caret(new_cursor_pos));
self.scroll_to_selection_end(true);
}
fn backspace(&mut self, buffer: &mut T) {
let to_del = if self.selection.is_caret() {
let del_start = offset_for_delete_backwards(&self.selection, buffer);
del_start..self.selection.anchor
} else {
self.selection.range()
};
self.external_selection_change = Some(Selection::caret(to_del.start));
buffer.edit(to_del, "");
self.scroll_to_selection_end(true);
}
fn do_mouse_down(&mut self, point: Point, mods: Modifiers, count: u8) {
let point = point - Vec2::new(self.alignment_offset, 0.0);
let pos = self.layout.text_position_for_point(point);
if mods.shift() {
self.selection.active = pos;
} else {
let Range { start, end } = self.sel_region_for_pos(pos, count);
self.selection = Selection::new(start, end);
self.drag_granularity = match count {
2 => DragGranularity::Word { start, end },
3 => DragGranularity::Paragraph { start, end },
_ => DragGranularity::Grapheme,
};
}
}
fn do_drag(&mut self, point: Point) {
let point = point - Vec2::new(self.alignment_offset, 0.0);
//FIXME: this should behave differently if we were double or triple clicked
let pos = self.layout.text_position_for_point(point);
let text = match self.layout.text() {
Some(text) => text,
None => return,
};
let (start, end) = match self.drag_granularity {
DragGranularity::Grapheme => (self.selection.anchor, pos),
DragGranularity::Word { start, end } => {
let word_range = self.word_for_pos(pos);
if pos <= start {
(end, word_range.start)
} else {
(start, word_range.end)
}
}
DragGranularity::Paragraph { start, end } => {
let par_start = text.preceding_line_break(pos);
let par_end = text.next_line_break(pos);
if pos <= start {
(end, par_start)
} else {
(start, par_end)
}
}
};
self.selection = Selection::new(start, end);
self.scroll_to_selection_end(false);
}
/// Returns a line suitable for drawing a standard cursor.
pub fn cursor_line_for_text_position(&self, pos: usize) -> Line {
let line = self.layout.cursor_line_for_text_position(pos);
line + Vec2::new(self.alignment_offset, 0.0)
}
fn sel_region_for_pos(&mut self, pos: usize, click_count: u8) -> Range<usize> {
match click_count {
1 => pos..pos,
2 => self.word_for_pos(pos),
_ => {
let text = match self.layout.text() {
Some(text) => text,
None => return pos..pos,
};
let line_min = text.preceding_line_break(pos);
let line_max = text.next_line_break(pos);
line_min..line_max
}
}
}
fn word_for_pos(&self, pos: usize) -> Range<usize> {
let layout = match self.layout.layout() {
Some(layout) => layout,
None => return pos..pos,
};
let line_n = layout.hit_test_text_position(pos).line;
let lm = layout.line_metric(line_n).unwrap();
let text = layout.line_text(line_n).unwrap();
let rel_pos = pos - lm.start_offset;
let mut range = text::movement::word_range_for_pos(text, rel_pos);
range.start += lm.start_offset;
range.end += lm.start_offset;
range
}
}
impl<T: TextStorage> EditSessionHandle<T> {
fn new(inner: Arc<RefCell<EditSession<T>>>) -> Self {
let text = inner.borrow().layout.text().cloned().unwrap();
EditSessionHandle { text, inner }
}
}

View File

@ -1,417 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! A type for laying out, drawing, and interacting with text.
use std::rc::Rc;
use kurbo::{Line, Point, Rect, Size};
use parley::layout::{Alignment, Cursor};
use parley::style::{FontFamily, GenericFamily};
use parley::{Layout, LayoutContext};
use vello::peniko::{Brush, Color};
use crate::PaintCtx;
use super::attribute::Link;
use super::font_descriptor::FontDescriptor;
use super::storage::TextStorage;
/// A component for displaying text on screen.
///
/// This is a type intended to be used by other widgets that display text.
/// It allows for the text itself as well as font and other styling information
/// to be set and modified. It wraps an inner layout object, and handles
/// invalidating and rebuilding it as required.
///
/// This object is not valid until the [`rebuild_if_needed`] method has been
/// called. You should generally do this in your widget's [`layout`] method.
/// Additionally, you should call [`needs_rebuild_after_update`]
/// as part of your widget's [`update`] method; if this returns `true`, you will need
/// to call [`rebuild_if_needed`] again, generally by scheduling another [`layout`]
/// pass.
///
/// [`layout`]: trait.Widget.html#tymethod.layout
/// [`update`]: trait.Widget.html#tymethod.update
/// [`needs_rebuild_after_update`]: #method.needs_rebuild_after_update
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
#[derive(Clone)]
pub struct TextLayout<T> {
// TODO - remove Option
text: Option<T>,
font: FontDescriptor,
// when set, this will be used to override the size in he font descriptor.
// This provides an easy way to change only the font size, while still
// using a `FontDescriptor` in the `Env`.
text_size_override: Option<f32>,
text_color: Color,
layout: Option<Layout<Brush>>,
wrap_width: f64,
alignment: Alignment,
links: Rc<[(Rect, usize)]>,
}
/// Metrics describing the layout text.
#[derive(Debug, Clone, Copy, Default)]
pub struct LayoutMetrics {
/// The nominal size of the layout.
pub size: Size,
/// The distance from the nominal top of the layout to the first baseline.
pub first_baseline: f32,
/// The width of the layout, inclusive of trailing whitespace.
pub trailing_whitespace_width: f32,
//TODO: add inking_rect
}
impl<T> TextLayout<T> {
/// Create a new `TextLayout` object.
///
/// You must set the text ([`set_text`]) before using this object.
///
/// [`set_text`]: #method.set_text
pub fn new() -> Self {
TextLayout {
text: None,
font: FontDescriptor::new(FontFamily::Generic(GenericFamily::SystemUi)),
text_color: crate::theme::TEXT_COLOR,
text_size_override: None,
layout: None,
wrap_width: f64::INFINITY,
alignment: Default::default(),
links: Rc::new([]),
}
}
/// Set the default text color for this layout.
pub fn set_text_color(&mut self, color: Color) {
if color != self.text_color {
self.text_color = color;
self.layout = None;
}
}
/// Set the default font.
///
/// The argument is a [`FontDescriptor`].
///
/// [`FontDescriptor`]: struct.FontDescriptor.html
pub fn set_font(&mut self, font: FontDescriptor) {
if font != self.font {
self.font = font;
self.layout = None;
self.text_size_override = None;
}
}
/// Set the font size.
///
/// This overrides the size in the [`FontDescriptor`] provided to [`set_font`].
///
/// [`set_font`]: #method.set_font.html
/// [`FontDescriptor`]: struct.FontDescriptor.html
pub fn set_text_size(&mut self, size: f32) {
if Some(&size) != self.text_size_override.as_ref() {
self.text_size_override = Some(size);
self.layout = None;
}
}
/// Set the width at which to wrap words.
///
/// You may pass `f64::INFINITY` to disable word wrapping
/// (the default behaviour).
pub fn set_wrap_width(&mut self, width: f64) {
let width = width.max(0.0);
// 1e-4 is an arbitrary small-enough value that we don't care to rewrap
if (width - self.wrap_width).abs() > 1e-4 {
self.wrap_width = width;
self.layout = None;
}
}
/// Set the [`TextAlignment`] for this layout.
///
/// [`TextAlignment`]: enum.TextAlignment.html
pub fn set_text_alignment(&mut self, alignment: Alignment) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout = None;
}
}
// /// Returns `true` if this layout's text appears to be right-to-left.
// ///
// /// See [`piet::util::first_strong_rtl`] for more information.
// ///
// /// [`piet::util::first_strong_rtl`]: crate::piet::util::first_strong_rtl
// pub fn text_is_rtl(&self) -> bool {
// self.text_is_rtl
// }
}
impl<T: TextStorage> TextLayout<T> {
/// Create a new `TextLayout` with the provided text.
///
/// This is useful when the text is not tied to application data.
pub fn from_text(text: impl Into<T>) -> Self {
let mut this = TextLayout::new();
this.set_text(text.into());
this
}
/// Returns `true` if this layout needs to be rebuilt.
///
/// This happens (for instance) after style attributes are modified.
///
/// This does not account for things like the text changing, handling that
/// is the responsibility of the user.
pub fn needs_rebuild(&self) -> bool {
self.layout.is_none()
}
/// Set the text to display.
pub fn set_text(&mut self, text: T) {
if self.text.is_none() || !self.text.as_ref().unwrap().maybe_eq(&text) {
self.text = Some(text);
self.layout = None;
}
}
/// Returns the [`TextStorage`] backing this layout, if it exists.
pub fn text(&self) -> Option<&T> {
self.text.as_ref()
}
/// Returns the length of the [`TextStorage`] backing this layout, if it exists.
pub fn text_len(&self) -> usize {
if let Some(text) = &self.text {
text.as_str().len()
} else {
0
}
}
/// Returns the inner Piet [`TextLayout`] type.
///
/// [`TextLayout`]: ./piet/trait.TextLayout.html
pub fn layout(&self) -> Option<&Layout<Brush>> {
self.layout.as_ref()
}
/// The size of the laid-out text.
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn size(&self) -> Size {
self.layout
.as_ref()
.map(|layout| Size::new(layout.width().into(), layout.height().into()))
.unwrap_or_default()
}
/// Return the text's [`LayoutMetrics`].
///
/// This is not meaningful until [`rebuild_if_needed`] has been called.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
/// [`LayoutMetrics`]: struct.LayoutMetrics.html
pub fn layout_metrics(&self) -> LayoutMetrics {
debug_assert!(
self.layout.is_some(),
"TextLayout::layout_metrics called without rebuilding layout object. Text was '{}'",
self.text().as_ref().map(|s| s.as_str()).unwrap_or_default()
);
if let Some(layout) = self.layout.as_ref() {
let first_baseline = layout.get(0).unwrap().metrics().baseline;
let size = Size::new(layout.width().into(), layout.height().into());
LayoutMetrics {
size,
first_baseline,
trailing_whitespace_width: layout.width(),
}
} else {
LayoutMetrics::default()
}
}
/// For a given `Point` (relative to this object's origin), returns index
/// into the underlying text of the nearest grapheme boundary.
pub fn text_position_for_point(&self, point: Point) -> usize {
self.layout
.as_ref()
.map(|layout| Cursor::from_point(layout, point.x as f32, point.y as f32).insert_point)
.unwrap_or_default()
}
/// Given the utf-8 position of a character boundary in the underlying text,
/// return the `Point` (relative to this object's origin) representing the
/// boundary of the containing grapheme.
///
/// # Panics
///
/// Panics if `text_pos` is not a character boundary.
pub fn point_for_text_position(&self, text_pos: usize) -> Point {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
Point::new(
from_position.advance as f64,
(from_position.baseline + from_position.offset) as f64,
)
})
.unwrap_or_default()
}
// /// Given a utf-8 range in the underlying text, return a `Vec` of `Rect`s
// /// representing the nominal bounding boxes of the text in that range.
// ///
// /// # Panics
// ///
// /// Panics if the range start or end is not a character boundary.
// pub fn rects_for_range(&self, range: Range<usize>) -> Vec<Rect> {
// self.layout
// .as_ref()
// .map(|layout| layout.rects_for_range(range))
// .unwrap_or_default()
// }
/// Given the utf-8 position of a character boundary in the underlying text,
/// return a `Line` suitable for drawing a vertical cursor at that boundary.
pub fn cursor_line_for_text_position(&self, text_pos: usize) -> Line {
self.layout
.as_ref()
.map(|layout| {
let from_position = Cursor::from_position(layout, text_pos, /* TODO */ false);
let line_metrics = from_position.path.line(layout).unwrap().metrics();
let p1 = (from_position.advance as f64, line_metrics.baseline as f64);
let p2 = (
from_position.advance as f64,
(line_metrics.baseline + line_metrics.size()) as f64,
);
Line::new(p1, p2)
})
.unwrap_or_else(|| Line::new(Point::ZERO, Point::ZERO))
}
/// 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))?;
let text = self.text()?;
text.links().get(*i)
}
/// Rebuild the inner layout as needed.
///
/// This `TextLayout` object manages a lower-level layout object that may
/// need to be rebuilt in response to changes to the text or attributes
/// like the font.
///
/// This method should be called whenever any of these things may have changed.
/// A simple way to ensure this is correct is to always call this method
/// as part of your widget's [`layout`] method.
///
/// [`layout`]: trait.Widget.html#method.layout
pub fn rebuild_if_needed(&mut self, factory: &mut LayoutContext<Brush>) {
if let Some(text) = &self.text {
if self.layout.is_none() {
let font = self.font.clone();
let color = self.text_color;
let size_override = self.text_size_override;
let descriptor = if let Some(size) = size_override {
font.with_size(size)
} else {
font
};
let builder = factory.ranged_builder(fcx, text, 1.0);
builder
.push_default(StyleProperty)
.new_text_layout(text.clone())
.max_width(self.wrap_width)
.alignment(self.alignment)
.font(descriptor.family.clone(), descriptor.size)
.default_attribute(descriptor.weight)
.default_attribute(descriptor.style);
// .default_attribute(TextAttribute::TextColor(color));
let layout = text.add_attributes(builder).build().unwrap();
self.links = text
.links()
.iter()
.enumerate()
.flat_map(|(i, link)| {
layout
.rects_for_range(link.range())
.into_iter()
.map(move |rect| (rect, i))
})
.collect();
self.layout = Some(layout);
}
}
}
/// Draw the layout at the provided `Point`.
///
/// The origin of the layout is the top-left corner.
///
/// You must call [`rebuild_if_needed`] at some point before you first
/// call this method.
///
/// [`rebuild_if_needed`]: #method.rebuild_if_needed
pub fn draw(&self, ctx: &mut PaintCtx, point: impl Into<Point>) {
debug_assert!(
self.layout.is_some(),
"TextLayout::draw called without rebuilding layout object. Text was '{}'",
self.text
.as_ref()
.map(|t| t.as_str())
.unwrap_or("layout is missing text")
);
if let Some(layout) = self.layout.as_ref() {
ctx.draw_text(layout, point);
}
}
}
impl<T> std::fmt::Debug for TextLayout<T> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.debug_struct("TextLayout")
.field("font", &self.font)
.field("text_size_override", &self.text_size_override)
.field("text_color", &self.text_color)
.field(
"layout",
if self.layout.is_some() {
&"Some"
} else {
&"None"
},
)
.finish()
}
}
impl<T: TextStorage> Default for TextLayout<T> {
fn default() -> Self {
Self::new()
}
}

View File

@ -1,20 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Editing and displaying text.
// TODO
#![allow(clippy::all)]
mod attribute;
mod backspace;
mod editable_text;
mod font_descriptor;
mod input_component;
mod layout;
mod movement;
mod rich_text;
mod shell_text;
mod storage;
mod util;

View File

@ -1,193 +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::{
editable_text::EditableText,
layout::TextLayout,
shell_text::{Movement, Selection, VerticalMovement, WritingDirection},
storage::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<T: EditableText + TextStorage>(
m: Movement,
s: Selection,
layout: &TextLayout<T>,
modify: bool,
) -> Selection {
let (text, layout) = match (layout.text(), layout.layout()) {
(Some(text), Some(layout)) => (text, layout),
_ => {
debug_assert!(false, "movement() called before layout rebuild");
return s;
}
};
// TODO
let writing_direction = WritingDirection::LeftToRight;
let (offset, h_pos) = match m {
Movement::Grapheme(d) if d.is_upstream_for_direction(writing_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)
}
}
Movement::Grapheme(_) => {
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.hit_test_text_position(s.active);
let h_pos = s.h_pos.unwrap_or(cur_pos.point.x);
if cur_pos.line == 0 {
(0, Some(h_pos))
} else {
let lm = layout.line_metric(cur_pos.line).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)
}
Movement::Word(_) => {
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)
}
/// 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<usize> {
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);
}
}

View File

@ -1,233 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
//! Rich text with style spans.
use std::ops::{Range, RangeBounds};
use std::sync::Arc;
use parley::context::RangedBuilder;
use parley::fontique::{Style, Weight};
use parley::style::FontFamily;
use vello::peniko::{Brush, Color};
use super::attribute::{Attribute, AttributeSpans, Link};
use super::font_descriptor::FontDescriptor;
use super::storage::TextStorage;
use super::util;
use crate::ArcStr;
/// Text with optional style spans.
#[derive(Clone, Debug)]
pub struct RichText {
buffer: ArcStr,
attrs: Arc<AttributeSpans>,
links: Arc<[Link]>,
}
impl RichText {
/// Create a new `RichText` object with the provided text.
pub fn new(buffer: ArcStr) -> Self {
RichText::new_with_attributes(buffer, Default::default())
}
/// Create a new `RichText`, providing explicit attributes.
pub fn new_with_attributes(buffer: ArcStr, attributes: AttributeSpans) -> Self {
RichText {
buffer,
attrs: Arc::new(attributes),
links: Arc::new([]),
}
}
/// Builder-style method for adding an [`Attribute`] to a range of text.
///
/// [`Attribute`]: enum.Attribute.html
pub fn with_attribute(mut self, range: impl RangeBounds<usize>, attr: Attribute) -> Self {
self.add_attribute(range, attr);
self
}
/// The length of the buffer, in utf8 code units.
pub fn len(&self) -> usize {
self.buffer.len()
}
/// Returns `true` if the underlying buffer is empty.
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
/// Add an [`Attribute`] to the provided range of text.
///
/// [`Attribute`]: enum.Attribute.html
pub fn add_attribute(&mut self, range: impl RangeBounds<usize>, attr: Attribute) {
let range = util::resolve_range(range, self.buffer.len());
Arc::make_mut(&mut self.attrs).add(range, attr);
}
}
impl TextStorage for RichText {
fn as_str(&self) -> &str {
&self.buffer
}
fn add_attributes(
&self,
mut builder: RangedBuilder<'_, Brush, &str>,
) -> RangedBuilder<'_, Brush, &str> {
for (range, attr) in self.attrs.to_piet_attrs() {
builder.push(&attr, range);
}
builder
}
fn links(&self) -> &[Link] {
&self.links
}
fn maybe_eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.buffer, &other.buffer)
&& Arc::ptr_eq(&self.attrs, &other.attrs)
&& Arc::ptr_eq(&self.links, &other.links)
}
}
/// A builder for creating [`RichText`] objects.
///
/// This builder allows you to construct a [`RichText`] object by building up a sequence
/// of styled sub-strings; first you [`push`](RichTextBuilder::push) a `&str` onto the string,
/// and then you can optionally add styles to that text via the returned [`AttributesAdder`]
/// object.
///
/// # Example
/// ```
/// # use masonry::text::{Attribute, RichTextBuilder};
/// # use masonry::Color;
/// # use masonry::piet::FontWeight;
/// let mut builder = RichTextBuilder::new();
/// builder.push("Hello ");
/// builder.push("World!").weight(FontWeight::BOLD);
///
/// // Can also use write!
/// write!(builder, "Here is your number: {}", 1).underline(true).text_color(Color::RED);
///
/// let rich_text = builder.build();
/// ```
///
/// [`RichText`]: RichText
#[derive(Default)]
pub struct RichTextBuilder {
buffer: String,
attrs: AttributeSpans,
links: Vec<Link>,
}
impl RichTextBuilder {
/// Create a new `RichTextBuilder`.
pub fn new() -> Self {
Self::default()
}
/// Append a `&str` to the end of the text.
///
/// This method returns a [`AttributesAdder`] that can be used to style the newly
/// added string slice.
pub fn push(&mut self, string: &str) -> AttributesAdder {
let range = self.buffer.len()..(self.buffer.len() + string.len());
self.buffer.push_str(string);
self.add_attributes_for_range(range)
}
/// Glue for usage of the write! macro.
///
/// This method should generally not be invoked manually, but rather through the write! macro itself.
#[doc(hidden)]
pub fn write_fmt(&mut self, fmt: std::fmt::Arguments<'_>) -> AttributesAdder {
use std::fmt::Write;
let start = self.buffer.len();
self.buffer
.write_fmt(fmt)
.expect("a formatting trait implementation returned an error");
self.add_attributes_for_range(start..self.buffer.len())
}
/// Get an [`AttributesAdder`] for the given range.
///
/// This can be used to modify styles for a given range after it has been added.
pub fn add_attributes_for_range(&mut self, range: impl RangeBounds<usize>) -> AttributesAdder {
let range = util::resolve_range(range, self.buffer.len());
AttributesAdder {
rich_text_builder: self,
range,
}
}
/// Build the `RichText`.
pub fn build(self) -> RichText {
RichText {
buffer: self.buffer.into(),
attrs: self.attrs.into(),
links: self.links.into(),
}
}
}
/// Adds Attributes to the text.
///
/// See also: [`RichTextBuilder`](RichTextBuilder)
pub struct AttributesAdder<'a> {
rich_text_builder: &'a mut RichTextBuilder,
range: Range<usize>,
}
impl AttributesAdder<'_> {
/// Add the given attribute.
pub fn add_attr(&mut self, attr: Attribute) -> &mut Self {
self.rich_text_builder.attrs.add(self.range.clone(), attr);
self
}
/// Add a font size attribute.
pub fn size(&mut self, size: impl Into<f32>) -> &mut Self {
self.add_attr(Attribute::size(size));
self
}
/// Add a foreground color attribute.
pub fn text_color(&mut self, color: impl Into<Color>) -> &mut Self {
self.add_attr(Attribute::text_color(color));
self
}
/// Add a font family attribute.
pub fn font_family(&mut self, family: FontFamily<'static>) -> &mut Self {
self.add_attr(Attribute::font_family(family));
self
}
/// Add a `FontWeight` attribute.
pub fn weight(&mut self, weight: Weight) -> &mut Self {
self.add_attr(Attribute::weight(weight));
self
}
/// Add a `FontStyle` attribute.
pub fn style(&mut self, style: Style) -> &mut Self {
self.add_attr(Attribute::style(style));
self
}
/// Add a underline attribute.
pub fn underline(&mut self, underline: bool) -> &mut Self {
self.add_attr(Attribute::underline(underline));
self
}
/// Add a `FontDescriptor` attribute.
pub fn font_descriptor(&mut self, font: impl Into<FontDescriptor>) -> &mut Self {
self.add_attr(Attribute::font_descriptor(font));
self
}
//pub fn link(&mut self, command: impl Into<Command>) -> &mut Self;
}

View File

@ -1,527 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors and the Glazier Authors
// SPDX-License-Identifier: Apache-2.0
//! Types and functions for cross-platform text input.
//!
//! Text input is a notoriously difficult problem. Unlike many other aspects of
//! user interfaces, text input can not be correctly modeled using discrete
//! events passed from the platform to the application. For example, many mobile
//! phones implement autocorrect: when the user presses the spacebar, the
//! platform peeks at the word directly behind the caret, and potentially
//! replaces it if it's misspelled. This means the platform needs to know the
//! contents of a text field. On other devices, the platform may need to draw an
//! emoji window under the caret, or look up the on-screen locations of letters
//! for crossing out with a stylus, both of which require fetching on-screen
//! coordinates from the application.
//!
//! This is all to say: text editing is a bidirectional conversation between the
//! application and the platform. The application, when the platform asks for
//! it, serves up text field coordinates and content. The platform looks at
//! this information and combines it with input from keyboards (physical or
//! onscreen), voice, styluses, the user's language settings, and then sends
//! edit commands to the application.
//!
//! Many platforms have an additional complication: this input fusion often
//! happens in a different process from your application. If we don't
//! specifically account for this fact, we might get race conditions! In the
//! autocorrect example, if I sloppily type "meoow" and press space, the
//! platform might issue edits to "delete backwards one word and insert meow".
//! However, if I concurrently click somewhere else in the document to move the
//! caret, this will replace some *other* word with "meow", and leave the
//! "meoow" disappointingly present. To mitigate this problem, we use locks,
//! represented by the `InputHandler` trait.
//!
//! ## Lifecycle of a Text Input
//!
//! 1. The user clicks a link or switches tabs, and the window content now
//! contains a new text field. The application registers this new field by
//! calling `WindowHandle::add_text_field`, and gets a `TextFieldToken` that
//! represents this new field.
//! 2. The user clicks on that text field, focusing it. The application lets the
//! platform know by calling `WindowHandle::set_focused_text_field` with that
//! field's `TextFieldToken`.
//! 3. The user presses a key on the keyboard. The platform first calls
//! `WinHandler::key_down`. If this method returns `true`, the application
//! has indicated the keypress was captured, and we skip the remaining steps.
//! 4. If `key_down` returned `false`, `druid-shell` forwards the key event to the
//! platform's text input system
//! 5. The platform, in response to either this key event or some other user
//! action, determines it's time for some text input. It calls
//! `WinHandler::text_input` to acquire a lock on the text field's state.
//! The application returns an `InputHandler` object corresponding to the
//! requested text field. To prevent race conditions, your application may
//! not make any changes
//! to the text field's state until the platform drops the `InputHandler`.
//! 6. The platform calls various `InputHandler` methods to inspect and edit the
//! text field's state. Later, usually within a few milliseconds, the
//! platform drops the `InputHandler`, allowing the application to once again
//! make changes to the text field's state. These commands might be "insert
//! `q`" for a smartphone user tapping on their virtual keyboard, or
//! "move the caret one word left" for a user pressing the left arrow key
//! while holding control.
//! 7. Eventually, after many keypresses cause steps 36 to repeat, the user
//! unfocuses the text field. The application indicates this to the platform
//! by calling `set_focused_text_field`. Note that even though focus has
//! shifted away from our text field, the platform may still send edits to it
//! by calling `WinHandler::text_input`.
//! 8. At some point, the user clicks a link or switches a tab, and the text
//! field is no longer present in the window. The application calls
//! `WindowHandle::remove_text_field`, and the platform may no longer call
//! `WinHandler::text_input` to make changes to it.
//!
//! The application also has a series of steps it follows if it wants to make
//! its own changes to the text field's state:
//!
//! 1. The application determines it would like to make a change to the text
//! field; perhaps the user has scrolled and and the text field has changed
//! its visible location on screen, or perhaps the user has clicked to move
//! the caret to a new location.
//! 2. The application first checks to see if there's an outstanding
//! `InputHandler` lock for this text field; if so, it waits until the last
//! `InputHandler` is dropped before continuing.
//! 3. The application then makes the change to the text input. If the change
//! would affect state visible from an `InputHandler`, the application must
//! notify the platform via `WinHandler::update_text_field`.
//!
//! ## Supported Platforms
//!
//! Currently, `druid-shell` text input is fully implemented on macOS. Our goal
//! is to have full support for all `druid-shell` targets, but for now,
//! `InputHandler` calls are simulated from keypresses on other platforms, which
//! doesn't allow for IME input, dead keys, etc.
use std::ops::Range;
/// An event representing an application-initiated change in [`InputHandler`]
/// state.
///
/// When we change state that may have previously been retrieved from an
/// [`InputHandler`], we notify the platform so that it can invalidate any
/// data if necessary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Event {
/// Indicates the value returned by `InputHandler::selection` may have changed.
SelectionChanged,
/// Indicates the values returned by one or more of these methods may have changed:
/// - `InputHandler::hit_test_point`
/// - `InputHandler::line_range`
/// - `InputHandler::bounding_box`
/// - `InputHandler::slice_bounding_box`
LayoutChanged,
/// Indicates any value returned from any `InputHandler` method may have changed.
Reset,
}
/// A range of selected text, or a caret.
///
/// A caret is the blinking vertical bar where text is to be inserted. We
/// represent it as a selection with zero length, where `anchor == active`.
/// Indices are always expressed in UTF-8 bytes, and must be between 0 and the
/// document length, inclusive.
///
/// As an example, if the input caret is at the start of the document `hello
/// world`, we would expect both `anchor` and `active` to be `0`. If the user
/// holds shift and presses the right arrow key five times, we would expect the
/// word `hello` to be selected, the `anchor` to still be `0`, and the `active`
/// to now be `5`.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[non_exhaustive]
pub struct Selection {
/// The 'anchor' end of the selection.
///
/// This is the end of the selection that stays unchanged while holding
/// shift and pressing the arrow keys.
pub anchor: usize,
/// The 'active' end of the selection.
///
/// This is the end of the selection that moves while holding shift and
/// pressing the arrow keys.
pub active: usize,
/// The saved horizontal position, during vertical movement.
///
/// This should not be set by the IME; it will be tracked and handled by
/// the text field.
pub h_pos: Option<f64>,
}
#[allow(clippy::len_without_is_empty)]
impl Selection {
/// Create a new `Selection` with the provided `anchor` and `active` positions.
///
/// Both positions refer to UTF-8 byte indices in some text.
///
/// If your selection is a caret, you can use [`Selection::caret`] instead.
pub fn new(anchor: usize, active: usize) -> Selection {
Selection {
anchor,
active,
h_pos: None,
}
}
/// Create a new caret (zero-length selection) at the provided UTF-8 byte index.
///
/// `index` must be a grapheme cluster boundary.
pub fn caret(index: usize) -> Selection {
Selection {
anchor: index,
active: index,
h_pos: None,
}
}
/// Construct a new selection from this selection, with the provided h_pos.
///
/// # Note
///
/// `h_pos` is used to track the *pixel* location of the cursor when moving
/// vertically; lines may have available cursor positions at different
/// positions, and arrowing down and then back up should always result
/// in a cursor at the original starting location; doing this correctly
/// requires tracking this state.
///
/// You *probably* don't need to use this, unless you are implementing a new
/// text field, or otherwise implementing vertical cursor motion, in which
/// case you will want to set this during vertical motion if it is not
/// already set.
pub fn with_h_pos(mut self, h_pos: Option<f64>) -> Self {
self.h_pos = h_pos;
self
}
/// Create a new selection that is guaranteed to be valid for the provided
/// text.
#[must_use = "constrained constructs a new Selection"]
pub fn constrained(mut self, s: &str) -> Self {
let s_len = s.len();
self.anchor = self.anchor.min(s_len);
self.active = self.active.min(s_len);
while !s.is_char_boundary(self.anchor) {
self.anchor += 1;
}
while !s.is_char_boundary(self.active) {
self.active += 1;
}
self
}
/// Return the position of the upstream end of the selection.
///
/// This is end with the lesser byte index.
///
/// Because of bidirectional text, this is not necessarily "left".
pub fn min(&self) -> usize {
usize::min(self.anchor, self.active)
}
/// Return the position of the downstream end of the selection.
///
/// This is the end with the greater byte index.
///
/// Because of bidirectional text, this is not necessarily "right".
pub fn max(&self) -> usize {
usize::max(self.anchor, self.active)
}
/// The sequential range of the document represented by this selection.
///
/// This is the range that would be replaced if text were inserted at this
/// selection.
pub fn range(&self) -> Range<usize> {
self.min()..self.max()
}
/// The length, in bytes of the selected region.
///
/// If the selection is a caret, this is `0`.
pub fn len(&self) -> usize {
if self.anchor > self.active {
self.anchor - self.active
} else {
self.active - self.anchor
}
}
/// Returns `true` if the selection's length is `0`.
pub fn is_caret(&self) -> bool {
self.len() == 0
}
}
/// 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.
pub enum Affinity {
Upstream,
Downstream,
}
/// 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.
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,
}
/// A special text editing command sent from the platform to the application.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Action {
/// Moves the selection.
///
/// Before moving, if the active and the anchor of the selection are not at
/// the same position (it's a non-caret selection), then:
///
/// 1. First set both active and anchor to the same position: the
/// selection's upstream index if `Movement` is an upstream movement, or
/// the downstream index if `Movement` is a downstream movement.
///
/// 2. If `Movement` is `Grapheme`, then stop. Otherwise, apply the
/// `Movement` as per the usual rules.
Move(Movement),
/// Moves just the selection's active edge.
///
/// Equivalent to holding shift while performing movements or clicks on most
/// operating systems.
MoveSelecting(Movement),
/// Select the entire document.
SelectAll,
/// Expands the selection to the entire soft-wrapped line.
///
/// If multiple lines are already selected, expands the selection to
/// encompass all soft-wrapped lines that intersected with the prior
/// selection. If the selection is a caret is on a soft line break, uses
/// the affinity of the caret to determine which of the two lines to select.
/// `SelectLine` should be idempotent: it should never expand onto adjacent
/// lines.
SelectLine,
/// Expands the selection to the entire hard-wrapped line.
///
/// If multiple lines are already selected, expands the selection to
/// encompass all hard-wrapped lines that intersected with the prior
/// selection. `SelectParagraph` should be idempotent: it should never
/// expand onto adjacent lines.
SelectParagraph,
/// Expands the selection to the entire word.
///
/// If multiple words are already selected, expands the selection to
/// encompass all words that intersected with the prior selection. If the
/// selection is a caret is on a word boundary, selects the word downstream
/// of the caret. `SelectWord` should be idempotent: it should never expand
/// onto adjacent words.
///
/// For more information on what these so-called "words" are, see
/// [Unicode Text Segmentation](https://unicode.org/reports/tr29/#Word_Boundaries).
SelectWord,
/// Deletes some text.
///
/// If some text is already selected, `Movement` is ignored, and the
/// selection is deleted. If the selection's anchor is the same as the
/// active, then first apply `MoveSelecting(Movement)` and then delete the
/// resulting selection.
Delete(Movement),
/// Delete backwards, potentially breaking graphemes.
///
/// A special kind of backspace that, instead of deleting the entire
/// grapheme upstream of the caret, may in some cases and character sets
/// delete a subset of that grapheme's code points.
DecomposingBackspace,
/// Maps the characters in the selection to uppercase.
///
/// For more information on case mapping, see the
/// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
UppercaseSelection,
/// Maps the characters in the selection to lowercase.
///
/// For more information on case mapping, see the
/// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
LowercaseSelection,
/// Maps the characters in the selection to titlecase.
///
/// When calculating whether a character is at the beginning of a word, you
/// may have to peek outside the selection to other characters in the document.
///
/// For more information on case mapping, see the
/// [Unicode Case Mapping FAQ](https://unicode.org/faq/casemap_charprop.html#7)
TitlecaseSelection,
/// Inserts a newline character into the document.
InsertNewLine {
/// If `true`, then always insert a newline, even if normally you
/// would run a keyboard shortcut attached to the return key, like
/// sending a message or activating autocomplete.
///
/// On macOS, this is triggered by pressing option-return.
ignore_hotkey: bool,
/// Either `U+000A`, `U+2029`, or `U+2028`. For instance, on macOS, control-enter inserts `U+2028`.
//FIXME: what about windows?
newline_type: char,
},
/// Inserts a tab character into the document.
InsertTab {
/// If `true`, then always insert a tab, even if normally you would run
/// a keyboard shortcut attached to the return key, like indenting a
/// line or activating autocomplete.
///
/// On macOS, this is triggered by pressing option-tab.
ignore_hotkey: bool,
},
/// Indicates the reverse of inserting tab; corresponds to shift-tab on most
/// operating systems.
InsertBacktab,
InsertSingleQuoteIgnoringSmartQuotes,
InsertDoubleQuoteIgnoringSmartQuotes,
/// Scrolls the text field without modifying the selection.
Scroll(VerticalMovement),
/// Centers the selection vertically in the text field.
///
/// The average of the anchor's y and the active's y should be exactly
/// halfway down the field. If the selection is taller than the text
/// field's visible height, then instead scrolls the minimum distance such
/// that the text field is completely vertically filled by the selection.
ScrollToSelection,
/// Sets the writing direction of the selected text or caret.
SetSelectionWritingDirection(WritingDirection),
/// Sets the writing direction of all paragraphs that partially or fully
/// intersect with the selection or caret.
SetParagraphWritingDirection(WritingDirection),
/// Cancels the current window or operation.
///
/// Triggered on most operating systems with escape.
Cancel,
}

View File

@ -1,74 +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 vello::peniko::Brush;
use super::attribute::Link;
/// A type that represents text that can be displayed.
pub trait TextStorage: Clone {
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.
#[allow(unused_variables)]
fn add_attributes(&self, builder: RangedBuilder<Brush, &str>) -> RangedBuilder<Brush, &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;
}
/// A reference counted string slice.
///
/// This is a data-friendly way to represent strings in Masonry. Unlike `String`
/// it cannot be mutated, but unlike `String` it can be cheaply cloned.
pub type ArcStr = Arc<str>;
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<String> {
fn as_str(&self) -> &str {
self.deref()
}
fn maybe_eq(&self, other: &Self) -> bool {
self == other
}
}

View File

@ -1,21 +0,0 @@
// Copyright 2018 the Xilem Authors and the Druid Authors
// SPDX-License-Identifier: Apache-2.0
use std::ops::{Bound, Range, RangeBounds};
/// Resolves a `RangeBounds` into a range in the range 0..len.
pub fn resolve_range(range: impl RangeBounds<usize>, len: usize) -> Range<usize> {
let start = match range.start_bound() {
Bound::Unbounded => 0,
Bound::Included(n) => *n,
Bound::Excluded(n) => *n + 1,
};
let end = match range.end_bound() {
Bound::Unbounded => len,
Bound::Included(n) => *n + 1,
Bound::Excluded(n) => *n,
};
start.min(len)..end.min(len)
}