Skip to main content

ftui_render/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Render kernel: cells, buffers, diffs, and ANSI presentation.
4//!
5//! # Role in FrankenTUI
6//! `ftui-render` is the deterministic rendering engine. It turns a logical
7//! `Frame` into a `Buffer`, computes diffs, and emits minimal ANSI output via
8//! the `Presenter`.
9//!
10//! # Primary responsibilities
11//! - **Cell/Buffer**: 2D grid with fixed-size cells and scissor/opacity stacks.
12//! - **BufferDiff**: efficient change detection between frames.
13//! - **Presenter**: stateful ANSI emitter with cursor/mode tracking.
14//! - **Frame**: rendering surface used by widgets and application views.
15//!
16//! # How it fits in the system
17//! `ftui-runtime` calls your model's `view()` to render into a `Frame`. That
18//! frame becomes a `Buffer`, which is diffed and presented to the terminal via
19//! `TerminalWriter`. This crate is the kernel of FrankenTUI's flicker-free,
20//! deterministic output guarantees.
21
22pub mod alloc_budget;
23pub mod ansi;
24pub mod arena;
25pub mod budget;
26pub mod buffer;
27pub mod cell;
28pub mod counting_writer;
29pub mod diff;
30pub mod diff_strategy;
31pub mod drawing;
32pub mod fit_metrics;
33pub mod frame;
34pub mod frame_guardrails;
35pub mod grapheme_pool;
36pub mod headless;
37pub mod link_registry;
38pub mod presenter;
39pub mod render_certificate;
40
41pub mod quotient_filter;
42pub mod roaring_bitmap;
43pub mod sanitize;
44pub mod spatial_hit_index;
45pub mod terminal_model;
46
47// Terminal color fidelity is owned by ftui-core and re-exported here so the
48// downstream style layer can share the exact contract without a dependency
49// cycle back from the render kernel.
50pub use ftui_core::terminal_capabilities::ColorDepth;
51
52// Re-export text width helpers from ftui-core (single source of truth).
53pub(crate) use ftui_core::text_width::{char_width, display_width, grapheme_width};
54
55#[cfg(test)]
56mod tests {
57    use super::{char_width, display_width, grapheme_width};
58
59    // ── display_width ────────────────────────────────────────────────
60
61    #[test]
62    fn display_width_matches_expected_samples() {
63        // Avoid CJK samples to keep results independent of locale/CJK width flags.
64        // Note: ftui-core strips VS16 (U+FE0F) by default for terminal-realistic
65        // widths, so text-default emoji like ❤️/⌨️/⚠️ measure as their base
66        // text-presentation width rather than emoji-presentation width 2.
67        let samples = [
68            ("hello", 5usize),
69            ("😀", 2usize),
70            ("👩‍💻", 2usize),
71            ("🇺🇸", 2usize),
72            ("⭐", 2usize),
73            ("A😀B", 4usize),
74            ("ok ✅", 5usize),
75        ];
76        for (sample, expected) in samples {
77            assert_eq!(
78                display_width(sample),
79                expected,
80                "display width mismatch for {sample:?}"
81            );
82        }
83    }
84
85    #[test]
86    fn display_width_empty_string() {
87        assert_eq!(display_width(""), 0);
88    }
89
90    #[test]
91    fn display_width_single_ascii_char() {
92        assert_eq!(display_width("x"), 1);
93        assert_eq!(display_width(" "), 1);
94    }
95
96    #[test]
97    fn display_width_pure_ascii_fast_path() {
98        assert_eq!(display_width("Hello, World!"), 13);
99        assert_eq!(display_width("fn main() {}"), 12);
100    }
101
102    #[test]
103    fn display_width_ascii_with_tabs() {
104        assert_eq!(display_width("a\tb"), 3);
105        assert_eq!(display_width("\n"), 1);
106    }
107
108    #[test]
109    fn display_width_mixed_ascii_emoji() {
110        assert_eq!(display_width("hi 🎉"), 5);
111        assert_eq!(display_width("🚀start"), 7);
112    }
113
114    #[test]
115    fn display_width_zero_width_chars_in_string() {
116        let s = "a\u{00AD}b";
117        assert_eq!(display_width(s), 2);
118    }
119
120    #[test]
121    fn display_width_combining_characters() {
122        let s = "e\u{0301}";
123        assert_eq!(display_width(s), 1);
124    }
125
126    #[test]
127    fn display_width_multiple_emoji() {
128        assert_eq!(display_width("😀😀😀"), 6);
129    }
130
131    // ── grapheme_width ───────────────────────────────────────────────
132
133    #[test]
134    fn grapheme_width_matches_expected_samples() {
135        // VS16 emoji (❤️/⌨️/⚠️) removed — ftui-core strips VS16 by default
136        // (terminal-realistic) so their widths depend on base char EAW, not emoji
137        // presentation.  Dedicated VS16 tests live in ftui-core.
138        let samples = [
139            ("a", 1usize),
140            ("😀", 2usize),
141            ("👩‍💻", 2usize),
142            ("🇺🇸", 2usize),
143            ("👍🏽", 2usize),
144            ("⭐", 2usize),
145        ];
146        for (grapheme, expected) in samples {
147            assert_eq!(
148                grapheme_width(grapheme),
149                expected,
150                "grapheme width mismatch for {grapheme:?}"
151            );
152        }
153    }
154
155    #[test]
156    fn grapheme_width_ascii_space() {
157        assert_eq!(grapheme_width(" "), 1);
158    }
159
160    #[test]
161    fn grapheme_width_ascii_tilde() {
162        assert_eq!(grapheme_width("~"), 1);
163    }
164
165    #[test]
166    fn grapheme_width_tab() {
167        assert_eq!(grapheme_width("\t"), 1);
168    }
169
170    #[test]
171    fn grapheme_width_newline() {
172        assert_eq!(grapheme_width("\n"), 1);
173    }
174
175    #[test]
176    fn grapheme_width_combining_accent() {
177        assert_eq!(grapheme_width("e\u{0301}"), 1);
178    }
179
180    #[test]
181    fn grapheme_width_zero_width_space() {
182        assert_eq!(grapheme_width("\u{200B}"), 0);
183    }
184
185    #[test]
186    fn grapheme_width_zero_width_joiner() {
187        assert_eq!(grapheme_width("\u{200D}"), 0);
188    }
189
190    #[test]
191    fn grapheme_width_skin_tone_modifier() {
192        assert_eq!(grapheme_width("👍🏿"), 2);
193    }
194
195    // ── char_width ───────────────────────────────────────────────────
196
197    #[test]
198    fn char_width_ascii_printable() {
199        assert_eq!(char_width('A'), 1);
200        assert_eq!(char_width('z'), 1);
201        assert_eq!(char_width(' '), 1);
202        assert_eq!(char_width('~'), 1);
203        assert_eq!(char_width('!'), 1);
204    }
205
206    #[test]
207    fn char_width_ascii_whitespace() {
208        assert_eq!(char_width('\t'), 1);
209        assert_eq!(char_width('\n'), 1);
210        assert_eq!(char_width('\r'), 1);
211    }
212
213    #[test]
214    fn char_width_ascii_control() {
215        assert_eq!(char_width('\x00'), 0);
216        assert_eq!(char_width('\x01'), 0);
217        assert_eq!(char_width('\x1F'), 0);
218        assert_eq!(char_width('\x7F'), 0);
219    }
220
221    #[test]
222    fn char_width_zero_width_combining() {
223        assert_eq!(char_width('\u{0300}'), 0);
224        assert_eq!(char_width('\u{0301}'), 0);
225    }
226
227    #[test]
228    fn char_width_zero_width_special() {
229        assert_eq!(char_width('\u{200B}'), 0);
230        assert_eq!(char_width('\u{200D}'), 0);
231        assert_eq!(char_width('\u{FEFF}'), 0);
232        assert_eq!(char_width('\u{00AD}'), 0);
233    }
234
235    #[test]
236    fn char_width_variation_selectors() {
237        assert_eq!(char_width('\u{FE00}'), 0);
238        assert_eq!(char_width('\u{FE0F}'), 0);
239    }
240
241    #[test]
242    fn char_width_bidi_controls() {
243        assert_eq!(char_width('\u{200E}'), 0);
244        assert_eq!(char_width('\u{200F}'), 0);
245    }
246
247    #[test]
248    fn char_width_normal_non_ascii() {
249        assert_eq!(char_width('é'), 1);
250        assert_eq!(char_width('ñ'), 1);
251    }
252
253    #[test]
254    fn char_width_euro_sign() {
255        assert_eq!(char_width('€'), 1);
256    }
257}