#![allow(clippy::uninlined_format_args)]
mod common;
use common::harness::E2EHarness;
use opentui::buffer::ClipRect;
use opentui::color::Rgba;
use opentui::grapheme_pool::GraphemePool;
use opentui::style::Style;
use opentui::unicode::{display_width, display_width_char, graphemes, reorder_for_display};
use opentui::{EditBuffer, EditorView, OptimizedBuffer};
use opentui_rust as opentui;
fn verify_text_render(buffer: &OptimizedBuffer, x: u32, y: u32, text: &str, _style: Style) {
let mut col = x;
for grapheme in graphemes(text) {
let width = u32::try_from(display_width(grapheme)).expect("width fits u32");
if matches!(width, 0) {
continue;
}
let cell = buffer.get(col, y).expect("cell should exist");
assert!(
!cell.is_continuation(),
"first cell of grapheme '{}' at ({},{}) should not be continuation",
grapheme,
col,
y
);
for i in 1..width {
let cont_cell = buffer
.get(col + i, y)
.expect("continuation cell should exist");
assert!(
cont_cell.is_continuation(),
"cell at ({},{}) should be continuation for wide grapheme '{}'",
col + i,
y,
grapheme
);
}
col += width;
}
}
#[test]
fn test_cjk_basic_rendering() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let text = "\u{4E2D}\u{6587}"; buffer.draw_text(0, 0, text, Style::NONE);
verify_text_render(&buffer, 0, 0, text, Style::NONE);
let cell0 = buffer.get(0, 0).unwrap();
assert!(!cell0.is_continuation());
assert_eq!(cell0.display_width(), 2);
let cell1 = buffer.get(1, 0).unwrap();
assert!(cell1.is_continuation());
let cell2 = buffer.get(2, 0).unwrap();
assert!(!cell2.is_continuation());
assert_eq!(cell2.display_width(), 2);
let cell3 = buffer.get(3, 0).unwrap();
assert!(cell3.is_continuation());
}
#[test]
fn test_mixed_ascii_cjk() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let text = "Hello\u{4E16}\u{754C}World"; buffer.draw_text(0, 0, text, Style::NONE);
let total_width: usize = graphemes(text).map(display_width).sum();
assert_eq!(total_width, 14);
for i in 0..5 {
let cell = buffer.get(i, 0).unwrap();
assert!(
!cell.is_continuation(),
"ASCII char at {} should not be continuation",
i
);
}
let cell5 = buffer.get(5, 0).unwrap();
assert!(!cell5.is_continuation());
let cell6 = buffer.get(6, 0).unwrap();
assert!(cell6.is_continuation());
let cell7 = buffer.get(7, 0).unwrap();
assert!(!cell7.is_continuation());
let cell8 = buffer.get(8, 0).unwrap();
assert!(cell8.is_continuation());
for i in 9..14 {
let cell = buffer.get(i, 0).unwrap();
assert!(
!cell.is_continuation(),
"ASCII char at {} should not be continuation",
i
);
}
}
#[test]
fn test_cjk_at_line_boundaries() {
let mut buffer = OptimizedBuffer::new(10, 24);
buffer.clear(Rgba::BLACK);
let text = "\u{4E2D}"; buffer.draw_text(9, 0, text, Style::NONE);
}
#[test]
fn test_cjk_in_scissor_region() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
buffer.push_scissor(ClipRect::new(5, 0, 10, 10));
let text = "\u{4E2D}\u{6587}\u{5B57}\u{7B26}\u{6D4B}\u{8BD5}"; buffer.draw_text(5, 0, text, Style::NONE);
buffer.pop_scissor();
}
#[test]
fn test_basic_emoji_single_codepoint() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let emojis = ["\u{1F600}", "\u{1F601}", "\u{1F602}"];
for (i, emoji) in emojis.iter().enumerate() {
let x = u32::try_from(i * 2).expect("emoji offset fits u32");
buffer.draw_text(x, 0, emoji, Style::NONE);
let cell = buffer.get(x, 0).unwrap();
assert!(
!cell.is_continuation(),
"emoji at {} should not be continuation",
x
);
}
}
#[test]
fn test_emoji_with_skin_tone() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let emoji = "\u{1F44B}\u{1F3FD}"; let width = display_width(emoji);
buffer.draw_text_with_pool(&mut pool, 0, 0, emoji, Style::NONE);
let cell = buffer.get(0, 0).unwrap();
assert!(!cell.is_continuation());
assert_eq!(cell.display_width(), width);
}
#[test]
fn test_emoji_zwj_sequence() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}";
buffer.draw_text_with_pool(&mut pool, 0, 0, family, Style::NONE);
let grapheme_count: usize = graphemes(family).count();
assert_eq!(
grapheme_count, 1,
"ZWJ family should be single grapheme cluster"
);
let cell = buffer.get(0, 0).unwrap();
assert!(!cell.is_continuation());
}
#[test]
fn test_flag_emoji_regional_indicators() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let us_flag = "\u{1F1FA}\u{1F1F8}"; let jp_flag = "\u{1F1EF}\u{1F1F5}"; let gb_flag = "\u{1F1EC}\u{1F1E7}";
assert_eq!(graphemes(us_flag).count(), 1);
assert_eq!(graphemes(jp_flag).count(), 1);
assert_eq!(graphemes(gb_flag).count(), 1);
buffer.draw_text_with_pool(&mut pool, 0, 0, us_flag, Style::NONE);
buffer.draw_text_with_pool(&mut pool, 4, 0, jp_flag, Style::NONE);
buffer.draw_text_with_pool(&mut pool, 8, 0, gb_flag, Style::NONE);
let cell0 = buffer.get(0, 0).unwrap();
assert!(!cell0.is_continuation());
}
#[test]
fn test_emoji_in_text_flow() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let text = "Hello \u{1F600} World \u{1F44D}!"; buffer.draw_text(0, 0, text, Style::NONE);
let expected_width: usize = graphemes(text).map(display_width).sum();
assert!(expected_width > 0);
assert!(expected_width <= 80);
}
#[test]
fn test_combining_diacriticals_precomposed_vs_decomposed() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let precomposed = "\u{00E9}";
let decomposed = "e\u{0301}";
buffer.draw_text(0, 0, precomposed, Style::NONE);
buffer.draw_text(5, 0, decomposed, Style::NONE);
assert_eq!(graphemes(precomposed).count(), 1);
assert_eq!(graphemes(decomposed).count(), 1);
assert_eq!(display_width(precomposed), 1);
assert_eq!(display_width(decomposed), 1);
}
#[test]
fn test_multiple_combining_marks() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let text = "a\u{0301}\u{0302}\u{0303}";
buffer.draw_text_with_pool(&mut pool, 0, 0, text, Style::NONE);
assert_eq!(graphemes(text).count(), 1);
assert_eq!(display_width(text), 1);
let cell = buffer.get(0, 0).unwrap();
assert!(!cell.is_continuation());
assert_eq!(cell.display_width(), 1);
}
#[test]
fn test_combining_marks_at_boundaries() {
let mut buffer = OptimizedBuffer::new(10, 24);
buffer.clear(Rgba::BLACK);
let text = "test\u{0301}";
buffer.draw_text(5, 0, text, Style::NONE);
}
#[test]
fn test_zero_width_joiner() {
assert_eq!(display_width_char('\u{200D}'), 0);
let without_zwj = "\u{1F468}\u{1F469}"; let with_zwj = "\u{1F468}\u{200D}\u{1F469}";
assert_eq!(graphemes(without_zwj).count(), 2);
assert_eq!(graphemes(with_zwj).count(), 1);
}
#[test]
fn test_zero_width_non_joiner() {
assert_eq!(display_width_char('\u{200C}'), 0);
let text = "a\u{200C}b";
let total_width = display_width(text);
assert_eq!(
total_width, 2,
"a+ZWNJ+b should have display width 2 (ZWNJ is invisible)"
);
}
#[test]
fn test_variation_selectors() {
let text_presentation = "\u{2764}"; let emoji_presentation = "\u{2764}\u{FE0F}";
assert_eq!(graphemes(text_presentation).count(), 1);
assert_eq!(graphemes(emoji_presentation).count(), 1);
}
#[test]
fn test_tab_characters() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let text = "A\tB";
buffer.draw_text(0, 0, text, Style::NONE);
}
#[test]
fn test_newline_variants() {
let lf_width = display_width_char('\n');
let cr_width = display_width_char('\r');
assert!(lf_width <= 1, "LF should not be wide");
assert!(cr_width <= 1, "CR should not be wide");
let crlf = "\r\n";
let crlf_width = display_width(crlf);
assert!(crlf_width <= 2, "CRLF should not be wide");
}
#[test]
fn test_control_characters() {
for c in 0..32u8 {
let ch = c as char;
assert_eq!(
display_width_char(ch),
0,
"control char U+{:04X} should have width 0",
c
);
}
assert_eq!(display_width_char('\u{007F}'), 0);
}
#[test]
fn test_many_different_graphemes() {
let mut buffer = OptimizedBuffer::new(200, 50);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let mut grapheme_list = Vec::new();
for c in 'A'..='Z' {
grapheme_list.push(c.to_string());
}
for c in 'a'..='z' {
grapheme_list.push(c.to_string());
}
for c in '\u{4E00}'..'\u{4E50}' {
grapheme_list.push(c.to_string());
}
for c in '\u{1F600}'..'\u{1F650}' {
grapheme_list.push(c.to_string());
}
for c in '\u{0391}'..'\u{03C9}' {
grapheme_list.push(c.to_string());
}
for c in '\u{0410}'..'\u{044F}' {
grapheme_list.push(c.to_string());
}
for c in '\u{05D0}'..'\u{05EA}' {
grapheme_list.push(c.to_string());
}
for c in '\u{0621}'..'\u{064A}' {
grapheme_list.push(c.to_string());
}
let text = grapheme_list.join("");
let grapheme_count = graphemes(&text).count();
assert!(
grapheme_count >= 200,
"should have at least 200 graphemes, got {}",
grapheme_count
);
let mut x = 0u32;
let mut y = 0u32;
for grapheme in graphemes(&text) {
let w = u32::try_from(display_width(grapheme)).expect("grapheme width fits in u32");
if x + w > 200 {
x = 0;
y += 1;
if y >= 50 {
break;
}
}
buffer.draw_text_with_pool(&mut pool, x, y, grapheme, Style::NONE);
x += w.max(1); }
}
#[test]
fn test_very_long_grapheme_cluster() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
let mut long_grapheme = String::from("e");
for _ in 0..50 {
long_grapheme.push('\u{0301}'); }
assert_eq!(graphemes(&long_grapheme).count(), 1);
assert_eq!(display_width(&long_grapheme), 1);
buffer.draw_text_with_pool(&mut pool, 0, 0, &long_grapheme, Style::NONE);
let cell = buffer.get(0, 0).unwrap();
assert!(!cell.is_continuation());
}
#[test]
fn test_alternating_width_characters() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let text = "A\u{4E2D}B\u{6587}C\u{5B57}D\u{7B26}E";
buffer.draw_text(0, 0, text, Style::NONE);
let total_width: usize = graphemes(text).map(display_width).sum();
assert_eq!(total_width, 13);
let positions = [
(0, false), (1, false), (2, true), (3, false), (4, false), (5, true), (6, false), (7, false), (8, true), (9, false), (10, false), (11, true), (12, false), ];
for (pos, should_be_continuation) in positions {
let cell = buffer.get(pos, 0).unwrap();
assert_eq!(
cell.is_continuation(),
should_be_continuation,
"position {} should {}be continuation",
pos,
if should_be_continuation { "" } else { "not " }
);
}
}
#[test]
fn test_full_buffer_with_unicode() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
buffer.clear(Rgba::BLACK);
for y in 0..24 {
let text = match y % 4 {
0 => "ASCII text with numbers 0123456789 and symbols !@#$%",
1 => "\u{4E2D}\u{6587}\u{65E5}\u{672C}\u{8A9E}\u{D55C}\u{AE00}",
2 => "\u{1F600}\u{1F601}\u{1F602}\u{1F603}\u{1F604}\u{1F605}",
_ => "e\u{0301} a\u{0300} n\u{0303} o\u{0302} u\u{0308}",
};
buffer.draw_text_with_pool(&mut pool, 0, y, text, Style::NONE);
}
let (width, height) = buffer.size();
assert_eq!(width, 80);
assert_eq!(height, 24);
}
#[test]
fn test_unicode_width_consistency() {
let definite_cases = [
("A", 1),
("\u{4E2D}", 2), ("\u{1F600}", 2), ("e\u{0301}", 1), ("\u{00E9}", 1), ("\u{200D}", 0), ];
for (grapheme, expected_width) in definite_cases {
let width = display_width(grapheme);
assert_eq!(
width, expected_width,
"grapheme {:?} should have width {}, got {}",
grapheme, expected_width, width
);
}
let control_cases = ["\t", "\n"];
for grapheme in control_cases {
let width = display_width(grapheme);
assert!(
width <= 1,
"control character {:?} should have width <= 1, got {}",
grapheme,
width
);
}
}
#[test]
fn test_rtl_characters() {
let mut buffer = OptimizedBuffer::new(80, 24);
buffer.clear(Rgba::BLACK);
let hebrew = "\u{05E9}\u{05DC}\u{05D5}\u{05DD}"; buffer.draw_text(0, 0, hebrew, Style::NONE);
let arabic = "\u{0645}\u{0631}\u{062D}\u{0628}\u{0627}"; buffer.draw_text(0, 1, arabic, Style::NONE);
let mixed = "Hello \u{05E9}\u{05DC}\u{05D5}\u{05DD} World";
buffer.draw_text(0, 2, mixed, Style::NONE);
}
#[test]
fn test_e2e_bidi_reorder_and_render() {
let mut harness = E2EHarness::new("unicode", "bidi_reorder", 20, 2);
harness
.log()
.info("init", "Starting BiDi reorder + render test");
let logical = "abc ืืื";
let visual = reorder_for_display(logical);
assert_eq!(visual, "abc ืืื", "BiDi reorder should flip RTL run");
let mut buffer = OptimizedBuffer::new(20, 2);
buffer.clear(Rgba::BLACK);
buffer.draw_text(0, 0, &visual, Style::NONE);
for (idx, ch) in visual.chars().enumerate() {
let idx_u32 = u32::try_from(idx).expect("index fits u32");
let cell = buffer
.get(idx_u32, 0)
.unwrap_or_else(|| unreachable!("No cell at ({}, 0)", idx));
assert_eq!(
cell.content.as_char(),
Some(ch),
"Visual order char mismatch at index {idx}"
);
}
let mirrored = reorder_for_display("\u{05D0}(ื)ื.");
assert_eq!(
mirrored, ".ื(ื)ื",
"BiDi mirroring should flip parentheses in RTL run"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E BiDi reorder + render works");
}
#[test]
fn test_e2e_bidi_editor_selection_render() {
let mut harness = E2EHarness::new("unicode", "bidi_editor_selection", 40, 3);
harness
.log()
.info("init", "Starting BiDi editor selection render test");
let text = "Hello ืฉืืื World";
let rtl = "ืฉืืื";
let byte_start = text.find(rtl).expect("RTL substring should exist");
let rtl_start = text[..byte_start].chars().count();
let rtl_end = rtl_start + rtl.chars().count();
let edit_buffer = EditBuffer::with_text(text);
let mut view = EditorView::new(edit_buffer);
let selection_bg = Rgba::from_rgb_u8(60, 60, 120);
view.set_selection_style(Style::builder().bg(selection_bg).build());
view.set_selection(rtl_start, rtl_end);
let mut buffer = OptimizedBuffer::new(40, 3);
buffer.clear(Rgba::BLACK);
view.render_to(&mut buffer, 0, 0, 40, 3);
for (i, ch) in rtl.chars().enumerate() {
let col = u32::try_from(rtl_start + i).expect("selection index fits u32");
let cell = buffer
.get(col, 0)
.unwrap_or_else(|| unreachable!("No cell at ({col}, 0)"));
let color_close = |a: Rgba, b: Rgba| {
const EPS: f32 = 0.01;
(a.r - b.r).abs() < EPS
&& (a.g - b.g).abs() < EPS
&& (a.b - b.b).abs() < EPS
&& (a.a - b.a).abs() < EPS
};
let is_expected_char = cell.content.as_char().is_some_and(|c| c.eq(&ch));
assert!(
is_expected_char && color_close(cell.bg, selection_bg),
"Selected RTL cell should have selection background"
);
}
harness.finish(true);
eprintln!("[TEST] PASS: E2E BiDi editor selection render works");
}
#[test]
fn test_e2e_grapheme_pool_stress_and_compaction() {
let mut harness = E2EHarness::new("unicode", "grapheme_pool_stress", 40, 4);
harness
.log()
.info("init", "Starting grapheme pool stress + compaction test");
let mut pool = GraphemePool::new();
let mut buffer = OptimizedBuffer::new(40, 4);
let emoji = "๐จโ๐ฉโ๐งโ๐ฆ Family ๐ Party ๐ Rocket";
for _ in 0..200 {
buffer.draw_text_with_pool(&mut pool, 0, 0, emoji, Style::NONE);
buffer.clear_with_pool(&mut pool, Rgba::BLACK);
}
let stats = pool.stats();
harness
.log()
.info("stats", format!("Pool stats after clears: {stats:?}"));
assert!(
stats.active_slots <= 2,
"Pool should not leak active slots after clear: {stats:?}"
);
let mut ids = Vec::with_capacity(1200);
for i in 0..1200 {
let id = pool.alloc(&format!("g{i}"));
ids.push(id);
}
for (i, id) in ids.iter().enumerate() {
if !matches!(i % 3, 0) {
pool.decref(*id);
}
}
assert!(pool.should_compact(), "Pool should recommend compaction");
let result = pool.compact();
harness
.log()
.info("compact", format!("Compaction result: {result:?}"));
assert!(
result.slots_freed > 0,
"Compaction should free slots: {result:?}"
);
harness.finish(true);
eprintln!("[TEST] PASS: E2E grapheme pool stress + compaction works");
}
#[test]
fn test_private_use_area() {
let pua_char = '\u{E000}';
let width = display_width_char(pua_char);
assert!(width <= 2, "PUA character width should be reasonable");
}
#[test]
fn test_surrogates_and_noncharacters() {
let nonchar = '\u{FFFE}';
let width = display_width_char(nonchar);
assert!(width <= 2);
let replacement = '\u{FFFD}';
let width = display_width_char(replacement);
assert!(width <= 2);
}