Share the frontend code that does not depend on the toolkit

The frontends were copies of each other. The help was the same text three
times, the chain that picks the screen to show was the same branches three
times, and the SDL2 and SDL3 keyboards, joysticks and mice were the same files
twice with the constants of each toolkit.

frontend/shared holds them now: the view with what is on screen and the help,
the keyboard with the codes of the Apple II keyboard and the commands of the
emulator, the paddles with the four axis and the three buttons, the mouse, and
the mixer with every sound source of the machine already attached. The
frontends keep what belongs to their toolkit: the translation of their keys to
shared.Key, the clipboard, the instance ids of the SDL3 joysticks and the file
system Ebitengine hands the dropped files in. The drop targets take the drag
over the window too, so a frontend only tells which drive the pointer is on.

One implementation aligns what the frontends did differently, following a2sdl
where they disagreed:

- Ctrl with a letter was broken on a2ebiten and a2wasm. The key was already
  zero based and 97 was subtracted from it again, so Ctrl-A sent 160 and not 1.
- The libretro core gets Ctrl-Left, the Delete of the Base64A.
- a2sdl3 marks the drive under the pointer when the areas are shown with F8.
- a2sdl finds the apple keys by scancode, so they work on the layouts where the
  alt keys produce another symbol, and releases them when the window loses the
  focus. Both come from a2sdl3.

a2ebiten keeps Ctrl-F5 for the readout of the speed it draws on the corner of
the screen, the only key that does something else anywhere.

The mouse leaves the pointer where it was on a window without size instead of
dividing by zero, which SDL3 can report when asking for the size fails.

What needs no machine is tested: the codes of the keyboard, the paddles, the
mouse and the drive the drop targets mark.
This commit is contained in:
Iván Izaguirre authored and Iván Izaguirre committed 2026-08-12 18:05:28 +02:00
1 parent 66b5cac559
commit 88e59ae78d
35 files changed
+1290 -978

No files matched your search

+1 -1
View File
@@ -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
+5
View File
@@ -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.
+2 -7
View File
@@ -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
+4 -2
View File
@@ -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.NewMixer(a),
}
}
-17
View File
@@ -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 {
+51 -106
View File
@@ -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
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
return shared.KeyBackspace
case ebiten.KeyEnter, ebiten.KeyNumpadEnter:
return shared.KeyReturn
case ebiten.KeyTab:
result = 9
return shared.KeyTab
case ebiten.KeyDelete:
result = 127 // 24 in the Base64A
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
// 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
}
+10 -47
View File
@@ -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,6 +17,7 @@ import (
type Game struct {
a *izapple2.Apple2
image *ebiten.Image
view *shared.View
keyboard *ebitenKeyboard
speaker *ebitenAudio
dropTargets *ebitenDropTargets
@@ -53,21 +53,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)
@@ -133,15 +122,14 @@ 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),
dropTargets: newEbitenDropTargets(a),
}
for _, source := range a.GetAudioSources() {
source.SetAudioSink(game.speaker.mixer.NewSource())
}
var err error
game.fontSource, err = text.NewGoTextFaceSource(bytes.NewReader(fonts.MPlus1pRegular_ttf))
@@ -155,31 +143,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 .
+5 -3
View File
@@ -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.NewMixer(a),
samplesPerFrame: samplesPerFrame,
mono: make([]float32, size),
stereo: make([]C.int16_t, 2*size),
+1 -4
View File
@@ -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())
+28 -20
View File
@@ -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
return shared.KeyDelete
}
if modifiers&C.RETROKMOD_CTRL != 0 &&
keycode >= C.RETROK_a && keycode <= C.RETROK_z {
return uint8(keycode-C.RETROK_a) + 1, true
}
return 0, false
return shared.KeyNone
}
+17 -51
View File
@@ -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
`
///////////////////////////////////////
+4 -2
View File
@@ -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.NewMixer(a),
}
}
-18
View File
@@ -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 {
+36 -105
View File
@@ -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
joyCount := min(sdl.NumJoysticks(), 2)
for iJoy := range joyCount {
sdl.JoystickOpen(iJoy)
}
// 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
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)
}
}
// 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
}
}
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]
}
return value
}
func (j *sdlJoysticks) ReadPaddle(i int) (uint8, bool) {
return j.paddle[i], j.hasPaddle[i]
return -1
}
+50 -132
View File
@@ -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
}
switch key.Sym {
k.keyboard.PutKey(sdlKey(key.Sym), ctrl, shift)
}
// 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
case sdl.K_LEFT:
if ctrl {
result = 31 // Base64A
} else {
result = 8
}
case sdl.K_RIGHT:
result = 21
// Apple //e
case sdl.K_UP:
result = 11 // 31 in the Base64A
case sdl.K_DOWN:
result = 10
return shared.KeyBackspace
case sdl.K_RETURN, sdl.K_RETURN2:
return shared.KeyReturn
case sdl.K_TAB:
result = 9
return shared.KeyTab
case sdl.K_DELETE:
result = 127 // 24 in the Base64A
return shared.KeyDelete
case sdl.K_LEFT:
return shared.KeyLeft
case sdl.K_RIGHT:
return shared.KeyRight
case sdl.K_UP:
return shared.KeyUp
case sdl.K_DOWN:
return shared.KeyDown
// Base64A clone particularities
case sdl.K_F3:
result = 127 // Base64A
// 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)
}
return shared.KeyPause
case sdl.K_PRINTSCREEN:
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)
}
}
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
}
-35
View File
@@ -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
+14 -52
View File
@@ -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
`
///////////////////////////////////////
+4 -2
View File
@@ -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.NewMixer(a),
buf: make([]float32, bufferSamples),
}
}
+19 -34
View File
@@ -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,28 +59,6 @@ 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()
}
return d.targets.Snapshot(selected)
}
func (d *sdl3DropTargets) driveAt(x float32) int {
width, _, err := d.window.Size()
if err != nil {
@@ -98,3 +67,19 @@ func (d *sdl3DropTargets) driveAt(x float32) int {
return d.targets.DriveAt(int(x), int(width))
}
// 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 || mouseX < 0 || mouseX >= float32(width) {
return -1
}
return d.targets.DriveAt(int(mouseX), int(width))
}
+37 -106
View File
@@ -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)
}
}
// 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
}
// 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{}
}
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
}
+50 -132
View File
@@ -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
}
switch keyEvent.Key {
k.keyboard.PutKey(sdl3Key(keyEvent.Key), ctrl, shift)
}
// 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
case sdl.K_LEFT:
if ctrl {
result = 31 // Base64A
} else {
result = 8
}
case sdl.K_RIGHT:
result = 21
// Apple //e
case sdl.K_UP:
result = 11 // 31 in the Base64A
case sdl.K_DOWN:
result = 10
return shared.KeyBackspace
case sdl.K_RETURN, sdl.K_RETURN2:
return shared.KeyReturn
case sdl.K_TAB:
result = 9
return shared.KeyTab
case sdl.K_DELETE:
result = 127 // 24 in the Base64A
return shared.KeyDelete
case sdl.K_LEFT:
return shared.KeyLeft
case sdl.K_RIGHT:
return shared.KeyRight
case sdl.K_UP:
return shared.KeyUp
case sdl.K_DOWN:
return shared.KeyDown
// Base64A clone particularities
case sdl.K_F3:
result = 127 // Base64A
// 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)
}
return shared.KeyPause
case sdl.K_PRINTSCREEN:
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)
}
}
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
}
-35
View File
@@ -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
+37 -55
View File
@@ -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 \]./
// 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
}
if result != 0 {
k.keyChannel.PutChar(result)
}
return shared.KeyNone
}
+61 -12
View File
@@ -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 {
// 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))
}
// 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)
}
// FlashDrive returns the drive the last file dropped went to
func (d *DropTargets) FlashDrive() int {
return d.flashDrive
}
// 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*/)
+39
View File
@@ -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
+141
View File
@@ -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'")
}
}
+98
View File
@@ -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
}
+56
View File
@@ -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)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
package shared
import (
"github.com/ivanizag/izapple2"
"github.com/ivanizag/izapple2/audio"
)
// NewMixer 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 {
mixer := audio.NewMixer(a.GetClockMhz())
for _, source := range a.GetAudioSources() {
source.SetAudioSink(mixer.NewSource())
}
return mixer
}
+39
View File
@@ -0,0 +1,39 @@
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 = uint16(65536 * x / width)
m.y = uint16(65536 * y / height)
}
// SetButton presses or releases the button of the mouse
func (m *Mouse) SetButton(pressed bool) {
m.pressed = pressed
}
// 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
+25
View File
@@ -0,0 +1,25 @@
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)
}
}
+158
View File
@@ -0,0 +1,158 @@
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
}
// 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]
}
// 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))
}
+147
View File
@@ -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")
}
}
+113
View File
@@ -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
`
+21
View File
@@ -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")
}
}