package widget import ( "image/color" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/driver/desktop" col "fyne.io/fyne/v2/internal/color" "fyne.io/fyne/v2/internal/svg" "fyne.io/fyne/v2/internal/widget" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" ) // ButtonAlign represents the horizontal alignment of a button. type ButtonAlign int // ButtonIconPlacement represents the ordering of icon & text within a button. type ButtonIconPlacement int // ButtonImportance represents how prominent the button should appear // // Since: 1.4 // // Deprecated: Use widget.Importance instead type ButtonImportance = Importance // ButtonStyle determines the behaviour and rendering of a button. type ButtonStyle int const ( // ButtonAlignCenter aligns the icon and the text centrally. ButtonAlignCenter ButtonAlign = iota // ButtonAlignLeading aligns the icon and the text with the leading edge. ButtonAlignLeading // ButtonAlignTrailing aligns the icon and the text with the trailing edge. ButtonAlignTrailing ) const ( // ButtonIconLeadingText aligns the icon on the leading edge of the text. ButtonIconLeadingText ButtonIconPlacement = iota // ButtonIconTrailingText aligns the icon on the trailing edge of the text. ButtonIconTrailingText ) var _ fyne.Focusable = (*Button)(nil) // Button widget has a text label and triggers an event func when clicked type Button struct { DisableableWidget Text string Icon fyne.Resource // Specify how prominent the button should be, High will highlight the button and Low will remove some decoration. // // Since: 1.4 Importance Importance Alignment ButtonAlign IconPlacement ButtonIconPlacement OnTapped func() `json:"-"` hovered, focused bool tapAnim *fyne.Animation } // NewButton creates a new button widget with the set label and tap handler func NewButton(label string, tapped func()) *Button { button := &Button{ Text: label, OnTapped: tapped, } button.ExtendBaseWidget(button) return button } // NewButtonWithIcon creates a new button widget with the specified label, themed icon and tap handler func NewButtonWithIcon(label string, icon fyne.Resource, tapped func()) *Button { button := &Button{ Text: label, Icon: icon, OnTapped: tapped, } button.ExtendBaseWidget(button) return button } // CreateRenderer is a private method to Fyne which links this widget to its renderer func (b *Button) CreateRenderer() fyne.WidgetRenderer { b.ExtendBaseWidget(b) th := b.Theme() v := fyne.CurrentApp().Settings().ThemeVariant() seg := &TextSegment{Text: b.Text, Style: RichTextStyleStrong} seg.Style.Alignment = fyne.TextAlignCenter text := NewRichText(seg) text.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding)) background := canvas.NewRectangle(th.Color(theme.ColorNameButton, v)) background.CornerRadius = th.Size(theme.SizeNameInputRadius) tapBG := canvas.NewRectangle(color.Transparent) b.tapAnim = newButtonTapAnimation(tapBG, b, th) b.tapAnim.Curve = fyne.AnimationEaseOut objects := []fyne.CanvasObject{ background, tapBG, text, } r := &buttonRenderer{ BaseRenderer: widget.NewBaseRenderer(objects), background: background, tapBG: tapBG, button: b, label: text, layout: layout.NewHBoxLayout(), } r.updateIconAndText() r.applyTheme() return r } // Cursor returns the cursor type of this widget func (b *Button) Cursor() desktop.Cursor { return desktop.DefaultCursor } // FocusGained is a hook called by the focus handling logic after this object gained the focus. func (b *Button) FocusGained() { b.focused = true b.Refresh() } // FocusLost is a hook called by the focus handling logic after this object lost the focus. func (b *Button) FocusLost() { b.focused = false b.Refresh() } // MinSize returns the size that this widget should not shrink below func (b *Button) MinSize() fyne.Size { b.ExtendBaseWidget(b) return b.BaseWidget.MinSize() } // MouseIn is called when a desktop pointer enters the widget func (b *Button) MouseIn(*desktop.MouseEvent) { b.hovered = true b.Refresh() } // MouseMoved is called when a desktop pointer hovers over the widget func (b *Button) MouseMoved(*desktop.MouseEvent) { } // MouseOut is called when a desktop pointer exits the widget func (b *Button) MouseOut() { b.hovered = false b.Refresh() } // SetIcon updates the icon on a label - pass nil to hide an icon func (b *Button) SetIcon(icon fyne.Resource) { b.Icon = icon b.Refresh() } // SetText allows the button label to be changed func (b *Button) SetText(text string) { b.Text = text b.Refresh() } // Tapped is called when a pointer tapped event is captured and triggers any tap handler func (b *Button) Tapped(*fyne.PointEvent) { if b.Disabled() { return } b.tapAnimation() if onTapped := b.OnTapped; onTapped != nil { onTapped() } } // TypedRune is a hook called by the input handling logic on text input events if this object is focused. func (b *Button) TypedRune(rune) { } // TypedKey is a hook called by the input handling logic on key events if this object is focused. func (b *Button) TypedKey(ev *fyne.KeyEvent) { if ev.Name == fyne.KeySpace { b.Tapped(nil) } } func (b *Button) tapAnimation() { if b.tapAnim == nil { return } b.tapAnim.Stop() if fyne.CurrentApp().Settings().ShowAnimations() { b.tapAnim.Start() } } type buttonRenderer struct { widget.BaseRenderer icon *canvas.Image label *RichText background *canvas.Rectangle tapBG *canvas.Rectangle button *Button layout fyne.Layout } // Layout the components of the button widget func (r *buttonRenderer) Layout(size fyne.Size) { r.background.Resize(size) r.tapBG.Resize(size) th := r.button.Theme() padding := r.padding(th) hasIcon := r.icon != nil hasLabel := r.label.Segments[0].(*TextSegment).Text != "" if !hasIcon && !hasLabel { // Nothing to layout return } iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon)) labelSize := r.label.MinSize() if hasLabel { if hasIcon { // Both var objects []fyne.CanvasObject if r.button.IconPlacement == ButtonIconLeadingText { objects = append(objects, r.icon, r.label) } else { objects = append(objects, r.label, r.icon) } r.icon.SetMinSize(iconSize) min := r.layout.MinSize(objects) r.layout.Layout(objects, min) pos := alignedPosition(r.button.Alignment, padding, min, size) labelOff := (min.Height - labelSize.Height) / 2 r.label.Move(r.label.Position().Add(pos).AddXY(0, labelOff)) r.icon.Move(r.icon.Position().Add(pos)) } else { // Label Only r.label.Move(alignedPosition(r.button.Alignment, padding, labelSize, size)) r.label.Resize(labelSize) } } else { // Icon Only r.icon.Move(alignedPosition(r.button.Alignment, padding, iconSize, size)) r.icon.Resize(iconSize) } } // MinSize calculates the minimum size of a button. // This is based on the contained text, any icon that is set and a standard // amount of padding added. func (r *buttonRenderer) MinSize() (size fyne.Size) { th := r.button.Theme() hasIcon := r.icon != nil hasLabel := r.label.Segments[0].(*TextSegment).Text != "" iconSize := fyne.NewSquareSize(th.Size(theme.SizeNameInlineIcon)) labelSize := r.label.MinSize() if hasLabel { size.Width = labelSize.Width } if hasIcon { if hasLabel { size.Width += th.Size(theme.SizeNamePadding) } size.Width += iconSize.Width } size.Height = fyne.Max(labelSize.Height, iconSize.Height) size = size.Add(r.padding(th)) return } func (r *buttonRenderer) Refresh() { th := r.button.Theme() r.label.inset = fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding)) r.label.Segments[0].(*TextSegment).Text = r.button.Text r.updateIconAndText() r.applyTheme() r.background.Refresh() r.Layout(r.button.Size()) canvas.Refresh(r.button.super()) } // applyTheme updates this button to match the current theme // must be called with the button propertyLock RLocked func (r *buttonRenderer) applyTheme() { th := r.button.Theme() v := fyne.CurrentApp().Settings().ThemeVariant() fgColorName, bgColorName, bgBlendName := r.buttonColorNames() if bg := r.background; bg != nil { bgColor := color.Color(color.Transparent) if bgColorName != "" { bgColor = th.Color(bgColorName, v) } if bgBlendName != "" { bgColor = blendColor(bgColor, th.Color(bgBlendName, v)) } bg.FillColor = bgColor bg.CornerRadius = th.Size(theme.SizeNameInputRadius) bg.Refresh() } r.label.Segments[0].(*TextSegment).Style.ColorName = fgColorName r.label.Refresh() if r.icon != nil && r.icon.Resource != nil { icon := r.icon.Resource if r.button.Importance != MediumImportance && r.button.Importance != LowImportance { if thRes, ok := icon.(fyne.ThemedResource); ok { if thRes.ThemeColorName() != fgColorName { icon = theme.NewColoredResource(icon, fgColorName) } } } r.icon.Resource = icon r.icon.Refresh() } } func (r *buttonRenderer) buttonColorNames() (foreground, background, backgroundBlend fyne.ThemeColorName) { foreground = theme.ColorNameForeground b := r.button if b.Disabled() { foreground = theme.ColorNameDisabled if b.Importance != LowImportance { background = theme.ColorNameDisabledButton } } else if b.focused { backgroundBlend = theme.ColorNameFocus } else if b.hovered { backgroundBlend = theme.ColorNameHover } if background == "" { switch b.Importance { case DangerImportance: foreground = theme.ColorNameForegroundOnError background = theme.ColorNameError case HighImportance: foreground = theme.ColorNameForegroundOnPrimary background = theme.ColorNamePrimary case LowImportance: if backgroundBlend != "" { background = theme.ColorNameButton } case SuccessImportance: foreground = theme.ColorNameForegroundOnSuccess background = theme.ColorNameSuccess case WarningImportance: foreground = theme.ColorNameForegroundOnWarning background = theme.ColorNameWarning default: background = theme.ColorNameButton } } return } func (r *buttonRenderer) padding(th fyne.Theme) fyne.Size { return fyne.NewSquareSize(th.Size(theme.SizeNameInnerPadding) * 2) } // must be called with r.button.propertyLock RLocked func (r *buttonRenderer) updateIconAndText() { if r.button.Icon != nil && !r.button.Hidden { icon := r.button.Icon if r.icon == nil { r.icon = canvas.NewImageFromResource(icon) r.icon.FillMode = canvas.ImageFillContain r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon}) } // TODO support disabling bitmap resource not just SVG if r.button.Disabled() && svg.IsResourceSVG(icon) { icon = theme.NewDisabledResource(icon) } r.icon.Resource = icon r.icon.Refresh() r.icon.Show() } else if r.icon != nil { r.icon.Hide() } if r.button.Text == "" { r.label.Hide() } else { r.label.Show() } r.label.Refresh() } func alignedPosition(align ButtonAlign, padding, objectSize, layoutSize fyne.Size) (pos fyne.Position) { pos.Y = (layoutSize.Height - objectSize.Height) / 2 switch align { case ButtonAlignCenter: pos.X = (layoutSize.Width - objectSize.Width) / 2 case ButtonAlignLeading: pos.X = padding.Width / 2 case ButtonAlignTrailing: pos.X = layoutSize.Width - objectSize.Width - padding.Width/2 } return } func blendColor(under, over color.Color) color.Color { // This alpha blends with the over operator, and accounts for RGBA() returning alpha-premultiplied values dstR, dstG, dstB, dstA := under.RGBA() srcR, srcG, srcB, srcA := over.RGBA() srcAlpha := float32(srcA) / 0xFFFF dstAlpha := float32(dstA) / 0xFFFF outAlpha := srcAlpha + dstAlpha*(1-srcAlpha) outR := srcR + uint32(float32(dstR)*(1-srcAlpha)) outG := srcG + uint32(float32(dstG)*(1-srcAlpha)) outB := srcB + uint32(float32(dstB)*(1-srcAlpha)) // We create an RGBA64 here because the color components are already alpha-premultiplied 16-bit values (they're just stored in uint32s). return color.RGBA64{R: uint16(outR), G: uint16(outG), B: uint16(outB), A: uint16(outAlpha * 0xFFFF)} } func newButtonTapAnimation(bg *canvas.Rectangle, w fyne.Widget, th fyne.Theme) *fyne.Animation { v := fyne.CurrentApp().Settings().ThemeVariant() return fyne.NewAnimation(canvas.DurationStandard, func(done float32) { mid := w.Size().Width / 2 size := mid * done bg.Resize(fyne.NewSize(size*2, w.Size().Height)) bg.Move(fyne.NewPos(mid-size, 0)) r, g, bb, a := col.ToNRGBA(th.Color(theme.ColorNamePressed, v)) aa := uint8(a) fade := aa - uint8(float32(aa)*done) if fade > 0 { bg.FillColor = &color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(bb), A: fade} } else { bg.FillColor = color.Transparent } canvas.Refresh(bg) }) }