diff --git a/main.go b/main.go index c87f1bf..1a601d6 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "image/color" "strconv" "strings" @@ -62,6 +63,11 @@ func main() { } } + clearHistory := func() { + calculations = nil + historyList.Refresh() + } + // Roll button rollButton := newCustomButton2WithImportance("ROLL", widget.HighImportance, roll) @@ -69,87 +75,129 @@ func main() { roll() } - buttons := []fyne.CanvasObject{ - newCustomButton2("H", func() { - diceInputEntry.SetText(diceInputEntry.Text + "H") - }), - newCustomButton2("dX", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d") - }), - newCustomButton2("d4", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d4") - }), - newCustomButton2("d6", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d6") - }), - newCustomButton2("d8", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d8") - }), - newCustomButton2("L", func() { - diceInputEntry.SetText(diceInputEntry.Text + "L") - }), - newCustomButton2("d10", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d10") - }), - newCustomButton2("d12", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d12") - }), - newCustomButton2("d20", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d20") - }), - newCustomButton2("d100", func() { - diceInputEntry.SetText(diceInputEntry.Text + "d100") - }), - createCalcButton("7", diceInputEntry), - createCalcButton("8", diceInputEntry), - createCalcButton("9", diceInputEntry), - newCustomButton2("*", func() { - diceInputEntry.SetText(diceInputEntry.Text + "*") - }), - newCustomButton2("BKSP", func() { - text := diceInputEntry.Text - if len(text) > 0 { - diceInputEntry.SetText(text[:len(text)-1]) - } - }), - createCalcButton("4", diceInputEntry), - createCalcButton("5", diceInputEntry), - createCalcButton("6", diceInputEntry), - newCustomButton2("/", func() { - diceInputEntry.SetText(diceInputEntry.Text + "/") - }), - newCustomButton2("C", func() { - diceInputEntry.SetText("") - }), - createCalcButton("1", diceInputEntry), - createCalcButton("2", diceInputEntry), - createCalcButton("3", diceInputEntry), - newCustomButton2("+", func() { - diceInputEntry.SetText(diceInputEntry.Text + "+") - }), - newCustomButton2("📊", func() { - diceInput := strings.TrimSpace(diceInputEntry.Text) - if diceInput == "" { - return - } - ShowStatisticsWindow(diceInput) - roll() - }), - newCustomButton2(".", func() { - diceInputEntry.SetText(diceInputEntry.Text + ".") - }), - createCalcButton("0", diceInputEntry), - newCustomButton2("^", func() { - diceInputEntry.SetText(diceInputEntry.Text + "^") - }), - newCustomButton2("-", func() { - diceInputEntry.SetText(diceInputEntry.Text + "-") - }), - rollButton, + graphButton := newCustomButton2WithColors("📊", color.NRGBA{R: 46, G: 160, B: 67, A: 255}, color.White, func() { + diceInput := strings.TrimSpace(diceInputEntry.Text) + if diceInput == "" { + return + } + ShowStatisticsWindow(diceInput) + roll() + }) + + lButton := newCustomButton2("L", func() { + diceInputEntry.SetText(diceInputEntry.Text + "L") + }) + hButton := newCustomButton2("H", func() { + diceInputEntry.SetText(diceInputEntry.Text + "H") + }) + dxButton := newCustomButton2("dX", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d") + }) + d4Button := newCustomButton2("d4", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d4") + }) + d6Button := newCustomButton2("d6", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d6") + }) + d8Button := newCustomButton2("d8", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d8") + }) + d10Button := newCustomButton2("d10", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d10") + }) + d12Button := newCustomButton2("d12", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d12") + }) + d20Button := newCustomButton2("d20", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d20") + }) + d100Button := newCustomButton2("d100", func() { + diceInputEntry.SetText(diceInputEntry.Text + "d100") + }) + sevenButton := createCalcButton("7", diceInputEntry) + eightButton := createCalcButton("8", diceInputEntry) + nineButton := createCalcButton("9", diceInputEntry) + fourButton := createCalcButton("4", diceInputEntry) + fiveButton := createCalcButton("5", diceInputEntry) + sixButton := createCalcButton("6", diceInputEntry) + oneButton := createCalcButton("1", diceInputEntry) + twoButton := createCalcButton("2", diceInputEntry) + threeButton := createCalcButton("3", diceInputEntry) + zeroButton := createCalcButton("0", diceInputEntry) + multiplyButton := newCustomButton2("*", func() { + diceInputEntry.SetText(diceInputEntry.Text + "*") + }) + backspaceButton := newCustomButton2("BKSP", func() { + text := diceInputEntry.Text + if len(text) > 0 { + diceInputEntry.SetText(text[:len(text)-1]) + } + }) + divideButton := newCustomButton2("/", func() { + diceInputEntry.SetText(diceInputEntry.Text + "/") + }) + clearButton := newCustomButton2("C", func() { + diceInputEntry.SetText("") + }) + addButton := newCustomButton2("+", func() { + diceInputEntry.SetText(diceInputEntry.Text + "+") + }) + acButton := newCustomButton2WithColors("AC", color.NRGBA{R: 198, G: 40, B: 40, A: 255}, color.White, clearHistory) + decimalButton := newCustomButton2(".", func() { + diceInputEntry.SetText(diceInputEntry.Text + ".") + }) + powerButton := newCustomButton2("^", func() { + diceInputEntry.SetText(diceInputEntry.Text + "^") + }) + subtractButton := newCustomButton2("-", func() { + diceInputEntry.SetText(diceInputEntry.Text + "-") + }) + + keypadButtons := []fyne.CanvasObject{ + lButton, hButton, dxButton, d4Button, d6Button, graphButton, + d8Button, d10Button, d12Button, d20Button, d100Button, + sevenButton, eightButton, nineButton, multiplyButton, backspaceButton, rollButton, + fourButton, fiveButton, sixButton, divideButton, clearButton, + oneButton, twoButton, threeButton, addButton, acButton, + decimalButton, zeroButton, powerButton, subtractButton, } - buttonsContainer := container.New(newAspectRatioLayout(3.0/2.0, 7, 5)) - for _, button := range buttons { + buttonSpans := map[fyne.CanvasObject]gridSpan{ + lButton: {row: 0, col: 0, rowSpan: 1, colSpan: 1}, + hButton: {row: 0, col: 1, rowSpan: 1, colSpan: 1}, + dxButton: {row: 0, col: 2, rowSpan: 1, colSpan: 1}, + d4Button: {row: 0, col: 3, rowSpan: 1, colSpan: 1}, + d6Button: {row: 0, col: 4, rowSpan: 1, colSpan: 1}, + graphButton: {row: 0, col: 5, rowSpan: 2, colSpan: 1}, + d8Button: {row: 1, col: 0, rowSpan: 1, colSpan: 1}, + d10Button: {row: 1, col: 1, rowSpan: 1, colSpan: 1}, + d12Button: {row: 1, col: 2, rowSpan: 1, colSpan: 1}, + d20Button: {row: 1, col: 3, rowSpan: 1, colSpan: 1}, + d100Button: {row: 1, col: 4, rowSpan: 1, colSpan: 1}, + sevenButton: {row: 2, col: 0, rowSpan: 1, colSpan: 1}, + eightButton: {row: 2, col: 1, rowSpan: 1, colSpan: 1}, + nineButton: {row: 2, col: 2, rowSpan: 1, colSpan: 1}, + multiplyButton: {row: 2, col: 3, rowSpan: 1, colSpan: 1}, + backspaceButton: {row: 2, col: 4, rowSpan: 1, colSpan: 1}, + rollButton: {row: 2, col: 5, rowSpan: 4, colSpan: 1}, + fourButton: {row: 3, col: 0, rowSpan: 1, colSpan: 1}, + fiveButton: {row: 3, col: 1, rowSpan: 1, colSpan: 1}, + sixButton: {row: 3, col: 2, rowSpan: 1, colSpan: 1}, + divideButton: {row: 3, col: 3, rowSpan: 1, colSpan: 1}, + clearButton: {row: 3, col: 4, rowSpan: 1, colSpan: 1}, + oneButton: {row: 4, col: 0, rowSpan: 1, colSpan: 1}, + twoButton: {row: 4, col: 1, rowSpan: 1, colSpan: 1}, + threeButton: {row: 4, col: 2, rowSpan: 1, colSpan: 1}, + addButton: {row: 4, col: 3, rowSpan: 1, colSpan: 1}, + acButton: {row: 4, col: 4, rowSpan: 2, colSpan: 1}, + decimalButton: {row: 5, col: 0, rowSpan: 1, colSpan: 1}, + zeroButton: {row: 5, col: 1, rowSpan: 1, colSpan: 1}, + powerButton: {row: 5, col: 2, rowSpan: 1, colSpan: 1}, + subtractButton: {row: 5, col: 3, rowSpan: 1, colSpan: 1}, + } + + buttonsContainer := container.New(newSpanGridLayout(6, 6, buttonSpans)) + for _, button := range keypadButtons { buttonsContainer.Add(button) } diff --git a/ui.go b/ui.go index 1cefe5d..ddfc81f 100644 --- a/ui.go +++ b/ui.go @@ -2,6 +2,7 @@ package main import ( "image/color" + "unicode/utf8" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" @@ -80,6 +81,9 @@ type customButton2 struct { Text string OnTapped func() Importance widget.Importance + TextSize float32 + Background color.Color + Foreground color.Color } func (b *customButton2) CreateRenderer() fyne.WidgetRenderer { @@ -122,9 +126,12 @@ type customButton2Renderer struct { func (r *customButton2Renderer) Layout(size fyne.Size) { r.background.Resize(size) r.text.Resize(size) - newTextSize := size.Height * 0.4 - if size.Width < newTextSize { - newTextSize = size.Width * 0.4 + newTextSize := r.button.TextSize + if newTextSize <= 0 { + newTextSize = size.Height * 0.4 + if size.Width < newTextSize { + newTextSize = size.Width * 0.4 + } } r.text.TextSize = newTextSize } @@ -135,6 +142,7 @@ func (r *customButton2Renderer) MinSize() fyne.Size { func (r *customButton2Renderer) Refresh() { r.text.Text = r.button.Text + r.text.TextSize = r.button.TextSize r.text.Color = r.button.textColor() r.text.Refresh() r.background.FillColor = r.button.backgroundColor() @@ -149,6 +157,9 @@ func (r *customButton2Renderer) Destroy() { } func (b *customButton2) backgroundColor() color.Color { + if b.Background != nil { + return b.Background + } variant := fyne.CurrentApp().Settings().ThemeVariant() switch b.Importance { case widget.HighImportance: @@ -159,6 +170,9 @@ func (b *customButton2) backgroundColor() color.Color { } func (b *customButton2) textColor() color.Color { + if b.Foreground != nil { + return b.Foreground + } variant := fyne.CurrentApp().Settings().ThemeVariant() switch b.Importance { case widget.HighImportance: @@ -187,6 +201,17 @@ func newCustomButton2WithImportance(label string, importance widget.Importance, return b } +func newCustomButton2WithColors(label string, background, foreground color.Color, onTap func()) *customButton2 { + b := &customButton2{ + Text: label, + OnTapped: onTap, + Background: background, + Foreground: foreground, + } + b.ExtendBaseWidget(b) + return b +} + // customEntry is a custom entry widget with configurable text size type customEntry struct { widget.Entry @@ -302,3 +327,103 @@ func (a *aspectRatioLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) func (a *aspectRatioLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { return fyne.NewSize(10, 10) } + +type gridSpan struct { + row int + col int + rowSpan int + colSpan int +} + +type spanGridLayout struct { + rows int + cols int + spans map[fyne.CanvasObject]gridSpan +} + +func newSpanGridLayout(rows, cols int, spans map[fyne.CanvasObject]gridSpan) fyne.Layout { + return &spanGridLayout{ + rows: rows, + cols: cols, + spans: spans, + } +} + +func (s *spanGridLayout) Layout(objects []fyne.CanvasObject, size fyne.Size) { + if s.rows <= 0 || s.cols <= 0 { + return + } + + cellWidth := size.Width / float32(s.cols) + cellHeight := size.Height / float32(s.rows) + uniformTextSize := float32(0) + + for _, o := range objects { + button, ok := o.(*customButton2) + if !ok { + continue + } + + span, ok := s.spans[o] + if !ok { + continue + } + + rowSpan := max(span.rowSpan, 1) + colSpan := max(span.colSpan, 1) + buttonSize := fyne.NewSize(float32(colSpan)*cellWidth, float32(rowSpan)*cellHeight) + candidateSize := maxUniformButtonTextSize(button.Text, buttonSize) + if candidateSize <= 0 { + continue + } + if uniformTextSize == 0 || candidateSize < uniformTextSize { + uniformTextSize = candidateSize + } + } + + for _, o := range objects { + if button, ok := o.(*customButton2); ok { + button.TextSize = uniformTextSize + } + + span, ok := s.spans[o] + if !ok { + continue + } + + rowSpan := max(span.rowSpan, 1) + colSpan := max(span.colSpan, 1) + + x := float32(span.col) * cellWidth + y := float32(span.row) * cellHeight + width := float32(colSpan) * cellWidth + height := float32(rowSpan) * cellHeight + + o.Move(fyne.NewPos(x, y)) + o.Resize(fyne.NewSize(width, height)) + } +} + +func (s *spanGridLayout) MinSize(objects []fyne.CanvasObject) fyne.Size { + return fyne.NewSize(10, 10) +} + +func maxUniformButtonTextSize(label string, size fyne.Size) float32 { + const ( + horizontalPaddingFactor = 0.75 + heightFactor = 0.42 + avgCharWidthFactor = 0.62 + ) + + maxByHeight := size.Height * heightFactor + charCount := utf8.RuneCountInString(label) + if charCount <= 0 { + return maxByHeight + } + + maxByWidth := (size.Width * horizontalPaddingFactor) / (float32(charCount) * avgCharWidthFactor) + if maxByWidth < maxByHeight { + return maxByWidth + } + return maxByHeight +}