Underline hyperlinks only on mouse hover (#3856)

- **PR Description**

Followup to #3825: we decided there that we don't want to underline
links in delta diffs by default, but only on mouse hover. This PR does
that; it makes it possible to decide per view whether links should be
underlined always, or only on hover. We set this to only on hover for
the main views, so that links in diffs are not underlined (also affects
the status view though), but all other links we want to underline always
for better discoverability.
This commit is contained in:
Stefan Haller 2024-08-24 17:49:10 +02:00 committed by GitHub
commit 8a8490d97d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 120 additions and 10 deletions

2
go.mod
View file

@ -16,7 +16,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511
github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e

4
go.sum
View file

@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511 h1:FN3QrzVxV3lM/SdvBCz2lUtfW0VOKLUMHj5xYWdv3Mc=
github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4 h1:2su9wjacqT/WxvNrzzdvA6rBJa6n/yZ/jvaS1r60HfM=
github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=

View file

@ -117,6 +117,7 @@ func (gui *Gui) createAllViews() error {
view.Title = gui.c.Tr.DiffTitle
view.Wrap = true
view.IgnoreCarriageReturns = true
view.UnderlineHyperLinksOnlyOnHover = true
}
gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges

View file

@ -180,6 +180,8 @@ type Gui struct {
suspended bool
taskManager *TaskManager
lastHoverView *View
}
type NewGuiOpts struct {
@ -836,7 +838,7 @@ func (g *Gui) processRemainingEvents() error {
// etc.)
func (g *Gui) handleEvent(ev *GocuiEvent) error {
switch ev.Type {
case eventKey, eventMouse:
case eventKey, eventMouse, eventMouseMove:
return g.onKey(ev)
case eventError:
return ev.Err
@ -1395,6 +1397,19 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
return err
}
case eventMouseMove:
mx, my := ev.MouseX, ev.MouseY
v, err := g.VisibleViewByPosition(mx, my)
if err != nil {
break
}
if g.lastHoverView != nil && g.lastHoverView != v {
g.lastHoverView.lastHoverPosition = nil
g.lastHoverView.hoveredHyperlink = nil
}
g.lastHoverView = v
v.onMouseMove(mx, my)
default:
}

View file

@ -176,6 +176,7 @@ const (
eventKey
eventResize
eventMouse
eventMouseMove // only used when no button is down, otherwise it's eventMouse
eventFocus
eventInterrupt
eventError
@ -387,7 +388,11 @@ func (g *Gui) pollEvent() GocuiEvent {
if !wheeling {
switch dragState {
case NOT_DRAGGING:
return GocuiEvent{Type: eventNone}
return GocuiEvent{
Type: eventMouseMove,
MouseX: x,
MouseY: y,
}
// if we haven't released the left mouse button and we've moved the cursor then we're dragging
case MAYBE_DRAGGING:
if x != lastX || y != lastY {

View file

@ -56,6 +56,13 @@ type View struct {
// tained is true if the viewLines must be updated
tainted bool
// the last position that the mouse was hovering over; nil if the mouse is outside of
// this view, or not hovering over a cell
lastHoverPosition *pos
// the location of the hyperlink that the mouse is currently hovering over; nil if none
hoveredHyperlink *SearchPosition
// internal representation of the view's buffer. We will keep viewLines around
// from a previous render until we explicitly set them to nil, allowing us to
// render the same content twice without flicker. Wherever we want to render
@ -180,6 +187,14 @@ type View struct {
// if true, the user can scroll all the way past the last item until it appears at the top of the view
CanScrollPastBottom bool
// if true, the view will underline hyperlinks only when the cursor is on
// them; otherwise, they will always be underlined
UnderlineHyperLinksOnlyOnHover bool
}
type pos struct {
x, y int
}
// call this in the event of a view resize, or if you want to render new content
@ -188,6 +203,7 @@ type View struct {
func (v *View) clearViewLines() {
v.tainted = true
v.viewLines = nil
v.clearHover()
}
type searcher struct {
@ -532,6 +548,10 @@ func (v *View) setRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
}
}
if v.isHoveredHyperlink(x, y) {
fgColor |= AttrUnderline
}
// Don't display NUL characters
if ch == 0 {
ch = ' '
@ -756,6 +776,7 @@ func (v *View) WriteRunes(p []rune) {
// writeRunes copies slice of runes into internal lines buffer.
func (v *View) writeRunes(p []rune) {
v.tainted = true
v.clearHover()
// Fill with empty cells, if writing outside current view buffer
v.makeWriteable(v.wx, v.wy)
@ -1164,7 +1185,7 @@ func (v *View) draw() error {
if bgColor == ColorDefault {
bgColor = v.BgColor
}
if c.hyperlink != "" {
if c.hyperlink != "" && !v.UnderlineHyperLinksOnlyOnHover {
fgColor |= AttrUnderline
}
@ -1236,6 +1257,15 @@ func (v *View) isPatternMatchedRune(x, y int) (bool, bool) {
return false, false
}
func (v *View) isHoveredHyperlink(x, y int) bool {
if v.UnderlineHyperLinksOnlyOnHover && v.hoveredHyperlink != nil {
adjustedY := y + v.oy
adjustedX := x + v.ox
return adjustedY == v.hoveredHyperlink.Y && adjustedX >= v.hoveredHyperlink.XStart && adjustedX < v.hoveredHyperlink.XEnd
}
return false
}
// realPosition returns the position in the internal buffer corresponding to the
// point (x, y) of the view.
func (v *View) realPosition(vx, vy int) (x, y int, err error) {
@ -1406,6 +1436,7 @@ func (v *View) SetHighlight(y int, on bool) error {
}
v.tainted = true
v.lines[y] = cells
v.clearHover()
return nil
}
@ -1672,8 +1703,12 @@ func (v *View) ScrollUp(amount int) {
amount = v.oy
}
v.oy -= amount
v.cy += amount
if amount != 0 {
v.oy -= amount
v.cy += amount
v.clearHover()
}
}
// ensures we don't scroll past the end of the view's content
@ -1682,6 +1717,8 @@ func (v *View) ScrollDown(amount int) {
if adjustedAmount > 0 {
v.oy += adjustedAmount
v.cy -= adjustedAmount
v.clearHover()
}
}
@ -1690,12 +1727,18 @@ func (v *View) ScrollLeft(amount int) {
if newOx < 0 {
newOx = 0
}
v.ox = newOx
if newOx != v.ox {
v.ox = newOx
v.clearHover()
}
}
// not applying any limits to this
func (v *View) ScrollRight(amount int) {
v.ox += amount
v.clearHover()
}
func (v *View) adjustDownwardScrollAmount(scrollHeight int) int {
@ -1769,3 +1812,49 @@ func containsColoredTextInLine(fgColorStr string, text string, line []cell) bool
return strings.Contains(currentMatch, text)
}
func (v *View) onMouseMove(x int, y int) {
if v.Editable || !v.UnderlineHyperLinksOnlyOnHover {
return
}
// newCx and newCy are relative to the view port, i.e. to the visible area of the view
newCx := x - v.x0 - 1
newCy := y - v.y0 - 1
// newX and newY are relative to the view's content, independent of its scroll position
newX := newCx + v.ox
newY := newCy + v.oy
if newY >= 0 && newY <= len(v.viewLines)-1 && newX >= 0 && newX <= len(v.viewLines[newY].line)-1 {
if v.lastHoverPosition == nil || v.lastHoverPosition.x != newX || v.lastHoverPosition.y != newY {
v.hoveredHyperlink = v.findHyperlinkAt(newX, newY)
}
v.lastHoverPosition = &pos{x: newX, y: newY}
} else {
v.lastHoverPosition = nil
v.hoveredHyperlink = nil
}
}
func (v *View) findHyperlinkAt(x, y int) *SearchPosition {
linkStr := v.viewLines[y].line[x].hyperlink
if linkStr == "" {
return nil
}
xStart := x
for xStart > 0 && v.viewLines[y].line[xStart-1].hyperlink == linkStr {
xStart--
}
xEnd := x + 1
for xEnd < len(v.viewLines[y].line) && v.viewLines[y].line[xEnd].hyperlink == linkStr {
xEnd++
}
return &SearchPosition{XStart: xStart, XEnd: xEnd, Y: y}
}
func (v *View) clearHover() {
v.hoveredHyperlink = nil
v.lastHoverPosition = nil
}

2
vendor/modules.txt vendored
View file

@ -172,7 +172,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/filesystem
github.com/jesseduffield/go-git/v5/utils/merkletrie/index
github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
# github.com/jesseduffield/gocui v0.3.1-0.20240824094505-8cce5f5d2511
# github.com/jesseduffield/gocui v0.3.1-0.20240824154427-0fc91d5098e4
## explicit; go 1.12
github.com/jesseduffield/gocui
# github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10