Skip to content

Commit 7bdb1d1

Browse files
committed
Implement minimal piscope dev tools package
* show GUI toolbar when user presses CTRL+SHIFT+I * print color and coords in screen inspector * pause/resume * go to next/prev frame * take snapshot
1 parent 24cf1d3 commit 7bdb1d1

File tree

8 files changed

+349
-114
lines changed

8 files changed

+349
-114
lines changed

piscope/internal/commands.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package internal
5+
6+
import (
7+
"github.com/elgopher/pi"
8+
"github.com/elgopher/pi/pidebug"
9+
"github.com/elgopher/pi/pisnap"
10+
"log"
11+
)
12+
13+
func enterConsoleMode() {
14+
log.Println("Entering console")
15+
prev := pi.SetColor(*bgColor)
16+
pi.Rect(0, 0, pi.Screen().W()-1, pi.Screen().H()-1)
17+
pi.SetColor(prev)
18+
consoleMode = true
19+
}
20+
21+
func exitConsoleMode() {
22+
log.Println("Exiting console")
23+
theScreenRecorder.ShowPrev()
24+
theScreenRecorder.Reset()
25+
consoleMode = false
26+
pidebug.SetPaused(false)
27+
}
28+
29+
func captureSnapshot() {
30+
f, err := pisnap.CaptureOrErr()
31+
if err != nil {
32+
log.Println("Error capturing screenshot:", err)
33+
} else {
34+
log.Println("Screenshot saved to", f)
35+
}
36+
}
37+
38+
func showPrevSnapshot() {
39+
pidebug.SetPaused(true)
40+
theScreenRecorder.ShowPrev()
41+
}
42+
43+
func showNextSnapshot() {
44+
if !theScreenRecorder.ShowNext() {
45+
pidebug.SetPaused(false)
46+
pauseOnNextFrame = true
47+
}
48+
}
49+
50+
func pauseOrResume() {
51+
if consoleMode {
52+
theScreenRecorder.GoToLast()
53+
54+
pidebug.SetPaused(!pidebug.Paused())
55+
}
56+
}

piscope/internal/gui.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package internal
5+
6+
import (
7+
"github.com/elgopher/pi"
8+
"github.com/elgopher/pi/pidebug"
9+
"github.com/elgopher/pi/pigui"
10+
)
11+
12+
func attachToolbar(parent *pigui.Element) *pigui.Element {
13+
toolbar := pigui.Attach(parent, 0, pi.Screen().H()-9, pi.Screen().W(), 9)
14+
toolbar.OnDraw = func(event pigui.DrawEvent) {
15+
prev := pi.SetColor(*bgColor)
16+
defer pi.SetColor(prev)
17+
pi.RectFill(0, 0, toolbar.W, toolbar.H)
18+
}
19+
20+
// attachIconButton(toolbar, icons.AlignTop, 0) // icon hidden until implemented
21+
// attachIconButton(toolbar, icons.Screen, 8) // icon hidden for now because screen inspector is the only tab
22+
// attachIconButton(toolbar, icons.Palette, 16) // icon hidden until implemented
23+
// attachIconButton(toolbar, icons.Variables, 24) // icon hidden until implemented
24+
// attachIconButton(toolbar, icons.Paint, 32) // icon hidden until implemented
25+
26+
snap := attachIconButton(toolbar, icons.Snap, pi.Screen().W()-34)
27+
snap.OnTapped = func(event pigui.Event) {
28+
captureSnapshot()
29+
}
30+
31+
prev := attachIconButton(toolbar, icons.Prev, pi.Screen().W()-24)
32+
prev.OnTapped = func(event pigui.Event) {
33+
showPrevSnapshot()
34+
}
35+
prev.OnUpdate = func(pigui.UpdateEvent) {
36+
if theScreenRecorder.HasPrev() {
37+
prev.Icon = icons.Prev
38+
} else {
39+
prev.Icon = pi.Sprite{}
40+
}
41+
}
42+
43+
playPause := attachIconButton(toolbar, icons.Pause, pi.Screen().W()-19)
44+
playPause.OnTapped = func(pigui.Event) {
45+
pauseOrResume()
46+
}
47+
playPause.OnUpdate = func(pigui.UpdateEvent) {
48+
if pidebug.Paused() {
49+
playPause.Icon = icons.Pause
50+
} else {
51+
playPause.Icon = icons.Play
52+
}
53+
}
54+
55+
next := attachIconButton(toolbar, icons.Next, pi.Screen().W()-14)
56+
next.OnTapped = func(event pigui.Event) {
57+
showNextSnapshot()
58+
}
59+
60+
exit := attachIconButton(toolbar, icons.Exit, pi.Screen().W()-8)
61+
exit.OnTapped = func(event pigui.Event) {
62+
exitConsoleMode()
63+
}
64+
65+
return toolbar
66+
}
67+
68+
type IconButton struct {
69+
*pigui.Element
70+
71+
Icon pi.Sprite
72+
}
73+
74+
func attachIconButton(parent *pigui.Element, icon pi.Sprite, x int) *IconButton {
75+
btn := pigui.Attach(parent, x, 0, icon.W, icon.H+1)
76+
iconBtn := &IconButton{Icon: icon, Element: btn}
77+
btn.OnDraw = func(event pigui.DrawEvent) {
78+
y := 0
79+
if event.Pressed {
80+
y = 1
81+
}
82+
pi.Spr(iconBtn.Icon, 0, y)
83+
}
84+
return iconBtn
85+
}

piscope/internal/icons.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package internal
5+
6+
import (
7+
_ "embed"
8+
"github.com/elgopher/pi"
9+
)
10+
11+
//go:embed "icons.png"
12+
var iconsPNG []byte
13+
14+
var icons = struct {
15+
AlignTop pi.Sprite
16+
AlignBottom pi.Sprite
17+
Screen pi.Sprite
18+
Palette pi.Sprite
19+
ColorTables pi.Sprite
20+
Variables pi.Sprite
21+
Paint pi.Sprite
22+
Separator pi.Sprite
23+
Snap pi.Sprite
24+
Prev pi.Sprite
25+
Pause pi.Sprite
26+
Play pi.Sprite
27+
Next pi.Sprite
28+
Exit pi.Sprite
29+
}{}
30+
31+
func init() {
32+
// TODO Decode palette
33+
iconsSheet := pi.DecodeCanvas(iconsPNG)
34+
icons.AlignTop = pi.SpriteFrom(iconsSheet, 0, 0, 8, 8)
35+
icons.AlignBottom = pi.SpriteFrom(iconsSheet, 8, 0, 8, 8)
36+
icons.Screen = pi.SpriteFrom(iconsSheet, 16, 0, 8, 8)
37+
icons.Palette = pi.SpriteFrom(iconsSheet, 24, 0, 8, 8)
38+
icons.ColorTables = pi.SpriteFrom(iconsSheet, 32, 0, 8, 8)
39+
icons.Variables = pi.SpriteFrom(iconsSheet, 40, 0, 8, 8)
40+
icons.Paint = pi.SpriteFrom(iconsSheet, 48, 0, 8, 8)
41+
icons.Separator = pi.SpriteFrom(iconsSheet, 58, 0, 4, 8)
42+
icons.Snap = pi.SpriteFrom(iconsSheet, 64, 0, 8, 8)
43+
icons.Prev = pi.SpriteFrom(iconsSheet, 74, 0, 5, 8)
44+
icons.Pause = pi.SpriteFrom(iconsSheet, 82, 0, 5, 8)
45+
icons.Play = pi.SpriteFrom(iconsSheet, 90, 0, 5, 8)
46+
icons.Next = pi.SpriteFrom(iconsSheet, 98, 0, 5, 8)
47+
icons.Exit = pi.SpriteFrom(iconsSheet, 112, 0, 8, 8)
48+
}

piscope/internal/icons.png

320 Bytes
Loading

piscope/internal/input.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package internal
5+
6+
import (
7+
"github.com/elgopher/pi/pievent"
8+
"github.com/elgopher/pi/pikey"
9+
)
10+
11+
func handleInputInConsoleMode() {
12+
right := pikey.Duration(pikey.Right)
13+
if right > 0 {
14+
if right == 1 || right > 10 {
15+
showNextSnapshot()
16+
}
17+
} else {
18+
left := pikey.Duration(pikey.Left)
19+
if left == 1 || left > 10 {
20+
showPrevSnapshot()
21+
}
22+
}
23+
}
24+
25+
func registerShortcuts() {
26+
// CTRL+SHIFT+I
27+
onCtrlShiftI := func() {
28+
if !consoleMode {
29+
enterConsoleMode()
30+
} else {
31+
exitConsoleMode()
32+
}
33+
}
34+
pikey.RegisterShortcut(onCtrlShiftI, pikey.Control, pikey.Shift, pikey.I)
35+
36+
// F12
37+
f12Down := pikey.Event{Type: pikey.EventDown, Key: pikey.F12}
38+
pikey.DebugTarget().Subscribe(f12Down, func(pikey.Event, pievent.Handler) {
39+
if consoleMode {
40+
captureSnapshot()
41+
}
42+
})
43+
44+
// Space
45+
spaceDown := pikey.Event{Type: pikey.EventDown, Key: pikey.Space}
46+
pikey.DebugTarget().Subscribe(spaceDown, func(pikey.Event, pievent.Handler) {
47+
pauseOrResume()
48+
})
49+
50+
// Esc
51+
escDown := pikey.Event{Type: pikey.EventDown, Key: pikey.Esc}
52+
pikey.DebugTarget().Subscribe(escDown, func(pikey.Event, pievent.Handler) {
53+
if consoleMode {
54+
exitConsoleMode()
55+
}
56+
})
57+
}

piscope/internal/internal.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2025 Jacek Olszak
2+
// This code is licensed under MIT license (see LICENSE for details)
3+
4+
package internal
5+
6+
import (
7+
"fmt"
8+
"github.com/elgopher/pi"
9+
"github.com/elgopher/pi/picofont"
10+
"github.com/elgopher/pi/pidebug"
11+
"github.com/elgopher/pi/pievent"
12+
"github.com/elgopher/pi/pigui"
13+
"github.com/elgopher/pi/piloop"
14+
"github.com/elgopher/pi/pimouse"
15+
)
16+
17+
var bgColor *pi.Color
18+
19+
var consoleMode, pauseOnNextFrame bool
20+
21+
// Start launches the developer tools.
22+
//
23+
// Obecnie piscope wymaga, żeby gra miała rozdzielczość conajmniej 128
24+
// pikseli w poziomie oraz 16 pikseli w pionie. Dodatkowa paleta gry musi
25+
// używać conajmniej 2 kolorów.
26+
//
27+
// Pressing Ctrl+Shift+I will activate the tools in the game
28+
func Start(backgroundColor *pi.Color) {
29+
bgColor = backgroundColor
30+
31+
// TODO Handle screen size change event and redraw entire gui.
32+
33+
smallFont := picofont.Sheet
34+
35+
registerShortcuts()
36+
37+
gui := pigui.New()
38+
attachToolbar(gui)
39+
40+
piloop.DebugTarget().Subscribe(piloop.EventUpdate, func(piloop.Event, pievent.Handler) {
41+
if consoleMode {
42+
gui.Update()
43+
44+
if !pidebug.Paused() {
45+
theScreenRecorder.Save()
46+
}
47+
48+
if pauseOnNextFrame {
49+
pidebug.SetPaused(true)
50+
pauseOnNextFrame = false
51+
}
52+
53+
handleInputInConsoleMode()
54+
}
55+
})
56+
57+
piloop.DebugTarget().Subscribe(piloop.EventLateDraw, func(piloop.Event, pievent.Handler) {
58+
if consoleMode {
59+
gui.Draw()
60+
61+
screen := pi.Screen()
62+
63+
y := screen.H() - smallFont.Height - 1
64+
65+
prev := pi.SetColor(*bgColor)
66+
defer pi.SetColor(prev)
67+
68+
pixelColor := pi.GetPixel(pimouse.Position.X, pimouse.Position.Y)
69+
if pixelColor != *bgColor {
70+
pi.SetColor(pixelColor)
71+
} else {
72+
pi.SetColor(1)
73+
}
74+
msg := fmt.Sprintf("%d(%d,%d)", pixelColor, pimouse.Position.X, pimouse.Position.Y)
75+
smallFont.Print(msg, 50, y+2)
76+
}
77+
})
78+
}
Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2025 Jacek Olszak
22
// This code is licensed under MIT license (see LICENSE for details)
33

4-
package piscope
4+
package internal
55

66
import (
77
"github.com/elgopher/pi"
@@ -48,6 +48,10 @@ func (s *screenRecorder) Save() {
4848
s.shift = 0
4949
}
5050

51+
func (s *screenRecorder) HasPrev() bool {
52+
return -s.shift+1 <= s.snapshots.Len()
53+
}
54+
5155
func (s *screenRecorder) ShowPrev() bool {
5256
if -s.shift+1 > s.snapshots.Len() {
5357
return false
@@ -77,3 +81,7 @@ func (s *screenRecorder) Reset() {
7781
s.snapshots.Reset()
7882
s.shift = 0
7983
}
84+
85+
func (s *screenRecorder) GoToLast() {
86+
s.shift = 0
87+
}

0 commit comments

Comments
 (0)