Skip to main content

cranpose_render_wgpu/
lib.rs

1//! WGPU renderer backend for GPU-accelerated 2D rendering.
2//!
3//! This renderer uses WGPU for cross-platform GPU support across
4//! desktop (Windows/Mac/Linux), web (WebGPU), and mobile (Android/iOS).
5
6mod effect_renderer;
7mod offscreen;
8mod pipeline;
9mod render;
10mod scene;
11mod shader_cache;
12mod shaders;
13
14pub use scene::{BackdropLayer, ClickAction, DrawShape, HitRegion, ImageDraw, Scene, TextDraw};
15
16use cranpose_core::{MemoryApplier, NodeId};
17use cranpose_render_common::{RenderScene, Renderer};
18use cranpose_ui::{set_text_measurer, LayoutTree, TextMeasurer};
19use cranpose_ui_graphics::Size;
20use glyphon::{Attrs, Buffer, FontSystem, Metrics, Shaping};
21use lru::LruCache;
22use render::GpuRenderer;
23use rustc_hash::FxHasher;
24use std::collections::HashMap;
25use std::hash::{Hash, Hasher};
26use std::num::NonZeroUsize;
27use std::rc::Rc;
28use std::sync::{Arc, Mutex};
29
30/// Size-only cache for ultra-fast text measurement lookups.
31/// Key: (text_hash, font_size_fixed_point)
32/// Value: (text_content, size) - text stored to handle hash collisions
33type TextSizeCache = Arc<Mutex<LruCache<(u64, i32), (String, Size)>>>;
34
35#[derive(Debug)]
36pub enum WgpuRendererError {
37    Layout(String),
38    Wgpu(String),
39}
40
41/// CPU-readable RGBA frame captured from the renderer output.
42#[derive(Debug, Clone)]
43pub struct CapturedFrame {
44    pub width: u32,
45    pub height: u32,
46    pub pixels: Vec<u8>,
47}
48
49/// Unified hash key for text caching - shared between measurement and rendering.
50#[derive(Clone, PartialEq, Eq, Hash)]
51pub(crate) enum TextKey {
52    Content(String),
53    Node(NodeId),
54}
55
56#[derive(Clone, PartialEq, Eq)]
57pub(crate) struct TextCacheKey {
58    key: TextKey,
59    scale_bits: u32, // f32 as bits for hashing
60}
61
62impl TextCacheKey {
63    fn new(text: &str, font_size: f32) -> Self {
64        Self {
65            key: TextKey::Content(text.to_string()),
66            scale_bits: font_size.to_bits(),
67        }
68    }
69
70    fn for_node(node_id: NodeId, font_size: f32) -> Self {
71        Self {
72            key: TextKey::Node(node_id),
73            scale_bits: font_size.to_bits(),
74        }
75    }
76}
77
78impl Hash for TextCacheKey {
79    fn hash<H: Hasher>(&self, state: &mut H) {
80        self.key.hash(state);
81        self.scale_bits.hash(state);
82    }
83}
84
85/// Cached text buffer shared between measurement and rendering
86pub(crate) struct SharedTextBuffer {
87    pub(crate) buffer: Buffer,
88    text: String,
89    font_size: f32,
90    /// Cached size to avoid recalculating on every access
91    cached_size: Option<Size>,
92}
93
94impl SharedTextBuffer {
95    /// Ensure the buffer has the correct text and font_size, only reshaping if needed
96    pub(crate) fn ensure(
97        &mut self,
98        font_system: &mut FontSystem,
99        text: &str,
100        font_size: f32,
101        attrs: Attrs,
102    ) {
103        let text_changed = self.text != text;
104        let font_changed = (self.font_size - font_size).abs() > 0.1;
105
106        // Only reshape if something actually changed
107        if !text_changed && !font_changed {
108            return; // Nothing changed, skip reshape
109        }
110
111        // Set metrics and size for unlimited layout
112        let metrics = Metrics::new(font_size, font_size * 1.4);
113        self.buffer.set_metrics(font_system, metrics);
114        self.buffer
115            .set_size(font_system, Some(f32::MAX), Some(f32::MAX));
116
117        // Set text and shape
118        self.buffer
119            .set_text(font_system, text, &attrs, Shaping::Advanced);
120        self.buffer.shape_until_scroll(font_system, false);
121
122        // Update cached values
123        self.text.clear();
124        self.text.push_str(text);
125        self.font_size = font_size;
126        self.cached_size = None; // Invalidate size cache
127    }
128
129    /// Get or calculate the size of the shaped text
130    pub(crate) fn size(&mut self, font_size: f32) -> Size {
131        if let Some(size) = self.cached_size {
132            return size;
133        }
134
135        // Calculate size from buffer
136        let mut max_width = 0.0f32;
137        let layout_runs = self.buffer.layout_runs();
138        for run in layout_runs {
139            max_width = max_width.max(run.line_w);
140        }
141        let total_height = self.buffer.lines.len() as f32 * font_size * 1.4;
142
143        let size = Size {
144            width: max_width,
145            height: total_height,
146        };
147
148        self.cached_size = Some(size);
149        size
150    }
151}
152
153/// Shared cache for text buffers used by both measurement and rendering
154pub(crate) type SharedTextCache = Arc<Mutex<HashMap<TextCacheKey, SharedTextBuffer>>>;
155
156/// Trim text cache if it exceeds MAX_CACHE_ITEMS.
157/// Removes the oldest half of entries when limit is reached.
158pub(crate) fn trim_text_cache(cache: &mut HashMap<TextCacheKey, SharedTextBuffer>) {
159    if cache.len() > MAX_CACHE_ITEMS {
160        let target_size = MAX_CACHE_ITEMS / 2;
161        let to_remove = cache.len() - target_size;
162
163        // Remove oldest entries (arbitrary keys from the front)
164        let keys_to_remove: Vec<TextCacheKey> = cache.keys().take(to_remove).cloned().collect();
165
166        for key in keys_to_remove {
167            cache.remove(&key);
168        }
169
170        log::debug!(
171            "Trimmed text cache from {} to {} entries",
172            cache.len() + to_remove,
173            cache.len()
174        );
175    }
176}
177
178/// Maximum number of cached text buffers before trimming occurs
179const MAX_CACHE_ITEMS: usize = 256;
180
181/// WGPU-based renderer for GPU-accelerated 2D rendering.
182///
183/// This renderer supports:
184/// - GPU-accelerated shape rendering (rectangles, rounded rectangles)
185/// - Gradients (solid, linear, radial)
186/// - GPU text rendering via glyphon
187/// - Cross-platform support (Desktop, Web, Mobile)
188pub struct WgpuRenderer {
189    scene: Scene,
190    gpu_renderer: Option<GpuRenderer>,
191    font_system: Arc<Mutex<FontSystem>>,
192    /// Shared text buffer cache used by both measurement and rendering
193    text_cache: SharedTextCache,
194    /// Root scale factor for text rendering (use for density scaling)
195    root_scale: f32,
196}
197
198impl WgpuRenderer {
199    /// Create a new WGPU renderer with the specified font data.
200    ///
201    /// This is the recommended constructor for applications.
202    /// Call `init_gpu` before rendering.
203    ///
204    /// # Example
205    ///
206    /// ```text
207    /// let font_light = include_bytes!("path/to/font-light.ttf");
208    /// let font_regular = include_bytes!("path/to/font-regular.ttf");
209    /// let renderer = WgpuRenderer::new_with_fonts(&[font_light, font_regular]);
210    /// ```
211    pub fn new_with_fonts(fonts: &[&[u8]]) -> Self {
212        let mut font_system = FontSystem::new();
213
214        // On Android, DO NOT load system fonts
215        // Modern Android uses Variable Fonts for Roboto which can cause
216        // rasterization corruption or font ID conflicts with glyphon.
217        // Use only our bundled static Roboto fonts for consistent rendering.
218        #[cfg(target_os = "android")]
219        {
220            log::info!("Skipping Android system fonts - using application-provided fonts");
221            // font_system.db_mut().load_fonts_dir("/system/fonts");  // DISABLED
222        }
223
224        // Load application-provided fonts
225        for (i, font_data) in fonts.iter().enumerate() {
226            log::info!("Loading font #{}, size: {} bytes", i, font_data.len());
227            font_system.db_mut().load_font_data(font_data.to_vec());
228        }
229
230        let face_count = font_system.db().faces().count();
231        log::info!("Total font faces loaded: {}", face_count);
232
233        if face_count == 0 {
234            log::error!("No fonts loaded! Text rendering will fail!");
235        }
236
237        let font_system = Arc::new(Mutex::new(font_system));
238
239        // Create shared text cache for both measurement and rendering
240        let text_cache = Arc::new(Mutex::new(HashMap::new()));
241
242        let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
243        set_text_measurer(text_measurer.clone());
244
245        Self {
246            scene: Scene::new(),
247            gpu_renderer: None,
248            font_system,
249            text_cache,
250            root_scale: 1.0,
251        }
252    }
253
254    /// Create a new WGPU renderer without any fonts.
255    ///
256    /// **Warning:** This is for internal use only. Applications should use `new_with_fonts()`.
257    /// Text rendering will fail without fonts.
258    pub fn new() -> Self {
259        let font_system = FontSystem::new();
260        let font_system = Arc::new(Mutex::new(font_system));
261        let text_cache = Arc::new(Mutex::new(HashMap::new()));
262
263        let text_measurer = WgpuTextMeasurer::new(font_system.clone(), text_cache.clone());
264        set_text_measurer(text_measurer.clone());
265
266        Self {
267            scene: Scene::new(),
268            gpu_renderer: None,
269            font_system,
270            text_cache,
271            root_scale: 1.0,
272        }
273    }
274
275    /// Initialize GPU resources with a WGPU device and queue.
276    pub fn init_gpu(
277        &mut self,
278        device: Arc<wgpu::Device>,
279        queue: Arc<wgpu::Queue>,
280        surface_format: wgpu::TextureFormat,
281    ) {
282        self.gpu_renderer = Some(GpuRenderer::new(
283            device,
284            queue,
285            surface_format,
286            self.font_system.clone(),
287            self.text_cache.clone(),
288        ));
289    }
290
291    /// Set root scale factor for text rendering (e.g., density scaling on Android)
292    pub fn set_root_scale(&mut self, scale: f32) {
293        self.root_scale = scale;
294    }
295
296    /// Render the scene to a texture view.
297    pub fn render(
298        &mut self,
299        view: &wgpu::TextureView,
300        width: u32,
301        height: u32,
302    ) -> Result<(), WgpuRendererError> {
303        if let Some(gpu_renderer) = &mut self.gpu_renderer {
304            gpu_renderer
305                .render(
306                    view,
307                    &self.scene.shapes,
308                    &self.scene.images,
309                    &self.scene.texts,
310                    &self.scene.shadow_draws,
311                    &self.scene.effect_layers,
312                    &self.scene.backdrop_layers,
313                    width,
314                    height,
315                    self.root_scale,
316                )
317                .map_err(WgpuRendererError::Wgpu)
318        } else {
319            Err(WgpuRendererError::Wgpu(
320                "GPU renderer not initialized. Call init_gpu() first.".to_string(),
321            ))
322        }
323    }
324
325    /// Render the current scene into an RGBA pixel buffer for robot tests.
326    ///
327    /// Uses the renderer's configured root scale.
328    pub fn capture_frame(
329        &mut self,
330        width: u32,
331        height: u32,
332    ) -> Result<CapturedFrame, WgpuRendererError> {
333        self.capture_frame_with_scale(width, height, self.root_scale)
334    }
335
336    /// Render the current scene into an RGBA pixel buffer with an explicit scale.
337    pub fn capture_frame_with_scale(
338        &mut self,
339        width: u32,
340        height: u32,
341        root_scale: f32,
342    ) -> Result<CapturedFrame, WgpuRendererError> {
343        if let Some(gpu_renderer) = &mut self.gpu_renderer {
344            let pixels = gpu_renderer
345                .render_to_rgba_pixels(
346                    &self.scene.shapes,
347                    &self.scene.images,
348                    &self.scene.texts,
349                    &self.scene.shadow_draws,
350                    &self.scene.effect_layers,
351                    &self.scene.backdrop_layers,
352                    width,
353                    height,
354                    root_scale,
355                )
356                .map_err(WgpuRendererError::Wgpu)?;
357            Ok(CapturedFrame {
358                width,
359                height,
360                pixels,
361            })
362        } else {
363            Err(WgpuRendererError::Wgpu(
364                "GPU renderer not initialized. Call init_gpu() first.".to_string(),
365            ))
366        }
367    }
368
369    /// Get access to the WGPU device (for surface configuration).
370    pub fn device(&self) -> &wgpu::Device {
371        self.gpu_renderer
372            .as_ref()
373            .map(|r| &*r.device)
374            .expect("GPU renderer not initialized")
375    }
376}
377
378impl Default for WgpuRenderer {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384impl Renderer for WgpuRenderer {
385    type Scene = Scene;
386    type Error = WgpuRendererError;
387
388    fn scene(&self) -> &Self::Scene {
389        &self.scene
390    }
391
392    fn scene_mut(&mut self) -> &mut Self::Scene {
393        &mut self.scene
394    }
395
396    fn rebuild_scene(
397        &mut self,
398        layout_tree: &LayoutTree,
399        _viewport: Size,
400    ) -> Result<(), Self::Error> {
401        self.scene.clear();
402        // Build scene in logical dp - scaling happens in GPU vertex upload
403        pipeline::render_layout_tree(layout_tree.root(), &mut self.scene);
404        Ok(())
405    }
406
407    fn rebuild_scene_from_applier(
408        &mut self,
409        applier: &mut MemoryApplier,
410        root: NodeId,
411        _viewport: Size,
412    ) -> Result<(), Self::Error> {
413        self.scene.clear();
414        // Build scene in logical dp - scaling happens in GPU vertex upload
415        // Traverse layout nodes via applier instead of rebuilding LayoutTree
416        pipeline::render_from_applier(applier, root, &mut self.scene, 1.0);
417        Ok(())
418    }
419
420    fn draw_dev_overlay(&mut self, text: &str, viewport: Size) {
421        use cranpose_ui_graphics::{BlendMode, Brush, Color, Rect, RoundedCornerShape};
422
423        // Draw FPS text in top-right corner with semi-transparent background
424        // Position: 8px from right edge, 8px from top
425        let padding = 8.0;
426        let font_size = 14.0;
427
428        // Measure text width (approximate: ~7px per character at 14px font)
429        let char_width = 7.0;
430        let text_width = text.len() as f32 * char_width;
431        let text_height = font_size * 1.4;
432
433        let x = viewport.width - text_width - padding * 2.0;
434        let y = padding;
435
436        // Add background rectangle (dark semi-transparent)
437        let bg_rect = Rect {
438            x,
439            y,
440            width: text_width + padding,
441            height: text_height + padding / 2.0,
442        };
443        self.scene.push_shape(
444            bg_rect,
445            Brush::Solid(Color(0.0, 0.0, 0.0, 0.7)),
446            Some(RoundedCornerShape::uniform(4.0)),
447            None,
448            BlendMode::SrcOver,
449        );
450
451        // Add text (green color for visibility)
452        let text_rect = Rect {
453            x: x + padding / 2.0,
454            y: y + padding / 4.0,
455            width: text_width,
456            height: text_height,
457        };
458        self.scene.push_text(
459            NodeId::MAX,
460            text_rect,
461            Rc::from(text),
462            Color(0.0, 1.0, 0.0, 1.0), // Green
463            font_size,
464            1.0,
465            None,
466        );
467    }
468}
469
470fn resolve_font_size(style: &cranpose_ui::text::TextStyle) -> f32 {
471    match style.font_size {
472        cranpose_ui::text::TextUnit::Sp(v) => v,
473        cranpose_ui::text::TextUnit::Em(v) => v * 14.0,
474        cranpose_ui::text::TextUnit::Unspecified => 14.0,
475    }
476}
477
478// Text measurer implementation for WGPU
479
480// Text measurer implementation for WGPU
481
482#[derive(Clone)]
483struct WgpuTextMeasurer {
484    font_system: Arc<Mutex<FontSystem>>,
485    size_cache: TextSizeCache,
486    /// Shared buffer cache used by both measurement and rendering
487    text_cache: SharedTextCache,
488}
489
490impl WgpuTextMeasurer {
491    fn new(font_system: Arc<Mutex<FontSystem>>, text_cache: SharedTextCache) -> Self {
492        Self {
493            font_system,
494            // Larger cache size (1024) reduces misses, FxHasher for faster lookups
495            size_cache: Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(1024).unwrap()))),
496            text_cache,
497        }
498    }
499}
500
501// Base font size in logical units (dp) - shared between measurement and rendering
502
503impl TextMeasurer for WgpuTextMeasurer {
504    fn measure(
505        &self,
506        text: &str,
507        style: &cranpose_ui::text::TextStyle,
508    ) -> cranpose_ui::TextMetrics {
509        let font_size = resolve_font_size(style);
510        let size_int = (font_size * 100.0) as i32;
511
512        // Calculate hash to avoid allocating String for lookup
513        // FxHasher is ~3x faster than DefaultHasher for short strings
514        let mut hasher = FxHasher::default();
515        text.hash(&mut hasher);
516        let text_hash = hasher.finish();
517        let cache_key = (text_hash, size_int);
518
519        // Check size cache first (fastest path)
520        {
521            let mut cache = self.size_cache.lock().unwrap();
522            if let Some((cached_text, size)) = cache.get(&cache_key) {
523                // Verify partial collision
524                if cached_text == text {
525                    return cranpose_ui::TextMetrics {
526                        width: size.width,
527                        height: size.height,
528                        line_height: font_size * 1.4,
529                        line_count: 1, // Cached entries are single-line simplified
530                    };
531                }
532            }
533        }
534
535        // Get or create text buffer
536        let text_buffer_key = TextCacheKey::new(text, font_size);
537        let mut font_system = self.font_system.lock().unwrap();
538        let mut text_cache = self.text_cache.lock().unwrap();
539
540        // Get or create buffer and calculate size
541        let size = {
542            let buffer = text_cache.entry(text_buffer_key).or_insert_with(|| {
543                let buffer =
544                    Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
545                SharedTextBuffer {
546                    buffer,
547                    text: String::new(),
548                    font_size: 0.0,
549                    cached_size: None,
550                }
551            });
552
553            // Ensure buffer has the correct text
554            buffer.ensure(&mut font_system, text, font_size, Attrs::new());
555
556            // Calculate size if not cached
557            buffer.size(font_size)
558        };
559
560        // Trim cache if needed (after we're done with buffer reference)
561        trim_text_cache(&mut text_cache);
562
563        drop(font_system);
564        drop(text_cache);
565
566        // Cache the size result
567        let mut size_cache = self.size_cache.lock().unwrap();
568        // Only allocate string on cache miss
569        size_cache.put(cache_key, (text.to_string(), size));
570
571        // Calculate line info for multiline support
572        let line_height = font_size * 1.4;
573        let line_count = text.split('\n').count().max(1);
574
575        cranpose_ui::TextMetrics {
576            width: size.width,
577            height: size.height,
578            line_height,
579            line_count,
580        }
581    }
582
583    fn get_offset_for_position(
584        &self,
585        text: &str,
586        style: &cranpose_ui::text::TextStyle,
587        x: f32,
588        y: f32,
589    ) -> usize {
590        let font_size = resolve_font_size(style);
591        if text.is_empty() {
592            return 0;
593        }
594
595        let line_height = font_size * 1.4;
596
597        // Calculate which line was clicked based on Y coordinate
598        let line_index = (y / line_height).floor().max(0.0) as usize;
599        let lines: Vec<&str> = text.split('\n').collect();
600        let target_line = line_index.min(lines.len().saturating_sub(1));
601
602        // Calculate byte offset to start of target line
603        let mut line_start_byte = 0;
604        for line in lines.iter().take(target_line) {
605            line_start_byte += line.len() + 1; // +1 for newline
606        }
607
608        // Get the text of the target line for hit testing
609        let line_text = lines.get(target_line).unwrap_or(&"");
610
611        if line_text.is_empty() {
612            return line_start_byte;
613        }
614
615        // Use glyphon's hit testing for the specific line
616        let cache_key = TextCacheKey::new(line_text, font_size);
617        let mut font_system = self.font_system.lock().unwrap();
618        let mut text_cache = self.text_cache.lock().unwrap();
619
620        let buffer = text_cache.entry(cache_key).or_insert_with(|| {
621            let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, font_size * 1.4));
622            SharedTextBuffer {
623                buffer,
624                text: String::new(),
625                font_size: 0.0,
626                cached_size: None,
627            }
628        });
629
630        buffer.ensure(&mut font_system, line_text, font_size, Attrs::new());
631
632        // Find closest glyph position using layout runs
633        let mut best_offset = 0;
634        let mut best_distance = f32::INFINITY;
635
636        for run in buffer.buffer.layout_runs() {
637            let mut glyph_x = 0.0f32;
638            for glyph in run.glyphs.iter() {
639                // Check distance to left edge of glyph
640                let left_dist = (x - glyph_x).abs();
641                if left_dist < best_distance {
642                    best_distance = left_dist;
643                    // glyph.start is byte index in line_text
644                    best_offset = glyph.start;
645                }
646
647                // Update x position for next glyph
648                glyph_x += glyph.w;
649
650                // Check distance to right edge (after glyph)
651                let right_dist = (x - glyph_x).abs();
652                if right_dist < best_distance {
653                    best_distance = right_dist;
654                    best_offset = glyph.end;
655                }
656            }
657        }
658
659        // Return absolute byte offset (line start + offset within line)
660        line_start_byte + best_offset.min(line_text.len())
661    }
662
663    fn get_cursor_x_for_offset(
664        &self,
665        text: &str,
666        style: &cranpose_ui::text::TextStyle,
667        offset: usize,
668    ) -> f32 {
669        let clamped_offset = offset.min(text.len());
670        if clamped_offset == 0 {
671            return 0.0;
672        }
673
674        // Measure text up to offset
675        let prefix = &text[..clamped_offset];
676        self.measure(prefix, style).width
677    }
678
679    fn layout(
680        &self,
681        text: &str,
682        style: &cranpose_ui::text::TextStyle,
683    ) -> cranpose_ui::text_layout_result::TextLayoutResult {
684        use cranpose_ui::text_layout_result::{LineLayout, TextLayoutResult};
685
686        let font_size = resolve_font_size(style);
687        let line_height = font_size * 1.4;
688
689        // Get buffer to extract glyph positions
690        let cache_key = TextCacheKey::new(text, font_size);
691        let mut font_system = self.font_system.lock().unwrap();
692        let mut text_cache = self.text_cache.lock().unwrap();
693
694        let buffer = text_cache.entry(cache_key.clone()).or_insert_with(|| {
695            let buffer = Buffer::new(&mut font_system, Metrics::new(font_size, line_height));
696            SharedTextBuffer {
697                buffer,
698                text: String::new(),
699                font_size: 0.0,
700                cached_size: None,
701            }
702        });
703        buffer.ensure(&mut font_system, text, font_size, Attrs::new());
704
705        // Extract glyph positions from layout runs
706        let mut glyph_x_positions = Vec::new();
707        let mut char_to_byte = Vec::new();
708        let mut lines = Vec::new();
709
710        let mut current_line_y = 0.0f32;
711        let mut line_start_offset = 0usize;
712
713        for run in buffer.buffer.layout_runs() {
714            let line_idx = run.line_i;
715            let line_y = line_idx as f32 * line_height;
716
717            // Track line boundaries
718            if lines.is_empty() || line_y != current_line_y {
719                if !lines.is_empty() {
720                    // Close previous line
721                    if let Some(_last) = lines.last_mut() {
722                        // end_offset will be updated when we see a newline or end
723                    }
724                }
725                current_line_y = line_y;
726            }
727
728            for glyph in run.glyphs.iter() {
729                glyph_x_positions.push(glyph.x);
730                char_to_byte.push(glyph.start);
731
732                // Track line end
733                if glyph.end > line_start_offset {
734                    line_start_offset = glyph.end;
735                }
736            }
737        }
738
739        // Add end position
740        let total_width = self.measure(text, style).width;
741        glyph_x_positions.push(total_width);
742        char_to_byte.push(text.len());
743
744        // Build lines from text newlines
745        let mut y = 0.0f32;
746        let mut line_start = 0usize;
747        for (i, line_text) in text.split('\n').enumerate() {
748            let line_end = if i == text.split('\n').count() - 1 {
749                text.len()
750            } else {
751                line_start + line_text.len()
752            };
753
754            lines.push(LineLayout {
755                start_offset: line_start,
756                end_offset: line_end,
757                y,
758                height: line_height,
759            });
760
761            line_start = line_end + 1;
762            y += line_height;
763        }
764
765        if lines.is_empty() {
766            lines.push(LineLayout {
767                start_offset: 0,
768                end_offset: 0,
769                y: 0.0,
770                height: line_height,
771            });
772        }
773
774        let metrics = self.measure(text, style);
775        TextLayoutResult::new(
776            metrics.width,
777            metrics.height,
778            line_height,
779            glyph_x_positions,
780            char_to_byte,
781            lines,
782            text,
783        )
784    }
785}