client refactor
This commit is contained in:
+4
-3
@@ -11,7 +11,7 @@ import (
|
|||||||
type interactiveRaster struct {
|
type interactiveRaster struct {
|
||||||
widget.BaseWidget
|
widget.BaseWidget
|
||||||
|
|
||||||
edit *editor
|
edit *client
|
||||||
min fyne.Size
|
min fyne.Size
|
||||||
raster *canvas.Raster
|
raster *canvas.Raster
|
||||||
onLayout func(fyne.Size)
|
onLayout func(fyne.Size)
|
||||||
@@ -49,7 +49,9 @@ func (r *interactiveRaster) Tapped(ev *fyne.PointEvent) {
|
|||||||
// TappedSecondary is a right-click event
|
// TappedSecondary is a right-click event
|
||||||
func (r *interactiveRaster) TappedSecondary(*fyne.PointEvent) {}
|
func (r *interactiveRaster) TappedSecondary(*fyne.PointEvent) {}
|
||||||
|
|
||||||
func newInteractiveRaster(edit *editor, raster *canvas.Raster,
|
func newInteractiveRaster(
|
||||||
|
edit *client,
|
||||||
|
raster *canvas.Raster,
|
||||||
onLayout func(fyne.Size),
|
onLayout func(fyne.Size),
|
||||||
onScrolled func(*fyne.ScrollEvent),
|
onScrolled func(*fyne.ScrollEvent),
|
||||||
onDragged func(*fyne.DragEvent),
|
onDragged func(*fyne.DragEvent),
|
||||||
@@ -57,7 +59,6 @@ func newInteractiveRaster(edit *editor, raster *canvas.Raster,
|
|||||||
onTapped func(*fyne.PointEvent),
|
onTapped func(*fyne.PointEvent),
|
||||||
) *interactiveRaster {
|
) *interactiveRaster {
|
||||||
r := &interactiveRaster{
|
r := &interactiveRaster{
|
||||||
// raster: canvas.NewRaster(edit.draw),
|
|
||||||
raster: raster,
|
raster: raster,
|
||||||
edit: edit,
|
edit: edit,
|
||||||
onLayout: onLayout,
|
onLayout: onLayout,
|
||||||
|
|||||||
+7
-8
@@ -5,21 +5,20 @@ import (
|
|||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
)
|
)
|
||||||
|
|
||||||
type client struct {
|
type ui struct {
|
||||||
app fyne.App
|
app fyne.App
|
||||||
window fyne.Window
|
window fyne.Window
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient() *client {
|
func NewUI() *ui {
|
||||||
c := &client{}
|
c := &ui{}
|
||||||
c.app = app.New()
|
c.app = app.New()
|
||||||
c.window = c.app.NewWindow("Galaxy+")
|
c.window = c.app.NewWindow("Galaxy Plus")
|
||||||
editor := NewEditor()
|
client := NewClient()
|
||||||
editor.BuildUI(c.window)
|
client.BuildUI(c.window)
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) Run() {
|
func (c *ui) Run() {
|
||||||
c.window.ShowAndRun()
|
c.window.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,3 +188,40 @@ func TestCopyViewportRGBA_CopiesROIAndIsIndependentFromSource(t *testing.T) {
|
|||||||
require.Equal(t, byte(marginY), dst.Pix[offDst+1])
|
require.Equal(t, byte(marginY), dst.Pix[offDst+1])
|
||||||
require.Equal(t, byte(255), dst.Pix[offDst+3])
|
require.Equal(t, byte(255), dst.Pix[offDst+3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEventPosToPixel_FloorMapping(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
e := &client{}
|
||||||
|
|
||||||
|
// Pretend raster logical is 100x50, pixel is 1000x500.
|
||||||
|
e.metaMu.Lock()
|
||||||
|
e.lastRasterLogicW = 100
|
||||||
|
e.lastRasterLogicH = 50
|
||||||
|
e.lastRasterPxW = 1000
|
||||||
|
e.lastRasterPxH = 500
|
||||||
|
e.metaMu.Unlock()
|
||||||
|
|
||||||
|
x, y, ok := e.eventPosToPixel(0, 0)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, 0, x)
|
||||||
|
require.Equal(t, 0, y)
|
||||||
|
|
||||||
|
// Middle
|
||||||
|
x, y, ok = e.eventPosToPixel(50, 25)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, 500, x)
|
||||||
|
require.Equal(t, 250, y)
|
||||||
|
|
||||||
|
// Near max logical should map near max pixel with floor.
|
||||||
|
x, y, ok = e.eventPosToPixel(99.9, 49.9)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.GreaterOrEqual(t, x, 998)
|
||||||
|
require.GreaterOrEqual(t, y, 498)
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
x, y, ok = e.eventPosToPixel(-10, 999)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, 0, x)
|
||||||
|
require.Equal(t, 500, y)
|
||||||
|
}
|
||||||
@@ -3,6 +3,6 @@ package main
|
|||||||
import "github.com/iliadenisov/galaxy/client"
|
import "github.com/iliadenisov/galaxy/client"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
c := client.NewClient()
|
c := client.NewUI()
|
||||||
c.Run()
|
c.Run()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,38 +3,6 @@ package client
|
|||||||
/*
|
/*
|
||||||
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).
|
Fyne-friendly latest-wins coalescing for canvas.NewRaster(draw func(w,h int) image.Image).
|
||||||
|
|
||||||
How to use (integration sketch):
|
|
||||||
|
|
||||||
type editor struct {
|
|
||||||
w *world.World
|
|
||||||
drawer world.PrimitiveDrawer // wraps gg.Context over a backing *image.RGBA
|
|
||||||
raster *canvas.Raster
|
|
||||||
|
|
||||||
co *client.RasterCoalescer[world.RenderParams]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *editor) initCoalescer() {
|
|
||||||
exec := client.FyneMainThreadExecutor{} // uses fyne.CurrentApp().Driver().RunOnMain
|
|
||||||
e.co = client.NewRasterCoalescer(exec, e.raster, func(wPx, hPx int, p world.RenderParams) image.Image {
|
|
||||||
// your existing draw pipeline:
|
|
||||||
// 1) ensure viewport/margins inside p are consistent with wPx/hPx if needed
|
|
||||||
// 2) call e.w.Render(e.drawer, p)
|
|
||||||
// 3) get image from gg.Context, crop margins, return
|
|
||||||
_ = e.w.Render(e.drawer, p)
|
|
||||||
return e.drawerImageCropped() // your code
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call from input handlers (pan/zoom/etc). Can be from any goroutine.
|
|
||||||
func (e *editor) RefreshUI(p world.RenderParams) {
|
|
||||||
e.co.Request(p) // schedules raster.Refresh() on UI thread
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raster draw callback:
|
|
||||||
func (e *editor) draw(wPx, hPx int) image.Image {
|
|
||||||
return e.co.Draw(wPx, hPx)
|
|
||||||
}
|
|
||||||
|
|
||||||
Key property:
|
Key property:
|
||||||
- draw() renders at most once per invocation (never loops).
|
- draw() renders at most once per invocation (never loops).
|
||||||
- if new requests arrived while drawing, we schedule exactly one extra Refresh.
|
- if new requests arrived while drawing, we schedule exactly one extra Refresh.
|
||||||
|
|||||||
+145
-200
@@ -3,7 +3,6 @@ package client
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
|
||||||
"math"
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@@ -15,36 +14,42 @@ import (
|
|||||||
"github.com/iliadenisov/galaxy/client/world"
|
"github.com/iliadenisov/galaxy/client/world"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Editor interface {
|
type client struct {
|
||||||
BuildUI(fyne.Window)
|
world *world.World
|
||||||
}
|
drawer *world.GGDrawer
|
||||||
|
raster *canvas.Raster
|
||||||
|
co *RasterCoalescer[world.RenderParams]
|
||||||
|
pan *PanController
|
||||||
|
|
||||||
type editor struct {
|
// Protected camera/options state (UI-facing). This is the "base" params snapshot.
|
||||||
world *world.World
|
// Viewport/margins are NOT stored here; they come from raster draw callback.
|
||||||
drawer *world.GGDrawer
|
mu sync.RWMutex
|
||||||
raster *canvas.Raster
|
wp *world.RenderParams
|
||||||
canvasScale float32
|
canvasScale float32
|
||||||
|
|
||||||
win fyne.Window
|
// Latest raster geometry metadata for correct event->pixel conversion:
|
||||||
|
// - logical size: raster.Size() (Fyne units)
|
||||||
|
// - pixel size: last (wPx,hPx) passed to draw callback
|
||||||
|
metaMu sync.RWMutex
|
||||||
|
lastRasterLogicW float32
|
||||||
|
lastRasterLogicH float32
|
||||||
|
lastRasterPxW int
|
||||||
|
lastRasterPxH int
|
||||||
|
lastCanvasScale float32 // optional, useful for debugging
|
||||||
|
|
||||||
// Coalescer for latest-wins refresh scheduling.
|
// Snapshot of params actually used for the last render (includes viewport/margins).
|
||||||
co *RasterCoalescer[world.RenderParams]
|
// Used for HitTest and to keep UI interactions consistent with what the user sees.
|
||||||
|
lastRenderedMu sync.RWMutex
|
||||||
|
lastRenderedParams world.RenderParams
|
||||||
|
|
||||||
pan *PanController
|
// Indexing / backing-canvas caches (owned by client because it depends on UI geometry)
|
||||||
|
lastIndexedViewportW int
|
||||||
|
lastIndexedViewportH int
|
||||||
|
lastIndexedZoomFp int
|
||||||
|
|
||||||
// Protected render params state. Stored as value to avoid aliasing issues.
|
|
||||||
mu sync.RWMutex
|
|
||||||
wp *world.RenderParams
|
|
||||||
|
|
||||||
// Last viewport size we indexed the world for.
|
|
||||||
lastViewportW int
|
|
||||||
lastViewportH int
|
|
||||||
|
|
||||||
// Optional: you can keep the last expanded canvas size to avoid reallocations.
|
|
||||||
lastCanvasW int
|
lastCanvasW int
|
||||||
lastCanvasH int
|
lastCanvasH int
|
||||||
|
|
||||||
// Reusable viewport buffer to avoid per-frame allocations.
|
|
||||||
viewportImg *image.RGBA
|
viewportImg *image.RGBA
|
||||||
viewportW int
|
viewportW int
|
||||||
viewportH int
|
viewportH int
|
||||||
@@ -52,99 +57,160 @@ type editor struct {
|
|||||||
hits []world.Hit
|
hits []world.Hit
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editor) CanvasScale() float32 { return e.canvasScale }
|
func NewClient() *client {
|
||||||
|
e := &client{
|
||||||
|
world: nil,
|
||||||
|
wp: &world.RenderParams{
|
||||||
|
CameraZoom: 1.0,
|
||||||
|
Options: &world.RenderOptions{DisableWrapScroll: false},
|
||||||
|
},
|
||||||
|
lastCanvasScale: 1.0,
|
||||||
|
hits: make([]world.Hit, 5),
|
||||||
|
}
|
||||||
|
|
||||||
func (e *editor) ForceFullRedraw() {
|
e.drawer = &world.GGDrawer{DC: nil}
|
||||||
|
|
||||||
|
e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image {
|
||||||
|
return e.draw(wPx, hPx)
|
||||||
|
})
|
||||||
|
|
||||||
|
e.pan = NewPanController(e)
|
||||||
|
|
||||||
|
e.co = NewRasterCoalescer(
|
||||||
|
FyneExecutor{},
|
||||||
|
e.raster,
|
||||||
|
func(wPx, hPx int, p world.RenderParams) image.Image {
|
||||||
|
return e.renderRasterImage(wPx, hPx, p)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
e.RequestRefresh()
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) CanvasScale() float32 {
|
||||||
|
e.metaMu.RLock()
|
||||||
|
defer e.metaMu.RUnlock()
|
||||||
|
if e.lastCanvasScale <= 0 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return e.lastCanvasScale
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) ForceFullRedraw() {
|
||||||
|
if e.world == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
e.world.ForceFullRedrawNext()
|
e.world.ForceFullRedrawNext()
|
||||||
}
|
}
|
||||||
|
|
||||||
// здесь определяю, изменились ли границы raster, если да - обновляю размеры viewport, margin и корректирую zoom
|
func (e *client) onRasterWidgetLayout(fyne.Size) {
|
||||||
func (e *editor) updateSizes() {
|
e.updateSizes()
|
||||||
canvas := fyne.CurrentApp().Driver().CanvasForObject(e.raster)
|
}
|
||||||
if canvas == nil {
|
|
||||||
|
// updateSizes updates only metadata we need for event->pixel conversion and schedules a redraw.
|
||||||
|
// It must NOT try to compute pixel viewport sizes (those are known in raster draw callback).
|
||||||
|
func (e *client) updateSizes() {
|
||||||
|
canvasObj := fyne.CurrentApp().Driver().CanvasForObject(e.raster)
|
||||||
|
if canvasObj == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
size := e.raster.Size()
|
sz := e.raster.Size() // logical (Fyne units)
|
||||||
e.canvasScale = canvas.Scale()
|
scale := canvasObj.Scale()
|
||||||
|
|
||||||
width := int(size.Width * e.canvasScale)
|
e.metaMu.Lock()
|
||||||
height := int(size.Height * e.canvasScale)
|
e.lastRasterLogicW = sz.Width
|
||||||
|
e.lastRasterLogicH = sz.Height
|
||||||
|
e.lastCanvasScale = scale
|
||||||
|
e.metaMu.Unlock()
|
||||||
|
|
||||||
if width <= 0 || height <= 0 {
|
e.RequestRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) onDragged(ev *fyne.DragEvent) {
|
||||||
|
e.pan.Dragged(ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) onDradEnd() {
|
||||||
|
e.pan.DragEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *client) onTapped(ev *fyne.PointEvent) {
|
||||||
|
if e.world == nil || ev == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if width != e.wp.ViewportWidthPx || height != e.wp.ViewportHeightPx {
|
xPx, yPx, ok := e.eventPosToPixel(ev.Position.X, ev.Position.Y)
|
||||||
e.wp.ViewportWidthPx = width
|
if !ok {
|
||||||
e.wp.ViewportHeightPx = height
|
return
|
||||||
e.wp.MarginXPx = e.wp.ViewportWidthPx / 4
|
|
||||||
e.wp.MarginYPx = e.wp.ViewportHeightPx / 4
|
|
||||||
defer func() { e.co.Request(*e.wp) }()
|
|
||||||
}
|
}
|
||||||
if e.world != nil {
|
|
||||||
e.wp.CameraZoom = e.world.CorrectCameraZoom(e.wp.CameraZoom, e.wp.ViewportWidthPx, e.wp.ViewportHeightPx)
|
params := e.getLastRenderedParams()
|
||||||
e.world.IndexOnViewportChange(e.wp.ViewportWidthPx, e.wp.ViewportHeightPx, e.wp.CameraZoom)
|
hits, err := e.world.HitTest(e.hits, ¶ms, xPx, yPx)
|
||||||
e.world.ClampRenderParamsNoWrap(e.wp)
|
if err != nil {
|
||||||
|
// In UI you probably don't want panic; keep your existing handling.
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := func(v int) float64 { return float64(v) / float64(world.SCALE) }
|
||||||
|
|
||||||
|
for _, hit := range hits {
|
||||||
|
var coord string
|
||||||
|
if hit.Kind == world.KindLine {
|
||||||
|
coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
|
||||||
|
} else {
|
||||||
|
coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
|
||||||
|
}
|
||||||
|
fmt.Println("hit:", hit.ID, "Coord:", coord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editor) onScrolled(s *fyne.ScrollEvent) {
|
func (e *client) onScrolled(s *fyne.ScrollEvent) {
|
||||||
vw := e.wp.ViewportWidthPx
|
if e.world == nil || s == nil {
|
||||||
vh := e.wp.ViewportHeightPx
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use last rendered viewport sizes (pixel) for zoom logic.
|
||||||
|
e.metaMu.RLock()
|
||||||
|
vw := e.lastRasterPxW
|
||||||
|
vh := e.lastRasterPxH
|
||||||
|
e.metaMu.RUnlock()
|
||||||
if vw <= 0 || vh <= 0 {
|
if vw <= 0 || vh <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cursor position in viewport pixels (Fyne units -> px by multiplying).
|
cxPx, cyPx, ok := e.eventPosToPixel(s.Position.X, s.Position.Y)
|
||||||
cxPx := int(float32(s.Position.X) * e.canvasScale)
|
if !ok {
|
||||||
cyPx := int(float32(s.Position.Y) * e.canvasScale)
|
return
|
||||||
|
|
||||||
if cxPx < 0 {
|
|
||||||
cxPx = 0
|
|
||||||
} else if cxPx > vw {
|
|
||||||
cxPx = vw
|
|
||||||
}
|
|
||||||
if cyPx < 0 {
|
|
||||||
cyPx = 0
|
|
||||||
} else if cyPx > vh {
|
|
||||||
cyPx = vh
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
e.mu.Lock()
|
||||||
oldZoom := e.wp.CameraZoom
|
oldZoom := e.wp.CameraZoom
|
||||||
|
|
||||||
// Exponential zoom:
|
// Exponential zoom factor; tune later.
|
||||||
// - Each "notch" multiplies zoom by a fixed factor.
|
|
||||||
// - Using DY directly as exponent gives smooth trackpad behavior too.
|
|
||||||
//
|
|
||||||
// Tune base:
|
|
||||||
// base=1.10 => ~10% per wheel step (if DY≈1 per step)
|
|
||||||
// base=1.05 => ~5% per step
|
|
||||||
//
|
|
||||||
// In user settings, better store on percents userZoomLevelPercent = (0,100]: base = 1 + (userZoomLevelPercent) / 10
|
|
||||||
const base = 1.005
|
const base = 1.005
|
||||||
|
|
||||||
// Negative DY => zoom out, positive DY => zoom in (depending on platform settings).
|
|
||||||
// If you want inverted direction, negate float64(s.Scrolled.DY).
|
|
||||||
delta := float64(s.Scrolled.DY)
|
delta := float64(s.Scrolled.DY)
|
||||||
|
|
||||||
newZoom := oldZoom * math.Pow(base, delta)
|
newZoom := oldZoom * math.Pow(base, delta)
|
||||||
|
|
||||||
// Clamp/correct (min/max + prevent wrap if needed).
|
|
||||||
newZoom = e.world.CorrectCameraZoom(newZoom, vw, vh)
|
newZoom = e.world.CorrectCameraZoom(newZoom, vw, vh)
|
||||||
if newZoom == oldZoom {
|
if newZoom == oldZoom {
|
||||||
|
e.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oldZoomFp, err := world.CameraZoomToWorldFixed(oldZoom)
|
oldZoomFp, err := world.CameraZoomToWorldFixed(oldZoom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
newZoomFp, err := world.CameraZoomToWorldFixed(newZoom)
|
newZoomFp, err := world.CameraZoomToWorldFixed(newZoom)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.mu.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pivot zoom for no-wrap behavior.
|
||||||
newCamX, newCamY := world.PivotZoomCameraNoWrap(
|
newCamX, newCamY := world.PivotZoomCameraNoWrap(
|
||||||
e.wp.CameraXWorldFp, e.wp.CameraYWorldFp,
|
e.wp.CameraXWorldFp, e.wp.CameraYWorldFp,
|
||||||
vw, vh,
|
vw, vh,
|
||||||
@@ -155,56 +221,15 @@ func (e *editor) onScrolled(s *fyne.ScrollEvent) {
|
|||||||
e.wp.CameraZoom = newZoom
|
e.wp.CameraZoom = newZoom
|
||||||
e.wp.CameraXWorldFp = newCamX
|
e.wp.CameraXWorldFp = newCamX
|
||||||
e.wp.CameraYWorldFp = newCamY
|
e.wp.CameraYWorldFp = newCamY
|
||||||
|
e.mu.Unlock()
|
||||||
|
|
||||||
e.world.IndexOnViewportChange(vw, vh, e.wp.CameraZoom)
|
// Any zoom change should rebuild index and force full redraw.
|
||||||
|
|
||||||
// No-wrap clamp to avoid "detaching" from borders.
|
|
||||||
e.world.ClampRenderParamsNoWrap(e.wp)
|
|
||||||
|
|
||||||
// Zoom changes are best done as full redraw.
|
|
||||||
e.world.ForceFullRedrawNext()
|
e.world.ForceFullRedrawNext()
|
||||||
|
e.RequestRefresh()
|
||||||
e.co.Request(*e.wp)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editor) onDragged(ev *fyne.DragEvent) {
|
func (e *client) BuildUI(w fyne.Window) {
|
||||||
e.pan.Dragged(ev)
|
mapCanvas := newInteractiveRaster(e, e.raster, e.onRasterWidgetLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
|
||||||
}
|
|
||||||
|
|
||||||
func (e *editor) onDradEnd() {
|
|
||||||
e.pan.DragEnd()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *editor) onTapped(ev *fyne.PointEvent) {
|
|
||||||
if e.world == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hits, err := e.world.HitTest(e.hits, e.wp, int(ev.Position.X*e.canvasScale), int(ev.Position.Y*e.canvasScale))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
m := func(v int) float64 {
|
|
||||||
return float64(v) / float64(world.SCALE)
|
|
||||||
}
|
|
||||||
var coord string
|
|
||||||
for _, hit := range hits {
|
|
||||||
if hit.Kind == world.KindLine {
|
|
||||||
coord = fmt.Sprintf("{%f,%f - %f,%f}", m(hit.X1), m(hit.Y1), m(hit.X2), m(hit.Y2))
|
|
||||||
} else {
|
|
||||||
coord = fmt.Sprintf("{%f,%f}", m(hit.X), m(hit.Y))
|
|
||||||
}
|
|
||||||
fmt.Println("hit:", hit.ID, "Coord:", coord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *editor) onMapLayout(s fyne.Size) {
|
|
||||||
e.updateSizes()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *editor) BuildUI(w fyne.Window) {
|
|
||||||
e.win = w
|
|
||||||
|
|
||||||
mapCanvas := newInteractiveRaster(e, e.raster, e.onMapLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped)
|
|
||||||
mapCanvas.SetMinSize(fyne.NewSize(292, 292))
|
mapCanvas.SetMinSize(fyne.NewSize(292, 292))
|
||||||
|
|
||||||
toolbar := widget.NewToolbar(
|
toolbar := widget.NewToolbar(
|
||||||
@@ -244,7 +269,7 @@ func (e *editor) BuildUI(w fyne.Window) {
|
|||||||
w.SetContent(content)
|
w.SetContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editor) loadWorld(w *world.World) {
|
func (e *client) loadWorld(w *world.World) {
|
||||||
if e.world == nil {
|
if e.world == nil {
|
||||||
w.SetCircleRadiusScaleFp(world.SCALE / 4)
|
w.SetCircleRadiusScaleFp(world.SCALE / 4)
|
||||||
e.world = w
|
e.world = w
|
||||||
@@ -252,7 +277,6 @@ func (e *editor) loadWorld(w *world.World) {
|
|||||||
e.wp.CameraXWorldFp = w.W / 2
|
e.wp.CameraXWorldFp = w.W / 2
|
||||||
e.wp.CameraYWorldFp = w.H / 2
|
e.wp.CameraYWorldFp = w.H / 2
|
||||||
e.world.SetTheme(world.ThemeDark)
|
e.world.SetTheme(world.ThemeDark)
|
||||||
e.updateSizes()
|
|
||||||
} else {
|
} else {
|
||||||
if e.world.Theme().ID() == "theme.light.v1" {
|
if e.world.Theme().ID() == "theme.light.v1" {
|
||||||
e.world.SetTheme(world.ThemeDark)
|
e.world.SetTheme(world.ThemeDark)
|
||||||
@@ -262,82 +286,3 @@ func (e *editor) loadWorld(w *world.World) {
|
|||||||
}
|
}
|
||||||
e.RequestRefresh()
|
e.RequestRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEditor() *editor {
|
|
||||||
e := &editor{
|
|
||||||
world: nil,
|
|
||||||
wp: &world.RenderParams{
|
|
||||||
CameraZoom: 1.0,
|
|
||||||
Options: &world.RenderOptions{DisableWrapScroll: false},
|
|
||||||
},
|
|
||||||
canvasScale: 1.0,
|
|
||||||
hits: make([]world.Hit, 5),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a drawer with some initial context; real size will be adjusted on first draw.
|
|
||||||
e.drawer = &world.GGDrawer{DC: nil}
|
|
||||||
|
|
||||||
// Create raster; its draw callback delegates to coalescer.
|
|
||||||
e.raster = canvas.NewRaster(func(wPx, hPx int) image.Image {
|
|
||||||
return e.draw(wPx, hPx)
|
|
||||||
})
|
|
||||||
|
|
||||||
e.pan = NewPanController(e)
|
|
||||||
|
|
||||||
// Wire coalescer: it schedules raster.Refresh() on UI thread and renders once per draw call.
|
|
||||||
exec := FyneExecutor{}
|
|
||||||
e.co = NewRasterCoalescer(
|
|
||||||
exec,
|
|
||||||
e.raster, // Refresher
|
|
||||||
func(wPx, hPx int, p world.RenderParams) image.Image {
|
|
||||||
// This runs on UI thread (inside draw). It must return an image.
|
|
||||||
return e.renderRasterImage(wPx, hPx, p)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kick initial draw.
|
|
||||||
e.RequestRefresh()
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockWorld() *world.World {
|
|
||||||
w := world.NewWorld(300, 300)
|
|
||||||
mockWorldInit(w)
|
|
||||||
return w
|
|
||||||
}
|
|
||||||
|
|
||||||
func mockWorldInit(w *world.World) {
|
|
||||||
lineStyle := w.AddStyleLine(world.StyleOverride{
|
|
||||||
StrokeColor: color.RGBA{R: 0, G: 255, B: 0, A: 255},
|
|
||||||
StrokeWidthPx: new(3.0),
|
|
||||||
StrokeDashes: new([]float64{10.}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := w.AddCircle(150, 150, 50); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.AddCircle(150, 299, 30); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if _, err := w.AddCircle(299, 150, 30); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.AddLine(100, 20, 200, 30, world.LineWithStyleID(lineStyle), world.LineWithPriority(500)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.AddLine(50, 50, 250, 100); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.AddPoint(10, 10); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.AddPoint(25, 255); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/iliadenisov/galaxy/client/world"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mockWorld() *world.World {
|
||||||
|
w := world.NewWorld(300, 300)
|
||||||
|
mockWorldInit(w)
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockWorldInit(w *world.World) {
|
||||||
|
lineStyle := w.AddStyleLine(world.StyleOverride{
|
||||||
|
StrokeColor: color.RGBA{R: 0, G: 255, B: 0, A: 255},
|
||||||
|
StrokeWidthPx: new(3.0),
|
||||||
|
StrokeDashes: new([]float64{10.}),
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := w.AddCircle(150, 150, 50); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.AddCircle(150, 299, 30); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if _, err := w.AddCircle(299, 150, 30); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.AddLine(100, 20, 200, 30, world.LineWithStyleID(lineStyle), world.LineWithPriority(500)); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.AddLine(50, 50, 250, 100); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.AddPoint(10, 10); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.AddPoint(25, 255); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
-36
@@ -2,6 +2,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image"
|
"image"
|
||||||
|
"math"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"github.com/fogleman/gg"
|
"github.com/fogleman/gg"
|
||||||
@@ -39,13 +40,8 @@ func (FyneExecutor) Post(fn func()) {
|
|||||||
fyne.Do(fn)
|
fyne.Do(fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widget returns the fyne CanvasObject to add into your UI.
|
|
||||||
func (e *editor) Widget() fyne.CanvasObject {
|
|
||||||
return e.raster
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetParams returns a copy of current render params for external reads.
|
// GetParams returns a copy of current render params for external reads.
|
||||||
func (e *editor) GetParams() world.RenderParams {
|
func (e *client) GetParams() world.RenderParams {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
defer e.mu.RUnlock()
|
defer e.mu.RUnlock()
|
||||||
return *e.wp
|
return *e.wp
|
||||||
@@ -53,70 +49,83 @@ func (e *editor) GetParams() world.RenderParams {
|
|||||||
|
|
||||||
// UpdateParams applies a modification function to render params and schedules a refresh.
|
// UpdateParams applies a modification function to render params and schedules a refresh.
|
||||||
// This is a safe way to mutate camera/zoom from event handlers.
|
// This is a safe way to mutate camera/zoom from event handlers.
|
||||||
func (e *editor) UpdateParams(fn func(p *world.RenderParams)) {
|
func (e *client) UpdateParams(fn func(p *world.RenderParams)) {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
fn(e.wp)
|
fn(e.wp)
|
||||||
|
p := *e.wp
|
||||||
// IMPORTANT: clamp camera if no-wrap
|
|
||||||
e.world.ClampRenderParamsNoWrap(e.wp)
|
|
||||||
|
|
||||||
p := e.wp // snapshot
|
|
||||||
e.mu.Unlock()
|
e.mu.Unlock()
|
||||||
|
|
||||||
e.co.Request(*p)
|
e.co.Request(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RequestRefresh schedules a refresh with the current params snapshot.
|
// RequestRefresh schedules a refresh with the current params snapshot.
|
||||||
// Useful if you changed world objects and want to redraw.
|
// Useful if you changed world objects and want to redraw.
|
||||||
func (e *editor) RequestRefresh() {
|
func (e *client) RequestRefresh() {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
p := e.wp
|
p := *e.wp
|
||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
e.co.Request(*p)
|
e.co.Request(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
// draw is the raster callback. It must be cheap and must not block on multiple re-renders.
|
// draw is the raster callback. It must be cheap and must not block on multiple re-renders.
|
||||||
// It delegates coalescing + rendering decision to RasterCoalescer.
|
// It delegates coalescing + rendering decision to RasterCoalescer.
|
||||||
func (e *editor) draw(wPx, hPx int) image.Image {
|
func (e *client) draw(wPx, hPx int) image.Image {
|
||||||
return e.co.Draw(wPx, hPx)
|
return e.co.Draw(wPx, hPx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderRasterImage renders the expanded canvas into the GGDrawer backing image,
|
// renderRasterImage renders the expanded canvas into the GGDrawer backing image,
|
||||||
// then copies only the viewport ROI into a reusable viewport buffer and returns it.
|
// then copies only the viewport ROI into a reusable viewport buffer and returns it.
|
||||||
func (e *editor) renderRasterImage(viewportW, viewportH int, p world.RenderParams) image.Image {
|
func (e *client) renderRasterImage(viewportW, viewportH int, p world.RenderParams) image.Image {
|
||||||
if e.world == nil {
|
if e.world == nil {
|
||||||
return blankImage
|
return image.NewRGBA(image.Rect(0, 0, 0, 0))
|
||||||
}
|
}
|
||||||
// 1) Viewport sizes come from raster draw callback.
|
|
||||||
|
// Record current raster pixel size (used for event coordinate conversion).
|
||||||
|
e.metaMu.Lock()
|
||||||
|
e.lastRasterPxW = viewportW
|
||||||
|
e.lastRasterPxH = viewportH
|
||||||
|
e.metaMu.Unlock()
|
||||||
|
|
||||||
|
// Fill viewport/margins derived from draw callback.
|
||||||
p.ViewportWidthPx = viewportW
|
p.ViewportWidthPx = viewportW
|
||||||
p.ViewportHeightPx = viewportH
|
p.ViewportHeightPx = viewportH
|
||||||
p.MarginXPx = viewportW / 4
|
p.MarginXPx = viewportW / 4
|
||||||
p.MarginYPx = viewportH / 4
|
p.MarginYPx = viewportH / 4
|
||||||
|
|
||||||
// 2) Ensure indexing is up-to-date when viewport size changed.
|
// Correct zoom for viewport/world constraints, and clamp camera for no-wrap.
|
||||||
if viewportW != e.lastViewportW || viewportH != e.lastViewportH {
|
p.CameraZoom = e.world.CorrectCameraZoom(p.CameraZoom, viewportW, viewportH)
|
||||||
e.world.IndexOnViewportChange(viewportW, viewportH, p.CameraZoom)
|
|
||||||
e.lastViewportW = viewportW
|
// Ensure indexing is up-to-date when viewport size or zoom changes.
|
||||||
e.lastViewportH = viewportH
|
zoomFp, err := p.CameraZoomFp()
|
||||||
|
if err == nil {
|
||||||
|
if viewportW != e.lastIndexedViewportW || viewportH != e.lastIndexedViewportH || zoomFp != e.lastIndexedZoomFp {
|
||||||
|
e.world.IndexOnViewportChange(viewportW, viewportH, p.CameraZoom)
|
||||||
|
e.lastIndexedViewportW = viewportW
|
||||||
|
e.lastIndexedViewportH = viewportH
|
||||||
|
e.lastIndexedZoomFp = zoomFp
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Ensure GG backing canvas is sized for expanded canvas.
|
e.world.ClampRenderParamsNoWrap(&p)
|
||||||
|
|
||||||
|
// Ensure backing expanded canvas (gg context) is sized properly.
|
||||||
canvasW := p.CanvasWidthPx()
|
canvasW := p.CanvasWidthPx()
|
||||||
canvasH := p.CanvasHeightPx()
|
canvasH := p.CanvasHeightPx()
|
||||||
e.ensureDrawerCanvas(canvasW, canvasH)
|
e.ensureDrawerCanvas(canvasW, canvasH)
|
||||||
|
|
||||||
// Savety clamp
|
// Render into expanded canvas backing.
|
||||||
e.world.ClampRenderParamsNoWrap(&p)
|
_ = e.world.Render(e.drawer, p) // TODO: handle error
|
||||||
|
|
||||||
// 4) Render into expanded canvas backing (full or incremental is decided inside world.Render).
|
// Save snapshot of params actually used for this render (for HitTest consistency).
|
||||||
_ = e.world.Render(e.drawer, p) // handle error in your real code
|
e.lastRenderedMu.Lock()
|
||||||
|
e.lastRenderedParams = p
|
||||||
|
e.lastRenderedMu.Unlock()
|
||||||
|
|
||||||
// 5) Copy viewport ROI into reusable viewport buffer and return it.
|
// Copy viewport ROI into reusable viewport buffer and return it.
|
||||||
e.ensureViewportBuffer(viewportW, viewportH)
|
e.ensureViewportBuffer(viewportW, viewportH)
|
||||||
|
|
||||||
src, ok := e.drawer.DC.Image().(*image.RGBA)
|
src, ok := e.drawer.DC.Image().(*image.RGBA)
|
||||||
if !ok || src == nil {
|
if !ok || src == nil {
|
||||||
// Should not happen if GGDrawer is backed by RGBA.
|
|
||||||
return image.NewRGBA(image.Rect(0, 0, viewportW, viewportH))
|
return image.NewRGBA(image.Rect(0, 0, viewportW, viewportH))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +134,7 @@ func (e *editor) renderRasterImage(viewportW, viewportH int, p world.RenderParam
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ensureDrawerCanvas ensures drawer has a gg.Context sized to canvasW x canvasH.
|
// ensureDrawerCanvas ensures drawer has a gg.Context sized to canvasW x canvasH.
|
||||||
func (e *editor) ensureDrawerCanvas(canvasW, canvasH int) {
|
func (e *client) ensureDrawerCanvas(canvasW, canvasH int) {
|
||||||
if e.drawer.DC != nil && e.lastCanvasW == canvasW && e.lastCanvasH == canvasH {
|
if e.drawer.DC != nil && e.lastCanvasW == canvasW && e.lastCanvasH == canvasH {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -133,11 +142,9 @@ func (e *editor) ensureDrawerCanvas(canvasW, canvasH int) {
|
|||||||
e.drawer.DC = NewGGContextRGBA(canvasW, canvasH)
|
e.drawer.DC = NewGGContextRGBA(canvasW, canvasH)
|
||||||
e.lastCanvasW = canvasW
|
e.lastCanvasW = canvasW
|
||||||
e.lastCanvasH = canvasH
|
e.lastCanvasH = canvasH
|
||||||
// e.wp.CameraXWorldFp = e.wp.ViewportWidthPx / 2 * world.SCALE
|
|
||||||
// e.wp.CameraYWorldFp = e.wp.ViewportHeightPx / 2 * world.SCALE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *editor) ensureViewportBuffer(w, h int) {
|
func (e *client) ensureViewportBuffer(w, h int) {
|
||||||
if e.viewportImg != nil && e.viewportW == w && e.viewportH == h {
|
if e.viewportImg != nil && e.viewportW == w && e.viewportH == h {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -146,6 +153,45 @@ func (e *editor) ensureViewportBuffer(w, h int) {
|
|||||||
e.viewportH = h
|
e.viewportH = h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *client) getLastRenderedParams() world.RenderParams {
|
||||||
|
e.lastRenderedMu.RLock()
|
||||||
|
defer e.lastRenderedMu.RUnlock()
|
||||||
|
return e.lastRenderedParams
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventPosToPixel converts event logical coordinates (Fyne units) into pixel coordinates,
|
||||||
|
// using the last known raster logical size and the last draw callback pixel size.
|
||||||
|
//
|
||||||
|
// pixelX = floor(eventX * rasterPixelWidth / rasterLogicalWidth)
|
||||||
|
func (e *client) eventPosToPixel(eventX, eventY float32) (xPx, yPx int, ok bool) {
|
||||||
|
e.metaMu.RLock()
|
||||||
|
logW := e.lastRasterLogicW
|
||||||
|
logH := e.lastRasterLogicH
|
||||||
|
pxW := e.lastRasterPxW
|
||||||
|
pxH := e.lastRasterPxH
|
||||||
|
e.metaMu.RUnlock()
|
||||||
|
|
||||||
|
if logW <= 0 || logH <= 0 || pxW <= 0 || pxH <= 0 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
x := int(math.Floor(float64(eventX) * float64(pxW) / float64(logW)))
|
||||||
|
y := int(math.Floor(float64(eventY) * float64(pxH) / float64(logH)))
|
||||||
|
|
||||||
|
// Clamp to viewport bounds.
|
||||||
|
if x < 0 {
|
||||||
|
x = 0
|
||||||
|
} else if x > pxW {
|
||||||
|
x = pxW
|
||||||
|
}
|
||||||
|
if y < 0 {
|
||||||
|
y = 0
|
||||||
|
} else if y > pxH {
|
||||||
|
y = pxH
|
||||||
|
}
|
||||||
|
return x, y, true
|
||||||
|
}
|
||||||
|
|
||||||
// copyViewportRGBA copies a viewport rectangle from src RGBA into dst RGBA.
|
// copyViewportRGBA copies a viewport rectangle from src RGBA into dst RGBA.
|
||||||
// dst must be sized exactly (0,0)-(vw,vh). This is allocation-free.
|
// dst must be sized exactly (0,0)-(vw,vh). This is allocation-free.
|
||||||
// It avoids SubImage aliasing issues: dst becomes independent from src backing memory.
|
// It avoids SubImage aliasing issues: dst becomes independent from src backing memory.
|
||||||
|
|||||||
+7
-7
@@ -9,10 +9,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Editor pan integration for Fyne Draggable:
|
Client pan integration for Fyne Draggable:
|
||||||
|
|
||||||
- DragEvent provides absolute coordinates in "Fyne units".
|
- DragEvent provides absolute coordinates in "Fyne units".
|
||||||
- Editor knows canvasScale (Fyne units per pixel) and converts to pixels.
|
- Client knows canvasScale (Fyne units per pixel) and converts to pixels.
|
||||||
- We keep last drag position and compute dx/dy ourselves.
|
- We keep last drag position and compute dx/dy ourselves.
|
||||||
- We update camera center in world-fixed (CameraXWorldFp/YWorldFp).
|
- We update camera center in world-fixed (CameraXWorldFp/YWorldFp).
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ Sign convention (map follows pointer):
|
|||||||
- Drag down (dyPx > 0): move world content down => move camera up => CameraYWorldFp -= dyWorldFp
|
- Drag down (dyPx > 0): move world content down => move camera up => CameraYWorldFp -= dyWorldFp
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// draggableEditor is the minimal interface we need from your editor implementation.
|
// draggableClient is the minimal interface we need from your client implementation.
|
||||||
// If your Editor already has these methods/fields, you can fold the code directly into it.
|
// If your Client already has these methods/fields, you can fold the code directly into it.
|
||||||
type draggableEditor interface {
|
type draggableClient interface {
|
||||||
// CanvasScale returns the fyne-units-per-pixel scale factor.
|
// CanvasScale returns the fyne-units-per-pixel scale factor.
|
||||||
CanvasScale() float32
|
CanvasScale() float32
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ type draggableEditor interface {
|
|||||||
|
|
||||||
// PanController holds per-drag transient state.
|
// PanController holds per-drag transient state.
|
||||||
type PanController struct {
|
type PanController struct {
|
||||||
ed draggableEditor
|
ed draggableClient
|
||||||
|
|
||||||
dragging bool
|
dragging bool
|
||||||
lastFx float32 // last absolute position in Fyne units
|
lastFx float32 // last absolute position in Fyne units
|
||||||
@@ -50,7 +50,7 @@ type PanController struct {
|
|||||||
remPxY float32
|
remPxY float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPanController(ed draggableEditor) *PanController {
|
func NewPanController(ed draggableClient) *PanController {
|
||||||
return &PanController{ed: ed}
|
return &PanController{ed: ed}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/iliadenisov/galaxy/client/world"
|
"github.com/iliadenisov/galaxy/client/world"
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeEditor struct {
|
type fakeClient struct {
|
||||||
scale float32
|
scale float32
|
||||||
p world.RenderParams
|
p world.RenderParams
|
||||||
|
|
||||||
@@ -20,21 +20,21 @@ type fakeEditor struct {
|
|||||||
refresh int
|
refresh int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *fakeEditor) CanvasScale() float32 { return e.scale }
|
func (e *fakeClient) CanvasScale() float32 { return e.scale }
|
||||||
|
|
||||||
func (e *fakeEditor) UpdateParams(fn func(p *world.RenderParams)) {
|
func (e *fakeClient) UpdateParams(fn func(p *world.RenderParams)) {
|
||||||
fn(&e.p)
|
fn(&e.p)
|
||||||
e.updates++
|
e.updates++
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *fakeEditor) RequestRefresh() { e.refresh++ }
|
func (e *fakeClient) RequestRefresh() { e.refresh++ }
|
||||||
|
|
||||||
func (e *fakeEditor) ForceFullRedraw() { e.forced = true }
|
func (e *fakeClient) ForceFullRedraw() { e.forced = true }
|
||||||
|
|
||||||
func TestPanController_DraggedUpdatesCameraByDeltaPx(t *testing.T) {
|
func TestPanController_DraggedUpdatesCameraByDeltaPx(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fe := &fakeEditor{
|
fe := &fakeClient{
|
||||||
scale: 1.0, // 1 fyne unit == 1 px for the test
|
scale: 1.0, // 1 fyne unit == 1 px for the test
|
||||||
p: world.RenderParams{
|
p: world.RenderParams{
|
||||||
CameraZoom: 1.0,
|
CameraZoom: 1.0,
|
||||||
@@ -60,7 +60,7 @@ func TestPanController_DraggedUpdatesCameraByDeltaPx(t *testing.T) {
|
|||||||
func TestPanController_DraggedUsesCanvasScaleByMultiplying(t *testing.T) {
|
func TestPanController_DraggedUsesCanvasScaleByMultiplying(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fe := &fakeEditor{
|
fe := &fakeClient{
|
||||||
scale: 2.0, // 2 px per fyne unit
|
scale: 2.0, // 2 px per fyne unit
|
||||||
p: world.RenderParams{
|
p: world.RenderParams{
|
||||||
CameraZoom: 1.0,
|
CameraZoom: 1.0,
|
||||||
@@ -82,7 +82,7 @@ func TestPanController_DraggedUsesCanvasScaleByMultiplying(t *testing.T) {
|
|||||||
func TestPanController_DragEndForcesFullRedrawAndRefresh(t *testing.T) {
|
func TestPanController_DragEndForcesFullRedrawAndRefresh(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fe := &fakeEditor{
|
fe := &fakeClient{
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
p: world.RenderParams{
|
p: world.RenderParams{
|
||||||
CameraZoom: 1.0,
|
CameraZoom: 1.0,
|
||||||
|
|||||||
Reference in New Issue
Block a user