cranpose-ui 0.9.5

UI primitives for Cranpose
Documentation
use std::{cell::Cell, rc::Rc};

use cranpose_ui::{
    AppContext, TextLayoutOptions, TextLayoutResult, TextMeasurer, TextMetrics, TextStyle,
    layout_text, measure_text, prepare_text_layout, set_text_measurer,
    text::{AnnotatedString, LinkAnnotation},
};

fn annotated_with_link(annotation: &str, link: LinkAnnotation) -> AnnotatedString {
    AnnotatedString::builder()
        .push_string_annotation("payload", annotation)
        .push_link(link)
        .append("tap")
        .pop()
        .pop()
        .to_annotated_string()
}

fn prepare(text: &AnnotatedString) -> cranpose_ui::PreparedTextLayout {
    prepare_text_layout(
        text,
        &TextStyle::default(),
        TextLayoutOptions::default(),
        None,
    )
}

struct AnnotationSensitiveMeasurer;

impl TextMeasurer for AnnotationSensitiveMeasurer {
    fn measure(&self, text: &AnnotatedString, _style: &TextStyle) -> TextMetrics {
        let width = text.string_annotations[0].item.annotation.len() as f32;
        TextMetrics {
            width,
            height: 1.0,
            line_height: 1.0,
            line_count: 1,
        }
    }

    fn get_offset_for_position(
        &self,
        _text: &AnnotatedString,
        _style: &TextStyle,
        _x: f32,
        _y: f32,
    ) -> usize {
        0
    }

    fn get_cursor_x_for_offset(
        &self,
        _text: &AnnotatedString,
        _style: &TextStyle,
        _offset: usize,
    ) -> f32 {
        0.0
    }

    fn layout(&self, text: &AnnotatedString, _style: &TextStyle) -> TextLayoutResult {
        let char_width = text.string_annotations[0].item.annotation.len() as f32;
        TextLayoutResult::monospaced(&text.text, char_width, 1.0)
    }
}

#[test]
fn shared_measurement_caches_do_not_hide_annotations_from_custom_measurers() {
    AppContext::new().enter(|| {
        set_text_measurer(AnnotationSensitiveMeasurer);
        let first = annotated_with_link(
            "a",
            LinkAnnotation::Url("https://example.invalid/first".into()),
        );
        let second = annotated_with_link(
            "longer payload",
            LinkAnnotation::Url("https://example.invalid/second".into()),
        );

        let first_metrics = measure_text(&first, &TextStyle::default());
        let second_metrics = measure_text(&second, &TextStyle::default());
        assert!(second_metrics.width > first_metrics.width);

        let first_layout = layout_text(&first, &TextStyle::default());
        let second_layout = layout_text(&second, &TextStyle::default());
        assert!(second_layout.width > first_layout.width);
    });
}

#[test]
fn prepared_cache_keeps_string_annotation_values_for_equal_text_and_style() {
    AppContext::new().enter(|| {
        let first = annotated_with_link(
            "first payload",
            LinkAnnotation::Url("https://example.invalid/first".into()),
        );
        let second = annotated_with_link(
            "second payload",
            LinkAnnotation::Url("https://example.invalid/second".into()),
        );

        let _ = prepare(&first);
        let prepared = prepare(&second);

        assert_eq!(
            prepared.text.string_annotations[0].item.annotation,
            "second payload"
        );
    });
}

#[test]
fn prepared_cache_keeps_url_identity_for_equal_text_and_style() {
    AppContext::new().enter(|| {
        let first = annotated_with_link(
            "payload",
            LinkAnnotation::Url("https://example.invalid/first".into()),
        );
        let second = annotated_with_link(
            "payload",
            LinkAnnotation::Url("https://example.invalid/second".into()),
        );

        let _ = prepare(&first);
        let prepared = prepare(&second);

        assert!(matches!(
            &prepared.text.link_annotations[0].item,
            LinkAnnotation::Url(url) if url == "https://example.invalid/second"
        ));
    });
}

#[test]
fn prepared_cache_keeps_click_handler_for_equal_link_tag() {
    AppContext::new().enter(|| {
        let first_calls = Rc::new(Cell::new(0));
        let first_calls_for_handler = Rc::clone(&first_calls);
        let first = annotated_with_link(
            "payload",
            LinkAnnotation::Clickable {
                tag: "same-tag".into(),
                handler: Rc::new(move || {
                    first_calls_for_handler.set(first_calls_for_handler.get() + 1);
                }),
            },
        );

        let second_calls = Rc::new(Cell::new(0));
        let second_calls_for_handler = Rc::clone(&second_calls);
        let second = annotated_with_link(
            "payload",
            LinkAnnotation::Clickable {
                tag: "same-tag".into(),
                handler: Rc::new(move || {
                    second_calls_for_handler.set(second_calls_for_handler.get() + 1);
                }),
            },
        );

        let _ = prepare(&first);
        let prepared = prepare(&second);
        if let Some(link) = prepared.text.link_annotations.first()
            && let LinkAnnotation::Clickable { handler, .. } = &link.item
        {
            handler();
        }

        assert_eq!(first_calls.get(), 0);
        assert_eq!(second_calls.get(), 1);
    });
}