ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
140 lines
5.1 KiB
Rust
140 lines
5.1 KiB
Rust
//! Where pages sit for a mode, zoom and turn (PDF-01).
|
|
|
|
/// How pages follow one another.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
Continuous,
|
|
Single,
|
|
Two,
|
|
}
|
|
|
|
/// How big pages are drawn.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Zoom {
|
|
Width,
|
|
Page,
|
|
/// Pixels a point; 96 / 72 is actual size.
|
|
Scale(f32),
|
|
}
|
|
|
|
/// Points to pixels at actual size.
|
|
pub const ACTUAL: f32 = 96.0 / 72.0;
|
|
/// Space around and between pages, in pixels.
|
|
pub const GAP: f32 = 16.0;
|
|
|
|
/// One page placed, in pixels from the top left.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Placed {
|
|
pub page: usize,
|
|
pub x: f32,
|
|
pub y: f32,
|
|
pub w: f32,
|
|
pub h: f32,
|
|
}
|
|
|
|
/// The pages placed, the whole size, the scale used.
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub struct Layout {
|
|
pub placed: Vec<Placed>,
|
|
pub size: (f32, f32),
|
|
pub scale: f32,
|
|
}
|
|
|
|
/// A page's size in points once turned.
|
|
pub fn turned((w, h): (f32, f32), rotate: i32) -> (f32, f32) {
|
|
match rotate.rem_euclid(360) {
|
|
90 | 270 => (h, w),
|
|
_ => (w, h),
|
|
}
|
|
}
|
|
|
|
/// Lays `sizes` out in `view` pixels; `current` shows alone.
|
|
pub fn layout(sizes: &[(f32, f32)], rotate: i32, (mode, zoom): (Mode, Zoom), view: (f32, f32), current: usize) -> Layout {
|
|
let sizes: Vec<(f32, f32)> = sizes.iter().map(|size| turned(*size, rotate)).collect();
|
|
let shown: Vec<usize> = match mode {
|
|
Mode::Single => sizes.len().checked_sub(1).map(|last| current.min(last)).into_iter().collect(),
|
|
_ => (0..sizes.len()).collect(),
|
|
};
|
|
let across = if mode == Mode::Two { 2.0 } else { 1.0 };
|
|
// Rows of one or two pages, as wide as theirs.
|
|
let rows: Vec<Vec<usize>> = match mode {
|
|
Mode::Two => shown.chunks(2).map(<[usize]>::to_vec).collect(),
|
|
_ => shown.iter().map(|page| vec![*page]).collect(),
|
|
};
|
|
let widest = (rows.iter())
|
|
.map(|row| row.iter().map(|page| sizes[*page].0).sum::<f32>())
|
|
.fold(0.0, f32::max)
|
|
.max(1.0);
|
|
let room = (view.0 - GAP * (1.0 + across)).max(1.0);
|
|
let scale = match zoom {
|
|
Zoom::Scale(scale) => scale,
|
|
Zoom::Width => room / widest,
|
|
Zoom::Page => {
|
|
let tallest = shown.iter().map(|page| sizes[*page].1).fold(0.0, f32::max).max(1.0);
|
|
(room / widest).min((view.1 - 2.0 * GAP).max(1.0) / tallest)
|
|
}
|
|
};
|
|
let width = (widest * scale + GAP * (1.0 + across)).max(view.0);
|
|
let (mut placed, mut y) = (Vec::new(), GAP);
|
|
for row in rows {
|
|
let row_w = row.iter().map(|page| sizes[*page].0 * scale).sum::<f32>() + GAP * (row.len() as f32 - 1.0);
|
|
let row_h = row.iter().map(|page| sizes[*page].1 * scale).fold(0.0, f32::max);
|
|
let mut x = (width - row_w) / 2.0;
|
|
for page in row {
|
|
let (w, h) = (sizes[page].0 * scale, sizes[page].1 * scale);
|
|
placed.push(Placed { page, x, y: y + (row_h - h) / 2.0, w, h });
|
|
x += w + GAP;
|
|
}
|
|
y += row_h + GAP;
|
|
}
|
|
Layout { placed, size: (width, y.max(view.1)), scale }
|
|
}
|
|
|
|
/// Places on screen between `top` and `bottom`.
|
|
pub fn visible(layout: &Layout, top: f32, bottom: f32) -> impl Iterator<Item = &Placed> {
|
|
layout.placed.iter().filter(move |place| place.y < bottom && place.y + place.h > top)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const LETTER: (f32, f32) = (612.0, 792.0);
|
|
|
|
#[test]
|
|
fn continuous_fits_the_width_and_stacks_with_gaps() {
|
|
let out = layout(&[LETTER, LETTER], 0, (Mode::Continuous, Zoom::Width), (644.0, 500.0), 0);
|
|
assert_eq!(out.scale, 1.0, "612 points in 612 pixels of room");
|
|
assert_eq!(out.placed[0], Placed { page: 0, x: 16.0, y: 16.0, w: 612.0, h: 792.0 });
|
|
assert_eq!(out.placed[1].y, 16.0 + 792.0 + 16.0);
|
|
assert_eq!(out.size, (644.0, 16.0 + 2.0 * (792.0 + 16.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn single_shows_one_page_and_two_pairs_them() {
|
|
let pages = [LETTER, LETTER, LETTER];
|
|
let one = layout(&pages, 0, (Mode::Single, Zoom::Scale(1.0)), (800.0, 600.0), 2);
|
|
assert_eq!(one.placed.iter().map(|p| p.page).collect::<Vec<_>>(), [2]);
|
|
let two = layout(&pages, 0, (Mode::Two, Zoom::Scale(1.0)), (800.0, 600.0), 0);
|
|
let rows: Vec<(usize, f32)> = two.placed.iter().map(|p| (p.page, p.y)).collect();
|
|
assert_eq!(rows, [(0, 16.0), (1, 16.0), (2, 824.0)]);
|
|
assert_eq!(two.placed[1].x - two.placed[0].x, 612.0 + 16.0, "side by side");
|
|
}
|
|
|
|
#[test]
|
|
fn a_quarter_turn_swaps_sides_and_page_zoom_fits_the_height() {
|
|
let out = layout(&[LETTER], 90, (Mode::Continuous, Zoom::Page), (1000.0, 644.0), 0);
|
|
assert_eq!((out.placed[0].w / out.scale, out.placed[0].h / out.scale), (792.0, 612.0));
|
|
assert!((out.placed[0].h - 612.0).abs() < 0.01, "the height fits");
|
|
let shown: Vec<usize> = visible(&out, 0.0, 10.0).map(|p| p.page).collect();
|
|
assert!(shown.is_empty(), "above the first page");
|
|
}
|
|
|
|
#[test]
|
|
fn no_pages_lay_out_as_nothing() {
|
|
for mode in [Mode::Continuous, Mode::Single, Mode::Two] {
|
|
assert!(layout(&[], 0, (mode, Zoom::Page), (800.0, 600.0), 0).placed.is_empty());
|
|
}
|
|
}
|
|
}
|