Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c3e4502
Add citation/reference hover preview
koppor May 6, 2026
0334fbc
Add wheel-zoom to citation hover popup
koppor May 7, 2026
232e061
Merge branch 'master' into add-reference-hovering
koppor May 7, 2026
77b017f
Use fixed-ratio popup for non-reference hover targets
koppor May 7, 2026
200e9b8
Merge branch 'master' into add-reference-hovering
koppor May 7, 2026
577c69f
Loosen continuation-indent tolerance in citation detection
koppor May 8, 2026
3a8facb
Drop overly-aggressive 'external content' termination rule
koppor May 8, 2026
3d9e269
Use page-half landscape view for non-reference hover targets
koppor May 8, 2026
5c94e9c
Keep fitted box for single-line description-list bib entries
koppor May 8, 2026
5915fb5
Handle figures, headings, code listings, and abbreviation links in hover
koppor May 8, 2026
450add4
Merge branch 'master' into add-reference-hovering
koppor May 8, 2026
93ab2c6
Tighter popup geometry and figure/caption-aware region detection
koppor May 11, 2026
576c23a
Scroll popup with mouse wheel; Ctrl+wheel zooms
koppor May 12, 2026
eebf044
Robust bracket-entry detection, German labels, figure-body popups
koppor May 13, 2026
929de36
Hyphenated multi-line captions; bracket-label baseline tolerance
koppor May 13, 2026
c76ea35
Handle malformed /XYZ dests and sparse-text dest pages in hover preview
koppor May 14, 2026
bcdc920
Fix copyright year in RefHover.* (2024 -> 2026)
koppor May 14, 2026
ca594fe
Merge remote-tracking branch 'origin/master' into add-reference-hovering
koppor May 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions cmd/gen-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,15 @@ const globalPrefs: Field[] = [
),
setVersion(mkField("ScrollbarInSinglePage", Bool, false, "if true, we show scrollbar in single page mode"), "3.6"),
setVersion(mkField("SmoothScroll", Bool, false, "if true, implements smooth scrolling"), "3.6"),
setVersion(
mkField(
"EnableCitationHover",
Bool,
true,
"if true, hovering an internal-document link shows a popup rendering the destination region (citation entry, figure, footnote)",
),
"3.7",
),
setVersion(
mkField(
"FastScrollOverScrollbar",
Expand Down
5 changes: 5 additions & 0 deletions docs/md/Advanced-options-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@ ScrollbarInSinglePage = false
; if true, implements smooth scrolling (introduced in version 3.6)
SmoothScroll = false

; if true, hovering an internal-document link shows a popup rendering the
; destination region (citation entry, figure, footnote) (introduced in version
; 3.7)
EnableCitationHover = true

; if true, mouse wheel scrolling is faster when mouse is over a scrollbar
; (introduced in version 3.6)
FastScrollOverScrollbar = false
Expand Down
1 change: 1 addition & 0 deletions docs/md/Version-history.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Available in [pre-release](https://www.sumatrapdfreader.org/prerelease) builds.
- fix Edit Annotations window not restoring to the correct monitor in multi-monitor setups
- use `GetFileAttributesEx` instead of opening files for change detection on network drives, avoiding Windows Defender re-scans
- fix toolbar page number misalignment when `PrinterAccess` is revoked in `sumatrapdfrestrict.ini`
- add citation/reference hover preview: hovering an internal-document link (e.g. a `[1]` citation, figure reference, or footnote marker) now shows a small popup rendering the destination region, so you can see the bibliography entry / figure / footnote without leaving the current page. Toggle with the `EnableCitationHover` advanced setting (fixes [#128](https://github.com/sumatrapdfreader/sumatrapdf/issues/128), [#4221](https://github.com/sumatrapdfreader/sumatrapdf/issues/4221))

## 3.6.1 (2026-04-06)

Expand Down
87 changes: 87 additions & 0 deletions src/Canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@
#include "Toolbar.h"
#include "Translations.h"

#include "RefHover.h"

#include "utils/Log.h"

// if set instead of trying to render pages we don't have, we simply do nothing
Expand Down Expand Up @@ -793,6 +795,24 @@ static bool gShowAnnotationNotification = true;
// Forward declaration
static RectF CalculateResizedRect(MainWindow* win, int x, int y);

// Returns true when el is an internal-document link (not an external URL or
// file launch). Used as a heuristic for "this is probably a citation link".
static bool IsCitationLink(IPageElement* el) {
if (!el || !el->Is(kindPageElementDest)) {
return false;
}
IPageDestination* dest = el->AsLink();
if (!dest) {
return false;
}
Kind k = dest->GetKind();
if (k == kindDestinationLaunchURL || k == kindDestinationLaunchFile) {
return false;
}
int destPage = PageDestGetPageNo(dest);
return destPage > 0;
}

static void OnMouseMove(MainWindow* win, int x, int y, WPARAM) {
DisplayModel* dm = win->AsFixed();
// ReportIf(!dm); // can happen if reload fails, we delete DisplayModel
Expand Down Expand Up @@ -902,6 +922,39 @@ static void OnMouseMove(MainWindow* win, int x, int y, WPARAM) {
RemoveNotificationsForGroup(win->hwndCanvas, kNotifAnnotation);
}
win->annotationUnderCursor = annot;

// Citation hover: render the destination region of an internal
// link (typically the bibliography entry) into a popup.
if (gGlobalPrefs->enableCitationHover) {
if (!win->refHover) {
win->refHover = RefHoverCreate(win->hwndCanvas);
}
IPageElement* el = dm->GetElementAtPos(pos, nullptr);
if (win->refHover && IsCitationLink(el)) {
IPageDestination* dest = el->AsLink();
int destPage = PageDestGetPageNo(dest);
RectF destPt = PageDestGetDestPoint(dest);
Point screenPt = {x, y};
ClientToScreen(win->hwndCanvas, (POINT*)&screenPt);
int srcPage = el->GetPageNo();
RectF srcRect = el->GetRect();
Rect pageScreenRect{};
PageInfo* pi = (srcPage > 0) ? dm->GetPageInfo(srcPage) : nullptr;
if (pi && !pi->pageOnScreen.IsEmpty()) {
pageScreenRect = pi->pageOnScreen;
POINT topLeft = {pageScreenRect.x, pageScreenRect.y};
ClientToScreen(win->hwndCanvas, &topLeft);
pageScreenRect.x = topLeft.x;
pageScreenRect.y = topLeft.y;
}
RefHoverSchedule(win->refHover, win->hwndCanvas, screenPt, destPage, destPt.x, destPt.y, srcPage,
srcRect, pageScreenRect);
} else if (win->refHover) {
RefHoverHide(win->refHover, win->hwndCanvas);
}
} else if (win->refHover) {
RefHoverHide(win->refHover, win->hwndCanvas);
}
break;
}

Expand Down Expand Up @@ -2093,6 +2146,29 @@ static LRESULT CanvasOnMouseWheel(MainWindow* win, UINT msg, WPARAM wp, LPARAM l
return res;
}

// Mouse-wheel on the citation-hover popup (cursor still on the citation
// link that opened it). Avoids moving the cursor onto the popup itself,
// which would dismiss the hover.
// plain wheel → scroll popup content (rolls over to prev/next page)
// ctrl+wheel → zoom popup content
if (win->refHover && win->refHover->hwndPopup && IsWindowVisible(win->refHover->hwndPopup)) {
DisplayModel* dmHover = win->AsFixed();
if (dmHover) {
Point pt = HwndGetCursorPos(win->hwndCanvas);
IPageElement* elHover = dmHover->GetElementAtPos(pt, nullptr);
if (IsCitationLink(elHover)) {
short delta = GET_WHEEL_DELTA_WPARAM(wp);
bool isCtrl = (LOWORD(wp) & MK_CONTROL) || IsCtrlPressed();
if (isCtrl) {
RefHoverWheelZoom(win->refHover, dmHover->GetEngine(), delta);
} else {
RefHoverWheelScroll(win->refHover, dmHover->GetEngine(), delta);
}
return 0;
}
}
}

DisplayModel* dm = win->AsFixed();

// Note: not all mouse drivers correctly report the Ctrl key's state
Expand Down Expand Up @@ -2848,6 +2924,17 @@ static void OnTimer(MainWindow* win, HWND hwnd, WPARAM timerId) {
}
break;

case kRefHoverTimerID: {
DisplayModel* dm = win->AsFixed();
EngineBase* engine = dm ? dm->GetEngine() : nullptr;
float pageZoom = 1.f;
if (dm && win->refHover && win->refHover->pendingDestPage > 0) {
pageZoom = dm->GetZoomReal(win->refHover->pendingDestPage);
}
RefHoverOnTimer(win->refHover, hwnd, engine, pageZoom);
break;
}

case HIDE_FWDSRCHMARK_TIMER_ID:
win->fwdSearchMark.hideStep++;
if (1 == win->fwdSearchMark.hideStep) {
Expand Down
12 changes: 12 additions & 0 deletions src/EngineBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ struct IPageDestination : KindBase {
virtual RectF GetRect2() { return rect; }
// optional zoom level on the above returned page
virtual float GetZoom2() { return zoom; }
// anchor point (x, y) on the destination page; rect's dx/dy may be 0.
// Default falls back to GetRect2 (callers should still tolerate (0,0)).
virtual RectF GetDestPoint2() { return GetRect2(); }

// string value associated with the destination (e.g. a path or a URL)
virtual char* GetValue2() { return nullptr; }
Expand Down Expand Up @@ -119,6 +122,15 @@ static inline RectF PageDestGetRect(IPageDestination* dest) {
return dest->GetRect2();
}

// anchor point on the destination page (x, y in user-space). Returns {0,0,0,0}
// when the destination has no specific anchor.
static inline RectF PageDestGetDestPoint(IPageDestination* dest) {
if (!dest) {
return {};
}
return dest->GetDestPoint2();
}

// optional zoom level on the above returned page
static inline float PageDestGetZoom(IPageDestination* dest) {
return dest->GetZoom2();
Expand Down
23 changes: 22 additions & 1 deletion src/EngineMupdf.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,11 @@ struct PageDestinationMupdf : IPageDestination {
char* value = nullptr;
char* name = nullptr;

// anchor (x, y) on the destination page resolved from the link URI;
// -1 means "not resolved" (e.g. external URL or file launch).
float destX = -1.f;
float destY = -1.f;

PageDestinationMupdf(fz_link* l, fz_outline* o) {
// exactly one must be provided
kind = kindDestinationMupdf;
Expand All @@ -107,6 +112,16 @@ struct PageDestinationMupdf : IPageDestination {
}
return rect;
}

RectF GetDestPoint2() override {
if (outline) {
return RectF{outline->x, outline->y, 0, 0};
}
if (destY >= 0.f) {
return RectF{destX, destY, 0, 0};
}
return {};
}
~PageDestinationMupdf() override {
str::Free(value);
str::Free(name);
Expand Down Expand Up @@ -223,7 +238,13 @@ static IPageDestination* NewPageDestinationMupdf(fz_context* ctx, fz_document* d

auto dest = new PageDestinationMupdf(link, outline);
dest->rect = FzGetRectF(link, outline);
dest->pageNo = FzGetPageNo(ctx, doc, link, outline);
{
float x = 0, y = 0;
const char* destUri = link ? link->uri : (outline ? outline->uri : nullptr);
dest->pageNo = ResolveLink(ctx, doc, destUri, &x, &y);
dest->destX = x;
dest->destY = y;
}
return dest;
}

Expand Down
2 changes: 2 additions & 0 deletions src/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include "OverlayScrollbar.h"
#include "SumatraPDF.h"
#include "MainWindow.h"
#include "RefHover.h"
#include "WindowTab.h"
#include "TableOfContents.h"
#include "resource.h"
Expand Down Expand Up @@ -109,6 +110,7 @@ void CreateMovePatternLazy(MainWindow* win) {

MainWindow::~MainWindow() {
KillTimer(hwndCanvas, kSmoothScrollTimerID);
RefHoverDestroy(refHover);
FinishStressTest(this);

ReportIf(TabCount() > 0);
Expand Down
2 changes: 2 additions & 0 deletions src/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ struct WindowTab;

struct Annotation;
struct ILinkHandler;
struct RefHoverState;

// Current action being performed with a mouse
enum class MouseAction {
Expand Down Expand Up @@ -268,6 +269,7 @@ struct MainWindow {
IPageElement* linkOnLastButtonDown = nullptr;
AutoFreeStr urlOnLastButtonDown;
Annotation* annotationUnderCursor = nullptr;
RefHoverState* refHover = nullptr;
// highlight rectangle for element under cursor during context menu (in page coordinates)
RectF contextMenuHighlightRect{};
int contextMenuHighlightPageNo = 0;
Expand Down
Loading