button.go 12 KB

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