1
0

main.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. // Command mpckit is MPCkit, the desktop kit editor for the MPC Sample.
  2. package main
  3. import (
  4. "embed"
  5. "log"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "sync/atomic"
  10. "github.com/wailsapp/wails/v3/pkg/application"
  11. "github.com/wailsapp/wails/v3/pkg/events"
  12. "git.monoexp.com/bajdev/mpckit/internal/app"
  13. )
  14. // The built Svelte app (frontend/dist), embedded in the binary.
  15. //
  16. //go:embed all:frontend/dist
  17. var assets embed.FS
  18. // Window sizes are in device-independent pixels, 16:9 like most displays.
  19. const (
  20. maxWidth, maxHeight = 1280, 720 // largest default size
  21. minWidth, minHeight = 960, 540 // smallest the layout is designed for
  22. screenShare = 0.9 // default size: this share of the screen's work area
  23. )
  24. // Events Go sends to the frontend.
  25. const (
  26. eventState = "state" // a new app.State after a change the frontend did not ask for
  27. eventError = "error" // a message to show
  28. eventClose = "close" // the window was asked to close with unsaved changes
  29. )
  30. func init() {
  31. application.RegisterEvent[app.State](eventState)
  32. application.RegisterEvent[string](eventError)
  33. application.RegisterEvent[application.Void](eventClose)
  34. }
  35. func main() {
  36. d := &dialogs{}
  37. svc, host := app.NewService(d)
  38. a := newApp(svc, host)
  39. d.a = a
  40. w := newWindow(a)
  41. guardClose(a, w, host)
  42. handleFileDrops(a, w, host)
  43. if err := a.Run(); err != nil {
  44. log.Fatal(err)
  45. }
  46. }
  47. // newApp creates the application. Only svc is bound; host serves the
  48. // sample previews.
  49. func newApp(svc *app.Service, host *app.Host) *application.App {
  50. var a *application.App
  51. a = application.New(application.Options{
  52. Name: "MPCkit",
  53. Description: "Desktop kit editor for the MPC Sample",
  54. // Wails exits on a panic in an event handler unless this is set.
  55. // Log it and tell the user instead, so a case the checks missed
  56. // cannot close the app and lose their changes.
  57. PanicHandler: func(p *application.PanicDetails) {
  58. log.Printf("panic: %v\n%s", p.Error, p.StackTrace)
  59. a.Event.Emit(eventError, "Something went wrong inside MPCkit, and the last action may not have finished.")
  60. },
  61. Services: []application.Service{
  62. application.NewService(svc),
  63. },
  64. Assets: application.AssetOptions{
  65. Handler: application.AssetFileServerFS(assets),
  66. Middleware: host.AudioMiddleware,
  67. },
  68. Mac: application.MacOptions{
  69. ApplicationShouldTerminateAfterLastWindowClosed: true,
  70. },
  71. })
  72. return a
  73. }
  74. // newWindow opens the main window. It starts hidden and is sized once the
  75. // screens are known, so it fits the display instead of being clamped by it.
  76. func newWindow(a *application.App) *application.WebviewWindow {
  77. w := a.Window.NewWithOptions(application.WebviewWindowOptions{
  78. Title: "MPCkit",
  79. Width: maxWidth,
  80. Height: maxHeight,
  81. MinWidth: minWidth,
  82. MinHeight: minHeight,
  83. Hidden: true,
  84. EnableFileDrop: true,
  85. BackgroundColour: application.NewRGB(0x15, 0x17, 0x1A),
  86. URL: "/",
  87. })
  88. a.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
  89. if s := a.Screen.GetPrimary(); s != nil {
  90. width, height := fitSize(s.WorkArea.Width, s.WorkArea.Height)
  91. w.SetSize(width, height)
  92. }
  93. w.Center()
  94. w.Show()
  95. })
  96. return w
  97. }
  98. // guardClose handles closing with unsaved changes: stop, and let the
  99. // frontend ask. If the user chooses to discard them, the frontend calls
  100. // CloseWindow, which closes for real.
  101. func guardClose(a *application.App, w *application.WebviewWindow, host *app.Host) {
  102. var closeConfirmed atomic.Bool
  103. host.SetCloser(func() {
  104. closeConfirmed.Store(true)
  105. w.Close()
  106. })
  107. w.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
  108. if closeConfirmed.Load() || !host.Dirty() {
  109. return
  110. }
  111. e.Cancel()
  112. a.Event.Emit(eventClose)
  113. })
  114. }
  115. // handleFileDrops assigns a WAV dropped on a pad (an element with
  116. // data-file-drop-target and data-slot) to that pad.
  117. func handleFileDrops(a *application.App, w *application.WebviewWindow, host *app.Host) {
  118. w.OnWindowEvent(events.Common.WindowFilesDropped, func(e *application.WindowEvent) {
  119. ctx := e.Context()
  120. target := ctx.DropTargetDetails()
  121. if target == nil {
  122. return
  123. }
  124. slot, err := strconv.Atoi(target.Attributes["data-slot"])
  125. if err != nil {
  126. return
  127. }
  128. for _, f := range ctx.DroppedFiles() {
  129. if strings.EqualFold(filepath.Ext(f), ".wav") {
  130. st, err := host.AssignSamplePath(slot, f)
  131. a.Event.Emit(eventState, st)
  132. if err != nil {
  133. a.Event.Emit(eventError, err.Error())
  134. }
  135. return
  136. }
  137. }
  138. a.Event.Emit(eventError, "Only WAV files can be assigned to a pad.")
  139. })
  140. }
  141. // fitSize returns a 16:9 window size that takes screenShare of the work
  142. // area (areaW x areaH), capped at maxWidth x maxHeight and no smaller than
  143. // minWidth x minHeight.
  144. func fitSize(areaW, areaH int) (int, int) {
  145. width := min(maxWidth, int(float64(areaW)*screenShare))
  146. height := width * 9 / 16
  147. if limit := int(float64(areaH) * screenShare); height > limit {
  148. height = limit
  149. width = height * 16 / 9
  150. }
  151. return max(width, minWidth), max(height, minHeight)
  152. }