Merge pull request #207 from dweymouth/feature/equalizer
Add 15 band graphic equalizer in Settings dialog
This commit is contained in:
@@ -192,6 +192,13 @@ func (a *App) setupMPV() error {
|
||||
})
|
||||
a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
|
||||
|
||||
eq := &player.ISO15BandEqualizer{
|
||||
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
|
||||
Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
|
||||
}
|
||||
copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands)
|
||||
a.Player.SetEqualizer(eq)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ type LocalPlaybackConfig struct {
|
||||
AudioExclusive bool
|
||||
InMemoryCacheSizeMB int
|
||||
Volume int
|
||||
EqualizerEnabled bool
|
||||
EqualizerPreamp float64
|
||||
GraphicEqualizerBands []float64
|
||||
}
|
||||
|
||||
type ScrobbleConfig struct {
|
||||
@@ -155,6 +158,9 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
AudioExclusive: false,
|
||||
InMemoryCacheSizeMB: 30,
|
||||
Volume: 100,
|
||||
EqualizerEnabled: false,
|
||||
EqualizerPreamp: 0,
|
||||
GraphicEqualizerBands: make([]float64, 15),
|
||||
},
|
||||
Scrobbling: ScrobbleConfig{
|
||||
Enabled: true,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
package player
|
||||
|
||||
// Equalizer implementations based on the ffmpeg 'equalizer' filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Equalizer interface {
|
||||
IsEnabled() bool
|
||||
Preamp() float64
|
||||
Curve() EqualizerCurve
|
||||
Type() string
|
||||
// Returns the band frequencies as strings friendly for display
|
||||
BandFrequencies() []string
|
||||
}
|
||||
|
||||
type ISO15BandEqualizer struct {
|
||||
Disabled bool
|
||||
EQPreamp float64
|
||||
BandGains [15]float64
|
||||
}
|
||||
|
||||
var (
|
||||
iso15Bands = []string{"25", "40", "63", "100", "160", "250", "400", "630", "1k", "1.6k", "2.5k", "4k", "6.3k", "10k", "16k"}
|
||||
iso15FMult = math.Pow(2, 2./3)
|
||||
)
|
||||
|
||||
var _ Equalizer = (*ISO15BandEqualizer)(nil)
|
||||
|
||||
func (i *ISO15BandEqualizer) IsEnabled() bool {
|
||||
return !i.Disabled
|
||||
}
|
||||
|
||||
func (i *ISO15BandEqualizer) Preamp() float64 {
|
||||
return i.EQPreamp
|
||||
}
|
||||
|
||||
func (i *ISO15BandEqualizer) Curve() EqualizerCurve {
|
||||
fC := float64(25)
|
||||
curve := make([]EqualizerBand, 0, len(i.BandGains))
|
||||
for _, bandGain := range i.BandGains {
|
||||
curve = append(curve, EqualizerBand{
|
||||
Frequency: int(math.Round(fC)),
|
||||
Width: 2. / 3,
|
||||
WidthType: WidthTypeOctave,
|
||||
Gain: bandGain,
|
||||
})
|
||||
fC *= iso15FMult
|
||||
}
|
||||
return curve
|
||||
}
|
||||
|
||||
func (*ISO15BandEqualizer) BandFrequencies() []string {
|
||||
ret := make([]string, len(iso15Bands))
|
||||
copy(ret, iso15Bands)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (*ISO15BandEqualizer) Type() string {
|
||||
return "ISO15Band"
|
||||
}
|
||||
|
||||
type WidthType int
|
||||
|
||||
const (
|
||||
WidthTypeHz WidthType = iota
|
||||
WidthTypeKhz
|
||||
WidthTypeQ
|
||||
WidthTypeOctave
|
||||
WidthTypeSlope
|
||||
)
|
||||
|
||||
type EqualizerBand struct {
|
||||
Frequency int
|
||||
Gain float64
|
||||
Width float64
|
||||
WidthType WidthType
|
||||
}
|
||||
|
||||
type EqualizerCurve []EqualizerBand
|
||||
|
||||
func (e EqualizerCurve) String() string {
|
||||
var sb strings.Builder
|
||||
first := true
|
||||
for _, band := range e {
|
||||
if s := band.String(); s != "" {
|
||||
if !first {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(s)
|
||||
first = false
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (e EqualizerBand) String() string {
|
||||
if math.Abs(e.Gain) < 0.02 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("equalizer=f=%d:g=%0.2f:t=%s:w=%0.2f",
|
||||
e.Frequency, e.Gain, e.WidthType.String(), e.Width)
|
||||
}
|
||||
|
||||
func (w WidthType) String() string {
|
||||
switch w {
|
||||
case WidthTypeHz:
|
||||
return "h"
|
||||
case WidthTypeKhz:
|
||||
return "k"
|
||||
case WidthTypeQ:
|
||||
return "q"
|
||||
case WidthTypeOctave:
|
||||
return "o"
|
||||
case WidthTypeSlope:
|
||||
return "s"
|
||||
}
|
||||
return "x" // not reached
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/dweymouth/go-mpv"
|
||||
@@ -113,6 +114,7 @@ type Player struct {
|
||||
curPlaylistPos int64
|
||||
prePausedState State
|
||||
clientName string
|
||||
equalizer Equalizer
|
||||
|
||||
bgCancel context.CancelFunc
|
||||
|
||||
@@ -501,6 +503,28 @@ func (p *Player) SetAudioDevice(deviceName string) error {
|
||||
return p.mpv.SetPropertyString("audio-device", deviceName)
|
||||
}
|
||||
|
||||
func (p *Player) SetEqualizer(eq Equalizer) error {
|
||||
p.equalizer = eq
|
||||
if eq == nil || !eq.IsEnabled() {
|
||||
return p.mpv.SetPropertyString("af", "")
|
||||
}
|
||||
af := ""
|
||||
if math.Abs(eq.Preamp()) > 0.01 {
|
||||
af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp())
|
||||
}
|
||||
eqAF := eq.Curve().String()
|
||||
if af == "" {
|
||||
af = eqAF
|
||||
} else if eqAF != "" {
|
||||
af = fmt.Sprintf("%s,%s", af, eqAF)
|
||||
}
|
||||
return p.mpv.SetPropertyString("af", af)
|
||||
}
|
||||
|
||||
func (p *Player) Equalizer() Equalizer {
|
||||
return p.equalizer
|
||||
}
|
||||
|
||||
func (p *Player) GetMediaInfo() (MediaInfo, error) {
|
||||
var info MediaInfo
|
||||
n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE)
|
||||
|
||||
@@ -453,7 +453,8 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
||||
devs = []player.AudioDevice{{Name: "auto", Description: "Autoselect device"}}
|
||||
}
|
||||
|
||||
dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, c.MainWindow)
|
||||
bands := c.App.Player.Equalizer().BandFrequencies()
|
||||
dlg := dialogs.NewSettingsDialog(c.App.Config, devs, themeFiles, bands, c.MainWindow)
|
||||
dlg.OnReplayGainSettingsChanged = func() {
|
||||
c.App.PlaybackManager.SetReplayGainOptions(c.App.Config.ReplayGain)
|
||||
}
|
||||
@@ -464,6 +465,14 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
|
||||
c.App.Player.SetAudioDevice(c.App.Config.LocalPlayback.AudioDeviceName)
|
||||
}
|
||||
dlg.OnThemeSettingChanged = themeUpdateCallbk
|
||||
dlg.OnEqualizerSettingsChanged = func() {
|
||||
// currently we only have one equalizer type
|
||||
eq := c.App.Player.Equalizer().(*player.ISO15BandEqualizer)
|
||||
eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled
|
||||
eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp
|
||||
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
|
||||
c.App.Player.SetEqualizer(eq)
|
||||
}
|
||||
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
||||
dlg.OnDismiss = func() {
|
||||
pop.Hide()
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
package dialogs
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/supersonic/ui/layouts"
|
||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||
"github.com/dweymouth/supersonic/ui/util"
|
||||
)
|
||||
|
||||
type GraphicEqualizer struct {
|
||||
widget.BaseWidget
|
||||
|
||||
OnChanged func(band int, gain float64)
|
||||
OnPreampChanged func(gain float64)
|
||||
|
||||
container *fyne.Container
|
||||
}
|
||||
|
||||
func NewGraphicEqualizer(preamp float64, bandFreqs []string, bandGains []float64) *GraphicEqualizer {
|
||||
g := &GraphicEqualizer{}
|
||||
g.ExtendBaseWidget(g)
|
||||
g.buildSliders(preamp, bandFreqs, bandGains)
|
||||
|
||||
return g
|
||||
}
|
||||
|
||||
func (g *GraphicEqualizer) buildSliders(preamp float64, bands []string, bandGains []float64) {
|
||||
rng := container.NewVBox(
|
||||
newCaptionTextSizeLabel("+12", fyne.TextAlignTrailing),
|
||||
layout.NewSpacer(),
|
||||
newCaptionTextSizeLabel("0", fyne.TextAlignTrailing),
|
||||
layout.NewSpacer(),
|
||||
newCaptionTextSizeLabel("-12", fyne.TextAlignTrailing),
|
||||
)
|
||||
bandSliders := container.New(layouts.NewGridLayoutWithColumnsAndPadding(len(bands)+2, -16))
|
||||
pre := newCaptionTextSizeLabel("Pre", fyne.TextAlignCenter)
|
||||
preampSlider := newEQSlider()
|
||||
preampSlider.SetValue(preamp)
|
||||
preampSlider.OnChanged = func(f float64) {
|
||||
if g.OnPreampChanged != nil {
|
||||
g.OnPreampChanged(f)
|
||||
}
|
||||
}
|
||||
bandSliders.Add(container.NewBorder(nil, pre, nil, nil, preampSlider))
|
||||
bandSliders.Add(container.NewBorder(nil, widget.NewLabel(""), nil, nil, rng))
|
||||
for i, band := range bands {
|
||||
s := newEQSlider()
|
||||
if i < len(bandGains) {
|
||||
s.SetValue(bandGains[i])
|
||||
}
|
||||
s.OnChanged = func(i int) func(float64) {
|
||||
return func(f float64) {
|
||||
if g.OnChanged != nil {
|
||||
g.OnChanged(i, f)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
l := newCaptionTextSizeLabel(band, fyne.TextAlignCenter)
|
||||
c := container.NewBorder(nil, l, nil, nil, s)
|
||||
bandSliders.Add(c)
|
||||
}
|
||||
g.container = container.NewMax(
|
||||
container.NewBorder(nil, widget.NewLabel(""), nil, nil,
|
||||
container.NewBorder(nil, nil, util.NewHSpace(5), util.NewHSpace(5),
|
||||
container.NewVBox(
|
||||
layout.NewSpacer(),
|
||||
myTheme.NewThemedRectangle(theme.ColorNameInputBackground),
|
||||
layout.NewSpacer(),
|
||||
),
|
||||
),
|
||||
),
|
||||
bandSliders,
|
||||
)
|
||||
}
|
||||
|
||||
func newCaptionTextSizeLabel(text string, alignment fyne.TextAlign) *widget.RichText {
|
||||
l := widget.NewRichTextWithText(text)
|
||||
ts := l.Segments[0].(*widget.TextSegment)
|
||||
ts.Style.SizeName = theme.SizeNameCaptionText
|
||||
ts.Style.Alignment = alignment
|
||||
return l
|
||||
}
|
||||
|
||||
func (g *GraphicEqualizer) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(g.container)
|
||||
}
|
||||
|
||||
type eqSlider struct {
|
||||
widget.Slider
|
||||
tappedAt int64
|
||||
}
|
||||
|
||||
func newEQSlider() *eqSlider {
|
||||
s := &eqSlider{
|
||||
Slider: widget.Slider{
|
||||
Orientation: widget.Vertical,
|
||||
Min: -12,
|
||||
Max: 12,
|
||||
Step: 0.1,
|
||||
},
|
||||
}
|
||||
s.ExtendBaseWidget(s)
|
||||
return s
|
||||
}
|
||||
|
||||
// We implement our own double tapping so that the Tapped behavior
|
||||
// can be triggered instantly.
|
||||
func (s *eqSlider) DoubleTapped(e *fyne.PointEvent) {
|
||||
s.SetValue(0)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend"
|
||||
@@ -35,6 +36,7 @@ type SettingsDialog struct {
|
||||
OnAudioDeviceSettingChanged func()
|
||||
OnThemeSettingChanged func()
|
||||
OnDismiss func()
|
||||
OnEqualizerSettingsChanged func()
|
||||
|
||||
config *backend.Config
|
||||
audioDevices []player.AudioDevice
|
||||
@@ -45,13 +47,20 @@ type SettingsDialog struct {
|
||||
}
|
||||
|
||||
// TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor.
|
||||
func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDevice, themeFileList map[string]string, window fyne.Window) *SettingsDialog {
|
||||
func NewSettingsDialog(
|
||||
config *backend.Config,
|
||||
audioDeviceList []player.AudioDevice,
|
||||
themeFileList map[string]string,
|
||||
equalizerBands []string,
|
||||
window fyne.Window,
|
||||
) *SettingsDialog {
|
||||
s := &SettingsDialog{config: config, audioDevices: audioDeviceList, themeFiles: themeFileList}
|
||||
s.ExtendBaseWidget(s)
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
s.createGeneralTab(),
|
||||
s.createPlaybackTab(),
|
||||
s.createEqualizerTab(equalizerBands),
|
||||
s.createExperimentalTab(window),
|
||||
)
|
||||
// workaround issue where inactivated tabs don't fully update when theme setting is changed
|
||||
@@ -320,6 +329,34 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem {
|
||||
))
|
||||
}
|
||||
|
||||
func (s *SettingsDialog) createEqualizerTab(eqBands []string) *container.TabItem {
|
||||
enabled := widget.NewCheck("Enabled", func(b bool) {
|
||||
s.config.LocalPlayback.EqualizerEnabled = b
|
||||
if s.OnEqualizerSettingsChanged != nil {
|
||||
s.OnEqualizerSettingsChanged()
|
||||
}
|
||||
})
|
||||
enabled.Checked = s.config.LocalPlayback.EqualizerEnabled
|
||||
geq := NewGraphicEqualizer(s.config.LocalPlayback.EqualizerPreamp,
|
||||
eqBands,
|
||||
s.config.LocalPlayback.GraphicEqualizerBands)
|
||||
debouncer := util.NewDebouncer(350*time.Millisecond, func() {
|
||||
if s.OnEqualizerSettingsChanged != nil {
|
||||
s.OnEqualizerSettingsChanged()
|
||||
}
|
||||
})
|
||||
geq.OnChanged = func(b int, g float64) {
|
||||
s.config.LocalPlayback.GraphicEqualizerBands[b] = g
|
||||
debouncer()
|
||||
}
|
||||
geq.OnPreampChanged = func(g float64) {
|
||||
s.config.LocalPlayback.EqualizerPreamp = g
|
||||
debouncer()
|
||||
}
|
||||
cont := container.NewBorder(container.NewHBox(enabled), nil, nil, nil, geq)
|
||||
return container.NewTabItem("Equalizer", cont)
|
||||
}
|
||||
|
||||
func (s *SettingsDialog) createExperimentalTab(window fyne.Window) *container.TabItem {
|
||||
warningLabel := widget.NewLabel("WARNING: these settings are experimental and may " +
|
||||
"make the application buggy or increase system resource use. " +
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package layouts
|
||||
|
||||
// Forked from fyne.io/fyne/v2/layout/gridlayout.go
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
)
|
||||
|
||||
// Declare conformity with Layout interface
|
||||
var _ fyne.Layout = (*gridLayout)(nil)
|
||||
|
||||
type gridLayout struct {
|
||||
Cols int
|
||||
Padding float32
|
||||
vertical, adapt bool
|
||||
}
|
||||
|
||||
// NewAdaptiveGridLayout returns a new grid layout which uses columns when horizontal but rows when vertical.
|
||||
func NewAdaptiveGridLayout(rowcols int) fyne.Layout {
|
||||
return &gridLayout{Cols: rowcols, adapt: true}
|
||||
}
|
||||
|
||||
// NewGridLayout returns a grid layout arranged in a specified number of columns.
|
||||
// The number of rows will depend on how many children are in the container that uses this layout.
|
||||
func NewGridLayout(cols int) fyne.Layout {
|
||||
return NewGridLayoutWithColumns(cols)
|
||||
}
|
||||
|
||||
// NewGridLayoutWithColumns returns a new grid layout that specifies a column count and wrap to new rows when needed.
|
||||
func NewGridLayoutWithColumns(cols int) fyne.Layout {
|
||||
return &gridLayout{Cols: cols}
|
||||
}
|
||||
|
||||
// NewGridLayoutWithRows returns a new grid layout that specifies a row count that creates new rows as required.
|
||||
func NewGridLayoutWithRows(rows int) fyne.Layout {
|
||||
return &gridLayout{Cols: rows, vertical: true}
|
||||
}
|
||||
|
||||
func NewGridLayoutWithColumnsAndPadding(cols int, padding float32) fyne.Layout {
|
||||
return &gridLayout{Cols: cols, Padding: padding}
|
||||
}
|
||||
|
||||
func (g *gridLayout) horizontal() bool {
|
||||
if g.adapt {
|
||||
return fyne.IsHorizontal(fyne.CurrentDevice().Orientation())
|
||||
}
|
||||
|
||||
return !g.vertical
|
||||
}
|
||||
|
||||
func (g *gridLayout) countRows(objects []fyne.CanvasObject) int {
|
||||
if g.Cols < 1 {
|
||||
g.Cols = 1
|
||||
}
|
||||
count := 0
|
||||
for _, child := range objects {
|
||||
if child.Visible() {
|
||||
count++
|
||||
}
|
||||
}
|
||||
|
||||
return int(math.Ceil(float64(count) / float64(g.Cols)))
|
||||
}
|
||||
|
||||
// Get the leading (top or left) edge of a grid cell.
|
||||
// size is the ideal cell size and the offset is which col or row its on.
|
||||
func getLeading(size float64, padding float32, offset int) float32 {
|
||||
ret := (size + float64(padding)) * float64(offset)
|
||||
|
||||
return float32(ret)
|
||||
}
|
||||
|
||||
// Get the trailing (bottom or right) edge of a grid cell.
|
||||
// size is the ideal cell size and the offset is which col or row its on.
|
||||
func getTrailing(size float64, padding float32, offset int) float32 {
|
||||
return getLeading(size, padding, offset+1) - padding
|
||||
}
|
||||
|
||||
// Layout is called to pack all child objects into a specified size.
|
||||
// For a GridLayout this will pack objects into a table format with the number
|
||||
// of columns specified in our constructor.
|
||||
func (g *gridLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) {
|
||||
rows := g.countRows(objects)
|
||||
|
||||
padding := theme.Padding() + g.Padding
|
||||
padWidth := float32(g.Cols-1) * padding
|
||||
padHeight := float32(rows-1) * padding
|
||||
cellWidth := float64(size.Width-padWidth) / float64(g.Cols)
|
||||
cellHeight := float64(size.Height-padHeight) / float64(rows)
|
||||
|
||||
if !g.horizontal() {
|
||||
padWidth, padHeight = padHeight, padWidth
|
||||
cellWidth = float64(size.Width-padWidth) / float64(rows)
|
||||
cellHeight = float64(size.Height-padHeight) / float64(g.Cols)
|
||||
}
|
||||
|
||||
row, col := 0, 0
|
||||
i := 0
|
||||
for _, child := range objects {
|
||||
if !child.Visible() {
|
||||
continue
|
||||
}
|
||||
|
||||
x1 := getLeading(cellWidth, padding, col)
|
||||
y1 := getLeading(cellHeight, padding, row)
|
||||
x2 := getTrailing(cellWidth, padding, col)
|
||||
y2 := getTrailing(cellHeight, padding, row)
|
||||
|
||||
child.Move(fyne.NewPos(x1, y1))
|
||||
child.Resize(fyne.NewSize(x2-x1, y2-y1))
|
||||
|
||||
if g.horizontal() {
|
||||
if (i+1)%g.Cols == 0 {
|
||||
row++
|
||||
col = 0
|
||||
} else {
|
||||
col++
|
||||
}
|
||||
} else {
|
||||
if (i+1)%g.Cols == 0 {
|
||||
col++
|
||||
row = 0
|
||||
} else {
|
||||
row++
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
// MinSize finds the smallest size that satisfies all the child objects.
|
||||
// For a GridLayout this is the size of the largest child object multiplied by
|
||||
// the required number of columns and rows, with appropriate padding between
|
||||
// children.
|
||||
func (g *gridLayout) MinSize(objects []fyne.CanvasObject) fyne.Size {
|
||||
rows := g.countRows(objects)
|
||||
minSize := fyne.NewSize(0, 0)
|
||||
for _, child := range objects {
|
||||
if !child.Visible() {
|
||||
continue
|
||||
}
|
||||
|
||||
minSize = minSize.Max(child.MinSize())
|
||||
}
|
||||
|
||||
padding := theme.Padding() + g.Padding
|
||||
if g.horizontal() {
|
||||
minContentSize := fyne.NewSize(minSize.Width*float32(g.Cols), minSize.Height*float32(rows))
|
||||
return minContentSize.Add(fyne.NewSize(padding*fyne.Max(float32(g.Cols-1), 0), padding*fyne.Max(float32(rows-1), 0)))
|
||||
}
|
||||
|
||||
minContentSize := fyne.NewSize(minSize.Width*float32(rows), minSize.Height*float32(g.Cols))
|
||||
return minContentSize.Add(fyne.NewSize(padding*fyne.Max(float32(rows-1), 0), padding*fyne.Max(float32(g.Cols-1), 0)))
|
||||
}
|
||||
Reference in New Issue
Block a user