From 5f32d36c2968ee9540231636bee82550f702c60c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 19 Oct 2023 08:42:13 -0700 Subject: [PATCH] backport hyperlink fix from github.com/fyne-io/fyne/pull/4320 --- widget/hyperlink.go | 84 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/widget/hyperlink.go b/widget/hyperlink.go index 95d31d8ad2..55f3a4c078 100644 --- a/widget/hyperlink.go +++ b/widget/hyperlink.go @@ -28,6 +28,7 @@ type Hyperlink struct { // Since: 2.2 OnTapped func() `json:"-"` + textSize fyne.Size // updated in syncSegments focused, hovered bool provider *RichText } @@ -67,7 +68,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. @@ -83,19 +87,60 @@ 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.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*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+innerPad+innerPad/2 } // Refresh triggers a redraw of the hyperlink. @@ -156,12 +201,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() } @@ -172,7 +225,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.openURL() + hl.invokeAction() } } @@ -196,6 +249,7 @@ func (hl *Hyperlink) syncSegments() { }, Text: hl.Text, }} + hl.textSize = fyne.MeasureText(hl.Text, theme.TextSize(), hl.TextStyle) } var _ fyne.WidgetRenderer = (*hyperlinkRenderer)(nil) @@ -212,11 +266,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) - r.focus.Move(fyne.NewPos(theme.InnerPadding()/2, theme.InnerPadding()/2)) - r.focus.Resize(fyne.NewSize(s.Width-theme.InnerPadding(), s.Height-theme.InnerPadding())) - r.under.Move(fyne.NewPos(theme.InnerPadding(), s.Height-theme.InnerPadding())) - r.under.Resize(fyne.NewSize(s.Width-theme.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.InnerPadding())) + r.under.Resize(fyne.NewSize(w-innerPad, 1)) } func (r *hyperlinkRenderer) MinSize() fyne.Size {