ratatui_unity/ffi/
builders.rs

1//! Multi-call builder entry points: styled paragraph, chart, and canvas.
2
3use crate::ffi::util::{cstr_to_string, slice_from, state_mut, style_from_rgba};
4use crate::terminal::{
5    AxisInfo, CanvasShape, DatasetInfo, PendingCanvas, PendingChart, PendingStyledParagraph,
6    SpanInfo, WidgetCommand,
7};
8use std::ffi::c_void;
9use std::os::raw::c_char;
10
11// ─── StyledParagraph builder ─────────────────────────────────────────────────
12
13/// Starts a multi-style paragraph builder.
14///
15/// Builder lifecycle:
16/// 1. [`ratatui_styled_para_begin`] — open the builder for `area_id`.
17/// 2. Zero or more [`ratatui_styled_para_span`] calls — append styled spans
18///    to the current line.
19/// 3. Zero or more [`ratatui_styled_para_newline`] calls — start a new line.
20/// 4. [`ratatui_styled_para_end`] — flush the builder into the command queue.
21///
22/// Only one styled-paragraph builder may be active at a time per handle.
23/// Beginning a new one before `_end` discards the previous one.
24///
25/// # Parameters
26/// - `alignment`: `0` Left, `1` Center, `2` Right.
27/// - `wrap`: non-zero to enable word wrapping (`trim: false`).
28#[no_mangle]
29pub extern "C" fn ratatui_styled_para_begin(
30    handle: *mut c_void,
31    area_id: u32,
32    alignment: u8,
33    wrap: u8,
34) {
35    let Some(state) = state_mut(handle) else { return; };
36    state.pending_styled_para = Some(PendingStyledParagraph {
37        area_id,
38        alignment,
39        wrap: wrap != 0,
40        lines: vec![vec![]],
41    });
42}
43
44/// Appends a styled [`Span`](ratatui::text::Span) to the current line of the
45/// pending styled paragraph.
46///
47/// Does nothing if no builder is active. Style parameters follow the same
48/// encoding as [`ratatui_set_style`](crate::ratatui_set_style).
49#[no_mangle]
50pub extern "C" fn ratatui_styled_para_span(
51    handle: *mut c_void,
52    text: *const c_char,
53    fg_r: u8, fg_g: u8, fg_b: u8, use_default_fg: u8,
54    bg_r: u8, bg_g: u8, bg_b: u8, use_default_bg: u8,
55    modifiers: u8,
56) {
57    let Some(state) = state_mut(handle) else { return; };
58    if let Some(ref mut pending) = state.pending_styled_para {
59        let style = style_from_rgba(
60            fg_r, fg_g, fg_b, use_default_fg,
61            bg_r, bg_g, bg_b, use_default_bg,
62            modifiers,
63        );
64        let span = SpanInfo { text: cstr_to_string(text), style };
65        if let Some(last_line) = pending.lines.last_mut() {
66            last_line.push(span);
67        }
68    }
69}
70
71/// Starts a new line in the pending styled paragraph.
72///
73/// Does nothing if no builder is active.
74#[no_mangle]
75pub extern "C" fn ratatui_styled_para_newline(handle: *mut c_void) {
76    let Some(state) = state_mut(handle) else { return; };
77    if let Some(ref mut pending) = state.pending_styled_para {
78        pending.lines.push(vec![]);
79    }
80}
81
82/// Finalizes the pending styled paragraph and queues it for rendering.
83///
84/// Does nothing if no builder is active.
85#[no_mangle]
86pub extern "C" fn ratatui_styled_para_end(handle: *mut c_void) {
87    let Some(state) = state_mut(handle) else { return; };
88    if let Some(pending) = state.pending_styled_para.take() {
89        state.commands.push(WidgetCommand::StyledParagraph {
90            area_id: pending.area_id,
91            alignment: pending.alignment,
92            wrap: pending.wrap,
93            lines: pending.lines,
94        });
95    }
96}
97
98// ─── Chart builder ───────────────────────────────────────────────────────────
99
100/// Starts a [`Chart`](ratatui::widgets::Chart) builder.
101///
102/// Builder lifecycle:
103/// 1. [`ratatui_chart_begin`] — open the builder for `area_id`.
104/// 2. Optionally [`ratatui_chart_x_axis`] and/or [`ratatui_chart_y_axis`] —
105///    set axis titles and bounds.
106/// 3. Zero or more [`ratatui_chart_dataset`] calls — add datasets.
107/// 4. [`ratatui_chart_end`] — flush the builder into the command queue.
108///
109/// Only one chart builder may be active at a time per handle.
110#[no_mangle]
111pub extern "C" fn ratatui_chart_begin(handle: *mut c_void, area_id: u32) {
112    let Some(state) = state_mut(handle) else { return; };
113    state.pending_chart = Some(PendingChart {
114        area_id,
115        x_axis: None,
116        y_axis: None,
117        datasets: Vec::new(),
118    });
119}
120
121/// Sets the X axis title and `[min, max]` data bounds of the pending chart.
122///
123/// Does nothing if no chart builder is active.
124#[no_mangle]
125pub extern "C" fn ratatui_chart_x_axis(
126    handle: *mut c_void,
127    title: *const c_char,
128    min: f64,
129    max: f64,
130) {
131    let Some(state) = state_mut(handle) else { return; };
132    if let Some(ref mut pending) = state.pending_chart {
133        pending.x_axis = Some(AxisInfo { title: cstr_to_string(title), min, max });
134    }
135}
136
137/// Sets the Y axis title and `[min, max]` data bounds of the pending chart.
138///
139/// Does nothing if no chart builder is active.
140#[no_mangle]
141pub extern "C" fn ratatui_chart_y_axis(
142    handle: *mut c_void,
143    title: *const c_char,
144    min: f64,
145    max: f64,
146) {
147    let Some(state) = state_mut(handle) else { return; };
148    if let Some(ref mut pending) = state.pending_chart {
149        pending.y_axis = Some(AxisInfo { title: cstr_to_string(title), min, max });
150    }
151}
152
153/// Adds a [`Dataset`](ratatui::widgets::Dataset) to the pending chart.
154///
155/// # Parameters
156/// - `name`: dataset legend label.
157/// - `marker`: `0` Dot, `1` Braille, `2` HalfBlock, `3` Block.
158/// - `r`, `g`, `b`: dataset color.
159/// - `data`: pointer to `point_count * 2` `f64` values, interleaved as
160///   `[x0, y0, x1, y1, …]`.
161/// - `point_count`: number of `(x, y)` pairs.
162///
163/// Does nothing if no chart builder is active or `data` is null.
164#[no_mangle]
165pub extern "C" fn ratatui_chart_dataset(
166    handle: *mut c_void,
167    name: *const c_char,
168    marker: u8,
169    r: u8, g: u8, b: u8,
170    data: *const f64,
171    point_count: u32,
172) {
173    if data.is_null() { return; }
174    let Some(state) = state_mut(handle) else { return; };
175    if let Some(ref mut pending) = state.pending_chart {
176        // Multiply in usize: `point_count * 2` can overflow u32.
177        let raw = slice_from(data, point_count as usize * 2);
178        let points: Vec<(f64, f64)> = raw.chunks(2).map(|c| (c[0], c[1])).collect();
179        pending.datasets.push(DatasetInfo {
180            name: cstr_to_string(name),
181            marker,
182            r, g, b,
183            points,
184        });
185    }
186}
187
188/// Finalizes the pending chart and queues it for rendering.
189///
190/// Does nothing if no chart builder is active.
191#[no_mangle]
192pub extern "C" fn ratatui_chart_end(handle: *mut c_void) {
193    let Some(state) = state_mut(handle) else { return; };
194    if let Some(pending) = state.pending_chart.take() {
195        state.commands.push(WidgetCommand::Chart {
196            area_id: pending.area_id,
197            x_axis: pending.x_axis,
198            y_axis: pending.y_axis,
199            datasets: pending.datasets,
200        });
201    }
202}
203
204// ─── Canvas builder ──────────────────────────────────────────────────────────
205
206/// Starts a [`Canvas`](ratatui::widgets::canvas::Canvas) builder.
207///
208/// Builder lifecycle:
209/// 1. [`ratatui_canvas_begin`] — open the builder for `area_id` with the
210///    given data-space bounds and marker style.
211/// 2. Zero or more shape calls — [`ratatui_canvas_map`],
212///    [`ratatui_canvas_line`], [`ratatui_canvas_circle`],
213///    [`ratatui_canvas_rectangle`], [`ratatui_canvas_text`],
214///    [`ratatui_canvas_points`], [`ratatui_canvas_layer`].
215/// 3. [`ratatui_canvas_end`] — flush the builder into the command queue.
216///
217/// Only one canvas builder may be active at a time per handle.
218///
219/// # Parameters
220/// - `x_min`, `x_max`, `y_min`, `y_max`: data-space bounds mapped onto the
221///   area.
222/// - `marker`: `0` Dot, `1` Braille, `2` HalfBlock, `3` Block.
223#[no_mangle]
224pub extern "C" fn ratatui_canvas_begin(
225    handle: *mut c_void,
226    area_id: u32,
227    x_min: f64, x_max: f64,
228    y_min: f64, y_max: f64,
229    marker: u8,
230) {
231    let Some(state) = state_mut(handle) else { return; };
232    state.pending_canvas = Some(PendingCanvas {
233        area_id,
234        x_min, x_max, y_min, y_max,
235        marker,
236        shapes: Vec::new(),
237    });
238}
239
240/// Draws the world map on the pending canvas.
241///
242/// # Parameters
243/// - `resolution`: `0` Low, any other value High.
244///
245/// Does nothing if no canvas builder is active.
246#[no_mangle]
247pub extern "C" fn ratatui_canvas_map(handle: *mut c_void, resolution: u8) {
248    let Some(state) = state_mut(handle) else { return; };
249    if let Some(ref mut p) = state.pending_canvas {
250        p.shapes.push(CanvasShape::Map { resolution });
251    }
252}
253
254/// Flushes the current canvas layer.
255///
256/// Subsequent shapes are drawn on a new layer on top of all previously drawn
257/// content. Does nothing if no canvas builder is active.
258#[no_mangle]
259pub extern "C" fn ratatui_canvas_layer(handle: *mut c_void) {
260    let Some(state) = state_mut(handle) else { return; };
261    if let Some(ref mut p) = state.pending_canvas { p.shapes.push(CanvasShape::Layer); }
262}
263
264/// Draws a colored line from `(x1, y1)` to `(x2, y2)` on the pending canvas.
265///
266/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
267#[no_mangle]
268pub extern "C" fn ratatui_canvas_line(
269    handle: *mut c_void,
270    x1: f64, y1: f64, x2: f64, y2: f64,
271    r: u8, g: u8, b: u8,
272) {
273    let Some(state) = state_mut(handle) else { return; };
274    if let Some(ref mut p) = state.pending_canvas {
275        p.shapes.push(CanvasShape::Line { x1, y1, x2, y2, r, g, b });
276    }
277}
278
279/// Draws a colored circle centered at `(x, y)` with the given `radius`.
280///
281/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
282#[no_mangle]
283pub extern "C" fn ratatui_canvas_circle(
284    handle: *mut c_void,
285    x: f64, y: f64, radius: f64,
286    r: u8, g: u8, b: u8,
287) {
288    let Some(state) = state_mut(handle) else { return; };
289    if let Some(ref mut p) = state.pending_canvas {
290        p.shapes.push(CanvasShape::Circle { x, y, radius, r, g, b });
291    }
292}
293
294/// Draws a colored rectangle outline anchored at `(x, y)` with size `(w, h)`.
295///
296/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
297#[no_mangle]
298pub extern "C" fn ratatui_canvas_rectangle(
299    handle: *mut c_void,
300    x: f64, y: f64, w: f64, h: f64,
301    r: u8, g: u8, b: u8,
302) {
303    let Some(state) = state_mut(handle) else { return; };
304    if let Some(ref mut p) = state.pending_canvas {
305        p.shapes.push(CanvasShape::Rectangle { x, y, w, h, r, g, b });
306    }
307}
308
309/// Draws colored text anchored at `(x, y)` on the pending canvas.
310///
311/// Coordinates are in data space (see [`ratatui_canvas_begin`]).
312#[no_mangle]
313pub extern "C" fn ratatui_canvas_text(
314    handle: *mut c_void,
315    x: f64, y: f64,
316    text: *const c_char,
317    r: u8, g: u8, b: u8,
318) {
319    let Some(state) = state_mut(handle) else { return; };
320    if let Some(ref mut p) = state.pending_canvas {
321        p.shapes.push(CanvasShape::Text { x, y, text: cstr_to_string(text), r, g, b });
322    }
323}
324
325/// Draws a colored point cloud on the pending canvas.
326///
327/// # Parameters
328/// - `coords`: pointer to `count * 2` `f64` values, interleaved as
329///   `[x0, y0, x1, y1, …]`.
330/// - `count`: number of `(x, y)` pairs.
331///
332/// Coordinates are in data space (see [`ratatui_canvas_begin`]). Does nothing
333/// if no canvas builder is active or `coords` is null.
334#[no_mangle]
335pub extern "C" fn ratatui_canvas_points(
336    handle: *mut c_void,
337    coords: *const f64,
338    count: u32,
339    r: u8, g: u8, b: u8,
340) {
341    if coords.is_null() { return; }
342    let Some(state) = state_mut(handle) else { return; };
343    if let Some(ref mut p) = state.pending_canvas {
344        // Multiply in usize: `count * 2` can overflow u32.
345        let raw = slice_from(coords, count as usize * 2);
346        let pts: Vec<(f64, f64)> = raw.chunks(2).map(|c| (c[0], c[1])).collect();
347        p.shapes.push(CanvasShape::Points { coords: pts, r, g, b });
348    }
349}
350
351/// Finalizes the pending canvas and queues it for rendering.
352///
353/// Does nothing if no canvas builder is active.
354#[no_mangle]
355pub extern "C" fn ratatui_canvas_end(handle: *mut c_void) {
356    let Some(state) = state_mut(handle) else { return; };
357    if let Some(pending) = state.pending_canvas.take() {
358        state.commands.push(WidgetCommand::Canvas {
359            area_id: pending.area_id,
360            x_min: pending.x_min,
361            x_max: pending.x_max,
362            y_min: pending.y_min,
363            y_max: pending.y_max,
364            marker: pending.marker,
365            shapes: pending.shapes,
366        });
367    }
368}