- a page centers only while it fits the view; a wider one starts in reach - the reveal runs after the pages' prepaint and keys on the hit itself, so a new hit at the same index and a hit current from the first frame come into view - the chapter list scrolls - zen takes focus when turned on from outside, so Escape leaves it, and hands focus back - the gallery keeps a page's due date and tags
256 lines
7.7 KiB
Rust
256 lines
7.7 KiB
Rust
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<RefCell<Option<(usize, Point<f32>)>>>;
|
|
|
|
/// 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<TextInput>,
|
|
width: f32,
|
|
hits: Vec<PageHit>,
|
|
current: Option<usize>,
|
|
noted: Noted,
|
|
}
|
|
|
|
impl Render for Reader {
|
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
let pages: Vec<DocPage> = (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<Reader>, 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<usize>,
|
|
cx: &mut TestAppContext,
|
|
) -> (Entity<Reader>, 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<Pixels> {
|
|
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<f32>) {
|
|
let at = on_pages(cx);
|
|
pin_at(at, cx, noted)
|
|
}
|
|
|
|
fn pin_at(at: Point<Pixels>, cx: &mut VisualTestContext, noted: &Noted) -> (usize, Point<f32>) {
|
|
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<f32>) {
|
|
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<TextInput>,
|
|
on: bool,
|
|
left: usize,
|
|
}
|
|
|
|
impl Render for Zen {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> 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);
|
|
});
|
|
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");
|
|
}
|