makepad_render/
text.rs

1
2use crate::cx::*;
3
4#[derive(Clone)]
5pub enum Wrapping {
6    Char,
7    Word,
8    Line,
9    None,
10    Ellipsis(f32)
11}
12
13#[derive(Clone, Copy)]
14pub struct TextStyle {
15    pub font: Font,
16    pub font_size: f32,
17    pub brightness: f32,
18    pub curve: f32,
19    pub line_spacing: f32,
20    pub top_drop: f32,
21    pub height_factor: f32,
22}
23
24impl Default for TextStyle {
25    fn default() -> Self {
26        TextStyle {
27            font: Font::default(),
28            font_size: 8.0,
29            brightness: 1.0,
30            curve: 0.6,
31            line_spacing: 1.4,
32            top_drop: 1.1,
33            height_factor: 1.3,
34        }
35    }
36}
37
38#[derive(Clone)]
39pub struct Text {
40    pub text_style: TextStyle,
41    pub shader: Shader,
42    pub color: Color,
43    pub z: f32,
44    pub wrapping: Wrapping,
45    pub font_scale: f32,
46}
47
48
49impl Text {
50    pub fn new(cx: &mut Cx) -> Self {
51        Self {
52            text_style: TextStyle::default(),
53            shader: cx.add_shader(Self::def_text_shader(), "TextAtlas"),
54            z: 0.0,
55            wrapping: Wrapping::Word,
56            color: color("white"),
57            font_scale: 1.0,
58        }
59    }
60    
61    pub fn instance_font_tc() -> InstanceVec4 {uid!()}
62    pub fn instance_color() -> InstanceColor {uid!()}
63    pub fn instance_x() -> InstanceFloat {uid!()}
64    pub fn instance_y() -> InstanceFloat {uid!()}
65    pub fn instance_w() -> InstanceFloat {uid!()}
66    pub fn instance_h() -> InstanceFloat {uid!()}
67    pub fn instance_z() -> InstanceFloat {uid!()}
68    pub fn instance_base_x() -> InstanceFloat {uid!()}
69    pub fn instance_base_y() -> InstanceFloat {uid!()}
70    pub fn instance_font_size() -> InstanceFloat {uid!()}
71    pub fn instance_marker() -> InstanceFloat {uid!()}
72    pub fn instance_char_offset() -> InstanceFloat {uid!()}
73    
74    pub fn uniform_brightness() -> UniformFloat {uid!()}
75    pub fn uniform_curve() -> UniformFloat {uid!()}
76    
77    pub fn def_text_shader() -> ShaderGen {
78        // lets add the draw shader lib
79        let mut sg = ShaderGen::new();
80        sg.geometry_vertices = vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0];
81        sg.geometry_indices = vec![0, 1, 2, 0, 3, 2];
82        sg.compose(shader_ast!({
83            let geom: vec2<Geometry>;
84            let texturez: texture2d<Texture>;
85            
86            let font_tc: Self::instance_font_tc();
87            let color: Self::instance_color();
88            let x: Self::instance_x();
89            let y: Self::instance_y();
90            let w: Self::instance_w();
91            let h: Self::instance_h();
92            let z: Self::instance_z();
93            let base_x: Self::instance_base_x();
94            let base_y: Self::instance_base_y();
95            let font_size: Self::instance_font_size();
96            let char_offset: Self::instance_char_offset();
97            let marker: Self::instance_marker();
98            
99            let tex_coord1: vec2<Varying>;
100            let tex_coord2: vec2<Varying>;
101            let tex_coord3: vec2<Varying>;
102            let clipped: vec2<Varying>;
103            //let rect: vec4<Varying>;
104            
105            let brightness: Self::uniform_brightness();
106            let curve: Self::uniform_curve();
107            
108            fn get_color()->vec4{
109                return color
110            }
111            
112            fn pixel() -> vec4 {
113                let dx = dfdx(vec2(tex_coord1.x * 2048.0, 0.)).x;
114                let dp = 1.0 / 2048.0;
115                
116                // basic hardcoded mipmapping so it stops 'swimming' in VR
117                // mipmaps are stored in red/green/blue channel
118                let s = 1.0;
119                if dx > 5.0 {
120                    s = 0.7;
121                }
122                else if dx > 2.75 { 
123                    s = (
124                        sample2d(texturez, tex_coord3.xy + vec2(0., 0.)).z
125                            + sample2d(texturez, tex_coord3.xy + vec2(dp, 0.)).z
126                            + sample2d(texturez, tex_coord3.xy + vec2(0., dp)).z
127                            + sample2d(texturez, tex_coord3.xy + vec2(dp, dp)).z
128                    ) * 0.25;
129                }
130                else if dx > 1.75 { 
131                    s = sample2d(texturez, tex_coord3.xy).z;
132                }
133                else if dx > 1.3 {
134                    s = sample2d(texturez, tex_coord2.xy).y;
135                }
136                else {
137                    s = sample2d(texturez, tex_coord1.xy).x;
138                }
139                
140                s = pow(s, curve);
141                let col = get_color();
142                return vec4(s * col.rgb * brightness * col.a, s * col.a); // + color("#a");
143            }
144            
145            fn vertex() -> vec4 {
146                let min_pos = vec2(x, y);
147                let max_pos = vec2(x + w, y - h);
148                
149                clipped = clamp(
150                    mix(min_pos, max_pos, geom) - draw_scroll.xy,
151                    draw_clip.xy,
152                    draw_clip.zw
153                );
154                
155                let normalized: vec2 = (clipped - min_pos + draw_scroll.xy) / vec2(w,-h);
156                //rect = vec4(min_pos.x, min_pos.y, max_pos.x, max_pos.y) - draw_scroll.xyxy;
157                
158                tex_coord1 = mix(
159                    font_tc.xy,
160                    font_tc.zw,
161                    normalized.xy
162                );
163                
164                tex_coord2 = mix(
165                    font_tc.xy,
166                    font_tc.xy + (font_tc.zw - font_tc.xy) * 0.75,
167                    normalized.xy
168                );
169                
170                tex_coord3 = mix(
171                    font_tc.xy,
172                    font_tc.xy + (font_tc.zw - font_tc.xy) * 0.6,
173                    normalized.xy
174                );
175                
176                return camera_projection * (camera_view * (view_transform * vec4(clipped.x, clipped.y, z + draw_zbias, 1.)));
177            }
178        }))
179    }
180    
181    pub fn begin_text(&mut self, cx: &mut Cx) -> AlignedInstance {
182        
183        //let font_id = self.font.font_id.unwrap();
184        let inst = cx.new_instance(&self.shader, 0);
185        let aligned = cx.align_instance(inst);
186        let text_style = &self.text_style;
187        let brightness = text_style.brightness;
188        let curve = text_style.curve;
189        if aligned.inst.need_uniforms_now(cx) {
190            aligned.inst.push_uniform_texture_2d_id(cx, cx.fonts_atlas.texture_id);
191            aligned.inst.push_uniform_float(cx, brightness);
192            aligned.inst.push_uniform_float(cx, curve);
193        }
194        return aligned
195    }
196    
197    pub fn add_text<F>(&mut self, cx: &mut Cx, geom_x: f32, geom_y: f32, char_offset: usize, aligned: &mut AlignedInstance, chunk: &[char], mut char_callback: F)
198    where F: FnMut(char, usize, f32, f32) -> f32
199    {
200        let text_style = &self.text_style;
201        let mut geom_x = geom_x;
202        let mut char_offset = char_offset;
203        let font_id = text_style.font.font_id.unwrap();
204        
205        let cxfont = &mut cx.fonts[font_id];
206        
207        let dpi_factor = cx.current_dpi_factor;
208        
209        //let geom_y = (geom_y * dpi_factor).floor() / dpi_factor;
210        let atlas_page_id = cxfont.get_atlas_page_id(dpi_factor, text_style.font_size);
211        
212        let font = &mut cxfont.font_loaded.as_ref().unwrap();
213        
214        let font_size_logical = text_style.font_size * 96.0 / (72.0 * font.units_per_em);
215        let font_size_pixels = font_size_logical * dpi_factor;
216        
217        let atlas_page = &mut cxfont.atlas_pages[atlas_page_id];
218        
219        let instance = {
220            let cxview = &mut cx.views[aligned.inst.view_id];
221            let draw_call = &mut cxview.draw_calls[aligned.inst.draw_call_id];
222            &mut draw_call.instance
223        };
224        
225        for wc in chunk {
226            let unicode = *wc as usize;
227            let glyph_id = font.char_code_to_glyph_index_map[unicode];
228            if glyph_id >= font.glyphs.len() {
229                println!("GLYPHID OUT OF BOUNDS {} {} len is {}", unicode, glyph_id, font.glyphs.len());
230                continue;
231            }
232            let glyph = &font.glyphs[glyph_id];
233            
234            let advance = glyph.horizontal_metrics.advance_width * font_size_logical * self.font_scale;
235            
236            // snap width/height to pixel granularity
237            let w = ((glyph.bounds.p_max.x - glyph.bounds.p_min.x) * font_size_pixels).ceil() + 1.0;
238            let h = ((glyph.bounds.p_max.y - glyph.bounds.p_min.y) * font_size_pixels).ceil() + 1.0;
239            
240            // this one needs pixel snapping
241            let min_pos_x = geom_x + font_size_logical * glyph.bounds.p_min.x;
242            let min_pos_y = geom_y - font_size_logical * glyph.bounds.p_min.y + text_style.font_size * text_style.top_drop;
243            
244            // compute subpixel shift
245            let subpixel_x_fract = min_pos_x - (min_pos_x * dpi_factor).floor() / dpi_factor;
246            let subpixel_y_fract = min_pos_y - (min_pos_y * dpi_factor).floor() / dpi_factor;
247            
248            // scale and snap it
249            let scaled_min_pos_x = geom_x + font_size_logical * self.font_scale * glyph.bounds.p_min.x - subpixel_x_fract;
250            let scaled_min_pos_y = geom_y - font_size_logical * self.font_scale * glyph.bounds.p_min.y + text_style.font_size * self.font_scale * text_style.top_drop - subpixel_y_fract;
251            
252            // only use a subpixel id for small fonts
253            let subpixel_id = if text_style.font_size>32.0 {
254                0
255            }
256            else { // subtle 64 index subpixel id
257                ((subpixel_y_fract * 7.0) as usize) << 3 |
258                (subpixel_x_fract * 7.0) as usize
259            };
260            
261            let tc = if let Some(tc) = &atlas_page.atlas_glyphs[glyph_id][subpixel_id] {
262                //println!("{} {} {} {}", tc.tx1,tc.tx2,tc.ty1,tc.ty2);
263                tc
264            }
265            else {
266                // see if we can fit it
267                // allocate slot
268                cx.fonts_atlas.atlas_todo.push(CxFontsAtlasTodo {
269                    subpixel_x_fract,
270                    subpixel_y_fract,
271                    font_id,
272                    atlas_page_id,
273                    glyph_id,
274                    subpixel_id
275                });
276                
277                atlas_page.atlas_glyphs[glyph_id][subpixel_id] = Some(
278                    cx.fonts_atlas.alloc_atlas_glyph(&cxfont.path, w, h)
279                );
280                
281                atlas_page.atlas_glyphs[glyph_id][subpixel_id].as_ref().unwrap()
282            };
283            
284            // give the callback a chance to do things
285            let marker = char_callback(*wc, char_offset, geom_x, advance);
286            
287            let data = [
288                tc.tx1,
289                tc.ty1,
290                tc.tx2,
291                tc.ty2,
292                self.color.r, // color
293                self.color.g,
294                self.color.b,
295                self.color.a,
296                scaled_min_pos_x,
297                scaled_min_pos_y,
298                w * self.font_scale / dpi_factor,
299                h * self.font_scale / dpi_factor,
300                self.z + 0.00001 * min_pos_x, //slight z-bias so we don't get z-fighting with neighbouring chars overlap a bit
301                geom_x,
302                geom_y,
303                text_style.font_size,
304                char_offset as f32, // char_offset
305                marker, // marker
306            ];
307            instance.extend_from_slice(&data);
308            // !TODO make sure a derived shader adds 'empty' values here.
309            
310            geom_x += advance;
311            char_offset += 1;
312            aligned.inst.instance_count += 1;
313        }
314    }
315    
316    pub fn end_text(&mut self, cx: &mut Cx, aligned: &AlignedInstance) -> Area {
317        cx.update_aligned_instance_count(aligned);
318        aligned.inst.into()
319    }
320    
321    pub fn draw_text(&mut self, cx: &mut Cx, text: &str) -> Area {
322        let mut aligned = self.begin_text(cx);
323        
324        let mut chunk = Vec::new();
325        let mut width = 0.0;
326        let mut elipct = 0;
327        let text_style = &self.text_style;
328        let font_size = text_style.font_size;
329        let line_spacing = text_style.line_spacing;
330        let height_factor = text_style.height_factor;
331        let mut iter = text.chars().peekable();
332        
333        let font_id = text_style.font.font_id.unwrap();
334        let font_size_logical = text_style.font_size * 96.0 / (72.0 * cx.fonts[font_id].font_loaded.as_ref().unwrap().units_per_em);
335        
336        while let Some(c) = iter.next() {
337            let last = iter.peek().is_none();
338            
339            let mut emit = last;
340            let mut newline = false;
341            let slot = if c < '\u{10000}' {
342                cx.fonts[font_id].font_loaded.as_ref().unwrap().char_code_to_glyph_index_map[c as usize]
343            } else {
344                0
345            };
346            if c == '\n' {
347                emit = true;
348                newline = true;
349            }            
350            if slot != 0 {
351                let glyph = &cx.fonts[font_id].font_loaded.as_ref().unwrap().glyphs[slot];
352                width += glyph.horizontal_metrics.advance_width * font_size_logical * self.font_scale;
353                match self.wrapping {
354                    Wrapping::Char => {
355                        chunk.push(c);
356                        emit = true
357                    },
358                    Wrapping::Word => {
359                        chunk.push(c);
360                        if c == ' ' || c == '\t' || c == ',' || c == '\n'{
361                            emit = true;
362                        }
363                    },
364                    Wrapping::Line => {
365                        chunk.push(c);
366                        if c == 10 as char || c == 13 as char {
367                            emit = true;
368                        }
369                        newline = true;
370                    },
371                    Wrapping::None => {
372                        chunk.push(c);
373                    },
374                    Wrapping::Ellipsis(ellipsis_width) => {
375                        if width>ellipsis_width { // output ...
376                            if elipct < 3 {
377                                chunk.push('.');
378                                elipct += 1;
379                            }
380                        }
381                        else {
382                            chunk.push(c)
383                        }
384                    }
385                }
386            }
387            if emit {
388                let height = font_size * height_factor * self.font_scale;
389                let geom = cx.walk_turtle(Walk {
390                    width: Width::Fix(width),
391                    height: Height::Fix(height),
392                    margin: Margin::zero()
393                });
394                
395                self.add_text(cx, geom.x, geom.y, 0, &mut aligned, &chunk, | _, _, _, _ | {0.0});
396                width = 0.0;
397                chunk.truncate(0);
398                if newline {
399                    cx.turtle_new_line_min_height(font_size * line_spacing * self.font_scale);
400                }
401            }
402        }
403        self.end_text(cx, &aligned)
404    }
405    
406    // looks up text with the behavior of a text selection mouse cursor
407    pub fn find_closest_offset(&self, cx: &Cx, area: &Area, pos: Vec2) -> usize {
408        let scroll_pos = area.get_scroll_pos(cx);
409        let spos = Vec2 {x: pos.x + scroll_pos.x, y: pos.y + scroll_pos.y};
410        let x_o = area.get_instance_offset(cx, Self::instance_base_x().instance_type()).unwrap();
411        let y_o = area.get_instance_offset(cx, Self::instance_base_y().instance_type()).unwrap();
412        let w_o = area.get_instance_offset(cx, Self::instance_w().instance_type()).unwrap();
413        let font_size_o = area.get_instance_offset(cx, Self::instance_font_size().instance_type()).unwrap();
414        let char_offset_o = area.get_instance_offset(cx, Self::instance_char_offset().instance_type()).unwrap();
415        let read = area.get_read_ref(cx);
416        let text_style = &self.text_style;
417        let line_spacing = text_style.line_spacing;
418        let mut index = 0;
419        if let Some(read) = read {
420            while index < read.count {
421                let y = read.buffer[read.offset + y_o + index * read.slots];
422                let font_size = read.buffer[read.offset + font_size_o + index * read.slots];
423                if y + font_size * line_spacing > spos.y { // alright lets find our next x
424                    while index < read.count {
425                        let x = read.buffer[read.offset + x_o + index * read.slots];
426                        let y = read.buffer[read.offset + y_o + index * read.slots];
427                        //let font_size = read.buffer[read.offset + font_size_o + index* read.slots];
428                        let w = read.buffer[read.offset + w_o + index * read.slots];
429                        if x > spos.x + w * 0.5 || y > spos.y {
430                            let prev_index = if index == 0 {0}else {index - 1};
431                            let prev_x = read.buffer[read.offset + x_o + prev_index * read.slots];
432                            let prev_w = read.buffer[read.offset + w_o + prev_index * read.slots];
433                            if index < read.count - 1 && prev_x > spos.x + prev_w { // fix newline jump-back
434                                return read.buffer[read.offset + char_offset_o + index * read.slots] as usize;
435                            }
436                            return read.buffer[read.offset + char_offset_o + prev_index * read.slots] as usize;
437                        }
438                        index += 1;
439                    }
440                }
441                index += 1;
442            }
443            if read.count == 0 {
444                return 0
445            }
446            return read.buffer[read.offset + char_offset_o + (read.count - 1) * read.slots] as usize;
447        }
448        return 0
449    }
450    
451    pub fn get_monospace_base(&self, cx: &Cx) -> Vec2 {
452        let font_id = self.text_style.font.font_id.unwrap();
453        let font = cx.fonts[font_id].font_loaded.as_ref().unwrap();
454        let slot = font.char_code_to_glyph_index_map[33];
455        let glyph = &font.glyphs[slot];
456        
457        //let font_size = if let Some(font_size) = font_size{font_size}else{self.font_size};
458        Vec2 {
459            x: glyph.horizontal_metrics.advance_width * (96.0 / (72.0 * font.units_per_em)),
460            y: self.text_style.line_spacing
461        }
462    }
463}