New Features
-
narrow_to_sectioncommand (and Lua API): Narrow the view to a named section from the document outline. With no arguments a picker dialog lists all outline entries (indented to show hierarchy); with an argument the first exact then substring match is used. The narrow region extends from the section's start page through the page where the next non-descendant section begins (inclusive), so overflow content that spills onto the next heading's page is always visible. Works with embedded, generated, and loaded outlines. Also exposed to Lua asview:narrow_to_section(title). Descendant detection uses both depth (properly nested outlines) and title prefix (flat PDF outlines where sub-entries share the parent's depth level). -
narrow_to_pagescommand (and Lua API): Narrow the view to an inclusive range of 1-indexed pages instead of a single-page rubber-banded region. Invoke asnarrow_to_pages 5 10ornarrow_to_pages 5-10; the narrow rect becomes the union of the scene rects of every page in the range, scrolling and rendering focus stay inside the range, andsearchis already page-range aware so it only searches within those pages.widen_regionexits pages-narrow the same way it exits region-narrow. Also exposed to Lua asview:narrow_to_pages(start, end). -
search_belowandsearch_abovecommands (and Lua APIs): Two new directional-search commands scope the search to a page range relative to the current page.search_belowsearches fromm_pagenoforward (inclusive);search_abovesearches from page 0 throughm_pageno(inclusive). Both accept an optional inline term (searches immediately) or open the search bar with the direction primed — Enter then runs the scoped search. The scope is one-shot: after the search is dispatched,DocumentViewreverts to full-document scope so the next plainsearchbehaves normally. Narrow region mode still takes precedence over the directional scope. Also exposed to Lua asview:search_below(query, regex?)andview:search_above(query, regex?). -
Narrowed search restricts hits to the narrowed page and region: With a narrow region active,
searchnow runs only on the narrow page (via a newpageToparameter onModel::search) instead of scanning the entire document. Results are then post-filtered inDocumentView::handleSearchResults/handlePartialSearchResultsto drop any hit whose quad center falls outside the narrow rect. The search count reported to the search bar reflects the filtered hits so navigation index/total stays consistent. -
open_confignow handles bothconfig.tomlandinit.lua: The command opensconfig.tomlwhen present, falls back toinit.luawhen only the Lua config exists, and shows a chooser dialog when both are present so users can pick which file to edit. -
Narrow-region indicator in the statusbar: When the document is in narrow region mode, an orange N badge appears in the left section of the statusbar. Hovering it shows a tooltip explaining how to exit. The badge is cleared automatically when Wide Region is invoked, and is correctly seeded when switching between tabs.
-
Viewport-clipped rendering at high zoom for major performance improvement: When zoom exceeds 2.5×, the render pipeline now clips the MuPDF pixmap to the visible viewport region plus a 50% margin in each direction, instead of rasterising the entire page. At extreme zoom levels (e.g. 8×) the full page can be 50–100× larger than the viewport; only ~2× the viewport area is now rasterised, giving proportional speedups. The clipped image is drawn at the correct offset within the full-page bounding rect so link, annotation, and search overlays remain correctly positioned. A fresh clip is requested automatically after each scroll or zoom change.
-
Region selection and context menu now available in image mode: Region select mode can now be activated when viewing images (JPEG, PNG, GIF, etc.) via the mode menu or keyboard shortcut. Drawing a rubber band shows the context menu with all compatible actions. The context menu adapts to the file type: "Copy Region as Image (Custom DPI)" is hidden for pure raster images (re-rendering at a different DPI is only meaningful for vector/text-based formats), and "Copy Text from Region" is hidden unless the document supports text extraction.
-
Generate outline from document text: When a PDF has no embedded outline, a heuristic outline can be generated via the
generate_outlinecommand or the "Generate Outline" menu entry. The algorithm scans all pages for text lines whose maximum character font size is at leastoutline.generate_heading_ratiotimes the modal (body) body text size (default 1.2×). Up tooutline.generate_max_levelsdistinct font-size tiers are recognised as heading levels (default 3). The generated outline is shown in the standard outline picker and persists for the lifetime of the document tab — callingShowOutline(picker_outline) will use it automatically if no real outline exists. -
Export outline to JSON file: The
export_outlinecommand opens a save-file dialog and writes the currently active outline (real PDF outline, or generated/loaded synthetic outline) to a human-readable JSON file. Each entry recordstitle,page(1-based),x,y, and achildrenarray for nested outlines. -
Load outline from JSON file: The
load_outlinecommand opens a file dialog, reads a previously exported outline JSON, and immediately displays it in the outline picker. The loaded outline is stored per-document and persists across tab switches for the session — invokingpicker_outlineon a document with no embedded outline will automatically fall back to the loaded one.
Removed
--check-configCLI flag removed: The TOML config validator maintained a hard-coded whitelist of every valid section and key, which drifted out of sync with the actual config schema as options were added. The flag and itscheckConfigFile()implementation have been removed.
Bug Fixes
-
Fix search bar close button leaving search highlights on the page: Clicking the close button in the search bar only called
clearFocus()andhide(), so the last search's highlights and index/count labels stayed active until a new search was started. It now also clears the input and emits an emptysearchRequestedsignal — the same path the input takes when its text is emptied — so highlights are dropped and search mode exits cleanly. -
Fix Visual Line mode not activating when selected via the statusbar mode label: Clicking the mode label to cycle into "Visual Line" only called
GraphicsView::setMode(VisualLine), leavingDocumentView::m_visual_line_modefalse and never runningsnapVisualLine()— so the label updated but the mode did nothing.NextSelectionModenow routes throughset_visual_line_mode(true)when entering VisualLine and tears it down cleanly when cycling out. -
Fix
applyNarrowrestoring text selection mode in image mode: After drawing a narrow region, the mode was restored usingTextSelectionas the hardcoded fallback when the document's default mode wasNone. Image documents haveNoneas their default mode (no text layer), so they always ended up inTextSelectionafter narrowing — a mode that does nothing for images. The fallback now restoresRegionSelectionfor image documents instead. -
Fix narrow region breaking after document rotation or flip: Two bugs compounded. (1) The dim overlay and scrolling-bounds scene rect were never refreshed after the async re-render that follows a rotation — the overlay stayed frozen at the pre-rotation position until the next scroll. Fixed by calling
refreshNarrowVisuals()fromrenderPageFromImage()when the re-rendered page is the narrow page. (2) The narrow region was stored as normalized fractions of the page's width/height. A 90° rotation swaps the page dimensions, so the same fractions mapped to a completely different visual area. Fixed by geometrically remappingm_narrow_local_normalizedbefore each rotation/flip: 90° CW(l, t, w, h)→(t, 1−l−w, h, w); 90° CCW →(1−t−h, l, h, w); FlipH →(1−l−w, t, w, h); FlipV →(l, 1−t−h, w, h). -
Fix text highlight annotation not applied for short adjacent-line selections: In TextHighlight mode, the annotation was only created when the release-time drag distance exceeded 50 units in scene coordinates. Two bugs compounded: (1) the threshold was measured in scene units, so at high zoom
mapToScenecompressed the movement — e.g. dragging across two lines at 4× zoom yielded only ~12 scene units, well below 50; (2) even with a corrected screen-pixel threshold, 50 pixels is too large for a typical one-line-down drag (~25 px). The root fix: TextHighlight mode no longer gates annotation creation on the drag threshold at all. The annotation handler already guards viahasTextSelection(), which is false when no mouse movement occurred (no mousemove emission → no selection built), so accidental single-click highlights are still impossible. Additionally, the drag check for TextSelection mode (visual selection overlay) was switched from scene-coordinate distance to screen-pixel distance so it remains zoom-independent.