feather_ui/render/
textbox.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::rc::Rc;
6
7use cosmic_text::Cursor;
8use unicode_segmentation::UnicodeSegmentation;
9
10use crate::Error;
11use crate::color::sRGB;
12use crate::render::compositor::DataFlags;
13use crate::render::{compositor, text};
14
15pub struct Instance {
16    pub text_buffer: Rc<RefCell<cosmic_text::Buffer>>,
17    pub padding: crate::PxPerimeter,
18    pub cursor: Cursor,
19    pub selection: Option<(Cursor, Cursor)>,
20    pub selection_bg: sRGB,
21    pub selection_color: sRGB,
22    pub color: sRGB,
23    pub cursor_color: sRGB,
24    pub scale: f32,
25}
26
27impl Instance {
28    fn draw_box(
29        x: f32,
30        y: f32,
31        mut w: f32,
32        mut h: f32,
33        bounds: crate::PxRect,
34        color: sRGB,
35    ) -> compositor::Data {
36        // When we are drawing boxes that need to line up with each other, this is a
37        // worst-case scenario for the compositor's antialiasing. The only way
38        // to antialias arbitrary selection boxes correctly is to use a texture
39        // cache or a custom shader. Instead of doing that, we just pixel-snap
40        // everything.
41        w = w.min((bounds.bottomright().x - x).max(0.0));
42        h = h.min((bounds.bottomright().y - y).max(0.0));
43        let bx = x.max(bounds.topleft().x);
44        let by = y.max(bounds.topleft().y);
45        w -= bx - x;
46        h -= by - y;
47
48        compositor::Data {
49            pos: [bx.round(), by.round()].into(),
50            dim: [w.round(), h.round()].into(),
51            uv: [0.0, 0.0].into(),
52            uvdim: [0.0, 0.0].into(),
53            color: color.as_32bit().rgba,
54            flags: DataFlags::new().with_tex(u8::MAX).with_raw(true).into(),
55            ..Default::default()
56        }
57    }
58}
59
60impl crate::render::Renderable for Instance {
61    fn render(
62        &self,
63        area: crate::PxRect,
64        driver: &crate::graphics::Driver,
65        compositor: &mut compositor::CompositorView<'_>,
66    ) -> Result<(), Error> {
67        let buffer = self.text_buffer.borrow();
68        // Padding works differently in a textbox than in a static text field, because a
69        // textbox cannot having non-clipping regions outside the text area, or
70        // you'll get rendering errors when scrolling.
71        let area = area + self.padding;
72        let pos = area.topleft();
73
74        let bounds = area.intersect(compositor.current_clip());
75
76        let bounds_top = bounds.topleft().y as i32;
77        let bounds_bottom = bounds.bottomright().y as i32;
78        let bounds_min_x = (bounds.topleft().x as i32).max(0);
79        let bounds_min_y = bounds_top.max(0);
80        let bounds_max_x = bounds.bottomright().x as i32;
81        let bounds_max_y = bounds_bottom;
82        let color = self.color.into();
83        let selection_color = self.selection_color.into();
84
85        let is_run_visible = |run: &cosmic_text::LayoutRun| {
86            let start_y_physical = (pos.y + (run.line_top * self.scale)) as i32;
87            let end_y_physical = start_y_physical + (run.line_height * self.scale) as i32;
88
89            start_y_physical <= bounds_bottom && bounds_top <= end_y_physical
90        };
91
92        for run in buffer
93            .layout_runs()
94            .skip_while(|run| !is_run_visible(run))
95            .take_while(is_run_visible)
96        {
97            let line_i = run.line_i;
98            let line_top = run.line_top;
99            let line_height = run.line_height;
100
101            // Highlight selection
102            if let Some((start, end)) = &self.selection
103                && line_i >= start.line
104                && line_i <= end.line
105            {
106                let mut range_opt = None;
107                for glyph in run.glyphs.iter() {
108                    // Guess x offset based on characters
109                    let cluster = &run.text[glyph.start..glyph.end];
110                    let total = cluster.grapheme_indices(true).count();
111                    let mut c_x = glyph.x;
112                    let c_w = glyph.w / total as f32;
113                    for (i, c) in cluster.grapheme_indices(true) {
114                        let c_start = glyph.start + i;
115                        let c_end = glyph.start + i + c.len();
116                        if (start.line != line_i || c_end > start.index)
117                            && (end.line != line_i || c_start < end.index)
118                        {
119                            range_opt = match range_opt.take() {
120                                Some((min, max)) => Some((
121                                    std::cmp::min(min, c_x as i32),
122                                    std::cmp::max(max, (c_x + c_w) as i32),
123                                )),
124                                None => Some((c_x as i32, (c_x + c_w) as i32)),
125                            };
126                        } else if let Some((min, max)) = range_opt.take() {
127                            compositor.preprocessed(Self::draw_box(
128                                min as f32 + pos.x,
129                                line_top + pos.y,
130                                std::cmp::max(0, max - min) as f32,
131                                line_height,
132                                bounds,
133                                self.selection_bg,
134                            ));
135                        }
136                        c_x += c_w;
137                    }
138                }
139
140                if run.glyphs.is_empty() && end.line > line_i {
141                    // Highlight all of internal empty lines
142                    range_opt = Some((0, buffer.size().0.unwrap_or(0.0) as i32));
143                }
144
145                if let Some((mut min, mut max)) = range_opt.take() {
146                    if end.line > line_i {
147                        // Draw to end of line
148                        if run.rtl {
149                            min = 0;
150                        } else if let (Some(w), _) = buffer.size() {
151                            max = w.round() as i32;
152                        } else if max == 0 {
153                            max = (buffer.metrics().font_size * 0.5) as i32;
154                        }
155                    }
156                    compositor.preprocessed(Self::draw_box(
157                        min as f32 + pos.x,
158                        line_top + pos.y,
159                        std::cmp::max(0, max - min) as f32,
160                        line_height,
161                        bounds,
162                        self.selection_bg,
163                    ));
164                }
165            }
166
167            // Draw text
168            for glyph in run.glyphs.iter() {
169                let physical_glyph = glyph.physical((pos.x, pos.y), self.scale);
170
171                let mut color = match glyph.color_opt {
172                    Some(some) => some,
173                    None => color,
174                };
175
176                if let Some((start, end)) = self.selection
177                    && line_i >= start.line
178                    && line_i <= end.line
179                    && (start.line != line_i || glyph.end > start.index)
180                    && (end.line != line_i || glyph.start < end.index)
181                {
182                    color = selection_color;
183                }
184
185                text::Instance::write_glyph(
186                    physical_glyph.cache_key,
187                    &mut driver.font_system.write(),
188                    &mut driver.glyphs.write(),
189                    &driver.device,
190                    &driver.queue,
191                    &mut driver.atlas.write(),
192                    &mut driver.swash_cache.write(),
193                )?;
194
195                if let Some(data) = text::Instance::prepare_glyph(
196                    physical_glyph.x,
197                    physical_glyph.y,
198                    run.line_y,
199                    self.scale,
200                    color,
201                    bounds_min_x,
202                    bounds_min_y,
203                    bounds_max_x,
204                    bounds_max_y,
205                    text::Instance::get_glyph(physical_glyph.cache_key, &driver.glyphs.read())
206                        .ok_or(Error::GlyphCacheFailure)?,
207                )? {
208                    compositor.preprocessed(data);
209                }
210            }
211
212            // Draw cursor
213            if let Some((x, y)) = crate::editor::cursor_position(&self.cursor, &run) {
214                compositor.preprocessed(Self::draw_box(
215                    x as f32 + pos.x,
216                    y as f32 + pos.y,
217                    1.0,
218                    line_height,
219                    bounds,
220                    self.cursor_color,
221                ));
222            }
223        }
224
225        Ok(())
226    }
227}