Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion core/button/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,9 @@ func handleActivationKey(w *Widget, e *event.KeyEvent) bool {
}
}

// fireOnClick calls the configured onClick handler if present.
// fireOnClick plays the click sound and calls the configured onClick handler.
func fireOnClick(w *Widget) {
widget.PlaySound(widget.SoundClick)
if w.cfg.onClick != nil {
w.cfg.onClick()
}
Expand Down
4 changes: 3 additions & 1 deletion core/checkbox/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ func handleActivationKey(w *Widget, e *event.KeyEvent) bool {
}
}

// fireToggle toggles the checked state and calls the configured onToggle handler.
// fireToggle plays the click sound, toggles the checked state, and calls the
// configured onToggle handler.
func fireToggle(w *Widget) {
widget.PlaySound(widget.SoundClick)
newChecked := !w.cfg.ResolvedChecked()
// TWO-WAY: if a CheckedSignal is bound, write back the new state.
if w.cfg.checkedSignal != nil {
Expand Down
2 changes: 2 additions & 0 deletions core/collapsible/collapsible.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,8 @@ func (w *Widget) Unmount() {

// setExpandedState updates the expanded state and starts animation if needed.
func (w *Widget) setExpandedState(expanded bool) {
widget.PlaySound(widget.SoundClick)

// Update state source.
if w.cfg.expandedSignal != nil {
w.cfg.expandedSignal.Set(expanded)
Expand Down
2 changes: 2 additions & 0 deletions core/dialog/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func (w *Widget) Show(ctx widget.Context) {
return
}

widget.PlaySound(widget.SoundAlert)

w.visible = true
w.SetVisible(true)

Expand Down
2 changes: 2 additions & 0 deletions core/dropdown/widget.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ func (w *Widget) selectItem(ctx widget.Context, index int) {
return
}

widget.PlaySound(widget.SoundClick)

w.selectedIndex = index

// Update signal if bound.
Expand Down
1 change: 1 addition & 0 deletions core/menu/menu.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ func (m *menuPanel) activateHighlighted(ctx widget.Context) bool {

// selectItem fires the item's action and closes the entire menu tree.
func (m *menuPanel) selectItem(item *MenuItem) {
widget.PlaySound(widget.SoundClick)
if m.onSelect != nil {
m.onSelect(item)
}
Expand Down
2 changes: 2 additions & 0 deletions core/radio/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,8 @@ func (g *Group) selectValue(value string) *Item {
return nil
}

widget.PlaySound(widget.SoundClick)

// TWO-WAY: if a SelectedSignal is bound, write back the new value.
if selectedSignal != nil {
selectedSignal.Set(value)
Expand Down
1 change: 1 addition & 0 deletions core/tabview/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func (w *Widget) selectTab(idx int) {
if idx == w.cfg.ResolvedSelected() {
return
}
widget.PlaySound(widget.SoundClick)
w.cfg.setSelected(idx)
if w.cfg.onSelect != nil {
w.cfg.onSelect(idx)
Expand Down
22 changes: 22 additions & 0 deletions desktop/sound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package desktop

import (
"github.com/gogpu/gogpu/sound"
"github.com/gogpu/ui/widget"
)

func init() {
// Register the SoundPlayer so that widget.PlaySound() delegates to
// gogpu/sound.Play() without widget code importing gogpu directly.
// Sound is disabled by default; the app enables it via
// gogpu.DefaultConfig().WithSoundFeedback(true) which calls
// sound.SetEnabled(true).
widget.RegisterSoundPlayer(func(e widget.SoundEvent) {
switch e {
case widget.SoundClick:
sound.Play(sound.Click)
case widget.SoundAlert:
sound.Play(sound.Alert)
}
})
}
39 changes: 31 additions & 8 deletions examples/sounds/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import (
)

func main() {
// Enable platform sounds globally. All interactive widgets (button,
// checkbox, radio, dropdown, collapsible, tabview, menu) now auto-play
// a click sound on activation — no manual sound.Play needed.
sound.SetEnabled(true)

m3 := material3.New(widget.Hex(0x1565C0))
Expand All @@ -30,28 +33,26 @@ func main() {

root := primitives.VBox(
primitives.Text("System Sounds Demo").FontSize(22).Bold(),
primitives.Text("Every interaction plays a platform system sound").
primitives.Text("Widgets auto-play sounds — no manual sound.Play needed").
FontSize(13).Color(widget.RGBA(0.5, 0.5, 0.5, 1)),

primitives.Text("Checkboxes").FontSize(16).Bold(),
primitives.Text("Checkboxes (auto click sound)").FontSize(16).Bold(),
checkbox.New(
checkbox.LabelOpt("Enable notifications"),
checkbox.OnToggle(func(checked bool) {
sound.Play(sound.Click)
fmt.Println("notifications:", checked)
}),
checkbox.PainterOpt(cp),
),
checkbox.New(
checkbox.LabelOpt("Dark mode"),
checkbox.OnToggle(func(checked bool) {
sound.Play(sound.Click)
fmt.Println("dark mode:", checked)
}),
checkbox.PainterOpt(cp),
),

primitives.Text("Theme").FontSize(16).Bold(),
primitives.Text("Radio (auto click sound)").FontSize(16).Bold(),
radio.NewGroup(
radio.Items(
radio.ItemDef{Value: "light", Label: "Light"},
Expand All @@ -60,13 +61,12 @@ func main() {
),
radio.Selected("light"),
radio.OnChange(func(v string) {
sound.Play(sound.Click)
fmt.Println("theme:", v)
}),
radio.GroupPainter(rp),
),

primitives.Text("Volume").FontSize(16).Bold(),
primitives.Text("Slider (no sound — continuous drag)").FontSize(16).Bold(),
slider.New(
slider.Min(0),
slider.Max(100),
Expand All @@ -77,6 +77,29 @@ func main() {
slider.PainterOpt(sp),
),

primitives.Text("Buttons (auto click sound)").FontSize(16).Bold(),
primitives.HBox(
button.New(
button.Text("Action 1"),
button.OnClick(func() { fmt.Println("action 1") }),
button.PainterOpt(bp),
button.VariantOpt(button.Filled),
),
button.New(
button.Text("Action 2"),
button.OnClick(func() { fmt.Println("action 2") }),
button.PainterOpt(bp),
button.VariantOpt(button.Tonal),
),
button.New(
button.Text("Action 3"),
button.OnClick(func() { fmt.Println("action 3") }),
button.PainterOpt(bp),
button.VariantOpt(button.Outlined),
),
).Gap(8),

primitives.Text("Special sounds (manual)").FontSize(16).Bold(),
primitives.HBox(
button.New(
button.Text("Success"),
Expand All @@ -101,7 +124,7 @@ func main() {

gogpuApp := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("gogpu/ui — System Sounds").
WithSize(400, 520))
WithSize(400, 600))

uiApp := app.New(
app.WithWindowProvider(gogpuApp),
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ go 1.25.0

require (
github.com/coregx/signals v0.1.1
github.com/gogpu/gg v0.50.13
github.com/gogpu/gogpu v0.50.0
github.com/gogpu/gg v0.50.14
github.com/gogpu/gogpu v0.50.2
github.com/gogpu/gpucontext v0.24.0
github.com/gogpu/gputypes v0.5.1
github.com/gogpu/wgpu v0.30.36
github.com/gogpu/wgpu v0.30.37
golang.org/x/image v0.44.0
)

Expand Down
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ github.com/go-webgpu/goffi v0.6.3 h1:p4gKGikHBAQ/8iUiew9MV4C5M1ZGIsk8QGFGuJjMj+A
github.com/go-webgpu/goffi v0.6.3/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM=
github.com/go-webgpu/webgpu v0.5.5 h1:pIrXzRg0LRlNjNmR+ZNo/ERN88YaObzbCY5oYhir5SA=
github.com/go-webgpu/webgpu v0.5.5/go.mod h1:vgIuNTa1UlZ4njCGY6Pmp/0c5T5o2Ml2fQ0znLc7B98=
github.com/gogpu/gg v0.50.13 h1:GIDqgR6CozprmFD3MgeMhFkXbZ76FH5ofZuwblCPOy4=
github.com/gogpu/gg v0.50.13/go.mod h1:cV+vVHM75pf7qPDIKPLSFUQuazolClb8xSW9tdQTJRU=
github.com/gogpu/gogpu v0.50.0 h1:qyUi5vBte/m7NDd49aN+YJffn36V4jGJydXCg37NR9E=
github.com/gogpu/gogpu v0.50.0/go.mod h1:hIEFjkuVV2+eOkayR7gS38h7UrnUOZfcQXXQbxt36c0=
github.com/gogpu/gg v0.50.14 h1:JhahLKhiij7XaFWkjYBnw7/HOV25FAXqD1KS9/ceHIA=
github.com/gogpu/gg v0.50.14/go.mod h1:CCA/RZOr+RrpWPzOYPnAWaYCjWfI543RPr+JeaJV38I=
github.com/gogpu/gogpu v0.50.2 h1:7FzoGH2aBr5MDOLpu7RONwKQYvqQoZle4v44o8jXkvU=
github.com/gogpu/gogpu v0.50.2/go.mod h1:hIEFjkuVV2+eOkayR7gS38h7UrnUOZfcQXXQbxt36c0=
github.com/gogpu/gpucontext v0.24.0 h1:YQ0FxGqXEO8LQB+6/TOqZOV1fn0mO9UDYR0obSU3R6o=
github.com/gogpu/gpucontext v0.24.0/go.mod h1:OrT137boh5yPhqBEhF4UQKIOu1Jq74SLQzYa/m6JbNo=
github.com/gogpu/gputypes v0.5.1 h1:X38OPcP6umQqqubzzJYL6Nm1tXHSNQj6TRSAoxdAJmg=
github.com/gogpu/gputypes v0.5.1/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw=
github.com/gogpu/naga v0.18.0 h1:2y83HUcAnlwEZMuHOiYTMdNYM9J8rev40gkI4zJ96Uk=
github.com/gogpu/naga v0.18.0/go.mod h1:15sQaHKkbqXcwTN+hHYGLsA0WBBnkmYzne/eF5p5WEg=
github.com/gogpu/wgpu v0.30.36 h1:8r4tYLCo8qG0ZaOQxsCh4BpZJ5u8CL3Ab/XDOwMH3co=
github.com/gogpu/wgpu v0.30.36/go.mod h1:fi3zxQmJnjPRMqqA5wl2kta2Z6kxdf8HQxEORNT1Lt4=
github.com/gogpu/wgpu v0.30.37 h1:OhgiFY/RU+v+JuD8pq0c0SV3iCi1UFy4j8/JqEYEmCw=
github.com/gogpu/wgpu v0.30.37/go.mod h1:fi3zxQmJnjPRMqqA5wl2kta2Z6kxdf8HQxEORNT1Lt4=
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
Expand Down
44 changes: 44 additions & 0 deletions widget/sound.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package widget

// SoundEvent represents a UI interaction sound type.
// Widget code uses these constants to request sound feedback without
// importing platform-specific sound packages. The actual playback
// is handled by a [SoundPlayer] registered via [RegisterSoundPlayer].
type SoundEvent int

const (
// SoundClick is the default UI interaction sound (button click,
// checkbox toggle, radio select, menu item activation).
SoundClick SoundEvent = iota

// SoundAlert is a notification/dialog sound played when a modal
// dialog is shown.
SoundAlert
)

// SoundPlayer is a callback that plays a system sound for the given event.
// Registered by the app/desktop layer to bridge widget sound requests
// to the platform sound API (gogpu/sound) without a direct import.
type SoundPlayer func(SoundEvent)

// soundPlayer holds the registered sound callback.
// Set by the app layer during initialization via RegisterSoundPlayer.
var soundPlayer SoundPlayer

// RegisterSoundPlayer registers the callback that plays system sounds.
// Called by the desktop layer during initialization to inject the
// platform sound implementation. Only one player may be registered;
// subsequent calls replace the previous one.
func RegisterSoundPlayer(p SoundPlayer) {
soundPlayer = p
}

// PlaySound plays a UI sound if a [SoundPlayer] is registered.
// This is a no-op if no player has been registered (sound disabled
// or app layer not wired). Widgets call this before firing their
// user-facing callbacks so the sound plays immediately on interaction.
func PlaySound(e SoundEvent) {
if soundPlayer != nil {
soundPlayer(e)
}
}
60 changes: 60 additions & 0 deletions widget/sound_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package widget

import "testing"

func TestPlaySound_NoPlayer(t *testing.T) {
// Save and restore the global player.
old := soundPlayer
defer func() { soundPlayer = old }()

soundPlayer = nil

// Should not panic when no player is registered.
PlaySound(SoundClick)
PlaySound(SoundAlert)
}

func TestPlaySound_WithPlayer(t *testing.T) {
old := soundPlayer
defer func() { soundPlayer = old }()

var received []SoundEvent
RegisterSoundPlayer(func(e SoundEvent) {
received = append(received, e)
})

PlaySound(SoundClick)
PlaySound(SoundAlert)
PlaySound(SoundClick)

if len(received) != 3 {
t.Fatalf("expected 3 events, got %d", len(received))
}
if received[0] != SoundClick {
t.Errorf("event[0] = %d, want SoundClick(%d)", received[0], SoundClick)
}
if received[1] != SoundAlert {
t.Errorf("event[1] = %d, want SoundAlert(%d)", received[1], SoundAlert)
}
if received[2] != SoundClick {
t.Errorf("event[2] = %d, want SoundClick(%d)", received[2], SoundClick)
}
}

func TestRegisterSoundPlayer_Replaces(t *testing.T) {
old := soundPlayer
defer func() { soundPlayer = old }()

var first, second bool
RegisterSoundPlayer(func(SoundEvent) { first = true })
RegisterSoundPlayer(func(SoundEvent) { second = true })

PlaySound(SoundClick)

if first {
t.Error("first player should NOT have been called after replacement")
}
if !second {
t.Error("second player should have been called")
}
}
Loading