Skip to content

Links with highlight#153

Open
tuncbkose wants to merge 1 commit into
itsjunetime:mainfrom
tuncbkose:tbk/links3
Open

Links with highlight#153
tuncbkose wants to merge 1 commit into
itsjunetime:mainfrom
tuncbkose:tbk/links3

Conversation

@tuncbkose

@tuncbkose tuncbkose commented Jun 6, 2026

Copy link
Copy Markdown

Hello. This is yet another attempt at implementing link support (#81).

I was playing around with this independently (and made a bad implementation) when I realized that @perhurt had a nice looking but slightly outdated PR at #95. This PR is a combination of #95 and me implementing highlighting instead of the links menu. I would like to note that I didn't built on top of that branch directly as to avoid having to rebase but if this PR may get merged, @perhurt should get credits as well. I can try to include them in the git history if we get there.

I am aware of at least 3 issues:

  • if there are multiple pages visible, cycling between links doesn't cross page boundaries
  • the link ordering from mu-pdf may be a bit weird. At least, I had 2 documents I tried with, one that I compiled from typst and the example pdf from clickable PDF links; added webbrowser, query link handling, and click… #135. The former was fine but the latter had a unexpected ordering.
  • mu-pdf seems to split one link into multiple if it crosses line boundaries. I've implemented some logic to detect that and combine them so the entire link would be highlighted but currently the logic is incomplete because I couldn't find any margin information (see renderer.rs::extract_page_links).

I am also not very experienced with Rust so any feedback is welcome.

I will try to look into the issues along with adding mouse support like #135, but if someone else built upon this (or implemented this feature independently) I would be very happy as well.

@itsjunetime itsjunetime left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally I really like this; thanks so much for submitting it. I haven't tested it yet, but if you're willing to clean it up a bit and we can resolve the issue with page margins, I'd love to get this merged in.

Comment thread src/renderer.rs
// check if it contains the current term to see if it needs a re-render
if rendered.successful && rendered.num_search_found.is_some() {
if rendered.successful && rendered.num_search_found.is_some()
&& !(page_num == start_point && link_highlight_changed){

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I follow why we would want to not continue if page_num == start_point && link_highlight_changed - could you add an explanation to the comment?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that if !link_highlight_changed, then there is no reason to re-render it and that link highlighting should be limited to visible pages, thus no need to look at page_num != start_point.

That said I am certain this will need correcting when I fix displaying 2 pages (I'll also have to check if one can display more than 2 pages, I look almost exclusively at 1 page docs).

Comment thread src/renderer.rs
fill_default::<PrevRender>(&mut rendered, n_pages.get());
let mut start_point = 0;

// todo there must be a better way to do this

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I think it works totally fine for now. It's a simple bool that seems to encapsulate all you care about.

Comment thread src/renderer.rs
scale_factor: f32,
highlighted_link: Option<usize>,
) -> Result<Vec<Link>, mupdf::error::Error> {
let link_groups = page.links()?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could do with a comment on what link_groups represents - I assume it's a Vec with each item being, effectively, one link split across multiple Link items to represent that it exists on multiple pages at once?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See next comment

Comment thread src/renderer.rs
Comment on lines +656 to +662
// but something like this is needed in case there are two separate links with the same uri
// across lines
let is_same = prev_link.uri == link.uri
&& prev_link.dest == link.dest
&& prev_link.bounds.y1 == link.bounds.y0;
// && prev_link.bounds.x1 == bounds.x1
// && link.bounds.x0 == bounds.x0;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give more detail on what exactly the problem is (with page margins; why are they weird right now?) and how we might be able to fix this? I don't think I fully understand how page margins could be getting in the way right now so I'm not quite sure what the problem exactly looks like or what the solution might be.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mupdf::Link is a rectangle + some destination. So in a document such as

.... li-\n
nk ...

mupdf creates two Link objects, one for each line.

As a user, my preference would be that if I cycle the highlight to this link, the entire link should be highlighted at once, instead of it being separated into two (or more if for some reason it is long enough to span more than 2 lines). Consequently, we need to detect whether two Link objects actually belong to the same link (which I termed link_group above but it can probably have a better name + clearer comments).

To check if they are the same link, then need to point at the same destination and their bounds (let's call them r0 and r1) should be "contiguous". A naive definition of contiguous here would be that r0.x1 is at the line end, r1 is right below r0 (r1.y0 is equal to r0.y1) and r1.x0 is at the line start (and similar if the link spans >2 lines). Here, line start and end are controlled by page margins etc on a quick search, I wasn't sure how one could determine this.

More generally, this naive definition will not work if a link is spanning columns (or pages when 2 pages are displayed, but that may be more acceptable). So I was hoping to look at mupdf more to see if there is a better way to do this. (Maybe it would be easier in #156?)

Also, this is a problem solely for active highlighting, so if covering all links with a clickable rectangle is enough, this problem would go away. But I imagine a keyboard-compatible solution would be preferable.

Comment thread src/renderer.rs
acc
});

Ok(link_groups

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're just gonna re-iterate over link_groups, we can probably avoid constructing all those intermediate vecs, right?

Comment thread src/tui.rs
match bottom_msg {
BottomMessage::Help => {
// Create spans with different colors for help message and links info
let mut spans = vec![Span::styled(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very small nit: I think it would probably be nice to keep the help suggestion in the bottom-right corner so that it doesn't move around a bunch, and just have the link text appear to its left.

Comment thread src/tui.rs
Comment on lines +897 to +905
'c' => highlighted_link.inspect(|&l| {
let link = &current_page_links[l];
// Copy to clipboard using copypasta
use copypasta::{ClipboardContext, ClipboardProvider};
if let LinkDestination::URI(ref uri) = link.dest
&& let Ok(mut ctx) = ClipboardContext::new() {
let _ = ctx.set_contents(uri.clone());
}
}).and(None),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this feels smart and fun but I think I would still appreciate a simple

'c' => {
	if let Some(ref l) = highlighted_link { ... }
	None
}

:)

Comment thread src/tui.rs
}
}
},
key @ (KeyCode::Tab | KeyCode::BackTab) => {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I have BackTab on my keyboard - is that maybe synthesized from shift+tab? If not, I feel like it would be nice to add that as a way to go back 1 link.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe one needs to use KeyCode::BackTab over Tab + checking for Shift modifier and in my quick testing it gave the exact behavior you describe.

Comment thread src/tui.rs
(None, _) => Some(InputAction::HighlightLink(Some(0))), // No link is highlighted yet, highlight the first one
(Some(l), KeyCode::Tab) => Some(InputAction::HighlightLink(Some((l + 1) % n_links))),
(Some(l), KeyCode::BackTab) => Some(InputAction::HighlightLink(Some((l + n_links - 1) % n_links))),
_ => unreachable!() // key can't be anything else

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is accurate and will work fine. But every unreachable makes me a bit uncomfortable ... I can't think of a clean way to remove this but I just want to mark it to force myself to think about it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand that, but don't have an immediate idea how to change it either.

@tuncbkose tuncbkose left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for the comments! I've written some explanations but I won't be able to do a second iteration of this MR until next week probably. Hope that is not too annoying.

I will keep an eye on #156 in case that may make handling links easier as well.

Comment thread src/renderer.rs
// check if it contains the current term to see if it needs a re-render
if rendered.successful && rendered.num_search_found.is_some() {
if rendered.successful && rendered.num_search_found.is_some()
&& !(page_num == start_point && link_highlight_changed){

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that if !link_highlight_changed, then there is no reason to re-render it and that link highlighting should be limited to visible pages, thus no need to look at page_num != start_point.

That said I am certain this will need correcting when I fix displaying 2 pages (I'll also have to check if one can display more than 2 pages, I look almost exclusively at 1 page docs).

Comment thread src/renderer.rs
Comment on lines +656 to +662
// but something like this is needed in case there are two separate links with the same uri
// across lines
let is_same = prev_link.uri == link.uri
&& prev_link.dest == link.dest
&& prev_link.bounds.y1 == link.bounds.y0;
// && prev_link.bounds.x1 == bounds.x1
// && link.bounds.x0 == bounds.x0;

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A mupdf::Link is a rectangle + some destination. So in a document such as

.... li-\n
nk ...

mupdf creates two Link objects, one for each line.

As a user, my preference would be that if I cycle the highlight to this link, the entire link should be highlighted at once, instead of it being separated into two (or more if for some reason it is long enough to span more than 2 lines). Consequently, we need to detect whether two Link objects actually belong to the same link (which I termed link_group above but it can probably have a better name + clearer comments).

To check if they are the same link, then need to point at the same destination and their bounds (let's call them r0 and r1) should be "contiguous". A naive definition of contiguous here would be that r0.x1 is at the line end, r1 is right below r0 (r1.y0 is equal to r0.y1) and r1.x0 is at the line start (and similar if the link spans >2 lines). Here, line start and end are controlled by page margins etc on a quick search, I wasn't sure how one could determine this.

More generally, this naive definition will not work if a link is spanning columns (or pages when 2 pages are displayed, but that may be more acceptable). So I was hoping to look at mupdf more to see if there is a better way to do this. (Maybe it would be easier in #156?)

Also, this is a problem solely for active highlighting, so if covering all links with a clickable rectangle is enough, this problem would go away. But I imagine a keyboard-compatible solution would be preferable.

Comment thread src/renderer.rs
scale_factor: f32,
highlighted_link: Option<usize>,
) -> Result<Vec<Link>, mupdf::error::Error> {
let link_groups = page.links()?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See next comment

Comment thread src/tui.rs
}
}
},
key @ (KeyCode::Tab | KeyCode::BackTab) => {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe one needs to use KeyCode::BackTab over Tab + checking for Shift modifier and in my quick testing it gave the exact behavior you describe.

Comment thread src/tui.rs
(None, _) => Some(InputAction::HighlightLink(Some(0))), // No link is highlighted yet, highlight the first one
(Some(l), KeyCode::Tab) => Some(InputAction::HighlightLink(Some((l + 1) % n_links))),
(Some(l), KeyCode::BackTab) => Some(InputAction::HighlightLink(Some((l + n_links - 1) % n_links))),
_ => unreachable!() // key can't be anything else

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can understand that, but don't have an immediate idea how to change it either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants