Skip to main content

typf_core/
ffi.rs

1//! C-ABI compatible types for FFI consumers.
2//!
3// FFI code requires unsafe by nature - all unsafe usage is documented
4#![allow(unsafe_code)]
5//!
6//! This module provides `#[repr(C)]` types that can be safely passed across FFI
7//! boundaries. These types mirror the internal Rust types but use C-compatible
8//! layouts for integration with C, Python (via ctypes/cffi), and other languages.
9//!
10//! # Memory Ownership
11//!
12//! - Types ending in `C` are C-ABI compatible copies
13//! - Slice views (`*const T`, `len`) are borrowed - caller must not free
14//! - Owned arrays should be freed via the corresponding `_free()` function
15//!
16//! # Example (conceptual C usage)
17//!
18//! ```c
19//! TypfShapingResultC result;
20//! int err = typf_shape_text("hb", "Hello", "/path/font.ttf", 24.0, &result);
21//! if (err == 0) {
22//!     for (uint32_t i = 0; i < result.glyph_count; i++) {
23//!         printf("Glyph %u at (%.1f, %.1f)\n",
24//!                result.glyphs[i].glyph_id,
25//!                result.glyphs[i].x,
26//!                result.glyphs[i].y);
27//!     }
28//!     typf_shaping_result_free(&result);
29//! }
30//! ```
31
32// this_file: crates/typf-core/src/ffi.rs
33
34use crate::types::{Direction, PositionedGlyph, ShapingResult};
35
36/// C-ABI compatible positioned glyph.
37///
38/// This struct matches the layout expected by external renderers like Cairo:
39/// - `glyph_id`: Index into the font's glyph table
40/// - `x`, `y`: Position in user space (typically pixels)
41/// - `advance`: Horizontal advance width
42/// - `cluster`: Cluster index for text segmentation (useful for cursor positioning)
43///
44/// # Size and Alignment
45///
46/// This struct is 20 bytes with 4-byte alignment on all platforms.
47#[repr(C)]
48#[derive(Debug, Clone, Copy, PartialEq)]
49pub struct PositionedGlyphC {
50    /// Glyph index in the font (maps to `cairo_glyph_t.index`)
51    pub glyph_id: u32,
52    /// Horizontal position in user space
53    pub x: f32,
54    /// Vertical position in user space
55    pub y: f32,
56    /// Horizontal advance width
57    pub advance: f32,
58    /// Cluster index (maps to original text position)
59    pub cluster: u32,
60}
61
62impl From<&PositionedGlyph> for PositionedGlyphC {
63    fn from(g: &PositionedGlyph) -> Self {
64        Self {
65            glyph_id: g.id,
66            x: g.x,
67            y: g.y,
68            advance: g.advance,
69            cluster: g.cluster,
70        }
71    }
72}
73
74impl From<PositionedGlyph> for PositionedGlyphC {
75    fn from(g: PositionedGlyph) -> Self {
76        Self::from(&g)
77    }
78}
79
80impl From<&PositionedGlyphC> for PositionedGlyph {
81    fn from(g: &PositionedGlyphC) -> Self {
82        Self {
83            id: g.glyph_id,
84            x: g.x,
85            y: g.y,
86            advance: g.advance,
87            cluster: g.cluster,
88        }
89    }
90}
91
92/// Text direction as a C-compatible enum.
93///
94/// Values match common conventions:
95/// - 0: Left-to-right (Latin, Cyrillic)
96/// - 1: Right-to-left (Arabic, Hebrew)
97/// - 2: Top-to-bottom (Traditional Chinese, Japanese)
98/// - 3: Bottom-to-top (rare)
99#[repr(u8)]
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum DirectionC {
102    LeftToRight = 0,
103    RightToLeft = 1,
104    TopToBottom = 2,
105    BottomToTop = 3,
106}
107
108impl From<Direction> for DirectionC {
109    fn from(d: Direction) -> Self {
110        match d {
111            Direction::LeftToRight => DirectionC::LeftToRight,
112            Direction::RightToLeft => DirectionC::RightToLeft,
113            Direction::TopToBottom => DirectionC::TopToBottom,
114            Direction::BottomToTop => DirectionC::BottomToTop,
115        }
116    }
117}
118
119impl From<DirectionC> for Direction {
120    fn from(d: DirectionC) -> Self {
121        match d {
122            DirectionC::LeftToRight => Direction::LeftToRight,
123            DirectionC::RightToLeft => Direction::RightToLeft,
124            DirectionC::TopToBottom => Direction::TopToBottom,
125            DirectionC::BottomToTop => Direction::BottomToTop,
126        }
127    }
128}
129
130/// C-ABI compatible shaping result.
131///
132/// Contains a pointer to an array of positioned glyphs plus metadata.
133///
134/// # Memory Ownership
135///
136/// When returned from FFI functions, the caller owns the memory and must call
137/// `typf_shaping_result_free()` to release it. The `glyphs` pointer is valid
138/// until freed.
139///
140/// # Null Safety
141///
142/// - `glyphs` may be null if `glyph_count` is 0
143/// - Always check `glyph_count` before dereferencing `glyphs`
144#[repr(C)]
145#[derive(Debug)]
146pub struct ShapingResultC {
147    /// Pointer to array of positioned glyphs (owned)
148    pub glyphs: *mut PositionedGlyphC,
149    /// Number of glyphs in the array
150    pub glyph_count: u32,
151    /// Total horizontal advance width
152    pub advance_width: f32,
153    /// Total vertical advance height
154    pub advance_height: f32,
155    /// Text direction
156    pub direction: DirectionC,
157    /// Reserved for future use (padding)
158    pub _reserved: [u8; 3],
159}
160
161impl ShapingResultC {
162    /// Creates a new ShapingResultC by converting from a Rust ShapingResult.
163    ///
164    /// This allocates a new array for the glyphs. The caller is responsible
165    /// for freeing it via `free()`.
166    pub fn from_rust(result: &ShapingResult) -> Self {
167        let glyph_count = result.glyphs.len() as u32;
168        let glyphs = if glyph_count > 0 {
169            let mut vec: Vec<PositionedGlyphC> =
170                result.glyphs.iter().map(PositionedGlyphC::from).collect();
171            let ptr = vec.as_mut_ptr();
172            std::mem::forget(vec); // Transfer ownership to C
173            ptr
174        } else {
175            std::ptr::null_mut()
176        };
177
178        Self {
179            glyphs,
180            glyph_count,
181            advance_width: result.advance_width,
182            advance_height: result.advance_height,
183            direction: result.direction.into(),
184            _reserved: [0; 3],
185        }
186    }
187
188    /// Frees the memory allocated for glyphs.
189    ///
190    /// # Safety
191    ///
192    /// - Must only be called once per ShapingResultC
193    /// - The glyphs pointer must have been allocated by `from_rust()`
194    pub unsafe fn free(&mut self) {
195        if !self.glyphs.is_null() && self.glyph_count > 0 {
196            let _ = Vec::from_raw_parts(
197                self.glyphs,
198                self.glyph_count as usize,
199                self.glyph_count as usize,
200            );
201            self.glyphs = std::ptr::null_mut();
202            self.glyph_count = 0;
203        }
204    }
205
206    /// Returns a slice view of the glyphs.
207    ///
208    /// # Safety
209    ///
210    /// The returned slice is valid only while this ShapingResultC is valid
211    /// and has not been freed.
212    pub unsafe fn glyphs_slice(&self) -> &[PositionedGlyphC] {
213        if self.glyphs.is_null() || self.glyph_count == 0 {
214            &[]
215        } else {
216            std::slice::from_raw_parts(self.glyphs, self.glyph_count as usize)
217        }
218    }
219
220    /// Converts back to a Rust ShapingResult.
221    ///
222    /// This creates a new owned Vec, leaving this ShapingResultC unchanged.
223    ///
224    /// # Safety
225    ///
226    /// The glyphs pointer must be valid.
227    pub unsafe fn to_rust(&self) -> ShapingResult {
228        let glyphs: Vec<PositionedGlyph> = self
229            .glyphs_slice()
230            .iter()
231            .map(PositionedGlyph::from)
232            .collect();
233
234        ShapingResult {
235            glyphs,
236            advance_width: self.advance_width,
237            advance_height: self.advance_height,
238            direction: self.direction.into(),
239        }
240    }
241}
242
243/// Glyph iterator for zero-copy consumption.
244///
245/// This provides a way for layout engines to iterate over shaped glyphs
246/// without taking ownership of the underlying data.
247///
248/// # Example
249///
250/// ```rust
251/// use typf_core::ffi::GlyphIterator;
252/// use typf_core::types::ShapingResult;
253///
254/// fn process_glyphs(result: &ShapingResult) {
255///     let iter = GlyphIterator::new(result);
256///     for glyph in iter {
257///         println!("Glyph {} at ({}, {})", glyph.glyph_id, glyph.x, glyph.y);
258///     }
259/// }
260/// ```
261pub struct GlyphIterator<'a> {
262    glyphs: &'a [PositionedGlyph],
263    index: usize,
264}
265
266impl<'a> GlyphIterator<'a> {
267    /// Creates a new iterator over the glyphs in a shaping result.
268    pub fn new(result: &'a ShapingResult) -> Self {
269        Self {
270            glyphs: &result.glyphs,
271            index: 0,
272        }
273    }
274
275    /// Returns the total number of glyphs.
276    pub fn len(&self) -> usize {
277        self.glyphs.len()
278    }
279
280    /// Returns true if there are no glyphs.
281    pub fn is_empty(&self) -> bool {
282        self.glyphs.is_empty()
283    }
284
285    /// Returns the remaining number of glyphs to iterate.
286    pub fn remaining(&self) -> usize {
287        self.glyphs.len().saturating_sub(self.index)
288    }
289}
290
291impl<'a> Iterator for GlyphIterator<'a> {
292    type Item = PositionedGlyphC;
293
294    fn next(&mut self) -> Option<Self::Item> {
295        if self.index < self.glyphs.len() {
296            let glyph = PositionedGlyphC::from(&self.glyphs[self.index]);
297            self.index += 1;
298            Some(glyph)
299        } else {
300            None
301        }
302    }
303
304    fn size_hint(&self) -> (usize, Option<usize>) {
305        let remaining = self.remaining();
306        (remaining, Some(remaining))
307    }
308}
309
310impl<'a> ExactSizeIterator for GlyphIterator<'a> {}
311
312// =============================================================================
313// Stage 5: Mesh ABI for GPU Upload
314// =============================================================================
315
316/// Vertex for GPU rendering (2D position only).
317///
318/// This is the minimal vertex format for text rendering. Position is in
319/// normalized device coordinates or pixel space depending on your pipeline.
320///
321/// Compatible with wgpu `VertexFormat::Float32x2` at offset 0.
322///
323/// # Memory Layout
324///
325/// - Size: 8 bytes
326/// - Alignment: 4 bytes
327/// - Can be cast to `&[u8]` via `bytemuck::cast_slice()` or `as_bytes()`
328///
329/// # Example (wgpu)
330///
331/// ```rust,ignore
332/// wgpu::VertexBufferLayout {
333///     array_stride: 8,
334///     step_mode: wgpu::VertexStepMode::Vertex,
335///     attributes: &[wgpu::VertexAttribute {
336///         offset: 0,
337///         shader_location: 0,
338///         format: wgpu::VertexFormat::Float32x2,
339///     }],
340/// }
341/// ```
342#[repr(C)]
343#[derive(Debug, Clone, Copy, PartialEq, Default)]
344pub struct Vertex2D {
345    /// X coordinate
346    pub x: f32,
347    /// Y coordinate
348    pub y: f32,
349}
350
351impl Vertex2D {
352    /// Creates a new 2D vertex.
353    #[inline]
354    pub const fn new(x: f32, y: f32) -> Self {
355        Self { x, y }
356    }
357
358    /// Returns the vertex data as a byte slice.
359    ///
360    /// # Safety
361    ///
362    /// The returned slice is valid for the lifetime of self.
363    #[inline]
364    pub fn as_bytes(&self) -> &[u8] {
365        // SAFETY: Vertex2D is repr(C) with only f32 fields, no padding
366        unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, 8) }
367    }
368}
369
370/// Vertex with position and UV coordinates for textured rendering.
371///
372/// Useful for rendering bitmap glyphs or SDF textures. UV coordinates
373/// are typically in [0, 1] range for texture sampling.
374///
375/// Compatible with wgpu `Float32x2` at offset 0 (position) and offset 8 (uv).
376///
377/// # Memory Layout
378///
379/// - Size: 16 bytes
380/// - Alignment: 4 bytes
381#[repr(C)]
382#[derive(Debug, Clone, Copy, PartialEq, Default)]
383pub struct VertexUV {
384    /// X coordinate (position)
385    pub x: f32,
386    /// Y coordinate (position)
387    pub y: f32,
388    /// U coordinate (texture)
389    pub u: f32,
390    /// V coordinate (texture)
391    pub v: f32,
392}
393
394impl VertexUV {
395    /// Creates a new vertex with position and UV.
396    #[inline]
397    pub const fn new(x: f32, y: f32, u: f32, v: f32) -> Self {
398        Self { x, y, u, v }
399    }
400
401    /// Returns the vertex data as a byte slice.
402    #[inline]
403    pub fn as_bytes(&self) -> &[u8] {
404        // SAFETY: VertexUV is repr(C) with only f32 fields, no padding
405        unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, 16) }
406    }
407}
408
409/// Vertex with position and RGBA color.
410///
411/// Useful for colored glyph rendering (COLR glyphs, colored text).
412/// Color values are in [0, 1] range.
413///
414/// # Memory Layout
415///
416/// - Size: 24 bytes
417/// - Alignment: 4 bytes
418#[repr(C)]
419#[derive(Debug, Clone, Copy, PartialEq, Default)]
420pub struct VertexColor {
421    /// X coordinate
422    pub x: f32,
423    /// Y coordinate
424    pub y: f32,
425    /// Red component [0, 1]
426    pub r: f32,
427    /// Green component [0, 1]
428    pub g: f32,
429    /// Blue component [0, 1]
430    pub b: f32,
431    /// Alpha component [0, 1]
432    pub a: f32,
433}
434
435impl VertexColor {
436    /// Creates a new vertex with position and color.
437    #[inline]
438    pub const fn new(x: f32, y: f32, r: f32, g: f32, b: f32, a: f32) -> Self {
439        Self { x, y, r, g, b, a }
440    }
441
442    /// Returns the vertex data as a byte slice.
443    #[inline]
444    pub fn as_bytes(&self) -> &[u8] {
445        // SAFETY: VertexColor is repr(C) with only f32 fields, no padding
446        unsafe { std::slice::from_raw_parts(self as *const Self as *const u8, 24) }
447    }
448}
449
450/// A mesh of triangles for GPU rendering.
451///
452/// Contains vertices and indices suitable for indexed drawing.
453/// Indices reference vertices by their position in the `vertices` array.
454///
455/// # Zero-Copy Upload
456///
457/// Use `vertices_bytes()` and `indices_bytes()` to get byte slices
458/// that can be directly uploaded to GPU buffers without copying:
459///
460/// ```rust,ignore
461/// let mesh: GlyphMesh<Vertex2D> = /* ... */;
462/// let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
463///     label: Some("Glyph Vertices"),
464///     contents: mesh.vertices_bytes(),
465///     usage: wgpu::BufferUsages::VERTEX,
466/// });
467/// let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
468///     label: Some("Glyph Indices"),
469///     contents: mesh.indices_bytes(),
470///     usage: wgpu::BufferUsages::INDEX,
471/// });
472/// ```
473#[derive(Debug, Clone)]
474pub struct GlyphMesh<V> {
475    /// Vertex data (position, optionally UV/color)
476    pub vertices: Vec<V>,
477    /// Triangle indices (3 indices per triangle)
478    pub indices: Vec<u32>,
479    /// Glyph ID this mesh represents
480    pub glyph_id: u32,
481}
482
483impl<V> GlyphMesh<V> {
484    /// Creates a new empty mesh for a glyph.
485    pub fn new(glyph_id: u32) -> Self {
486        Self {
487            vertices: Vec::new(),
488            indices: Vec::new(),
489            glyph_id,
490        }
491    }
492
493    /// Creates a mesh with pre-allocated capacity.
494    pub fn with_capacity(glyph_id: u32, vertex_capacity: usize, index_capacity: usize) -> Self {
495        Self {
496            vertices: Vec::with_capacity(vertex_capacity),
497            indices: Vec::with_capacity(index_capacity),
498            glyph_id,
499        }
500    }
501
502    /// Returns the number of triangles in this mesh.
503    pub fn triangle_count(&self) -> usize {
504        self.indices.len() / 3
505    }
506
507    /// Returns true if the mesh has no geometry.
508    pub fn is_empty(&self) -> bool {
509        self.vertices.is_empty() || self.indices.is_empty()
510    }
511}
512
513impl<V: Copy> GlyphMesh<V> {
514    /// Returns the vertex data as a byte slice for GPU upload.
515    ///
516    /// # Safety
517    ///
518    /// This assumes V is a repr(C) struct with no padding.
519    /// Use only with Vertex2D, VertexUV, or VertexColor.
520    pub fn vertices_bytes(&self) -> &[u8] {
521        if self.vertices.is_empty() {
522            return &[];
523        }
524        let ptr = self.vertices.as_ptr() as *const u8;
525        let len = self.vertices.len() * std::mem::size_of::<V>();
526        // SAFETY: vertices is a contiguous Vec, V is repr(C)
527        unsafe { std::slice::from_raw_parts(ptr, len) }
528    }
529}
530
531impl<V> GlyphMesh<V> {
532    /// Returns the index data as a byte slice for GPU upload.
533    pub fn indices_bytes(&self) -> &[u8] {
534        if self.indices.is_empty() {
535            return &[];
536        }
537        let ptr = self.indices.as_ptr() as *const u8;
538        let len = self.indices.len() * 4; // u32 = 4 bytes
539                                          // SAFETY: indices is a contiguous Vec of u32
540        unsafe { std::slice::from_raw_parts(ptr, len) }
541    }
542}
543
544/// A collection of glyph meshes ready for GPU rendering.
545///
546/// This is the Stage 5 output format for GPU pipelines. Each glyph
547/// has its own mesh with position offset applied.
548///
549/// # Memory Layout
550///
551/// All vertex types are `#[repr(C)]` with documented sizes:
552/// - `Vertex2D`: 8 bytes (2 × f32)
553/// - `VertexUV`: 16 bytes (4 × f32)
554/// - `VertexColor`: 24 bytes (6 × f32)
555/// - Indices: 4 bytes each (u32)
556///
557/// # Merging Meshes
558///
559/// For batch rendering, use `merge_all()` to combine glyphs into a single mesh:
560///
561/// ```rust,ignore
562/// let render_mesh = RenderMesh::<Vertex2D>::new();
563/// // ... add glyph meshes ...
564/// let (vertices, indices) = render_mesh.merge_all();
565/// ```
566#[derive(Debug, Clone)]
567pub struct RenderMesh<V> {
568    /// Individual glyph meshes
569    pub glyphs: Vec<GlyphMesh<V>>,
570    /// Total vertex count across all glyphs
571    pub total_vertices: usize,
572    /// Total index count across all glyphs
573    pub total_indices: usize,
574}
575
576impl<V> Default for RenderMesh<V> {
577    fn default() -> Self {
578        Self::new()
579    }
580}
581
582impl<V> RenderMesh<V> {
583    /// Creates a new empty render mesh.
584    pub fn new() -> Self {
585        Self {
586            glyphs: Vec::new(),
587            total_vertices: 0,
588            total_indices: 0,
589        }
590    }
591
592    /// Creates a render mesh with pre-allocated capacity.
593    pub fn with_capacity(glyph_count: usize) -> Self {
594        Self {
595            glyphs: Vec::with_capacity(glyph_count),
596            total_vertices: 0,
597            total_indices: 0,
598        }
599    }
600
601    /// Adds a glyph mesh to the render mesh.
602    pub fn push(&mut self, mesh: GlyphMesh<V>) {
603        self.total_vertices += mesh.vertices.len();
604        self.total_indices += mesh.indices.len();
605        self.glyphs.push(mesh);
606    }
607
608    /// Returns the number of glyphs in this mesh.
609    pub fn glyph_count(&self) -> usize {
610        self.glyphs.len()
611    }
612
613    /// Returns true if no geometry has been added.
614    pub fn is_empty(&self) -> bool {
615        self.glyphs.is_empty()
616    }
617
618    /// Returns the total triangle count across all glyphs.
619    pub fn total_triangles(&self) -> usize {
620        self.total_indices / 3
621    }
622}
623
624impl<V: Copy> RenderMesh<V> {
625    /// Merges all glyph meshes into single vertex and index buffers.
626    ///
627    /// Index values are adjusted to account for the merged vertex offset.
628    /// This is the most efficient format for GPU rendering.
629    ///
630    /// Returns (vertices, indices) ready for buffer upload.
631    pub fn merge_all(&self) -> (Vec<V>, Vec<u32>) {
632        let mut vertices = Vec::with_capacity(self.total_vertices);
633        let mut indices = Vec::with_capacity(self.total_indices);
634
635        for glyph in &self.glyphs {
636            let base_index = vertices.len() as u32;
637            vertices.extend_from_slice(&glyph.vertices);
638            indices.extend(glyph.indices.iter().map(|i| i + base_index));
639        }
640
641        (vertices, indices)
642    }
643
644    /// Merges all meshes and returns byte slices for direct GPU upload.
645    ///
646    /// This avoids intermediate allocations when possible.
647    pub fn merge_to_bytes(&self) -> (Vec<u8>, Vec<u8>) {
648        let (vertices, indices) = self.merge_all();
649
650        let vertex_bytes = if vertices.is_empty() {
651            Vec::new()
652        } else {
653            let ptr = vertices.as_ptr() as *const u8;
654            let len = vertices.len() * std::mem::size_of::<V>();
655            // SAFETY: vertices is a contiguous Vec, V is repr(C)
656            unsafe { std::slice::from_raw_parts(ptr, len).to_vec() }
657        };
658
659        let index_bytes = if indices.is_empty() {
660            Vec::new()
661        } else {
662            let ptr = indices.as_ptr() as *const u8;
663            let len = indices.len() * 4;
664            // SAFETY: indices is a contiguous Vec of u32
665            unsafe { std::slice::from_raw_parts(ptr, len).to_vec() }
666        };
667
668        (vertex_bytes, index_bytes)
669    }
670}
671
672// =============================================================================
673// Size and alignment assertions (compile-time checks)
674// =============================================================================
675
676const _: () = {
677    // PositionedGlyphC should be exactly 20 bytes
678    assert!(std::mem::size_of::<PositionedGlyphC>() == 20);
679    // PositionedGlyphC should have 4-byte alignment
680    assert!(std::mem::align_of::<PositionedGlyphC>() == 4);
681    // DirectionC should be 1 byte
682    assert!(std::mem::size_of::<DirectionC>() == 1);
683
684    // Stage 5 mesh vertex types
685    assert!(std::mem::size_of::<Vertex2D>() == 8);
686    assert!(std::mem::align_of::<Vertex2D>() == 4);
687    assert!(std::mem::size_of::<VertexUV>() == 16);
688    assert!(std::mem::align_of::<VertexUV>() == 4);
689    assert!(std::mem::size_of::<VertexColor>() == 24);
690    assert!(std::mem::align_of::<VertexColor>() == 4);
691};
692
693#[cfg(test)]
694mod tests {
695    use super::*;
696
697    #[test]
698    fn test_positioned_glyph_c_conversion() {
699        let rust_glyph = PositionedGlyph {
700            id: 42,
701            x: 10.5,
702            y: 20.5,
703            advance: 15.0,
704            cluster: 3,
705        };
706
707        let c_glyph = PositionedGlyphC::from(&rust_glyph);
708        assert_eq!(c_glyph.glyph_id, 42);
709        assert_eq!(c_glyph.x, 10.5);
710        assert_eq!(c_glyph.y, 20.5);
711        assert_eq!(c_glyph.advance, 15.0);
712        assert_eq!(c_glyph.cluster, 3);
713
714        let back = PositionedGlyph::from(&c_glyph);
715        assert_eq!(back, rust_glyph);
716    }
717
718    #[test]
719    fn test_direction_c_conversion() {
720        assert_eq!(
721            DirectionC::from(Direction::LeftToRight),
722            DirectionC::LeftToRight
723        );
724        assert_eq!(
725            DirectionC::from(Direction::RightToLeft),
726            DirectionC::RightToLeft
727        );
728        assert_eq!(
729            DirectionC::from(Direction::TopToBottom),
730            DirectionC::TopToBottom
731        );
732        assert_eq!(
733            DirectionC::from(Direction::BottomToTop),
734            DirectionC::BottomToTop
735        );
736
737        assert_eq!(
738            Direction::from(DirectionC::LeftToRight),
739            Direction::LeftToRight
740        );
741        assert_eq!(
742            Direction::from(DirectionC::RightToLeft),
743            Direction::RightToLeft
744        );
745    }
746
747    #[test]
748    fn test_shaping_result_c_roundtrip() {
749        let rust_result = ShapingResult {
750            glyphs: vec![
751                PositionedGlyph {
752                    id: 1,
753                    x: 0.0,
754                    y: 0.0,
755                    advance: 10.0,
756                    cluster: 0,
757                },
758                PositionedGlyph {
759                    id: 2,
760                    x: 10.0,
761                    y: 0.0,
762                    advance: 12.0,
763                    cluster: 1,
764                },
765                PositionedGlyph {
766                    id: 3,
767                    x: 22.0,
768                    y: 0.0,
769                    advance: 8.0,
770                    cluster: 2,
771                },
772            ],
773            advance_width: 30.0,
774            advance_height: 0.0,
775            direction: Direction::LeftToRight,
776        };
777
778        let mut c_result = ShapingResultC::from_rust(&rust_result);
779        assert_eq!(c_result.glyph_count, 3);
780        assert_eq!(c_result.advance_width, 30.0);
781        assert_eq!(c_result.direction, DirectionC::LeftToRight);
782
783        unsafe {
784            let slice = c_result.glyphs_slice();
785            assert_eq!(slice.len(), 3);
786            assert_eq!(slice[0].glyph_id, 1);
787            assert_eq!(slice[1].x, 10.0);
788            assert_eq!(slice[2].advance, 8.0);
789
790            let back = c_result.to_rust();
791            assert_eq!(back.glyphs.len(), 3);
792            assert_eq!(back.advance_width, 30.0);
793            assert_eq!(back.direction, Direction::LeftToRight);
794
795            c_result.free();
796            assert!(c_result.glyphs.is_null());
797            assert_eq!(c_result.glyph_count, 0);
798        }
799    }
800
801    #[test]
802    fn test_shaping_result_c_empty() {
803        let rust_result = ShapingResult {
804            glyphs: vec![],
805            advance_width: 0.0,
806            advance_height: 0.0,
807            direction: Direction::LeftToRight,
808        };
809
810        let mut c_result = ShapingResultC::from_rust(&rust_result);
811        assert_eq!(c_result.glyph_count, 0);
812        assert!(c_result.glyphs.is_null());
813
814        unsafe {
815            let slice = c_result.glyphs_slice();
816            assert!(slice.is_empty());
817            c_result.free(); // Should be safe even for empty
818        }
819    }
820
821    #[test]
822    fn test_glyph_iterator() {
823        let result = ShapingResult {
824            glyphs: vec![
825                PositionedGlyph {
826                    id: 1,
827                    x: 0.0,
828                    y: 0.0,
829                    advance: 10.0,
830                    cluster: 0,
831                },
832                PositionedGlyph {
833                    id: 2,
834                    x: 10.0,
835                    y: 0.0,
836                    advance: 12.0,
837                    cluster: 1,
838                },
839            ],
840            advance_width: 22.0,
841            advance_height: 0.0,
842            direction: Direction::LeftToRight,
843        };
844
845        let iter = GlyphIterator::new(&result);
846        assert_eq!(iter.len(), 2);
847        assert!(!iter.is_empty());
848        assert_eq!(iter.remaining(), 2);
849
850        let collected: Vec<_> = GlyphIterator::new(&result).collect();
851        assert_eq!(collected.len(), 2);
852        assert_eq!(collected[0].glyph_id, 1);
853        assert_eq!(collected[1].glyph_id, 2);
854    }
855
856    #[test]
857    fn test_size_alignment() {
858        // Verify our compile-time assertions match runtime
859        assert_eq!(std::mem::size_of::<PositionedGlyphC>(), 20);
860        assert_eq!(std::mem::align_of::<PositionedGlyphC>(), 4);
861        assert_eq!(std::mem::size_of::<DirectionC>(), 1);
862    }
863
864    // =========================================================================
865    // Stage 5 Mesh ABI Tests
866    // =========================================================================
867
868    #[test]
869    fn test_vertex2d_size_alignment() {
870        assert_eq!(std::mem::size_of::<Vertex2D>(), 8);
871        assert_eq!(std::mem::align_of::<Vertex2D>(), 4);
872    }
873
874    #[test]
875    fn test_vertex_uv_size_alignment() {
876        assert_eq!(std::mem::size_of::<VertexUV>(), 16);
877        assert_eq!(std::mem::align_of::<VertexUV>(), 4);
878    }
879
880    #[test]
881    fn test_vertex_color_size_alignment() {
882        assert_eq!(std::mem::size_of::<VertexColor>(), 24);
883        assert_eq!(std::mem::align_of::<VertexColor>(), 4);
884    }
885
886    #[test]
887    fn test_vertex2d_as_bytes() {
888        let v = Vertex2D::new(1.0, 2.0);
889        let bytes = v.as_bytes();
890        assert_eq!(bytes.len(), 8);
891
892        // Verify byte representation matches f32 layout
893        let x_bytes = 1.0_f32.to_ne_bytes();
894        let y_bytes = 2.0_f32.to_ne_bytes();
895        assert_eq!(&bytes[0..4], &x_bytes);
896        assert_eq!(&bytes[4..8], &y_bytes);
897    }
898
899    #[test]
900    fn test_vertex_uv_as_bytes() {
901        let v = VertexUV::new(1.0, 2.0, 0.5, 0.75);
902        let bytes = v.as_bytes();
903        assert_eq!(bytes.len(), 16);
904
905        // Verify byte representation
906        let x_bytes = 1.0_f32.to_ne_bytes();
907        let u_bytes = 0.5_f32.to_ne_bytes();
908        assert_eq!(&bytes[0..4], &x_bytes);
909        assert_eq!(&bytes[8..12], &u_bytes);
910    }
911
912    #[test]
913    fn test_vertex_color_as_bytes() {
914        let v = VertexColor::new(1.0, 2.0, 1.0, 0.0, 0.0, 1.0);
915        let bytes = v.as_bytes();
916        assert_eq!(bytes.len(), 24);
917
918        // Verify byte representation
919        let r_bytes = 1.0_f32.to_ne_bytes();
920        assert_eq!(&bytes[8..12], &r_bytes); // r is at offset 8
921    }
922
923    #[test]
924    fn test_glyph_mesh_creation() {
925        let mesh: GlyphMesh<Vertex2D> = GlyphMesh::new(42);
926        assert_eq!(mesh.glyph_id, 42);
927        assert!(mesh.is_empty());
928        assert_eq!(mesh.triangle_count(), 0);
929    }
930
931    #[test]
932    fn test_glyph_mesh_with_triangle() {
933        let mut mesh: GlyphMesh<Vertex2D> = GlyphMesh::new(1);
934        mesh.vertices = vec![
935            Vertex2D::new(0.0, 0.0),
936            Vertex2D::new(1.0, 0.0),
937            Vertex2D::new(0.5, 1.0),
938        ];
939        mesh.indices = vec![0, 1, 2];
940
941        assert!(!mesh.is_empty());
942        assert_eq!(mesh.triangle_count(), 1);
943
944        let vb = mesh.vertices_bytes();
945        assert_eq!(vb.len(), 24); // 3 vertices × 8 bytes
946
947        let ib = mesh.indices_bytes();
948        assert_eq!(ib.len(), 12); // 3 indices × 4 bytes
949    }
950
951    #[test]
952    fn test_render_mesh_push() {
953        let mut rm: RenderMesh<Vertex2D> = RenderMesh::new();
954        assert!(rm.is_empty());
955
956        let mut mesh1 = GlyphMesh::new(1);
957        mesh1.vertices = vec![
958            Vertex2D::new(0.0, 0.0),
959            Vertex2D::new(1.0, 0.0),
960            Vertex2D::new(0.5, 1.0),
961        ];
962        mesh1.indices = vec![0, 1, 2];
963
964        rm.push(mesh1);
965        assert_eq!(rm.glyph_count(), 1);
966        assert_eq!(rm.total_vertices, 3);
967        assert_eq!(rm.total_indices, 3);
968        assert_eq!(rm.total_triangles(), 1);
969    }
970
971    #[test]
972    fn test_render_mesh_merge() {
973        let mut rm: RenderMesh<Vertex2D> = RenderMesh::new();
974
975        // First glyph: triangle at origin
976        let mut mesh1 = GlyphMesh::new(1);
977        mesh1.vertices = vec![
978            Vertex2D::new(0.0, 0.0),
979            Vertex2D::new(1.0, 0.0),
980            Vertex2D::new(0.5, 1.0),
981        ];
982        mesh1.indices = vec![0, 1, 2];
983
984        // Second glyph: triangle offset by 2
985        let mut mesh2 = GlyphMesh::new(2);
986        mesh2.vertices = vec![
987            Vertex2D::new(2.0, 0.0),
988            Vertex2D::new(3.0, 0.0),
989            Vertex2D::new(2.5, 1.0),
990        ];
991        mesh2.indices = vec![0, 1, 2];
992
993        rm.push(mesh1);
994        rm.push(mesh2);
995
996        let (vertices, indices) = rm.merge_all();
997        assert_eq!(vertices.len(), 6);
998        assert_eq!(indices.len(), 6);
999
1000        // First triangle indices unchanged
1001        assert_eq!(indices[0], 0);
1002        assert_eq!(indices[1], 1);
1003        assert_eq!(indices[2], 2);
1004
1005        // Second triangle indices offset by 3 (base_index)
1006        assert_eq!(indices[3], 3);
1007        assert_eq!(indices[4], 4);
1008        assert_eq!(indices[5], 5);
1009    }
1010
1011    #[test]
1012    fn test_render_mesh_merge_to_bytes() {
1013        let mut rm: RenderMesh<Vertex2D> = RenderMesh::new();
1014
1015        let mut mesh = GlyphMesh::new(1);
1016        mesh.vertices = vec![
1017            Vertex2D::new(0.0, 0.0),
1018            Vertex2D::new(1.0, 0.0),
1019            Vertex2D::new(0.5, 1.0),
1020        ];
1021        mesh.indices = vec![0, 1, 2];
1022
1023        rm.push(mesh);
1024
1025        let (vb, ib) = rm.merge_to_bytes();
1026        assert_eq!(vb.len(), 24); // 3 vertices × 8 bytes
1027        assert_eq!(ib.len(), 12); // 3 indices × 4 bytes
1028    }
1029
1030    #[test]
1031    fn test_render_mesh_empty() {
1032        let rm: RenderMesh<Vertex2D> = RenderMesh::new();
1033        let (vertices, indices) = rm.merge_all();
1034        assert!(vertices.is_empty());
1035        assert!(indices.is_empty());
1036
1037        let (vb, ib) = rm.merge_to_bytes();
1038        assert!(vb.is_empty());
1039        assert!(ib.is_empty());
1040    }
1041
1042    #[test]
1043    fn test_vertex_default() {
1044        let v2d = Vertex2D::default();
1045        assert_eq!(v2d.x, 0.0);
1046        assert_eq!(v2d.y, 0.0);
1047
1048        let vuv = VertexUV::default();
1049        assert_eq!(vuv.x, 0.0);
1050        assert_eq!(vuv.u, 0.0);
1051
1052        let vc = VertexColor::default();
1053        assert_eq!(vc.x, 0.0);
1054        assert_eq!(vc.r, 0.0);
1055        assert_eq!(vc.a, 0.0);
1056    }
1057}