cranpose-app-shell 0.1.154

Application orchestration shell for Cranpose
Documentation
use cranpose_ui_layout::MeasureScope;

use super::*;

struct FontSizedTextMeasurer;

impl FontSizedTextMeasurer {
    fn width(text: &cranpose_ui::text::AnnotatedString, style: &TextStyle) -> f32 {
        text.text.chars().count() as f32 * style.resolve_font_size(14.0)
    }
}

impl cranpose_ui::TextMeasurer for FontSizedTextMeasurer {
    fn measure(
        &self,
        text: &cranpose_ui::text::AnnotatedString,
        style: &TextStyle,
    ) -> cranpose_ui::TextMetrics {
        let size = style.resolve_font_size(14.0);
        cranpose_ui::TextMetrics {
            width: Self::width(text, style),
            height: size,
            line_height: size,
            line_count: 1,
        }
    }

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

    fn get_cursor_x_for_offset(
        &self,
        text: &cranpose_ui::text::AnnotatedString,
        style: &TextStyle,
        _offset: usize,
    ) -> f32 {
        Self::width(text, style)
    }

    fn layout(
        &self,
        text: &cranpose_ui::text::AnnotatedString,
        style: &TextStyle,
    ) -> cranpose_ui::TextLayoutResult {
        let size = style.resolve_font_size(14.0);
        cranpose_ui::TextLayoutResult::monospaced(&text.text, size, size)
    }
}

const GRID_WIDTH_PER_UNIT: f32 = 10.0;

const SCALED_LABEL: &str = "Scaled label";
const NEIGHBOUR_LABEL: &str = "Neighbour";

#[composable]
#[allow(non_snake_case)]
fn LabelsBesideAGridWideBox(grid_box: Rc<Cell<Option<NodeId>>>) {
    Column(Modifier::empty(), ColumnSpec::default(), move || {
        Text(SCALED_LABEL, Modifier::empty(), TextStyle::default());
        grid_box.set(Some(cranpose_ui::SubcomposeLayout(
            Modifier::empty(),
            |scope, _constraints| {
                let width = scope.density() * scope.font_scale() * GRID_WIDTH_PER_UNIT;
                scope.layout(width, 1.0, [])
            },
        )));
        Text(NEIGHBOUR_LABEL, Modifier::empty(), TextStyle::default());
    });
}

struct Fixture {
    shell: AppShell<TestRenderer>,
    grid_box: NodeId,
    neighbour: NodeId,
}

impl Fixture {
    fn new() -> Self {
        let grid_box = Rc::new(Cell::new(None));
        let grid_box_in_content = Rc::clone(&grid_box);
        let mut shell = AppShell::new(
            TestRenderer::default(),
            location_key(file!(), line!(), column!()),
            move || LabelsBesideAGridWideBox(Rc::clone(&grid_box_in_content)),
        );
        shell.app_context().set_text_measurer(FontSizedTextMeasurer);
        shell.update();
        let neighbour = {
            let tree = shell.layout_tree().expect("layout tree available");
            find_layout_box_with_text(tree.root(), NEIGHBOUR_LABEL)
                .expect("neighbour label laid out")
                .node_id
        };
        Self {
            shell,
            grid_box: grid_box.get().expect("grid box composed"),
            neighbour,
        }
    }

    fn neighbour_does(&self, neighbour: Neighbour) {
        let node = self.neighbour;
        if let Neighbour::Repassing = neighbour {
            Rc::clone(self.shell.app_context()).enter(|| cranpose_ui::schedule_layout_repass(node));
        }
    }

    fn scaled_label_size(&mut self) -> (f32, f32) {
        self.shell.update();
        let tree = self.shell.layout_tree().expect("layout tree available");
        let found =
            find_layout_box_with_text(tree.root(), SCALED_LABEL).expect("scaled label laid out");
        (found.rect.width, found.rect.height)
    }

    fn grid_box_width(&mut self) -> f32 {
        fn find(node: &cranpose_ui::LayoutBox, id: NodeId) -> Option<f32> {
            if node.node_id == id {
                return Some(node.rect.width);
            }
            node.children.iter().find_map(|child| find(child, id))
        }
        self.shell.update();
        let tree = self.shell.layout_tree().expect("layout tree available");
        find(tree.root(), self.grid_box).expect("grid box laid out")
    }
}

#[derive(Clone, Copy)]
enum Neighbour {
    Idle,
    Repassing,
}

fn assert_font_scale_resizes_text(neighbour: Neighbour) {
    let _guard = test_guard();
    let mut fixture = Fixture::new();
    let (width, height) = fixture.scaled_label_size();

    fixture.shell.set_font_scale(1.5);
    fixture.neighbour_does(neighbour);

    assert_eq!(
        fixture.scaled_label_size(),
        (width * 1.5, height * 1.5),
        "text keeps the size it measured at the old font scale and draws past its box"
    );
    assert_eq!(
        fixture.grid_box_width(),
        1.5 * GRID_WIDTH_PER_UNIT,
        "a node composed before the font scale changed measures with the old one"
    );
}

fn assert_density_remeasures_the_grid_box(neighbour: Neighbour) {
    let _guard = test_guard();
    let mut fixture = Fixture::new();
    assert_eq!(fixture.grid_box_width(), GRID_WIDTH_PER_UNIT);

    fixture.shell.set_density(2.0);
    fixture.neighbour_does(neighbour);

    assert_eq!(
        fixture.grid_box_width(),
        2.0 * GRID_WIDTH_PER_UNIT,
        "a node measured at the old density keeps that measurement"
    );
}

#[test]
fn a_font_scale_change_remeasures_text_that_did_not_change() {
    assert_font_scale_resizes_text(Neighbour::Idle);
}

#[test]
fn a_font_scale_change_remeasures_text_while_a_scoped_repass_is_pending() {
    assert_font_scale_resizes_text(Neighbour::Repassing);
}

#[test]
fn a_density_change_remeasures_a_node_that_did_not_change() {
    assert_density_remeasures_the_grid_box(Neighbour::Idle);
}

#[test]
fn a_density_change_remeasures_layout_while_a_scoped_repass_is_pending() {
    assert_density_remeasures_the_grid_box(Neighbour::Repassing);
}

#[test]
fn a_global_layout_invalidation_survives_a_scoped_repass_in_the_same_frame() {
    let _guard = test_guard();
    let mut fixture = Fixture::new();
    let (width, _) = fixture.scaled_label_size();

    Rc::clone(fixture.shell.app_context()).enter(|| cranpose_ui::set_font_scale(2.0));
    fixture.neighbour_does(Neighbour::Repassing);

    assert_eq!(
        fixture.scaled_label_size().0,
        width * 2.0,
        "the scoped repass downgraded the global invalidation to a subtree pass"
    );
}