mirror of https://github.com/linebender/xilem
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:
parent
e9efc8cb18
commit
6589ac343d
|
@ -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.
|
|
@ -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),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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.,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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 3–6 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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue