package main
import (
"context"
"fmt"
"maps"
"os"
"slices"
"sync"
"sync/atomic"
"github.com/gogpu/gg"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gg/text"
"github.com/gogpu/gogpu"
"github.com/gogpu/gpucontext"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// setAppMenu sets the application menu with a custom "About Fractal" menu item,
// which renders a small translucent window with some text when selected.
ui.app.SetMenu(gogpu.NewMenu().
// TODO(jbunds): fix bug whereby selecting the custom "About Fractal" item from the
// application menu incorrectly renders a new frame to the primary window
AddItem(gogpu.MenuItem{Title: "About Fractal", Role: gogpu.RoleAbout, Action: func() {
ui.aboutWindow.Show() // immediately show the About window
ui.aboutWindowHasFocus.Store(true) // record that the About window gained focus
}}).
AddItem(gogpu.MenuItem{Separator: true}).
AddItem(gogpu.MenuItem{Title: "Settings…", Role: gogpu.RolePreferences}).
AddItem(gogpu.MenuItem{Separator: true}).
AddItem(gogpu.MenuItem{Title: "Services", Role: gogpu.RoleServices}).
AddItem(gogpu.MenuItem{Separator: true}).
AddItem(gogpu.MenuItem{Title: "Hide Fractal", Role: gogpu.RoleHide}).
AddItem(gogpu.MenuItem{Title: "Hide Others", Role: gogpu.RoleHideOthers}).
AddItem(gogpu.MenuItem{Title: "Show All", Role: gogpu.RoleShowAll}).
AddItem(gogpu.MenuItem{Separator: true}).
AddItem(gogpu.MenuItem{Title: "Quit Fractal", Role: gogpu.RoleQuit, Action: func() { ui.app.Quit() }}))
var (
drawOnce sync.Once
release func()
)
// TODO(jbunds): investigate why pointer events clicking on the About window are routed
// to the primary window's SetOnPointer handler instead of here
ui.aboutWindow.SetOnPointer(func(e gpucontext.PointerEvent) {
if e.Type == gpucontext.PointerDown {
ui.aboutWindowHasFocus.Store(true)
}
})
ui.aboutWindow.SetOnKeyPress(func(key gpucontext.Key, mods gpucontext.Modifiers) {
if mods.HasSuper() && key == gpucontext.KeyW { // ⌘+W
ui.hideAboutWindow.Store(true) // defer aboutWindow.Hide() to OnUpdate() to avoid GoGPU internal mutex deadlock
}
})
ui.aboutWindow.SetOnDraw(func(dc *gogpu.Context) {
drawOnce.Do(func() {
release = drawAboutWindow(ui.app, dc, &ui.renderer)
})
})
ui.aboutWindow.SetOnClose(func() bool {
if release != nil {
release()
}
ui.hideAboutWindow.Store(true) // defer aboutWindow.Hide() to OnUpdate() to avoid GoGPU internal mutex deadlock
return false // reject the close window request and hide the window instead to preserve window handle and callbacks
})
}
// addFractalsMenu creates a "Fractals" menu to allow users to select a new combination of
// fractal kind ("Mandelbrot" or "Julia") and parameter of interest (target x, y coordinates
// to zoom in on for the Mandelbrot set, or complex constant for the filled Julia set)
// from a preset list of named (or unnamed) parameters.
//
// Selecting a new fractal from the menu also schedules a rebuild of the "Themes" menu's
// Action closures seeded with the new renderer so that the two menus are always in sync.
fractals := fractals()
labels, sortedMenuItems := labels(fractals)
fractalsMenu := gogpu.NewMenuWithTitle("Fractals")
for _, kind := range kinds(fractals) {
fractalMenu := gogpu.NewMenu()
for _, name := range sortedMenuItems[kind] {
newFractal := fractals[name]
label := labels[kind][name]
fractalMenu.AddItem(gogpu.MenuItem{Title: label, Action: func() { // TODO(jbunds): disable the menu item for the fractal currently being rendered
curRenderer := ui.renderer.Load().(*renderer)
if newFractal.kind == curRenderer.fractal.kind &&
newFractal.name == curRenderer.fractal.name { // the current fractal was selected
// the primary window should always be shown when any fractal is selected from the menu,
// but syncAnimation() checks ui.primaryWindow.Visible(), (TODO(jbunds): clean up)
// so primaryWindow.Show() is called before syncAnimation()
wasHidden := !ui.primaryWindow.Visible()
ui.primaryWindow.Show() // show primary window if it was hidden
ui.aboutWindowHasFocus.Store(false) // record About window lost focus
ui.syncAnimation() // resume animation if animating
if !ui.animating.Load() && !wasHidden { // if animation is paused
ui.app.RequestRedraw() // refresh paused frame when re-shown
}
return
}
// instantiate and initialize a new renderer with the new fractal and the current theme
newRenderer := newRenderer(newFractal, shaderCode[newFractal.kind], curRenderer.theme)
newRenderer.init(ui.app, curRenderer.theme)
oldRenderer := ui.renderer.Swap(newRenderer).(*renderer)
oldRenderer.release()
ui.app.SetTitle(newFractal.titleText)
ui.primaryWindow.Show() // always show the primary window after selection of a new fractal
ui.aboutWindowHasFocus.Store(false) // if the About window was open, record it lost focus
ui.syncAnimation() // sync animation state
ui.scheduleMenuRebuild() // rebuild the "Themes" menu's Action closures seeded with the new renderer
ui.newProgressBar(ctx) // instantiate a new progress bar for the new fractal
}})
}
fractalsMenu.AddItem(gogpu.MenuItem{
Title: cases.Title(language.English).String(kind),
Submenu: fractalMenu,
})
}
// TODO(jbunds): determine what causes the animation to pause when the user clicks on any menu header
ui.app.SetCustomMenu("fractals", fractalsMenu)
}
// rebuildThemesMenu rebuilds the "Themes" menu when a new fractal is selected from the "Fractals" menu
// so its Action closures are seeded with the new renderer instantiated to render the new fractal.
themesMenu := gogpu.NewMenuWithTitle("Themes")
for _, cs := range slices.Sorted(maps.Keys(colorSchemes())) {
themesMenu.AddItem(gogpu.MenuItem{Title: cs, Action: func() { // TODO(jbunds): prepend a checkmark to the current theme menu item and disable it
cr := ui.renderer.Load().(*renderer)
if cr.theme == cs { return }
// apply the new theme to the current renderer
newRenderer := newRenderer(cr.fractal, shaderCode[cr.fractal.kind], cs)
newRenderer.init(ui.app, cs)
oldRenderer := ui.renderer.Swap(newRenderer).(*renderer)
oldRenderer.release()
// the primary window should always be shown when any theme is selected from the menu,
// but syncAnimation() checks ui.primaryWindow.Visible(), (TODO(jbunds): clean up)
// so primaryWindow.Show() is called before syncAnimation()
wasHidden := !ui.primaryWindow.Visible()
ui.primaryWindow.Show()
ui.aboutWindowHasFocus.Store(false)
ui.syncAnimation()
if !ui.animating.Load() && !wasHidden { // if animation is paused
ui.app.RequestRedraw() // render the initial frame of the fractal with the new theme
}
ui.newProgressBar(ctx)
}})
}
ui.app.SetCustomMenu("themes", themesMenu)
}
// addWindowMenu adds a standard "Window" menu.
// TODO(jbunds): fix this so a "Window" menu is actually added to the menu bar.
if winMenu := app.GetSystemMenu(gogpu.SystemMenuWindow); winMenu != nil {
winMenu.AddItem(gogpu.MenuItem{Title: "Minimize", Role: gogpu.RoleMinimize})
winMenu.AddItem(gogpu.MenuItem{Title: "Zoom", Role: gogpu.RoleZoom})
winMenu.AddItem(gogpu.MenuItem{Separator: true})
winMenu.AddItem(gogpu.MenuItem{Title: "Enter Full Screen", Role: gogpu.RoleFullScreen})
winMenu.AddItem(gogpu.MenuItem{Title: "Show / Hide All", Role: gogpu.RoleShowAll})
winMenu.AddItem(gogpu.MenuItem{Separator: true})
winMenu.AddItem(gogpu.MenuItem{Title: "Bring All to Front", Role: gogpu.RoleBringAllToFront})
winMenu.AddItem(gogpu.MenuItem{Title: "Close", Role: gogpu.RoleClose})
}
}
// drawAboutWindow draws the About window once on demand.
canvas, err := ggcanvas.New(app.GPUContextProvider(), aboutWidth, aboutHeight)
if err != nil {
panic(err)
}
var (
renderOnce sync.Once
view *gpucontext.TextureView
release func()
)
// assumes the bimedians (perpendicular bisectors) of both windows (primary and About) are aligned
offsetX := mainWidth / 2.0 - aboutWidth / 2.0
offsetY := mainHeight / 2.0 - aboutHeight / 2.0
ar := cr.Load().(*renderer) // in case the user selects one of the menu items from the "Fractals" menu before selecting the About menu item
err = canvas.Draw(func(cc *gg.Context) {
renderOnce.Do(func() {
view, release = renderAboutWindow(cc, ar.assets.fontSource.Face(10))
})
cc.MarkFrameRendered() // ?
// composite the About window TextureView atop the background
cc.Push() // save pre-translation state
cc.Translate(-offsetX, -offsetY) // align the background of the About window with the fractal image beneath
cc.DrawGPUTextureBase(ar.assets.fractalView, 0, 0, aboutWidth, aboutHeight)
cc.Pop() // restore pre-translation state so the overlay and text remain unaffected by the translation
cc.SetRGBA(0, 0, 0, 0.6)
cc.DrawRectangle(0, 0, aboutWidth, aboutHeight)
cc.Fill()
cc.DrawGPUTexture(*view, 0, 0, aboutWidth, aboutHeight)
})
if err != nil {
panic(err)
}
if err := canvas.Render(dc.RenderTarget()); err != nil { // or canvas.RenderTo(dc.AsTextureDrawer())
panic(err)
}
return release
}
// renderAboutWindow renders the About window content to an off-screen texture.
view, release := cc.CreateOffscreenTexture(aboutWidth, aboutHeight)
if release == nil {
fmt.Fprint(os.Stderr, "GPU unavailable") // TODO(jbunds): implement CPU fallback ?
}
cc.BeginGPUFrame()
cc.ClearWithColor(gg.Transparent)
cc.SetColor(gg.Transparent)
cc.DrawRectangle(0, 0, aboutWidth, aboutHeight)
cc.Fill()
cc.SetColor(gg.RGB(1, 0.9725, 0.8627)) // approximates "cornsilk"
cc.SetFont(fontFace)
cc.DrawString("Fractal 1.0", 15, 30)
cc.DrawString("Jeff Bunds", 15, 50)
cc.DrawString("Copyright © 2026", 15, 70)
cc.Fill()
if err := cc.FlushGPUWithViewPreserveContent(view, aboutWidth, aboutHeight); err != nil {
fmt.Fprint(os.Stderr, "GPU unavailable") // TODO(jbunds): implement CPU fallback ?
}
return &view, release
}