feather_ui/
text.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use std::cell::RefCell;
5use std::ops;
6use std::rc::Rc;
7use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
8
9use cosmic_text::{Affinity, AttrsList, Cursor, Metrics};
10use smallvec::SmallVec;
11
12use crate::graphics::point_to_pixel;
13
14/// Represents a single change, recording the (`start`,`end`) range of the new string, and the old string
15/// that used to be contained in that range. `start` and `end` might be equal, which represents a deletion.
16/// Likewise, old might be empty, which represents an insertion.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Change {
19    pub start: Cursor,
20    pub end: Cursor,
21    pub old: SmallVec<[u8; 4]>,
22    pub attrs: Option<AttrsList>,
23}
24
25#[derive(Debug)]
26pub struct EditBuffer {
27    pub(crate) buffer: Rc<RefCell<cosmic_text::Buffer>>,
28    pub(crate) count: AtomicUsize,
29    pub(crate) reflow: AtomicBool,
30    cursor: AtomicUsize,
31    select: AtomicUsize, // If there's a selection, this is different from cursor and points at the end. Can be less than cursor.
32}
33
34impl Default for EditBuffer {
35    fn default() -> Self {
36        Self {
37            buffer: Rc::new(RefCell::new(cosmic_text::Buffer::new_empty(Metrics::new(
38                1.0, 1.0,
39            )))),
40            count: Default::default(),
41            reflow: Default::default(),
42            cursor: Default::default(),
43            select: Default::default(),
44        }
45    }
46}
47impl Clone for EditBuffer {
48    fn clone(&self) -> Self {
49        Self {
50            buffer: self.buffer.clone(),
51            count: self.count.load(Ordering::Relaxed).into(),
52            reflow: self.reflow.load(Ordering::Relaxed).into(),
53            cursor: self.cursor.load(Ordering::Relaxed).into(),
54            select: self.select.load(Ordering::Relaxed).into(),
55        }
56    }
57}
58
59impl EditBuffer {
60    pub fn new(text: &str, cursor: (usize, usize)) -> Self {
61        let this = Self {
62            buffer: Rc::new(RefCell::new(cosmic_text::Buffer::new_empty(Metrics {
63                font_size: 1.0,
64                line_height: 1.0,
65            }))),
66            count: 0.into(),
67            reflow: true.into(),
68            cursor: cursor.0.into(),
69            select: cursor.1.into(),
70        };
71        this.set_content(text);
72        this
73    }
74    pub fn get_content(&self) -> String {
75        let mut s = String::new();
76        s.reserve(
77            self.buffer
78                .borrow()
79                .lines
80                .iter()
81                .fold(0, |c, l| c + l.text().len() + l.ending().as_str().len()),
82        );
83        for line in &self.buffer.borrow().lines {
84            s.push_str(line.text());
85            s.push_str(line.ending().as_str());
86        }
87        s
88    }
89
90    pub fn set_content(&self, content: &str) {
91        let mut buffer = self.buffer.borrow_mut();
92        buffer.lines.clear();
93        for (range, ending) in cosmic_text::LineIter::new(content) {
94            buffer.lines.push(cosmic_text::BufferLine::new(
95                &content[range],
96                ending,
97                AttrsList::new(&cosmic_text::Attrs::new()),
98                cosmic_text::Shaping::Advanced,
99            ));
100        }
101        if buffer.lines.is_empty() {
102            buffer.lines.push(cosmic_text::BufferLine::new(
103                "",
104                cosmic_text::LineEnding::default(),
105                AttrsList::new(&cosmic_text::Attrs::new()),
106                cosmic_text::Shaping::Advanced,
107            ));
108        }
109        self.reflow.store(true, Ordering::Release);
110        self.count.fetch_add(1, Ordering::Release);
111    }
112
113    pub fn edit(
114        &self,
115        multisplice: &[(ops::Range<usize>, String)],
116    ) -> SmallVec<[(ops::Range<usize>, String); 1]> {
117        let mut text = self.get_content();
118        if multisplice.len() == 1 {
119            let (range, replace) = &multisplice[0];
120            let old = text[range.clone()].to_string();
121            text.replace_range(range.clone(), replace);
122            self.set_content(&text);
123            [(range.start..replace.len(), old)].into()
124        } else {
125            // To preserve the validity of the ranges, we have to assemble the string piecewise
126            let mut undo = SmallVec::new();
127            let mut last = 0;
128            let mut s = String::new();
129            {
130                for (range, replace) in multisplice {
131                    s.push_str(&text[last..range.start]);
132                    s.push_str(replace);
133                    undo.push((range.start..replace.len(), text[range.clone()].to_string()));
134                    last = range.end;
135                }
136
137                s.push_str(&text[last..]);
138            };
139            self.set_content(&s);
140            undo
141        }
142    }
143
144    fn compact(mut idx: usize, affinity: Affinity) -> usize {
145        const FLAG: usize = 1 << (usize::BITS - 1);
146        idx &= !FLAG;
147        if affinity == Affinity::After {
148            idx |= FLAG;
149        }
150        idx
151    }
152    fn expand(cursor: usize) -> (usize, Affinity) {
153        const FLAG: usize = 1 << (usize::BITS - 1);
154        (
155            cursor & (!FLAG),
156            if (cursor & FLAG) != 0 {
157                Affinity::After
158            } else {
159                Affinity::Before
160            },
161        )
162    }
163
164    pub fn get_cursor(&self) -> (usize, Affinity) {
165        Self::expand(self.cursor.load(Ordering::Relaxed))
166    }
167
168    pub fn get_selection(&self) -> (usize, Affinity) {
169        Self::expand(self.select.load(Ordering::Relaxed))
170    }
171
172    pub fn set_cursor(&self, cursor: usize, affinity: Affinity) {
173        let start = Self::compact(cursor, affinity);
174        self.cursor.store(start, Ordering::Release);
175        self.select.store(start, Ordering::Release);
176        self.count.fetch_add(1, Ordering::Release);
177    }
178    pub fn set_selection(&self, start: (usize, Affinity), end: (usize, Affinity)) {
179        let cursor = Self::compact(start.0, start.1);
180        let select = Self::compact(end.0, end.1);
181        self.cursor.store(cursor, Ordering::Release);
182        self.select.store(select, Ordering::Release);
183        self.count.fetch_add(1, Ordering::Release);
184    }
185
186    pub fn to_cursor(buffer: &crate::cosmic_text::Buffer, cursor: (usize, Affinity)) -> Cursor {
187        let mut lines = 0;
188        let (mut idx, mut affinity) = cursor;
189        for line in &buffer.lines {
190            let len = line.text().len();
191            if len >= idx {
192                break;
193            }
194            idx -= len;
195            lines += 1;
196            if idx < line.ending().as_str().len() {
197                affinity = Affinity::Before;
198                idx = 0;
199                break;
200            }
201            idx -= line.ending().as_str().len();
202        }
203        Cursor {
204            line: lines,
205            index: idx,
206            affinity,
207        }
208    }
209
210    pub fn from_cursor(buffer: &crate::cosmic_text::Buffer, cursor: Cursor) -> (usize, Affinity) {
211        let mut idx = 0;
212        for line in buffer.lines.iter().take(cursor.line) {
213            idx += line.text().len() + line.ending().as_str().len();
214        }
215        (idx + cursor.index, cursor.affinity)
216    }
217
218    pub fn flowtext(
219        &self,
220        font_system: &mut crate::cosmic_text::FontSystem,
221        font_size: f32,
222        line_height: f32,
223        wrap: cosmic_text::Wrap,
224        align: Option<cosmic_text::Align>,
225        dpi: crate::RelDim,
226        attrs: cosmic_text::Attrs<'_>,
227    ) {
228        let mut text_buffer = self.buffer.borrow_mut();
229
230        let metrics = cosmic_text::Metrics::new(
231            point_to_pixel(font_size, dpi.width),
232            point_to_pixel(line_height, dpi.height),
233        );
234
235        if text_buffer.metrics() != metrics {
236            text_buffer.set_metrics(font_system, metrics);
237        }
238        if text_buffer.wrap() != wrap {
239            text_buffer.set_wrap(font_system, wrap);
240        }
241        for line in &mut text_buffer.lines {
242            line.set_attrs_list(AttrsList::new(&attrs));
243            line.set_align(align);
244        }
245        text_buffer.shape_until_scroll(font_system, false);
246        self.reflow.store(false, Ordering::Release);
247    }
248}
249
250#[derive(Default, Debug)]
251pub struct EditView {
252    pub(crate) obj: Rc<EditBuffer>,
253    count: usize,
254    reflow: bool,
255}
256
257impl EditView {
258    pub fn get(&self) -> &EditBuffer {
259        &self.obj
260    }
261}
262
263// Ensures each clone gets a fresh snapshot to capture changes
264impl Clone for EditView {
265    fn clone(&self) -> Self {
266        Self {
267            obj: self.obj.clone(),
268            count: self.obj.count.load(Ordering::Acquire),
269            reflow: self.obj.reflow.load(Ordering::Acquire),
270        }
271    }
272}
273
274impl Eq for EditView {}
275impl PartialEq for EditView {
276    fn eq(&self, other: &Self) -> bool {
277        self.count == other.count
278            && self.reflow == other.reflow
279            && Rc::ptr_eq(&self.obj, &other.obj)
280    }
281}
282
283impl From<Rc<EditBuffer>> for EditView {
284    fn from(value: Rc<EditBuffer>) -> Self {
285        Self {
286            obj: value.clone(),
287            count: value.count.load(Ordering::Acquire),
288            reflow: value.reflow.load(Ordering::Acquire),
289        }
290    }
291}
292
293impl From<EditBuffer> for EditView {
294    fn from(value: EditBuffer) -> Self {
295        let value = Rc::new(value);
296        Self {
297            obj: value.clone(),
298            count: value.count.load(Ordering::Acquire),
299            reflow: value.reflow.load(Ordering::Acquire),
300        }
301    }
302}