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 widget call.
17//!    - Queue widget commands (e.g. [`ratatui_block`], [`ratatui_paragraph`],
18//!      [`ratatui_chart_begin`] / [`ratatui_chart_end`], …).
19//!    - Call [`ratatui_end_frame`] (or [`ratatui_end_frame_hashed`]) to draw
20//!      the queue and rasterize the cell grid into an RGB24 pixel buffer.
21//! 3. When done, call [`ratatui_destroy`] to release the handle.
22//!
23//! ## Memory & lifetime
24//!
25//! The handle returned by [`ratatui_create`] is an opaque pointer to a
26//! heap-allocated `TerminalState`. The pixel
27//! buffer pointer returned by `ratatui_end_frame*` is owned by the handle and
28//! is only valid until the next FFI call that mutates the handle. The caller
29//! must copy the bytes before issuing further calls if it wants to retain them.
30//!
31//! ## Safety
32//!
33//! All FFI entry points perform null-pointer checks on the handle and on any
34//! pointer argument they dereference. Strings are read as null-terminated
35//! `*const c_char`. Slices passed as `(ptr, len)` pairs must reference valid
36//! memory for the duration of the call.
37
38mod color;
39mod commands;
40mod font;
41mod renderer;
42mod terminal;
43
44use crate::commands::{do_split, render_all_commands};
45use crate::terminal::{
46    AxisInfo, CanvasShape, DatasetInfo, PendingCanvas, PendingChart, PendingStyledParagraph,
47    SpanInfo, TerminalState, WidgetCommand,
48};
49use ratatui::style::{Color, Modifier, Style};
50use std::ffi::{c_void, CStr};
51use std::os::raw::c_char;
52
53// ─── Helpers ─────────────────────────────────────────────────────────────────
54
55/// Reinterprets an opaque handle as a mutable reference to [`TerminalState`].
56///
57/// # Safety
58///
59/// `handle` must be a non-null pointer previously returned by
60/// [`ratatui_create`] and not yet passed to [`ratatui_destroy`]. The borrow's
61/// lifetime is unbounded; callers must ensure no other reference to the same
62/// state is active for the duration of the returned borrow.
63unsafe fn state_mut<'a>(handle: *mut c_void) -> &'a mut TerminalState {
64    &mut *(handle as *mut TerminalState)
65}
66
67/// Copies a nullable null-terminated C string into an owned [`String`].
68///
69/// Returns an empty `String` when `ptr` is null. Invalid UTF-8 is replaced
70/// using [`String::from_utf8_lossy`].
71///
72/// # Safety
73///
74/// `ptr`, if non-null, must point to a valid null-terminated byte sequence.
75unsafe fn cstr_to_string(ptr: *const c_char) -> String {
76    if ptr.is_null() {
77        return String::new();
78    }
79    CStr::from_ptr(ptr).to_string_lossy().into_owned()
80}
81
82/// Builds a ratatui [`Style`] from the packed FFI representation.
83///
84/// `use_default_fg` / `use_default_bg`: non-zero means "leave foreground /
85/// background unset so the terminal default is used"; zero means apply the
86/// given RGB triple.
87///
88/// `modifiers` is a bit field:
89/// - `0x01` Bold
90/// - `0x02` Italic
91/// - `0x04` Underlined
92/// - `0x08` Dim
93fn style_from_rgba(
94    fg_r: u8, fg_g: u8, fg_b: u8, use_default_fg: u8,
95    bg_r: u8, bg_g: u8, bg_b: u8, use_default_bg: u8,
96    modifiers: u8,
97) -> Style {
98    let mut style = Style::default();
99    if use_default_fg == 0 { style = style.fg(Color::Rgb(fg_r, fg_g, fg_b)); }
100    if use_default_bg == 0 { style = style.bg(Color::Rgb(bg_r, bg_g, bg_b)); }
101    let mut modifier = Modifier::empty();
102    if modifiers & 0x01 != 0 { modifier |= Modifier::BOLD; }
103    if modifiers & 0x02 != 0 { modifier |= Modifier::ITALIC; }
104    if modifiers & 0x04 != 0 { modifier |= Modifier::UNDERLINED; }
105    if modifiers & 0x08 != 0 { modifier |= Modifier::DIM; }
106    if !modifier.is_empty() { style = style.add_modifier(modifier); }
107    style
108}
109
110// ─── Background color ─────────────────────────────────────────────────────────
111
112/// Sets the RGB background color used by the rasterizer for cells whose
113/// background is [`Color::Reset`].
114///
115/// The value persists across frames until changed again. Setting this between
116/// frames is supported; setting it mid-frame only affects subsequent calls
117/// to `ratatui_end_frame*`.
118#[no_mangle]
119pub extern "C" fn ratatui_set_background_color(
120    handle: *mut c_void,
121    r: u8, g: u8, b: u8,
122) {
123    if handle.is_null() { return; }
124    let state = unsafe { state_mut(handle) };
125    state.background_color = [r, g, b];
126}
127
128// ─── Lifecycle ───────────────────────────────────────────────────────────────
129
130/// Creates a terminal instance and returns an opaque handle.
131///
132/// The resulting handle owns:
133/// - a ratatui [`Terminal`](ratatui::Terminal) backed by [`TestBackend`](ratatui::backend::TestBackend)
134///   sized `cols × rows` cells,
135/// - a glyph-cached `FontManager` at `font_size` pixels,
136/// - a pre-allocated RGB24 pixel buffer matching `cols × rows × cell_size`.
137///
138/// The handle must eventually be released with [`ratatui_destroy`].
139///
140/// # Parameters
141/// - `cols`: terminal width in character cells.
142/// - `rows`: terminal height in character cells.
143/// - `font_size`: glyph rasterization size in pixels (e.g. `14.0`).
144#[no_mangle]
145pub extern "C" fn ratatui_create(cols: u16, rows: u16, font_size: f32) -> *mut c_void {
146    let state = Box::new(TerminalState::new(cols, rows, font_size));
147    Box::into_raw(state) as *mut c_void
148}
149
150/// Destroys a terminal handle created by [`ratatui_create`].
151///
152/// After this call the handle and any previously returned pixel-buffer
153/// pointers are invalid and must not be used. A null handle is a no-op.
154#[no_mangle]
155pub extern "C" fn ratatui_destroy(handle: *mut c_void) {
156    if !handle.is_null() {
157        unsafe { drop(Box::from_raw(handle as *mut TerminalState)); }
158    }
159}
160
161/// Replaces the embedded JetBrains Mono font with custom TTF bytes.
162///
163/// The cell width/height are recomputed from the new font's metrics, the
164/// glyph cache is dropped, and the pixel buffer is resized to the new
165/// `cols × cell_size` dimensions. After a successful call the values
166/// reported by [`ratatui_pixel_width`] / [`ratatui_pixel_height`] change
167/// accordingly; callers must re-query them and resize any host-side
168/// texture before the next `ratatui_end_frame*` call.
169///
170/// # Returns
171/// `1` on success, `0` if `handle` is null, `font_data` is null/empty, or the
172/// bytes are not a valid font.
173#[no_mangle]
174pub extern "C" fn ratatui_set_custom_font(
175    handle: *mut c_void,
176    font_data: *const u8,
177    font_len: u32,
178) -> u8 {
179    if handle.is_null() || font_data.is_null() || font_len == 0 { return 0; }
180    let state = unsafe { state_mut(handle) };
181    let bytes = unsafe { std::slice::from_raw_parts(font_data, font_len as usize) };
182    let ok = state.font.set_custom_font(bytes);
183    if ok {
184        state.resync_pixel_dimensions();
185    }
186    u8::from(ok)
187}
188
189// ─── Frame ───────────────────────────────────────────────────────────────────
190
191/// Begins a new frame.
192///
193/// Clears the queued widget command list, drops any in-progress builder state
194/// (styled paragraph, chart, canvas), resets the pending style to default, and
195/// resets the area map so that only the root area (id `0`) remains. Must be
196/// called before issuing widget commands for the new frame.
197#[no_mangle]
198pub extern "C" fn ratatui_begin_frame(handle: *mut c_void) {
199    if handle.is_null() { return; }
200    unsafe { state_mut(handle) }.begin_frame();
201}
202
203/// Renders all queued widget commands and rasterizes the cell buffer.
204///
205/// The returned pointer addresses a flat RGB24 buffer of size
206/// `pixel_width * pixel_height * 3` bytes, owned by the handle. The pointer is
207/// valid until the next FFI call that mutates the handle (typically the next
208/// `ratatui_end_frame*` call), at which point the buffer may be overwritten.
209///
210/// Returns `null` only when `handle` is null.
211#[no_mangle]
212pub extern "C" fn ratatui_end_frame(handle: *mut c_void) -> *const u8 {
213    if handle.is_null() { return std::ptr::null(); }
214    let state = unsafe { state_mut(handle) };
215    render_all_commands(state);
216    state.rasterize();
217    state.pixel_buffer.as_ptr()
218}
219
220/// Like `ratatui_end_frame`, but skips rasterization when the cell buffer
221/// is unchanged from the previous frame (hash-based dirty check).
222/// Returns a valid pixel pointer when content changed, or null when unchanged.
223/// The previous frame's pixel buffer remains valid when null is returned.
224#[no_mangle]
225pub extern "C" fn ratatui_end_frame_hashed(handle: *mut c_void) -> *const u8 {
226    if handle.is_null() { return std::ptr::null(); }
227    let state = unsafe { state_mut(handle) };
228    render_all_commands(state);
229
230    let hash = {
231        let buffer = state.terminal.backend().buffer();
232        crate::renderer::compute_buffer_hash(buffer)
233    };
234
235    if state.last_buffer_hash == Some(hash) {
236        return std::ptr::null();
237    }
238
239    state.last_buffer_hash = Some(hash);
240    state.rasterize();
241    state.pixel_buffer.as_ptr()
242}
243
244/// Returns the width of the pixel buffer in pixels (`cols * cell_width`).
245///
246/// Returns `0` if `handle` is null.
247#[no_mangle]
248pub extern "C" fn ratatui_pixel_width(handle: *const c_void) -> u32 {
249    if handle.is_null() { return 0; }
250    unsafe { &*(handle as *const TerminalState) }.pixel_width
251}
252
253/// Returns the height of the pixel buffer in pixels (`rows * cell_height`).
254///
255/// Returns `0` if `handle` is null.
256#[no_mangle]
257pub extern "C" fn ratatui_pixel_height(handle: *const c_void) -> u32 {
258    if handle.is_null() { return 0; }
259    unsafe { &*(handle as *const TerminalState) }.pixel_height
260}
261
262// ─── Layout ──────────────────────────────────────────────────────────────────
263
264/// Returns the id of the root area, which always covers the whole terminal.
265///
266/// The root id is the constant `0`; this getter exists for symmetry with the
267/// host-side area API.
268#[no_mangle]
269pub extern "C" fn ratatui_root_area(_handle: *const c_void) -> u32 { 0 }
270
271/// Splits an existing area into `count` child areas and writes their ids into
272/// `out_ids`.
273///
274/// # Parameters
275/// - `area_id`: id of the parent area to split.
276/// - `direction`: `0` = horizontal split (left → right), any other value =
277///   vertical split (top → bottom).
278/// - `constraint_types`: array of length `count` describing each child's
279///   constraint kind. Values: `0` = Length, `1` = Min, `2` = Max,
280///   `3` = Percentage, `4` (or any other) = Fill.
281/// - `constraint_values`: array of length `count` with the numeric value
282///   matching the constraint kind (cells for Length/Min/Max, 0..=100 for
283///   Percentage, weight for Fill).
284/// - `count`: number of child areas requested.
285/// - `out_ids`: caller-allocated buffer of length `count` that receives the
286///   ids of the newly registered child areas.
287///
288/// # Returns
289/// The number of child areas actually written. Returns `0` if any required
290/// pointer is null, `count` is zero, or the parent id is unknown.
291#[no_mangle]
292pub extern "C" fn ratatui_split(
293    handle: *mut c_void,
294    area_id: u32,
295    direction: u8,
296    constraint_types: *const u8,
297    constraint_values: *const u16,
298    count: u32,
299    out_ids: *mut u32,
300) -> u32 {
301    if handle.is_null()
302        || constraint_types.is_null()
303        || constraint_values.is_null()
304        || out_ids.is_null()
305        || count == 0
306    {
307        return 0;
308    }
309    let state = unsafe { state_mut(handle) };
310    let types = unsafe { std::slice::from_raw_parts(constraint_types, count as usize) };
311    let values = unsafe { std::slice::from_raw_parts(constraint_values, count as usize) };
312    let out = unsafe { std::slice::from_raw_parts_mut(out_ids, count as usize) };
313    do_split(state, area_id, direction, types, values, out)
314}
315
316/// Returns a new area id covering the inner rectangle of `area_id` shrunk by
317/// the given margins on each side.
318///
319/// # Parameters
320/// - `area_id`: parent area id.
321/// - `horizontal`: cells to remove from the left and right edges.
322/// - `vertical`: cells to remove from the top and bottom edges.
323///
324/// # Returns
325/// The id of the newly registered inner area, or [`u32::MAX`] if `handle` is
326/// null or `area_id` is unknown.
327#[no_mangle]
328pub extern "C" fn ratatui_inner(
329    handle: *mut c_void,
330    area_id: u32,
331    horizontal: u16,
332    vertical: u16,
333) -> u32 {
334    if handle.is_null() { return u32::MAX; }
335    let state = unsafe { state_mut(handle) };
336    let area = match state.area_map.get(&area_id).copied() {
337        Some(r) => r,
338        None => return u32::MAX,
339    };
340    use ratatui::layout::Margin;
341    let inner = area.inner(Margin { horizontal, vertical });
342    state.register_area(inner)
343}
344
345// ─── Style ───────────────────────────────────────────────────────────────────
346
347/// Sets the pending style consumed by the next widget-producing FFI call.
348///
349/// The pending style is reset to default after each widget call and at the
350/// start of every frame. Widgets that do not accept a style (e.g. scrollbar,
351/// calendar, chart, canvas) ignore the pending style.
352///
353/// # Parameters
354/// - `fg_r`, `fg_g`, `fg_b`: foreground RGB components.
355/// - `use_default_fg`: non-zero to leave the foreground unset (terminal
356///   default); zero to apply the given RGB triple.
357/// - `bg_r`, `bg_g`, `bg_b`: background RGB components.
358/// - `use_default_bg`: non-zero to leave the background unset; zero to apply
359///   the given RGB triple.
360/// - `modifiers`: bit field — `0x01` Bold, `0x02` Italic, `0x04` Underlined,
361///   `0x08` Dim.
362#[no_mangle]
363pub extern "C" fn ratatui_set_style(
364    handle: *mut c_void,
365    fg_r: u8, fg_g: u8, fg_b: u8, use_default_fg: u8,
366    bg_r: u8, bg_g: u8, bg_b: u8, use_default_bg: u8,
367    modifiers: u8,
368) {
369    if handle.is_null() { return; }
370    let state = unsafe { state_mut(handle) };
371    state.pending_style = style_from_rgba(
372        fg_r, fg_g, fg_b, use_default_fg,
373        bg_r, bg_g, bg_b, use_default_bg,
374        modifiers,
375    );
376}
377
378// ─── Basic widgets ────────────────────────────────────────────────────────────
379
380/// Queues a [`Block`](ratatui::widgets::Block) widget with an optional title
381/// and per-edge borders.
382///
383/// `borders` is a bit field — `0x01` Top, `0x02` Bottom, `0x04` Left,
384/// `0x08` Right. The value `0x0F` is treated as "all borders".
385///
386/// The pending style (see [`ratatui_set_style`]) is consumed and applied to
387/// the block.
388#[no_mangle]
389pub extern "C" fn ratatui_block(
390    handle: *mut c_void,
391    area_id: u32,
392    title: *const c_char,
393    borders: u8,
394) {
395    if handle.is_null() { return; }
396    let state = unsafe { state_mut(handle) };
397    let style = state.take_style();
398    state.commands.push(WidgetCommand::Block {
399        area_id,
400        title: unsafe { cstr_to_string(title) },
401        borders,
402        style,
403    });
404}
405
406/// Queues a uniformly styled [`Paragraph`](ratatui::widgets::Paragraph).
407///
408/// # Parameters
409/// - `text`: paragraph contents. Embedded `\n` produces line breaks.
410/// - `alignment`: `0` Left, `1` Center, `2` Right.
411/// - `wrap`: non-zero to enable word wrapping (`trim: false`).
412///
413/// For multi-style text use the styled-paragraph builder
414/// ([`ratatui_styled_para_begin`] / [`ratatui_styled_para_span`] /
415/// [`ratatui_styled_para_newline`] / [`ratatui_styled_para_end`]).
416#[no_mangle]
417pub extern "C" fn ratatui_paragraph(
418    handle: *mut c_void,
419    area_id: u32,
420    text: *const c_char,
421    alignment: u8,
422    wrap: u8,
423) {
424    if handle.is_null() { return; }
425    let state = unsafe { state_mut(handle) };
426    let style = state.take_style();
427    state.commands.push(WidgetCommand::Paragraph {
428        area_id,
429        text: unsafe { cstr_to_string(text) },
430        alignment,
431        wrap: wrap != 0,
432        style,
433    });
434}
435
436/// Queues a [`List`](ratatui::widgets::List) widget.
437///
438/// # Parameters
439/// - `items`: newline-separated list entries.
440/// - `selected`: zero-based index of the highlighted row, or `-1` for no
441///   selection. The highlight uses `"> "` as the prefix and a bold modifier.
442#[no_mangle]
443pub extern "C" fn ratatui_list(
444    handle: *mut c_void,
445    area_id: u32,
446    items: *const c_char,
447    selected: i32,
448) {
449    if handle.is_null() { return; }
450    let state = unsafe { state_mut(handle) };
451    let style = state.take_style();
452    state.commands.push(WidgetCommand::List {
453        area_id,
454        items: unsafe { cstr_to_string(items) },
455        selected,
456        style,
457    });
458}
459
460/// Queues a block-style [`Gauge`](ratatui::widgets::Gauge).
461///
462/// # Parameters
463/// - `ratio`: progress in `[0.0, 1.0]`. Values outside the range are clamped.
464/// - `label`: optional text overlaid on the gauge (pass `null` or empty for none).
465#[no_mangle]
466pub extern "C" fn ratatui_gauge(
467    handle: *mut c_void,
468    area_id: u32,
469    ratio: f32,
470    label: *const c_char,
471) {
472    if handle.is_null() { return; }
473    let state = unsafe { state_mut(handle) };
474    let style = state.take_style();
475    state.commands.push(WidgetCommand::Gauge {
476        area_id,
477        ratio: ratio as f64,
478        label: unsafe { cstr_to_string(label) },
479        style,
480    });
481}
482
483/// Queues a [`Tabs`](ratatui::widgets::Tabs) bar.
484///
485/// # Parameters
486/// - `titles`: newline-separated tab labels.
487/// - `selected`: zero-based index of the active tab.
488///
489/// The pending style's foreground color (or cyan if unset) is used as the
490/// highlight background of the active tab.
491#[no_mangle]
492pub extern "C" fn ratatui_tabs(
493    handle: *mut c_void,
494    area_id: u32,
495    titles: *const c_char,
496    selected: u32,
497) {
498    if handle.is_null() { return; }
499    let state = unsafe { state_mut(handle) };
500    let style = state.take_style();
501    state.commands.push(WidgetCommand::Tabs {
502        area_id,
503        titles: unsafe { cstr_to_string(titles) },
504        selected,
505        style,
506    });
507}
508
509/// Queues a [`Sparkline`](ratatui::widgets::Sparkline) from raw `u64` samples.
510///
511/// # Parameters
512/// - `data`: pointer to `len` `u64` samples.
513/// - `len`: number of samples.
514#[no_mangle]
515pub extern "C" fn ratatui_sparkline(
516    handle: *mut c_void,
517    area_id: u32,
518    data: *const u64,
519    len: u32,
520) {
521    if handle.is_null() || data.is_null() { return; }
522    let state = unsafe { state_mut(handle) };
523    let style = state.take_style();
524    let data_vec = unsafe { std::slice::from_raw_parts(data, len as usize) }.to_vec();
525    state.commands.push(WidgetCommand::Sparkline { area_id, data: data_vec, style });
526}
527
528/// Queues a [`Table`](ratatui::widgets::Table) with equal-width columns.
529///
530/// `data` format:
531/// - First line: tab-separated header cells.
532/// - Subsequent lines: one row per line; cells separated by tabs.
533///
534/// For typed column widths and row selection use [`ratatui_table_ex`].
535#[no_mangle]
536pub extern "C" fn ratatui_table(
537    handle: *mut c_void,
538    area_id: u32,
539    data: *const c_char,
540) {
541    if handle.is_null() { return; }
542    let state = unsafe { state_mut(handle) };
543    let style = state.take_style();
544    state.commands.push(WidgetCommand::Table {
545        area_id,
546        data: unsafe { cstr_to_string(data) },
547        style,
548    });
549}
550
551// ─── New widgets: BarChart, LineGauge, Scrollbar, Calendar, TableEx ──────────
552
553/// Queues a [`BarChart`](ratatui::widgets::BarChart).
554///
555/// `data` format: one bar per line, label and value separated by a tab.
556/// Malformed lines (missing tab or non-numeric value) are silently skipped.
557///
558/// # Parameters
559/// - `bar_width`: width of each bar in cells.
560/// - `bar_gap`: gap between bars in cells.
561#[no_mangle]
562pub extern "C" fn ratatui_barchart(
563    handle: *mut c_void,
564    area_id: u32,
565    data: *const c_char,
566    bar_width: u16,
567    bar_gap: u16,
568) {
569    if handle.is_null() { return; }
570    let state = unsafe { state_mut(handle) };
571    let style = state.take_style();
572    let data_str = unsafe { cstr_to_string(data) };
573    let bars: Vec<(String, u64)> = data_str
574        .lines()
575        .filter_map(|line| {
576            let mut parts = line.splitn(2, '\t');
577            let label = parts.next()?.to_string();
578            let value: u64 = parts.next()?.trim().parse().ok()?;
579            Some((label, value))
580        })
581        .collect();
582    state.commands.push(WidgetCommand::BarChart { area_id, bars, bar_width, bar_gap, style });
583}
584
585/// Queues a horizontal single-line [`LineGauge`](ratatui::widgets::LineGauge).
586///
587/// # Parameters
588/// - `ratio`: progress in `[0.0, 1.0]`; values outside the range are clamped.
589/// - `label`: text shown next to the gauge (pass `null` or empty for none).
590#[no_mangle]
591pub extern "C" fn ratatui_line_gauge(
592    handle: *mut c_void,
593    area_id: u32,
594    ratio: f32,
595    label: *const c_char,
596) {
597    if handle.is_null() { return; }
598    let state = unsafe { state_mut(handle) };
599    let style = state.take_style();
600    state.commands.push(WidgetCommand::LineGauge {
601        area_id,
602        ratio: ratio as f64,
603        label: unsafe { cstr_to_string(label) },
604        style,
605    });
606}
607
608/// Queues a [`Scrollbar`](ratatui::widgets::Scrollbar).
609///
610/// # Parameters
611/// - `content_length`: total scrollable length in cells.
612/// - `position`: current scroll offset in cells (`0..=content_length`).
613/// - `viewport_length`: visible portion of the content in cells.
614/// - `orientation`: `0` VerticalRight, `1` VerticalLeft, `2` HorizontalBottom,
615///   `3` HorizontalTop.
616#[no_mangle]
617pub extern "C" fn ratatui_scrollbar(
618    handle: *mut c_void,
619    area_id: u32,
620    content_length: u32,
621    position: u32,
622    viewport_length: u32,
623    orientation: u8,
624) {
625    if handle.is_null() { return; }
626    let state = unsafe { state_mut(handle) };
627    state.commands.push(WidgetCommand::Scrollbar {
628        area_id,
629        content_length,
630        position,
631        viewport_length,
632        orientation,
633    });
634}
635
636/// Queues a monthly calendar
637/// ([`Monthly`](ratatui::widgets::calendar::Monthly)).
638///
639/// Invalid dates fall back to January 1 of `year`, and if that also fails,
640/// to 2024-01-01. The `widget-calendar` Cargo feature must be enabled
641/// (it is, by default, in this crate).
642///
643/// # Parameters
644/// - `year`: full year (e.g. `2026`).
645/// - `month`: `1..=12`.
646/// - `day`: `1..=28` (later days are clamped to `28` to avoid month overflow).
647#[no_mangle]
648pub extern "C" fn ratatui_calendar(
649    handle: *mut c_void,
650    area_id: u32,
651    year: i32,
652    month: u8,
653    day: u8,
654) {
655    if handle.is_null() { return; }
656    let state = unsafe { state_mut(handle) };
657    state.commands.push(WidgetCommand::Calendar { area_id, year, month, day });
658}
659
660/// Queues an extended [`Table`](ratatui::widgets::Table) with typed column
661/// widths and optional row highlighting.
662///
663/// `data` follows the same format as [`ratatui_table`] (first line headers,
664/// subsequent lines rows; tab-separated cells).
665///
666/// # Parameters
667/// - `col_types` / `col_values`: parallel arrays of length `col_count`
668///   describing each column's constraint kind and value. Same encoding as
669///   [`ratatui_split`]. Pass `null` (or `col_count == 0`) for equal-width
670///   distribution.
671/// - `selected_row`: zero-based index of the highlighted row, or `-1` for
672///   no selection. The highlight uses a bold modifier.
673#[no_mangle]
674pub extern "C" fn ratatui_table_ex(
675    handle: *mut c_void,
676    area_id: u32,
677    data: *const c_char,
678    col_types: *const u8,
679    col_values: *const u16,
680    col_count: u32,
681    selected_row: i32,
682) {
683    if handle.is_null() { return; }
684    let state = unsafe { state_mut(handle) };
685    let style = state.take_style();
686    let col_constraints: Vec<(u8, u16)> =
687        if col_types.is_null() || col_values.is_null() || col_count == 0 {
688            Vec::new()
689        } else {
690            let types = unsafe { std::slice::from_raw_parts(col_types, col_count as usize) };
691            let values = unsafe { std::slice::from_raw_parts(col_values, col_count as usize) };
692            types.iter().zip(values.iter()).map(|(&t, &v)| (t, v)).collect()
693        };
694    state.commands.push(WidgetCommand::TableEx {
695        area_id,
696        data: unsafe { cstr_to_string(data) },
697        col_constraints,
698        selected_row,
699        style,
700    });
701}
702
703// ─── StyledParagraph builder ─────────────────────────────────────────────────
704
705/// Starts a multi-style paragraph builder.
706///
707/// Builder lifecycle:
708/// 1. [`ratatui_styled_para_begin`] — open the builder for `area_id`.
709/// 2. Zero or more [`ratatui_styled_para_span`] calls — append styled spans
710///    to the current line.
711/// 3. Zero or more [`ratatui_styled_para_newline`] calls — start a new line.
712/// 4. [`ratatui_styled_para_end`] — flush the builder into the command queue.
713///
714/// Only one styled-paragraph builder may be active at a time per handle.
715/// Beginning a new one before `_end` discards the previous one.
716///
717/// # Parameters
718/// - `alignment`: `0` Left, `1` Center, `2` Right.
719/// - `wrap`: non-zero to enable word wrapping (`trim: false`).
720#[no_mangle]
721pub extern "C" fn ratatui_styled_para_begin(
722    handle: *mut c_void,
723    area_id: u32,
724    alignment: u8,
725    wrap: u8,
726) {
727    if handle.is_null() { return; }
728    let state = unsafe { state_mut(handle) };
729    state.pending_styled_para = Some(PendingStyledParagraph {
730        area_id,
731        alignment,
732        wrap: wrap != 0,
733        lines: vec![vec![]],
734    });
735}
736
737/// Appends a styled [`Span`](ratatui::text::Span) to the current line of the
738/// pending styled paragraph.
739///
740/// Does nothing if no builder is active. Style parameters follow the same
741/// encoding as [`ratatui_set_style`].
742#[no_mangle]
743pub extern "C" fn ratatui_styled_para_span(
744    handle: *mut c_void,
745    text: *const c_char,
746    fg_r: u8, fg_g: u8, fg_b: u8, use_default_fg: u8,
747    bg_r: u8, bg_g: u8, bg_b: u8, use_default_bg: u8,
748    modifiers: u8,
749) {
750    if handle.is_null() { return; }
751    let state = unsafe { state_mut(handle) };
752    if let Some(ref mut pending) = state.pending_styled_para {
753        let style = style_from_rgba(
754            fg_r, fg_g, fg_b, use_default_fg,
755            bg_r, bg_g, bg_b, use_default_bg,
756            modifiers,
757        );
758        let span = SpanInfo { text: unsafe { cstr_to_string(text) }, style };
759        if let Some(last_line) = pending.lines.last_mut() {
760            last_line.push(span);
761        }
762    }
763}
764
765/// Starts a new line in the pending styled paragraph.
766///
767/// Does nothing if no builder is active.
768#[no_mangle]
769pub extern "C" fn ratatui_styled_para_newline(handle: *mut c_void) {
770    if handle.is_null() { return; }
771    let state = unsafe { state_mut(handle) };
772    if let Some(ref mut pending) = state.pending_styled_para {
773        pending.lines.push(vec![]);
774    }
775}
776
777/// Finalizes the pending styled paragraph and queues it for rendering.
778///
779/// Does nothing if no builder is active.
780#[no_mangle]
781pub extern "C" fn ratatui_styled_para_end(handle: *mut c_void) {
782    if handle.is_null() { return; }
783    let state = unsafe { state_mut(handle) };
784    if let Some(pending) = state.pending_styled_para.take() {
785        state.commands.push(WidgetCommand::StyledParagraph {
786            area_id: pending.area_id,
787            alignment: pending.alignment,
788            wrap: pending.wrap,
789            lines: pending.lines,
790        });
791    }
792}
793
794// ─── Chart builder ────────────────────────────────────────────────────────────
795
796/// Starts a [`Chart`](ratatui::widgets::Chart) builder.
797///
798/// Builder lifecycle:
799/// 1. [`ratatui_chart_begin`] — open the builder for `area_id`.
800/// 2. Optionally [`ratatui_chart_x_axis`] and/or [`ratatui_chart_y_axis`] —
801///    set axis titles and bounds.
802/// 3. Zero or more [`ratatui_chart_dataset`] calls — add datasets.
803/// 4. [`ratatui_chart_end`] — flush the builder into the command queue.
804///
805/// Only one chart builder may be active at a time per handle.
806#[no_mangle]
807pub extern "C" fn ratatui_chart_begin(handle: *mut c_void, area_id: u32) {
808    if handle.is_null() { return; }
809    let state = unsafe { state_mut(handle) };
810    state.pending_chart = Some(PendingChart {
811        area_id,
812        x_axis: None,
813        y_axis: None,
814        datasets: Vec::new(),
815    });
816}
817
818/// Sets the X axis title and `[min, max]` data bounds of the pending chart.
819///
820/// Does nothing if no chart builder is active.
821#[no_mangle]
822pub extern "C" fn ratatui_chart_x_axis(
823    handle: *mut c_void,
824    title: *const c_char,
825    min: f64,
826    max: f64,
827) {
828    if handle.is_null() { return; }
829    let state = unsafe { state_mut(handle) };
830    if let Some(ref mut pending) = state.pending_chart {
831        pending.x_axis = Some(AxisInfo { title: unsafe { cstr_to_string(title) }, min, max });
832    }
833}
834
835/// Sets the Y axis title and `[min, max]` data bounds of the pending chart.
836///
837/// Does nothing if no chart builder is active.
838#[no_mangle]
839pub extern "C" fn ratatui_chart_y_axis(
840    handle: *mut c_void,
841    title: *const c_char,
842    min: f64,
843    max: f64,
844) {
845    if handle.is_null() { return; }
846    let state = unsafe { state_mut(handle) };
847    if let Some(ref mut pending) = state.pending_chart {
848        pending.y_axis = Some(AxisInfo { title: unsafe { cstr_to_string(title) }, min, max });
849    }
850}
851
852/// Adds a [`Dataset`](ratatui::widgets::Dataset) to the pending chart.
853///
854/// # Parameters
855/// - `name`: dataset legend label.
856/// - `marker`: `0` Dot, `1` Braille, `2` HalfBlock, `3` Block.
857/// - `r`, `g`, `b`: dataset color.
858/// - `data`: pointer to `point_count * 2` `f64` values, interleaved as
859///   `[x0, y0, x1, y1, …]`.
860/// - `point_count`: number of `(x, y)` pairs.
861///
862/// Does nothing if no chart builder is active or `data` is null.
863#[no_mangle]
864pub extern "C" fn ratatui_chart_dataset(
865    handle: *mut c_void,
866    name: *const c_char,
867    marker: u8,
868    r: u8, g: u8, b: u8,
869    data: *const f64,
870    point_count: u32,
871) {
872    if handle.is_null() || data.is_null() { return; }
873    let state = unsafe { state_mut(handle) };
874    if let Some(ref mut pending) = state.pending_chart {
875        // Multiply in usize: `point_count * 2` can overflow u32.
876        let raw = unsafe { std::slice::from_raw_parts(data, point_count as usize * 2) };
877        let points: Vec<(f64, f64)> = raw.chunks(2).map(|c| (c[0], c[1])).collect();
878        pending.datasets.push(DatasetInfo {
879            name: unsafe { cstr_to_string(name) },
880            marker,
881            r, g, b,
882            points,
883        });
884    }
885}
886
887/// Finalizes the pending chart and queues it for rendering.
888///
889/// Does nothing if no chart builder is active.
890#[no_mangle]
891pub extern "C" fn ratatui_chart_end(handle: *mut c_void) {
892    if handle.is_null() { return; }
893    let state = unsafe { state_mut(handle) };
894    if let Some(pending) = state.pending_chart.take() {
895        state.commands.push(WidgetCommand::Chart {
896            area_id: pending.area_id,
897            x_axis: pending.x_axis,
898            y_axis: pending.y_axis,
899            datasets: pending.datasets,
900        });
901    }
902}
903
904// ─── Canvas builder ───────────────────────────────────────────────────────────
905
906/// Starts a [`Canvas`](ratatui::widgets::canvas::Canvas) builder.
907///
908/// Builder lifecycle:
909/// 1. [`ratatui_canvas_begin`] — open the builder for `area_id` with the
910///    given data-space bounds and marker style.
911/// 2. Zero or more shape calls — [`ratatui_canvas_map`],
912///    [`ratatui_canvas_line`], [`ratatui_canvas_circle`],
913///    [`ratatui_canvas_rectangle`], [`ratatui_canvas_text`],
914///    [`ratatui_canvas_points`], [`ratatui_canvas_layer`].
915/// 3. [`ratatui_canvas_end`] — flush the builder into the command queue.
916///
917/// Only one canvas builder may be active at a time per handle.
918///
919/// # Parameters
920/// - `x_min`, `x_max`, `y_min`, `y_max`: data-space bounds mapped onto the
921///   area.
922/// - `marker`: `0` Dot, `1` Braille, `2` HalfBlock, `3` Block.
923#[no_mangle]
924pub extern "C" fn ratatui_canvas_begin(
925    handle: *mut c_void,
926    area_id: u32,
927    x_min: f64, x_max: f64,
928    y_min: f64, y_max: f64,
929    marker: u8,
930) {
931    if handle.is_null() { return; }
932    let state = unsafe { state_mut(handle) };
933    state.pending_canvas = Some(PendingCanvas {
934        area_id,
935        x_min, x_max, y_min, y_max,
936        marker,
937        shapes: Vec::new(),
938    });
939}
940
941/// Draws the world map on the pending canvas.
942///
943/// # Parameters
944/// - `resolution`: `0` Low, any other value High.
945///
946/// Does nothing if no canvas builder is active.
947#[no_mangle]
948pub extern "C" fn ratatui_canvas_map(handle: *mut c_void, resolution: u8) {
949    if handle.is_null() { return; }
950    let state = unsafe { state_mut(handle) };
951    if let Some(ref mut p) = state.pending_canvas {
952        p.shapes.push(CanvasShape::Map { resolution });
953    }
954}
955
956/// Flushes the current canvas layer.
957///
958/// Subsequent shapes are drawn on a new layer on top of all previously drawn
959/// content. Does nothing if no canvas builder is active.
960#[no_mangle]
961pub extern "C" fn ratatui_canvas_layer(handle: *mut c_void) {
962    if handle.is_null() { return; }
963    let state = unsafe { state_mut(handle) };
964    if let Some(ref mut p) = state.pending_canvas { p.shapes.push(CanvasShape::Layer); }
965}
966
967/// Draws a colored line from `(x1, y1)` to `(x2, y2)` on the pending canvas.
968///
969/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
970#[no_mangle]
971pub extern "C" fn ratatui_canvas_line(
972    handle: *mut c_void,
973    x1: f64, y1: f64, x2: f64, y2: f64,
974    r: u8, g: u8, b: u8,
975) {
976    if handle.is_null() { return; }
977    let state = unsafe { state_mut(handle) };
978    if let Some(ref mut p) = state.pending_canvas {
979        p.shapes.push(CanvasShape::Line { x1, y1, x2, y2, r, g, b });
980    }
981}
982
983/// Draws a colored circle centered at `(x, y)` with the given `radius`.
984///
985/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
986#[no_mangle]
987pub extern "C" fn ratatui_canvas_circle(
988    handle: *mut c_void,
989    x: f64, y: f64, radius: f64,
990    r: u8, g: u8, b: u8,
991) {
992    if handle.is_null() { return; }
993    let state = unsafe { state_mut(handle) };
994    if let Some(ref mut p) = state.pending_canvas {
995        p.shapes.push(CanvasShape::Circle { x, y, radius, r, g, b });
996    }
997}
998
999/// Draws a colored rectangle outline anchored at `(x, y)` with size `(w, h)`.
1000///
1001/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
1002#[no_mangle]
1003pub extern "C" fn ratatui_canvas_rectangle(
1004    handle: *mut c_void,
1005    x: f64, y: f64, w: f64, h: f64,
1006    r: u8, g: u8, b: u8,
1007) {
1008    if handle.is_null() { return; }
1009    let state = unsafe { state_mut(handle) };
1010    if let Some(ref mut p) = state.pending_canvas {
1011        p.shapes.push(CanvasShape::Rectangle { x, y, w, h, r, g, b });
1012    }
1013}
1014
1015/// Draws colored text anchored at `(x, y)` on the pending canvas.
1016///
1017/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
1018#[no_mangle]
1019pub extern "C" fn ratatui_canvas_text(
1020    handle: *mut c_void,
1021    x: f64, y: f64,
1022    text: *const c_char,
1023    r: u8, g: u8, b: u8,
1024) {
1025    if handle.is_null() { return; }
1026    let state = unsafe { state_mut(handle) };
1027    if let Some(ref mut p) = state.pending_canvas {
1028        p.shapes.push(CanvasShape::Text { x, y, text: unsafe { cstr_to_string(text) }, r, g, b });
1029    }
1030}
1031
1032/// Draws a colored point cloud on the pending canvas.
1033///
1034/// # Parameters
1035/// - `coords`: pointer to `count * 2` `f64` values, interleaved as
1036///   `[x0, y0, x1, y1, …]`.
1037/// - `count`: number of `(x, y)` pairs.
1038///
1039/// Coordinates are in data space (see [`ratatui_canvas_begin`]). Does nothing
1040/// if no canvas builder is active or `coords` is null.
1041#[no_mangle]
1042pub extern "C" fn ratatui_canvas_points(
1043    handle: *mut c_void,
1044    coords: *const f64,
1045    count: u32,
1046    r: u8, g: u8, b: u8,
1047) {
1048    if handle.is_null() || coords.is_null() { return; }
1049    let state = unsafe { state_mut(handle) };
1050    if let Some(ref mut p) = state.pending_canvas {
1051        // Multiply in usize: `count * 2` can overflow u32.
1052        let raw = unsafe { std::slice::from_raw_parts(coords, count as usize * 2) };
1053        let pts: Vec<(f64, f64)> = raw.chunks(2).map(|c| (c[0], c[1])).collect();
1054        p.shapes.push(CanvasShape::Points { coords: pts, r, g, b });
1055    }
1056}
1057
1058/// Finalizes the pending canvas and queues it for rendering.
1059///
1060/// Does nothing if no canvas builder is active.
1061#[no_mangle]
1062pub extern "C" fn ratatui_canvas_end(handle: *mut c_void) {
1063    if handle.is_null() { return; }
1064    let state = unsafe { state_mut(handle) };
1065    if let Some(pending) = state.pending_canvas.take() {
1066        state.commands.push(WidgetCommand::Canvas {
1067            area_id: pending.area_id,
1068            x_min: pending.x_min,
1069            x_max: pending.x_max,
1070            y_min: pending.y_min,
1071            y_max: pending.y_max,
1072            marker: pending.marker,
1073            shapes: pending.shapes,
1074        });
1075    }
1076}
1077
1078// ─── Input / Hit-Testing ─────────────────────────────────────────────────────
1079
1080/// Returns the most specific area id covering the given terminal cell.
1081///
1082/// When several registered areas contain `(col, row)` the one with the
1083/// smallest cell count (the most deeply nested) wins. Returns `0` (root) when
1084/// no registered area matches.
1085///
1086/// Useful for mapping pointer input back into the layout tree.
1087#[no_mangle]
1088pub extern "C" fn ratatui_hit_test(
1089    handle: *mut c_void,
1090    col: u16,
1091    row: u16,
1092) -> u32 {
1093    if handle.is_null() { return 0; }
1094    let state = unsafe { state_mut(handle) };
1095    let mut best_id = 0u32;
1096    let mut best_area = u32::MAX;
1097
1098    for (&id, &rect) in &state.area_map {
1099        if col >= rect.x && col < rect.x + rect.width
1100            && row >= rect.y && row < rect.y + rect.height
1101        {
1102            let area = (rect.width as u32) * (rect.height as u32);
1103            if area < best_area {
1104                best_area = area;
1105                best_id = id;
1106            }
1107        }
1108    }
1109    best_id
1110}
1111
1112/// Returns the cell-space rectangle of the given area as a packed `u64`.
1113///
1114/// The four `u16` fields are packed little-endian:
1115///
1116/// ```text
1117/// bits  0..16  -> x
1118/// bits 16..32  -> y
1119/// bits 32..48  -> width
1120/// bits 48..64  -> height
1121/// ```
1122///
1123/// Returns `0` if `handle` is null or the area id is unknown.
1124#[no_mangle]
1125pub extern "C" fn ratatui_get_area_rect(
1126    handle: *const c_void,
1127    area_id: u32,
1128) -> u64 {
1129    if handle.is_null() { return 0; }
1130    let state = unsafe { &*(handle as *const TerminalState) };
1131    match state.area_map.get(&area_id) {
1132        Some(rect) => {
1133            (rect.x as u64)
1134                | ((rect.y as u64) << 16)
1135                | ((rect.width as u64) << 32)
1136                | ((rect.height as u64) << 48)
1137        }
1138        None => 0,
1139    }
1140}
1141
1142// ─── Utility ─────────────────────────────────────────────────────────────────
1143
1144/// Returns the library version as a static null-terminated C string.
1145///
1146/// The returned pointer is valid for the lifetime of the process and must
1147/// not be freed by the caller. The value matches `CARGO_PKG_VERSION`.
1148#[no_mangle]
1149pub extern "C" fn ratatui_version() -> *const c_char {
1150    concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155    use super::*;
1156    use std::ffi::CString;
1157
1158    /// Regression: 65536 tab-separated columns used to truncate `col_count`
1159    /// to 0 in the u16 cast and panic with a division by zero — and, before
1160    /// the column cap, stalled the layout solver for hours.
1161    #[test]
1162    fn table_with_more_than_u16_max_columns_does_not_panic() {
1163        let handle = ratatui_create(10, 5, 14.0);
1164        ratatui_begin_frame(handle);
1165        let data = CString::new(vec!["h"; 65536].join("\t")).unwrap();
1166        ratatui_table(handle, 0, data.as_ptr());
1167        ratatui_end_frame(handle);
1168        ratatui_destroy(handle);
1169    }
1170
1171    /// Regression: invalid font bytes must be rejected without panicking
1172    /// (the crate is built with `panic = "abort"` in release).
1173    #[test]
1174    fn set_custom_font_with_invalid_bytes_returns_zero() {
1175        let handle = ratatui_create(10, 5, 14.0);
1176        let bytes = [0u8; 16];
1177        assert_eq!(ratatui_set_custom_font(handle, bytes.as_ptr(), bytes.len() as u32), 0);
1178        ratatui_destroy(handle);
1179    }
1180
1181    /// Regression: after a successful font swap the reported pixel dimensions
1182    /// must match the rasterized buffer size.
1183    #[test]
1184    fn set_custom_font_resyncs_pixel_dimensions() {
1185        let handle = ratatui_create(10, 5, 14.0);
1186        let bytes = include_bytes!("../fonts/JetBrainsMono-Regular.ttf");
1187        assert_eq!(
1188            ratatui_set_custom_font(handle, bytes.as_ptr(), bytes.len() as u32),
1189            1
1190        );
1191        let w = ratatui_pixel_width(handle);
1192        let h = ratatui_pixel_height(handle);
1193        ratatui_begin_frame(handle);
1194        ratatui_end_frame(handle);
1195        let state = unsafe { &*(handle as *const TerminalState) };
1196        assert_eq!(state.pixel_buffer.len(), w as usize * h as usize * 3);
1197        ratatui_destroy(handle);
1198    }
1199}