use std::{cell::RefCell, rc::Rc}; use gpui::{ AppContext as _, Bounds, Context, Entity, Focusable, IntoElement, Modifiers, ParentElement, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Styled, TestAppContext, TouchPhase, VisualTestContext, Window, div, point, px, size, }; use super::{DocPage, DocumentViewer, PageHit, ZenMode}; use crate::{ editor::FindWidget, forms::{Input, TextInput}, theme::{ActiveTheme, ControlSize, Theme}, }; type Noted = Rc)>>>; /// A hit on `page`, 400 points down. fn hit(page: usize) -> PageHit { PageHit { page, bounds: Bounds::new(point(40.0, 400.0), size(60.0, 12.0)), } } /// Three tall pages and the hits found; a press while pinning records the note asked. struct Reader { query: Entity, width: f32, hits: Vec, current: Option, noted: Noted, } impl Render for Reader { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { let pages: Vec = (0..3) .map(|ix| DocPage { source: format!("page-{ix}.png").into(), width: self.width, height: 1000.0, }) .collect(); let noted = self.noted.clone(); div().size_full().child( DocumentViewer::new("viewer", "lift.pdf", pages) .find( FindWidget::new("find", &self.query, self.current, self.hits.len()), self.hits.clone(), self.current, ) .notes(Vec::new(), move |page, at, _, _| { *noted.borrow_mut() = Some((page, at)) }), ) } } fn open(cx: &mut TestAppContext) -> (Entity, Noted, &mut VisualTestContext) { open_with(400.0, None, cx) } /// Pages `width` points wide, with a hit on the second, current from the first frame when `current` says. fn open_with( width: f32, current: Option, cx: &mut TestAppContext, ) -> (Entity, Noted, &mut VisualTestContext) { cx.update(Theme::init); let noted = Noted::default(); let seen = noted.clone(); let (reader, cx) = cx.add_window_view(move |window, cx| Reader { query: cx.new(|cx| TextInput::new(window, cx)), width, hits: vec![hit(1)], current, noted: seen, }); settle(cx); (reader, noted, cx) } fn settle(cx: &mut VisualTestContext) { cx.run_until_parked(); cx.update(|window, _| window.refresh()); cx.run_until_parked(); } /// A point on the pages, below the header and the find bar, at the view's middle. fn on_pages(cx: &mut VisualTestContext) -> Point { cx.update(|window, _| point(window.viewport_size().width / 2.0, px(300.0))) } fn wheel(down: f32, cx: &mut VisualTestContext) { let at = on_pages(cx); cx.simulate_event(ScrollWheelEvent { position: at, delta: ScrollDelta::Pixels(point(px(0.0), px(-down))), modifiers: Modifiers::none(), touch_phase: TouchPhase::Moved, }); settle(cx); } /// Turns pinning on with the header's last button, then presses the pages at their middle; returns the note asked. fn pin(cx: &mut VisualTestContext, noted: &Noted) -> (usize, Point) { let at = on_pages(cx); pin_at(at, cx, noted) } fn pin_at(at: Point, cx: &mut VisualTestContext, noted: &Noted) -> (usize, Point) { let toggle = cx.update(|window, cx| { let header = cx .theme() .control_height(ControlSize::Lg) .to_pixels(window.rem_size()); point( window.viewport_size().width - px(17.0), px(1.0) + header / 2.0, ) }); cx.simulate_click(toggle, Modifiers::none()); settle(cx); cx.simulate_click(at, Modifiers::none()); settle(cx); noted .borrow_mut() .take() .expect("a press while pinning asks for a note") } #[gpui::test] fn a_note_lands_where_the_press_is_on_a_scrolled_page(cx: &mut TestAppContext) { let (_, noted, cx) = open(cx); let (page, top) = pin(cx, ¬ed); wheel(150.0, cx); let at = on_pages(cx); cx.simulate_click(at, Modifiers::none()); settle(cx); let (scrolled_page, scrolled) = noted.borrow_mut().take().expect("pinning stays on"); assert_eq!((scrolled_page, page), (0, 0)); assert_eq!(scrolled.x, top.x); assert_eq!(scrolled.y, top.y + 150.0, "the press counts the scroll"); } /// Scrolls `down`, makes the hit current, then pins a note at the same window point. fn revealed(down: f32, cx: &mut TestAppContext) -> (usize, Point) { let (reader, noted, cx) = open(cx); if down > 0.0 { wheel(down, cx); } reader.update(cx, |reader, cx| { reader.current = Some(0); cx.notify(); }); settle(cx); pin(cx, ¬ed) } #[gpui::test] fn a_current_hit_comes_to_one_place_from_any_scroll(cx: &mut TestAppContext) { let from_top = revealed(0.0, cx); let from_below = revealed(300.0, cx); assert_eq!(from_top.0, 1, "the hit's page comes into view"); assert_eq!(from_top, from_below); } #[gpui::test] fn a_page_wider_than_the_view_starts_at_its_left_edge(cx: &mut TestAppContext) { let (_, noted, cx) = open_with(3000.0, None, cx); let (page, at) = pin_at(point(px(60.0), px(300.0)), cx, ¬ed); assert_eq!(page, 0); assert!( at.x < 100.0, "the page's left edge is in reach, not at {}", at.x ); } #[gpui::test] fn a_new_hit_at_the_same_index_moves_the_view(cx: &mut TestAppContext) { let (reader, noted, cx) = open(cx); reader.update(cx, |reader, cx| { reader.current = Some(0); cx.notify(); }); settle(cx); reader.update(cx, |reader, cx| { reader.hits = vec![hit(2)]; cx.notify(); }); settle(cx); assert_eq!(pin(cx, ¬ed).0, 2, "the view follows the new hit"); } #[gpui::test] fn a_hit_current_from_the_first_frame_comes_into_view(cx: &mut TestAppContext) { let (_, noted, first) = open_with(400.0, Some(0), cx); let at_once = pin(first, ¬ed); let later = revealed(0.0, cx); assert_eq!(at_once, later); } /// A field outside a zen view, and how often Escape left zen. struct Zen { outside: Entity, on: bool, left: usize, } impl Render for Zen { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { let me = cx.entity(); div().size_full().child(Input::new(&self.outside)).child( ZenMode::new("zen", self.on) .on_exit(move |_, cx| { me.update(cx, |zen, cx| { zen.on = false; zen.left += 1; cx.notify(); }) }) .child(div().child("A quiet page")), ) } } #[gpui::test] fn escape_leaves_zen_turned_on_from_outside_and_hands_focus_back(cx: &mut TestAppContext) { cx.update(|cx| { Theme::init(cx); crate::forms::bind_keys(cx); }); let (zen, cx) = cx.add_window_view(|window, cx| Zen { outside: cx.new(|cx| TextInput::new(window, cx)), on: false, left: 0, }); cx.update(|window, cx| { window.activate_window(); let outside = zen.read(cx).outside.focus_handle(cx); window.focus(&outside, cx); }); settle(cx); zen.update(cx, |zen, cx| { zen.on = true; cx.notify(); }); settle(cx); cx.simulate_keystrokes("escape"); settle(cx); assert_eq!(zen.read_with(cx, |zen, _| zen.left), 1); let back = cx.update(|window, cx| zen.read(cx).outside.focus_handle(cx).is_focused(window)); assert!(back, "focus returns to where it was"); } #[gpui::test] fn hits_past_the_pages_are_left_and_the_current_follows(cx: &mut TestAppContext) { let (reader, _, cx) = open_with(400.0, None, cx); reader.update(cx, |reader, cx| { reader.hits = vec![hit(7), hit(2), hit(9), hit(1)]; reader.current = Some(3); cx.notify(); }); settle(cx); reader.update(cx, |reader, cx| { reader.current = Some(12); cx.notify(); }); settle(cx); let (_, _, cx) = open_with(400.0, Some(usize::MAX), cx); settle(cx); } /// Versions whose selected one left with a shorter history. struct Versions(Vec, usize); impl Render for Versions { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div().size_full().child(super::VersionHistory::new( "history", self.0.clone(), self.1, )) } } #[gpui::test] fn a_selected_version_past_the_history_selects_none(cx: &mut TestAppContext) { cx.update(Theme::init); let version = super::Version { at: jiff::Timestamp::UNIX_EPOCH, author: "Ann".into(), markdown: "# Plan".into(), }; let (view, cx) = cx.add_window_view(|_, _| Versions(vec![version], 4)); settle(cx); view.update(cx, |view, cx| { *view = Versions(Vec::new(), usize::MAX); cx.notify(); }); settle(cx); } /// Markdown drawn `width` points wide, its pictures read in `base`. struct Page { source: &'static str, base: std::path::PathBuf, width: f32, } impl Render for Page { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() .w(px(self.width)) .child(super::MarkdownRenderer::new("page", self.source).base(self.base.as_path())) } } /// A 200×100 picture drawn in a `width` column. fn drawn_picture(width: f32, cx: &mut TestAppContext) -> gpui::Size { let folder = std::env::temp_dir().join(format!("ely-picture-{}-{width}", std::process::id())); std::fs::create_dir_all(folder.join("assets")).expect("a folder"); let mut png = Vec::new(); image::RgbaImage::new(200, 100) .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png) .expect("a png"); std::fs::write(folder.join("assets/wide shot.png"), png).expect("the picture"); cx.update(Theme::init); let (_, cx) = cx.add_window_view(|_, _| Page { source: "Before ![shot](assets/wide%20shot.png) ", base: folder.clone(), width, }); let mut drawn = None; // The decode runs on the background executor. for _ in 0..50 { settle(cx); drawn = cx.debug_bounds("markdown-picture"); if drawn.is_some() { break; } } std::fs::remove_dir_all(&folder).expect("cleaned"); drawn.expect("the picture drew").size } #[gpui::test] fn a_picture_beside_the_file_draws_at_its_ratio_within_the_column(cx: &mut TestAppContext) { let shrunk = drawn_picture(120.0, cx); assert_eq!(shrunk, size(px(120.0), px(60.0)), "shrunk at 2:1"); } #[gpui::test] fn a_picture_narrower_than_the_column_keeps_its_size(cx: &mut TestAppContext) { assert_eq!(drawn_picture(400.0, cx), size(px(200.0), px(100.0))); } #[gpui::test] fn a_formula_that_fails_shows_why_and_its_source(cx: &mut TestAppContext) { cx.update(Theme::init); let (_, cx) = cx.add_window_view(|_, _| Page { source: "$$x^2^3$$\n", base: std::env::temp_dir(), width: 300.0, }); settle(cx); assert!(cx.debug_bounds("latex-error").is_some(), "the failure drew"); }