diff --git a/doc/frontend_ebiten.md b/doc/frontend_ebiten.md index 34fd00f2..535cc0c3 100644 --- a/doc/frontend_ebiten.md +++ b/doc/frontend_ebiten.md @@ -1,9 +1,9 @@ # The Ebitengine frontend `a2ebiten` opens a window with [Ebitengine](https://ebitengine.org/). It has -the screen with all its modes, the sound, the diskettes dropped on the window -and the same function keys as [a2sdl](frontend_sdl2.md), and it is missing the -things around them: no joysticks and no mouse. +the screen with all its modes, the sound, the mouse, the diskettes dropped on +the window and the same function keys as [a2sdl](frontend_sdl2.md), and it is +missing the things around them: no joysticks. ## Building @@ -58,10 +58,16 @@ so the area used is the one the pointer was last seen on. Check with F8 before dragging, or look at the areas shown after the drop to see where the file landed. +## The mouse + +The models that use a mouse, like `desktop`, work with the pointer on the +window. Ebitengine has no mouse events, so the pointer is read on every frame, +and its position is taken on the picture and not on the window: it lands on the +same place whatever the size of the window is. + ## What is missing - **Joysticks and paddles**, and the mouse as a joystick. -- **The mouse**, so the models that use it, like `desktop`, are not much use. - **Pasting** from the clipboard. The window is 564x384 and can be resized. The picture is generated at a fixed diff --git a/frontend/a2ebiten/ebitenAudio.go b/frontend/a2ebiten/ebitenAudio.go index 82fc63a8..be326140 100644 --- a/frontend/a2ebiten/ebitenAudio.go +++ b/frontend/a2ebiten/ebitenAudio.go @@ -23,7 +23,7 @@ type ebitenAudio struct { func newEbitenAudio(a *izapple2.Apple2) *ebitenAudio { return &ebitenAudio{ - mixer: shared.NewMixer(a), + mixer: shared.NewFrontMixer(a), } } diff --git a/frontend/a2ebiten/main.go b/frontend/a2ebiten/main.go index e1155077..0d358873 100644 --- a/frontend/a2ebiten/main.go +++ b/frontend/a2ebiten/main.go @@ -20,6 +20,7 @@ type Game struct { view *shared.View keyboard *ebitenKeyboard speaker *ebitenAudio + mouse *shared.Mouse dropTargets *ebitenDropTargets fontSource *text.GoTextFaceSource @@ -41,6 +42,7 @@ var hudColor = color.RGBA{208, 241, 141, 255} // Yellow func (g *Game) Update() error { g.keyboard.update() g.speaker.update() + g.updateMouse() g.dropTargets.update() if g.paused != g.a.IsPaused() { @@ -71,6 +73,20 @@ func (g *Game) Update() error { return nil } +/* +updateMouse reads where the pointer is and whether its button is pressed. +Ebitengine has no mouse events, they are polled when the frame is updated. + +The positions are on the virtual screen the game is laid out on, not on the +window, so the pointer lands on the same place of the picture whatever the size +of the window is. +*/ +func (g *Game) updateMouse() { + x, y := ebiten.CursorPosition() + g.mouse.SetPosition(x, y, virtualWidth, virtualHeight) + g.mouse.SetButton(ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) +} + func (g *Game) Draw(screen *ebiten.Image) { if g.image != nil { op := &ebiten.DrawImageOptions{} @@ -128,8 +144,10 @@ func ebitenRun(a *izapple2.Apple2) { view: view, keyboard: newEbitenKeyBoard(a, view), speaker: newEbitenAudio(a), + mouse: shared.NewMouse(), dropTargets: newEbitenDropTargets(a), } + a.SetMouseProvider(game.mouse) var err error game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf)) diff --git a/frontend/a2libretro/audio.go b/frontend/a2libretro/audio.go index 1306e7ff..596a35d8 100644 --- a/frontend/a2libretro/audio.go +++ b/frontend/a2libretro/audio.go @@ -40,7 +40,7 @@ func newAudioOutput(a *izapple2.Apple2) *audioOutput { size := int(samplesPerFrame) + 2 return &audioOutput{ - mixer: shared.NewMixer(a), + mixer: shared.NewFrontMixer(a), samplesPerFrame: samplesPerFrame, mono: make([]float32, size), stereo: make([]C.int16_t, 2*size), diff --git a/frontend/a2sdl/sdlAudio.go b/frontend/a2sdl/sdlAudio.go index a37d1e65..afd8a175 100644 --- a/frontend/a2sdl/sdlAudio.go +++ b/frontend/a2sdl/sdlAudio.go @@ -34,7 +34,7 @@ var theSDLAudio atomic.Pointer[sdlAudio] func newSDLAudio(a *izapple2.Apple2) *sdlAudio { return &sdlAudio{ - mixer: shared.NewMixer(a), + mixer: shared.NewFrontMixer(a), } } diff --git a/frontend/a2sdl3/sdl3Audio.go b/frontend/a2sdl3/sdl3Audio.go index 8c8b1208..3e5ea253 100644 --- a/frontend/a2sdl3/sdl3Audio.go +++ b/frontend/a2sdl3/sdl3Audio.go @@ -27,7 +27,7 @@ type sdl3Audio struct { func newSDL3Audio(a *izapple2.Apple2) *sdl3Audio { return &sdl3Audio{ - mixer: shared.NewMixer(a), + mixer: shared.NewFrontMixer(a), buf: make([]float32, bufferSamples), } } diff --git a/frontend/shared/mixer.go b/frontend/shared/mixer.go index dc6ad9cc..744ad7be 100644 --- a/frontend/shared/mixer.go +++ b/frontend/shared/mixer.go @@ -5,9 +5,9 @@ import ( "github.com/ivanizag/izapple2/audio" ) -// NewMixer creates the mixer of a machine with every sound source of it +// NewFrontMixer creates the mixer of a machine with every sound source of it // attached, ready for the frontend to send the samples to its audio device -func NewMixer(a *izapple2.Apple2) *audio.Mixer { +func NewFrontMixer(a *izapple2.Apple2) *audio.Mixer { mixer := audio.NewMixer(a.GetClockMhz()) for _, source := range a.GetAudioSources() { source.SetAudioSink(mixer.NewSource()) diff --git a/frontend/shared/mouse.go b/frontend/shared/mouse.go index c71ff51f..132f90ab 100644 --- a/frontend/shared/mouse.go +++ b/frontend/shared/mouse.go @@ -21,8 +21,8 @@ func (m *Mouse) SetPosition(x int, y int, width int, height int) { return } - m.x = uint16(65536 * x / width) - m.y = uint16(65536 * y / height) + m.x = mousePosition(x, width) + m.y = mousePosition(y, height) } // SetButton presses or releases the button of the mouse @@ -30,10 +30,16 @@ func (m *Mouse) SetButton(pressed bool) { m.pressed = pressed } +// mousePosition turns a position on the window into the full range the machine +// reads the mouse in, keeping what is outside the window on the edge +func mousePosition(position int, size int) uint16 { + return uint16(max(min(65536*position/size, 65535), 0)) +} + +// izapple2.MouseProvider implementation + // ReadMouse returns the position of the pointer on the window and whether its // button is pressed func (m *Mouse) ReadMouse() (x uint16, y uint16, pressed bool) { return m.x, m.y, m.pressed } - -// TODO: SDL_WarpMouseInWindow diff --git a/frontend/shared/mouse_test.go b/frontend/shared/mouse_test.go index 03ad9524..07b8d203 100644 --- a/frontend/shared/mouse_test.go +++ b/frontend/shared/mouse_test.go @@ -22,4 +22,10 @@ func TestMouse(t *testing.T) { if x, y, _ := m.ReadMouse(); x != 32768 || y != 16384 { t.Errorf("the mouse moved to (%v, %v) on a window without size", x, y) } + + // The pointer outside the window stays on the edge + m.SetPosition(-10, 999, 640, 480) + if x, y, _ := m.ReadMouse(); x != 0 || y != 65535 { + t.Errorf("the mouse is at (%v, %v), expected (0, 65535)", x, y) + } } diff --git a/frontend/shared/paddles.go b/frontend/shared/paddles.go index 2ee7678f..95d5d3c4 100644 --- a/frontend/shared/paddles.go +++ b/frontend/shared/paddles.go @@ -130,6 +130,14 @@ func (p *Paddles) SetMouseButton(button int, pressed bool) { p.mouseButtons[button] = pressed } +// mouseToPaddle turns a position on the window into the value of a paddle +// centered on the center of the window +func mouseToPaddle(x int, width int) uint8 { + return uint8(max(min(x-(width/2)+paddleCentered, 255), 0)) +} + +// izapple2.JoysticksProvider implementation + // ReadButton returns whether a pushbutton is pressed, as the machine reads it func (p *Paddles) ReadButton(i int) bool { switch i { @@ -150,9 +158,3 @@ func (p *Paddles) ReadButton(i int) bool { func (p *Paddles) ReadPaddle(i int) (uint8, bool) { return p.paddle[i], p.hasPaddle[i] } - -// mouseToPaddle turns a position on the window into the value of a paddle -// centered on the center of the window -func mouseToPaddle(x int, width int) uint8 { - return uint8(max(min(x-(width/2)+paddleCentered, 255), 0)) -}