button.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  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. r.tapBG.Resize(size)
  240. hasIcon := r.icon != nil
  241. hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
  242. if !hasIcon && !hasLabel {
  243. // Nothing to layout
  244. return
  245. }
  246. iconSize := fyne.NewSquareSize(theme.IconInlineSize())
  247. labelSize := r.label.MinSize()
  248. padding := r.padding()
  249. if hasLabel {
  250. if hasIcon {
  251. // Both
  252. var objects []fyne.CanvasObject
  253. if r.button.IconPlacement == ButtonIconLeadingText {
  254. objects = append(objects, r.icon, r.label)
  255. } else {
  256. objects = append(objects, r.label, r.icon)
  257. }
  258. r.icon.SetMinSize(iconSize)
  259. min := r.layout.MinSize(objects)
  260. r.layout.Layout(objects, min)
  261. pos := alignedPosition(r.button.Alignment, padding, min, size)
  262. labelOff := (min.Height - labelSize.Height) / 2
  263. r.label.Move(r.label.Position().Add(pos).AddXY(0, labelOff))
  264. r.icon.Move(r.icon.Position().Add(pos))
  265. } else {
  266. // Label Only
  267. r.label.Move(alignedPosition(r.button.Alignment, padding, labelSize, size))
  268. r.label.Resize(labelSize)
  269. }
  270. } else {
  271. // Icon Only
  272. r.icon.Move(alignedPosition(r.button.Alignment, padding, iconSize, size))
  273. r.icon.Resize(iconSize)
  274. }
  275. }
  276. // MinSize calculates the minimum size of a button.
  277. // This is based on the contained text, any icon that is set and a standard
  278. // amount of padding added.
  279. func (r *buttonRenderer) MinSize() (size fyne.Size) {
  280. hasIcon := r.icon != nil
  281. hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
  282. iconSize := fyne.NewSquareSize(theme.IconInlineSize())
  283. labelSize := r.label.MinSize()
  284. if hasLabel {
  285. size.Width = labelSize.Width
  286. }
  287. if hasIcon {
  288. if hasLabel {
  289. size.Width += theme.Padding()
  290. }
  291. size.Width += iconSize.Width
  292. }
  293. size.Height = fyne.Max(labelSize.Height, iconSize.Height)
  294. size = size.Add(r.padding())
  295. return
  296. }
  297. func (r *buttonRenderer) Refresh() {
  298. r.label.inset = fyne.NewSize(theme.InnerPadding(), theme.InnerPadding())
  299. r.label.Segments[0].(*TextSegment).Text = r.button.Text
  300. r.updateIconAndText()
  301. r.applyTheme()
  302. r.background.Refresh()
  303. r.Layout(r.button.Size())
  304. canvas.Refresh(r.button.super())
  305. }
  306. // applyTheme updates this button to match the current theme
  307. func (r *buttonRenderer) applyTheme() {
  308. r.button.applyButtonTheme()
  309. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
  310. switch {
  311. case r.button.disabled:
  312. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
  313. case r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance:
  314. if r.button.focused {
  315. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
  316. } else {
  317. r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameBackground
  318. }
  319. }
  320. r.label.Refresh()
  321. if r.icon != nil && r.icon.Resource != nil {
  322. switch res := r.icon.Resource.(type) {
  323. case *theme.ThemedResource:
  324. if r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance {
  325. r.icon.Resource = theme.NewInvertedThemedResource(res)
  326. r.icon.Refresh()
  327. }
  328. case *theme.InvertedThemedResource:
  329. if r.button.Importance != HighImportance && r.button.Importance != DangerImportance && r.button.Importance != WarningImportance && r.button.Importance != SuccessImportance {
  330. r.icon.Resource = res.Original()
  331. r.icon.Refresh()
  332. }
  333. }
  334. }
  335. }
  336. func (r *buttonRenderer) padding() fyne.Size {
  337. return fyne.NewSquareSize(theme.InnerPadding() * 2)
  338. }
  339. func (r *buttonRenderer) updateIconAndText() {
  340. if r.button.Icon != nil && r.button.Visible() {
  341. if r.icon == nil {
  342. r.icon = canvas.NewImageFromResource(r.button.Icon)
  343. r.icon.FillMode = canvas.ImageFillContain
  344. r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon})
  345. }
  346. if r.button.Disabled() {
  347. r.icon.Resource = theme.NewDisabledResource(r.button.Icon)
  348. } else {
  349. r.icon.Resource = r.button.Icon
  350. }
  351. r.icon.Refresh()
  352. r.icon.Show()
  353. } else if r.icon != nil {
  354. r.icon.Hide()
  355. }
  356. if r.button.Text == "" {
  357. r.label.Hide()
  358. } else {
  359. r.label.Show()
  360. }
  361. r.label.Refresh()
  362. }
  363. func alignedPosition(align ButtonAlign, padding, objectSize, layoutSize fyne.Size) (pos fyne.Position) {
  364. pos.Y = (layoutSize.Height - objectSize.Height) / 2
  365. switch align {
  366. case ButtonAlignCenter:
  367. pos.X = (layoutSize.Width - objectSize.Width) / 2
  368. case ButtonAlignLeading:
  369. pos.X = padding.Width / 2
  370. case ButtonAlignTrailing:
  371. pos.X = layoutSize.Width - objectSize.Width - padding.Width/2
  372. }
  373. return
  374. }
  375. func blendColor(under, over color.Color) color.Color {
  376. // This alpha blends with the over operator, and accounts for RGBA() returning alpha-premultiplied values
  377. dstR, dstG, dstB, dstA := under.RGBA()
  378. srcR, srcG, srcB, srcA := over.RGBA()
  379. srcAlpha := float32(srcA) / 0xFFFF
  380. dstAlpha := float32(dstA) / 0xFFFF
  381. outAlpha := srcAlpha + dstAlpha*(1-srcAlpha)
  382. outR := srcR + uint32(float32(dstR)*(1-srcAlpha))
  383. outG := srcG + uint32(float32(dstG)*(1-srcAlpha))
  384. outB := srcB + uint32(float32(dstB)*(1-srcAlpha))
  385. // We create an RGBA64 here because the color components are already alpha-premultiplied 16-bit values (they're just stored in uint32s).
  386. return color.RGBA64{R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outAlpha * 0xFFFF)}
  387. }
  388. func newButtonTapAnimation(bg *canvas.Rectangle, w fyne.Widget) *fyne.Animation {
  389. return fyne.NewAnimation(canvas.DurationStandard, func(done float32) {
  390. mid := w.Size().Width / 2
  391. size := mid * done
  392. bg.Resize(fyne.NewSize(size*2, w.Size().Height))
  393. bg.Move(fyne.NewPos(mid-size, 0))
  394. r, g, bb, a := col.ToNRGBA(theme.PressedColor())
  395. aa := uint8(a)
  396. fade := aa - uint8(float32(aa)*done)
  397. if fade > 0 {
  398. bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade}
  399. } else {
  400. bg.FillColor = color.Transparent
  401. }
  402. canvas.Refresh(bg)
  403. })
  404. }