mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
ebiten: implement DroppedFiles to replace AppendDroppedFiles
Closes #2552
This commit is contained in:
parent
aa52402a90
commit
f009dd8dd2
@ -16,42 +16,58 @@ package main
|
||||
|
||||
import (
|
||||
"image/png"
|
||||
"io/fs"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
imageMutex sync.Mutex
|
||||
images []*ebiten.Image
|
||||
}
|
||||
|
||||
func (g *Game) Update() error {
|
||||
for _, f := range ebiten.AppendDroppedFiles(nil) {
|
||||
// Calling Close is not mandatory, but it is sligtly good to save memory.
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
fi, err := f.Stat()
|
||||
if files := ebiten.DroppedFiles(); files != nil {
|
||||
go fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
log.Printf("%v", err)
|
||||
continue
|
||||
return err
|
||||
}
|
||||
|
||||
fi, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Name: %s, Size: %d, IsDir: %t, ModTime: %v", fi.Name(), fi.Size(), fi.IsDir(), fi.ModTime())
|
||||
|
||||
f, err := files.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
log.Printf("decoding PNG failed: %v", err)
|
||||
continue
|
||||
}
|
||||
g.images = append(g.images, ebiten.NewImageFromImage(img))
|
||||
return nil
|
||||
}
|
||||
eimg := ebiten.NewImageFromImage(img)
|
||||
|
||||
g.imageMutex.Lock()
|
||||
g.images = append(g.images, eimg)
|
||||
g.imageMutex.Unlock()
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Game) Draw(screen *ebiten.Image) {
|
||||
g.imageMutex.Lock()
|
||||
defer g.imageMutex.Unlock()
|
||||
|
||||
if len(g.images) == 0 {
|
||||
ebitenutil.DebugPrint(screen, "Drop PNG files onto this window!")
|
||||
return
|
||||
|
4
input.go
4
input.go
@ -478,8 +478,8 @@ func (i *inputState) windowBeingClosed() bool {
|
||||
return i.state.WindowBeingClosed
|
||||
}
|
||||
|
||||
func (i *inputState) appendDroppedFiles(files []fs.File) []fs.File {
|
||||
func (i *inputState) droppedFiles() fs.FS {
|
||||
i.m.Lock()
|
||||
defer i.m.Unlock()
|
||||
return append(files, i.state.DroppedFiles...)
|
||||
return i.state.DroppedFiles
|
||||
}
|
||||
|
293
internal/file/file_js.go
Normal file
293
internal/file/file_js.go
Normal file
@ -0,0 +1,293 @@
|
||||
// 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 file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"syscall/js"
|
||||
"time"
|
||||
)
|
||||
|
||||
type FileEntryFS struct {
|
||||
rootEntry js.Value
|
||||
}
|
||||
|
||||
func NewFileEntryFS(root js.Value) *FileEntryFS {
|
||||
return &FileEntryFS{
|
||||
rootEntry: root,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileEntryFS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
return &dir{entry: f.rootEntry}, nil
|
||||
}
|
||||
|
||||
var chEntry chan js.Value
|
||||
cbSuccess := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chEntry <- args[0]
|
||||
close(chEntry)
|
||||
return nil
|
||||
})
|
||||
defer cbSuccess.Release()
|
||||
|
||||
cbFailure := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
close(chEntry)
|
||||
return nil
|
||||
})
|
||||
defer cbFailure.Release()
|
||||
|
||||
chEntry = make(chan js.Value)
|
||||
f.rootEntry.Call("getFile", name, nil, cbSuccess, cbFailure)
|
||||
if entry := <-chEntry; entry.Truthy() {
|
||||
return &file{entry: entry}, nil
|
||||
}
|
||||
|
||||
chEntry = make(chan js.Value)
|
||||
f.rootEntry.Call("getDirectory", name, nil, cbSuccess, cbFailure)
|
||||
if entry := <-chEntry; entry.Truthy() {
|
||||
return &dir{entry: entry}, nil
|
||||
}
|
||||
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
type file struct {
|
||||
entry js.Value
|
||||
file js.Value
|
||||
offset int64
|
||||
uint8Array js.Value
|
||||
}
|
||||
|
||||
func getFile(entry js.Value) js.Value {
|
||||
ch := make(chan js.Value, 1)
|
||||
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
ch <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cb.Release()
|
||||
|
||||
entry.Call("file", cb)
|
||||
return <-ch
|
||||
}
|
||||
|
||||
func (f *file) ensureFile() js.Value {
|
||||
if f.file.Truthy() {
|
||||
return f.file
|
||||
}
|
||||
|
||||
f.file = getFile(f.entry)
|
||||
return f.file
|
||||
}
|
||||
|
||||
func (f *file) Stat() (fs.FileInfo, error) {
|
||||
return &fileInfo{
|
||||
entry: f.entry,
|
||||
file: f.ensureFile(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *file) Read(buf []byte) (int, error) {
|
||||
if !f.uint8Array.Truthy() {
|
||||
chArrayBuffer := make(chan js.Value, 1)
|
||||
cbThen := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chArrayBuffer <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cbThen.Release()
|
||||
|
||||
chError := make(chan js.Value, 1)
|
||||
cbCatch := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chError <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cbCatch.Release()
|
||||
|
||||
f.ensureFile().Call("arrayBuffer").Call("then", cbThen).Call("catch", cbCatch)
|
||||
select {
|
||||
case ab := <-chArrayBuffer:
|
||||
f.uint8Array = js.Global().Get("Uint8Array").New(ab)
|
||||
case err := <-chError:
|
||||
return 0, fmt.Errorf("%s", err.Call("toString").String())
|
||||
}
|
||||
}
|
||||
|
||||
size := int64(f.uint8Array.Get("byteLength").Float())
|
||||
if f.offset >= size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
slice := f.uint8Array.Call("subarray", f.offset, f.offset+int64(len(buf)))
|
||||
n := slice.Get("byteLength").Int()
|
||||
js.CopyBytesToGo(buf[:n], slice)
|
||||
f.offset += int64(n)
|
||||
if f.offset >= size {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *file) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dir struct {
|
||||
entry js.Value
|
||||
entries []js.Value
|
||||
offset int
|
||||
}
|
||||
|
||||
func (d *dir) Stat() (fs.FileInfo, error) {
|
||||
return &fileInfo{
|
||||
entry: d.entry,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *dir) Read(buf []byte) (int, error) {
|
||||
return 0, &fs.PathError{
|
||||
Op: "read",
|
||||
Path: d.entry.Get("name").String(),
|
||||
Err: errors.New("is a directory"),
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dir) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dir) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||
if d.entries == nil {
|
||||
ch := make(chan struct{})
|
||||
var rec js.Func
|
||||
cb := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
entries := args[0]
|
||||
if entries.Length() == 0 {
|
||||
close(ch)
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < entries.Length(); i++ {
|
||||
ent := entries.Index(i)
|
||||
// A name can be empty when this directory is a root directory.
|
||||
if ent.Get("name").String() == "" {
|
||||
continue
|
||||
}
|
||||
if !ent.Get("isFile").Bool() && !ent.Get("isDirectory").Bool() {
|
||||
continue
|
||||
}
|
||||
d.entries = append(d.entries, ent)
|
||||
}
|
||||
rec.Value.Call("call")
|
||||
return nil
|
||||
})
|
||||
defer cb.Release()
|
||||
|
||||
reader := d.entry.Call("createReader")
|
||||
rec = js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
reader.Call("readEntries", cb)
|
||||
return nil
|
||||
})
|
||||
defer rec.Release()
|
||||
|
||||
rec.Value.Call("call")
|
||||
<-ch
|
||||
}
|
||||
|
||||
n := len(d.entries) - d.offset
|
||||
|
||||
if n == 0 {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if count > 0 && n > count {
|
||||
n = count
|
||||
}
|
||||
|
||||
ents := make([]fs.DirEntry, n)
|
||||
for i := range ents {
|
||||
fi := &fileInfo{
|
||||
entry: d.entries[d.offset+i],
|
||||
}
|
||||
if fi.entry.Get("isFile").Bool() {
|
||||
fi.file = getFile(fi.entry)
|
||||
}
|
||||
ents[i] = fs.FileInfoToDirEntry(fi)
|
||||
}
|
||||
d.offset += n
|
||||
|
||||
return ents, nil
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
entry js.Value
|
||||
file js.Value
|
||||
}
|
||||
|
||||
func (f *fileInfo) Name() string {
|
||||
return f.entry.Get("name").String()
|
||||
}
|
||||
|
||||
func (f *fileInfo) Size() int64 {
|
||||
if !f.file.Truthy() {
|
||||
return 0
|
||||
}
|
||||
return int64(f.file.Get("size").Float())
|
||||
}
|
||||
|
||||
func (f *fileInfo) Mode() fs.FileMode {
|
||||
if !f.file.Truthy() {
|
||||
return 0555 | fs.ModeDir
|
||||
}
|
||||
return 0444
|
||||
}
|
||||
|
||||
func (f *fileInfo) ModTime() time.Time {
|
||||
if !f.file.Truthy() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.UnixMilli(int64(f.file.Get("lastModified").Float()))
|
||||
}
|
||||
|
||||
func (f *fileInfo) IsDir() bool {
|
||||
if !f.file.Truthy() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *fileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
166
internal/file/file_notjs.go
Normal file
166
internal/file/file_notjs.go
Normal file
@ -0,0 +1,166 @@
|
||||
// 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.
|
||||
|
||||
//go:build !js
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type VirtualFS struct {
|
||||
root virtualFSRoot
|
||||
}
|
||||
|
||||
func NewVirtualFS(paths []string) *VirtualFS {
|
||||
fs := &VirtualFS{}
|
||||
fs.root.addRealPaths(paths)
|
||||
return fs
|
||||
}
|
||||
|
||||
func (v *VirtualFS) Open(name string) (fs.File, error) {
|
||||
if !fs.ValidPath(name) {
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
return &v.root, nil
|
||||
}
|
||||
|
||||
// A valid path must not include a token "." or "..", except for "." itself.
|
||||
es := strings.Split(name, "/")
|
||||
for _, realPath := range v.root.realPaths {
|
||||
if filepath.Base(realPath) != es[0] {
|
||||
continue
|
||||
}
|
||||
return os.Open(filepath.Join(append([]string{realPath}, es[1:]...)...))
|
||||
}
|
||||
|
||||
return nil, &fs.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: fs.ErrNotExist,
|
||||
}
|
||||
}
|
||||
|
||||
type virtualFSRoot struct {
|
||||
realPaths []string
|
||||
offset int
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
func (v *virtualFSRoot) addRealPaths(paths []string) {
|
||||
v.m.Lock()
|
||||
defer v.m.Unlock()
|
||||
|
||||
for _, path := range paths {
|
||||
// If the path consists entirely of separators, filepath.Base returns a single separator.
|
||||
// On Windows, filepath.Base(`C:\`) == `\`.
|
||||
// Skip root directory paths on purpose. This is almost the same behavior as the Chrome browser.
|
||||
if filepath.Base(path) == string(filepath.Separator) {
|
||||
continue
|
||||
}
|
||||
v.realPaths = append(v.realPaths, path)
|
||||
}
|
||||
sort.Strings(v.realPaths)
|
||||
}
|
||||
|
||||
func (v *virtualFSRoot) Stat() (fs.FileInfo, error) {
|
||||
return &virtualFSRootFileInfo{}, nil
|
||||
}
|
||||
|
||||
func (v *virtualFSRoot) Read([]byte) (int, error) {
|
||||
return 0, &fs.PathError{
|
||||
Op: "read",
|
||||
Path: "/",
|
||||
Err: errors.New("is a directory"),
|
||||
}
|
||||
}
|
||||
|
||||
func (v *virtualFSRoot) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *virtualFSRoot) ReadDir(count int) ([]fs.DirEntry, error) {
|
||||
v.m.Lock()
|
||||
defer v.m.Unlock()
|
||||
|
||||
n := len(v.realPaths) - v.offset
|
||||
|
||||
if n == 0 {
|
||||
if count <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if count > 0 && n > count {
|
||||
n = count
|
||||
}
|
||||
|
||||
ents := make([]fs.DirEntry, n)
|
||||
for i := range ents {
|
||||
fi, err := os.Stat(v.realPaths[v.offset+i])
|
||||
if err != nil {
|
||||
if count <= 0 {
|
||||
return ents, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ents[i] = fs.FileInfoToDirEntry(fi)
|
||||
}
|
||||
v.offset += n
|
||||
|
||||
return ents, nil
|
||||
}
|
||||
|
||||
type virtualFSRootFileInfo struct {
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) Name() string {
|
||||
return "."
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) Mode() fs.FileMode {
|
||||
return 0555 | fs.ModeDir
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *virtualFSRootFileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
@ -48,7 +48,7 @@ type InputState struct {
|
||||
Touches []Touch
|
||||
Runes []rune
|
||||
WindowBeingClosed bool
|
||||
DroppedFiles []fs.File
|
||||
DroppedFiles fs.FS
|
||||
}
|
||||
|
||||
func (i *InputState) copyAndReset(dst *InputState) {
|
||||
@ -61,10 +61,7 @@ func (i *InputState) copyAndReset(dst *InputState) {
|
||||
dst.Touches = append(dst.Touches[:0], i.Touches...)
|
||||
dst.Runes = append(dst.Runes[:0], i.Runes...)
|
||||
dst.WindowBeingClosed = i.WindowBeingClosed
|
||||
for idx := range dst.DroppedFiles {
|
||||
dst.DroppedFiles[idx] = nil
|
||||
}
|
||||
dst.DroppedFiles = append(dst.DroppedFiles[:0], i.DroppedFiles...)
|
||||
dst.DroppedFiles = i.DroppedFiles
|
||||
|
||||
// Reset the members that are updated by deltas, rather than absolute values.
|
||||
i.WheelX = 0
|
||||
@ -73,10 +70,7 @@ func (i *InputState) copyAndReset(dst *InputState) {
|
||||
|
||||
// Reset the members that are never reset until they are explicitly done.
|
||||
i.WindowBeingClosed = false
|
||||
for idx := range i.DroppedFiles {
|
||||
i.DroppedFiles[idx] = nil
|
||||
}
|
||||
i.DroppedFiles = i.DroppedFiles[:0]
|
||||
i.DroppedFiles = nil
|
||||
}
|
||||
|
||||
func (i *InputState) appendRune(r rune) {
|
||||
@ -85,7 +79,3 @@ func (i *InputState) appendRune(r rune) {
|
||||
}
|
||||
i.Runes = append(i.Runes, r)
|
||||
}
|
||||
|
||||
func (i *InputState) appendDroppedFiles(files []fs.File) {
|
||||
i.DroppedFiles = append(i.DroppedFiles, files...)
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/fs"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
@ -29,6 +28,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/devicescale"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/file"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
|
||||
@ -751,22 +751,9 @@ func (u *userInterfaceImpl) registerWindowFramebufferSizeCallback() {
|
||||
func (u *userInterfaceImpl) registerDropCallback() {
|
||||
if u.dropCallback == nil {
|
||||
u.dropCallback = glfw.ToDropCallback(func(_ *glfw.Window, names []string) {
|
||||
var files []fs.File
|
||||
for _, name := range names {
|
||||
f, err := os.Open(name)
|
||||
if err != nil {
|
||||
files = append(files, &errorFile{
|
||||
name: name,
|
||||
err: err,
|
||||
})
|
||||
continue
|
||||
}
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
u.m.Lock()
|
||||
defer u.m.Unlock()
|
||||
u.inputState.appendDroppedFiles(files)
|
||||
u.inputState.DroppedFiles = file.NewVirtualFS(names)
|
||||
})
|
||||
}
|
||||
u.window.SetDropCallback(u.dropCallback)
|
||||
@ -1544,20 +1531,3 @@ func (u *userInterfaceImpl) setOrigWindowPos(x, y int) {
|
||||
func IsScreenTransparentAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type errorFile struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *errorFile) Stat() (fs.FileInfo, error) {
|
||||
return nil, fmt.Errorf("ui: failed to open %s: %w", e.name, e.err)
|
||||
}
|
||||
|
||||
func (e *errorFile) Read(buf []byte) (int, error) {
|
||||
return 0, fmt.Errorf("ui: failed to open %s: %w", e.name, e.err)
|
||||
}
|
||||
|
||||
func (e *errorFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
@ -15,14 +15,12 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"sync"
|
||||
"syscall/js"
|
||||
"time"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/devicescale"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/file"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
|
||||
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl"
|
||||
@ -650,33 +648,13 @@ func (u *userInterfaceImpl) appendDroppedFiles(data js.Value) {
|
||||
u.dropFileM.Lock()
|
||||
defer u.dropFileM.Unlock()
|
||||
|
||||
chFile := make(chan js.Value, 1)
|
||||
cbFile := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chFile <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cbFile.Release()
|
||||
|
||||
items := data.Get("items")
|
||||
for i := 0; i < items.Length(); i++ {
|
||||
entry := items.Index(i).Call("webkitGetAsEntry")
|
||||
var f fs.File
|
||||
switch {
|
||||
case entry.Get("isFile").Bool():
|
||||
entry.Call("file", cbFile)
|
||||
jsFile := <-chFile
|
||||
f = &file{
|
||||
v: jsFile,
|
||||
}
|
||||
case entry.Get("isDirectory").Bool():
|
||||
f = &dir{
|
||||
entry: entry,
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
u.inputState.DroppedFiles = append(u.inputState.DroppedFiles, f)
|
||||
if items.Length() <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
fs := items.Index(0).Call("webkitGetAsEntry").Get("filesystem").Get("root")
|
||||
u.inputState.DroppedFiles = file.NewFileEntryFS(fs)
|
||||
}
|
||||
|
||||
func (u *userInterfaceImpl) forceUpdateOnMinimumFPSMode() {
|
||||
@ -751,133 +729,3 @@ func (u *userInterfaceImpl) updateIconIfNeeded() {
|
||||
func IsScreenTransparentAvailable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type file struct {
|
||||
v js.Value
|
||||
cursor int64
|
||||
uint8Array js.Value
|
||||
}
|
||||
|
||||
func (f *file) Stat() (fs.FileInfo, error) {
|
||||
return &fileInfo{v: f.v}, nil
|
||||
}
|
||||
|
||||
func (f *file) Read(buf []byte) (int, error) {
|
||||
if !f.uint8Array.Truthy() {
|
||||
chArrayBuffer := make(chan js.Value, 1)
|
||||
cbThen := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chArrayBuffer <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cbThen.Release()
|
||||
|
||||
chError := make(chan js.Value, 1)
|
||||
cbCatch := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
chError <- args[0]
|
||||
return nil
|
||||
})
|
||||
defer cbCatch.Release()
|
||||
|
||||
f.v.Call("arrayBuffer").Call("then", cbThen).Call("catch", cbCatch)
|
||||
select {
|
||||
case ab := <-chArrayBuffer:
|
||||
f.uint8Array = js.Global().Get("Uint8Array").New(ab)
|
||||
case err := <-chError:
|
||||
return 0, fmt.Errorf("%s", err.Call("toString").String())
|
||||
}
|
||||
}
|
||||
|
||||
size := int64(f.uint8Array.Get("byteLength").Float())
|
||||
if f.cursor >= size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
slice := f.uint8Array.Call("subarray", f.cursor, f.cursor+int64(len(buf)))
|
||||
n := slice.Get("byteLength").Int()
|
||||
js.CopyBytesToGo(buf[:n], slice)
|
||||
f.cursor += int64(n)
|
||||
if f.cursor >= size {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *file) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
v js.Value
|
||||
}
|
||||
|
||||
func (f *fileInfo) Name() string {
|
||||
return f.v.Get("name").String()
|
||||
}
|
||||
|
||||
func (f *fileInfo) Size() int64 {
|
||||
return int64(f.v.Get("size").Float())
|
||||
}
|
||||
|
||||
func (f *fileInfo) Mode() fs.FileMode {
|
||||
return 0400
|
||||
}
|
||||
|
||||
func (f *fileInfo) ModTime() time.Time {
|
||||
return time.UnixMilli(int64(f.v.Get("lastModified").Float()))
|
||||
}
|
||||
|
||||
func (f *fileInfo) IsDir() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *fileInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dir struct {
|
||||
entry js.Value
|
||||
}
|
||||
|
||||
func (d *dir) Stat() (fs.FileInfo, error) {
|
||||
return &dirInfo{entry: d.entry}, nil
|
||||
}
|
||||
|
||||
func (d *dir) Read(buf []byte) (int, error) {
|
||||
return 0, fmt.Errorf("%s is a directory", d.entry.Get("name").String())
|
||||
}
|
||||
|
||||
func (d *dir) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dirInfo struct {
|
||||
entry js.Value
|
||||
}
|
||||
|
||||
func (d *dirInfo) Name() string {
|
||||
return d.entry.Get("name").String()
|
||||
}
|
||||
|
||||
func (d *dirInfo) Size() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (d *dirInfo) Mode() fs.FileMode {
|
||||
return 0400 | fs.ModeDir
|
||||
}
|
||||
|
||||
func (d *dirInfo) ModTime() time.Time {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (d *dirInfo) IsDir() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (d *dirInfo) Sys() any {
|
||||
return nil
|
||||
}
|
||||
|
13
run.go
13
run.go
@ -666,13 +666,12 @@ func toUIRunOptions(options *RunGameOptions) *ui.RunOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// AppendDroppedFiles appends files, dropped at the time Update is called, to the given slice,
|
||||
// and returns the extended buffer.
|
||||
// Giving a slice that already has enough capacity works efficiently.
|
||||
// DroppedFiles returns a virtual file system that includes only dropped files and/or directories
|
||||
// at its root directory, at the time Update is called.
|
||||
//
|
||||
// AppendDroppedFiles works on desktops and browsers.
|
||||
// DroppedFiles works on desktops and browsers.
|
||||
//
|
||||
// AppendDroppedFiles is concurrent-safe.
|
||||
func AppendDroppedFiles(files []fs.File) []fs.File {
|
||||
return theInputState.appendDroppedFiles(files)
|
||||
// DroppedFiles is concurrent-safe.
|
||||
func DroppedFiles() fs.FS {
|
||||
return theInputState.droppedFiles()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user