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