464 lines
13 KiB
Go
464 lines
13 KiB
Go
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/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
|
|
background *canvas.Rectangle
|
|
}
|
|
|
|
// 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)
|
|
seg := &TextSegment{Text: b.Text, Style: RichTextStyleStrong}
|
|
seg.Style.Alignment = fyne.TextAlignCenter
|
|
text := NewRichText(seg)
|
|
text.inset = fyne.NewSquareSize(theme.InnerPadding())
|
|
|
|
b.background = canvas.NewRectangle(theme.ButtonColor())
|
|
b.background.CornerRadius = theme.InputRadiusSize()
|
|
tapBG := canvas.NewRectangle(color.Transparent)
|
|
b.tapAnim = newButtonTapAnimation(tapBG, b)
|
|
b.tapAnim.Curve = fyne.AnimationEaseOut
|
|
objects := []fyne.CanvasObject{
|
|
b.background,
|
|
tapBG,
|
|
text,
|
|
}
|
|
r := &buttonRenderer{
|
|
BaseRenderer: widget.NewBaseRenderer(objects),
|
|
background: b.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.applyButtonTheme()
|
|
}
|
|
|
|
// 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.applyButtonTheme()
|
|
}
|
|
|
|
// 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()
|
|
b.applyButtonTheme()
|
|
|
|
if b.OnTapped != nil {
|
|
b.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) applyButtonTheme() {
|
|
if b.background == nil {
|
|
return
|
|
}
|
|
|
|
b.background.FillColor = b.buttonColor()
|
|
b.background.CornerRadius = theme.InputRadiusSize()
|
|
b.background.Refresh()
|
|
}
|
|
|
|
func (b *Button) buttonColor() color.Color {
|
|
switch {
|
|
case b.Disabled():
|
|
if b.Importance == LowImportance {
|
|
return color.Transparent
|
|
}
|
|
return theme.DisabledButtonColor()
|
|
case b.focused:
|
|
bg := theme.ButtonColor()
|
|
if b.Importance == HighImportance {
|
|
bg = theme.PrimaryColor()
|
|
} else if b.Importance == DangerImportance {
|
|
bg = theme.ErrorColor()
|
|
} else if b.Importance == WarningImportance {
|
|
bg = theme.WarningColor()
|
|
} else if b.Importance == SuccessImportance {
|
|
bg = theme.SuccessColor()
|
|
}
|
|
|
|
return blendColor(bg, theme.FocusColor())
|
|
case b.hovered:
|
|
bg := theme.ButtonColor()
|
|
if b.Importance == HighImportance {
|
|
bg = theme.PrimaryColor()
|
|
} else if b.Importance == DangerImportance {
|
|
bg = theme.ErrorColor()
|
|
} else if b.Importance == WarningImportance {
|
|
bg = theme.WarningColor()
|
|
} else if b.Importance == SuccessImportance {
|
|
bg = theme.SuccessColor()
|
|
}
|
|
|
|
return blendColor(bg, theme.HoverColor())
|
|
case b.Importance == HighImportance:
|
|
return theme.PrimaryColor()
|
|
case b.Importance == LowImportance:
|
|
return color.Transparent
|
|
case b.Importance == DangerImportance:
|
|
return theme.ErrorColor()
|
|
case b.Importance == WarningImportance:
|
|
return theme.WarningColor()
|
|
case b.Importance == SuccessImportance:
|
|
return theme.SuccessColor()
|
|
default:
|
|
return theme.ButtonColor()
|
|
}
|
|
}
|
|
|
|
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)
|
|
|
|
hasIcon := r.icon != nil
|
|
hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
|
|
if !hasIcon && !hasLabel {
|
|
// Nothing to layout
|
|
return
|
|
}
|
|
iconSize := fyne.NewSquareSize(theme.IconInlineSize())
|
|
labelSize := r.label.MinSize()
|
|
padding := r.padding()
|
|
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) {
|
|
hasIcon := r.icon != nil
|
|
hasLabel := r.label.Segments[0].(*TextSegment).Text != ""
|
|
iconSize := fyne.NewSquareSize(theme.IconInlineSize())
|
|
labelSize := r.label.MinSize()
|
|
if hasLabel {
|
|
size.Width = labelSize.Width
|
|
}
|
|
if hasIcon {
|
|
if hasLabel {
|
|
size.Width += theme.Padding()
|
|
}
|
|
size.Width += iconSize.Width
|
|
}
|
|
size.Height = fyne.Max(labelSize.Height, iconSize.Height)
|
|
size = size.Add(r.padding())
|
|
return
|
|
}
|
|
|
|
func (r *buttonRenderer) Refresh() {
|
|
r.label.inset = fyne.NewSize(theme.InnerPadding(), theme.InnerPadding())
|
|
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
|
|
func (r *buttonRenderer) applyTheme() {
|
|
r.button.applyButtonTheme()
|
|
r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
|
|
switch {
|
|
case r.button.disabled:
|
|
r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameDisabled
|
|
case r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance:
|
|
if r.button.focused {
|
|
r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameForeground
|
|
} else {
|
|
r.label.Segments[0].(*TextSegment).Style.ColorName = theme.ColorNameBackground
|
|
}
|
|
}
|
|
r.label.Refresh()
|
|
if r.icon != nil && r.icon.Resource != nil {
|
|
switch res := r.icon.Resource.(type) {
|
|
case *theme.ThemedResource:
|
|
if r.button.Importance == HighImportance || r.button.Importance == DangerImportance || r.button.Importance == WarningImportance || r.button.Importance == SuccessImportance {
|
|
r.icon.Resource = theme.NewInvertedThemedResource(res)
|
|
r.icon.Refresh()
|
|
}
|
|
case *theme.InvertedThemedResource:
|
|
if r.button.Importance != HighImportance && r.button.Importance != DangerImportance && r.button.Importance != WarningImportance && r.button.Importance != SuccessImportance {
|
|
r.icon.Resource = res.Original()
|
|
r.icon.Refresh()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *buttonRenderer) padding() fyne.Size {
|
|
return fyne.NewSquareSize(theme.InnerPadding() * 2)
|
|
}
|
|
|
|
func (r *buttonRenderer) updateIconAndText() {
|
|
if r.button.Icon != nil && r.button.Visible() {
|
|
if r.icon == nil {
|
|
r.icon = canvas.NewImageFromResource(r.button.Icon)
|
|
r.icon.FillMode = canvas.ImageFillContain
|
|
r.SetObjects([]fyne.CanvasObject{r.background, r.tapBG, r.label, r.icon})
|
|
}
|
|
if r.button.Disabled() {
|
|
r.icon.Resource = theme.NewDisabledResource(r.button.Icon)
|
|
} else {
|
|
r.icon.Resource = r.button.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) *fyne.Animation {
|
|
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(theme.PressedColor())
|
|
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)
|
|
})
|
|
}
|