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