diff --git a/calculations.go b/calculations.go index 731b9ff..9789389 100644 --- a/calculations.go +++ b/calculations.go @@ -18,30 +18,31 @@ func init() { // CalculateDice parses a dice expression and returns the result // Supports formats like: 2d20, 3d6+5, 2d20H, 2d20L, 1d20+2d6, etc. -func CalculateDice(expression string) (float64, error) { +func CalculateDice(expression string) (float64, string, error) { expression = strings.TrimSpace(expression) if expression == "" { - return 0, fmt.Errorf("empty expression") + return 0, "", fmt.Errorf("empty expression") } // Expand all dice notations to their rolled values - expanded, err := expandDiceNotation(expression) + expanded, diceRolls, err := expandDiceNotation(expression) if err != nil { - return 0, err + return 0, "", err } // Evaluate the resulting mathematical expression result, err := evaluateMathExpression(expanded) if err != nil { - return 0, err + return 0, "", err } - return result, nil + return result, diceRolls, nil } // expandDiceNotation finds all dice notation in the expression and replaces them with rolled values -func expandDiceNotation(expression string) (string, error) { +func expandDiceNotation(expression string) (string, string, error) { result := expression + diceRollsStr := expression // Pattern to match dice notation: [count]d[sides][H|L] // Examples: d20, 2d6, 3d6H, 4d8L, dx (where x is placeholder) @@ -73,23 +74,27 @@ func expandDiceNotation(expression string) (string, error) { var err error count, err = strconv.Atoi(countStr) if err != nil || count <= 0 { - return "", fmt.Errorf("invalid dice count: %s", countStr) + return "", "", fmt.Errorf("invalid dice count: %s", countStr) } } // Determine sides var sides int if sidesStr == "x" { - return "", fmt.Errorf("dx requires a number (e.g., d20). Please use a specific die like d20 or d100") + return "", "", fmt.Errorf("dx requires a number (e.g., d20). Please use a specific die like d20 or d100") } var err error sides, err = strconv.Atoi(sidesStr) if err != nil || sides <= 0 { - return "", fmt.Errorf("invalid dice sides: %s", sidesStr) + return "", "", fmt.Errorf("invalid dice sides: %s", sidesStr) } // Roll the dice rolls := rollDiceSet(count, sides) + var rollsStr []string + for _, r := range rolls { + rollsStr = append(rollsStr, strconv.Itoa(r)) + } // Apply modifier (H for highest, L for lowest) var value float64 @@ -117,9 +122,10 @@ func expandDiceNotation(expression string) (string, error) { // Replace the dice notation with its value in the result string result = result[:start] + strconv.FormatFloat(value, 'f', -1, 64) + result[end:] + diceRollsStr = diceRollsStr[:start] + fmt.Sprintf("(%dd%d: %s)", count, sides, strings.Join(rollsStr, ", ")) + diceRollsStr[end:] } - return result, nil + return result, diceRollsStr, nil } // rollDiceSet rolls count dice with the given number of sides diff --git a/history.go b/history.go new file mode 100644 index 0000000..324cab3 --- /dev/null +++ b/history.go @@ -0,0 +1,95 @@ +package main + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +type calculation struct { + id int + equation string + diceRolls string + result string +} + +type historyItemRenderer struct { + item *historyItem + equationLabel *widget.Label + diceRollsLabel *widget.Label + resultLabel *widget.Label + objects []fyne.CanvasObject +} + +func (r *historyItemRenderer) MinSize() fyne.Size { + return r.objects[0].MinSize() +} + +func (r *historyItemRenderer) Layout(size fyne.Size) { + r.objects[0].Resize(size) +} + +func (r *historyItemRenderer) ApplyTheme() { +} + +func (r *historyItemRenderer) Refresh() { + r.equationLabel.SetText(r.item.calc.equation) + r.diceRollsLabel.SetText(r.item.calc.diceRolls) + r.resultLabel.SetText(r.item.calc.result) + r.equationLabel.Refresh() + r.diceRollsLabel.Refresh() + r.resultLabel.Refresh() +} + +func (r *historyItemRenderer) Objects() []fyne.CanvasObject { + return r.objects +} + +func (r *historyItemRenderer) Destroy() { +} + +type historyItem struct { + widget.BaseWidget + calc *calculation +} + +func (h *historyItem) CreateRenderer() fyne.WidgetRenderer { + h.ExtendBaseWidget(h) + equationLabel := widget.NewLabel(h.calc.equation) + equationLabel.TextStyle.Bold = true + + diceRollsLabel := widget.NewLabel(h.calc.diceRolls) + diceRollsLabel.TextStyle.Italic = true + + resultLabel := widget.NewLabel(h.calc.result) + resultLabel.Alignment = fyne.TextAlignTrailing + + layout := container.NewBorder( + nil, + nil, + container.NewVBox( + equationLabel, + diceRollsLabel, + ), + resultLabel, + ) + + return &historyItemRenderer{ + item: h, + equationLabel: equationLabel, + diceRollsLabel: diceRollsLabel, + resultLabel: resultLabel, + objects: []fyne.CanvasObject{layout}, + } +} + +func (h *historyItem) SetCalculation(c *calculation) { + h.calc = c + h.Refresh() +} + +func newHistoryItem(c *calculation) *historyItem { + item := &historyItem{calc: c} + item.ExtendBaseWidget(item) + return item +} diff --git a/main.go b/main.go index 5fae9f4..e87922f 100644 --- a/main.go +++ b/main.go @@ -2,13 +2,11 @@ package main import ( "fmt" - "image/color" "strconv" "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/widget" ) @@ -17,30 +15,50 @@ func main() { myApp := app.New() myWindow := myApp.NewWindow("Dice Statistics Calculator") - // Output display - outputDisplay := canvas.NewText("0", color.White) - outputDisplay.TextSize = 32 + var calculations []*calculation + var historyList *widget.List // Dice input bar diceInputEntry := widget.NewEntry() diceInputEntry.SetPlaceHolder("e.g., 2d20H, 3d6+5") + historyList = widget.NewList( + func() int { + return len(calculations) + }, + func() fyne.CanvasObject { + return newHistoryItem(&calculation{}) + }, + func(i widget.ListItemID, o fyne.CanvasObject) { + o.(*historyItem).SetCalculation(calculations[i]) + }, + ) + // Roll button rollButton := widget.NewButton("ROLL", func() { diceInput := strings.TrimSpace(diceInputEntry.Text) if diceInput == "" { - outputDisplay.Text = "Error: Empty input" - outputDisplay.Refresh() return } - result, err := CalculateDice(diceInput) + result, diceRolls, err := CalculateDice(diceInput) if err != nil { - outputDisplay.Text = fmt.Sprintf("Error: %v", err) + // TODO: show error to user } else { - outputDisplay.Text = strconv.FormatFloat(result, 'g', -1, 64) + id := 0 + if len(calculations) > 0 { + id = calculations[len(calculations)-1].id + 1 + } + c := &calculation{ + id: id, + equation: diceInput, + diceRolls: diceRolls, + result: fmt.Sprintf("= %s", strconv.FormatFloat(result, 'g', -1, 64)), + } + calculations = append(calculations, c) + historyList.Refresh() + diceInputEntry.SetText("") } - outputDisplay.Refresh() }) rollButton.Importance = widget.HighImportance @@ -95,8 +113,6 @@ func main() { }), widget.NewButton("C", func() { diceInputEntry.SetText("") - outputDisplay.Text = "0" - outputDisplay.Refresh() }), createCalcButton("1", diceInputEntry), createCalcButton("2", diceInputEntry), @@ -125,25 +141,18 @@ func main() { buttonsContainer.Add(button) } - // Output section - outputSection := container.NewVBox( - widget.NewLabel("Result:"), - outputDisplay, - ) - - // Main layout: output at top, dice bar below, calculator buttons below that - mainContent := container.NewBorder( - outputSection, - container.NewVBox( - widget.NewLabel("Dice Expression:"), - diceInputEntry, - ), + topContent := container.NewBorder( + diceInputEntry, nil, nil, - buttonsContainer, + nil, + historyList, ) - myWindow.SetContent(mainContent) + split := container.NewVSplit(topContent, buttonsContainer) + split.Offset = 0.5 // Start with a 50/50 split + + myWindow.SetContent(split) myWindow.Resize(fyne.NewSize(400, 600)) myWindow.ShowAndRun() }