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