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;
39
40pub mod quotient_filter;
41pub mod roaring_bitmap;
42pub mod sanitize;
43pub mod spatial_hit_index;
44pub mod terminal_model;
45
46// Re-export text width helpers from ftui-core (single source of truth).
47pub(crate) use ftui_core::text_width::{char_width, display_width, grapheme_width};
48
49#[cfg(test)]
50mod tests {
51    use super::{char_width, display_width, grapheme_width};
52
53    // ── display_width ────────────────────────────────────────────────
54
55    #[test]
56    fn display_width_matches_expected_samples() {
57        // Avoid CJK samples to keep results independent of locale/CJK width flags.
58        // Note: ftui-core strips VS16 (U+FE0F) by default for terminal-realistic
59        // widths, so text-default emoji like ❤️/⌨️/⚠️ measure as their base
60        // text-presentation width rather than emoji-presentation width 2.
61        let samples = [
62            ("hello", 5usize),
63            ("😀", 2usize),
64            ("👩‍💻", 2usize),
65            ("🇺🇸", 2usize),
66            ("⭐", 2usize),
67            ("A😀B", 4usize),
68            ("ok ✅", 5usize),
69        ];
70        for (sample, expected) in samples {
71            assert_eq!(
72                display_width(sample),
73                expected,
74                "display width mismatch for {sample:?}"
75            );
76        }
77    }
78
79    #[test]
80    fn display_width_empty_string() {
81        assert_eq!(display_width(""), 0);
82    }
83
84    #[test]
85    fn display_width_single_ascii_char() {
86        assert_eq!(display_width("x"), 1);
87        assert_eq!(display_width(" "), 1);
88    }
89
90    #[test]
91    fn display_width_pure_ascii_fast_path() {
92        assert_eq!(display_width("Hello, World!"), 13);
93        assert_eq!(display_width("fn main() {}"), 12);
94    }
95
96    #[test]
97    fn display_width_ascii_with_tabs() {
98        assert_eq!(display_width("a\tb"), 3);
99        assert_eq!(display_width("\n"), 1);
100    }
101
102    #[test]
103    fn display_width_mixed_ascii_emoji() {
104        assert_eq!(display_width("hi 🎉"), 5);
105        assert_eq!(display_width("🚀start"), 7);
106    }
107
108    #[test]
109    fn display_width_zero_width_chars_in_string() {
110        let s = "a\u{00AD}b";
111        assert_eq!(display_width(s), 2);
112    }
113
114    #[test]
115    fn display_width_combining_characters() {
116        let s = "e\u{0301}";
117        assert_eq!(display_width(s), 1);
118    }
119
120    #[test]
121    fn display_width_multiple_emoji() {
122        assert_eq!(display_width("😀😀😀"), 6);
123    }
124
125    // ── grapheme_width ───────────────────────────────────────────────
126
127    #[test]
128    fn grapheme_width_matches_expected_samples() {
129        // VS16 emoji (❤️/⌨️/⚠️) removed — ftui-core strips VS16 by default
130        // (terminal-realistic) so their widths depend on base char EAW, not emoji
131        // presentation.  Dedicated VS16 tests live in ftui-core.
132        let samples = [
133            ("a", 1usize),
134            ("😀", 2usize),
135            ("👩‍💻", 2usize),
136            ("🇺🇸", 2usize),
137            ("👍🏽", 2usize),
138            ("⭐", 2usize),
139        ];
140        for (grapheme, expected) in samples {
141            assert_eq!(
142                grapheme_width(grapheme),
143                expected,
144                "grapheme width mismatch for {grapheme:?}"
145            );
146        }
147    }
148
149    #[test]
150    fn grapheme_width_ascii_space() {
151        assert_eq!(grapheme_width(" "), 1);
152    }
153
154    #[test]
155    fn grapheme_width_ascii_tilde() {
156        assert_eq!(grapheme_width("~"), 1);
157    }
158
159    #[test]
160    fn grapheme_width_tab() {
161        assert_eq!(grapheme_width("\t"), 1);
162    }
163
164    #[test]
165    fn grapheme_width_newline() {
166        assert_eq!(grapheme_width("\n"), 1);
167    }
168
169    #[test]
170    fn grapheme_width_combining_accent() {
171        assert_eq!(grapheme_width("e\u{0301}"), 1);
172    }
173
174    #[test]
175    fn grapheme_width_zero_width_space() {
176        assert_eq!(grapheme_width("\u{200B}"), 0);
177    }
178
179    #[test]
180    fn grapheme_width_zero_width_joiner() {
181        assert_eq!(grapheme_width("\u{200D}"), 0);
182    }
183
184    #[test]
185    fn grapheme_width_skin_tone_modifier() {
186        assert_eq!(grapheme_width("👍🏿"), 2);
187    }
188
189    // ── char_width ───────────────────────────────────────────────────
190
191    #[test]
192    fn char_width_ascii_printable() {
193        assert_eq!(char_width('A'), 1);
194        assert_eq!(char_width('z'), 1);
195        assert_eq!(char_width(' '), 1);
196        assert_eq!(char_width('~'), 1);
197        assert_eq!(char_width('!'), 1);
198    }
199
200    #[test]
201    fn char_width_ascii_whitespace() {
202        assert_eq!(char_width('\t'), 1);
203        assert_eq!(char_width('\n'), 1);
204        assert_eq!(char_width('\r'), 1);
205    }
206
207    #[test]
208    fn char_width_ascii_control() {
209        assert_eq!(char_width('\x00'), 0);
210        assert_eq!(char_width('\x01'), 0);
211        assert_eq!(char_width('\x1F'), 0);
212        assert_eq!(char_width('\x7F'), 0);
213    }
214
215    #[test]
216    fn char_width_zero_width_combining() {
217        assert_eq!(char_width('\u{0300}'), 0);
218        assert_eq!(char_width('\u{0301}'), 0);
219    }
220
221    #[test]
222    fn char_width_zero_width_special() {
223        assert_eq!(char_width('\u{200B}'), 0);
224        assert_eq!(char_width('\u{200D}'), 0);
225        assert_eq!(char_width('\u{FEFF}'), 0);
226        assert_eq!(char_width('\u{00AD}'), 0);
227    }
228
229    #[test]
230    fn char_width_variation_selectors() {
231        assert_eq!(char_width('\u{FE00}'), 0);
232        assert_eq!(char_width('\u{FE0F}'), 0);
233    }
234
235    #[test]
236    fn char_width_bidi_controls() {
237        assert_eq!(char_width('\u{200E}'), 0);
238        assert_eq!(char_width('\u{200F}'), 0);
239    }
240
241    #[test]
242    fn char_width_normal_non_ascii() {
243        assert_eq!(char_width('é'), 1);
244        assert_eq!(char_width('ñ'), 1);
245    }
246
247    #[test]
248    fn char_width_euro_sign() {
249        assert_eq!(char_width('€'), 1);
250    }
251}