cranpose-ui 0.1.164

UI primitives for Cranpose
Documentation
use super::*;

#[test]
fn tap_classification_escalates_within_time_and_slop() {
    assert_eq!(classify_tap_count(None, 0, 10.0, 10.0, 500, 24.0), 1);
    assert_eq!(
        classify_tap_count(Some((1, 10.0, 10.0)), 100, 11.0, 12.0, 500, 24.0),
        2
    );
    assert_eq!(
        classify_tap_count(Some((2, 10.0, 10.0)), 100, 11.0, 12.0, 500, 24.0),
        3
    );
    assert_eq!(
        classify_tap_count(Some((3, 10.0, 10.0)), 100, 11.0, 12.0, 500, 24.0),
        4
    );
    assert_eq!(
        classify_tap_count(Some((4, 10.0, 10.0)), 100, 11.0, 12.0, 500, 24.0),
        5
    );
}

#[test]
fn tap_classification_resets_past_timeout_or_slop() {
    assert_eq!(
        classify_tap_count(Some((1, 10.0, 10.0)), 600, 10.0, 10.0, 500, 24.0),
        1
    );
    assert_eq!(
        classify_tap_count(Some((1, 10.0, 10.0)), 50, 100.0, 10.0, 500, 24.0),
        1
    );
    assert_eq!(
        classify_tap_count(Some((3, 10.0, 10.0)), 600, 10.0, 10.0, 500, 24.0),
        1
    );
}

#[test]
fn tap_inside_selection_cycles_word_line_paragraph_by_location() {
    use SelectionGranularity::*;

    let mut count = resolve_selection_tap_count(1, 0, true, false);
    assert_eq!(count, 2);
    assert_eq!(tap_selection_granularity(count), Word);

    count = resolve_selection_tap_count(1, count, true, true);
    assert_eq!(count, 3);
    assert_eq!(tap_selection_granularity(count), Line);

    count = resolve_selection_tap_count(1, count, true, true);
    assert_eq!(count, 4);
    assert_eq!(tap_selection_granularity(count), Paragraph);

    count = resolve_selection_tap_count(1, count, true, true);
    assert_eq!(count, 5);
    assert_eq!(tap_selection_granularity(count), Word);

    let reset = resolve_selection_tap_count(1, count, true, false);
    assert_eq!(reset, 2);
    assert_eq!(tap_selection_granularity(reset), Word);
}

#[test]
fn resolve_tap_count_preserves_rapid_multitap_and_caret() {
    assert_eq!(resolve_selection_tap_count(2, 1, false, false), 2);
    assert_eq!(resolve_selection_tap_count(3, 2, true, true), 3);
    assert_eq!(resolve_selection_tap_count(1, 4, false, true), 1);
}

#[test]
fn tap_granularity_grows_then_cycles() {
    use SelectionGranularity::*;
    assert_eq!(tap_selection_granularity(0), Caret);
    assert_eq!(tap_selection_granularity(1), Caret);
    assert_eq!(tap_selection_granularity(2), Word);
    assert_eq!(tap_selection_granularity(3), Line);
    assert_eq!(tap_selection_granularity(4), Paragraph);
    assert_eq!(tap_selection_granularity(5), Word);
    assert_eq!(tap_selection_granularity(6), Line);
    assert_eq!(tap_selection_granularity(7), Paragraph);
    assert_eq!(tap_selection_granularity(8), Word);
}

#[test]
fn paragraph_boundaries_span_blank_line_delimited_blocks() {
    let text = "line one\nline two\n\nsecond para\nstill second\n\n\nthird";
    let (s, e) = find_paragraph_boundaries(text, 3);
    assert_eq!(&text[s..e], "line one\nline two");
    let (s, e) = find_paragraph_boundaries(text, 20);
    assert_eq!(&text[s..e], "second para\nstill second");
    let (s, e) = find_paragraph_boundaries(text, text.len());
    assert_eq!(&text[s..e], "third");
}

#[test]
fn paragraph_boundaries_no_blank_line_is_whole_text() {
    let text = "just\none\nblock";
    assert_eq!(find_paragraph_boundaries(text, 5), (0, text.len()));
}

#[test]
fn paragraph_boundaries_are_unicode_aware() {
    let text = "\u{4e2d}\u{6587}\u{6bb5}\u{843d}\n\n\u{6b21}";
    let first = "\u{4e2d}\u{6587}\u{6bb5}\u{843d}";
    let (s, e) = find_paragraph_boundaries(text, 3);
    assert_eq!(&text[s..e], first);
    assert!(text.is_char_boundary(s) && text.is_char_boundary(e));
}

#[test]
fn line_boundaries_span_between_newlines() {
    let text = "first line\nsecond line\nthird";
    assert_eq!(find_line_boundaries(text, 15), (11, 22));
    assert_eq!(find_line_boundaries(text, 0), (0, 10));
    assert_eq!(find_line_boundaries(text, 25), (23, text.len()));
}

#[test]
fn line_boundaries_handle_unicode_and_empty_lines() {
    let text = "\u{00e9}\u{00e8}\n\n\u{4e2d}\u{6587}";
    let (start, end) = find_line_boundaries(text, "\u{00e9}\u{00e8}\n".len());
    assert_eq!(start, end);
    let last = find_line_boundaries(text, text.len());
    assert_eq!(&text[last.0..last.1], "\u{4e2d}\u{6587}");
}

#[test]
fn handle_path_is_valid_and_spans_the_line_box() {
    let (x, top, bottom) = (40.0_f32, 20.0_f32, 40.0_f32);
    for kind in [
        HandleKind::Cursor,
        HandleKind::SelectionStart,
        HandleKind::SelectionEnd,
    ] {
        let data = handle_path_data(kind, x, top, bottom, HANDLE_RADIUS);
        let path =
            cranpose_ui_graphics::VectorPath::parse(&data).expect("handle path must be valid SVG");
        assert!(!path.is_empty(), "{kind:?} handle must have geometry");
        let bounds = path.bounds();
        assert!(bounds.y <= top + 0.5, "{kind:?} must reach the line top");
        assert!(
            bounds.y + bounds.height >= bottom - 0.5,
            "{kind:?} must reach the line bottom"
        );
        assert!((bounds.x - (x - HANDLE_RADIUS)).abs() <= 0.5);
        assert!((bounds.x + bounds.width - (x + HANDLE_RADIUS)).abs() <= 0.5);
    }
}

#[test]
fn selection_handle_dots_sit_on_the_correct_side_of_the_line() {
    let (x, top, bottom, r) = (40.0_f32, 20.0_f32, 40.0_f32, HANDLE_RADIUS);
    let eps = 0.5_f32;

    let bounds = |kind: HandleKind| {
        let data = handle_path_data(kind, x, top, bottom, r);
        cranpose_ui_graphics::VectorPath::parse(&data)
            .expect("valid handle path")
            .bounds()
    };

    let start = bounds(HandleKind::SelectionStart);
    assert!(
        (start.y - (top - 2.0 * r + HANDLE_DOT_LINE_OVERLAP)).abs() <= eps,
        "start dot must ride on top of the line (top at {}, expected {})",
        start.y,
        top - 2.0 * r + HANDLE_DOT_LINE_OVERLAP
    );
    assert!(
        start.y + start.height <= bottom + eps,
        "start handle must not extend below the line box"
    );

    for kind in [HandleKind::SelectionEnd, HandleKind::Cursor] {
        let b = bounds(kind);
        assert!(
            (b.y + b.height - (bottom + 2.0 * r - HANDLE_DOT_LINE_OVERLAP)).abs() <= eps,
            "{kind:?} dot must hang below the line (bottom at {}, expected {})",
            b.y + b.height,
            bottom + 2.0 * r - HANDLE_DOT_LINE_OVERLAP
        );
        assert!(
            b.y >= top - eps,
            "{kind:?} handle must not extend above the line box"
        );
    }
}

#[test]
fn caret_visual_line_resolves_wrapped_visual_lines() {
    let ranges = vec![0..5usize, 5..9, 10..12];

    assert_eq!(
        caret_visual_line(&ranges, 0, LineAffinity::Downstream),
        (0, 0)
    );
    assert_eq!(
        caret_visual_line(&ranges, 3, LineAffinity::Downstream),
        (0, 0)
    );
    assert_eq!(
        caret_visual_line(&ranges, 5, LineAffinity::Downstream),
        (1, 5)
    );
    assert_eq!(
        caret_visual_line(&ranges, 7, LineAffinity::Downstream),
        (1, 5)
    );
    assert_eq!(
        caret_visual_line(&ranges, 9, LineAffinity::Downstream),
        (1, 5)
    );
    assert_eq!(
        caret_visual_line(&ranges, 11, LineAffinity::Downstream),
        (2, 10)
    );
    assert_eq!(
        caret_visual_line(&ranges, 12, LineAffinity::Downstream),
        (2, 10)
    );
}

#[test]
fn start_handle_grab_never_drifts() {
    let mut grab = HandleGrabOffset::begin_for(108.0, 100.0, false);
    assert_eq!(grab.track(108.0), 8.0);
    assert_eq!(grab.track(160.0), 8.0, "no drift on long downward travel");
    assert_eq!(grab.drift_progress(), 0.0);
}

#[test]
fn grab_offset_has_follow_drift_and_strict_phases() {
    let mut grab = HandleGrabOffset::begin(108.0, 100.0);
    assert_eq!(grab.bias(), 8.0);

    let direct_bias = grab.track(108.0);
    assert_eq!(direct_bias, 8.0, "initial travel follows exactly");
    assert_eq!(108.0 + direct_bias, 116.0);

    let drifting_bias = grab.track(132.0);
    assert!(drifting_bias < 8.0 && drifting_bias > grab_bias_full_view());
    assert!((0.0..1.0).contains(&grab.drift_progress()));

    assert_eq!(grab.track(156.0), grab_bias_full_view());
    assert_eq!(grab.drift_progress(), 1.0);
    assert_eq!(grab.track(220.0), grab_bias_full_view());
}

#[test]
fn grab_offset_is_cadence_independent_and_never_unwinds() {
    let mut single = HandleGrabOffset::begin(108.0, 100.0);
    single.track(140.0);

    let mut sampled = HandleGrabOffset::begin(108.0, 100.0);
    for y in [104.0, 109.0, 116.0, 130.0, 140.0] {
        sampled.track(y);
    }
    assert_eq!(sampled.bias(), single.bias());
    assert_eq!(sampled.drift_progress(), single.drift_progress());

    let migrated = sampled.bias();
    sampled.track(90.0);
    assert_eq!(
        sampled.bias(),
        migrated,
        "upward travel cannot unwind drift"
    );

    let deep = grab_bias_full_view() - 10.0;
    let mut already_visible = HandleGrabOffset::begin(deep, 0.0);
    already_visible.track(100.0);
    assert_eq!(already_visible.bias(), deep);
}

#[test]
fn caret_visual_line_handles_empty_ranges() {
    assert_eq!(caret_visual_line(&[], 5, LineAffinity::Upstream), (0, 0));
    assert_eq!(caret_visual_line(&[], 5, LineAffinity::Downstream), (0, 0));
}

#[test]
fn caret_visual_line_upstream_anchors_shared_wrap_boundary_to_upper_line() {
    let ranges = vec![0..5usize, 5..9, 10..12];

    assert_eq!(
        caret_visual_line(&ranges, 5, LineAffinity::Upstream),
        (0, 0)
    );
    assert_eq!(
        caret_visual_line(&ranges, 5, LineAffinity::Downstream),
        (1, 5)
    );

    assert_eq!(
        caret_visual_line(&ranges, 3, LineAffinity::Upstream),
        (0, 0)
    );
    assert_eq!(
        caret_visual_line(&ranges, 7, LineAffinity::Upstream),
        (1, 5)
    );

    assert_eq!(
        caret_visual_line(&ranges, 10, LineAffinity::Upstream),
        (2, 10)
    );

    assert_eq!(
        caret_visual_line(&ranges, 12, LineAffinity::Upstream),
        (2, 10)
    );
}

#[test]
fn handle_drag_keeps_edges_from_crossing() {
    assert_eq!(
        selection_after_handle_drag(HandleKind::SelectionEnd, 5, 2, 20),
        (5, 6)
    );
    assert_eq!(
        selection_after_handle_drag(HandleKind::SelectionEnd, 5, 12, 20),
        (5, 12)
    );
    assert_eq!(
        selection_after_handle_drag(HandleKind::SelectionStart, 8, 10, 20),
        (7, 8)
    );
    assert_eq!(
        selection_after_handle_drag(HandleKind::SelectionStart, 8, 3, 20),
        (3, 8)
    );
    assert_eq!(
        selection_after_handle_drag(HandleKind::Cursor, 4, 9, 20),
        (9, 9)
    );
}