gpui-base 0.6.4

Behavior, interaction, and infrastructure foundations for GPUI applications.
Documentation
use std::sync::{
    Arc,
    atomic::{AtomicUsize, Ordering},
};

use gpui::{
    AppContext as _, Context, Entity, IntoElement, Modifiers, MouseButton, ParentElement as _,
    Pixels, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, rems,
};

use super::{InlineElement, MarkdownNode, SelectionFormat, TextView, TextViewState};

const BLOCKS: usize = 24;

struct VirtualInlineRoot {
    view: Entity<TextViewState>,
    prepared_size: Arc<AtomicUsize>,
    font_scale: f32,
    source_format: bool,
}

impl Render for VirtualInlineRoot {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        let prepared = self.prepared_size.clone();
        div().w(px(400.)).h(px(200.)).overflow_hidden().text_size(rems(self.font_scale))
            .child(crate::TextSelectionLayer)
            .child(TextView::new(&self.view).scrollable(true)
                .selection_format(if self.source_format { SelectionFormat::Source } else { SelectionFormat::Plain })

                .plugin(crate::text::markdown_ext::TestInlinePlugin::new("math").parse_with(|node, _| {
                    let markdown::mdast::Node::InlineMath(math) = node else { return None };
                    Some(MarkdownNode::new("math", ()).text(format!("{}²", math.value)))
                }).render_with(move |_, context, _, _| {
                    let extent = context.font_size() * (prepared.load(Ordering::Relaxed) as f32 / 16.);
                    let image = Arc::new(gpui::Image::from_bytes(gpui::ImageFormat::Svg,
                        b"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"40\"><path d=\"M0 0L40 40\" stroke=\"black\"/></svg>".to_vec()));
                    Some(InlineElement::new(gpui::img(image).w(extent).h(extent)).with_baseline(extent * 0.75))
                })))
    }
}

fn document() -> String {
    (0..BLOCKS)
        .map(|i| format!("{i}段 $x$ $y$ English"))
        .collect::<Vec<_>>()
        .join("\n\n")
}

fn settle(cx: &mut VisualTestContext) {
    cx.run_until_parked();
    cx.update(|window, cx| window.draw(cx).clear(cx));
    cx.run_until_parked();
}

fn total_height(root: &Entity<VirtualInlineRoot>, cx: &mut VisualTestContext) -> Pixels {
    root.read_with(cx, |root, cx| {
        let list = root.view.read(cx).list_state();
        assert_eq!(list.item_count(), BLOCKS);
        list.max_offset_for_scrollbar().y + list.viewport_bounds().size.height
    })
}

#[gpui::test]
fn inline_virtual_resource_growth_remeasures_all_blocks_and_preserves_copy(
    cx: &mut TestAppContext,
) {
    cx.update(crate::init);
    let prepared = Arc::new(AtomicUsize::new(16));
    let (root, cx) = cx.add_window_view(|_, cx| VirtualInlineRoot {
        view: cx.new(|cx| TextViewState::markdown(&document(), cx)),
        prepared_size: prepared.clone(),
        font_scale: 1.,
        source_format: false,
    });
    settle(cx);
    let before = total_height(&root, cx);
    let bounds = root.read_with(cx, |root, cx| {
        root.view.read(cx).selection_adapter.text_bounds()
    });
    let first = bounds.first().unwrap();
    let last = bounds.last().unwrap();
    let start = point(first.left() + px(0.1), first.top() + first.size.height / 2.);
    let end = point(last.right() - px(0.1), last.top() + last.size.height / 2.);
    cx.simulate_mouse_down(start, MouseButton::Left, Modifiers::default());
    settle(cx);
    cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::default());
    settle(cx);
    root.update(cx, |root, cx| {
        root.view.read(cx).list_state().scroll_to_end();
        cx.notify();
    });
    settle(cx);
    let bounds = root.read_with(cx, |root, cx| {
        root.view.read(cx).selection_adapter.text_bounds()
    });
    let last = bounds.last().unwrap();
    let end = point(last.right() - px(0.1), last.top() + last.size.height / 2.);
    cx.simulate_mouse_move(end, MouseButton::Left, Modifiers::default());
    settle(cx);
    cx.simulate_mouse_up(end, MouseButton::Left, Modifiers::default());
    settle(cx);
    let copy_before = root.read_with(cx, |root, cx| {
        root.view.read(cx).selected_text_in(Some(0..=BLOCKS - 1))
    });
    assert!(
        copy_before.contains("第12段 x² y² English"),
        "offscreen covered blocks must use their logical text"
    );
    prepared.store(96, Ordering::Relaxed);
    root.update(cx, |root, cx| {
        root.view
            .update(cx, |state, cx| state.invalidate_inline_layout(cx))
    });
    settle(cx);
    let after = total_height(&root, cx);
    assert!(after > before * 2., "before={before:?}, after={after:?}");
    assert_eq!(
        root.read_with(cx, |root, cx| root
            .view
            .read(cx)
            .selected_text_in(Some(0..=BLOCKS - 1))),
        copy_before
    );
    root.update(cx, |root, cx| {
        root.source_format = true;
        cx.notify();
    });
    settle(cx);
    let source = root.read_with(cx, |root, cx| {
        root.view.read(cx).selected_text_in(Some(0..=BLOCKS - 1))
    });
    assert!(source.contains("第12段 $x$ $y$ English"));
}

#[gpui::test]
fn inline_virtual_font_and_rem_zoom_remeasure_offscreen_blocks(cx: &mut TestAppContext) {
    cx.update(crate::init);
    let (root, cx) = cx.add_window_view(|_, cx| VirtualInlineRoot {
        view: cx.new(|cx| TextViewState::markdown(&document(), cx)),
        prepared_size: Arc::new(AtomicUsize::new(40)),
        font_scale: 1.,
        source_format: false,
    });
    settle(cx);
    let normal = total_height(&root, cx);
    let mut zoomed = normal;
    for scale in [1.5, 2.] {
        root.update(cx, |root, cx| {
            root.font_scale = scale;
            cx.notify();
        });
        settle(cx);
        zoomed = total_height(&root, cx);
        assert!(
            zoomed > normal * (scale * 0.85),
            "scale={scale}, normal={normal:?}, zoomed={zoomed:?}"
        );
    }
    cx.update(|window, cx| {
        window.set_rem_size(window.rem_size() * 1.5);
        window.refresh();
        cx.notify(root.entity_id());
    });
    settle(cx);
    let rem_zoomed = total_height(&root, cx);
    assert!(
        rem_zoomed > zoomed * 1.3,
        "zoomed={zoomed:?}, rem_zoomed={rem_zoomed:?}"
    );
}