use std::{cell::RefCell, rc::Rc};
use gpui::{
App, Context, Modifiers, MouseButton, Render, ScrollHandle, TestAppContext, VisualTestContext,
Window, div, point, prelude::*, px, size,
};
use markdown::{
BlockLayouts, Cursor, Doc, Editing, ImageOverlay, OnImage, OnImageMenu, Part, Selection,
SourceText, parse, render_source, render_with,
};
const WIDTH: f32 = 320.0;
const HEIGHT: f32 = 400.0;
struct Page {
doc: Doc,
source: Option<String>,
layouts: BlockLayouts,
scroll: ScrollHandle,
clicked: Rc<RefCell<Vec<usize>>>,
menued: Rc<RefCell<Vec<usize>>>,
hooked: bool,
overlay: Option<ImageOverlay>,
overlay_anchor: markdown::ImageOverlayCorner,
presses: usize,
virtualized: bool,
}
impl Render for Page {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let clicked = self.clicked.clone();
let image = self.hooked.then(|| {
Rc::new(move |ix: usize, _: &mut Window, _: &mut App| clicked.borrow_mut().push(ix))
as OnImage
});
let menued = self.menued.clone();
let image_menu = self.hooked.then(|| {
Rc::new(move |ix: usize, _, _: &mut Window, _: &mut App| menued.borrow_mut().push(ix))
as OnImageMenu
});
let editing = Editing {
layouts: self.virtualized.then_some(&self.layouts),
scroll: Some(&self.scroll),
image,
image_menu,
image_overlay: self.overlay.clone(),
image_overlay_corner: self.overlay_anchor,
..Editing::default()
};
let body = match &self.source {
Some(source) => render_source(&SourceText::new(source.as_str(), cx), editing, cx),
None => render_with(&self.doc, editing, window, cx),
};
div()
.id("page")
.w(px(WIDTH))
.h(px(HEIGHT))
.overflow_y_scroll()
.track_scroll(&self.scroll)
.on_any_mouse_down(cx.listener(|this, _, _, _| this.presses += 1))
.child(body)
}
}
fn open(
source: &str,
as_source: bool,
hooked: bool,
cx: &mut TestAppContext,
) -> (gpui::Entity<Page>, VisualTestContext) {
cx.update(|cx| theme::Theme::install(theme::Appearance::Dark, cx));
let window = cx.add_window(|_, _| Page {
doc: parse(source),
source: as_source.then(|| source.to_string()),
layouts: BlockLayouts::default(),
scroll: ScrollHandle::new(),
clicked: Rc::new(RefCell::new(Vec::new())),
menued: Rc::new(RefCell::new(Vec::new())),
hooked,
overlay: None,
overlay_anchor: markdown::ImageOverlayCorner::BottomRight,
presses: 0,
virtualized: true,
});
let page = window.root(cx).unwrap();
let mut visual = VisualTestContext::from_window(window.into(), cx);
visual.simulate_resize(size(px(WIDTH), px(HEIGHT)));
settle(&mut visual);
(page, visual)
}
fn settle(cx: &mut VisualTestContext) {
for _ in 0..8 {
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
fn paragraphs(count: usize) -> String {
(0..count)
.map(|ix| format!("paragraph {ix}"))
.collect::<Vec<_>>()
.join("\n\n")
}
fn shown(page: &gpui::Entity<Page>, range: Selection, cx: &mut VisualTestContext) -> bool {
let rows = cx.update(|_, cx| page.read(cx).layouts.rects(range));
rows.first()
.is_some_and(|row| row.top() >= px(0.0) && row.bottom() <= px(HEIGHT))
}
#[gpui::test]
fn a_reveal_scrolls_a_block_never_built_into_view(cx: &mut TestAppContext) {
let (page, mut cx) = open(¶graphs(300), false, false, cx);
let range = Selection::new(
Cursor::new(250, Part::Body, 0),
Cursor::new(250, Part::Body, 9),
);
assert!(!shown(&page, range, &mut cx), "the block starts off-screen");
cx.update(|_, cx| page.read(cx).layouts.reveal(range));
settle(&mut cx);
assert!(shown(&page, range, &mut cx), "the range is on screen");
}
#[gpui::test]
fn a_reveal_scrolls_back_up(cx: &mut TestAppContext) {
let (page, mut cx) = open(¶graphs(300), false, false, cx);
let (low, high) = (
Selection::new(
Cursor::new(280, Part::Body, 0),
Cursor::new(280, Part::Body, 9),
),
Selection::new(Cursor::new(3, Part::Body, 0), Cursor::new(3, Part::Body, 9)),
);
cx.update(|_, cx| page.read(cx).layouts.reveal(low));
settle(&mut cx);
cx.update(|_, cx| page.read(cx).layouts.reveal(high));
settle(&mut cx);
assert!(shown(&page, high, &mut cx), "the range is on screen");
}
#[gpui::test]
fn a_nearest_reveal_stops_at_the_bottom_edge(cx: &mut TestAppContext) {
let (page, mut cx) = open(¶graphs(300), false, false, cx);
let range = Selection::new(
Cursor::new(40, Part::Body, 0),
Cursor::new(40, Part::Body, 9),
);
cx.update(|_, cx| {
page.read(cx)
.layouts
.reveal_with(range, markdown::RevealMode::Nearest)
});
settle(&mut cx);
let rows = cx.update(|_, cx| page.read(cx).layouts.rects(range));
let bottom = rows.first().expect("the range painted").bottom();
assert!(
bottom <= px(HEIGHT - 16.0) && bottom > px(HEIGHT - 40.0),
"the range sits just inside the bottom, at {bottom:?}"
);
}
#[gpui::test]
fn a_nearest_reveal_leaves_a_shown_range_alone(cx: &mut TestAppContext) {
let (page, mut cx) = open(¶graphs(300), false, false, cx);
let range = Selection::new(Cursor::new(1, Part::Body, 0), Cursor::new(1, Part::Body, 9));
let rects = |cx: &mut VisualTestContext| cx.update(|_, cx| page.read(cx).layouts.rects(range));
let before = rects(&mut cx);
cx.update(|_, cx| {
page.read(cx)
.layouts
.reveal_with(range, markdown::RevealMode::Nearest)
});
settle(&mut cx);
assert_eq!(rects(&mut cx), before);
}
#[gpui::test]
fn a_reveal_in_source_finds_its_line(cx: &mut TestAppContext) {
let source = paragraphs(300);
let offset = source.find("paragraph 260").unwrap();
let (page, mut cx) = open(&source, true, false, cx);
let range = Selection::new(
Cursor::new(0, Part::Code, offset),
Cursor::new(0, Part::Code, offset + 13),
);
cx.update(|_, cx| page.read(cx).layouts.reveal(range));
settle(&mut cx);
assert!(shown(&page, range, &mut cx), "the line is on screen");
}
fn picture(page: &gpui::Entity<Page>, cx: &mut VisualTestContext) -> gpui::Bounds<gpui::Pixels> {
cx.update(|_, cx| page.read(cx).layouts.picture_bounds(1))
.expect("the picture painted")
}
fn clicked(page: &gpui::Entity<Page>, cx: &mut VisualTestContext) -> Vec<usize> {
cx.update(|_, cx| page.read(cx).clicked.borrow().clone())
}
const PIXEL: &[u8] = &[
137, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72, 68, 82, 0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0,
0, 0, 31, 21, 196, 137, 0, 0, 0, 13, 73, 68, 65, 84, 120, 218, 99, 100, 96, 248, 95, 15, 0, 2,
135, 1, 128, 235, 71, 186, 146, 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130,
];
fn picture_doc() -> String {
let path = std::env::temp_dir().join(format!("bezel-find-{}.png", std::process::id()));
std::fs::write(&path, PIXEL).unwrap();
format!("before\n\n\n\nafter", path.display())
}
#[gpui::test]
fn a_click_on_a_picture_calls_the_hook(cx: &mut TestAppContext) {
let (page, mut cx) = open(&picture_doc(), false, true, cx);
let center = picture(&page, &mut cx).center();
cx.simulate_click(center, Modifiers::default());
assert_eq!(clicked(&page, &mut cx), vec![1]);
}
#[gpui::test]
fn a_drag_over_a_picture_does_not(cx: &mut TestAppContext) {
let (page, mut cx) = open(&picture_doc(), false, true, cx);
let from = picture(&page, &mut cx).center();
let to = from + point(px(20.0), px(0.0));
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::default());
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::default());
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::default());
assert!(clicked(&page, &mut cx).is_empty());
}
#[gpui::test]
fn a_picture_without_a_hook_takes_nothing(cx: &mut TestAppContext) {
let (page, mut cx) = open(&picture_doc(), false, false, cx);
let center = picture(&page, &mut cx).center();
cx.simulate_click(center, Modifiers::default());
cx.simulate_mouse_down(center, MouseButton::Right, Modifiers::default());
assert!(clicked(&page, &mut cx).is_empty());
assert_eq!(cx.update(|_, cx| page.read(cx).presses), 2);
}
#[gpui::test]
fn a_right_press_on_a_picture_calls_the_menu_hook_and_stops(cx: &mut TestAppContext) {
let (page, mut cx) = open(&picture_doc(), false, true, cx);
let center = picture(&page, &mut cx).center();
cx.simulate_mouse_down(center, MouseButton::Right, Modifiers::default());
assert_eq!(
cx.update(|_, cx| page.read(cx).menued.borrow().clone()),
vec![1]
);
assert!(clicked(&page, &mut cx).is_empty());
assert_eq!(cx.update(|_, cx| page.read(cx).presses), 0);
}
#[gpui::test]
fn the_picture_overlay_is_inset_and_owns_its_presses(cx: &mut TestAppContext) {
for (virtualized, anchor) in [true, false].into_iter().flat_map(|virtualized| {
[
markdown::ImageOverlayCorner::TopLeft,
markdown::ImageOverlayCorner::TopRight,
markdown::ImageOverlayCorner::BottomLeft,
markdown::ImageOverlayCorner::BottomRight,
]
.map(|anchor| (virtualized, anchor))
}) {
let doc = picture_doc();
let (page, mut cx) = open(&doc, false, true, cx);
let original = picture(&page, &mut cx);
let painted = Rc::new(RefCell::new(None));
let hits = Rc::new(RefCell::new(0));
let bounds = painted.clone();
let clicks = hits.clone();
cx.update(|_, cx| {
page.update(cx, |page, cx| {
page.virtualized = virtualized;
page.overlay_anchor = anchor;
page.overlay = Some(Rc::new(move |ix, url, _, _| {
assert_eq!(ix, 1);
assert!(url.ends_with(".png"));
let bounds = bounds.clone();
let clicks = clicks.clone();
Some(
div()
.id("app-picture-control")
.size(px(24.0))
.on_click(move |_, _, _| *clicks.borrow_mut() += 1)
.child(
gpui::canvas(
|_, _, _| (),
move |rect, _, _, _| {
*bounds.borrow_mut() = Some(rect);
},
)
.size_full(),
)
.into_any_element(),
)
}));
cx.notify();
})
});
settle(&mut cx);
assert!(painted.borrow().is_none(), "hidden before hovering");
assert_eq!(picture(&page, &mut cx), original);
cx.simulate_mouse_move(original.center(), None, Modifiers::default());
settle(&mut cx);
let control = painted.borrow().expect("painted on picture hover");
let horizontal = match anchor {
markdown::ImageOverlayCorner::TopLeft | markdown::ImageOverlayCorner::BottomLeft => {
control.left() - original.left()
}
_ => original.right() - control.right(),
};
let vertical = match anchor {
markdown::ImageOverlayCorner::TopLeft | markdown::ImageOverlayCorner::TopRight => {
control.top() - original.top()
}
_ => original.bottom() - control.bottom(),
};
assert!((horizontal - px(6.0)).abs() < px(1.0));
assert!((vertical - px(6.0)).abs() < px(1.0));
cx.simulate_click(control.center(), Modifiers::default());
assert_eq!(*hits.borrow(), 1);
assert!(clicked(&page, &mut cx).is_empty());
assert_eq!(cx.update(|_, cx| page.read(cx).presses), 0);
*painted.borrow_mut() = None;
cx.simulate_mouse_move(point(px(310.0), px(390.0)), None, Modifiers::default());
settle(&mut cx);
assert!(painted.borrow().is_none(), "hidden after leaving picture");
cx.simulate_click(original.center(), Modifiers::default());
assert_eq!(clicked(&page, &mut cx), vec![1]);
}
}
#[gpui::test]
fn declining_an_overlay_leaves_picture_presses_alone(cx: &mut TestAppContext) {
let (page, mut cx) = open(&picture_doc(), false, true, cx);
let original = picture(&page, &mut cx);
cx.update(|_, cx| {
page.update(cx, |page, cx| {
page.overlay = Some(Rc::new(|_, _, _, _| None));
cx.notify();
})
});
settle(&mut cx);
assert_eq!(picture(&page, &mut cx), original);
cx.simulate_click(original.center(), Modifiers::default());
assert_eq!(clicked(&page, &mut cx), vec![1]);
assert_eq!(cx.update(|_, cx| page.read(cx).presses), 1);
}
fn unsized_doc(width: u32) -> String {
let path =
std::env::temp_dir().join(format!("bezel-unsized-{}-{width}.svg", std::process::id()));
let svg = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="20"/>"#);
std::fs::write(&path, svg).unwrap();
format!("before\n\n\n\nafter", path.display())
}
#[gpui::test]
fn an_unsized_picture_s_frame_hugs_it(cx: &mut TestAppContext) {
let (page, mut cx) = open(&unsized_doc(40), false, false, cx);
assert!(picture(&page, &mut cx).size.width < px(60.0));
}
#[gpui::test]
fn an_unsized_picture_wider_than_the_page_stays_on_it(cx: &mut TestAppContext) {
let (page, mut cx) = open(&unsized_doc(1000), false, false, cx);
assert!(picture(&page, &mut cx).size.width <= px(WIDTH));
}
#[gpui::test]
fn a_picture_narrowed_to_the_page_keeps_its_proportions(cx: &mut TestAppContext) {
let (page, mut cx) = open(&unsized_doc(1000), false, false, cx);
let frame = picture(&page, &mut cx).size;
assert!(frame.height < px(12.0), "{frame:?}");
}