Skip to content

Commit

Permalink
Merge pull request fyne-io#4320 from dweymouth/fixes/3528
Browse files Browse the repository at this point in the history
Hyperlink: resize underline and focus rectangles to text width; ignore tap outside of text area
  • Loading branch information
dweymouth authored Jan 5, 2024
2 parents 33809ea + fde9a20 commit 484a4bb
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 17 deletions.
10 changes: 7 additions & 3 deletions internal/driver/glfw/window_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,9 +245,13 @@ func TestWindow_Cursor(t *testing.T) {
textCursor := desktop.TextCursor
assert.Equal(t, textCursor, w.cursor)

w.mouseMoved(w.viewport, 10, float64(h.Position().Y+10))
pointerCursor := desktop.PointerCursor
assert.Equal(t, pointerCursor, w.cursor)
/*
// See fyne-io/fyne/issues/4513 - Hyperlink doesn't update its cursor type until
// mouse moves are processed in the event queue
w.mouseMoved(w.viewport, float64(h.Position().X+10), float64(h.Position().Y+10))
pointerCursor := desktop.PointerCursor
assert.Equal(t, pointerCursor, w.cursor)
*/

w.mouseMoved(w.viewport, 10, float64(b.Position().Y+10))
defaultCursor := desktop.DefaultCursor
Expand Down
85 changes: 71 additions & 14 deletions widget/hyperlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type Hyperlink struct {
// Since: 2.2
OnTapped func() `json:"-"`

textSize fyne.Size // updated in syncSegments
focused, hovered bool
provider *RichText
}
Expand Down Expand Up @@ -71,7 +72,10 @@ func (hl *Hyperlink) CreateRenderer() fyne.WidgetRenderer {

// Cursor returns the cursor type of this widget
func (hl *Hyperlink) Cursor() desktop.Cursor {
return desktop.PointerCursor
if hl.hovered {
return desktop.PointerCursor
}
return desktop.DefaultCursor
}

// FocusGained is a hook called by the focus handling logic after this object gained the focus.
Expand All @@ -87,19 +91,58 @@ func (hl *Hyperlink) FocusLost() {
}

// MouseIn is a hook that is called if the mouse pointer enters the element.
func (hl *Hyperlink) MouseIn(*desktop.MouseEvent) {
hl.hovered = true
hl.BaseWidget.Refresh()
func (hl *Hyperlink) MouseIn(e *desktop.MouseEvent) {
hl.MouseMoved(e)
}

// MouseMoved is a hook that is called if the mouse pointer moved over the element.
func (hl *Hyperlink) MouseMoved(*desktop.MouseEvent) {
func (hl *Hyperlink) MouseMoved(e *desktop.MouseEvent) {
oldHovered := hl.hovered
hl.hovered = hl.isPosOverText(e.Position)
if hl.hovered != oldHovered {
hl.BaseWidget.Refresh()
}
}

// MouseOut is a hook that is called if the mouse pointer leaves the element.
func (hl *Hyperlink) MouseOut() {
changed := hl.hovered
hl.hovered = false
hl.BaseWidget.Refresh()
if changed {
hl.BaseWidget.Refresh()
}
}

func (hl *Hyperlink) focusWidth() float32 {
innerPad := theme.InnerPadding()
return fyne.Min(hl.size.Width, hl.textSize.Width+innerPad+theme.Padding()*2) - innerPad
}

func (hl *Hyperlink) focusXPos() float32 {
switch hl.Alignment {
case fyne.TextAlignLeading:
return theme.InnerPadding() / 2
case fyne.TextAlignCenter:
return (hl.size.Width - hl.focusWidth()) / 2
case fyne.TextAlignTrailing:
return (hl.size.Width - hl.focusWidth()) - theme.InnerPadding()/2
default:
return 0 // unreached
}
}

func (hl *Hyperlink) isPosOverText(pos fyne.Position) bool {
innerPad := theme.InnerPadding()
pad := theme.Padding()
// If not rendered yet provider will be nil
lineCount := float32(1)
if hl.provider != nil {
lineCount = fyne.Max(lineCount, float32(len(hl.provider.rowBounds)))
}

xpos := hl.focusXPos()
return pos.X >= xpos && pos.X <= xpos+hl.focusWidth() &&
pos.Y >= innerPad/2 && pos.Y <= hl.textSize.Height*lineCount+pad*2+innerPad/2
}

// Refresh triggers a redraw of the hyperlink.
Expand Down Expand Up @@ -160,12 +203,20 @@ func (hl *Hyperlink) SetURLFromString(str string) error {
}

// Tapped is called when a pointer tapped event is captured and triggers any change handler
func (hl *Hyperlink) Tapped(*fyne.PointEvent) {
func (hl *Hyperlink) Tapped(e *fyne.PointEvent) {
// If not rendered yet (hl.provider == nil), register all taps
// in practice this probably only happens in our unit tests
if hl.provider != nil && !hl.isPosOverText(e.Position) {
return
}
hl.invokeAction()
}

func (hl *Hyperlink) invokeAction() {
if hl.OnTapped != nil {
hl.OnTapped()
return
}

hl.openURL()
}

Expand All @@ -176,7 +227,7 @@ func (hl *Hyperlink) TypedRune(rune) {
// TypedKey is a hook called by the input handling logic on key events if this object is focused.
func (hl *Hyperlink) TypedKey(ev *fyne.KeyEvent) {
if ev.Name == fyne.KeySpace {
hl.Tapped(nil)
hl.invokeAction()
}
}

Expand All @@ -201,6 +252,7 @@ func (hl *Hyperlink) syncSegments() {
},
Text: hl.Text,
}}
hl.textSize = fyne.MeasureText(hl.Text, theme.TextSize(), hl.TextStyle)
}

var _ fyne.WidgetRenderer = (*hyperlinkRenderer)(nil)
Expand All @@ -217,12 +269,17 @@ func (r *hyperlinkRenderer) Destroy() {
}

func (r *hyperlinkRenderer) Layout(s fyne.Size) {
innerPad := theme.InnerPadding()
w := r.hl.focusWidth()
xposFocus := r.hl.focusXPos()
xposUnderline := xposFocus + innerPad/2

r.hl.provider.Resize(s)
innerPadding := theme.InnerPadding()
r.focus.Move(fyne.NewSquareOffsetPos(innerPadding / 2))
r.focus.Resize(s.SubtractWidthHeight(innerPadding, innerPadding))
r.under.Move(fyne.NewPos(innerPadding, s.Height-innerPadding))
r.under.Resize(fyne.NewSize(s.Width-innerPadding*2, 1))
lineCount := float32(len(r.hl.provider.rowBounds))
r.focus.Move(fyne.NewPos(xposFocus, innerPad/2))
r.focus.Resize(fyne.NewSize(w, r.hl.textSize.Height*lineCount+innerPad))
r.under.Move(fyne.NewPos(xposUnderline, r.hl.textSize.Height*lineCount+theme.Padding()*2))
r.under.Resize(fyne.NewSize(w-innerPad, 1))
}

func (r *hyperlinkRenderer) MinSize() fyne.Size {
Expand Down
3 changes: 3 additions & 0 deletions widget/hyperlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ func TestHyperlink_Cursor(t *testing.T) {
hyperlink := NewHyperlink("Test", u)

assert.Nil(t, err)
assert.Equal(t, desktop.DefaultCursor, hyperlink.Cursor())

hyperlink.hovered = true
assert.Equal(t, desktop.PointerCursor, hyperlink.Cursor())
}

Expand Down

0 comments on commit 484a4bb

Please sign in to comment.