diff --git a/AGENTS.md b/AGENTS.md index 30be0b28..7360d659 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,7 +97,7 @@ The project includes E2E boot tests (`e2e_boot_test.go`, `e2e_woz_test.go`) that - Main emulator code is in the root package `izapple2` - Subpackages include: `storage`, `screen`, `fujinet`, `component` - Frontend implementations are in `frontend/` directory -- `frontend/shared` has the code more than one frontend needs and the emulator library has no place for, like the drop targets screen +- `frontend/shared` has the code more than one frontend needs and the emulator library has no place for, like the screen each frontend shows and the drop targets ### Naming Conventions 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/doc/frontend_sdl2.md b/doc/frontend_sdl2.md index a2faf739..ab14be60 100644 --- a/doc/frontend_sdl2.md +++ b/doc/frontend_sdl2.md @@ -105,6 +105,11 @@ Escape mapped to what the Apple II understands. The rest: On the Base64A, F3 is the Delete key of that keyboard. +The Apple keys are a place on the keyboard and are found by scancode, so they +work on the layouts where the alt keys produce another symbol. Losing the +window focus releases them, so one held while switching windows does not stay +pressed. + The title bar says which machine is running and shows `PAUSED!` while it is paused. With the four panels or the character map on screen it shows what is being displayed instead. diff --git a/doc/frontend_sdl3.md b/doc/frontend_sdl3.md index b3f8c8cb..84824749 100644 --- a/doc/frontend_sdl3.md +++ b/doc/frontend_sdl3.md @@ -44,11 +44,8 @@ The same command line as every other frontend, see The same as [a2sdl](frontend_sdl2.md#keys), including the paste with Shift-Insert or Cmd-V and the Open-Apple and Closed-Apple on the alt or option -keys. - -The Apple keys are found by scancode instead of by keycode. On the keyboard -layouts where the right alt is AltGr, SDL3 does not report it as an alt key, -and the Closed-Apple would never be pressed if it were looked up by keycode. +keys. Both frontends decide the keys with the same code, so they answer to the +same ones. ## What is different from SDL2 @@ -58,8 +55,6 @@ Nothing that you use, and a few things underneath: areas of the drives it can be dropped on is shown while it moves, with the one under the pointer marked. SDL2 only knows about the file once it is dropped, and has to show the areas with F8 or after the drop. -- Losing the window focus releases the joystick keys, so an Open-Apple held - while switching windows does not stay pressed. - Ctrl-C on the terminal quits the same way as closing the window, releasing everything on the way out. - The scaling of the picture is a property of the renderer instead of the diff --git a/frontend/a2ebiten/ebitenAudio.go b/frontend/a2ebiten/ebitenAudio.go index c8a5400b..be326140 100644 --- a/frontend/a2ebiten/ebitenAudio.go +++ b/frontend/a2ebiten/ebitenAudio.go @@ -6,7 +6,9 @@ import ( "github.com/hajimehoshi/ebiten/v2/audio" + "github.com/ivanizag/izapple2" a2audio "github.com/ivanizag/izapple2/audio" + "github.com/ivanizag/izapple2/frontend/shared" ) // ebitenAudio sends the mixed audio of the machine to the ebiten audio @@ -19,9 +21,9 @@ type ebitenAudio struct { samples []float32 } -func newEbitenAudio(clockMhz float64) *ebitenAudio { +func newEbitenAudio(a *izapple2.Apple2) *ebitenAudio { return &ebitenAudio{ - mixer: a2audio.NewMixer(clockMhz), + mixer: shared.NewFrontMixer(a), } } diff --git a/frontend/a2ebiten/ebitenDropTargets.go b/frontend/a2ebiten/ebitenDropTargets.go index 8021e604..a1be8ef3 100644 --- a/frontend/a2ebiten/ebitenDropTargets.go +++ b/frontend/a2ebiten/ebitenDropTargets.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "image" "io/fs" "os" @@ -106,22 +105,6 @@ func (d *ebitenDropTargets) dropped() int { return drive } -// showing returns whether the areas take over the screen, either because the -// user asked for them with F8 or because a file has just been dropped -func (d *ebitenDropTargets) showing(requested bool) bool { - return requested || d.targets.Flashing() -} - -// snapshot returns the screen with the areas, highlighting the drive that got -// the last file while the flash lasts, or the one under the pointer -func (d *ebitenDropTargets) snapshot() *image.RGBA { - selected := d.targets.FlashDrive() - if !d.targets.Flashing() { - selected = d.pointedDrive() - } - return d.targets.Snapshot(selected) -} - // pointedDrive returns the drive the mouse pointer is on. The positions are on // the virtual screen the game is laid out on, not on the window. func (d *ebitenDropTargets) pointedDrive() int { diff --git a/frontend/a2ebiten/ebitenKeyboard.go b/frontend/a2ebiten/ebitenKeyboard.go index de57b015..eedb62f3 100644 --- a/frontend/a2ebiten/ebitenKeyboard.go +++ b/frontend/a2ebiten/ebitenKeyboard.go @@ -4,33 +4,25 @@ import ( "fmt" "github.com/ivanizag/izapple2" - "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) +// ebitenKeyboard turns the key events of Ebitengine into the keys the +// frontends share type ebitenKeyboard struct { - a *izapple2.Apple2 - keyChannel *izapple2.KeyboardChannel + keyboard *shared.Keyboard - showHelp bool - showPages bool - showCharGen bool - showAltText bool - showFreq bool - showDropTargets bool - screenMode int + showFreq bool // The speed on the HUD, only this frontend has one debug bool } -func newEbitenKeyBoard(a *izapple2.Apple2) *ebitenKeyboard { +func newEbitenKeyBoard(a *izapple2.Apple2, view *shared.View) *ebitenKeyboard { var k ebitenKeyboard - k.a = a - k.keyChannel = izapple2.NewKeyboardChannel(a) - - k.screenMode = screen.ScreenModeColorScanlines + k.keyboard = shared.NewKeyboard(a, view) return &k } @@ -54,123 +46,76 @@ func (k *ebitenKeyboard) update() { } func (k *ebitenKeyboard) putText(text string) { - k.keyChannel.PutText(text) + k.keyboard.PutText(text) } func (k *ebitenKeyboard) putKey(key ebiten.Key) { - /* - See "Apple II reference manual", page 5 - - To get keys as understood by the Apple2 hardware run: - 10 A=PEEK(49152) - 20 PRINT A, A - 128 - 30 GOTO 10 - */ - ctrl := ebiten.IsKeyPressed(ebiten.KeyControl) shift := ebiten.IsKeyPressed(ebiten.KeyShift) - if ctrl { - if key >= ebiten.KeyA && key <= ebiten.KeyZ { - fmt.Println("Control Key: ", key.String()) - k.keyChannel.PutChar(uint8(key-ebiten.KeyA) - 97 + 1) - return - } + if ctrl && key >= ebiten.KeyA && key <= ebiten.KeyZ { + k.keyboard.PutCtrlLetter('a' + rune(key-ebiten.KeyA)) + return } - result := uint8(0) + if ctrl && key == ebiten.KeyF5 { + // Only this frontend draws the speed, the others report it + k.showFreq = !k.showFreq + return + } + + k.keyboard.PutKey(ebitenKey(key), ctrl, shift) +} +// ebitenKey translates a key of Ebitengine +func ebitenKey(key ebiten.Key) shared.Key { switch key { case ebiten.KeyEscape: - result = 27 + return shared.KeyEscape case ebiten.KeyBackspace: - result = 8 - case ebiten.KeyEnter: - result = 13 - case ebiten.KeyNumpadEnter: - result = 13 + return shared.KeyBackspace + case ebiten.KeyEnter, ebiten.KeyNumpadEnter: + return shared.KeyReturn + case ebiten.KeyTab: + return shared.KeyTab + case ebiten.KeyDelete: + return shared.KeyDelete case ebiten.KeyLeft: - if ctrl { - result = 31 // Base64A - } else { - result = 8 - } + return shared.KeyLeft case ebiten.KeyRight: - result = 21 - - // Apple //e + return shared.KeyRight case ebiten.KeyUp: - result = 11 // 31 in the Base64A + return shared.KeyUp case ebiten.KeyDown: - result = 10 - case ebiten.KeyTab: - result = 9 - case ebiten.KeyDelete: - result = 127 // 24 in the Base64A + return shared.KeyDown - // Base64A clone particularities - case ebiten.KeyF3: - result = 127 // Base64A - - // Control of the emulator case ebiten.KeyF1: - k.showHelp = !k.showHelp + return shared.KeyF1 case ebiten.KeyF2: - // While the help is shown, F2 alone triggers a reset. This is handy - // when the window manager (for example KDE) captures Ctrl-F2. - if ctrl || k.showHelp { - k.a.SendCommand(izapple2.CommandReset) - k.showHelp = false - } + return shared.KeyF2 + case ebiten.KeyF3: + return shared.KeyF3 case ebiten.KeyF4: - k.a.SendCommand(izapple2.CommandToggleCPUTrace) + return shared.KeyF4 case ebiten.KeyF5: - if ctrl { - k.showFreq = !k.showFreq - } else { - k.a.SendCommand(izapple2.CommandToggleSpeed) - } + return shared.KeyF5 case ebiten.KeyF6: - k.screenMode = screen.NextScreenMode(k.screenMode) + return shared.KeyF6 case ebiten.KeyF7: - k.showPages = !k.showPages + return shared.KeyF7 case ebiten.KeyF8: - k.showDropTargets = !k.showDropTargets - if k.showDropTargets { - // The help is shown on top of the drop targets, get it out of the way - k.showHelp = false - } + return shared.KeyF8 case ebiten.KeyF9: - k.a.SendCommand(izapple2.CommandDumpDebugInfo) + return shared.KeyF9 case ebiten.KeyF10: - if ctrl { - k.showCharGen = !k.showCharGen - } else if shift { - k.showAltText = !k.showAltText - } else { - k.a.SendCommand(izapple2.CommandNextCharGenPage) - } + return shared.KeyF10 case ebiten.KeyF12: - fallthrough - case ebiten.KeyPrintScreen: - if ctrl { - screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/") - } else { - err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png") - if err != nil { - fmt.Printf("Error saving snapshoot: %v.\n.", err) - } else { - fmt.Println("Saving snapshot 'snapshot.png'") - } - } + return shared.KeyF12 case ebiten.KeyPause: - k.a.SendCommand(izapple2.CommandPauseUnpause) + return shared.KeyPause + case ebiten.KeyPrintScreen: + return shared.KeyPrintScreen } - // Missing values 91 to 95. Usually control for [\]^_ - // On the Base64A it's control for \]./ - - if result != 0 { - k.keyChannel.PutChar(result) - } + return shared.KeyNone } diff --git a/frontend/a2ebiten/main.go b/frontend/a2ebiten/main.go index e628784f..0d358873 100644 --- a/frontend/a2ebiten/main.go +++ b/frontend/a2ebiten/main.go @@ -3,11 +3,10 @@ package main import ( "bytes" "fmt" - "image" "image/color" "github.com/ivanizag/izapple2" - a_screen "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/examples/resources/fonts" @@ -18,8 +17,10 @@ import ( type Game struct { a *izapple2.Apple2 image *ebiten.Image + 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() { @@ -53,21 +55,10 @@ func (g *Game) Update() error { } if g.updates%3 == 0 && !g.a.IsPaused() { // 20 times per second - var img *image.RGBA - vs := g.a.GetVideoSource() - if g.keyboard.showHelp { - img = a_screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/) - } else if g.dropTargets.showing(g.keyboard.showDropTargets) { - img = g.dropTargets.snapshot() - } else if g.keyboard.showCharGen { - cgPage, cgPages := g.a.GetCgPageInfo() - img = a_screen.SnapshotCharacterGenerator(vs, g.keyboard.showAltText) - ebiten.SetWindowTitle(fmt.Sprintf("%v character map, page %v/%v", g.a.Name, cgPage+1, cgPages)) - } else if g.keyboard.showPages { - img = a_screen.SnapshotParts(vs, g.keyboard.screenMode) - ebiten.SetWindowTitle(fmt.Sprintf("%v %v %vx%v", g.a.Name, a_screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2)) - } else { - img = a_screen.Snapshot(vs, g.keyboard.screenMode) + img, viewTitle := g.view.Snapshot(g.a, g.dropTargets.targets, + g.dropTargets.pointedDrive()) + if viewTitle != "" { + ebiten.SetWindowTitle(viewTitle) } if img != nil { g.image = ebiten.NewImageFromImage(img) @@ -82,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{} @@ -133,15 +138,16 @@ func ebitenRun(a *izapple2.Apple2) { title := "iz-" + a.Name + " (F1 for help)" ebiten.SetWindowTitle(title) + view := shared.NewView() game := &Game{ a: a, - keyboard: newEbitenKeyBoard(a), - speaker: newEbitenAudio(a.GetClockMhz()), + view: view, + keyboard: newEbitenKeyBoard(a, view), + speaker: newEbitenAudio(a), + mouse: shared.NewMouse(), dropTargets: newEbitenDropTargets(a), } - for _, source := range a.GetAudioSources() { - source.SetAudioSink(game.speaker.mixer.NewSource()) - } + a.SetMouseProvider(game.mouse) var err error game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf)) @@ -155,31 +161,6 @@ func ebitenRun(a *izapple2.Apple2) { } } -var helpMessage = ` - F1: Show/Hide help - Ctrl-F2: Reset - F1, F2: Reset - F4: Show/Hide CPU trace - F5: Fast/Normal speed - Ctrl-F5: Show speed - F6: Next screen mode - F7: Show/Hide pages - F8: Show/Hide drop targets - F10: Next character set - Ctrl-F10: Show/Hide character set - Shift-F10: Show/Hide alternate text - F12: Save screen snapshot - Pause: Pause the emulation - - Left alt or option key: Open-Apple - Right alt or option key: Closed-Apple - -Drop a file on a drive area to load it - - Run izapple2 -h for more options - https://github.com/ivanizag/izapple2 -` - /* To test the WebAssembly version, run: go run github.com/hajimehoshi/wasmserve@latest . diff --git a/frontend/a2libretro/audio.go b/frontend/a2libretro/audio.go index d36eb9fa..596a35d8 100644 --- a/frontend/a2libretro/audio.go +++ b/frontend/a2libretro/audio.go @@ -7,7 +7,9 @@ package main import "C" import ( + "github.com/ivanizag/izapple2" a2audio "github.com/ivanizag/izapple2/audio" + "github.com/ivanizag/izapple2/frontend/shared" ) const sampleRate = a2audio.SampleRate @@ -30,15 +32,15 @@ type audioOutput struct { stereo []C.int16_t } -func newAudioOutput(clockMhz float64) *audioOutput { - samplesPerFrame := sampleRate * cyclesPerFrame / (clockMhz * 1_000_000) +func newAudioOutput(a *izapple2.Apple2) *audioOutput { + samplesPerFrame := sampleRate * cyclesPerFrame / (a.GetClockMhz() * 1_000_000) // Room for a frame and a bit, the pending samples never add up to a full // extra sample size := int(samplesPerFrame) + 2 return &audioOutput{ - mixer: a2audio.NewMixer(clockMhz), + mixer: shared.NewFrontMixer(a), samplesPerFrame: samplesPerFrame, mono: make([]float32, size), stereo: make([]C.int16_t, 2*size), diff --git a/frontend/a2libretro/core.go b/frontend/a2libretro/core.go index 52c4eeac..bb52d521 100644 --- a/frontend/a2libretro/core.go +++ b/frontend/a2libretro/core.go @@ -75,13 +75,10 @@ func (c *core) build() bool { c.a = a c.video = newVideoOutput(options.screenMode) - c.audio = newAudioOutput(a.GetClockMhz()) + c.audio = newAudioOutput(a) c.keyboard = newKeyboard(a) c.joysticks = newJoysticks() - for _, source := range a.GetAudioSources() { - source.SetAudioSink(c.audio.mixer.NewSource()) - } a.SetJoysticksProvider(c.joysticks) a.SetMouseProvider(newMouse()) diff --git a/frontend/a2libretro/keyboard.go b/frontend/a2libretro/keyboard.go index f83e1a10..1fcc345c 100644 --- a/frontend/a2libretro/keyboard.go +++ b/frontend/a2libretro/keyboard.go @@ -10,6 +10,7 @@ import ( "unsafe" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/frontend/shared" ) // keyboard turns the key events of the frontend into the codes the Apple II @@ -70,8 +71,17 @@ func izapple2KeyboardEvent(down C.int, keycode C.uint, character C.uint32_t, key return } - if key, ok := specialKey(uint32(keycode), uint16(keyModifiers)); ok { - theCore.keyboard.channel.PutChar(key) + ctrl := keyModifiers&C.RETROKMOD_CTRL != 0 + + if ctrl && keycode >= C.RETROK_a && keycode <= C.RETROK_z { + if char, ok := shared.CharForCtrlLetter('a' + rune(keycode-C.RETROK_a)); ok { + theCore.keyboard.channel.PutChar(char) + return + } + } + + if char, ok := shared.CharForKey(retroKey(uint32(keycode)), ctrl); ok { + theCore.keyboard.channel.PutChar(char) return } @@ -80,32 +90,30 @@ func izapple2KeyboardEvent(down C.int, keycode C.uint, character C.uint32_t, key theCore.keyboard.channel.PutRune(rune(character)) } -// specialKey returns the code of the keys that do not come as a printable -// character, and of the control combinations -func specialKey(keycode uint32, modifiers uint16) (uint8, bool) { +// retroKey translates a key of libretro. Only the ones that do not come as a +// printable character are needed, the frontend keeps the rest of the keyboard +// for itself. +func retroKey(keycode uint32) shared.Key { switch keycode { case C.RETROK_RETURN, C.RETROK_KP_ENTER: - return 13, true + return shared.KeyReturn case C.RETROK_TAB: - return 9, true + return shared.KeyTab case C.RETROK_ESCAPE: - return 27, true - case C.RETROK_BACKSPACE, C.RETROK_LEFT: - return 8, true + return shared.KeyEscape + case C.RETROK_BACKSPACE: + return shared.KeyBackspace + case C.RETROK_LEFT: + return shared.KeyLeft case C.RETROK_RIGHT: - return 21, true + return shared.KeyRight case C.RETROK_UP: - return 11, true + return shared.KeyUp case C.RETROK_DOWN: - return 10, true + return shared.KeyDown case C.RETROK_DELETE: - return 127, true - } - - if modifiers&C.RETROKMOD_CTRL != 0 && - keycode >= C.RETROK_a && keycode <= C.RETROK_z { - return uint8(keycode-C.RETROK_a) + 1, true + return shared.KeyDelete } - return 0, false + return shared.KeyNone } diff --git a/frontend/a2sdl/main.go b/frontend/a2sdl/main.go index 954b76f0..d4345cdf 100644 --- a/frontend/a2sdl/main.go +++ b/frontend/a2sdl/main.go @@ -4,11 +4,10 @@ package main import ( "fmt" - "image" "unsafe" "github.com/ivanizag/izapple2" - "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/pkg/profile" "github.com/veandco/go-sdl2/sdl" @@ -56,18 +55,16 @@ func sdlRun(a *izapple2.Apple2) { sdl.EventState(sdl.DROPBEGIN, sdl.DISABLE) sdl.EventState(sdl.DROPCOMPLETE, sdl.DISABLE) - kp := newSDLKeyBoard(a) + view := shared.NewView() + kp := newSDLKeyBoard(a, view) - s := newSDLAudio(a.GetClockMhz()) - for _, source := range a.GetAudioSources() { - source.SetAudioSink(s.mixer.NewSource()) - } + s := newSDLAudio(a) s.start() j := newSDLJoysticks(!a.UsesMouse()) - a.SetJoysticksProvider(j) + a.SetJoysticksProvider(j.paddles) - m := newSDLMouse() + m := shared.NewMouse() a.SetMouseProvider(m) d := newSDLDropTargets(a, window) @@ -85,6 +82,10 @@ func sdlRun(a *izapple2.Apple2) { case *sdl.KeyboardEvent: kp.putKey(t) j.putKey(t) + case *sdl.WindowEvent: + if t.Event == sdl.WINDOWEVENT_FOCUS_LOST { + j.paddles.ReleaseAppleKeys() + } case *sdl.TextInputEvent: kp.putText(t.GetText()) case *sdl.JoyAxisEvent: @@ -94,10 +95,12 @@ func sdlRun(a *izapple2.Apple2) { case *sdl.MouseMotionEvent: w, h := window.GetSize() j.putMouseMotionEvent(t, w, h) - m.putMouseMotionEvent(t, w, h) + m.SetPosition(int(t.X), int(t.Y), int(w), int(h)) case *sdl.MouseButtonEvent: j.putMouseButtonEvent(t) - m.putMouseButtonEvent(t) + if sdlMouseButton(t.Button) == shared.MouseButtonLeft { + m.SetButton(t.State == sdl.PRESSED) + } case *sdl.DropEvent: switch t.Type { case sdl.DROPFILE: @@ -122,21 +125,9 @@ func sdlRun(a *izapple2.Apple2) { } if !a.IsPaused() { - var img *image.RGBA - vs := a.GetVideoSource() - if kp.showHelp { - img = screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/) - } else if d.showing(kp.showDropTargets) { - img = d.snapshot() - } else if kp.showCharGen { - cgPage, cgPages := a.GetCgPageInfo() - img = screen.SnapshotCharacterGenerator(vs, kp.showAltText) - window.SetTitle(fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages)) - } else if kp.showPages { - img = screen.SnapshotParts(vs, kp.screenMode) - window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2)) - } else { - img = screen.Snapshot(vs, kp.screenMode) + img, viewTitle := view.Snapshot(a, d.targets, d.pointedDrive()) + if viewTitle != "" { + window.SetTitle(viewTitle) } if img != nil { surface, err := sdl.CreateRGBSurfaceFrom(unsafe.Pointer(&img.Pix[0]), @@ -167,29 +158,4 @@ func sdlRun(a *izapple2.Apple2) { } -var helpMessage = ` - F1: Show/Hide help - Ctrl-F2: Reset - F1, F2: Reset - F4: Show/Hide CPU trace - F5: Fast/Normal speed - Ctrl-F5: Show speed - F6: Next screen mode - F7: Show/Hide pages - F8: Show/Hide drop targets - F10: Next character set - Ctrl-F10: Show/Hide character set - Shift-F10: Show/Hide alternate text - F12: Save screen snapshot - Pause: Pause the emulation - - Left alt or option key: Open-Apple - Right alt or option key: Closed-Apple - -Drop a file on a drive area to load it - - Run izapple2 -h for more options - https://github.com/ivanizag/izapple2 -` - /////////////////////////////////////// diff --git a/frontend/a2sdl/sdlAudio.go b/frontend/a2sdl/sdlAudio.go index f1624f54..afd8a175 100644 --- a/frontend/a2sdl/sdlAudio.go +++ b/frontend/a2sdl/sdlAudio.go @@ -10,7 +10,9 @@ import ( "sync/atomic" "unsafe" + "github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/audio" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/veandco/go-sdl2/sdl" ) @@ -30,9 +32,9 @@ because the callback runs on the SDL audio thread. */ var theSDLAudio atomic.Pointer[sdlAudio] -func newSDLAudio(clockMhz float64) *sdlAudio { +func newSDLAudio(a *izapple2.Apple2) *sdlAudio { return &sdlAudio{ - mixer: audio.NewMixer(clockMhz), + mixer: shared.NewFrontMixer(a), } } diff --git a/frontend/a2sdl/sdlDropTargets.go b/frontend/a2sdl/sdlDropTargets.go index 144bc781..0b968064 100644 --- a/frontend/a2sdl/sdlDropTargets.go +++ b/frontend/a2sdl/sdlDropTargets.go @@ -3,8 +3,6 @@ package main import ( - "image" - "github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/frontend/shared" "github.com/veandco/go-sdl2/sdl" @@ -54,22 +52,6 @@ func (d *sdlDropTargets) dropped() int { return drive } -// showing returns whether the areas take over the screen, either because the -// user asked for them with F8 or because a file has just been dropped -func (d *sdlDropTargets) showing(requested bool) bool { - return requested || d.targets.Flashing() -} - -// snapshot returns the screen with the areas, highlighting the drive that got -// the last file while the flash lasts, or the one under the pointer -func (d *sdlDropTargets) snapshot() *image.RGBA { - selected := d.targets.FlashDrive() - if !d.targets.Flashing() { - selected = d.pointedDrive() - } - return d.targets.Snapshot(selected) -} - // pointedDrive returns the drive the mouse pointer is on, -1 when it is // outside the window func (d *sdlDropTargets) pointedDrive() int { diff --git a/frontend/a2sdl/sdlJoysticks.go b/frontend/a2sdl/sdlJoysticks.go index 0454640d..1b020660 100644 --- a/frontend/a2sdl/sdlJoysticks.go +++ b/frontend/a2sdl/sdlJoysticks.go @@ -3,31 +3,17 @@ package main import ( + "github.com/ivanizag/izapple2/frontend/shared" "github.com/veandco/go-sdl2/sdl" ) -/* - Apple 2 supports four paddles and 3 pushbuttons. The first two paddles are -the X, Y axis of the first joystick. The second two correspond the the second -joystick. - Button 0 is the primary button of joystick 0. - Button 1 is the secondary button of joystick 0 but also the primary button of -joystick 1. - Button 2 is the secondary button of Joystick 1. -*/ - +// sdlJoysticks turns the joystick, mouse and apple key events of SDL2 into the +// paddles and buttons the machine reads type sdlJoysticks struct { - paddle [4]uint8 - hasPaddle [4]bool - button [4]bool - keys [3]bool - mousebuttons [3]bool - useMouse bool + paddles *shared.Paddles } func newSDLJoysticks(useMouseAlt bool) *sdlJoysticks { - var j sdlJoysticks - err := sdl.InitSubSystem(sdl.INIT_JOYSTICK) if err != nil { panic(err) @@ -35,82 +21,30 @@ func newSDLJoysticks(useMouseAlt bool) *sdlJoysticks { // Init up to two joysticks sdl.JoystickEventState(sdl.ENABLE) - joyCount := sdl.NumJoysticks() - for iJoy := 0; iJoy < joyCount && iJoy < 2; iJoy++ { - /*joystick := */ sdl.JoystickOpen(iJoy) - j.hasPaddle[iJoy*2] = true - j.hasPaddle[iJoy*2+1] = true - } - - // Initialize to max resistance if unplugged - j.paddle[0] = 255 - j.paddle[1] = 255 - j.paddle[2] = 255 - j.paddle[3] = 255 - - if useMouseAlt && !j.hasPaddle[0] { - // Use the mouse as joystick - j.useMouse = true - j.hasPaddle[0] = true - j.hasPaddle[1] = true - j.paddle[0] = 127 - j.paddle[1] = 127 + joyCount := min(sdl.NumJoysticks(), 2) + for iJoy := range joyCount { + sdl.JoystickOpen(iJoy) } - // To enter Apple IIe on self test mode - // j.keys[1] = true - + var j sdlJoysticks + j.paddles = shared.NewPaddles(joyCount, useMouseAlt) return &j } func (j *sdlJoysticks) putAxisEvent(e *sdl.JoyAxisEvent) { - if e.Which >= 2 || e.Axis >= 2 { - // Process only the first two axis of the first two joysticks - return - } - - j.paddle[uint8(e.Which)*2+e.Axis] = uint8((e.Value >> 8) + 128) + j.paddles.SetAxis(int(e.Which), int(e.Axis), e.Value) } func (j *sdlJoysticks) putButtonEvent(e *sdl.JoyButtonEvent) { - if e.Which >= 2 { - // Process only the buttons of the first two joysticks - return - } - - j.button[uint8(e.Which)*2+(e.Button%2)] = (e.State != 0) -} - -func mouseToJoyCentered(x int32, w int32) uint8 { - r := max(min(x-(w/2)+127, 255), 0) - return uint8(r) - + j.paddles.SetButton(int(e.Which), int(e.Button), e.State != 0) } func (j *sdlJoysticks) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) { - if j.useMouse { - // The mouse moves on all the window - // j.paddle[0] = mouseToJoyFull(e.X, width) - // j.paddle[1] = mouseToJoyFull(e.Y, height) - - // The mouse moves around the center of the window - j.paddle[0] = mouseToJoyCentered(e.X, width) - j.paddle[1] = mouseToJoyCentered(e.Y, height) - } + j.paddles.SetMousePosition(int(e.X), int(e.Y), int(width), int(height)) } func (j *sdlJoysticks) putMouseButtonEvent(e *sdl.MouseButtonEvent) { - if j.useMouse { - pressed := e.State == sdl.PRESSED - switch e.Button { - case 1: // BUTTON_LEFT - j.mousebuttons[0] = pressed - case 3: // BUTTON_RIGHT - j.mousebuttons[1] = pressed - case 2: // BUTTON_MIDDLE - j.mousebuttons[2] = pressed - } - } + j.paddles.SetMouseButton(sdlMouseButton(e.Button), e.State == sdl.PRESSED) } func (j *sdlJoysticks) putKey(keyEvent *sdl.KeyboardEvent) { @@ -119,34 +53,31 @@ func (j *sdlJoysticks) putKey(keyEvent *sdl.KeyboardEvent) { Actually the Apple//e does this with the open and solid apple keys. Alt key - button 0 - Open apple AltGr key - button 1- Solid apple - //Win key - button 2 (Not in the Apple //e keyboard) + + The apple keys are a place on the keyboard, not a symbol, so we look at + the scancode. The keycode is resolved through the current keymap: on + layouts where the left alt key is Meta_L instead of Alt_L it is not + K_LALT and the open apple would never be pressed. */ pressed := keyEvent.Type == sdl.KEYDOWN - switch keyEvent.Keysym.Sym { - case sdl.K_LALT: - j.keys[0] = pressed - case sdl.K_RALT: - j.keys[1] = pressed - // case sdl.K_LGUI: - // j.keys[2] = pressed + switch keyEvent.Keysym.Scancode { + case sdl.SCANCODE_LALT: + j.paddles.SetOpenApple(pressed) + case sdl.SCANCODE_RALT: + j.paddles.SetClosedApple(pressed) } - } -func (j *sdlJoysticks) ReadButton(i int) bool { - var value bool - switch i { - case 0: - value = j.button[0] || j.keys[0] || j.mousebuttons[0] - case 1: - // It can be secondary of first or primary of second - value = j.button[1] || j.button[2] || j.keys[1] || j.mousebuttons[1] - case 2: - value = j.button[3] || j.keys[2] || j.mousebuttons[2] +// sdlMouseButton translates a button of the mouse of SDL2 +func sdlMouseButton(button uint8) int { + switch button { + case sdl.BUTTON_LEFT: + return shared.MouseButtonLeft + case sdl.BUTTON_RIGHT: + return shared.MouseButtonRight + case sdl.BUTTON_MIDDLE: + return shared.MouseButtonMiddle } - return value -} -func (j *sdlJoysticks) ReadPaddle(i int) (uint8, bool) { - return j.paddle[i], j.hasPaddle[i] + return -1 } diff --git a/frontend/a2sdl/sdlKeyboard.go b/frontend/a2sdl/sdlKeyboard.go index 633aa3f1..b1b50396 100644 --- a/frontend/a2sdl/sdlKeyboard.go +++ b/frontend/a2sdl/sdlKeyboard.go @@ -3,49 +3,27 @@ package main import ( - "fmt" - "time" - "github.com/ivanizag/izapple2" - "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/veandco/go-sdl2/sdl" ) +// sdlKeyboard turns the key events of SDL2 into the keys the frontends share type sdlKeyboard struct { - a *izapple2.Apple2 - keyChannel *izapple2.KeyboardChannel - - showHelp bool - showPages bool - showCharGen bool - showAltText bool - showDropTargets bool - screenMode int + keyboard *shared.Keyboard } -func newSDLKeyBoard(a *izapple2.Apple2) *sdlKeyboard { +func newSDLKeyBoard(a *izapple2.Apple2, view *shared.View) *sdlKeyboard { var k sdlKeyboard - k.a = a - k.keyChannel = izapple2.NewKeyboardChannel(a) - - k.screenMode = screen.ScreenModeColorScanlines + k.keyboard = shared.NewKeyboard(a, view) return &k } func (k *sdlKeyboard) putText(text string) { - k.keyChannel.PutText(text) + k.keyboard.PutText(text) } func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) { - /* - See "Apple II reference manual", page 5 - - To get keys as understood by the Apple2 hardware run: - 10 A=PEEK(49152) - 20 PRINT A, A - 128 - 30 GOTO 10 - */ - if keyEvent.Type != sdl.KEYDOWN { // Process only key pushes return @@ -56,129 +34,69 @@ func (k *sdlKeyboard) putKey(keyEvent *sdl.KeyboardEvent) { shift := key.Mod&sdl.KMOD_SHIFT != 0 command := key.Mod&sdl.KMOD_GUI != 0 - if ctrl { - if key.Sym >= 'a' && key.Sym <= 'z' { - k.keyChannel.PutChar(uint8(key.Sym) - 97 + 1) - return - } + if ctrl && k.keyboard.PutCtrlLetter(rune(key.Sym)) { + return } - result := uint8(0) + // Pasting reads the clipboard of SDL, it is not a shared command + if (key.Sym == sdl.K_INSERT && shift) || (key.Sym == sdl.K_v && command) { + text, _ := sdl.GetClipboardText() + k.keyboard.Paste(text) + return + } + + k.keyboard.PutKey(sdlKey(key.Sym), ctrl, shift) +} - switch key.Sym { +// sdlKey translates a key of SDL2 +func sdlKey(sym sdl.Keycode) shared.Key { + switch sym { case sdl.K_ESCAPE: - result = 27 + return shared.KeyEscape case sdl.K_BACKSPACE: - result = 8 - case sdl.K_RETURN: - result = 13 - case sdl.K_RETURN2: - result = 13 + return shared.KeyBackspace + case sdl.K_RETURN, sdl.K_RETURN2: + return shared.KeyReturn + case sdl.K_TAB: + return shared.KeyTab + case sdl.K_DELETE: + return shared.KeyDelete case sdl.K_LEFT: - if ctrl { - result = 31 // Base64A - } else { - result = 8 - } + return shared.KeyLeft case sdl.K_RIGHT: - result = 21 - - // Apple //e + return shared.KeyRight case sdl.K_UP: - result = 11 // 31 in the Base64A + return shared.KeyUp case sdl.K_DOWN: - result = 10 - case sdl.K_TAB: - result = 9 - case sdl.K_DELETE: - result = 127 // 24 in the Base64A - - // Base64A clone particularities - case sdl.K_F3: - result = 127 // Base64A + return shared.KeyDown - // Control of the emulator case sdl.K_F1: - k.showHelp = !k.showHelp + return shared.KeyF1 case sdl.K_F2: - // While the help is shown, F2 alone triggers a reset. This is handy - // when the window manager (for example KDE) captures Ctrl-F2. - if ctrl || k.showHelp { - k.a.SendCommand(izapple2.CommandReset) - k.showHelp = false - } + return shared.KeyF2 + case sdl.K_F3: + return shared.KeyF3 case sdl.K_F4: - k.a.SendCommand(izapple2.CommandToggleCPUTrace) + return shared.KeyF4 case sdl.K_F5: - if ctrl { - k.a.SendCommand(izapple2.CommandShowSpeed) - } else { - k.a.SendCommand(izapple2.CommandToggleSpeed) - } + return shared.KeyF5 case sdl.K_F6: - k.screenMode = screen.NextScreenMode(k.screenMode) + return shared.KeyF6 case sdl.K_F7: - k.showPages = !k.showPages + return shared.KeyF7 case sdl.K_F8: - k.showDropTargets = !k.showDropTargets - if k.showDropTargets { - // The help is shown on top of the drop targets, get it out of the way - k.showHelp = false - } + return shared.KeyF8 case sdl.K_F9: - k.a.SendCommand(izapple2.CommandDumpDebugInfo) + return shared.KeyF9 case sdl.K_F10: - if ctrl { - k.showCharGen = !k.showCharGen - } else if shift { - k.showAltText = !k.showAltText - } else { - k.a.SendCommand(izapple2.CommandNextCharGenPage) - } + return shared.KeyF10 case sdl.K_F12: - fallthrough - case sdl.K_PRINTSCREEN: - if ctrl { - screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/") - } else { - err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png") - if err != nil { - fmt.Printf("Error saving snapshoot: %v.\n.", err) - } else { - fmt.Println("Saving snapshot 'snapshot.png'") - } - } + return shared.KeyF12 case sdl.K_PAUSE: - k.a.SendCommand(izapple2.CommandPauseUnpause) - case sdl.K_INSERT: - if shift { - text, _ := sdl.GetClipboardText() - go k.performPaste(text) - } - case sdl.K_v: - if command { - text, _ := sdl.GetClipboardText() - go k.performPaste(text) - } - } - - // Missing values 91 to 95. Usually control for [\]^_ - // On the Base64A it's control for \]./ - - if result != 0 { - k.keyChannel.PutChar(result) + return shared.KeyPause + case sdl.K_PRINTSCREEN: + return shared.KeyPrintScreen } -} -func (k *sdlKeyboard) performPaste(text string) { - // Note 1: Pasting is too fast, so we slow it down. - // Note 2: Need to translate CR/LF's - for _, ch := range text { - if ch == '\r' || ch == '\n' { - k.keyChannel.PutChar(13) - } else { - k.keyChannel.PutRune(ch) - } - time.Sleep(20 * time.Millisecond) - } + return shared.KeyNone } diff --git a/frontend/a2sdl/sdlMouse.go b/frontend/a2sdl/sdlMouse.go deleted file mode 100644 index ceec16d8..00000000 --- a/frontend/a2sdl/sdlMouse.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build !js - -package main - -import ( - "github.com/veandco/go-sdl2/sdl" -) - -type sdlMouse struct { - x uint16 - y uint16 - pressed bool -} - -func newSDLMouse() *sdlMouse { - var m sdlMouse - return &m -} - -func (m *sdlMouse) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) { - m.x = uint16(65536 * e.X / width) - m.y = uint16(65536 * e.Y / height) -} - -func (m *sdlMouse) putMouseButtonEvent(e *sdl.MouseButtonEvent) { - if e.Button == 1 { // BUTTTON_LEFT - m.pressed = e.State == sdl.PRESSED - } -} - -func (m *sdlMouse) ReadMouse() (x uint16, y uint16, pressed bool) { - return m.x, m.y, m.pressed -} - -// TODO: SDL_WarpMouseInWindow diff --git a/frontend/a2sdl3/main.go b/frontend/a2sdl3/main.go index d22906cb..2674d662 100644 --- a/frontend/a2sdl3/main.go +++ b/frontend/a2sdl3/main.go @@ -4,14 +4,13 @@ package main import ( "fmt" - "image" "os" "os/signal" "runtime" "syscall" "github.com/ivanizag/izapple2" - "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/Zyko0/go-sdl3/bin/binsdl" "github.com/Zyko0/go-sdl3/sdl" @@ -64,19 +63,17 @@ func sdlRun(a *izapple2.Apple2) { fmt.Printf("Error starting text input: %v.\n", err) } - kp := newSDL3Keyboard(a) + view := shared.NewView() + kp := newSDL3Keyboard(a, view) - s := newSDL3Audio(a.GetClockMhz()) - for _, source := range a.GetAudioSources() { - source.SetAudioSink(s.mixer.NewSource()) - } + s := newSDL3Audio(a) s.start() defer s.close() j := newSDL3Joysticks(!a.UsesMouse()) - a.SetJoysticksProvider(j) + a.SetJoysticksProvider(j.paddles) - m := newSDL3Mouse() + m := shared.NewMouse() a.SetMouseProvider(m) d := newSDL3DropTargets(a, window) @@ -112,7 +109,7 @@ func sdlRun(a *izapple2.Apple2) { kp.putKey(e) j.putKey(e) case sdl.EVENT_WINDOW_FOCUS_LOST: - j.releaseKeys() + j.paddles.ReleaseAppleKeys() case sdl.EVENT_TEXT_INPUT: kp.putText(event.TextInputEvent().Text) case sdl.EVENT_JOYSTICK_AXIS_MOTION: @@ -123,12 +120,14 @@ func sdlRun(a *izapple2.Apple2) { w, h, _ := window.Size() e := event.MouseMotionEvent() j.putMouseMotionEvent(e, w, h) - m.putMouseMotionEvent(e, w, h) + m.SetPosition(int(e.X), int(e.Y), int(w), int(h)) d.dragEnded() case sdl.EVENT_MOUSE_BUTTON_DOWN, sdl.EVENT_MOUSE_BUTTON_UP: e := event.MouseButtonEvent() j.putMouseButtonEvent(e) - m.putMouseButtonEvent(e) + if sdl3MouseButton(e.Button) == shared.MouseButtonLeft { + m.SetButton(e.Down) + } case sdl.EVENT_DROP_BEGIN: // Unlike SDL2, SDL3 reports the file being dragged over the // window, so the drop targets can be shown while it moves. @@ -160,21 +159,9 @@ func sdlRun(a *izapple2.Apple2) { } if !a.IsPaused() { - var img *image.RGBA - vs := a.GetVideoSource() - if kp.showHelp { - img = screen.SnapshotMessageGenerator(vs, helpMessage, false /*is80Columns*/) - } else if d.showing(kp.showDropTargets) { - img = d.snapshot() - } else if kp.showCharGen { - cgPage, cgPages := a.GetCgPageInfo() - img = screen.SnapshotCharacterGenerator(vs, kp.showAltText) - window.SetTitle(fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages)) - } else if kp.showPages { - img = screen.SnapshotParts(vs, kp.screenMode) - window.SetTitle(fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), img.Rect.Dx()/2, img.Rect.Dy()/2)) - } else { - img = screen.Snapshot(vs, kp.screenMode) + img, viewTitle := view.Snapshot(a, d.targets, d.pointedDrive()) + if viewTitle != "" { + window.SetTitle(viewTitle) } if img != nil { // image.RGBA stores the bytes as R, G, B, A. That is @@ -205,29 +192,4 @@ func sdlRun(a *izapple2.Apple2) { } } -var helpMessage = ` - F1: Show/Hide help - Ctrl-F2: Reset - F1, F2: Reset - F4: Show/Hide CPU trace - F5: Fast/Normal speed - Ctrl-F5: Show speed - F6: Next screen mode - F7: Show/Hide pages - F8: Show/Hide drop targets - F10: Next character set - Ctrl-F10: Show/Hide character set - Shift-F10: Show/Hide alternate text - F12: Save screen snapshot - Pause: Pause the emulation - - Left alt or option key: Open-Apple - Right alt or option key: Closed-Apple - -Drop a file on a drive area to load it - - Run izapple2 -h for more options - https://github.com/ivanizag/izapple2 -` - /////////////////////////////////////// diff --git a/frontend/a2sdl3/sdl3Audio.go b/frontend/a2sdl3/sdl3Audio.go index d7c1b7bd..3e5ea253 100644 --- a/frontend/a2sdl3/sdl3Audio.go +++ b/frontend/a2sdl3/sdl3Audio.go @@ -7,7 +7,9 @@ import ( "strconv" "unsafe" + "github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/audio" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/Zyko0/go-sdl3/sdl" ) @@ -23,9 +25,9 @@ type sdl3Audio struct { buf []float32 } -func newSDL3Audio(clockMhz float64) *sdl3Audio { +func newSDL3Audio(a *izapple2.Apple2) *sdl3Audio { return &sdl3Audio{ - mixer: audio.NewMixer(clockMhz), + mixer: shared.NewFrontMixer(a), buf: make([]float32, bufferSamples), } } diff --git a/frontend/a2sdl3/sdl3DropTargets.go b/frontend/a2sdl3/sdl3DropTargets.go index 10c00011..c706e0f4 100644 --- a/frontend/a2sdl3/sdl3DropTargets.go +++ b/frontend/a2sdl3/sdl3DropTargets.go @@ -3,8 +3,6 @@ package main import ( - "image" - "github.com/ivanizag/izapple2" "github.com/ivanizag/izapple2/frontend/shared" @@ -24,10 +22,6 @@ file went. type sdl3DropTargets struct { targets *shared.DropTargets window *sdl.Window - - dragging bool // A file is being dragged over the window - dragKnown bool // The position of the file has been reported - dragX float32 // Where the file is on the window } func newSDL3DropTargets(a *izapple2.Apple2, window *sdl.Window) *sdl3DropTargets { @@ -40,22 +34,19 @@ func newSDL3DropTargets(a *izapple2.Apple2, window *sdl.Window) *sdl3DropTargets // dragStarted is called when a file begins to be dragged over the window, // before knowing where it is func (d *sdl3DropTargets) dragStarted() { - d.dragging = true - d.dragKnown = false + d.targets.DragStarted() } // dragMoved tracks a file being dragged over the window func (d *sdl3DropTargets) dragMoved(x float32) { - d.dragging = true - d.dragKnown = true - d.dragX = x + d.targets.DragMoved(d.driveAt(x)) } // dragEnded is called when the file is dropped or leaves the window. The mouse // motion events do not arrive while a file is being dragged, so the first one // also means that the drag is over, whatever SDL reported. func (d *sdl3DropTargets) dragEnded() { - d.dragging = false + d.targets.DragEnded() } // dropped returns the drive that gets a file dropped at the given position of @@ -68,33 +59,27 @@ func (d *sdl3DropTargets) dropped(x float32) int { return drive } -// showing returns whether the areas take over the screen, either because a -// file is being dragged, because the user asked for them with F8, or because a -// file has just been dropped -func (d *sdl3DropTargets) showing(requested bool) bool { - return d.dragging || requested || d.targets.Flashing() -} - -// snapshot returns the screen with the areas, highlighting the drive under the -// file being dragged or the one that got the last file -func (d *sdl3DropTargets) snapshot() *image.RGBA { - selected := -1 - if d.dragging { - if d.dragKnown { - selected = d.driveAt(d.dragX) - } - } else if d.targets.Flashing() { - selected = d.targets.FlashDrive() +func (d *sdl3DropTargets) driveAt(x float32) int { + width, _, err := d.window.Size() + if err != nil { + return -1 } - return d.targets.Snapshot(selected) + return d.targets.DriveAt(int(x), int(width)) } -func (d *sdl3DropTargets) driveAt(x float32) int { +// pointedDrive returns the drive the mouse pointer is on, -1 when it is +// outside the window +func (d *sdl3DropTargets) pointedDrive() int { + if sdl.GetMouseFocus() != d.window { + return -1 + } + + _, mouseX, _ := sdl.GetMouseState() width, _, err := d.window.Size() - if err != nil { + if err != nil || mouseX < 0 || mouseX >= float32(width) { return -1 } - return d.targets.DriveAt(int(x), int(width)) + return d.targets.DriveAt(int(mouseX), int(width)) } diff --git a/frontend/a2sdl3/sdl3Joysticks.go b/frontend/a2sdl3/sdl3Joysticks.go index 324de7d8..adad2e22 100644 --- a/frontend/a2sdl3/sdl3Joysticks.go +++ b/frontend/a2sdl3/sdl3Joysticks.go @@ -3,126 +3,61 @@ package main import ( + "github.com/ivanizag/izapple2/frontend/shared" + "github.com/Zyko0/go-sdl3/sdl" ) -/* - Apple 2 supports four paddles and 3 pushbuttons. The first two paddles are -the X, Y axis of the first joystick. The second two correspond the the second -joystick. - Button 0 is the primary button of joystick 0. - Button 1 is the secondary button of joystick 0 but also the primary button of -joystick 1. - Button 2 is the secondary button of Joystick 1. -*/ - +// sdl3Joysticks turns the joystick, mouse and apple key events of SDL3 into +// the paddles and buttons the machine reads type sdl3Joysticks struct { - paddle [4]uint8 - hasPaddle [4]bool - button [4]bool - keys [3]bool - mousebuttons [3]bool - useMouse bool + paddles *shared.Paddles // SDL3 identifies joysticks with instance ids that are not indexes: // they start at 1 and grow as devices are plugged. The events carry // the instance id, so we keep the slot each one was assigned to. - slots map[sdl.JoystickID]uint8 + slots map[sdl.JoystickID]int } func newSDL3Joysticks(useMouseAlt bool) *sdl3Joysticks { var j sdl3Joysticks - j.slots = make(map[sdl.JoystickID]uint8) + j.slots = make(map[sdl.JoystickID]int) sdl.SetJoystickEventsEnabled(true) // Init up to two joysticks ids, err := sdl.GetJoysticks() if err == nil { - for slot, id := range ids { - if slot >= 2 { + for _, id := range ids { + if len(j.slots) >= 2 { break } _, err := id.OpenJoystick() if err != nil { continue } - j.slots[id] = uint8(slot) - j.hasPaddle[slot*2] = true - j.hasPaddle[slot*2+1] = true + j.slots[id] = len(j.slots) } } - // Initialize to max resistance if unplugged - j.paddle[0] = 255 - j.paddle[1] = 255 - j.paddle[2] = 255 - j.paddle[3] = 255 - - if useMouseAlt && !j.hasPaddle[0] { - // Use the mouse as joystick - j.useMouse = true - j.hasPaddle[0] = true - j.hasPaddle[1] = true - j.paddle[0] = 127 - j.paddle[1] = 127 - } - - // To enter Apple IIe on self test mode - // j.keys[1] = true - + j.paddles = shared.NewPaddles(len(j.slots), useMouseAlt) return &j } func (j *sdl3Joysticks) putAxisEvent(e *sdl.JoyAxisEvent) { - slot, ok := j.slots[e.Which] - if !ok || e.Axis >= 2 { - // Process only the first two axis of the first two joysticks - return - } - - j.paddle[slot*2+e.Axis] = uint8((e.Value >> 8) + 128) + j.paddles.SetAxis(j.slotOf(e.Which), int(e.Axis), e.Value) } func (j *sdl3Joysticks) putButtonEvent(e *sdl.JoyButtonEvent) { - slot, ok := j.slots[e.Which] - if !ok { - // Process only the buttons of the first two joysticks - return - } - - j.button[slot*2+(e.Button%2)] = e.Down -} - -func mouseToJoyCentered(x int32, w int32) uint8 { - r := max(min(x-(w/2)+127, 255), 0) - return uint8(r) - + j.paddles.SetButton(j.slotOf(e.Which), int(e.Button), e.Down) } func (j *sdl3Joysticks) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) { - if j.useMouse { - // The mouse moves on all the window - // j.paddle[0] = mouseToJoyFull(int32(e.X), width) - // j.paddle[1] = mouseToJoyFull(int32(e.Y), height) - - // The mouse moves around the center of the window - j.paddle[0] = mouseToJoyCentered(int32(e.X), width) - j.paddle[1] = mouseToJoyCentered(int32(e.Y), height) - } + j.paddles.SetMousePosition(int(e.X), int(e.Y), int(width), int(height)) } func (j *sdl3Joysticks) putMouseButtonEvent(e *sdl.MouseButtonEvent) { - if j.useMouse { - switch e.Button { - case 1: // BUTTON_LEFT - j.mousebuttons[0] = e.Down - case 3: // BUTTON_RIGHT - j.mousebuttons[1] = e.Down - case 2: // BUTTON_MIDDLE - j.mousebuttons[2] = e.Down - } - } + j.paddles.SetMouseButton(sdl3MouseButton(e.Button), e.Down) } func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) { @@ -131,7 +66,6 @@ func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) { Actually the Apple//e does this with the open and solid apple keys. Alt key - button 0 - Open apple AltGr key - button 1- Solid apple - //Win key - button 2 (Not in the Apple //e keyboard) The apple keys are a place on the keyboard, not a symbol, so we look at the scancode. Unlike SDL2, SDL3 resolves the keycode through the current @@ -140,36 +74,33 @@ func (j *sdl3Joysticks) putKey(keyEvent *sdl.KeyboardEvent) { */ switch keyEvent.Scancode { case sdl.SCANCODE_LALT: - j.keys[0] = keyEvent.Down + j.paddles.SetOpenApple(keyEvent.Down) case sdl.SCANCODE_RALT: - j.keys[1] = keyEvent.Down - // case sdl.SCANCODE_LGUI: - // j.keys[2] = keyEvent.Down + j.paddles.SetClosedApple(keyEvent.Down) } - } -// releaseKeys forgets the apple keys being pressed. The key up event of a key -// held while the window loses the focus goes to whoever took it, leaving the -// apple key pressed forever. -func (j *sdl3Joysticks) releaseKeys() { - j.keys = [3]bool{} +// slotOf returns the joystick an instance id was assigned to, -1 for the ones +// left out +func (j *sdl3Joysticks) slotOf(id sdl.JoystickID) int { + slot, ok := j.slots[id] + if !ok { + return -1 + } + + return slot } -func (j *sdl3Joysticks) ReadButton(i int) bool { - var value bool - switch i { - case 0: - value = j.button[0] || j.keys[0] || j.mousebuttons[0] - case 1: - // It can be secondary of first or primary of second - value = j.button[1] || j.button[2] || j.keys[1] || j.mousebuttons[1] - case 2: - value = j.button[3] || j.keys[2] || j.mousebuttons[2] +// sdl3MouseButton translates a button of the mouse of SDL3 +func sdl3MouseButton(button uint8) int { + switch sdl.MouseButtonFlags(button) { + case sdl.BUTTON_LEFT: + return shared.MouseButtonLeft + case sdl.BUTTON_RIGHT: + return shared.MouseButtonRight + case sdl.BUTTON_MIDDLE: + return shared.MouseButtonMiddle } - return value -} -func (j *sdl3Joysticks) ReadPaddle(i int) (uint8, bool) { - return j.paddle[i], j.hasPaddle[i] + return -1 } diff --git a/frontend/a2sdl3/sdl3Keyboard.go b/frontend/a2sdl3/sdl3Keyboard.go index 169942c3..7087a152 100644 --- a/frontend/a2sdl3/sdl3Keyboard.go +++ b/frontend/a2sdl3/sdl3Keyboard.go @@ -3,50 +3,28 @@ package main import ( - "fmt" - "time" - "github.com/ivanizag/izapple2" - "github.com/ivanizag/izapple2/screen" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/Zyko0/go-sdl3/sdl" ) +// sdl3Keyboard turns the key events of SDL3 into the keys the frontends share type sdl3Keyboard struct { - a *izapple2.Apple2 - keyChannel *izapple2.KeyboardChannel - - showHelp bool - showPages bool - showCharGen bool - showAltText bool - showDropTargets bool - screenMode int + keyboard *shared.Keyboard } -func newSDL3Keyboard(a *izapple2.Apple2) *sdl3Keyboard { +func newSDL3Keyboard(a *izapple2.Apple2, view *shared.View) *sdl3Keyboard { var k sdl3Keyboard - k.a = a - k.keyChannel = izapple2.NewKeyboardChannel(a) - - k.screenMode = screen.ScreenModeColorScanlines + k.keyboard = shared.NewKeyboard(a, view) return &k } func (k *sdl3Keyboard) putText(text string) { - k.keyChannel.PutText(text) + k.keyboard.PutText(text) } func (k *sdl3Keyboard) putKey(keyEvent *sdl.KeyboardEvent) { - /* - See "Apple II reference manual", page 5 - - To get keys as understood by the Apple2 hardware run: - 10 A=PEEK(49152) - 20 PRINT A, A - 128 - 30 GOTO 10 - */ - if !keyEvent.Down { // Process only key pushes return @@ -56,129 +34,69 @@ func (k *sdl3Keyboard) putKey(keyEvent *sdl.KeyboardEvent) { shift := keyEvent.Mod&sdl.KMOD_SHIFT != 0 command := keyEvent.Mod&sdl.KMOD_GUI != 0 - if ctrl { - if keyEvent.Key >= 'a' && keyEvent.Key <= 'z' { - k.keyChannel.PutChar(uint8(keyEvent.Key) - 97 + 1) - return - } + if ctrl && k.keyboard.PutCtrlLetter(rune(keyEvent.Key)) { + return } - result := uint8(0) + // Pasting reads the clipboard of SDL, it is not a shared command + if (keyEvent.Key == sdl.K_INSERT && shift) || (keyEvent.Key == 'v' && command) { + text, _ := sdl.GetClipboardText() + k.keyboard.Paste(text) + return + } + + k.keyboard.PutKey(sdl3Key(keyEvent.Key), ctrl, shift) +} - switch keyEvent.Key { +// sdl3Key translates a key of SDL3 +func sdl3Key(key sdl.Keycode) shared.Key { + switch key { case sdl.K_ESCAPE: - result = 27 + return shared.KeyEscape case sdl.K_BACKSPACE: - result = 8 - case sdl.K_RETURN: - result = 13 - case sdl.K_RETURN2: - result = 13 + return shared.KeyBackspace + case sdl.K_RETURN, sdl.K_RETURN2: + return shared.KeyReturn + case sdl.K_TAB: + return shared.KeyTab + case sdl.K_DELETE: + return shared.KeyDelete case sdl.K_LEFT: - if ctrl { - result = 31 // Base64A - } else { - result = 8 - } + return shared.KeyLeft case sdl.K_RIGHT: - result = 21 - - // Apple //e + return shared.KeyRight case sdl.K_UP: - result = 11 // 31 in the Base64A + return shared.KeyUp case sdl.K_DOWN: - result = 10 - case sdl.K_TAB: - result = 9 - case sdl.K_DELETE: - result = 127 // 24 in the Base64A - - // Base64A clone particularities - case sdl.K_F3: - result = 127 // Base64A + return shared.KeyDown - // Control of the emulator case sdl.K_F1: - k.showHelp = !k.showHelp + return shared.KeyF1 case sdl.K_F2: - // While the help is shown, F2 alone triggers a reset. This is handy - // when the window manager (for example KDE) captures Ctrl-F2. - if ctrl || k.showHelp { - k.a.SendCommand(izapple2.CommandReset) - k.showHelp = false - } + return shared.KeyF2 + case sdl.K_F3: + return shared.KeyF3 case sdl.K_F4: - k.a.SendCommand(izapple2.CommandToggleCPUTrace) + return shared.KeyF4 case sdl.K_F5: - if ctrl { - k.a.SendCommand(izapple2.CommandShowSpeed) - } else { - k.a.SendCommand(izapple2.CommandToggleSpeed) - } + return shared.KeyF5 case sdl.K_F6: - k.screenMode = screen.NextScreenMode(k.screenMode) + return shared.KeyF6 case sdl.K_F7: - k.showPages = !k.showPages + return shared.KeyF7 case sdl.K_F8: - k.showDropTargets = !k.showDropTargets - if k.showDropTargets { - // The help is shown on top of the drop targets, get it out of the way - k.showHelp = false - } + return shared.KeyF8 case sdl.K_F9: - k.a.SendCommand(izapple2.CommandDumpDebugInfo) + return shared.KeyF9 case sdl.K_F10: - if ctrl { - k.showCharGen = !k.showCharGen - } else if shift { - k.showAltText = !k.showAltText - } else { - k.a.SendCommand(izapple2.CommandNextCharGenPage) - } + return shared.KeyF10 case sdl.K_F12: - fallthrough - case sdl.K_PRINTSCREEN: - if ctrl { - screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/") - } else { - err := screen.SaveSnapshot(k.a.GetVideoSource(), screen.ScreenModeColorScanlines, "snapshot.png") - if err != nil { - fmt.Printf("Error saving snapshoot: %v.\n.", err) - } else { - fmt.Println("Saving snapshot 'snapshot.png'") - } - } + return shared.KeyF12 case sdl.K_PAUSE: - k.a.SendCommand(izapple2.CommandPauseUnpause) - case sdl.K_INSERT: - if shift { - text, _ := sdl.GetClipboardText() - go k.performPaste(text) - } - case 'v': - if command { - text, _ := sdl.GetClipboardText() - go k.performPaste(text) - } - } - - // Missing values 91 to 95. Usually control for [\]^_ - // On the Base64A it's control for \]./ - - if result != 0 { - k.keyChannel.PutChar(result) + return shared.KeyPause + case sdl.K_PRINTSCREEN: + return shared.KeyPrintScreen } -} -func (k *sdl3Keyboard) performPaste(text string) { - // Note 1: Pasting is too fast, so we slow it down. - // Note 2: Need to translate CR/LF's - for _, ch := range text { - if ch == '\r' || ch == '\n' { - k.keyChannel.PutChar(13) - } else { - k.keyChannel.PutRune(ch) - } - time.Sleep(20 * time.Millisecond) - } + return shared.KeyNone } diff --git a/frontend/a2sdl3/sdl3Mouse.go b/frontend/a2sdl3/sdl3Mouse.go deleted file mode 100644 index f216efcb..00000000 --- a/frontend/a2sdl3/sdl3Mouse.go +++ /dev/null @@ -1,35 +0,0 @@ -//go:build !js - -package main - -import ( - "github.com/Zyko0/go-sdl3/sdl" -) - -type sdl3Mouse struct { - x uint16 - y uint16 - pressed bool -} - -func newSDL3Mouse() *sdl3Mouse { - var m sdl3Mouse - return &m -} - -func (m *sdl3Mouse) putMouseMotionEvent(e *sdl.MouseMotionEvent, width int32, height int32) { - m.x = uint16(65536 * int32(e.X) / width) - m.y = uint16(65536 * int32(e.Y) / height) -} - -func (m *sdl3Mouse) putMouseButtonEvent(e *sdl.MouseButtonEvent) { - if e.Button == 1 { // BUTTTON_LEFT - m.pressed = e.Down - } -} - -func (m *sdl3Mouse) ReadMouse() (x uint16, y uint16, pressed bool) { - return m.x, m.y, m.pressed -} - -// TODO: SDL_WarpMouseInWindow diff --git a/frontend/a2wasm/go/wasmKeyboard.go b/frontend/a2wasm/go/wasmKeyboard.go index 17e76a5a..a1cd0746 100644 --- a/frontend/a2wasm/go/wasmKeyboard.go +++ b/frontend/a2wasm/go/wasmKeyboard.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/frontend/shared" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" @@ -53,77 +54,58 @@ func (k *wasmKeyboard) putChar(ch uint8) { } func (k *wasmKeyboard) putKey(key ebiten.Key) { - /* - See "Apple II reference manual", page 5 - - To get keys as understood by the Apple2 hardware run: - 10 A=PEEK(49152) - 20 PRINT A, A - 128 - 30 GOTO 10 - */ - ctrl := ebiten.IsKeyPressed(ebiten.KeyControl) - //#shift := ebiten.IsKeyPressed(ebiten.KeyShift) - if ctrl { - if key >= ebiten.KeyA && key <= ebiten.KeyZ { + if ctrl && key >= ebiten.KeyA && key <= ebiten.KeyZ { + if char, ok := shared.CharForCtrlLetter('a' + rune(key-ebiten.KeyA)); ok { fmt.Println("Control Key: ", key.String()) - k.keyChannel.PutChar(uint8(key-ebiten.KeyA) - 97 + 1) - return + k.keyChannel.PutChar(char) } + return } - result := uint8(0) + if char, ok := shared.CharForKey(wasmKey(key), ctrl); ok { + k.keyChannel.PutChar(char) + return + } + // Control of the emulator. The page has its own controls for the rest. switch key { - case ebiten.KeyEscape: - result = 27 - case ebiten.KeyBackspace: - result = 8 - case ebiten.KeyEnter: - result = 13 - case ebiten.KeyNumpadEnter: - result = 13 - case ebiten.KeyLeft: - if ctrl { - result = 31 // Base64A - } else { - result = 8 - } - case ebiten.KeyRight: - result = 21 - - // Apple //e - case ebiten.KeyUp: - result = 11 // 31 in the Base64A - case ebiten.KeyDown: - result = 10 - case ebiten.KeyTab: - result = 9 - case ebiten.KeyDelete: - result = 127 // 24 in the Base64A - - // Base64A clone particularities - case ebiten.KeyF3: - result = 127 // Base64A - - // Control of the emulator case ebiten.KeyF2: if ctrl { k.a.SendCommand(izapple2.CommandReset) } case ebiten.KeyF9: k.a.SendCommand(izapple2.CommandDumpDebugInfo) - case ebiten.KeyF12: - fallthrough - case ebiten.KeyPause: + case ebiten.KeyF12, ebiten.KeyPause: k.a.SendCommand(izapple2.CommandPauseUnpause) } +} - // Missing values 91 to 95. Usually control for [\]^_ - // On the Base64A it's control for \]./ - - if result != 0 { - k.keyChannel.PutChar(result) +// wasmKey translates a key of Ebitengine +func wasmKey(key ebiten.Key) shared.Key { + switch key { + case ebiten.KeyEscape: + return shared.KeyEscape + case ebiten.KeyBackspace: + return shared.KeyBackspace + case ebiten.KeyEnter, ebiten.KeyNumpadEnter: + return shared.KeyReturn + case ebiten.KeyTab: + return shared.KeyTab + case ebiten.KeyDelete: + return shared.KeyDelete + case ebiten.KeyLeft: + return shared.KeyLeft + case ebiten.KeyRight: + return shared.KeyRight + case ebiten.KeyUp: + return shared.KeyUp + case ebiten.KeyDown: + return shared.KeyDown + case ebiten.KeyF3: + return shared.KeyF3 } + + return shared.KeyNone } diff --git a/frontend/shared/dropTargets.go b/frontend/shared/dropTargets.go index 22ade123..d77394d9 100644 --- a/frontend/shared/dropTargets.go +++ b/frontend/shared/dropTargets.go @@ -51,17 +51,21 @@ type DropTargets struct { flashUntil time.Time // The areas are shown for a while after a drop flashDrive int + + dragging bool // A file is being dragged over the window + dragDrive int // The drive under it, -1 while it is not known } func NewDropTargets(a *izapple2.Apple2) *DropTargets { var d DropTargets d.a = a d.flashDrive = -1 + d.dragDrive = -1 return &d } -// Count returns how many drives a file can be dropped on -func (d *DropTargets) Count() int { +// count returns how many drives a file can be dropped on +func (d *DropTargets) count() int { return len(d.a.GetRemovableMediaDrives()) } @@ -69,7 +73,7 @@ func (d *DropTargets) Count() int { // a window of the given width. It returns -1 if there are no drives to drop a // file on. func (d *DropTargets) DriveAt(x int, width int) int { - return DropTargetIndex(x, width, d.Count()) + return DropTargetIndex(x, width, d.count()) } // Dropped notes the drive a file has just gone to, to show the areas for a @@ -79,19 +83,64 @@ func (d *DropTargets) Dropped(drive int) { d.flashUntil = time.Now().Add(DropTargetsFlashDuration) } -// Flashing returns whether the areas are being shown after a drop -func (d *DropTargets) Flashing() bool { - return time.Now().Before(d.flashUntil) +// DragStarted is called when a file begins to be dragged over the window, +// before knowing where it is +func (d *DropTargets) DragStarted() { + d.dragging = true + d.dragDrive = -1 +} + +// DragMoved tracks the drive a file dragged over the window is on. Pass -1 +// while it is not known. +func (d *DropTargets) DragMoved(drive int) { + d.dragging = true + d.dragDrive = drive +} + +// DragEnded is called when the file is dropped or leaves the window +func (d *DropTargets) DragEnded() { + d.dragging = false +} + +// Showing returns whether the areas take over the screen: because a file is +// being dragged over the window, because the user asked for them with F8, or +// because a file has just been dropped +func (d *DropTargets) Showing(requested bool) bool { + return d.dragging || requested || d.flashing() +} + +/* +SnapshotAt returns the screen with the areas of the drives, highlighting the +drive under the file being dragged, or the one that got the last file while the +flash lasts, or the one the pointer is on. + +Pass -1 as the drive pointed at where the pointer is outside the window or its +position is not known. +*/ +func (d *DropTargets) SnapshotAt(pointed int) *image.RGBA { + return d.snapshot(d.selected(pointed)) } -// FlashDrive returns the drive the last file dropped went to -func (d *DropTargets) FlashDrive() int { - return d.flashDrive +// selected returns the drive the areas highlight, -1 for none of them +func (d *DropTargets) selected(pointed int) int { + switch { + case d.dragging: + return d.dragDrive + case d.flashing(): + return d.flashDrive + } + + return pointed +} + +// flashing returns whether the areas are being shown after a drop +func (d *DropTargets) flashing() bool { + return time.Now().Before(d.flashUntil) } -// Snapshot returns the screen with the areas of the drives, with the one +// snapshot returns the screen with the areas of the drives, with the one // passed highlighted. Pass -1 to highlight none of them. -func (d *DropTargets) Snapshot(selected int) *image.RGBA { +func (d *DropTargets) snapshot(selected int) *image.RGBA { drives := d.a.GetRemovableMediaDrives() return screen.SnapshotMessageGenerator(d.a.GetVideoSource(), dropTargetsMessage(drives, selected), true /*is80Columns*/) diff --git a/frontend/shared/dropTargets_test.go b/frontend/shared/dropTargets_test.go index 158fc562..284caa5d 100644 --- a/frontend/shared/dropTargets_test.go +++ b/frontend/shared/dropTargets_test.go @@ -3,6 +3,7 @@ package shared import ( "strings" "testing" + "time" "github.com/ivanizag/izapple2" ) @@ -35,6 +36,44 @@ func TestDropTargetIndex(t *testing.T) { } } +// TestDropTargetsDrag follows a file dragged over the window and dropped, +// checking when the areas are shown and which drive they highlight +func TestDropTargetsDrag(t *testing.T) { + d := NewDropTargets(nil) + + steps := []struct { + name string + do func() + + showing bool // With the areas not asked for + selected int // With the pointer on drive 2 + }{ + {"at rest", func() {}, false, 2}, + {"a file enters the window", d.DragStarted, true, -1}, + {"the file moves over drive 1", func() { d.DragMoved(1) }, true, 1}, + {"the file is dropped on drive 1", func() { d.Dropped(1); d.DragEnded() }, true, 1}, + {"the flash is over", func() { d.flashUntil = time.Now() }, false, 2}, + } + + for _, step := range steps { + step.do() + + if actual := d.Showing(false); actual != step.showing { + t.Errorf("%v: Showing(false) is %v, expected %v", + step.name, actual, step.showing) + } + if actual := d.selected(2); actual != step.selected { + t.Errorf("%v: selected(2) is %v, expected %v", + step.name, actual, step.selected) + } + } + + // Nothing is going on by now, only the user asking shows the areas + if !d.Showing(true) { + t.Error("the areas asked for should be shown") + } +} + func TestSplitInLines(t *testing.T) { cases := []struct { text string diff --git a/frontend/shared/keyboard.go b/frontend/shared/keyboard.go new file mode 100644 index 00000000..0f163993 --- /dev/null +++ b/frontend/shared/keyboard.go @@ -0,0 +1,141 @@ +package shared + +import ( + "fmt" + "time" + + "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" +) + +/* +The keyboard of the frontends with a window: what the user types goes to the +machine, and the keys that command the emulator act on it and on the view. + +The frontends only translate the keys of their toolkit, they all answer to the +same ones. The help screen lists them, see HelpMessage. +*/ + +// Pasting is too fast for the Apple II, so it is slowed down to this per +// character +const pasteDelay = 20 * time.Millisecond + +// Keyboard sends what the user types to the machine and runs the commands the +// keys have on the emulator +type Keyboard struct { + a *izapple2.Apple2 + channel *izapple2.KeyboardChannel + view *View +} + +func NewKeyboard(a *izapple2.Apple2, view *View) *Keyboard { + var k Keyboard + k.a = a + k.channel = izapple2.NewKeyboardChannel(a) + k.view = view + return &k +} + +// PutText sends a text to the machine as the characters it is made of +func (k *Keyboard) PutText(text string) { + k.channel.PutText(text) +} + +// PutChar sends a character to the machine +func (k *Keyboard) PutChar(char uint8) { + k.channel.PutChar(char) +} + +// PutKey acts on a key pressed: it sends the character the Apple II keyboard +// would produce for it, or runs the command it has on the emulator +func (k *Keyboard) PutKey(key Key, ctrl bool, shift bool) { + if char, ok := CharForKey(key, ctrl); ok { + k.channel.PutChar(char) + return + } + + k.command(key, ctrl, shift) +} + +// PutCtrlLetter sends a letter pressed with the control key, and returns +// whether it was a letter at all +func (k *Keyboard) PutCtrlLetter(letter rune) bool { + char, ok := CharForCtrlLetter(letter) + if ok { + k.channel.PutChar(char) + } + return ok +} + +// Paste types a text on the machine. It returns at once, the text is typed on +// its own goroutine slowly enough for the Apple II to keep up. +func (k *Keyboard) Paste(text string) { + go func() { + for _, ch := range text { + if ch == '\r' || ch == '\n' { + // Translate the CR/LFs + k.channel.PutChar(13) + } else { + k.channel.PutRune(ch) + } + time.Sleep(pasteDelay) + } + }() +} + +// command runs what a key does to the emulator and to what is shown +func (k *Keyboard) command(key Key, ctrl bool, shift bool) { + switch key { + case KeyF1: + k.view.ToggleHelp() + case KeyF2: + // While the help is shown, F2 alone triggers a reset. This is handy + // when the window manager (for example KDE) captures Ctrl-F2. + if ctrl || k.view.ShowHelp { + k.a.SendCommand(izapple2.CommandReset) + k.view.ShowHelp = false + } + case KeyF4: + k.a.SendCommand(izapple2.CommandToggleCPUTrace) + case KeyF5: + if ctrl { + k.a.SendCommand(izapple2.CommandShowSpeed) + } else { + k.a.SendCommand(izapple2.CommandToggleSpeed) + } + case KeyF6: + k.view.NextScreenMode() + case KeyF7: + k.view.ShowPages = !k.view.ShowPages + case KeyF8: + k.view.ToggleDropTargets() + case KeyF9: + k.a.SendCommand(izapple2.CommandDumpDebugInfo) + case KeyF10: + if ctrl { + k.view.ShowCharGen = !k.view.ShowCharGen + } else if shift { + k.view.ShowAltText = !k.view.ShowAltText + } else { + k.a.SendCommand(izapple2.CommandNextCharGenPage) + } + case KeyF12, KeyPrintScreen: + if ctrl { + screen.AddScenario(k.a.GetVideoSource(), "../../screen/test_resources/") + } else { + k.saveSnapshot() + } + case KeyPause: + k.a.SendCommand(izapple2.CommandPauseUnpause) + } +} + +func (k *Keyboard) saveSnapshot() { + err := screen.SaveSnapshot(k.a.GetVideoSource(), + screen.ScreenModeColorScanlines, "snapshot.png") + if err != nil { + fmt.Printf("Error saving snapshoot: %v.\n.", err) + } else { + fmt.Println("Saving snapshot 'snapshot.png'") + } +} diff --git a/frontend/shared/keys.go b/frontend/shared/keys.go new file mode 100644 index 00000000..4c43f25c --- /dev/null +++ b/frontend/shared/keys.go @@ -0,0 +1,98 @@ +package shared + +/* +Every toolkit has its own constants for the keys of the keyboard. The frontends +translate theirs to the keys here, and this decides what the Apple II keyboard +would produce for them, so that all the frontends type the same. + +See "Apple II reference manual", page 5. + +To get the keys as understood by the Apple II hardware run: + + 10 A=PEEK(49152) + 20 PRINT A, A - 128 + 30 GOTO 10 +*/ + +// Key is a key of the keyboard, whatever the toolkit of the frontend calls it +type Key int + +const ( + KeyNone Key = iota // Not a key the frontends care about + + KeyEscape + KeyBackspace + KeyReturn + KeyTab + KeyDelete + KeyInsert + KeyLeft + KeyRight + KeyUp + KeyDown + + KeyF1 + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF12 + KeyPause + KeyPrintScreen +) + +// CharForKey returns the character the Apple II keyboard produces for a key, +// and whether it produces one at all +func CharForKey(key Key, ctrl bool) (uint8, bool) { + switch key { + case KeyEscape: + return 27, true + case KeyBackspace: + return 8, true + case KeyReturn: + return 13, true + case KeyLeft: + if ctrl { + return 31, true // Base64A + } + return 8, true + case KeyRight: + return 21, true + + // Apple //e + case KeyUp: + return 11, true // 31 in the Base64A + case KeyDown: + return 10, true + case KeyTab: + return 9, true + case KeyDelete: + return 127, true // 24 in the Base64A + + // Base64A clone particularities + case KeyF3: + return 127, true // Base64A + } + + // Missing values 91 to 95. Usually control for [\]^_ + // On the Base64A it's control for \]./ + return 0, false +} + +// CharForCtrlLetter returns the character a letter pressed with the control +// key produces, and whether it was a letter at all +func CharForCtrlLetter(letter rune) (uint8, bool) { + if letter >= 'A' && letter <= 'Z' { + letter += 'a' - 'A' + } + if letter < 'a' || letter > 'z' { + return 0, false + } + + return uint8(letter-'a') + 1, true +} diff --git a/frontend/shared/keys_test.go b/frontend/shared/keys_test.go new file mode 100644 index 00000000..b413b109 --- /dev/null +++ b/frontend/shared/keys_test.go @@ -0,0 +1,56 @@ +package shared + +import "testing" + +func TestCharForKey(t *testing.T) { + cases := []struct { + key Key + ctrl bool + expected uint8 + is bool + }{ + {KeyEscape, false, 27, true}, + {KeyBackspace, false, 8, true}, + {KeyReturn, false, 13, true}, + {KeyTab, false, 9, true}, + {KeyDelete, false, 127, true}, + {KeyLeft, false, 8, true}, + {KeyLeft, true, 31, true}, // Base64A + {KeyRight, false, 21, true}, + {KeyUp, false, 11, true}, + {KeyDown, false, 10, true}, + {KeyF3, false, 127, true}, // Base64A + {KeyF1, false, 0, false}, // A command of the emulator, not a character + {KeyF12, true, 0, false}, + {KeyNone, false, 0, false}, + } + + for _, c := range cases { + actual, is := CharForKey(c.key, c.ctrl) + if actual != c.expected || is != c.is { + t.Errorf("CharForKey(%v, %v) is (%v, %v), expected (%v, %v)", + c.key, c.ctrl, actual, is, c.expected, c.is) + } + } +} + +func TestCharForCtrlLetter(t *testing.T) { + letters := map[rune]uint8{ + 'a': 1, 'm': 13, 'z': 26, + 'A': 1, 'M': 13, 'Z': 26, // The upper case letters are the same keys + } + + for letter, expected := range letters { + actual, is := CharForCtrlLetter(letter) + if actual != expected || !is { + t.Errorf("CharForCtrlLetter(%q) is (%v, %v), expected (%v, true)", + letter, actual, is, expected) + } + } + + for _, letter := range []rune{'0', ' ', '[', '@', 'ñ'} { + if _, is := CharForCtrlLetter(letter); is { + t.Errorf("CharForCtrlLetter(%q) should not be a letter", letter) + } + } +} diff --git a/frontend/shared/mixer.go b/frontend/shared/mixer.go new file mode 100644 index 00000000..744ad7be --- /dev/null +++ b/frontend/shared/mixer.go @@ -0,0 +1,17 @@ +package shared + +import ( + "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/audio" +) + +// 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 NewFrontMixer(a *izapple2.Apple2) *audio.Mixer { + mixer := audio.NewMixer(a.GetClockMhz()) + for _, source := range a.GetAudioSources() { + source.SetAudioSink(mixer.NewSource()) + } + + return mixer +} diff --git a/frontend/shared/mouse.go b/frontend/shared/mouse.go new file mode 100644 index 00000000..132f90ab --- /dev/null +++ b/frontend/shared/mouse.go @@ -0,0 +1,45 @@ +package shared + +// Mouse is the position and the button of the pointer, as the mouse card of +// the machine reads them +type Mouse struct { + x uint16 + y uint16 + pressed bool +} + +func NewMouse() *Mouse { + var m Mouse + return &m +} + +// SetPosition places the pointer, from where it is on a window of the given +// size +func (m *Mouse) SetPosition(x int, y int, width int, height int) { + if width <= 0 || height <= 0 { + // The window is not sized yet + return + } + + m.x = mousePosition(x, width) + m.y = mousePosition(y, height) +} + +// SetButton presses or releases the button of the mouse +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 +} diff --git a/frontend/shared/mouse_test.go b/frontend/shared/mouse_test.go new file mode 100644 index 00000000..07b8d203 --- /dev/null +++ b/frontend/shared/mouse_test.go @@ -0,0 +1,31 @@ +package shared + +import "testing" + +func TestMouse(t *testing.T) { + m := NewMouse() + + m.SetPosition(320, 120, 640, 480) + x, y, pressed := m.ReadMouse() + if x != 32768 || y != 16384 || pressed { + t.Errorf("the mouse is at (%v, %v, %v), expected (32768, 16384, false)", + x, y, pressed) + } + + m.SetButton(true) + if _, _, pressed := m.ReadMouse(); !pressed { + t.Error("the button of the mouse should be pressed") + } + + // A window not sized yet leaves the pointer where it was + m.SetPosition(10, 10, 0, 0) + 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 new file mode 100644 index 00000000..95d5d3c4 --- /dev/null +++ b/frontend/shared/paddles.go @@ -0,0 +1,160 @@ +package shared + +/* +The Apple II supports four paddles and 3 pushbuttons. The first two paddles are +the X, Y axis of the first joystick. The second two correspond to the second +joystick. + + Button 0 is the primary button of joystick 0. + Button 1 is the secondary button of joystick 0 but also the primary button + of joystick 1. + Button 2 is the secondary button of joystick 1. + +The Apple //e reads the open and closed apple keys as the first two buttons. +Where there is no joystick, the mouse can move the first two paddles. +*/ + +// The buttons of the mouse, when it is what moves the paddles +const ( + MouseButtonLeft = iota + MouseButtonRight + MouseButtonMiddle +) + +const ( + paddleUnplugged = 255 // Max resistance when unplugged + paddleCentered = 127 +) + +// Paddles builds the paddles and buttons the machine reads from the events of +// a frontend +type Paddles struct { + paddle [4]uint8 + hasPaddle [4]bool + button [4]bool + appleKeys [2]bool // The open and the closed apple keys + mouseButtons [3]bool + useMouse bool +} + +/* +NewPaddles creates the paddles of a machine with the given number of joysticks, +of which only the first two are used. + +Without joysticks, useMouseAlt moves the first two paddles with the mouse. +*/ +func NewPaddles(joysticks int, useMouseAlt bool) *Paddles { + var p Paddles + + for i := range p.paddle { + p.paddle[i] = paddleUnplugged + } + for slot := 0; slot < joysticks && slot < 2; slot++ { + p.hasPaddle[slot*2] = true + p.hasPaddle[slot*2+1] = true + } + + if useMouseAlt && !p.hasPaddle[0] { + // Use the mouse as joystick + p.useMouse = true + p.hasPaddle[0] = true + p.hasPaddle[1] = true + p.paddle[0] = paddleCentered + p.paddle[1] = paddleCentered + } + + // To enter Apple IIe on self test mode + // p.appleKeys[1] = true + + return &p +} + +// SetAxis moves a paddle of a joystick, with the value on the full range of +// the axis +func (p *Paddles) SetAxis(joystick int, axis int, value int16) { + if joystick < 0 || joystick >= 2 || axis < 0 || axis >= 2 { + // Only the first two axis of the first two joysticks + return + } + + p.paddle[joystick*2+axis] = uint8((value >> 8) + 128) +} + +// SetButton presses or releases a button of a joystick. The buttons beyond the +// second are read as the second one. +func (p *Paddles) SetButton(joystick int, button int, pressed bool) { + if joystick < 0 || joystick >= 2 || button < 0 { + // Only the buttons of the first two joysticks + return + } + + p.button[joystick*2+(button%2)] = pressed +} + +// SetOpenApple presses or releases the open apple key, the first button +func (p *Paddles) SetOpenApple(pressed bool) { + p.appleKeys[0] = pressed +} + +// SetClosedApple presses or releases the closed apple key, the second button +func (p *Paddles) SetClosedApple(pressed bool) { + p.appleKeys[1] = pressed +} + +// ReleaseAppleKeys forgets the apple keys being pressed. The key up event of a +// key held while the window loses the focus goes to whoever took it, leaving +// the apple key pressed forever. +func (p *Paddles) ReleaseAppleKeys() { + p.appleKeys = [2]bool{} +} + +// SetMousePosition moves the paddles with the pointer on a window of the given +// size, where the mouse is what the machine has instead of a joystick. The +// paddles are centered on the center of the window. +func (p *Paddles) SetMousePosition(x int, y int, width int, height int) { + if !p.useMouse { + return + } + + p.paddle[0] = mouseToPaddle(x, width) + p.paddle[1] = mouseToPaddle(y, height) +} + +// SetMouseButton presses or releases a button of the mouse, where the mouse is +// what the machine has instead of a joystick +func (p *Paddles) SetMouseButton(button int, pressed bool) { + if !p.useMouse || button < 0 || button >= len(p.mouseButtons) { + return + } + + 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 { + case 0: + return p.button[0] || p.appleKeys[0] || p.mouseButtons[MouseButtonLeft] + case 1: + // It can be secondary of first or primary of second + return p.button[1] || p.button[2] || p.appleKeys[1] || + p.mouseButtons[MouseButtonRight] + case 2: + return p.button[3] || p.mouseButtons[MouseButtonMiddle] + } + + return false +} + +// ReadPaddle returns the resistance of a paddle and whether it is plugged +func (p *Paddles) ReadPaddle(i int) (uint8, bool) { + return p.paddle[i], p.hasPaddle[i] +} diff --git a/frontend/shared/paddles_test.go b/frontend/shared/paddles_test.go new file mode 100644 index 00000000..8e13994f --- /dev/null +++ b/frontend/shared/paddles_test.go @@ -0,0 +1,147 @@ +package shared + +import "testing" + +func TestPaddlesUnplugged(t *testing.T) { + p := NewPaddles(0, false) + + for i := range 4 { + value, plugged := p.ReadPaddle(i) + if plugged { + t.Errorf("paddle %v should not be plugged", i) + } + if value != paddleUnplugged { + t.Errorf("paddle %v is %v, expected %v", i, value, paddleUnplugged) + } + } + + for i := range 3 { + if p.ReadButton(i) { + t.Errorf("button %v should not be pressed", i) + } + } +} + +func TestPaddlesAxis(t *testing.T) { + p := NewPaddles(2, false) + + cases := []struct { + joystick int + axis int + value int16 + paddle int + expected uint8 + }{ + {0, 0, -32768, 0, 0}, // Full left + {0, 1, 0, 1, 128}, // Centered + {1, 0, 32767, 2, 255}, // Full right + {1, 1, -32768, 3, 0}, + } + + for _, c := range cases { + p.SetAxis(c.joystick, c.axis, c.value) + actual, plugged := p.ReadPaddle(c.paddle) + if actual != c.expected || !plugged { + t.Errorf("SetAxis(%v, %v, %v) leaves paddle %v at (%v, %v), expected (%v, true)", + c.joystick, c.axis, c.value, c.paddle, actual, plugged, c.expected) + } + } + + // The joysticks and axis beyond the ones the Apple II has are ignored + p.SetAxis(2, 0, 32767) + p.SetAxis(0, 2, 32767) + p.SetAxis(-1, 0, 32767) +} + +func TestPaddlesButtons(t *testing.T) { + cases := []struct { + joystick int + button int + expected int // The button of the machine it is read as + }{ + {0, 0, 0}, // Primary of the first joystick + {0, 1, 1}, // Secondary of the first, also primary of the second + {1, 0, 1}, + {1, 1, 2}, // Secondary of the second + {0, 2, 0}, // The buttons beyond the second repeat + } + + for _, c := range cases { + p := NewPaddles(2, false) + p.SetButton(c.joystick, c.button, true) + + for i := range 3 { + expected := i == c.expected + if p.ReadButton(i) != expected { + t.Errorf("button %v of joystick %v: button %v of the machine is %v, expected %v", + c.button, c.joystick, i, p.ReadButton(i), expected) + } + } + } +} + +func TestPaddlesAppleKeys(t *testing.T) { + p := NewPaddles(1, false) + + p.SetOpenApple(true) + p.SetClosedApple(true) + if !p.ReadButton(0) || !p.ReadButton(1) { + t.Error("the apple keys should be read as the first two buttons") + } + + p.ReleaseAppleKeys() + if p.ReadButton(0) || p.ReadButton(1) { + t.Error("the apple keys should be released") + } +} + +func TestPaddlesMouse(t *testing.T) { + p := NewPaddles(0, true) + + // The mouse takes the place of the first joystick, centered + value, plugged := p.ReadPaddle(0) + if !plugged || value != paddleCentered { + t.Errorf("the mouse paddle is (%v, %v), expected (%v, true)", + value, plugged, paddleCentered) + } + + cases := []struct { + x, y int + expected [2]uint8 + }{ + {200, 100, [2]uint8{127, 127}}, // The center of the window + {0, 0, [2]uint8{0, 27}}, + {400, 200, [2]uint8{255, 227}}, + } + + for _, c := range cases { + p.SetMousePosition(c.x, c.y, 400, 200) + x, _ := p.ReadPaddle(0) + y, _ := p.ReadPaddle(1) + if x != c.expected[0] || y != c.expected[1] { + t.Errorf("the pointer at %v,%v leaves the paddles at %v,%v, expected %v", + c.x, c.y, x, y, c.expected) + } + } + + p.SetMouseButton(MouseButtonRight, true) + if !p.ReadButton(1) { + t.Error("the right button should be read as the second button") + } +} + +func TestPaddlesMouseWithJoystick(t *testing.T) { + // With a joystick plugged the mouse is left for the machine to use + p := NewPaddles(1, true) + + p.SetAxis(0, 0, 0) + p.SetMousePosition(0, 0, 400, 200) + p.SetMouseButton(MouseButtonLeft, true) + + if value, _ := p.ReadPaddle(0); value != 128 { + t.Errorf("the paddle is %v, the mouse should not have moved it", value) + } + if p.ReadButton(0) { + t.Error("the mouse should not press the buttons") + } +} diff --git a/frontend/shared/view.go b/frontend/shared/view.go new file mode 100644 index 00000000..3c811d07 --- /dev/null +++ b/frontend/shared/view.go @@ -0,0 +1,113 @@ +package shared + +import ( + "fmt" + "image" + + "github.com/ivanizag/izapple2" + "github.com/ivanizag/izapple2/screen" +) + +/* +The frontends with a window show the screen of the machine, and on demand one +of the screens that take over it: the help, the areas of the drives a file can +be dropped on, the character generator or the pages of video memory. What is +shown and what the window is titled is the same on all of them, only the way +the image reaches the window changes. +*/ + +// View is the screen a frontend shows and the choices the user made about it +type View struct { + ShowHelp bool + ShowPages bool + ShowCharGen bool + ShowAltText bool + ShowDropTargets bool + ScreenMode int +} + +// NewView creates the view a frontend starts with +func NewView() *View { + var v View + v.ScreenMode = screen.ScreenModeColorScanlines + return &v +} + +// ToggleHelp shows or hides the help +func (v *View) ToggleHelp() { + v.ShowHelp = !v.ShowHelp +} + +// ToggleDropTargets shows or hides the areas of the drives a file can be +// dropped on +func (v *View) ToggleDropTargets() { + v.ShowDropTargets = !v.ShowDropTargets + if v.ShowDropTargets { + // The help is shown on top of the drop targets, get it out of the way + v.ShowHelp = false + } +} + +// NextScreenMode moves to the next way of rendering the screen +func (v *View) NextScreenMode() { + v.ScreenMode = screen.NextScreenMode(v.ScreenMode) +} + +/* +Snapshot returns the image to put on the window and the title it should have, +empty when the title does not change. + +The drive the pointer is on highlights the areas of the drop targets while they +are shown. Pass -1 where the pointer is outside the window or its position is +not known. +*/ +func (v *View) Snapshot(a *izapple2.Apple2, d *DropTargets, pointed int) (*image.RGBA, string) { + vs := a.GetVideoSource() + + switch { + case v.ShowHelp: + return screen.SnapshotMessageGenerator(vs, HelpMessage, false /*is80Columns*/), "" + + case d.Showing(v.ShowDropTargets): + return d.SnapshotAt(pointed), "" + + case v.ShowCharGen: + cgPage, cgPages := a.GetCgPageInfo() + return screen.SnapshotCharacterGenerator(vs, v.ShowAltText), + fmt.Sprintf("%v character map, page %v/%v", a.Name, cgPage+1, cgPages) + + case v.ShowPages: + img := screen.SnapshotParts(vs, v.ScreenMode) + return img, fmt.Sprintf("%v %v %vx%v", a.Name, screen.VideoModeName(vs), + img.Rect.Dx()/2, img.Rect.Dy()/2) + } + + return screen.Snapshot(vs, v.ScreenMode), "" +} + +// HelpMessage is the text shown with F1, the keys the frontends with a window +// have in common +const HelpMessage = ` + F1: Show/Hide help + Ctrl-F2: Reset + F1, F2: Reset + F4: Show/Hide CPU trace + F5: Fast/Normal speed + Ctrl-F5: Show speed + F6: Next screen mode + F7: Show/Hide pages + F8: Show/Hide drop targets + F10: Next character set + Ctrl-F10: Show/Hide character set + Shift-F10: Show/Hide alternate text + F12: Save screen snapshot + Pause: Pause the emulation + + Left alt or option key: Open-Apple + Right alt or option key: Closed-Apple + +Drop a file on a drive area to load it + + Run izapple2 -h for more options + https://github.com/ivanizag/izapple2 +` diff --git a/frontend/shared/view_test.go b/frontend/shared/view_test.go new file mode 100644 index 00000000..a5d07e76 --- /dev/null +++ b/frontend/shared/view_test.go @@ -0,0 +1,21 @@ +package shared + +import "testing" + +func TestViewToggleDropTargets(t *testing.T) { + v := NewView() + v.ToggleHelp() + + v.ToggleDropTargets() + if !v.ShowDropTargets { + t.Error("the drop targets should be shown") + } + if v.ShowHelp { + t.Error("the help should be out of the way of the drop targets") + } + + v.ToggleDropTargets() + if v.ShowDropTargets { + t.Error("the drop targets should be hidden") + } +}