Skip to main content

ratatui_unity/
lib.rs

1//! # ratatui_unity
2//!
3//! A C ABI wrapper around [`ratatui`] that renders terminal UIs to RGB24
4//! pixel buffers, suitable for embedding in game engines (e.g. Unity).
5//!
6//! The crate is compiled as both `cdylib` and `staticlib` so that it can be
7//! consumed from any host capable of calling C functions. All public entry
8//! points are `extern "C"` and `#[no_mangle]`; there is no idiomatic Rust API.
9//!
10//! ## High-level flow
11//!
12//! 1. Create a terminal handle with [`ratatui_create`].
13//! 2. For each frame:
14//!    - Call [`ratatui_begin_frame`] to reset per-frame state.
15//!    - Build a layout tree with [`ratatui_split`] / [`ratatui_inner`].
16//!    - Optionally set a style with [`ratatui_set_style`] before any
17//!      widget call.
18//!    - Queue widget commands (e.g. [`ratatui_block`], [`ratatui_paragraph`],
19//!      [`ratatui_chart_begin`] / [`ratatui_chart_end`], …).
20//!    - Call [`ratatui_end_frame`] (or [`ratatui_end_frame_hashed`]) to draw
21//!      the queue and rasterize the cell grid into an RGB24 pixel buffer.
22//! 3. When done, call [`ratatui_destroy`] to release the handle.
23//!
24//! ## Memory & lifetime
25//!
26//! The handle returned by [`ratatui_create`] is an opaque pointer to a
27//! heap-allocated `TerminalState`. The pixel
28//! buffer pointer returned by `ratatui_end_frame*` is owned by the handle and
29//! is only valid until the next FFI call that mutates the handle. The caller
30//! must copy the bytes before issuing further calls if it wants to retain them.
31//!
32//! ## Safety
33//!
34//! All FFI entry points perform null-pointer checks on the handle and on any
35//! pointer argument they dereference. Strings are read as null-terminated
36//! `*const c_char`. Slices passed as `(ptr, len)` pairs must reference valid
37//! memory for the duration of the call.
38//!
39//! ## Source layout
40//!
41//! The C ABI surface lives in `ffi` (split into `lifecycle`, `layout`,
42//! `style`, `widgets`, `builders`, and shared `util` helpers) and is
43//! re-exported here. The non-FFI internals live in `terminal`, `commands`,
44//! `renderer`, `font`, and `color`.
45
46mod color;
47mod commands;
48mod ffi;
49mod font;
50mod renderer;
51mod terminal;
52
53pub use ffi::*;
54
55#[cfg(test)]
56mod tests {
57    // Panicking via unwrap/expect is the standard way to fail a test.
58    #![allow(clippy::unwrap_used)]
59    use super::*;
60    use crate::ffi::util::state_ref;
61    use std::ffi::CString;
62
63    /// Regression: 65536 tab-separated columns used to truncate `col_count`
64    /// to 0 in the u16 cast and panic with a division by zero — and, before
65    /// the column cap, stalled the layout solver for hours.
66    #[test]
67    fn table_with_more_than_u16_max_columns_does_not_panic() {
68        let handle = ratatui_create(10, 5, 14.0);
69        ratatui_begin_frame(handle);
70        let data = CString::new(vec!["h"; 65536].join("\t")).unwrap();
71        ratatui_table(handle, 0, data.as_ptr());
72        ratatui_end_frame(handle);
73        ratatui_destroy(handle);
74    }
75
76    /// Regression: invalid font bytes must be rejected without panicking
77    /// (the crate is built with `panic = "abort"` in release).
78    #[test]
79    fn set_custom_font_with_invalid_bytes_returns_zero() {
80        let handle = ratatui_create(10, 5, 14.0);
81        let bytes = [0u8; 16];
82        assert_eq!(ratatui_set_custom_font(handle, bytes.as_ptr(), bytes.len() as u32), 0);
83        ratatui_destroy(handle);
84    }
85
86    /// Regression: after a successful font swap the reported pixel dimensions
87    /// must match the rasterized buffer size.
88    #[test]
89    fn set_custom_font_resyncs_pixel_dimensions() {
90        let handle = ratatui_create(10, 5, 14.0);
91        let bytes = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");
92        assert_eq!(
93            ratatui_set_custom_font(handle, bytes.as_ptr(), bytes.len() as u32),
94            1
95        );
96        let w = ratatui_pixel_width(handle);
97        let h = ratatui_pixel_height(handle);
98        ratatui_begin_frame(handle);
99        ratatui_end_frame(handle);
100        let state = state_ref(handle).unwrap();
101        assert_eq!(state.pixel_buffer.len(), w as usize * h as usize * 3);
102        ratatui_destroy(handle);
103    }
104}