Skip to main content

ui/input/
chip.rs

1//! Chips: ranges of a field's content painted as a glyph and a title.
2
3use icons::Icon;
4
5use super::*;
6
7/// A range of a field's content painted as `glyph` and `title` in place of its
8/// text, and edited as one unit — see [`TextField::set_chips`].
9#[derive(Clone, Debug, PartialEq, Eq)]
10pub struct Chip {
11    pub range: Range<usize>,
12    pub glyph: Icon,
13    pub title: SharedString,
14}
15
16/// What stands in a chip's glyph slot. An em space is as wide as the type is
17/// tall.
18const SLOT: &str = "\u{2003}";
19
20/// A field's content with every chip replaced by what it shows: the glyph's
21/// slot, a space, the title.
22pub(super) struct Chipped {
23    pub text: SharedString,
24    pub shown: Shown,
25    /// Each chip's glyph slot, in shown offsets.
26    pub slots: Vec<(Range<usize>, Icon)>,
27    /// Each chip's title, in shown offsets.
28    pub titles: Vec<Range<usize>>,
29}
30
31impl Chipped {
32    /// `text` shown as it is.
33    pub fn plain(text: SharedString) -> Self {
34        Self {
35            text,
36            shown: Shown::default(),
37            slots: Vec::new(),
38            titles: Vec::new(),
39        }
40    }
41}
42
43/// `content` with `chips` shown. `chips` is in document order; a chip that
44/// overlaps the one before it, is empty, runs past the end or cuts a character
45/// is left as text.
46pub(super) fn chipped(content: &SharedString, chips: &[Chip]) -> Chipped {
47    if chips.is_empty() {
48        return Chipped::plain(content.clone());
49    }
50    let mut text = String::with_capacity(content.len());
51    let (mut slots, mut titles, mut pairs) = (Vec::new(), Vec::new(), Vec::new());
52    let mut at = 0;
53    for chip in chips {
54        let range = &chip.range;
55        if range.start < at
56            || range.is_empty()
57            || range.end > content.len()
58            || !content.is_char_boundary(range.start)
59            || !content.is_char_boundary(range.end)
60        {
61            continue;
62        }
63        text.push_str(&content[at..range.start]);
64        let from = text.len();
65        text.push_str(SLOT);
66        slots.push((from..text.len(), chip.glyph.clone()));
67        text.push(' ');
68        let title = text.len();
69        // Shaping breaks lines at `\n`, and a chip is one unit on one line.
70        text.push_str(&chip.title.replace('\n', " "));
71        titles.push(title..text.len());
72        pairs.push((range.clone(), from..text.len()));
73        at = range.end;
74    }
75    text.push_str(&content[at..]);
76    Chipped {
77        text: text.into(),
78        shown: Shown::new(pairs),
79        slots,
80        titles,
81    }
82}
83
84/// Where a chip's range lands after `edit`. An edit reaching inside the chip
85/// takes it, so typing next to one never grows it.
86pub(super) fn follow(edit: Edit, range: &Range<usize>) -> Option<Range<usize>> {
87    if edit.start < range.end && range.start < edit.old_end {
88        return None;
89    }
90    edit.map(range.clone())
91}
92
93/// `offset` moved out of the chip it falls inside: to the chip's start when
94/// `back`, its end otherwise.
95pub(super) fn outside(chips: &[Chip], offset: usize, back: bool) -> usize {
96    match chips
97        .iter()
98        .find(|chip| chip.range.start < offset && offset < chip.range.end)
99    {
100        Some(chip) if back => chip.range.start,
101        Some(chip) => chip.range.end,
102        None => offset,
103    }
104}
105
106/// `range` grown to take whole every chip it cuts.
107pub(super) fn widen(chips: &[Chip], range: Range<usize>) -> Range<usize> {
108    outside(chips, range.start, true)..outside(chips, range.end, false)
109}