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}