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