469 lines
17 KiB
Rust
469 lines
17 KiB
Rust
use super::*;
|
|
|
|
mod clip;
|
|
mod commands;
|
|
mod fit;
|
|
mod input;
|
|
mod large;
|
|
mod theme;
|
|
use gpui::{App, Bounds, Entity, point, size};
|
|
|
|
#[test]
|
|
fn geom_converts_both_ways() {
|
|
let geom = Geom {
|
|
zoom: 2.0,
|
|
dpi: 2.0,
|
|
};
|
|
assert_eq!(geom.px_of(15), 2.0, "15 twips at 200% zoom is 2 logical px");
|
|
assert_eq!(geom.twips_of(2.0), 15, "2 logical px back to 15 twips");
|
|
assert_eq!(geom.tile_pixels(), 1024, "the texture is 1024 device px");
|
|
let base = Geom {
|
|
zoom: 1.0,
|
|
dpi: 2.0,
|
|
};
|
|
assert_eq!(base.px_of(TILE_TWIPS), 256.0, "one tile is 256 logical px");
|
|
assert_eq!(
|
|
base.twips_of(256.0),
|
|
TILE_TWIPS,
|
|
"256 logical px is one tile"
|
|
);
|
|
assert_eq!(base.tile_pixels(), 512, "the texture is 512 device px");
|
|
}
|
|
|
|
pub(super) fn live_view(
|
|
app: &mut App,
|
|
install: &std::path::Path,
|
|
) -> (Entity<LokView>, Arc<Mutex<crate::Worker>>) {
|
|
live_view_of(app, install, "note.fodt", crate::client::tests::FODT)
|
|
}
|
|
|
|
/// A view on `body` saved as `name`.
|
|
pub(super) fn live_view_of(
|
|
app: &mut App,
|
|
install: &std::path::Path,
|
|
name: &str,
|
|
body: &str,
|
|
) -> (Entity<LokView>, Arc<Mutex<crate::Worker>>) {
|
|
let dir = tempfile::tempdir().expect("a temp dir");
|
|
let mut worker =
|
|
crate::Worker::spawn(install, &dir.path().join("profile")).expect("the worker");
|
|
let source = dir.path().join(name);
|
|
std::fs::write(&source, body).expect("the fixture writes");
|
|
let doc = worker
|
|
.load(&format!("file://{}", source.display()))
|
|
.expect("the document loads");
|
|
std::mem::forget(dir);
|
|
let shared = Arc::new(Mutex::new(worker));
|
|
let laid = super::layout(&mut shared.lock().expect("the worker lock"), &doc)
|
|
.expect("the layout reads");
|
|
let view = LokView::open(Arc::clone(&shared), doc, laid, app);
|
|
(view, shared)
|
|
}
|
|
|
|
#[test]
|
|
fn edits_grow_the_view_size() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
let before = view.doc_size;
|
|
{
|
|
let mut worker = view.worker.lock().expect("the worker lock");
|
|
for _ in 0..80 {
|
|
worker.post_key(&view.doc, 0, 1280).expect("Return posts");
|
|
}
|
|
}
|
|
let measured = super::measure(&view.worker, &view.doc);
|
|
view.apply_batch(
|
|
crate::Events {
|
|
generation: 1,
|
|
all: true,
|
|
rects: Vec::new(),
|
|
..crate::Events::default()
|
|
},
|
|
Some(measured),
|
|
cx,
|
|
);
|
|
assert!(
|
|
view.doc_size.1 > before.1,
|
|
"the view size follows the document: {before:?} to {:?}",
|
|
view.doc_size
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn releasing_the_view_stops_the_watch() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
let (view, shared) = cx.update(|app| live_view(app, &install));
|
|
for _ in 0..5 {
|
|
cx.executor().tick();
|
|
}
|
|
let weak = Arc::downgrade(&shared);
|
|
drop(shared);
|
|
cx.update(|_| drop(view));
|
|
let mut released = false;
|
|
for _ in 0..20 {
|
|
cx.executor().tick();
|
|
released = weak.upgrade().is_none();
|
|
if released {
|
|
break;
|
|
}
|
|
}
|
|
assert!(released, "the idle watch releases the worker");
|
|
}
|
|
|
|
#[test]
|
|
fn tiles_paint_in_logical_pixels_at_2x() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
view.geom.dpi = 2.0;
|
|
let side = view.geom.tile_pixels() as usize;
|
|
assert_eq!(side, 512, "a 2x tile texture is 512 px");
|
|
for key in [(0, 0), (1, 0)] {
|
|
let tile = crate::Tile {
|
|
generation: 0,
|
|
pixels: vec![255u8; side * side * 4],
|
|
};
|
|
view.finish_tile(key, view.geom, Ok(tile), cx);
|
|
}
|
|
let bounds = Bounds {
|
|
origin: point(px(10.0), px(20.0)),
|
|
size: size(px(800.0), px(600.0)),
|
|
};
|
|
let ready = view.prepare(bounds).tiles;
|
|
let rects: Vec<_> = ready.iter().map(|(rect, ..)| *rect).collect();
|
|
assert!(rects.len() >= 2, "both cached tiles paint: {rects:?}");
|
|
assert_eq!(rects[0].size.width, px(256.0), "one tile is 256 logical px");
|
|
assert_eq!(rects[0].origin, point(px(10.0), px(20.0)));
|
|
assert_eq!(rects[1].origin.x, px(266.0), "the next tile sits 256 px on");
|
|
assert_eq!(view.doc_point(point(px(266.0), px(20.0))), (TILE_TWIPS, 0));
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_red_tile_stays_red_in_the_render_image() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
let side = view.geom.tile_pixels() as usize;
|
|
let red_bgra = [0u8, 0, 255, 255];
|
|
let tile = crate::Tile {
|
|
generation: 0,
|
|
pixels: red_bgra.repeat(side * side),
|
|
};
|
|
view.finish_tile((0, 0), view.geom, Ok(tile), cx);
|
|
let image = &view.tiles[&(0, 0)].image;
|
|
let bytes = image.as_bytes(0).expect("one frame");
|
|
assert_eq!(&bytes[..4], &red_bgra, "GPUI gets the BGRA bytes as is");
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn the_view_clamps_scroll_and_guards_generations() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
view.viewport = (800.0, 600.0);
|
|
view.scroll_by(0.0, -100_000.0, cx);
|
|
let max_y = (view.doc_size.1 - view.geom.twips_of(600.0)).max(0);
|
|
assert_eq!(view.scroll.1, max_y, "scroll clamps at the bottom");
|
|
view.scroll_by(0.0, 1_000_000.0, cx);
|
|
assert_eq!(view.scroll.1, 0, "scroll clamps at the top");
|
|
assert!(view.doc_size.0 > 0 && view.doc_size.1 > 0, "the size loads");
|
|
|
|
let geom = view.geom;
|
|
let side = geom.tile_pixels() as usize;
|
|
let painted = |generation: u64| {
|
|
Ok(crate::Tile {
|
|
generation,
|
|
pixels: vec![0u8; side * side * 4],
|
|
})
|
|
};
|
|
view.finish_tile((0, 0), geom, painted(0), cx);
|
|
assert_eq!(view.tile_count(), 1, "a current tile lands");
|
|
view.apply_batch(
|
|
crate::Events {
|
|
generation: 5,
|
|
all: true,
|
|
rects: Vec::new(),
|
|
..crate::Events::default()
|
|
},
|
|
None,
|
|
cx,
|
|
);
|
|
assert_eq!(view.tile_count(), 0, "an all batch outdates the grid");
|
|
assert_eq!(view.tiles.len(), 1, "the old image shows until its repaint");
|
|
view.finish_tile((0, 0), geom, painted(4), cx);
|
|
assert_eq!(view.tile_count(), 0, "a stale tile never lands");
|
|
view.finish_tile((0, 0), geom, painted(5), cx);
|
|
assert_eq!(view.tile_count(), 1, "a fresh tile lands again");
|
|
view.apply_batch(
|
|
crate::Events {
|
|
generation: 6,
|
|
all: false,
|
|
rects: vec![(100, 100, 50, 50)],
|
|
..crate::Events::default()
|
|
},
|
|
None,
|
|
cx,
|
|
);
|
|
assert_eq!(
|
|
view.tile_count(),
|
|
0,
|
|
"a rect batch outdates intersecting tiles"
|
|
);
|
|
view.finish_tile((0, 0), geom, painted(6), cx);
|
|
view.finish_tile((3, 3), geom, painted(6), cx);
|
|
view.apply_batch(
|
|
crate::Events {
|
|
generation: 7,
|
|
all: false,
|
|
rects: vec![(284, 1418, 3715, 551)],
|
|
..crate::Events::default()
|
|
},
|
|
None,
|
|
cx,
|
|
);
|
|
assert!(!view.tiles[&(3, 3)].stale, "a distant tile stays current");
|
|
assert!(
|
|
view.tiles[&(0, 0)].stale,
|
|
"the dirty tile waits for its repaint"
|
|
);
|
|
let bounds = Bounds {
|
|
origin: point(px(0.0), px(0.0)),
|
|
size: size(px(800.0), px(600.0)),
|
|
};
|
|
let painted = view.prepare(bounds);
|
|
assert!(!painted.tiles.is_empty(), "stale tiles still paint");
|
|
assert!(
|
|
view.fetching.contains(&(0, 0)),
|
|
"and their repaint is asked"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn scroll_stays_inside_after_shrink_zoom_and_resize() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
let bottom =
|
|
|view: &LokView| (view.doc_size.1 - view.geom.twips_of(view.viewport.1)).max(0);
|
|
view.viewport = (800.0, 600.0);
|
|
{
|
|
let mut worker = view.worker.lock().expect("the worker lock");
|
|
for _ in 0..80 {
|
|
worker.post_key(&view.doc, 0, 1280).expect("Return posts");
|
|
}
|
|
}
|
|
let batch = |generation| crate::Events {
|
|
generation,
|
|
all: true,
|
|
..crate::Events::default()
|
|
};
|
|
view.apply_batch(batch(1), None, cx);
|
|
view.scroll_by(0.0, -1_000_000.0, cx);
|
|
assert_eq!(view.scroll.1, bottom(view), "scrolled to the grown bottom");
|
|
{
|
|
let mut worker = view.worker.lock().expect("the worker lock");
|
|
worker
|
|
.run(&view.doc, "edit.select_all", &[])
|
|
.expect("select all");
|
|
worker.cut(&view.doc).expect("the cut runs");
|
|
}
|
|
view.apply_batch(batch(2), None, cx);
|
|
assert_eq!(
|
|
view.scroll.1,
|
|
bottom(view),
|
|
"a shrink pulls the scroll back"
|
|
);
|
|
|
|
view.set_zoom(2.0, cx);
|
|
view.scroll_by(0.0, -1_000_000.0, cx);
|
|
view.set_zoom(0.25, cx);
|
|
assert_eq!(
|
|
view.scroll.1,
|
|
bottom(view),
|
|
"a smaller zoom pulls the scroll back"
|
|
);
|
|
|
|
view.set_zoom(2.0, cx);
|
|
view.viewport = (800.0, 300.0);
|
|
view.scroll_by(0.0, -1_000_000.0, cx);
|
|
let bounds = Bounds {
|
|
origin: point(px(0.0), px(0.0)),
|
|
size: size(px(800.0), px(1200.0)),
|
|
};
|
|
view.prepare(bounds);
|
|
assert_eq!(
|
|
view.scroll.1,
|
|
bottom(view),
|
|
"a taller viewport pulls the scroll back"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_moved_cursor_scrolls_into_view_and_a_still_one_does_not() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
view.viewport = (800.0, 200.0);
|
|
let far = Some((1418, view.doc_size.1 - 600, 1, 276));
|
|
let batch = || crate::Events {
|
|
cursor: far,
|
|
..crate::Events::default()
|
|
};
|
|
view.apply_batch(batch(), None, cx);
|
|
assert!(view.scroll.1 > 0, "the view follows the cursor");
|
|
view.scroll_by(0.0, 1_000_000.0, cx);
|
|
view.apply_batch(batch(), None, cx);
|
|
assert_eq!(view.scroll.1, 0, "a still cursor leaves the scroll alone");
|
|
// Only a sheet grows past the kernel's size.
|
|
view.sheet = Some(crate::sheet::Geometry::default());
|
|
let far = view.doc_size.1 + 20_000;
|
|
let cell = crate::Cell {
|
|
cursor: Some((1418, far, 1280, 277)),
|
|
..crate::Cell::default()
|
|
};
|
|
view.apply_batch(
|
|
crate::Events {
|
|
cell,
|
|
..crate::Events::default()
|
|
},
|
|
None,
|
|
cx,
|
|
);
|
|
let height = view.geom.twips_of(view.viewport.1);
|
|
assert!(
|
|
view.scroll.1 <= far && far + 277 <= view.scroll.1 + height,
|
|
"a cell past the kernel's size still comes into view"
|
|
);
|
|
view.scroll = (0, 0);
|
|
let width = view.geom.twips_of(view.viewport.0);
|
|
view.show((width - 200, 0, 1280, 256), cx);
|
|
assert!(
|
|
view.scroll.0 > 0 && width - 200 + 1280 <= view.scroll.0 + width,
|
|
"a cell cut at the right edge comes into view"
|
|
);
|
|
view.scroll = (0, 0);
|
|
view.frozen = (0, 600);
|
|
view.show((1418, 300, 1280, 277), cx);
|
|
assert_eq!(view.scroll.1, 0, "a frozen row needs no scroll");
|
|
view.show((1418, 9000, 1280, 277), cx);
|
|
let scrolled = view.scroll.1;
|
|
assert!(scrolled > 0, "a far row scrolls the rest");
|
|
let row = view.place((0, 300)).y;
|
|
assert_eq!(f32::from(row), view.geom.px_of(300), "the frozen row stays");
|
|
let far = view.twips_at(view.place((0, 9000)));
|
|
assert!(
|
|
(far.1 - 9000).abs() <= 15,
|
|
"a scrolled row maps back: {far:?}"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn a_closing_view_hands_over_every_image() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
let side = view.geom.tile_pixels() as usize;
|
|
let tile = |generation| Ok(crate::Tile { generation, pixels: vec![0; side * side * 4] });
|
|
let (geom, generation) = (view.geom, view.generation);
|
|
view.finish_tile((0, 0), geom, tile(generation), cx);
|
|
view.finish_tile((0, 0), geom, tile(generation), cx);
|
|
assert_eq!(view.tiles.len(), 1);
|
|
let images = view.retire_all();
|
|
assert!(!images.is_empty(), "the shown and the replaced tile go");
|
|
assert!(view.tiles.is_empty() && view.retired.is_empty(), "the view keeps none");
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn zooming_keeps_the_pointed_spot_and_walks_the_steps() {
|
|
let Some(install) = crate::client::tests::install() else {
|
|
eprintln!("SKIP: no pinned LOK build");
|
|
return;
|
|
};
|
|
let cx = gpui::TestAppContext::single();
|
|
cx.update(|app| {
|
|
let (view, _) = live_view(app, &install);
|
|
view.update(app, |view, cx| {
|
|
view.viewport = (800.0, 600.0);
|
|
view.origin = gpui::point(gpui::px(0.0), gpui::px(0.0));
|
|
view.doc_size = (60_000, 80_000);
|
|
view.scroll = (3_000, 9_000);
|
|
view.fit = true;
|
|
let at = gpui::point(gpui::px(300.0), gpui::px(200.0));
|
|
let spot = |view: &LokView| {
|
|
(view.scroll.0 + view.geom.twips_of(300.0), view.scroll.1 + view.geom.twips_of(200.0))
|
|
};
|
|
let before = spot(view);
|
|
view.zoom_at(2.0, Some(at), cx);
|
|
let after = spot(view);
|
|
assert!((before.0 - after.0).abs() <= 1 && (before.1 - after.1).abs() <= 1, "{before:?} {after:?}");
|
|
assert!(!view.fitting(), "a chosen zoom ends fitting");
|
|
view.set_zoom(1.0, cx);
|
|
view.zoom_step(true, cx);
|
|
assert_eq!(view.zoom(), 1.25);
|
|
view.zoom_step(false, cx);
|
|
view.zoom_step(false, cx);
|
|
assert_eq!(view.zoom(), 0.8);
|
|
view.set_zoom(9.0, cx);
|
|
assert_eq!(view.zoom(), 4.0, "the most is 400 %");
|
|
view.zoom_step(true, cx);
|
|
assert_eq!(view.zoom(), 4.0);
|
|
view.fit_page(cx);
|
|
assert!(view.fitting());
|
|
});
|
|
});
|
|
}
|