sequoia-text 0.1.1

Sequoia GUI framework — text layout and shaping
Documentation
#![allow(clippy::unwrap_used, clippy::expect_used)]

use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
use std::rc::Rc;

use sequoia_buffer::{SpanAttr, StyledBuffer, hash_spans_rebased};
use sequoia_core::Color;
use sequoia_text::{FontStyle, FontWeight, TextSystem, WrapMode};

const EMOJI: &str = "👨‍👩‍👧";
const FLAG: &str = "🇵🇱";

fn doc_and_buf() -> (String, StyledBuffer) {
    let l0 = format!("hi {EMOJI} yo");
    let l1 = format!("flaga {FLAG} end");
    let l2 = "café déjà vu".to_owned();
    let doc = format!("{l0}\n{l1}\n{l2}");
    let mut buf = StyledBuffer::from(doc.clone());

    let e0 = doc.find(EMOJI).expect("emoji in doc");
    buf.set_attr(
        e0..e0 + EMOJI.len(),
        SpanAttr {
            bold: true,
            mention: Some(7),
            ..SpanAttr::plain()
        },
    );
    let f0 = doc.find(FLAG).expect("flag in doc");
    buf.set_attr(
        f0..f0 + FLAG.len(),
        SpanAttr {
            underline: true,
            link: Some("https://pl.example".into()),
            ..SpanAttr::plain()
        },
    );
    let d0 = doc.find("déjà").expect("deja in doc");
    buf.set_attr(
        d0..d0 + "déjà".len(),
        SpanAttr {
            color: Some(Color::BLUE),
            size_delta: 4.0,
            ..SpanAttr::plain()
        },
    );
    (doc, buf)
}

fn fonts_available(sys: &mut TextSystem) -> bool {
    sys.shape("M", 16.0).glyph_count() > 0
}

fn extra_for(buf: &StyledBuffer, line: usize) -> u64 {
    let (s, e) = buf.lines().line_range(line);
    let spans = buf.spans_for_line(s..e);
    if spans.iter().all(|x| x.is_empty()) {
        0
    } else {
        let mut h = DefaultHasher::new();
        hash_spans_rebased(&spans, s, &mut h);
        h.finish()
    }
}

#[test]
fn joint_buffer_to_shaped_lines_unicode() {
    let (doc, buf) = doc_and_buf();
    assert_eq!(buf.lines().line_count(), 3);
    let mut sys = TextSystem::new();

    let lines_text: Vec<String> = (0..3)
        .map(|i| {
            let (s, e) = buf.lines().line_range(i);
            buf.substring(s..e)
        })
        .collect();
    assert_eq!(lines_text.join("\n"), doc);
    for (i, text) in lines_text.iter().enumerate().take(3) {
        let (s, e) = buf.lines().line_range(i);
        let spans = buf.spans_for_line(s..e);
        assert_eq!(spans.len(), 1, "line {i}: exactly one styled run");
        for sp in &spans {
            assert!(sp.start >= s && sp.end <= e, "clipped to line {i}");

            let rel = (sp.start - s, sp.end - s);
            let slice = text.get(rel.0..rel.1).expect("span on char boundary");
            assert!(!slice.is_empty());
        }
    }

    let (s0, e0) = buf.lines().line_range(0);
    let sp0 = buf.spans_for_line(s0..e0);
    let t0 = buf.substring(s0..e0);
    assert_eq!(&t0[sp0[0].start - s0..sp0[0].end - s0], EMOJI);
    assert_eq!(sp0[0].attr.mention, Some(7));
    let (s1, e1) = buf.lines().line_range(1);
    let sp1 = buf.spans_for_line(s1..e1);
    let t1 = buf.substring(s1..e1);
    assert_eq!(&t1[sp1[0].start - s1..sp1[0].end - s1], FLAG);
    assert_eq!(sp1[0].attr.link.as_deref(), Some("https://pl.example"));

    let shaped = sys.shape_lines_visible_buffer(
        &buf,
        0..3,
        14.0,
        FontWeight::NORMAL,
        0.0,
        WrapMode::Word,
        0.0,
        None,
        FontStyle::Normal,
    );
    assert_eq!(shaped.len(), 3);
    for (i, line) in shaped.iter().enumerate() {
        assert!(line.height() > 0.0, "line {i} has height");
        assert!(line.row_count() >= 1);
        assert!(line.width() >= 0.0);
    }

    assert!(shaped[2].height() >= shaped[0].height());

    sys.reset_line_reshapes();
    let via_full = sys.shape_lines_visible_full(
        0..3,
        |i| {
            let (s, e) = buf.lines().line_range(i);
            Some(buf.substring(s..e))
        },
        14.0,
        FontWeight::NORMAL,
        0.0,
        WrapMode::Word,
        0.0,
        None,
        FontStyle::Normal,
        &|i| extra_for(&buf, i),
    );
    assert_eq!(sys.line_reshapes(), 0, "same keys across both APIs");
    for i in 0..3 {
        assert!(
            Rc::ptr_eq(&shaped[i], &via_full[i]),
            "line {i} shared entry"
        );
    }

    for i in 0..3 {
        let (s, e) = buf.lines().line_range(i);
        let text = buf.substring(s..e);
        let spans = buf.spans_for_line(s..e);
        assert_eq!(
            TextSystem::line_hash(&text, &spans, s, 0.0),
            sequoia_buffer::line_hash(&text, &sequoia_buffer::rebase_spans(&spans, s), 0.0),
            "digest agreement on line {i} ({text:?})"
        );
    }
}

#[test]
fn joint_rich_from_styled_unicode() {
    use sequoia_core::{RenderItem, RenderList};

    let (_doc, buf) = doc_and_buf();
    let mut sys = TextSystem::new();

    for i in 0..3 {
        let (s, e) = buf.lines().line_range(i);
        let text = buf.substring(s..e);
        let spans = buf.spans_for_line(s..e);
        let rich = sys.shape_rich_from_styled(
            &text,
            s,
            &spans,
            16.0,
            0.0,
            WrapMode::Word,
            0.0,
            sequoia_text::RichAlign::Left,
        );

        let char_len = text.chars().count();
        let mut covered = vec![false; char_len];
        for line in rich.lines() {
            for c in line.chars.clone() {
                covered[c] = true;
            }
        }
        assert!(covered.iter().all(|&b| b), "line {i} fully covered");
        assert!(rich.height() > 0.0);

        match i {
            0 => {
                assert_eq!(rich.span_mention(1), Some(7));
            }
            1 => assert_eq!(rich.span_link(1), Some("https://pl.example")),
            _ => {}
        }

        let again = sys.shape_rich_from_styled(
            &text,
            s,
            &spans,
            16.0,
            0.0,
            WrapMode::Word,
            0.0,
            sequoia_text::RichAlign::Left,
        );
        assert!(Rc::ptr_eq(&rich, &again), "rich cache hit on line {i}");
        let mut list = RenderList::new();
        sys.draw_rich(&rich, (0.0, 0.0), Color::WHITE, &mut list);
        for item in list.items() {
            if let RenderItem::Text(t) = item {
                for g in &t.glyphs {
                    for v in g.uv {
                        assert!((0.0..=1.0).contains(&v), "uv in range");
                    }
                }
            }
        }
    }

    if fonts_available(&mut sys) {
        let (s, e) = buf.lines().line_range(0);
        let text = buf.substring(s..e);
        let spans = buf.spans_for_line(s..e);
        let rich = sys.shape_rich_from_styled(
            &text,
            s,
            &spans,
            16.0,
            0.0,
            WrapMode::Word,
            0.0,
            sequoia_text::RichAlign::Left,
        );
        let mid_y = rich.line(0).expect("line").top + rich.line_height() * 0.5;
        let x = rich.caret_pos(0).x + 1.0;
        let hit = rich.hit_point(x, mid_y);
        assert_eq!(hit.span, Some(0), "plain 'h' span");
        assert_eq!(rich.span_mention(hit.span.expect("span")), None);
    }
}

#[test]
fn joint_edit_unicode_doc_invalidates_one_line() {
    let (_doc, mut buf) = doc_and_buf();
    let mut sys = TextSystem::new();
    let n = buf.lines().line_count();
    let before = sys.shape_lines_visible_buffer(
        &buf,
        0..n,
        14.0,
        FontWeight::NORMAL,
        0.0,
        WrapMode::Word,
        0.0,
        None,
        FontStyle::Normal,
    );

    let (s, _e) = buf.lines().line_range(0);
    let _ = buf.replace(s..s + 2, "HO", None);
    sys.invalidate_for_edit(buf.lines(), s, s + 2);
    sys.reset_line_reshapes();
    let after = sys.shape_lines_visible_buffer(
        &buf,
        0..n,
        14.0,
        FontWeight::NORMAL,
        0.0,
        WrapMode::Word,
        0.0,
        None,
        FontStyle::Normal,
    );
    assert_eq!(sys.line_reshapes(), 1, "only the edited line reshapes");
    assert!(!Rc::ptr_eq(&before[0], &after[0]));
    assert!(Rc::ptr_eq(&before[1], &after[1]), "flag line survives");
    assert!(Rc::ptr_eq(&before[2], &after[2]), "accents line survives");
}