//! Only the pages on screen, drawn in tiles (PDF-01, PERF-05). use std::{collections::HashSet, sync::Arc}; use ely_gpui_component::theme::ActiveTheme as _; use gpui::{ AnyElement, Context, InteractiveElement as _, IntoElement, MouseButton, ParentElement as _, Pixels, Point, RenderImage, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, point, px, }; use image::{Frame, ImageBuffer, Rgba}; use crate::layout::{GAP, Layout, Mode, Placed, layout, visible}; use crate::view::{PdfView, Reported}; /// A tile's side in device pixels. pub const TILE: i32 = 512; /// A page tile at one scale, turn and theme. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct TileKey { pub page: usize, /// Device pixels a point, times a thousand. pub scale: u32, pub rotate: i32, pub dark: bool, pub tx: i32, pub ty: i32, } /// A page point in its turned frame, still in points. pub fn to_view((x, y): (f32, f32), (w, h): (f32, f32), rotate: i32) -> (f32, f32) { match rotate.rem_euclid(360) { 90 => (h - y, x), 180 => (w - x, h - y), 270 => (y, w - x), _ => (x, y), } } /// The page point under a turned-frame point. pub fn from_view((x, y): (f32, f32), (w, h): (f32, f32), rotate: i32) -> (f32, f32) { match rotate.rem_euclid(360) { 90 => (y, h - x), 180 => (w - x, h - y), 270 => (w - y, x), _ => (x, y), } } /// A page rectangle on screen, relative to its placed page. pub fn rect_on(rect: [f32; 4], size: (f32, f32), rotate: i32, scale: f32) -> (f32, f32, f32, f32) { let a = to_view((rect[0], rect[1]), size, rotate); let b = to_view((rect[2], rect[3]), size, rotate); let (x0, y0) = (a.0.min(b.0), a.1.min(b.1)); let (x1, y1) = (a.0.max(b.0), a.1.max(b.1)); (x0 * scale, y0 * scale, (x1 - x0) * scale, (y1 - y0) * scale) } /// Tiles covering `[from, to)` view pixels of a `side`-pixel page. fn span(from: f32, to: f32, sf: f32, side: i32) -> std::ops::RangeInclusive { let first = (from.max(0.0) * sf).floor() as i32 / TILE; let last = ((to * sf).ceil() as i32 - 1).min(side - 1).max(0) / TILE; first..=last } /// Draws `image`, logging failure and first success (PERF-05). fn drawn_tile(image: Arc, page: usize, view: gpui::Entity) -> gpui::Canvas<()> { canvas( |_, _, _| {}, move |bounds, _, window, cx| match window.paint_image(bounds, bounds, Default::default(), image, 0, false) { Ok(()) if !view.read(cx).drawn => { view.update(cx, |view, _| view.drawn = true); log::info!("pdf: first tile drawn, page {page}"); } Ok(()) => {} Err(error) => log::error!("pdf: drawing a page failed: {error}"), }, ) } /// A tile as an image; GPUI takes BGRA. pub(crate) fn picture((w, h): (u32, u32), bgra: Vec) -> Option> { let image = ImageBuffer::, _>::from_raw(w, h, bgra)?; Some(Arc::new(RenderImage::new([Frame::new(image)]))) } impl PdfView { /// The layout in the room last painted. pub fn laid_out(&self) -> Layout { let room = (f32::from(self.room.size.width), f32::from(self.room.size.height)); layout(&self.sizes, self.rotate, (self.mode, self.zoom), room, self.current) } /// A window point as (page, point on it), if any. pub fn page_at(&self, at: Point) -> Option<(usize, (f32, f32))> { let laid = self.laid_out(); let offset = self.scroll.offset(); let x = f32::from(at.x - self.room.origin.x - offset.x); let y = f32::from(at.y - self.room.origin.y - offset.y); let place = laid.placed.iter().find(|p| x >= p.x && x < p.x + p.w && y >= p.y && y < p.y + p.h)?; let local = ((x - place.x) / laid.scale, (y - place.y) / laid.scale); Some((place.page, from_view(local, self.sizes[place.page], self.rotate))) } /// Freezes `page`'s place in the window for a gesture. pub fn grip(&mut self, page: usize) { let (laid, offset) = (self.laid_out(), self.scroll.offset()); self.grip = laid.placed.iter().find(|p| p.page == page).map(|place| { let origin = point(self.room.origin.x + offset.x + px(place.x), self.room.origin.y + offset.y + px(place.y)); (page, origin, laid.scale) }); } /// A window point on the gripped page, as pressed. pub fn held_at(&self, at: Point) -> Option<(usize, (f32, f32))> { let (page, origin, scale) = self.grip?; let local = (f32::from(at.x - origin.x) / scale, f32::from(at.y - origin.y) / scale); Some((page, from_view(local, self.sizes[page], self.rotate))) } /// Pages in view and their tiles; missing ones asked. pub fn page_view(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { let (sf, dark) = (window.scale_factor(), cx.theme().is_dark()); let (sunken, paper) = { let colors = &cx.theme().colors; (colors.sunken, if dark { colors.surface } else { colors.paper }) }; let laid = self.laid_out(); let (room_w, room_h) = (f32::from(self.room.size.width), f32::from(self.room.size.height)); if let Some((x, y)) = self.go_to.take() { self.scroll.set_offset(point(px(-x), px(-y))); } let (left, top) = (-f32::from(self.scroll.offset().x), -f32::from(self.scroll.offset().y)); let shown: Vec = visible(&laid, top, top + room_h) .filter(|p| p.x < left + room_w && p.x + p.w > left) .copied() .collect(); // The current page: first showing more than a sliver. if let Some(first) = shown.iter().find(|p| p.y + p.h > top + GAP * 3.0) { let paired = shown.iter().any(|p| p.page == self.current) && first.page / 2 == self.current / 2; let now = match self.mode { Mode::Single => self.current, // Either page of a pair in view may be current. Mode::Two if paired => self.current, _ => first.page, }; if now != self.current { self.current = now; cx.emit(Reported); } } let scale = (laid.scale * sf * 1000.0).round() as u32; let (mut wanted, mut pages, mut seen) = (Vec::new(), Vec::new(), HashSet::new()); for place in &shown { let (dw, dh) = ((place.w * sf).ceil() as i32, (place.h * sf).ceil() as i32); let (ys, xs) = ( span(top - place.y, top + room_h - place.y, sf, dh), span(left - place.x, left + room_w - place.x, sf, dw), ); let mut tiles = Vec::new(); for ty in ys { for tx in xs.clone() { let key = TileKey { page: place.page, scale, rotate: self.rotate, dark, tx, ty }; let rect = [tx * TILE, ty * TILE, TILE.min(dw - tx * TILE), TILE.min(dh - ty * TILE)]; seen.insert(key); match self.tiles.get(&key) { Some(image) => tiles.push( drawn_tile(image.clone(), place.page, cx.entity()) .absolute() .left(px(rect[0] as f32 / sf)) .top(px(rect[1] as f32 / sf)) .w(px(rect[2] as f32 / sf)) .h(px(rect[3] as f32 / sf)) .into_any_element(), ), None if !self.failed.contains(&key) => wanted.push((key, rect)), None => {} } } } pages.push((*place, tiles)); } // Tiles out of view go. let retired = &mut self.retired; self.tiles.retain(|key, image| { let kept = seen.contains(key); if !kept { retired.push(image.clone()); } kept }); let pages_shown: Vec = shown.iter().map(|place| place.page).collect(); self.ask_layers(&pages_shown, cx); self.ask_notes(pages_shown.iter().copied(), cx); self.paint(wanted, cx); let pages: Vec = pages .into_iter() .map(|(place, tiles)| { let mut marks = self.page_marks(&place, laid.scale, cx); marks.extend(self.edit_marks(&place, laid.scale, cx)); marks.extend(self.region_marks(&place, laid.scale, cx)); marks.extend(self.note_marks(&place, laid.scale, cx)); div() .absolute() .left(px(place.x)) .top(px(place.y)) .w(px(place.w)) .h(px(place.h)) .bg(paper) .children(tiles) .children(marks) .into_any_element() }) .collect(); let this = cx.entity(); let (down, moved, up) = (this.clone(), this.clone(), this.clone()); let pages = div() .id("pdf-pages") .size_full() .overflow_scroll() .track_scroll(&self.scroll) .bg(sunken) .on_mouse_down(MouseButton::Left, move |event, window, cx| { down.update(cx, |view, cx| { window.focus(&view.focus, cx); match view.tool { tool if tool.selects() => view.select_from(event.position, cx), tool if tool.edits() => view.press(event.position, cx), _ => view.sketch_from(event.position, cx), } }) }) .on_mouse_move(move |event, _, cx| { if event.dragging() { moved.update(cx, |view, cx| match view.tool { tool if tool.selects() => view.select_to(event.position, cx), tool if tool.edits() => view.drag_to(event.position, cx), _ => view.sketch_to(event.position, cx), }) } }) .on_mouse_up(MouseButton::Left, move |event, _, cx| { up.update(cx, |view, cx| match view.tool { tool if tool.selects() => view.finish_select(event.position, cx), tool if tool.edits() => view.release(cx), _ => view.sketch_end(cx), }) }) .on_scroll_wheel(cx.listener(|_, _, _, cx| cx.notify())) .child(div().relative().w(px(laid.size.0)).h(px(laid.size.1)).children(pages)); // Measures the view itself, which does not scroll. let measure = canvas( move |bounds, _, cx| { this.update(cx, |view, cx| { if view.room != bounds { view.room = bounds; cx.notify(); } }) }, |_, _, _, _| {}, ) .absolute() .top_0() .left_0() .size_full(); div().relative().size_full().child(pages).child(measure).into_any_element() } /// Asks for the first missing tile, one at a time. fn paint(&mut self, wanted: Vec<(TileKey, [i32; 4])>, cx: &mut Context) { let Some((key, rect)) = wanted.first().copied() else { return; }; if self.painting { return; } self.painting = true; let (doc, scale, version) = (self.doc, key.scale as f32 / 1000.0, self.version); self.off( move |worker| worker.render(doc, key.page, (scale, key.rotate), rect, key.dark), move |view, painted, cx| { view.painting = false; if view.version != version { log::info!("pdf: a tile from before an edit is dropped"); return cx.notify(); } let painted = painted.map(|tile| { if tile.damaged { view.found_damage(cx); } tile.bgra }); match painted.map(|bgra| picture((rect[2] as u32, rect[3] as u32), bgra)) { Ok(Some(image)) => { log::debug!("pdf: a tile of page {} came", key.page); if let Some(old) = view.tiles.insert(key, image) { view.retired.push(old); } } Ok(None) => log::error!("pdf: a tile came back the wrong size"), Err(error) if ely_pdf_engine::is_stale(&error) => log::info!("pdf: a tile from before an edit is dropped"), Err(error) => { // A failed tile is not asked again; once said. let first = view.failed.is_empty(); view.failed.insert(key); if first { view.fail("drawing a page", &error, cx); } } } cx.notify(); }, cx, ); } } #[cfg(test)] mod tests { use super::*; #[test] fn turning_and_back_lands_on_the_same_point() { let size = (612.0, 792.0); for rotate in [0, 90, 180, 270, -90] { let point = (100.0, 50.0); let there = to_view(point, size, rotate); assert_eq!(from_view(there, size, rotate), point, "{rotate}"); } assert_eq!(to_view((0.0, 0.0), size, 90), (792.0, 0.0), "the top left goes to the right"); assert_eq!(rect_on([0.0, 0.0, 10.0, 20.0], size, 90, 2.0), (1544.0, 0.0, 40.0, 20.0)); } #[test] fn a_view_edge_on_a_tile_edge_takes_no_tile_beyond() { assert_eq!(span(0.0, 512.0, 1.0, 2000), 0..=0); assert_eq!(span(256.0, 1024.0, 2.0, 4000), 1..=3); assert_eq!(span(0.0, 9000.0, 1.0, 2000), 0..=3, "the page ends first"); } }