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, Arc>) { 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, Arc>) { 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()); }); }); }