mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
parent
4676fd26f0
commit
ff51c4a2c7
28
event/event.go
Normal file
28
event/event.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2023 The Ebitengine Authors
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
type MouseMoveEvent struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type MouseDownEvent struct {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
type MouseUpEvent struct {
|
||||||
|
// TODO
|
||||||
|
}
|
@ -16,6 +16,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"flag"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
@ -25,6 +26,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/event"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
)
|
)
|
||||||
@ -38,6 +40,10 @@ const (
|
|||||||
screenHeight = 480
|
screenHeight = 480
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagEvent = flag.Bool("event", false, "use HandleEvent")
|
||||||
|
)
|
||||||
|
|
||||||
// Sprite represents an image.
|
// Sprite represents an image.
|
||||||
type Sprite struct {
|
type Sprite struct {
|
||||||
image *ebiten.Image
|
image *ebiten.Image
|
||||||
@ -117,6 +123,21 @@ func (t *TouchStrokeSource) IsJustReleased() bool {
|
|||||||
return inpututil.IsTouchJustReleased(t.ID)
|
return inpututil.IsTouchJustReleased(t.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MouseEventStrokeSource struct {
|
||||||
|
pressed bool
|
||||||
|
x float64
|
||||||
|
y float64
|
||||||
|
isJustReleased bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MouseEventStrokeSource) Position() (int, int) {
|
||||||
|
return int(m.x), int(m.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MouseEventStrokeSource) IsJustReleased() bool {
|
||||||
|
return m.isJustReleased
|
||||||
|
}
|
||||||
|
|
||||||
// Stroke manages the current drag state by mouse.
|
// Stroke manages the current drag state by mouse.
|
||||||
type Stroke struct {
|
type Stroke struct {
|
||||||
source StrokeSource
|
source StrokeSource
|
||||||
@ -160,9 +181,12 @@ func (s *Stroke) Sprite() *Sprite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
touchIDs []ebiten.TouchID
|
touchIDs []ebiten.TouchID
|
||||||
strokes map[*Stroke]struct{}
|
tickStrokes map[*Stroke]struct{}
|
||||||
sprites []*Sprite
|
eventStroke *Stroke
|
||||||
|
sprites []*Sprite
|
||||||
|
|
||||||
|
mouseEventStrokeSource MouseEventStrokeSource
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -205,8 +229,8 @@ func NewGame() *Game {
|
|||||||
|
|
||||||
// Initialize the game.
|
// Initialize the game.
|
||||||
return &Game{
|
return &Game{
|
||||||
strokes: map[*Stroke]struct{}{},
|
tickStrokes: map[*Stroke]struct{}{},
|
||||||
sprites: sprites,
|
sprites: sprites,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,11 +258,47 @@ func (g *Game) moveSpriteToFront(sprite *Sprite) {
|
|||||||
g.sprites = append(g.sprites, sprite)
|
g.sprites = append(g.sprites, sprite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Game) HandleEvent(e any) {
|
||||||
|
if !*flagEvent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := e.(type) {
|
||||||
|
case event.MouseDownEvent:
|
||||||
|
g.mouseEventStrokeSource.pressed = true
|
||||||
|
g.mouseEventStrokeSource.isJustReleased = false
|
||||||
|
case event.MouseMoveEvent:
|
||||||
|
if g.mouseEventStrokeSource.pressed {
|
||||||
|
g.mouseEventStrokeSource.x = e.X
|
||||||
|
g.mouseEventStrokeSource.y = e.Y
|
||||||
|
if g.eventStroke == nil {
|
||||||
|
if sp := g.spriteAt(int(e.X), int(e.Y)); sp != nil {
|
||||||
|
g.eventStroke = NewStroke(&g.mouseEventStrokeSource, sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case event.MouseUpEvent:
|
||||||
|
g.mouseEventStrokeSource.pressed = false
|
||||||
|
g.mouseEventStrokeSource.isJustReleased = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if g.eventStroke != nil {
|
||||||
|
g.eventStroke.Update()
|
||||||
|
if !g.eventStroke.sprite.dragged {
|
||||||
|
g.eventStroke = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
func (g *Game) Update() error {
|
||||||
|
if *flagEvent {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
|
||||||
if sp := g.spriteAt(ebiten.CursorPosition()); sp != nil {
|
if sp := g.spriteAt(ebiten.CursorPosition()); sp != nil {
|
||||||
s := NewStroke(&MouseStrokeSource{}, sp)
|
s := NewStroke(&MouseStrokeSource{}, sp)
|
||||||
g.strokes[s] = struct{}{}
|
g.tickStrokes[s] = struct{}{}
|
||||||
g.moveSpriteToFront(sp)
|
g.moveSpriteToFront(sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -246,15 +306,15 @@ func (g *Game) Update() error {
|
|||||||
for _, id := range g.touchIDs {
|
for _, id := range g.touchIDs {
|
||||||
if sp := g.spriteAt(ebiten.TouchPosition(id)); sp != nil {
|
if sp := g.spriteAt(ebiten.TouchPosition(id)); sp != nil {
|
||||||
s := NewStroke(&TouchStrokeSource{id}, sp)
|
s := NewStroke(&TouchStrokeSource{id}, sp)
|
||||||
g.strokes[s] = struct{}{}
|
g.tickStrokes[s] = struct{}{}
|
||||||
g.moveSpriteToFront(sp)
|
g.moveSpriteToFront(sp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for s := range g.strokes {
|
for s := range g.tickStrokes {
|
||||||
s.Update()
|
s.Update()
|
||||||
if !s.sprite.dragged {
|
if !s.sprite.dragged {
|
||||||
delete(g.strokes, s)
|
delete(g.tickStrokes, s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -277,6 +337,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
ebiten.SetWindowSize(screenWidth, screenHeight)
|
ebiten.SetWindowSize(screenWidth, screenHeight)
|
||||||
ebiten.SetWindowTitle("Drag & Drop (Ebitengine Demo)")
|
ebiten.SetWindowTitle("Drag & Drop (Ebitengine Demo)")
|
||||||
if err := ebiten.RunGame(NewGame()); err != nil {
|
if err := ebiten.RunGame(NewGame()); err != nil {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/colorm"
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/event"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -31,6 +33,10 @@ const (
|
|||||||
screenHeight = 480
|
screenHeight = 480
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagEvent = flag.Bool("event", false, "use HandleEvent")
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
brushImage *ebiten.Image
|
brushImage *ebiten.Image
|
||||||
)
|
)
|
||||||
@ -70,6 +76,8 @@ type Game struct {
|
|||||||
count int
|
count int
|
||||||
|
|
||||||
canvasImage *ebiten.Image
|
canvasImage *ebiten.Image
|
||||||
|
|
||||||
|
mousePressedByEvent bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGame() *Game {
|
func NewGame() *Game {
|
||||||
@ -80,14 +88,34 @@ func NewGame() *Game {
|
|||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Game) HandleEvent(e any) {
|
||||||
|
if !*flagEvent {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := e.(type) {
|
||||||
|
case event.MouseDownEvent:
|
||||||
|
g.mousePressedByEvent = true
|
||||||
|
case event.MouseMoveEvent:
|
||||||
|
if g.mousePressedByEvent {
|
||||||
|
g.paint(g.canvasImage, int(e.X), int(e.Y))
|
||||||
|
g.count++
|
||||||
|
}
|
||||||
|
case event.MouseUpEvent:
|
||||||
|
g.mousePressedByEvent = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Game) Update() error {
|
func (g *Game) Update() error {
|
||||||
drawn := false
|
var drawn bool
|
||||||
|
|
||||||
// Paint the brush by mouse dragging
|
// Paint the brush by mouse dragging
|
||||||
mx, my := ebiten.CursorPosition()
|
mx, my := ebiten.CursorPosition()
|
||||||
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
if !*flagEvent {
|
||||||
g.paint(g.canvasImage, mx, my)
|
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
|
||||||
drawn = true
|
g.paint(g.canvasImage, mx, my)
|
||||||
|
drawn = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
g.cursor = pos{
|
g.cursor = pos{
|
||||||
x: mx,
|
x: mx,
|
||||||
@ -142,6 +170,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
ebiten.SetWindowSize(screenWidth, screenHeight)
|
ebiten.SetWindowSize(screenWidth, screenHeight)
|
||||||
ebiten.SetWindowTitle("Paint (Ebitengine Demo)")
|
ebiten.SetWindowTitle("Paint (Ebitengine Demo)")
|
||||||
if err := ebiten.RunGame(NewGame()); err != nil {
|
if err := ebiten.RunGame(NewGame()); err != nil {
|
||||||
|
@ -114,6 +114,12 @@ func (g *gameForUI) NewScreenImage(width, height int) *ui.Image {
|
|||||||
return g.screen.image
|
return g.screen.image
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *gameForUI) HandleEvent(event any) {
|
||||||
|
if h, ok := g.game.(EventHandler); ok {
|
||||||
|
h.HandleEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float64) {
|
func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float64) {
|
||||||
if l, ok := g.game.(LayoutFer); ok {
|
if l, ok := g.game.(LayoutFer); ok {
|
||||||
return l.LayoutF(outsideWidth, outsideHeight)
|
return l.LayoutF(outsideWidth, outsideHeight)
|
||||||
|
@ -192,6 +192,9 @@ func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/*if endFrame {
|
||||||
|
sync = true
|
||||||
|
}*/
|
||||||
|
|
||||||
logger := debug.SwitchLogger()
|
logger := debug.SwitchLogger()
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
"github.com/hajimehoshi/ebiten/v2/internal/clock"
|
||||||
@ -33,6 +34,7 @@ type Game interface {
|
|||||||
NewOffscreenImage(width, height int) *Image
|
NewOffscreenImage(width, height int) *Image
|
||||||
NewScreenImage(width, height int) *Image
|
NewScreenImage(width, height int) *Image
|
||||||
Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
|
Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
|
||||||
|
HandleEvent(event any)
|
||||||
UpdateInputState(fn func(*InputState))
|
UpdateInputState(fn func(*InputState))
|
||||||
Update() error
|
Update() error
|
||||||
DrawOffscreen() error
|
DrawOffscreen() error
|
||||||
@ -40,7 +42,8 @@ type Game interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type context struct {
|
type context struct {
|
||||||
game Game
|
game Game
|
||||||
|
inputCh chan any
|
||||||
|
|
||||||
updateCalled bool
|
updateCalled bool
|
||||||
|
|
||||||
@ -55,12 +58,34 @@ type context struct {
|
|||||||
isOffscreenModified bool
|
isOffscreenModified bool
|
||||||
|
|
||||||
skipCount int
|
skipCount int
|
||||||
|
|
||||||
|
gameM sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func newContext(game Game) *context {
|
func newContext(game Game) *context {
|
||||||
return &context{
|
c := &context{
|
||||||
game: game,
|
game: game,
|
||||||
|
inputCh: make(chan any, 128),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create an independent goroutine from Update/Draw not to cause deadlock at (*UserInterface).readPixels.
|
||||||
|
go func() {
|
||||||
|
// TODO: Abort this loop when the game terminates.
|
||||||
|
for e := range c.inputCh {
|
||||||
|
e := e
|
||||||
|
func() {
|
||||||
|
c.gameM.Lock()
|
||||||
|
defer c.gameM.Unlock()
|
||||||
|
c.game.HandleEvent(e)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *context) sendInputEvent(event any) {
|
||||||
|
c.inputCh <- event
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, swapBuffersForGL func()) error {
|
func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, swapBuffersForGL func()) error {
|
||||||
@ -90,6 +115,18 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := c.updateGameInFrame(graphicsDriver, updateCount, outsideWidth, outsideHeight, deviceScaleFactor, ui, forceDraw, swapBuffersForGL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := atlas.SwapBuffers(graphicsDriver, swapBuffersForGL); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *context) updateGameInFrame(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool, swapBuffersForGL func()) (err error) {
|
||||||
debug.Logf("----\n")
|
debug.Logf("----\n")
|
||||||
|
|
||||||
if err := atlas.BeginFrame(graphicsDriver); err != nil {
|
if err := atlas.BeginFrame(graphicsDriver); err != nil {
|
||||||
@ -101,13 +138,11 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
|
|||||||
err = err1
|
err = err1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err1 := atlas.SwapBuffers(graphicsDriver, swapBuffersForGL); err1 != nil && err == nil {
|
|
||||||
err = err1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
c.gameM.Lock()
|
||||||
|
defer c.gameM.Unlock()
|
||||||
|
|
||||||
// ForceUpdate can be invoked even if the context is not initialized yet (#1591).
|
// ForceUpdate can be invoked even if the context is not initialized yet (#1591).
|
||||||
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
|
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -19,6 +19,7 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/event"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
|
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
|
||||||
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
|
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
|
||||||
)
|
)
|
||||||
@ -51,6 +52,35 @@ func (u *UserInterface) registerInputCallbacks() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For HandleEvent
|
||||||
|
if _, err := u.window.SetCursorPosCallback(func(w *glfw.Window, xpos float64, ypos float64) {
|
||||||
|
m, err := u.currentMonitor()
|
||||||
|
if err != nil {
|
||||||
|
u.setError(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
x := dipFromGLFWPixel(xpos, m)
|
||||||
|
y := dipFromGLFWPixel(ypos, m)
|
||||||
|
x, y = u.context.clientPositionToLogicalPosition(x, y, m.deviceScaleFactor())
|
||||||
|
u.context.sendInputEvent(event.MouseMoveEvent{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
})
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := u.window.SetMouseButtonCallback(func(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
|
||||||
|
// TODO: Use button and mods
|
||||||
|
switch action {
|
||||||
|
case glfw.Release:
|
||||||
|
u.context.sendInputEvent(event.MouseUpEvent{})
|
||||||
|
case glfw.Press:
|
||||||
|
u.context.sendInputEvent(event.MouseDownEvent{})
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
4
run.go
4
run.go
@ -93,6 +93,10 @@ type LayoutFer interface {
|
|||||||
LayoutF(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
|
LayoutF(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EventHandler interface {
|
||||||
|
HandleEvent(event any)
|
||||||
|
}
|
||||||
|
|
||||||
// FinalScreen represents the final screen image.
|
// FinalScreen represents the final screen image.
|
||||||
// FinalScreen implements a part of Image functions.
|
// FinalScreen implements a part of Image functions.
|
||||||
type FinalScreen interface {
|
type FinalScreen interface {
|
||||||
|
Loading…
Reference in New Issue
Block a user