Skip to content

gpui-pdf API

The complete public API of gpui-pdf — every exported item, with its signature, parameters, return contract, edge cases, and cost. For the what-and-why (the two layers, quick start, password flow, feature overview), see the README.

Everything below is the complete public surface — if it isn’t listed here, it isn’t public. Items in the Feature column require that Cargo feature (search implies markup); ”—” means always available.

ItemKindFeatureSignaturePurpose
Documenttype aliastype Document = hayro::PdfA parsed PDF, shared via Arc
LoadErrorenumLocked | Other(String)Why loading a PDF failed
parsefnfn parse(bytes: Arc<Vec<u8>>) -> Result<Arc<Document>, LoadError>Parse PDF bytes once, reuse per page
normalize_form_appearancesfnformsfn normalize_form_appearances(bytes: &[u8]) -> Option<Vec<u8>>Rewrite form-widget appearances so they render
form_fieldsfnformsfn form_fields(bytes: &[u8]) -> Vec<FormField>Every form widget: name, kind, page, rect, value
set_form_valuefnformsfn set_form_value(bytes: &[u8], name: &str, value: &str) -> Option<Vec<u8>>Write a field’s value + regenerate its appearance
FormFieldstructformsOne widget, described for a host UI
FieldKindenumformsText | Checkbox | Radio | Choice | SignatureWhat input a field takes
PdfView::form_fieldsmethodformsfn form_fields(&self) -> &[FormField]The loaded document’s fields (Tab order)
PdfView::reveal_fieldmethodformsfn reveal_field(&mut self, field: &FormField, cx) -> Option<Bounds<Pixels>>Scroll a field on-screen, return its window bounds
PdfView::replace_bytesmethodfn replace_bytes(&mut self, bytes: Vec<u8>, cx)Hot-swap the document (scroll/zoom kept, no blanking)
PdfView::set_on_open_externalmethodfn set_on_open_external(&mut self, f: OpenExternalFn)Button on the failure pane → host opens the OS viewer
OpenExternalFntype aliasRc<dyn Fn(&mut Window, &mut App)>The failure pane’s hand-off callback
PdfEvent::FieldClickedevent variantforms{ field: FormField, bounds: Bounds<Pixels> }A form widget was clicked — toggle or seat an input
parse_with_passwordfnfn parse_with_password(bytes: Arc<Vec<u8>>, password: &str) -> Result<Arc<Document>, LoadError>Parse an encrypted PDF
page_dimsfnfn page_dims(doc: &Document) -> Vec<(f32, f32)>Per-page (w, h) in points, no rasterization
render_pagefnfn render_page(doc: &Document, idx: usize, scale: f32) -> Result<Arc<RenderImage>, String>Rasterize one page to a BGRA bitmap
is_pdffnfn is_pdf(src: &str) -> bool.pdf extension check (sees through ?query)
PAGE_WIDTHconstconst PAGE_WIDTH: f32 = 820.0Base on-screen page width at zoom 1.0
keep_windowfnfn keep_window(dims: &[(f32, f32)], page_width: f32, scroll_y: f32, viewport_h: f32) -> (usize, usize)Which pages to keep rasterized
PdfStylestruct6 pub Hsla fields; impl DefaultColors for the viewer chrome
PdfStyleFntype aliasRc<dyn Fn() -> PdfStyle>Live style source, read at paint time
PdfQualityFntype aliasRc<dyn Fn() -> f32>Live render-quality source
PdfEventenumLockChanged, LoadFailedLock transitions + terminal load failures
PdfViewstructimpl Render + EventEmitter<PdfEvent>The ready-made page-virtualized viewer
PdfView::newconstructorfn new(path: PathBuf, style: PdfStyleFn, quality: PdfQualityFn, cx: &mut Context<Self>) -> SelfCreate a viewer; loads off-thread
PdfView::is_lockedmethodfn is_locked(&self) -> boolEncrypted and awaiting a password
PdfView::unlock_failedmethodfn unlock_failed(&self) -> boolLast unlock used a wrong password
PdfView::load_errormethodfn load_error(&self) -> Option<&SharedString>Terminal read/parse failure message
PdfView::unlockmethodfn unlock(&mut self, password: String, cx: &mut Context<Self>)Retry an encrypted PDF with a password
PdfView::releasemethodfn release(&mut self, window: &mut Window, cx: &mut Context<Self>)Free all bitmaps + GPU textures before drop
PdfView::detach_texturesmethodfn detach_textures(&mut self, window: &mut Window, cx: &mut Context<Self>)Free GPU textures, keep bitmaps (window move)
PdfView::set_zoommethodfn set_zoom(&mut self, zoom: f32, cx: &mut Context<Self>)Set zoom (clamped 0.5–3.0)
PdfView::zoom_inmethodfn zoom_in(&mut self, cx: &mut Context<Self>)Zoom in one step (×1.25)
PdfView::zoom_outmethodfn zoom_out(&mut self, cx: &mut Context<Self>)Zoom out one step (÷1.25)
PdfView::reset_zoommethodfn reset_zoom(&mut self, cx: &mut Context<Self>)Back to 100%
PdfView::fit_widthmethodfn fit_width(&mut self, cx: &mut Context<Self>)Sticky fit-to-width zoom (toggles)
PdfView::fit_pagemethodfn fit_page(&mut self, cx: &mut Context<Self>)Sticky whole-page fit zoom (toggles)
FitModeenumWidth | PageThe two zoom-to-fit modes
PdfView::go_to_pagemethodfn go_to_page(&mut self, index: usize, cx: &mut Context<Self>)Scroll a page to the viewport top
PdfView::next_pagemethodfn next_page(&mut self, cx: &mut Context<Self>)Go to the next page
PdfView::prev_pagemethodfn prev_page(&mut self, cx: &mut Context<Self>)Go to the previous page
PdfView::toggle_tocmethodfn toggle_toc(&mut self, cx: &mut Context<Self>)Toggle the table-of-contents panel
PdfView::has_outlinemethodfn has_outline(&self) -> boolWhether the PDF has bookmarks
PdfView::set_highlightsmethodmarkupfn set_highlights(&mut self, highlights: Vec<Highlight>, cx: &mut Context<Self>)Hand the viewer highlights to draw
PdfView::set_on_highlightmethodmarkupfn set_on_highlight(&mut self, handler: HighlightClickFn)Click handler for a highlight
PdfView::set_on_create_highlightmethodmarkupfn set_on_create_highlight(&mut self, handler: CreateHighlightFn)Handler for a finished drag-selection
PdfView::toggle_select_modemethodmarkupfn toggle_select_mode(&mut self, cx: &mut Context<Self>)Toggle drag-to-highlight mode
PdfView::toggle_area_modemethodmarkupfn toggle_area_mode(&mut self, cx: &mut Context<Self>)Toggle drag-a-box area mode
PdfView::set_on_create_areamethodmarkupfn set_on_create_area(&mut self, handler: CreateAreaFn, cx: &mut Context<Self>)Handler for a finished area drag
PdfView::set_highlight_palettemethodmarkupfn set_highlight_palette(&mut self, palette: Vec<(SharedString, Hsla)>, cx: &mut Context<Self>)Colors for the picker
PdfView::reveal_highlightmethodmarkupfn reveal_highlight(&mut self, page: usize, cx: &mut Context<Self>)Scroll to a page’s highlight and flash it
PdfView::toggle_searchmethodsearchfn toggle_search(&mut self, cx: &mut Context<Self>)Open/close the find bar
PdfView::close_searchmethodsearchfn close_search(&mut self, cx: &mut Context<Self>)Close the find bar, clear matches
PdfView::next_matchmethodsearchfn next_match(&mut self, cx: &mut Context<Self>)Focus the next match (wraps)
PdfView::prev_matchmethodsearchfn prev_match(&mut self, cx: &mut Context<Self>)Focus the previous match (wraps)
OutlineItemstruct{ title: String, level: usize, page: Option<usize> }One flattened outline (bookmark) entry
LinkTargetenumPage(usize) | Uri(String)Where a clickable PDF link points
PdfLinkstruct{ x, y, w, h: f32, target: LinkTarget }A /Link annotation, normalized rect
outlinefnfn outline(doc: &Document) -> Vec<OutlineItem>Extract the document outline
page_linksfnfn page_links(doc: &Document) -> Vec<Vec<PdfLink>>Extract link annotations per page
Highlightstructmarkup{ id: u64, page: usize, quote: String, occurrence: usize, color: Hsla }A quote-anchored highlight to draw
HighlightClickFntype aliasmarkupRc<dyn Fn(u64, &mut Window, &mut App)>Highlight click callback
CreateHighlightFntype aliasmarkupRc<dyn Fn(usize, String, usize, SharedString, &mut Window, &mut App)>Drag-selection-finished callback
CreateAreaFntype aliasmarkupRc<dyn Fn(usize, NormRect, SharedString, &mut Window, &mut App)>Area-drag-finished callback
NormRectstructmarkup{ x, y, w, h: f32 }Rect in normalized (0..1) page coords
NormPointstructmarkup{ x, y: f32 }Point in normalized page coords
Selectionstructmarkup{ quote: String, occurrence: usize, rects: Vec<NormRect> }A resolved drag selection
extract_page_textfnmarkupfn extract_page_text(doc: &Document, index: usize) -> Option<PageText>Extract a page’s text layer
PageTextstructmarkupA page’s text + per-glyph rects
PageText::is_emptymethodmarkupfn is_empty(&self) -> boolNo extractable text (pure scan)
PageText::textmethodmarkupfn text(&self) -> StringReadable reconstruction of the page
PageText::locatemethodmarkupfn locate(&self, needle: &str, occurrence: usize) -> Vec<NormRect>Find the nth occurrence of a quote
PageText::find_matchesmethodsearchfn find_matches(&self, needle: &str) -> Vec<Vec<NormRect>>All occurrences, reading order
PageText::selectmethodmarkupfn select(&self, from: NormPoint, to: NormPoint) -> Option<Selection>Resolve a drag into a quote
impl Default for PdfStyletrait implfn default() -> SelfNeutral dark palette
impl EventEmitter<PdfEvent> for PdfViewtrait implSubscribe via cx.subscribe
impl Render for PdfViewtrait implRender the Entity<PdfView> as a child view

pub type Document = hayro::hayro_syntax::Pdf;

A parsed PDF. Parse once (not per page) — re-parsing a large file for every page is slow and churns the allocator. hayro’s Pdf is Send + Sync and caches pages internally, so share it via Arc across background render tasks. The Document owns the file bytes it was parsed from.


pub enum LoadError {
Locked,
Other(String),
}

Why loading a PDF failed.

  • Locked — the PDF is encrypted and the supplied password was missing or wrong. The caller can prompt for a password and retry via parse_with_password.
  • Other(String) — any other failure: malformed file, or an encryption scheme hayro’s standard security handler doesn’t cover (public-key / certificate handlers, non-standard crypt filters). The string is a debug-formatted hayro error, for logging — not for display to users.

#[derive(Debug)] only — no std::error::Error impl, no Display.


pub fn parse(bytes: Arc<Vec<u8>>) -> Result<Arc<Document>, LoadError>

Parse a PDF’s bytes into a reusable Document. Exactly parse_with_password(bytes, "").

Parameters

NameTypeDescription
bytesArc<Vec<u8>>The whole file’s bytes. The Document keeps the Arc, so the caller can drop its copy.

ReturnsOk(Arc<Document>) ready for page_dims / render_page, or a LoadError.

Guarantees & edge cases

  • A password-protected file returns Err(LoadError::Locked) — retry with parse_with_password. This is the only condition mapped to Locked (hayro’s DecryptionError::PasswordProtected); every other parse or decryption failure is Other.
  • Never panics; malformed input is Err(Other).

Cost & threading — parses the cross-reference table and document structure (pages are lazy). Cheap for small files, but run it off the UI thread for large ones; the result is Send + Sync.


pub fn parse_with_password(
bytes: Arc<Vec<u8>>,
password: &str,
) -> Result<Arc<Document>, LoadError>

Like parse, but supplies a decryption password for an encrypted PDF.

Parameters

NameTypeDescription
bytesArc<Vec<u8>>The whole file’s bytes.
password&strThe password that opens the document. "" for an unencrypted file.

ReturnsOk(Arc<Document>), or Err(LoadError::Locked) when the file is password-protected and password is missing or incorrect, or Err(LoadError::Other) otherwise.

Guarantees & edge cases

  • Decryption is hayro’s, via the PDF standard security handler (the password-based scheme):

    AlgorithmPDF /VNotes
    RC4, 40-bit1legacy
    RC4, 40–128-bit2key length from /Length
    AES-1284AESV2 crypt filter (RC4 via a V2 filter also works)
    AES-2565 / 6AESV3 crypt filter; PDF 2.0 (revision 6)
  • Anything else — public-key / certificate handlers (/Filter/Standard), any non-standard crypt filter — surfaces as Err(Other), not Locked: don’t show a password prompt for it.

  • With the forms feature, the bytes first pass through normalize_form_appearances (both here and in parse) so form values and checkbox states display; an encrypted or form-free file passes through untouched.

  • Never panics.

Example

match gpui_pdf::parse_with_password(bytes, password) {
Ok(doc) => { /* render */ }
Err(gpui_pdf::LoadError::Locked) => { /* prompt for a password and retry */ }
Err(gpui_pdf::LoadError::Other(e)) => { /* malformed / unsupported encryption */ }
}

Feature: forms.

pub fn normalize_form_appearances(bytes: &[u8]) -> Option<Vec<u8>>

Rewrite a document’s bytes so every AcroForm widget has a directly-renderable appearance stream. hayro composites annotation /AP /N streams but (a) skips a checkbox/radio whose /N is a dictionary of states selected by /AS, and (b) never synthesizes an appearance for a valued text field that has none (the NeedAppearances case). This pass fixes both at the byte level — resolve /N through /AS; synthesize a Helvetica appearance for the field’s /V — so form PDFs display what they carry. parse / parse_with_password call it automatically under this feature; it’s public for hosts that manage bytes themselves.

Parameters

NameTypeDescription
bytes&[u8]The whole file’s bytes.

ReturnsSome(rewritten_bytes) only when something changed; None means keep the originals (no form widgets, nothing to fix, an encrypted file, or bytes lopdf can’t parse).

Guarantees & edge cases

  • Idempotent: running it on its own output returns None.
  • Encrypted documents are left untouched (None) — hayro decrypts on its own; rewriting would need the password.
  • /FT and /V are resolved up the /Parent chain (split field/widget pairs), bounded against cyclic chains.
  • Synthesized text is single-line, left-aligned, ~12 pt Helvetica (WinAnsi-lossy: characters outside Latin-1 become ?), clipped to the widget rect by the XObject’s BBox. /DA fonts, /Q quadding, comb fields, multiline layout, and rich-text values are not honored — this is display correctness, not a form engine.
  • Only /AP /N (the normal appearance) is touched; /D//R states and non-widget annotations pass through unchanged.
  • Never panics; any structural surprise inside a widget just skips that widget.

Cost — a full lopdf parse + serialize when changes are made (one-time, at load). A document with no /Subtype /Widget objects short-circuits after the parse.


Feature: forms.

pub fn form_fields(bytes: &[u8]) -> Vec<FormField>

Every form-field widget in the document, in page order — what a host needs to overlay inputs on the viewer.

Parameters

NameTypeDescription
bytes&[u8]The whole file’s bytes.

Returns — one FormField per widget (a radio group is one field name across several widgets, each reported with its own rect and on-state). Pushbuttons carry no value and are skipped.

Guarantees & edge cases

  • An encrypted or unparseable file yields an empty Vec — never an error.
  • rect is in PDF points with a bottom-left origin, corners normalized (x0 < x1, y0 < y1); to overlay on a rendered page, flip y against the page height and multiply by the render scale.
  • Names are fully qualified (/T joined root-first with . up the /Parent chain) — exactly the key set_form_value expects.
  • /FT, /V, /Ff, and /Opt resolve through field inheritance.

Feature: forms.

pub fn set_form_value(bytes: &[u8], name: &str, value: &str) -> Option<Vec<u8>>

Set the field named name and regenerate its appearance stream, so the written file renders correctly in every viewer — not just this crate’s.

Parameters

NameTypeDescription
bytes&[u8]The whole file’s bytes.
name&strA fully-qualified name from form_fields.
value&strText/Choice: the literal text. Checkbox/Radio: an on-state from FormField::options, or "Off" to clear.

ReturnsSome(rewritten_bytes) on success; None when nothing matched (unknown name, read-only or signature field, encrypted/unparseable file).

Guarantees & edge cases

  • /V is written on the dict that owns /FT (the parent for split field/widget pairs), so sibling widgets stay consistent; a radio group’s every widget gets its /AS set (value where that widget carries the state, Off elsewhere).
  • Text appearances are regenerated with the same synthesis as normalize_form_appearances — same ceilings (single-line, WinAnsi-lossy).
  • Read-only fields (/Ff bit 1) and signature fields are refused.
  • The caller owns persistence: write the returned bytes wherever the document lives, and re-parse to refresh a viewer.

Feature: forms.

pub struct FormField {
pub name: String, // fully-qualified field name (the set_form_value key)
pub kind: FieldKind,
pub page: usize, // 0-based page index
pub rect: (f32, f32, f32, f32), // PDF points, bottom-left origin, corners ordered
pub value: String, // text, or the on-state name / "Off" for buttons
pub read_only: bool, // /Ff bit 1
pub options: Vec<String>, // Choice: /Opt entries; Checkbox/Radio: on-state names
}

One widget, described for a host UI. See form_fields for the coordinate and naming contracts.


Feature: forms.

pub enum FieldKind { Text, Checkbox, Radio, Choice, Signature }

What input a field takes. Radio is one widget of a group (same name, several widgets). Signature is display-only — set_form_value refuses it. Pushbuttons never appear (no value to hold).


Feature: forms.

pub fn form_fields(&self) -> &[FormField]

The loaded document’s form fields, enumerated at load from the original bytes (not the display-normalized ones) — for a host driving Tab navigation. The order is form_fields’ page-then-document order. Empty before the load finishes and for form-free documents.


Feature: forms.

pub fn reveal_field(&mut self, field: &FormField, cx: &mut Context<Self>) -> Option<Bounds<Pixels>>

Scroll so field’s widget sits comfortably on-screen (a 56 px margin), then return its fresh window-space bounds — what a host needs to seat an input on a field reached by Tab rather than by click. None before the first layout. Every form widget is rendered as a transparent overlay with a hover tint and a pointer cursor; clicking one emits PdfEvent::FieldClicked with the same bounds shape.


pub fn replace_bytes(&mut self, bytes: Vec<u8>, cx: &mut Context<Self>)

Swap in a new version of the document — e.g. after set_form_value rewrote the file — keeping scroll, zoom, and view state. Re-parses off-thread. When the page count is unchanged, the old page bitmaps keep painting until their crisp replacements land (the same no-blanking swap as zoom/quality changes); a different page count resets the slots. Highlights’ cached text layers are dropped and rebuilt lazily. A parse failure is logged and the old document stays.


pub fn page_dims(doc: &Document) -> Vec<(f32, f32)>

Each page’s (width, height) in PDF points.

Parameters

NameTypeDescription
doc&DocumentA parsed PDF.

ReturnsVec<(f32, f32)>, one (width, height) per page, in page order (hayro’s render_dimensions, i.e. the size a render would produce — rotation already applied).

Guarantees & edge cases

  • A zero-page document returns an empty vec.
  • No rasterization — cheap enough to call on load, so a viewer can lay out correctly-sized page slots (and a correct scrollbar) before any page renders.

pub fn render_page(doc: &Document, idx: usize, scale: f32) -> Result<Arc<RenderImage>, String>

Rasterize a single page of an already-parsed Document.

Parameters

NameTypeDescription
doc&DocumentA parsed PDF.
idxusize0-based page index.
scalef32Pixels per PDF point (page point-size × this = bitmap size).

ReturnsArc<gpui::RenderImage> (a single frame), or Err(String) with a short human-readable message.

Guarantees & edge cases

  • The bitmap is BGRA, fully opaque, composited onto white: hayro produces premultiplied RGBA; each pixel is alpha-composited over white and channel-swapped, and the output alpha is forced to 255. Transparent PDF backgrounds therefore render white, not transparent.
  • An out-of-range idx returns Err (never panics).
  • Higher scale = sharper but more memory (pixels grow quadratically). PdfView derives its scale from display pixel ratio × zoom × quality and clamps it to 0.5–4.0; do something similar if you call this directly.

Cost & threading — CPU-bound rasterization of the whole page; run it on a background thread (Document is Send + Sync, so clone the Arc into the task).


pub fn is_pdf(src: &str) -> bool

True if a link/image src points at a PDF.

Parameters

NameTypeDescription
src&strA path or URL string.

Returnsbool: whether src — lowercased, trailing whitespace trimmed, and with any ?query suffix ignored (report.pdf?v=2 counts) — ends with .pdf. A #fragment is NOT stripped: for PDF refs it’s the page-jump anchor (file.pdf#p3), handled by callers.

Guarantees & edge cases

  • Pure string check — no filesystem access, no content sniffing.
  • A URL with a query string or fragment (report.pdf?v=2) is not detected; only a trailing extension is.

pub const PAGE_WIDTH: f32 = 820.0;

The base on-screen page width in points at zoom 1.0. PdfView lays pages out at PAGE_WIDTH × zoom wide (heights follow each page’s aspect ratio); pass the same value to keep_window if you reuse its math.


pub fn keep_window(
dims: &[(f32, f32)],
page_width: f32,
scroll_y: f32,
viewport_h: f32,
) -> (usize, usize)

The inclusive page-index range (start, end) to keep rasterized for a scroll position: the pages intersecting the viewport, padded by 3 pages on each side (so scrolling finds neighbors already rendered and small wiggles don’t thrash render/evict). This is the pure, unit-tested core of PdfView’s virtualization — use it to build your own viewer.

Parameters

NameTypeDescription
dims&[(f32, f32)]Per-page (w, h) in points (from page_dims).
page_widthf32On-screen column width in px (base × zoom).
scroll_yf32How far the content is scrolled down (px, ≥ 0; negatives are clamped).
viewport_hf32Visible height (px).

Returns(start, end), an inclusive 0-based index range, always within 0..dims.len().

Guarantees & edge cases

  • Empty dims(0, 0).
  • viewport_h ≤ 1.0 (first frame, before layout) → assumes a 900 px viewport so the first pages still render.
  • Mirrors PdfView’s exact slot layout: 16 px top padding, then each page’s aspect-scaled height, with a 10 px gap between pages. If your layout differs, the window will be offset.
  • Pure and cheap (one linear scan); never panics.

pub struct PdfStyle {
pub bg: Hsla, // viewer background
pub border: Hsla, // page-slot border + header divider
pub placeholder_bg: Hsla, // unrendered page slot + control hover
pub placeholder_fg: Hsla, // "Page N" / "Loading…" text
pub header_fg: Hsla, // header filename + control text
pub header_muted: Hsla, // "· N pages" / page counter text
}

Colors for the PdfView chrome. Clone + Copy. PdfStyle::default() is a neutral dark palette (near-black background, faint white borders/text).


pub type PdfStyleFn = Rc<dyn Fn() -> PdfStyle>;

Supplies the current PdfStyle at paint time. PdfView is a persistent entity (not rebuilt by its parent each frame), so it reads its colors through this closure on every render — return fresh colors each call and the viewer follows live theme changes (and can differ per window) with no push from the host.


pub type PdfQualityFn = Rc<dyn Fn() -> f32>;

Supplies the current render-quality multiplier at paint time: 1.0 = native DPI; lower is faster and softer, higher supersamples. Clamped internally to 0.25–3.0. Read like PdfStyleFn — when the returned value changes by more than 0.01, every open viewer bumps its render generation and re-rasterizes visible pages (showing the old bitmaps, rescaled, until the crisp ones land). This is why there is no set_quality method.


pub enum PdfEvent {
LockChanged,
LoadFailed,
}

Emitted (via impl EventEmitter<PdfEvent> for PdfView). LockChanged fires when the view’s lock state transitions: the load discovered an encrypted file, an unlock succeeded, or an unlock failed with a wrong password. LoadFailed fires when the file can’t be read or parsed — terminal for the view, which renders the failure message (see PdfView::load_error) in place of the loading placeholder; it also fires when a retry-unlock fails with a non-password error (e.g. an unsupported encryption handler), so a password prompt knows to stand down. Fired only on these transitions — not on every redraw.


pub struct PdfView { /* private */ }
impl Render for PdfView { /* … */ }
impl EventEmitter<PdfEvent> for PdfView { /* … */ }

A self-contained, page-virtualized PDF viewer entity. Every page gets a correctly-sized slot up front (so the scrollbar reflects the whole document), but only the pages within the visible range ±3 are rasterized; pages scrolled away are freed — CPU pixel buffer and GPU atlas texture — so memory is bounded by what’s on screen, not the page count.

Built-in chrome: a header with the filename, page navigation (‹ / ›, a click-to-edit page counter you can type a number into), zoom controls (−, %, +), a table-of-contents side panel (when the PDF has an outline), clickable link annotations overlaid on each page (internal → jump to page, external → cx.open_url), an overlay scrollbar, and a scroll-to-top button. With markup: a highlight pen + color picker; with search: a find bar (🔍).

Keyboard shortcuts (handled when the viewer is focused — it focuses itself on click): PageUp / PageDown / Home / End navigate; ⌘= / ⌘- / ⌘0 zoom; ⌘⌥G jumps to a page; ⌘⇧H toggles highlight mode (markup); ⌘F toggles find and ⌘G / ⌘⇧G step matches (search). ”⌘” is the platform secondary modifier (Ctrl elsewhere).

Each PdfView owns its own scroll handle, zoom, and document, so multiple viewers operate independently.

Rendering states — while loading (or after a failed load) the view renders a “Loading PDF…” placeholder. A file-read error or malformed PDF is logged and the view stays on that placeholder indefinitely — there is no error state or event for it. An encrypted file flips to locked instead (see is_locked).

pub fn new(
path: PathBuf,
style: PdfStyleFn,
quality: PdfQualityFn,
cx: &mut Context<Self>,
) -> Self

Create a viewer for path, kicking off the off-thread read + parse + measure. Call inside cx.new(|cx| PdfView::new(path, style, quality, cx)).

Parameters

NameTypeDescription
pathPathBufA local .pdf file. Read once on the background executor.
stylePdfStyleFnChrome colors, read at paint time.
qualityPdfQualityFnRender-quality multiplier, read at paint time.
cx&mut Context<Self>The entity context (from cx.new).

Returns — the viewer, immediately renderable (it shows “Loading PDF…” until the load lands).

Guarantees & edge cases

  • The file is read, parsed, and measured on the background executor; the outline and link annotations are extracted in the same pass. The UI never blocks.
  • Encrypted file → the view becomes locked and emits PdfEvent::LockChanged; the raw bytes are kept so unlock retries without re-reading the disk.
  • Unreadable or malformed file → log::error! and the view stays on the loading placeholder. No panic, no event.

Cost & threading — construction is cheap; the heavy work is spawned. Main thread (it’s a gpui entity).

Example

let view = cx.new(|cx| {
PdfView::new(path, Rc::new(PdfStyle::default), Rc::new(|| 1.0), cx)
});
// then `view.clone()` into your element tree; call `release` before dropping.
pub fn is_locked(&self) -> bool

Whether the PDF is encrypted and awaiting a password — the host should render its own password prompt and call unlock instead of rendering the viewer. Flips true when the initial load hits a password-protected file; flips false on a successful unlock. Parameters — none (&self).

pub fn unlock_failed(&self) -> bool

Whether the most recent unlock used a wrong password — drives the prompt’s “incorrect password” message. Cleared at the start of the next attempt (and on success). Parameters — none (&self).

pub fn load_error(&self) -> Option<&SharedString>

The terminal read/parse failure, if any — the viewer renders this message in place of the loading placeholder, and PdfEvent::LoadFailed fired when it was set. None while loading and after a successful parse. Parameters — none (&self).

pub fn set_on_open_external(&mut self, f: OpenExternalFn)

Set the handler behind the failure pane’s “Open in system viewer” button — a graceful hand-off for files hayro can’t parse (an unsupported encryption handler such as a public-key/certificate scheme, exotic transparency/blend features). The viewer stays host-agnostic: the host does the actual OS launch. Without a handler the pane shows only the error text.

Parameters

NameTypeDescription
fOpenExternalFnCalled from the button with (&mut Window, &mut App).
pub fn unlock(&mut self, password: String, cx: &mut Context<Self>)

Retry an encrypted PDF with password, reusing the bytes already read.

Parameters

NameTypeDescription
passwordStringThe user’s password attempt.
cx&mut Context<Self>Entity context.

Returns — nothing; the result arrives asynchronously.

Guarantees & edge cases

  • Asynchronous: re-parses off-thread, then either installs the document (viewer renders, is_locked → false) or sets unlock_failed and stays locked. Emits PdfEvent::LockChanged on either outcome, so the prompt can react.
  • Exception: if the retry fails with LoadError::Other (e.g. an unsupported encryption scheme), it is only logged — no event, no state change.
  • Silently a no-op if called before the initial load has read the file bytes.
pub fn release(&mut self, window: &mut Window, cx: &mut Context<Self>)

Free every rasterized page — CPU pixel buffers (by dropping the Arcs) and the GPU atlas textures. Call this before dropping the view (e.g. when its tab closes): gpui caches one atlas texture per RenderImage on paint and only frees it via drop_image; a plain drop leaks the textures.

Parameters

NameTypeDescription
window&mut WindowThe window whose atlas holds the textures.
cx&mut Context<Self>Entity context.

Returns — nothing. Idempotent (the slots are emptied). The view is unusable for display afterwards only in the sense that all pages re-rasterize if it renders again.

pub fn detach_textures(&mut self, window: &mut Window, cx: &mut Context<Self>)

Free the viewer’s GPU textures in window but keep its rendered page bitmaps — for a host moving the view to a different window (e.g. a tab drag). The kept bitmaps re-upload wherever the view next paints, so its pages appear there immediately, with scroll, zoom, and (for an encrypted file) the unlocked state intact.

Parameters — same as release. Returns — nothing.

pub fn set_zoom(&mut self, zoom: f32, cx: &mut Context<Self>)

Set the zoom factor, keeping the current page in view.

Parameters

NameTypeDescription
zoomf32Desired factor; clamped to 0.5–3.0. 1.0 = base width (PAGE_WIDTH).
cx&mut Context<Self>Entity context.

Returns — nothing.

Guarantees & edge cases

  • No-op if the clamped value is within 0.001 of the current zoom.
  • The current top page is re-anchored to the viewport top after the layout change.
  • Visible pages re-rasterize crisp at the new scale; their current bitmaps stay on screen (rescaled) until the fresh ones land, so nothing blanks. In-flight renders from the old scale are discarded.
pub fn zoom_in(&mut self, cx: &mut Context<Self>)
pub fn zoom_out(&mut self, cx: &mut Context<Self>)
pub fn reset_zoom(&mut self, cx: &mut Context<Self>)

One multiplicative step (×1.25 / ÷1.25) or back to 100% — all delegate to set_zoom (same clamping and no-blank behavior). Parameterscx only.

pub fn fit_width(&mut self, cx: &mut Context<Self>)

Fit the page column to the viewport width. Sticky: the zoom re-computes on every viewport resize (window/sidebar changes) until a manual zoom — the ± / % controls, ⌘±/⌘0, or set_zoom — takes over. Calling it while already active toggles the mode off. The header shows it as the control, highlighted while active. Parameterscx only.

pub fn fit_page(&mut self, cx: &mut Context<Self>)

Fit the whole current page inside the viewport, both axes — the fit uses that page’s aspect ratio, so mixed-size documents re-fit for the page current at the time of (re-)fitting, not per scrolled page. Sticky and toggling like fit_width; the header control. Parameterscx only.

pub fn go_to_page(&mut self, index: usize, cx: &mut Context<Self>)

Scroll so page index’s top sits at the viewport top.

Parameters

NameTypeDescription
indexusize0-based page; clamped to the last page.
cx&mut Context<Self>Entity context.

Guarantees & edge cases

  • No-op while the document is still loading (no page dims yet).
  • Page 0 scrolls to the true document top (keeping the column’s top padding).
pub fn next_page(&mut self, cx: &mut Context<Self>)
pub fn prev_page(&mut self, cx: &mut Context<Self>)

Step from the current page (the topmost page intersecting the viewport top) via go_to_page; clamped at both ends, no wrap. Parameterscx only.

pub fn toggle_toc(&mut self, cx: &mut Context<Self>)

Toggle the table-of-contents (outline) side panel. The panel only actually shows when has_outline is true; entries with an unresolved destination (named destinations) render muted and inert. Parameterscx only.

pub fn has_outline(&self) -> bool

Whether the document has an outline (bookmarks) — false until the load finishes, and false for PDFs without /Outlines. Use it to hide a TOC control. Parameters — none (&self).

(markup feature)

pub fn set_highlights(&mut self, highlights: Vec<Highlight>, cx: &mut Context<Self>)

Set the highlights to draw. The host derives these from its own store (e.g. the markdown blocks that quote this PDF); the viewer owns no highlight storage.

Parameters

NameTypeDescription
highlightsVec<Highlight>Replaces the current set entirely.
cx&mut Context<Self>Entity context.

Guarantees & edge cases

  • Pages with highlights extract their text layer lazily — off-thread, cached — as they scroll into view; then each quote is located and drawn as a translucent box per line it spans.
  • A quote that isn’t found on its page simply draws nothing (no error).
  • Coordinates are normalized, so highlights track zoom and DPI for free.

(markup feature)

pub fn set_on_highlight(&mut self, handler: HighlightClickFn)

Set the handler invoked with a highlight’s id when it’s clicked (e.g. to jump to the source note). Replaces any previous handler; unset means clicks do nothing.

Parameters

NameTypeDescription
handlerHighlightClickFnRc<dyn Fn(u64, &mut Window, &mut App)> — receives the clicked Highlight.id.

(markup feature)

pub fn set_on_create_highlight(&mut self, handler: CreateHighlightFn)

Set the handler invoked when a drag-selection finishes in highlight mode. Without one, selections resolve but nothing is stored.

Parameters

NameTypeDescription
handlerCreateHighlightFnReceives (page, quote, occurrence, color_label, &mut Window, &mut App).

Guarantees & edge cases

  • Only fires for a real drag: a bare click or tiny jitter (< 0.005 of the page in both axes) never creates a highlight.
  • The quote is a single-line join of the selected text; occurrence disambiguates a repeated quote so it re-locates to the right match; color_label is the label of the active palette swatch (empty if no palette was set).

(markup feature)

pub fn toggle_select_mode(&mut self, cx: &mut Context<Self>)

Toggle “highlight mode”: when on, dragging over text selects it and fires the create handler; the color picker pops down (if a palette is set). Turning it off cancels any in-progress selection and hides the picker. While on, every visible page extracts its text layer so drags can select anywhere. Also bound to ⌘⇧H. Parameterscx only.

(markup feature)

pub fn toggle_area_mode(&mut self, cx: &mut Context<Self>)

Toggle “area mode”: like highlight mode, but a drag marks a rectangular page region (a figure, a scanned paragraph) instead of selecting text, firing CreateAreaFn on release — no text layer involved, so it works on pages with none. The header shows it as the tool beside the pen; they share the color picker, and turning either mode on turns the other off. Parameterscx only.

(markup feature)

pub fn set_on_create_area(&mut self, handler: CreateAreaFn, cx: &mut Context<Self>)

Set the handler invoked when an area (box) drag finishes. Without one, the drag draws feedback but nothing is stored.

Parameters

NameTypeDescription
handlerCreateAreaFnReceives (page, rect, color_label, &mut Window, &mut App); rect is normalized page coords.
cx&mut Context<Self>

Guarantees & edge cases

  • The same minimum-drag threshold as text highlights applies (a bare click never creates one).
  • The rect is normalized against the page the drag started on; a drag that leaves the page clamps to the last position over it.

(markup feature)

pub fn set_highlight_palette(
&mut self,
palette: Vec<(SharedString, Hsla)>,
cx: &mut Context<Self>,
)

Set the highlight colors the picker offers, as (label, fill) pairs.

Parameters

NameTypeDescription
paletteVec<(SharedString, Hsla)>Swatches, in display order. The label is opaque to the viewer — echoed back via CreateHighlightFn so the host can store it and map it back to a fill for set_highlights.
cx&mut Context<Self>Entity context.

Guarantees & edge cases

  • If the active index falls outside the new palette, it resets to 0.
  • Empty palette → no picker; new highlights select with a default yellow and an empty label.

(markup feature)

pub fn reveal_highlight(&mut self, page: usize, cx: &mut Context<Self>)

Jump to a highlight from its note: scroll page into view and briefly flash the page’s highlights (brighter fill + outline for ~1.2 s) so the eye finds them.

Parameters

NameTypeDescription
pageusize0-based page; clamped to the last page.
cx&mut Context<Self>Entity context.

Guarantees & edge cases

  • Called before the document finishes loading → the jump is queued and applied once the document is measured.
  • If the page’s text layer is already extracted, the scroll lands the page’s first highlight just below the viewport top; otherwise it lands on the page top.
  • A newer reveal supersedes an in-flight flash (no early clear).

(search feature)

pub fn toggle_search(&mut self, cx: &mut Context<Self>)

Toggle the find-in-PDF bar (also bound to ⌘F). Opening kicks off text extraction for every page of the document (off-thread, cached — subsequent opens are instant) and computes matches for the current query; the match list refreshes once the last page’s text lands. Closing clears the matches. The query is typed directly into the bar (the viewer captures keystrokes while it’s open); Enter / ⇧Enter and ⌘G / ⌘⇧G step matches. A fresh query starts from the page currently being read, not the document top. Parameterscx only.

(search feature)

pub fn close_search(&mut self, cx: &mut Context<Self>)

Close the find bar and clear the matches (the query text is kept for the next open; extracted page text stays cached). Also bound to Esc while the bar is open. Parameterscx only.

(search feature)

pub fn next_match(&mut self, cx: &mut Context<Self>)
pub fn prev_match(&mut self, cx: &mut Context<Self>)

Focus the next / previous match in reading order (page, then top-to-bottom), wrapping at the ends, and scroll it into view — but only if it isn’t already comfortably visible, so stepping doesn’t yank the page around. No-op when there are no matches. Parameterscx only.


pub struct OutlineItem {
pub title: String, // the bookmark label
pub level: usize, // nesting depth, 0 = top level
pub page: Option<usize>, // 0-based target page, or None if unresolved
}

One entry in a PDF’s outline (bookmarks), flattened depth-first by outline. page is None when the destination couldn’t be resolved — currently that’s named destinations (the title still shows; PdfView renders these muted and inert). Clone + Debug + PartialEq + Eq.


pub enum LinkTarget {
Page(usize), // a 0-based page index within this document
Uri(String), // an external URI
}

Where a clickable PDF link points. Clone + Debug + PartialEq + Eq.


pub struct PdfLink {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
pub target: LinkTarget,
}

A clickable /Link annotation: its rectangle in normalized page coordinates (0..1 of the crop box, top-left origin — matching the rendered image, so multiply by the on-screen page size to overlay it) and its target. All components are clamped to 0..1. Clone + Debug + PartialEq.


pub fn outline(doc: &Document) -> Vec<OutlineItem>

Extract the document outline (bookmarks), flattened depth-first.

Parameters

NameTypeDescription
doc&DocumentA parsed PDF.

ReturnsVec<OutlineItem> in depth-first document order; empty when the PDF has no /Outlines.

Guarantees & edge cases

  • Destinations given as an explicit [pageRef /XYZ …] array — directly in /Dest or inside an /A GoTo action — resolve to a page index; named destinations are left unresolved (page: None).
  • Titles decode as UTF-16BE when they carry the BOM, otherwise as Latin-1 (a close-enough stand-in for PDFDocEncoding); leading/trailing whitespace trimmed.
  • Malformed or hostile trees can’t hang or OOM: a visited-set breaks cycles, and hard caps bound the walk (10 000 items, depth 32).
  • Never panics.

Cost & threading — walks the outline and the page tree (to map page object refs to indices); no rasterization. Pure and thread-safe; PdfView runs it once in the off-thread load.


pub fn page_links(doc: &Document) -> Vec<Vec<PdfLink>>

Extract the clickable /Link annotations for every page.

Parameters

NameTypeDescription
doc&DocumentA parsed PDF.

Returns — one Vec<PdfLink> per page, indexed by page; pages with no links get an empty vec (the outer vec always has exactly one entry per page).

Guarantees & edge cases

  • Handles /Dest explicit destination arrays and /A actions of type /URI (external) and /GoTo (internal). Other action types, named destinations, and empty URIs are skipped.
  • Rotated pages return an empty vec (their annotation rectangles would need rotating to line up with the render); so do degenerate (zero-area) crop boxes.
  • /Rect is converted from PDF user space (bottom-left origin) to the normalized top-left-origin crop-box coordinates of PdfLink.
  • Never panics.

Cost & threading — one pass over each page’s annotation array plus a page-tree walk; no rasterization. Pure and thread-safe.


pub type OpenExternalFn = Rc<dyn Fn(&mut Window, &mut gpui::App)>;

Invoked from the load-failure pane’s “Open in system viewer” button (see set_on_open_external).


pub enum FitMode { Width, Page }

The two zoom-to-fit modes (see fit_width / fit_page). Copy, Clone, PartialEq, Eq, Debug.


(markup feature)

pub struct Highlight {
pub id: u64, // host identifier, echoed back on click
pub page: usize, // 0-based page the quote is on
pub quote: String, // the text to locate (case-/whitespace-insensitive)
pub occurrence: usize, // which occurrence on the page (0-based)
pub color: Hsla, // fill color; drawn translucent (alpha overridden)
pub region: Option<NormRect>, // area highlight: drawn at this rect, no text layer
}

A highlight to draw on the PDF. Hand these to PdfView::set_highlights. A quote highlight (region: None) is located via the text layer and draws a translucent box over each line it spans; an area highlight (region: Some(rect)) draws one box at its stored rect directly — no text layer, so it works on scans and figures, and quote/occurrence are not used for locating. Both draw color at alpha 0.35 normally, 0.6 while flashing. Clone.


(markup feature)

pub type HighlightClickFn = Rc<dyn Fn(u64, &mut Window, &mut gpui::App)>;

Invoked with a Highlight’s id when the user clicks it. Install via PdfView::set_on_highlight.


(markup feature)

pub type CreateHighlightFn =
Rc<dyn Fn(usize, String, usize, SharedString, &mut Window, &mut gpui::App)>;

Invoked when the user finishes a drag-selection in highlight mode, with (page, quote, occurrence, color_label, window, app): the 0-based page, the selected one-line quote, which occurrence of it on the page (so it re-locates unambiguously), and the label of the picked palette color (the opaque tag from set_highlight_palette, for the host to store). Install via PdfView::set_on_create_highlight.


(markup feature)

pub type CreateAreaFn = Rc<dyn Fn(usize, NormRect, SharedString, &mut Window, &mut App)>;

Invoked when a box-drag finishes in area mode: the page (0-based), the dragged rect in normalized page coordinates, and the active palette color’s label. The host stores it and hands it back as a Highlight with region set.


(markup feature)

pub struct NormRect {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}

A rectangle in normalized page coordinates: each component is a fraction (0..1) of the page’s width/height, origin at the top-left. Resolution- and zoom-independent — multiply by the on-screen page rect at paint time. Clone + Copy + Debug + PartialEq.


(markup feature)

pub struct NormPoint {
pub x: f32,
pub y: f32,
}

A point in normalized page coordinates (0..1 of width/height, top-left origin). Clone + Copy + Debug + PartialEq.


(markup feature)

pub struct Selection {
pub quote: String, // the selected text, as a single-line quote
pub occurrence: usize, // which occurrence of that quote on the page
pub rects: Vec<NormRect>,// one rect per line, to draw while selecting
}

The result of a drag selection (PageText::select). The quote is single-spaced and trimmed (line breaks and gaps become single spaces), so it stores cleanly and re-locates with the whitespace-insensitive matcher. Clone + Debug.


(markup feature)

pub fn extract_page_text(doc: &Document, index: usize) -> Option<PageText>

Extract the text layer of one page by running a non-rasterizing hayro interpret pass with a glyph-collecting device — no heavyweight PDF library, only kurbo geometry.

Parameters

NameTypeDescription
doc&DocumentA parsed PDF.
indexusize0-based page index.

ReturnsSome(PageText), or None if the page doesn’t exist. A page that exists but has no text still returns Some (check is_empty).

Guarantees & edge cases

  • Records all glyphs, including invisible ones — that’s the searchable OCR text layer over scanned page images. Glyphs with no unicode mapping are skipped (they can’t match a quote).
  • The PDF’s own whitespace glyphs are kept (normalized to one space) as word boundaries, so a font’s intra-word letter-spacing isn’t mistaken for a space.
  • Never panics.

Cost & threading — cheaper than rendering, but it still parses and interprets the whole page: run it off-thread and cache the result (PdfView does both). Pure and thread-safe.


(markup feature)

pub struct PageText { /* private */ }

A page’s extracted text: the glyph runs in draw order, plus a lowercased, whitespace-stripped index for robust quote matching. Built by extract_page_text. All coordinates in and out are normalized (0..1, top-left origin).

pub fn is_empty(&self) -> bool

Whether the page has any extractable text — true for pure scans with no OCR layer (the host should then fall back to area markup). Parameters — none (&self).

pub fn text(&self) -> String

A readable reconstruction of the page text: spaces inserted on horizontal gaps, newlines on baseline changes. For search or display — locate uses the whitespace-insensitive index instead, so don’t feed this back into it expecting exact offsets. Parameters — none (&self). Empty string for an empty page.

pub fn locate(&self, needle: &str, occurrence: usize) -> Vec<NormRect>

Locate a quote on the page.

Parameters

NameTypeDescription
needle&strThe quote. Matched case- and whitespace-insensitively (both sides are lowercased with all whitespace removed), so a quote survives PDF spacing quirks.
occurrenceusizeWhich match to return, 0-based.

Returns — one NormRect per line the match spans (a wrapped quote highlights as multiple line boxes), or empty if the quote (or that occurrence of it) isn’t on the page.

Guarantees & edge cases

  • Empty or all-whitespace needle → empty vec.
  • Occurrences are counted with overlapping starts (successive scans begin one byte after the previous hit).
  • Runs on the same baseline merge into one rect per line; lines are returned top-to-bottom. Never panics (multi-byte characters are handled byte-aligned).

(search feature)

pub fn find_matches(&self, needle: &str) -> Vec<Vec<NormRect>>

Every non-overlapping case- and whitespace-insensitive match of needle on the page, each as one rect per line it spans — the building block for find-in-PDF.

Parameters

NameTypeDescription
needle&strThe query; same normalization as locate.

Returns — matches in reading order; empty for an empty query or no hits.

pub fn select(&self, from: NormPoint, to: NormPoint) -> Option<Selection>

Resolve a drag from from to to into a Selection: the run range between the glyphs nearest each endpoint (draw order ≈ reading order), its one-line quote, the occurrence index, and the rects to draw.

Parameters

NameTypeDescription
fromNormPointDrag start, normalized page coords.
toNormPointDrag end. Order doesn’t matter — endpoints are sorted.

ReturnsSome(Selection), or None if the page has no text or the selection resolves to only whitespace.

Guarantees & edge cases

  • “Nearest” has no distance limit: a drag in the page margin still snaps to the closest glyphs. The caller decides what counts as a deliberate drag (PdfView requires ≥ 0.005 of the page in either axis).
  • The quote is single-spaced and trimmed; occurrence counts earlier identical quotes on the page, so the stored highlight re-locates to the right match.