wrflib/
text_ins.rs

1// Copyright (c) 2021-present, Cruise LLC
2//
3// This source code is licensed under the Apache License, Version 2.0,
4// found in the LICENSE-APACHE file in the root directory of this source tree.
5// You may not use this file except in compliance with the License.
6
7//! Drawing text.
8
9use std::sync::RwLock;
10
11use crate::*;
12
13#[derive(Clone, Debug)]
14#[repr(C)]
15pub struct TextIns {
16    /// Texture coordinates for the bottom-left corner of the glyph in the texture atlas
17    pub font_t1: Vec2,
18    /// Texture coordinates for the top-right corner of the glyph in the texture atlas
19    pub font_t2: Vec2,
20    /// Color for a glyph, usually set at the same color as [`TextIns`]
21    pub color: Vec4,
22    /// Glyph position in view space
23    pub rect_pos: Vec2,
24    /// Glyph size in view space
25    pub rect_size: Vec2,
26    /// Depth offset (prevents z-fighting)
27    pub char_depth: f32,
28    /// Position used in [`TextIns::closest_offset`].
29    pub base: Vec2,
30    /// Font size in pixels
31    pub font_size: f32,
32    /// Character index in the text string
33    pub char_offset: f32,
34    /// TODO(JP): document.
35    pub marker: f32,
36}
37
38#[repr(C)]
39struct TextInsUniforms {
40    brightness: f32,
41    curve: f32,
42}
43
44pub static TEXT_INS_SHADER: Shader = Shader {
45    build_geom: Some(QuadIns::build_geom),
46    code_to_concatenate: &[
47        Cx::STD_SHADER,
48        code_fragment!(
49            r#"
50            uniform brightness: float;
51            uniform curve: float;
52
53            texture texture: texture2D;
54
55            instance font_t1: vec2;
56            instance font_t2: vec2;
57            instance color: vec4;
58            instance rect_pos: vec2;
59            instance rect_size: vec2;
60            instance char_depth: float;
61            instance base: vec2;
62            instance font_size: float;
63            instance char_offset: float;
64            instance marker: float;
65
66            geometry geom: vec2;
67
68            varying tex_coord1: vec2;
69            varying tex_coord2: vec2;
70            varying tex_coord3: vec2;
71            varying clipped: vec2;
72
73            fn get_color() -> vec4 {
74                return color;
75            }
76
77            fn pixel() -> vec4 {
78                let dx = dFdx(vec2(tex_coord1.x * 2048.0, 0.)).x;
79                let dp = 1.0 / 2048.0;
80
81                // basic hardcoded mipmapping so it stops 'swimming' in VR
82                // mipmaps are stored in red/green/blue channel
83                let s = 1.0;
84
85                if dx > 7.0 {
86                    s = 0.7;
87                }
88                else if dx > 2.75 {
89                    s = (
90                        sample2d(texture, tex_coord3.xy + vec2(0., 0.)).z
91                            + sample2d(texture, tex_coord3.xy + vec2(dp, 0.)).z
92                            + sample2d(texture, tex_coord3.xy + vec2(0., dp)).z
93                            + sample2d(texture, tex_coord3.xy + vec2(dp, dp)).z
94                    ) * 0.25;
95                }
96                else if dx > 1.75 {
97                    s = sample2d(texture, tex_coord3.xy).z;
98                }
99                else if dx > 1.3 {
100                    s = sample2d(texture, tex_coord2.xy).y;
101                }
102                else {
103                    s = sample2d(texture, tex_coord1.xy).x;
104                }
105
106                s = pow(s, curve);
107                let col = get_color(); //color!(white);//get_color();
108                return vec4(s * col.rgb * brightness * col.a, s * col.a);
109            }
110
111            fn vertex() -> vec4 {
112                let min_pos = vec2(rect_pos.x, rect_pos.y);
113                let max_pos = vec2(rect_pos.x + rect_size.x, rect_pos.y - rect_size.y);
114
115                clipped = clamp(
116                    mix(min_pos, max_pos, geom) - draw_scroll,
117                    draw_clip.xy,
118                    draw_clip.zw
119                );
120
121                let normalized: vec2 = (clipped - min_pos + draw_scroll) / vec2(rect_size.x, -rect_size.y);
122                //rect = vec4(min_pos.x, min_pos.y, max_pos.x, max_pos.y) - draw_scroll.xyxy;
123
124                tex_coord1 = mix(
125                    font_t1.xy,
126                    font_t2.xy,
127                    normalized.xy
128                );
129
130                tex_coord2 = mix(
131                    font_t1.xy,
132                    font_t1.xy + (font_t2.xy - font_t1.xy) * 0.75,
133                    normalized.xy
134                );
135
136                tex_coord3 = mix(
137                    font_t1.xy,
138                    font_t1.xy + (font_t2.xy - font_t1.xy) * 0.6,
139                    normalized.xy
140                );
141
142                return camera_projection * (camera_view * vec4(
143                    clipped.x,
144                    clipped.y,
145                    char_depth + draw_zbias,
146                    1.
147                ));
148            }"#
149        ),
150    ],
151    ..Shader::DEFAULT
152};
153
154// Some constants for text anchoring
155// Addition can be used to combine them together: LEFT + TOP
156// Values are multipled by offsets later. For example, CENTER_H
157// uses half of the horizontal offset.
158pub const TEXT_ANCHOR_LEFT: Vec2 = vec2(0., 0.);
159pub const TEXT_ANCHOR_CENTER_H: Vec2 = vec2(0.5, 0.);
160pub const TEXT_ANCHOR_RIGHT: Vec2 = vec2(1., 0.);
161pub const TEXT_ANCHOR_TOP: Vec2 = vec2(0., 0.);
162pub const TEXT_ANCHOR_CENTER_V: Vec2 = vec2(0., 0.5);
163pub const TEXT_ANCHOR_BOTTOM: Vec2 = vec2(0., 1.);
164
165/// Some props for how to render the text.
166#[derive(Debug)]
167pub struct TextInsProps {
168    /// TODO(JP): document.
169    pub text_style: TextStyle,
170    /// TODO(JP): document.
171    pub wrapping: Wrapping,
172    /// TODO(JP): document.
173    pub font_scale: f32,
174    /// TODO(JP): document.
175    pub draw_depth: f32,
176    /// TODO(JP): document.
177    pub color: Vec4,
178    /// By default, the position describes the top-left corner of the string
179    pub position_anchoring: Vec2,
180    /// See [`Padding`].
181    pub padding: Padding,
182}
183impl TextInsProps {
184    /// TODO(JP): Replace these with TextInsProps::default() when
185    /// <https://github.com/rust-lang/rust/issues/67792> gets done
186    pub const DEFAULT: TextInsProps = TextInsProps {
187        text_style: TEXT_STYLE_NORMAL,
188        wrapping: Wrapping::DEFAULT,
189        font_scale: 1.0,
190        draw_depth: 0.0,
191        color: COLOR_WHITE,
192        position_anchoring: vec2(0., 0.),
193        padding: Padding::DEFAULT,
194    };
195}
196impl Default for TextInsProps {
197    fn default() -> Self {
198        TextInsProps::DEFAULT
199    }
200}
201
202/// Determines when to emit a set of glyphs, which has roughly the effect of
203/// wrapping at these boundaries.
204#[derive(Copy, Clone, Debug)]
205pub enum Wrapping {
206    None,
207    Char,
208    Word,
209    /// TODO(JP): This seems to be equivalent to Wrapping::None (except for strings
210    /// with specifically char code 10 as newline) because we already do a check
211    /// to set emit=true and newline=true (and note that the newline=true
212    /// doesn't do anything without emit=true).
213    Line,
214    Ellipsis(f32),
215}
216impl Wrapping {
217    /// TODO(JP): Replace these with Wrapping::default() when
218    /// <https://github.com/rust-lang/rust/issues/67792> gets done
219    pub const DEFAULT: Wrapping = Wrapping::None;
220}
221impl Default for Wrapping {
222    fn default() -> Self {
223        Wrapping::DEFAULT
224    }
225}
226
227#[derive(Default)]
228pub struct DrawGlyphsProps {
229    pub text_style: TextStyle,
230    pub position_anchoring: Vec2,
231}
232
233impl TextIns {
234    pub fn generate_2d_glyphs<F>(
235        text_style: &TextStyle,
236        fonts_data: &RwLock<CxFontsData>,
237        dpi_factor: f32,
238        font_scale: f32,
239        draw_depth: f32,
240        color: Vec4,
241        pos: Vec2,
242        char_offset: usize,
243        chunk: &[char],
244        mut char_callback: F,
245    ) -> Vec<TextIns>
246    where
247        F: FnMut(char, usize, f32, f32) -> f32,
248    {
249        let mut ret = Vec::with_capacity(chunk.len());
250
251        let font_id = text_style.font.font_id;
252
253        let (atlas_page_id, mut read_lock) = get_font_atlas_page_id(fonts_data, font_id, dpi_factor, text_style.font_size);
254
255        let font_size_logical =
256            text_style.font_size * 96.0 / (72.0 * read_lock.fonts[font_id].font_loaded.as_ref().unwrap().units_per_em);
257        let font_size_pixels = font_size_logical * dpi_factor;
258
259        let mut x = pos.x;
260        let mut char_offset = char_offset;
261
262        for wc in chunk {
263            let unicode = *wc as usize;
264
265            // Scope the `cxfont` borrow to these variables.
266            let (glyph_id, advance, w, h, min_pos_x, subpixel_x_fract, subpixel_y_fract, scaled_min_pos_x, scaled_min_pos_y) = {
267                let cxfont = read_lock.fonts[font_id].font_loaded.as_ref().unwrap();
268                let glyph_id = cxfont.char_code_to_glyph_index_map[unicode];
269                if glyph_id >= cxfont.glyphs.len() {
270                    println!("GLYPHID OUT OF BOUNDS {} {} len is {}", unicode, glyph_id, cxfont.glyphs.len());
271                    continue;
272                }
273
274                let glyph = &cxfont.glyphs[glyph_id];
275
276                let advance = glyph.horizontal_metrics.advance_width * font_size_logical * font_scale;
277
278                // snap width/height to pixel granularity
279                let w = ((glyph.bounds.p_max.x - glyph.bounds.p_min.x) * font_size_pixels).ceil() + 1.0;
280                let h = ((glyph.bounds.p_max.y - glyph.bounds.p_min.y) * font_size_pixels).ceil() + 1.0;
281
282                // this one needs pixel snapping
283                let min_pos_x = x + font_size_logical * glyph.bounds.p_min.x;
284                let min_pos_y = pos.y - font_size_logical * glyph.bounds.p_min.y + text_style.font_size * text_style.top_drop;
285
286                // compute subpixel shift
287                let subpixel_x_fract = min_pos_x - (min_pos_x * dpi_factor).floor() / dpi_factor;
288                let subpixel_y_fract = min_pos_y - (min_pos_y * dpi_factor).floor() / dpi_factor;
289
290                // scale and snap it
291                let scaled_min_pos_x = x + font_size_logical * font_scale * glyph.bounds.p_min.x - subpixel_x_fract;
292                let scaled_min_pos_y = pos.y - font_size_logical * font_scale * glyph.bounds.p_min.y
293                    + text_style.font_size * font_scale * text_style.top_drop
294                    - subpixel_y_fract;
295
296                (glyph_id, advance, w, h, min_pos_x, subpixel_x_fract, subpixel_y_fract, scaled_min_pos_x, scaled_min_pos_y)
297            };
298
299            // only use a subpixel id for small fonts
300            let subpixel_id = if text_style.font_size > 32.0 {
301                0
302            } else {
303                // subtle 64 index subpixel id
304                ((subpixel_y_fract * 7.0) as usize) << 3 | (subpixel_x_fract * 7.0) as usize
305            };
306
307            let tc = if let Some(tc) = read_lock.fonts[font_id].atlas_pages[atlas_page_id].atlas_glyphs[glyph_id][subpixel_id] {
308                tc
309            } else {
310                // Drop `read_lock` to do some writes, and then reacquire it.
311                drop(read_lock);
312                {
313                    let mut write_fonts_data = fonts_data.write().unwrap();
314
315                    write_fonts_data.fonts_atlas.atlas_todo.push(CxFontsAtlasTodo {
316                        subpixel_x_fract,
317                        subpixel_y_fract,
318                        font_id,
319                        atlas_page_id,
320                        glyph_id,
321                        subpixel_id,
322                    });
323
324                    let new_glyph = write_fonts_data.fonts_atlas.alloc_atlas_glyph(w, h);
325                    write_fonts_data.fonts[font_id].atlas_pages[atlas_page_id].atlas_glyphs[glyph_id][subpixel_id] =
326                        Some(new_glyph);
327                }
328                read_lock = fonts_data.read().unwrap();
329                read_lock.fonts[font_id].atlas_pages[atlas_page_id].atlas_glyphs[glyph_id][subpixel_id].unwrap()
330            };
331
332            ret.push(TextIns {
333                font_t1: vec2(tc.tx1, tc.ty1),
334                font_t2: vec2(tc.tx2, tc.ty2),
335                color,
336                rect_pos: vec2(scaled_min_pos_x, scaled_min_pos_y),
337                rect_size: vec2(w * font_scale / dpi_factor, h * font_scale / dpi_factor),
338                char_depth: draw_depth + 0.00001 * min_pos_x,
339                base: vec2(x, pos.y),
340                font_size: text_style.font_size,
341                char_offset: char_offset as f32,
342
343                // give the callback a chance to do things
344                marker: char_callback(*wc, char_offset, x, advance),
345            });
346
347            x += advance;
348            char_offset += 1;
349        }
350
351        ret
352    }
353
354    pub fn set_color(cx: &mut Cx, area: Area, color: Vec4) {
355        let glyphs = area.get_slice_mut::<TextIns>(cx);
356        for glyph in glyphs {
357            glyph.color = color;
358        }
359    }
360
361    fn write_uniforms(cx: &mut Cx, area: &Area, text_style: &TextStyle) {
362        if area.is_first_instance() {
363            let texture_handle = cx.fonts_data.read().unwrap().get_fonts_atlas_texture_handle();
364            area.write_texture_2d(cx, "texture", texture_handle);
365            area.write_user_uniforms(cx, TextInsUniforms { brightness: text_style.brightness, curve: text_style.curve });
366        }
367    }
368
369    pub fn draw_glyphs(cx: &mut Cx, glyphs: &[TextIns], props: &DrawGlyphsProps) -> Area {
370        let area = if props.position_anchoring != vec2(0., 0.) {
371            // The horizontal offset is based on the total size of the string
372            let horizontal_offset = glyphs.iter().map(|g| g.rect_size.x).sum();
373            let vertical_offset = {
374                // The vertical offset is the logical size of the font
375                let text_style = TextInsProps::DEFAULT.text_style;
376                text_style.font_size * text_style.top_drop
377            };
378            let offset = vec2(horizontal_offset, vertical_offset);
379            let anchor_offset = offset * props.position_anchoring;
380
381            let moved_glyphs: Vec<TextIns> = glyphs
382                .iter()
383                .map(|g| {
384                    let mut g = g.clone();
385                    g.rect_pos -= anchor_offset; // Offset must be subtracted
386                    g
387                })
388                .collect();
389            cx.add_instances(&TEXT_INS_SHADER, &moved_glyphs)
390        } else {
391            cx.add_instances(&TEXT_INS_SHADER, glyphs)
392        };
393        Self::write_uniforms(cx, &area, &props.text_style);
394        area
395    }
396
397    pub fn draw_glyphs_with_scroll_sticky(
398        cx: &mut Cx,
399        glyphs: &[TextIns],
400        text_style: &TextStyle,
401        horizontal: bool,
402        vertical: bool,
403    ) -> Area {
404        let area = cx.add_instances_with_scroll_sticky(&TEXT_INS_SHADER, glyphs, horizontal, vertical);
405        Self::write_uniforms(cx, &area, text_style);
406        area
407    }
408
409    pub fn draw_str(cx: &mut Cx, text: &str, pos: Vec2, props: &TextInsProps) -> Area {
410        let glyphs = Self::generate_2d_glyphs(
411            &props.text_style,
412            &cx.fonts_data,
413            cx.current_dpi_factor,
414            props.font_scale,
415            props.draw_depth,
416            props.color,
417            pos,
418            0,
419            &text.chars().collect::<Vec<char>>(),
420            |_, _, _, _| 0.0,
421        );
422
423        Self::draw_glyphs(
424            cx,
425            &glyphs,
426            &DrawGlyphsProps { text_style: props.text_style, position_anchoring: props.position_anchoring },
427        )
428    }
429
430    /// TODO(JP): This doesn't seem to work well with [`Direction::Down`] (or other directions for
431    /// that matter). Not a high priority but might good to be aware of.
432    ///
433    /// [`TextInsProps::position_anchoring`] is ignored by this function.
434    pub fn draw_walk(cx: &mut Cx, text: &str, props: &TextInsProps) -> Area {
435        let mut width = 0.0;
436        let mut elipct = 0;
437
438        let text_style = &props.text_style;
439        let font_size = text_style.font_size;
440        let line_spacing = text_style.line_spacing;
441        let height_factor = text_style.height_factor;
442        let mut iter = text.chars().peekable();
443
444        let font_id = text_style.font.font_id;
445        let font_size_logical = text_style.font_size * 96.0
446            / (72.0 * cx.fonts_data.read().unwrap().fonts[font_id].font_loaded.as_ref().unwrap().units_per_em);
447
448        let mut buf = Vec::with_capacity(text.len());
449        let mut glyphs: Vec<TextIns> = Vec::with_capacity(text.len());
450
451        cx.begin_row(Width::Compute, Height::Compute);
452        cx.begin_padding_box(props.padding);
453        cx.begin_wrapping_box();
454
455        while let Some(c) = iter.next() {
456            let last = iter.peek().is_none();
457
458            let mut emit = last;
459            let mut newline = false;
460            let slot = if c < '\u{10000}' {
461                cx.fonts_data.read().unwrap().fonts[font_id].font_loaded.as_ref().unwrap().char_code_to_glyph_index_map
462                    [c as usize]
463            } else {
464                0
465            };
466            if c == '\n' {
467                emit = true;
468                newline = true;
469            }
470            if slot != 0 {
471                let read_fonts = &cx.fonts_data.read().unwrap().fonts;
472                let glyph = &read_fonts[font_id].font_loaded.as_ref().unwrap().glyphs[slot];
473                width += glyph.horizontal_metrics.advance_width * font_size_logical * props.font_scale;
474                match props.wrapping {
475                    Wrapping::Char => {
476                        buf.push(c);
477                        emit = true
478                    }
479                    Wrapping::Word => {
480                        buf.push(c);
481                        if c == ' ' || c == '\t' || c == ',' || c == '\n' {
482                            emit = true;
483                        }
484                    }
485                    Wrapping::Line => {
486                        buf.push(c);
487                        if c == 10 as char || c == 13 as char {
488                            emit = true;
489                        }
490                        newline = true;
491                    }
492                    Wrapping::None => {
493                        buf.push(c);
494                    }
495                    Wrapping::Ellipsis(ellipsis_width) => {
496                        if width > ellipsis_width {
497                            // output ...
498                            if elipct < 3 {
499                                buf.push('.');
500                                elipct += 1;
501                            }
502                        } else {
503                            buf.push(c)
504                        }
505                    }
506                }
507            }
508            if emit {
509                let height = font_size * height_factor * props.font_scale;
510                let rect = cx.add_box(LayoutSize { width: Width::Fix(width), height: Height::Fix(height) });
511
512                if !rect.pos.x.is_nan() && !rect.pos.y.is_nan() {
513                    glyphs.extend(Self::generate_2d_glyphs(
514                        &props.text_style,
515                        &cx.fonts_data,
516                        cx.current_dpi_factor,
517                        props.font_scale,
518                        props.draw_depth,
519                        props.color,
520                        rect.pos,
521                        0,
522                        &buf,
523                        |_, _, _, _| 0.0,
524                    ));
525                }
526
527                width = 0.0;
528                buf.truncate(0);
529                if newline {
530                    cx.draw_new_line_min_height(font_size * line_spacing * props.font_scale);
531                }
532            }
533        }
534
535        cx.end_wrapping_box();
536        cx.end_padding_box();
537        cx.end_row();
538
539        Self::draw_glyphs(
540            cx,
541            &glyphs,
542            &DrawGlyphsProps {
543                text_style: *text_style,
544                // Position anchoring is ignored when using walk
545                ..DrawGlyphsProps::default()
546            },
547        )
548    }
549
550    /// Looks up text with the behavior of a text selection mouse cursor.
551    pub fn closest_offset(cx: &Cx, area: &Area, pos: Vec2, line_spacing: f32) -> Option<usize> {
552        if let Area::InstanceRange(instance) = area {
553            if instance.instance_count == 0 {
554                return None;
555            }
556        }
557
558        let scroll_pos = area.get_scroll_pos(cx);
559        let spos = Vec2 { x: pos.x + scroll_pos.x, y: pos.y + scroll_pos.y };
560
561        let glyphs = area.get_slice::<TextIns>(cx);
562        let mut i = 0;
563        let len = glyphs.len();
564        while i < len {
565            let glyph = &glyphs[i];
566            if glyph.base.y + glyph.font_size * line_spacing > spos.y {
567                // Find a matching character within this line.
568                while i < len {
569                    let glyph = &glyphs[i];
570                    let width = glyph.rect_size.x;
571                    if glyph.base.x > spos.x + width * 0.5 || glyph.base.y > spos.y {
572                        let prev_glyph = &glyphs[if i == 0 { 0 } else { i - 1 }];
573                        let prev_width = prev_glyph.rect_size.x;
574                        if i < len - 1 && prev_glyph.base.x > spos.x + prev_width {
575                            // fix newline jump-back
576                            return Some(glyph.char_offset as usize);
577                        }
578                        return Some(prev_glyph.char_offset as usize);
579                    }
580                    i += 1;
581                }
582            }
583            i += 1;
584        }
585        Some(glyphs[len - 1].char_offset as usize)
586    }
587
588    pub fn get_monospace_base(cx: &Cx, text_style: &TextStyle) -> Vec2 {
589        let font_id = text_style.font.font_id;
590        let read_fonts = &cx.fonts_data.read().unwrap().fonts;
591        let font = read_fonts[font_id].font_loaded.as_ref().unwrap();
592        let slot = font.char_code_to_glyph_index_map[33];
593        let glyph = &font.glyphs[slot];
594
595        //let font_size = if let Some(font_size) = font_size{font_size}else{self.font_size};
596        Vec2 { x: glyph.horizontal_metrics.advance_width * (96.0 / (72.0 * font.units_per_em)), y: text_style.line_spacing }
597    }
598}