button.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. package widget
  2. import (
  3. "image/color"
  4. "fyne.io/fyne/v2"
  5. "fyne.io/fyne/v2/canvas"
  6. "fyne.io/fyne/v2/driver/desktop"
  7. col "fyne.io/fyne/v2/internal/color"
  8. "fyne.io/fyne/v2/internal/widget"
  9. "fyne.io/fyne/v2/layout"
  10. "fyne.io/fyne/v2/theme"
  11. )
  12. // ButtonAlign represents the horizontal alignment of a button.
  13. type ButtonAlign int
  14. // ButtonIconPlacement represents the ordering of icon & text within a button.
  15. type ButtonIconPlacement int
  16. // ButtonImportance represents how prominent the button should appear
  17. //
  18. // Since: 1.4
  19. //
  20. // Deprecated: Use widget.Importance instead
  21. type ButtonImportance = Importance
  22. // ButtonStyle determines the behaviour and rendering of a button.
  23. type ButtonStyle int
  24. const (
  25. // ButtonAlignCenter aligns the icon and the text centrally.
  26. ButtonAlignCenter ButtonAlign = iota
  27. // ButtonAlignLeading aligns the icon and the text with the leading edge.
  28. ButtonAlignLeading
  29. // ButtonAlignTrailing aligns the icon and the text with the trailing edge.
  30. ButtonAlignTrailing
  31. )
  32. const (
  33. // ButtonIconLeadingText aligns the icon on the leading edge of the text.
  34. ButtonIconLeadingText ButtonIconPlacement = iota
  35. // ButtonIconTrailingText aligns the icon on the trailing edge of the text.
  36. ButtonIconTrailingText
  37. )
  38. var _ fyne.Focusable = (*Button)(nil)
  39. // Button widget has a text label and triggers an event func when clicked
  40. type Button struct {
  41. DisableableWidget
  42. Text string
  43. Icon fyne.Resource
  44. // Specify how prominent the button should be, High will highlight the button and Low will remove some decoration.
  45. //
  46. // Since: 1.4
  47. Importance Importance
  48. Alignment ButtonAlign
  49. IconPlacement ButtonIconPlacement
  50. OnTapped func() `json:"-"`
  51. hovered, focused bool
  52. tapAnim *fyne.Animation
  53. background *canvas.Rectangle
  54. }
  55. // NewButton creates a new button widget with the set label and tap handler
  56. func NewButton(label string, tapped func()) *Button {
  57. button := &Button{
  58. Text: label,
  59. OnTapped: tapped,
  60. }
  61. button.ExtendBaseWidget(button)
  62. return button
  63. }
  64. // NewButtonWithIcon creates a new button widget with the specified label, themed icon and tap handler
  65. func NewButtonWithIcon(label string, icon fyne.Resource, tapped func()) *Button {
  66. button := &Button{
  67. Text: label,
  68. Icon: icon,
  69. OnTapped: tapped,
  70. }
  71. button.ExtendBaseWidget(button)
  72. return button
  73. }
  74. // CreateRenderer is a private method to Fyne which links this widget to its renderer
  75. func (b *Button) CreateRenderer() fyne.WidgetRenderer {
  76. b.ExtendBaseWidget(b)
  77. seg := &TextSegment{Text: b.Text, Style: RichTextStyleStrong}
  78. seg.Style.Alignment = fyne.TextAlignCenter
  79. text := NewRichText(seg)
  80. text.inset = fyne.NewSquareSize(theme.InnerPadding())
  81. b.background = canvas.NewRectangle(theme.ButtonColor())
  82. b.background.CornerRadius = theme.InputRadiusSize()
  83. tapBG := canvas.NewRectangle(color.Transparent)
  84. b.tapAnim = newButtonTapAnimation(tapBG, b)
  85. b.tapAnim.Curve = fyne.AnimationEaseOut
  86. objects := []fyne.CanvasObject{
  87. b.background,
  88. tapBG,
  89. text,
  90. }
  91. r := &buttonRenderer{
  92. BaseRenderer: widget.NewBaseRenderer(objects),
  93. background: b.background,
  94. tapBG: tapBG,
  95. button: b,
  96. label: text,
  97. layout: layout.NewHBoxLayout(),
  98. }
  99. r.updateIconAndText()
  100. r.applyTheme()
  101. return r
  102. }
  103. // Cursor returns the cursor type of this widget
  104. func (b *Button) Cursor() desktop.Cursor {
  105. return desktop.DefaultCursor
  106. }
  107. // FocusGained is a hook called by the focus handling logic after this object gained the focus.
  108. func (b *Button) FocusGained() {
  109. b.focused = true
  110. b.Refresh()
  111. }
  112. // FocusLost is a hook called by the focus handling logic after this object lost the focus.
  113. func (b *Button) FocusLost() {
  114. b.focused = false
  115. b.Refresh()
  116. }
  117. // MinSize returns the size that this widget should not shrink below
  118. func (b *Button) MinSize() fyne.Size {
  119. b.ExtendBaseWidget(b)
  120. return b.BaseWidget.MinSize()
  121. }
  122. // MouseIn is called when a desktop pointer enters the widget
  123. func (b *Button) MouseIn(*desktop.MouseEvent) {
  124. b.hovered = true
  125. b.applyButtonTheme()
  126. }
  127. // MouseMoved is called when a desktop pointer hovers over the widget
  128. func (b *Button) MouseMoved(*desktop.MouseEvent) {
  129. }
  130. // MouseOut is called when a desktop pointer exits the widget
  131. func (b *Button) MouseOut() {
  132. b.hovered = false
  133. b.applyButtonTheme()
  134. }
  135. // SetIcon updates the icon on a label - pass nil to hide an icon
  136. func (b *Button) SetIcon(icon fyne.Resource) {
  137. b.Icon = icon
  138. b.Refresh()
  139. }
  140. // SetText allows the button label to be changed
  141. func (b *Button) SetText(text string) {
  142. b.Text = text
  143. b.Refresh()
  144. }
  145. // Tapped is called when a pointer tapped event is captured and triggers any tap handler
  146. func (b *Button) Tapped(*fyne.PointEvent) {
  147. if b.Disabled() {
  148. return
  149. }
  150. b.tapAnimation()
  151. b.applyButtonTheme()
  152. if b.OnTapped != nil {
  153. b.OnTapped()
  154. }
  155. }
  156. // TypedRune is a hook called by the input handling logic on text input events if this object is focused.
  157. func (b *Button) TypedRune(rune) {
  158. }
  159. // TypedKey is a hook called by the input handling logic on key events if this object is focused.
  160. func (b *Button) TypedKey(ev *fyne.KeyEvent) {
  161. if ev.Name == fyne.KeySpace {
  162. b.Tapped(nil)
  163. }
  164. }
  165. func (b *Button) applyButtonTheme() {
  166. if b.background == nil {
  167. return
  168. }
  169. b.background.FillColor = b.buttonColor()
  170. b.background.CornerRadius = theme.InputRadiusSize()
  171. b.background.Refresh()
  172. }
  173. func (b *Button) buttonColor() color.Color {
  174. switch {
  175. case b.Disabled():
  176. if b.Importance == LowImportance {
  177. return color.Transparent
  178. }
  179. return theme.DisabledButtonColor()
  180. case b.focused:
  181. bg := theme.ButtonColor()
  182. if b.Importance == HighImportance {
  183. bg = theme.PrimaryColor()
  184. } else if b.Importance == DangerImportance {
  185. bg = theme.ErrorColor()
  186. } else if b.Importance == WarningImportance {
  187. bg = theme.WarningColor()
  188. } else if b.Importance == SuccessImportance {
  189. bg = theme.SuccessColor()
  190. }
  191. return blendColor(bg, theme.FocusColor())
  192. case b.hovered:
  193. bg := theme.ButtonColor()
  194. if b.Importance == HighImportance {
  195. bg = theme.PrimaryColor()
  196. } else if b.Importance == DangerImportance {
  197. bg = theme.ErrorColor()
  198. } else if b.Importance == WarningImportance {
  199. bg = theme.WarningColor()
  200. } else if b.Importance == SuccessImportance {
  201. bg = theme.SuccessColor()
  202. }
  203. return blendColor(bg, theme.HoverColor())
  204. case b.Importance == HighImportance:
  205. return theme.PrimaryColor()
  206. case b.Importance == LowImportance:
  207. return color.Transparent
  208. case b.Importance == DangerImportance:
  209. return theme.ErrorColor()
  210. case b.Importance == WarningImportance:
  211. return theme.WarningColor()
  212. case b.Importance == SuccessImportance:
  213. return theme.SuccessColor()
  214. default:
  215. return theme.ButtonColor()
  216. }
  217. }
  218. func (b *Button) tapAnimation() {
  219. if b.tapAnim == nil {
  220. return
  221. }
  222. b.tapAnim.Stop()
  223. if fyne.CurrentApp().Settings().ShowAnimations() {
  224. b.tapAnim.Start()
  225. }
  226. }
  227. type buttonRenderer struct {
  228. widget.BaseRenderer
  229. icon *canvas.Image
  230. label *RichText
  231. background *canvas.Rectangle
  232. tapBG *canvas.Rectangle
  233. button *Button
  234. layout fyne.Layout
  235. }
  236. // Layout the components of the button widget
  237. func (r *buttonRenderer) Layout(size fyne.Size) {
  238. r.background.Resize(size)
  239. hasIcon := r.icon != nil
  240. hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
  241. if !hasIcon && !hasLabel {
  242. // Nothing to layout
  243. return
  244. }
  245. iconSize := fyne.NewSquareSize(theme.IconInlineSize())
  246. labelSize := r.label.MinSize()
  247. padding := r.padding()
  248. if hasLabel {
  249. if hasIcon {
  250. // Both
  251. var objects []fyne.CanvasObject
  252. if r.button.IconPlacement == ButtonIconLeadingText {
  253. objects = append(objects, r.icon, r.label)
  254. } else {
  255. objects = append(objects, r.label, r.icon)
  256. }
  257. r.icon.SetMinSize(iconSize)
  258. min := r.layout.MinSize(objects)
  259. r.layout.Layout(objects, min)
  260. pos := alignedPosition(r.button.Alignment, padding, min, size)
  261. labelOff := (min.Height - labelSize.Height) / 2
  262. r.label.Move(r.label.Position().Add(pos).AddXY(0, labelOff))
  263. r.icon.Move(r.icon.Position().Add(pos))
  264. } else {
  265. // Label Only
  266. r.label.Move(alignedPosition(r.button.Alignment, padding, labelSize, size))
  267. r.label.Resize(labelSize)
  268. }
  269. } else {
  270. // Icon Only
  271. r.icon.Move(alignedPosition(r.button.Alignment, padding, iconSize, size))
  272. r.icon.Resize(iconSize)
  273. }
  274. }
  275. // MinSize calculates the minimum size of a button.
  276. // This is based on the contained text, any icon that is set and a standard
  277. // amount of padding added.
  278. func (r *buttonRenderer) MinSize() (size fyne.Size) {
  279. hasIcon := r.icon != nil
  280. hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
  281. iconSize := fyne.NewSquareSize(theme.IconInlineSize())
  282. labelSize := r.label.MinSize()
  283. if hasLabel {
  284. size.Width = labelSize.Width
  285. }
  286. if hasIcon {
  287. if hasLabel {
  288. size.Width += theme.Padding()
  289. }
  290. size.Width += iconSize.Width
  291. }
  292. size.Height = fyne.Max(labelSize.Height, iconSize.Height)
  293. size = size.Add(r.padding())
  294. return
  295. }
  296. func (r *buttonRenderer) Refresh() {
  297. r.label.inset = fyne.NewSize(theme.InnerPadding(), theme.InnerPadding())
  298. r.label.Segments[0].(*TextSegment).Text = r.button.Text
  299. r.updateIconAndText()
  300. r.applyTheme()
  301. r.background.Refresh()
  302. r.Layout(r.button.Size())
  303. canvas.Refresh(r.button.super())
  304. }
  305. // applyTheme updates this button to match the current theme
  306. func (r *buttonRenderer) applyTheme() {
  307. r.button.applyButtonTheme()
  308. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
  309. switch {
  310. case r.button.disabled:
  311. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
  312. case r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance:
  313. if r.button.focused {
  314. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
  315. } else {
  316. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameBackground
  317. }
  318. }
  319. r.label.Refresh()
  320. if r.icon != nil && r.icon.Resource != nil {
  321. switch res := r.icon.Resource.(type) {
  322. case *theme.ThemedResource:
  323. if r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance {
  324. r.icon.Resource = theme.NewInvertedThemedResource(res)
  325. r.icon.Refresh()
  326. }
  327. case *theme.InvertedThemedResource:
  328. if r.button.Importance != HighImportance && r.button.Importance != DangerImportance && r.button.Importance != WarningImportance && r.button.Importance != SuccessImportance {
  329. r.icon.Resource = res.Original()
  330. r.icon.Refresh()
  331. }
  332. }
  333. }
  334. }
  335. func (r *buttonRenderer) padding() fyne.Size {
  336. return fyne.NewSquareSize(theme.InnerPadding() * 2)
  337. }
  338. func (r *buttonRenderer) updateIconAndText() {
  339. if r.button.Icon != nil && r.button.Visible() {
  340. if r.icon == nil {
  341. r.icon = canvas.NewImageFromResource(r.button.Icon)
  342. r.icon.FillMode = canvas.ImageFillContain
  343. r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon})
  344. }
  345. if r.button.Disabled() {
  346. r.icon.Resource = theme.NewDisabledResource(r.button.Icon)
  347. } else {
  348. r.icon.Resource = r.button.Icon
  349. }
  350. r.icon.Refresh()
  351. r.icon.Show()
  352. } else if r.icon != nil {
  353. r.icon.Hide()
  354. }
  355. if r.button.Text == "" {
  356. r.label.Hide()
  357. } else {
  358. r.label.Show()
  359. }
  360. r.label.Refresh()
  361. }
  362. func alignedPosition(align ButtonAlign, padding, objectSize, layoutSize fyne.Size) (pos fyne.Position) {
  363. pos.Y = (layoutSize.Height - objectSize.Height) / 2
  364. switch align {
  365. case ButtonAlignCenter:
  366. pos.X = (layoutSize.Width - objectSize.Width) / 2
  367. case ButtonAlignLeading:
  368. pos.X = padding.Width / 2
  369. case ButtonAlignTrailing:
  370. pos.X = layoutSize.Width - objectSize.Width - padding.Width/2
  371. }
  372. return
  373. }
  374. func blendColor(under, over color.Color) color.Color {
  375. // This alpha blends with the over operator, and accounts for RGBA() returning alpha-premultiplied values
  376. dstR, dstG, dstB, dstA := under.RGBA()
  377. srcR, srcG, srcB, srcA := over.RGBA()
  378. srcAlpha := float32(srcA) / 0xFFFF
  379. dstAlpha := float32(dstA) / 0xFFFF
  380. outAlpha := srcAlpha + dstAlpha*(1-srcAlpha)
  381. outR := srcR + uint32(float32(dstR)*(1-srcAlpha))
  382. outG := srcG + uint32(float32(dstG)*(1-srcAlpha))
  383. outB := srcB + uint32(float32(dstB)*(1-srcAlpha))
  384. // We create an RGBA64 here because the color components are already alpha-premultiplied 16-bit values (they're just stored in uint32s).
  385. return color.RGBA64{R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outAlpha * 0xFFFF)}
  386. }
  387. func newButtonTapAnimation(bg *canvas.Rectangle, w fyne.Widget) *fyne.Animation {
  388. return fyne.NewAnimation(canvas.DurationStandard, func(done float32) {
  389. mid := w.Size().Width / 2
  390. size := mid * done
  391. bg.Resize(fyne.NewSize(size*2, w.Size().Height))
  392. bg.Move(fyne.NewPos(mid-size, 0))
  393. r, g, bb, a := col.ToNRGBA(theme.PressedColor())
  394. aa := uint8(a)
  395. fade := aa - uint8(float32(aa)*done)
  396. bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade}
  397. canvas.Refresh(bg)
  398. })
  399. }