adam-gui/vendor/fyne.io/fyne/v2/widget/button.go
2024-04-29 19:13:50 +02:00

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)
})
}