feather_ui/render/
textbox.rs

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