hyperlink.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. package widget
  2. import (
  3. "image/color"
  4. "net/url"
  5. "fyne.io/fyne/v2"
  6. "fyne.io/fyne/v2/canvas"
  7. "fyne.io/fyne/v2/driver/desktop"
  8. "fyne.io/fyne/v2/theme"
  9. )
  10. var _ fyne.Focusable = (*Hyperlink)(nil)
  11. var _ fyne.Widget = (*Hyperlink)(nil)
  12. // Hyperlink widget is a text component with appropriate padding and layout.
  13. // When clicked, the default web browser should open with a URL
  14. type Hyperlink struct {
  15. BaseWidget
  16. Text string
  17. URL *url.URL
  18. Alignment fyne.TextAlign // The alignment of the Text
  19. Wrapping fyne.TextWrap // The wrapping of the Text
  20. TextStyle fyne.TextStyle // The style of the hyperlink text
  21. // OnTapped overrides the default `fyne.OpenURL` call when the link is tapped
  22. //
  23. // Since: 2.2
  24. OnTapped func() `json:"-"`
  25. focused, hovered bool
  26. provider *RichText
  27. }
  28. // NewHyperlink creates a new hyperlink widget with the set text content
  29. func NewHyperlink(text string, url *url.URL) *Hyperlink {
  30. return NewHyperlinkWithStyle(text, url, fyne.TextAlignLeading, fyne.TextStyle{})
  31. }
  32. // NewHyperlinkWithStyle creates a new hyperlink widget with the set text content
  33. func NewHyperlinkWithStyle(text string, url *url.URL, alignment fyne.TextAlign, style fyne.TextStyle) *Hyperlink {
  34. hl := &Hyperlink{
  35. Text: text,
  36. URL: url,
  37. Alignment: alignment,
  38. TextStyle: style,
  39. }
  40. return hl
  41. }
  42. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  43. func (hl *Hyperlink) CreateRenderer() fyne.WidgetRenderer {
  44. hl.ExtendBaseWidget(hl)
  45. hl.provider = NewRichTextWithText(hl.Text)
  46. hl.provider.ExtendBaseWidget(hl.provider)
  47. hl.syncSegments()
  48. focus := canvas.NewRectangle(color.Transparent)
  49. focus.StrokeColor = theme.FocusColor()
  50. focus.StrokeWidth = 2
  51. focus.Hide()
  52. under := canvas.NewRectangle(theme.HyperlinkColor())
  53. under.Hide()
  54. return &hyperlinkRenderer{hl: hl, objects: []fyne.CanvasObject{hl.provider, focus, under}, focus: focus, under: under}
  55. }
  56. // Cursor returns the cursor type of this widget
  57. func (hl *Hyperlink) Cursor() desktop.Cursor {
  58. return desktop.PointerCursor
  59. }
  60. // FocusGained is a hook called by the focus handling logic after this object gained the focus.
  61. func (hl *Hyperlink) FocusGained() {
  62. hl.focused = true
  63. hl.BaseWidget.Refresh()
  64. }
  65. // FocusLost is a hook called by the focus handling logic after this object lost the focus.
  66. func (hl *Hyperlink) FocusLost() {
  67. hl.focused = false
  68. hl.BaseWidget.Refresh()
  69. }
  70. // MouseIn is a hook that is called if the mouse pointer enters the element.
  71. func (hl *Hyperlink) MouseIn(*desktop.MouseEvent) {
  72. hl.hovered = true
  73. hl.BaseWidget.Refresh()
  74. }
  75. // MouseMoved is a hook that is called if the mouse pointer moved over the element.
  76. func (hl *Hyperlink) MouseMoved(*desktop.MouseEvent) {
  77. }
  78. // MouseOut is a hook that is called if the mouse pointer leaves the element.
  79. func (hl *Hyperlink) MouseOut() {
  80. hl.hovered = false
  81. hl.BaseWidget.Refresh()
  82. }
  83. // Refresh triggers a redraw of the hyperlink.
  84. //
  85. // Implements: fyne.Widget
  86. func (hl *Hyperlink) Refresh() {
  87. if hl.provider == nil { // not created until visible
  88. return
  89. }
  90. hl.syncSegments()
  91. hl.provider.Refresh()
  92. hl.BaseWidget.Refresh()
  93. }
  94. // MinSize returns the smallest size this widget can shrink to
  95. func (hl *Hyperlink) MinSize() fyne.Size {
  96. if hl.provider == nil {
  97. hl.CreateRenderer()
  98. }
  99. return hl.provider.MinSize()
  100. }
  101. // Resize sets a new size for the hyperlink.
  102. // Note this should not be used if the widget is being managed by a Layout within a Container.
  103. func (hl *Hyperlink) Resize(size fyne.Size) {
  104. hl.BaseWidget.Resize(size)
  105. if hl.provider == nil { // not created until visible
  106. return
  107. }
  108. hl.provider.Resize(size)
  109. }
  110. // SetText sets the text of the hyperlink
  111. func (hl *Hyperlink) SetText(text string) {
  112. hl.Text = text
  113. if hl.provider == nil { // not created until visible
  114. return
  115. }
  116. hl.syncSegments()
  117. hl.provider.Refresh()
  118. }
  119. // SetURL sets the URL of the hyperlink, taking in a URL type
  120. func (hl *Hyperlink) SetURL(url *url.URL) {
  121. hl.URL = url
  122. }
  123. // SetURLFromString sets the URL of the hyperlink, taking in a string type
  124. func (hl *Hyperlink) SetURLFromString(str string) error {
  125. u, err := url.Parse(str)
  126. if err != nil {
  127. return err
  128. }
  129. hl.URL = u
  130. return nil
  131. }
  132. // Tapped is called when a pointer tapped event is captured and triggers any change handler
  133. func (hl *Hyperlink) Tapped(*fyne.PointEvent) {
  134. if hl.OnTapped != nil {
  135. hl.OnTapped()
  136. return
  137. }
  138. hl.openURL()
  139. }
  140. // TypedRune is a hook called by the input handling logic on text input events if this object is focused.
  141. func (hl *Hyperlink) TypedRune(rune) {
  142. }
  143. // TypedKey is a hook called by the input handling logic on key events if this object is focused.
  144. func (hl *Hyperlink) TypedKey(ev *fyne.KeyEvent) {
  145. if ev.Name == fyne.KeySpace {
  146. hl.Tapped(nil)
  147. }
  148. }
  149. func (hl *Hyperlink) openURL() {
  150. if hl.URL != nil {
  151. err := fyne.CurrentApp().OpenURL(hl.URL)
  152. if err != nil {
  153. fyne.LogError("Failed to open url", err)
  154. }
  155. }
  156. }
  157. func (hl *Hyperlink) syncSegments() {
  158. hl.provider.Wrapping = hl.Wrapping
  159. hl.provider.Segments = []RichTextSegment{&TextSegment{
  160. Style: RichTextStyle{
  161. Alignment: hl.Alignment,
  162. ColorName: theme.ColorNameHyperlink,
  163. Inline: true,
  164. TextStyle: hl.TextStyle,
  165. },
  166. Text: hl.Text,
  167. }}
  168. }
  169. var _ fyne.WidgetRenderer = (*hyperlinkRenderer)(nil)
  170. type hyperlinkRenderer struct {
  171. hl *Hyperlink
  172. focus *canvas.Rectangle
  173. under *canvas.Rectangle
  174. objects []fyne.CanvasObject
  175. }
  176. func (r *hyperlinkRenderer) Destroy() {
  177. }
  178. func (r *hyperlinkRenderer) Layout(s fyne.Size) {
  179. r.hl.provider.Resize(s)
  180. r.focus.Move(fyne.NewPos(theme.InnerPadding()/2, theme.InnerPadding()/2))
  181. r.focus.Resize(fyne.NewSize(s.Width-theme.InnerPadding(), s.Height-theme.InnerPadding()))
  182. r.under.Move(fyne.NewPos(theme.InnerPadding(), s.Height-theme.InnerPadding()))
  183. r.under.Resize(fyne.NewSize(s.Width-theme.InnerPadding()*2, 1))
  184. }
  185. func (r *hyperlinkRenderer) MinSize() fyne.Size {
  186. return r.hl.provider.MinSize()
  187. }
  188. func (r *hyperlinkRenderer) Objects() []fyne.CanvasObject {
  189. return r.objects
  190. }
  191. func (r *hyperlinkRenderer) Refresh() {
  192. r.hl.provider.Refresh()
  193. r.focus.StrokeColor = theme.FocusColor()
  194. r.focus.Hidden = !r.hl.focused
  195. r.under.StrokeColor = theme.HyperlinkColor()
  196. r.under.Hidden = !r.hl.hovered
  197. }