Files
Ely-GPUI-Components/src/documents/print.rs
T
ZacharyZhang-NY 9854cfa460 T21b and T21c: Documents, reading and knowledge
- DocumentViewer: pages the app drew on a desk, the page under the view's middle
  counted, zoom steps and fit, thumbnails, notes pinned where a press lands, and
  the owner's FindWidget over the pages, hits lit and the current one revealed
- PageThumbnailList, EpubReader (chapters, type size, progress), PrintPreview
- MarkdownRenderer feeds TableOfContents through sections and outline
- Footnote, TooltipTerm, Glossary, ReadingProgress, ZenMode
- Backlinks, Favorites, TrashBin, PageCover, PageIcon, TemplatePicker,
  VersionHistory with PageHistoryDiff
- editor::FindWidget shows the replace fold and option toggles only with handlers
- block editor tests move to documents/blocks
- icons: a-arrow-down, a-arrow-up
- gallery: reading and knowledge sections; scripted find, changes and zen
2026-09-26 04:34:32 -04:00

350 lines
11 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, InteractiveElement, IntoElement, ObjectFit, ParentElement, RenderOnce,
StatefulInteractiveElement, Styled, Window, div,
};
use super::viewer::DocPage;
use crate::{
buttons::{Button, ButtonVariant, SegmentedControl},
documents::blocks::source,
forms::{Choice, NumberInput, Run, Select},
primitives::Image,
theme::{ActiveTheme, ControlSize, Elevation, Radius, TextSize},
typography::{format::plural, tabular},
};
/// Sheets the app can print on.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Paper {
#[default]
Letter,
A4,
Legal,
}
/// Space kept around what prints.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Margins {
None,
Narrow,
#[default]
Normal,
}
/// How a print goes: paper, turn, margins and copies.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PrintSettings {
pub paper: Paper,
pub landscape: bool,
pub margins: Margins,
pub copies: u32,
}
impl Default for PrintSettings {
fn default() -> Self {
Self {
paper: Paper::default(),
landscape: false,
margins: Margins::default(),
copies: 1,
}
}
}
const PAPERS: [(Paper, &str); 3] = [
(Paper::Letter, "Letter"),
(Paper::A4, "A4"),
(Paper::Legal, "Legal"),
];
const MARGINS: [(Margins, &str); 3] = [
(Margins::None, "None"),
(Margins::Narrow, "Narrow"),
(Margins::Normal, "Normal"),
];
/// Most copies one print makes.
const MOST_COPIES: f64 = 99.0;
type OnSettings = Rc<dyn Fn(PrintSettings, &mut Window, &mut App)>;
/// A print before it goes: the pages the app laid out for the settings, small on a desk, and the settings beside, paper, turn, margins and copies; each change asks the app for new pages.
#[derive(IntoElement)]
pub struct PrintPreview {
id: ElementId,
pages: Rc<Vec<DocPage>>,
settings: PrintSettings,
on_change: OnSettings,
on_print: OnSettings,
on_cancel: Run,
}
impl PrintPreview {
pub fn new(
id: impl Into<ElementId>,
pages: impl Into<Rc<Vec<DocPage>>>,
settings: PrintSettings,
on_change: impl Fn(PrintSettings, &mut Window, &mut App) + 'static,
on_print: impl Fn(PrintSettings, &mut Window, &mut App) + 'static,
on_cancel: impl Fn(&mut Window, &mut App) + 'static,
) -> Self {
Self {
id: id.into(),
pages: pages.into(),
settings,
on_change: Rc::new(on_change),
on_print: Rc::new(on_print),
on_cancel: Rc::new(on_cancel),
}
}
}
fn row(label: &'static str, control: impl IntoElement, cx: &App) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_1()
.child(
div()
.text_size(cx.theme().text_size(TextSize::Sm))
.text_color(cx.theme().colors.fg_muted)
.child(label),
)
.child(control)
}
impl RenderOnce for PrintPreview {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme();
let colors = theme.colors.clone();
let settings = self.settings;
let paper = {
let on_change = self.on_change.clone();
Select::new(
(self.id.clone(), "paper"),
PAPERS.map(|(_, name)| Choice::new(name, name)),
)
.selected(
PAPERS
.iter()
.find(|(paper, _)| *paper == settings.paper)
.expect("a known paper")
.1,
)
.size(ControlSize::Sm)
.on_change(move |name, window, cx| {
let (paper, _) = PAPERS
.into_iter()
.find(|(_, named)| *named == name.as_ref())
.expect("a listed paper");
on_change(PrintSettings { paper, ..settings }, window, cx);
})
};
let turn = {
let on_change = self.on_change.clone();
SegmentedControl::new(
(self.id.clone(), "turn"),
if settings.landscape {
"landscape"
} else {
"portrait"
},
)
.size(ControlSize::Sm)
.segment("portrait", "Portrait", None)
.segment("landscape", "Landscape", None)
.on_change(move |value, window, cx| {
on_change(
PrintSettings {
landscape: value.as_ref() == "landscape",
..settings
},
window,
cx,
)
})
};
let margins = {
let on_change = self.on_change.clone();
Select::new(
(self.id.clone(), "margins"),
MARGINS.map(|(_, name)| Choice::new(name, name)),
)
.selected(
MARGINS
.iter()
.find(|(margins, _)| *margins == settings.margins)
.expect("known margins")
.1,
)
.size(ControlSize::Sm)
.on_change(move |name, window, cx| {
let (margins, _) = MARGINS
.into_iter()
.find(|(_, named)| *named == name.as_ref())
.expect("listed margins");
on_change(
PrintSettings {
margins,
..settings
},
window,
cx,
);
})
};
let copies = {
let on_change = self.on_change.clone();
NumberInput::new((self.id.clone(), "copies"), settings.copies as f64)
.range(1.0, MOST_COPIES)
.step(1.0)
.size(ControlSize::Sm)
.on_change(move |copies, window, cx| {
on_change(
PrintSettings {
copies: copies as u32,
..settings
},
window,
cx,
)
})
};
let (print, cancel) = (self.on_print.clone(), self.on_cancel.clone());
let thumb = theme.label_width();
div()
.flex()
.size_full()
.rounded(theme.radius(Radius::Lg))
.border_1()
.border_color(colors.border)
.overflow_hidden()
.child(
div()
.id((self.id.clone(), "desk"))
.flex_1()
.min_w_0()
.overflow_y_scroll()
.bg(colors.sunken)
.p_6()
.flex()
.flex_wrap()
.content_start()
.gap_4()
.children(self.pages.iter().enumerate().map(|(ix, page)| {
let (width, height) = sheet(settings);
div()
.flex()
.flex_col()
.items_center()
.gap_1()
.child(
div()
.w(thumb)
.h(thumb * (height / width))
.bg(colors.paper)
.shadow(theme.elevation(Elevation::Raised))
.p(thumb * margin(settings.margins))
.child(
Image::new(
(self.id.clone(), format!("sheet-{ix}")),
source(&page.source),
)
.fit(ObjectFit::Contain)
.size_full(),
),
)
.child(
tabular(
div()
.text_size(theme.text_size(TextSize::Xs))
.text_color(colors.fg_subtle),
)
.child(format!("{}", ix + 1)),
)
})),
)
.child(
div()
.flex_none()
.w(theme.sidebar_width(false))
.flex()
.flex_col()
.gap_4()
.p_4()
.border_l_1()
.border_color(colors.border)
.bg(colors.surface)
.child(div().text_color(colors.fg_muted).child(format!(
"{} · {}",
plural(self.pages.len() as u64, "page", "pages"),
plural(settings.copies as u64, "copy", "copies")
)))
.child(row("Paper", paper, cx))
.child(row("Turn", turn, cx))
.child(row("Margins", margins, cx))
.child(row("Copies", copies, cx))
.child(div().flex_1())
.child(
div()
.flex()
.justify_end()
.gap_2()
.child(
Button::new((self.id.clone(), "cancel"), "Cancel")
.variant(ButtonVariant::Ghost)
.on_click(move |_, window, cx| cancel(window, cx)),
)
.child(
Button::new((self.id.clone(), "print"), "Print")
.variant(ButtonVariant::Primary)
.on_click(move |_, window, cx| {
log::info!("print preview: print {settings:?}");
print(settings, window, cx)
}),
),
),
)
}
}
/// The share of a sheet's width each margin keeps.
fn margin(margins: Margins) -> f32 {
match margins {
Margins::None => 0.0,
Margins::Narrow => 0.04,
Margins::Normal => 0.08,
}
}
/// The paper's size in points, turned when landscape, for the app laying out pages.
pub fn sheet(settings: PrintSettings) -> (f32, f32) {
let (width, height) = match settings.paper {
Paper::Letter => (612.0, 792.0),
Paper::A4 => (595.0, 842.0),
Paper::Legal => (612.0, 1008.0),
};
if settings.landscape {
(height, width)
} else {
(width, height)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sheet_turns_on_its_side() {
let settings = PrintSettings {
paper: Paper::A4,
landscape: true,
..PrintSettings::default()
};
assert_eq!(sheet(settings), (842.0, 595.0));
assert_eq!(sheet(PrintSettings::default()), (612.0, 792.0));
}
}