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