diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 5625587a69..5b3ac3bfac 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -382,6 +382,8 @@ pub struct DocView { /// Screen rects of the pages drawn last frame (hit-testing, tests, automation). screen_rects: Vec<(usize, Rect)>, screen_xforms: Vec<(usize, PageXform)>, + /// Pixels per point last frame, for telling a rounding sliver of a page from a visible one. + screen_ppp: f32, /// The scroll viewport on screen last frame. viewport_screen: Rect, /// Pending zoom anchor: page, fractional position (also in gutters), and viewport offset. @@ -669,6 +671,7 @@ impl DocView { page_heights: info.pages.iter().map(|p| p.height).collect(), screen_rects: Vec::new(), screen_xforms: Vec::new(), + screen_ppp: 1.0, viewport_screen: Rect::NOTHING, zoom_anchor: None, shown: false, @@ -2010,6 +2013,7 @@ fn draw_pages( let origin = resp_rect.min - vec2(0.0, y_shift); let visible = viewport.translate(resp_rect.min.to_vec2()); view.screen_rects.clear(); + view.screen_ppp = ui.ctx().pixels_per_point(); view.screen_xforms.clear(); // The pointer as the pages see it: nothing over them (a dialog from the frame it opens), // or a press they hold. The page modules read it rather than egui's raw pointer state. diff --git a/crates/ui-egui/src/canvas/navigation.rs b/crates/ui-egui/src/canvas/navigation.rs index 8f88fff7a4..7f149d912b 100644 --- a/crates/ui-egui/src/canvas/navigation.rs +++ b/crates/ui-egui/src/canvas/navigation.rs @@ -32,7 +32,7 @@ impl DocView { // of pages of different heights, whose shorter page can start below it), else the // nearest one below. let top = self.viewport_screen.top(); - // A page showing less than SLIVER is rounding, not what the reader is looking at: a jump + // A page showing less than a pixel is rounding, not what the reader is looking at: a jump // to the next page leaves the previous one's bottom edge within a pixel of the top. Such a // sliver still counts when nothing else shows (the last page at the very end). let pick = |slack: f32| { @@ -42,7 +42,7 @@ impl DocView { .min_by_key(|(p, _)| *p) .or_else(|| showing().min_by(|a, b| a.1.rect.top().total_cmp(&b.1.rect.top()).then(a.0.cmp(&b.0)))) }; - let (page, xf) = pick(SLIVER).or_else(|| pick(0.0))?; + let (page, xf) = pick(sliver(self.screen_ppp)).or_else(|| pick(0.0))?; let p = info.pages.get(*page)?; // Above the page (in the gap before it) is the page's own top. The window's vertical is // the view's y, or its x when the view is turned a quarter. @@ -460,6 +460,23 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s /// How far ↓ / ↑ scroll at Normal speed (Scroll speed halves or doubles it). Page Down / Page Up /// keep this much of the previous screen in view. const KEY_SCROLL_LINE: f32 = 40.0; -/// How much of a page must show at the window's top edge to count as the page there, in points: less -/// is rounding, since page rectangles snap to whole pixels. -const SLIVER: f32 = 1.0; +/// How much of a page must show at the window's top edge to count as the page there, in points: one +/// physical pixel, since page rectangles snap to whole pixels and less is rounding. +fn sliver(pixels_per_point: f32) -> f32 { + if pixels_per_point.is_finite() && pixels_per_point > 0.0 { 1.0 / pixels_per_point } else { 1.0 } +} + +#[cfg(test)] +mod tests { + use super::sliver; + + #[test] + fn a_sliver_is_one_physical_pixel() { + assert_eq!(sliver(1.0), 1.0); + assert_eq!(sliver(2.0), 0.5); + assert_eq!(sliver(0.5), 2.0); + for bad in [0.0, -1.0, f32::NAN, f32::INFINITY] { + assert_eq!(sliver(bad), 1.0, "{bad}"); + } + } +}