diff --git a/client/client.go b/client/client.go index ecdebf6..a7b0bf2 100644 --- a/client/client.go +++ b/client/client.go @@ -14,10 +14,6 @@ func NewClient() *client { c := &client{} c.app = app.New() c.window = c.app.NewWindow("Galaxy+") - - // https://github.com/fyne-io/fyne/issues/418 - interactive raster - // https://github.com/fyne-io/fyne/issues/224 - resize - editor := NewEditor() editor.BuildUI(c.window) diff --git a/client/editor.go b/client/editor.go index 6162195..6708f7f 100644 --- a/client/editor.go +++ b/client/editor.go @@ -10,7 +10,8 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" "github.com/iliadenisov/galaxy/client/world" ) @@ -24,8 +25,7 @@ type editor struct { raster *canvas.Raster canvasScale float32 - canvas *interactiveRaster - win fyne.Window + win fyne.Window // Coalescer for latest-wins refresh scheduling. co *RasterCoalescer[world.RenderParams] @@ -58,10 +58,6 @@ func (e *editor) ForceFullRedraw() { e.world.ForceFullRedrawNext() } -func (e *editor) buildUI() fyne.CanvasObject { - return e.canvas -} - // здесь определяю, изменились ли границы raster, если да - обновляю размеры viewport, margin и корректирую zoom func (e *editor) updateSizes() { canvas := fyne.CurrentApp().Driver().CanvasForObject(e.raster) @@ -75,16 +71,21 @@ func (e *editor) updateSizes() { width := int(size.Width * e.canvasScale) height := int(size.Height * e.canvasScale) - if width > 0 && height > 0 && (width != e.wp.ViewportWidthPx || height != e.wp.ViewportHeightPx) { + if width <= 0 || height <= 0 { + return + } + + if width != e.wp.ViewportWidthPx || height != e.wp.ViewportHeightPx { e.wp.ViewportWidthPx = width e.wp.ViewportHeightPx = height 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) e.world.IndexOnViewportChange(e.wp.ViewportWidthPx, e.wp.ViewportHeightPx, e.wp.CameraZoom) e.world.ClampRenderParamsNoWrap(e.wp) - e.co.Request(*e.wp) } } @@ -175,6 +176,9 @@ func (e *editor) onDradEnd() { } 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) @@ -193,34 +197,68 @@ func (e *editor) onTapped(ev *fyne.PointEvent) { } } -func (e *editor) InitImage() { - s := fyne.NewSize(292, 292) - e.canvas.SetMinSize(s) - e.updateSizes() -} - func (e *editor) onMapLayout(s fyne.Size) { e.updateSizes() } func (e *editor) BuildUI(w fyne.Window) { e.win = w - content := container.New(layout.NewStackLayout(), e.buildUI()) + + mapCanvas := newInteractiveRaster(e, e.raster, e.onMapLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped) + mapCanvas.SetMinSize(fyne.NewSize(292, 292)) + + toolbar := widget.NewToolbar( + widget.NewToolbarAction( + theme.FolderIcon(), + func() { e.loadWorld(mockWorld()) }), + widget.NewToolbarSeparator(), + widget.NewToolbarAction( + theme.NavigateBackIcon(), + func() {}), + widget.NewToolbarAction( + theme.NavigateNextIcon(), + func() {}), + ) + + tabs := container.NewAppTabs( + container.NewTabItemWithIcon( + "Map", + theme.GridIcon(), + mapCanvas), + container.NewTabItemWithIcon( + "Calculator", + theme.ComputerIcon(), + container.NewStack(widget.NewButton("Calc", func() {})), + ), + ) + + content := container.NewBorder( + toolbar, // top + nil, // bottom + nil, // left + nil, // right + tabs, // center + ) + w.CenterOnScreen() w.SetContent(content) } +func (e *editor) loadWorld(w *world.World) { + e.world = w + // TODO: store camera position in user settings + e.wp.CameraXWorldFp = w.W / 2 + e.wp.CameraYWorldFp = w.H / 2 + e.updateSizes() + e.RequestRefresh() +} + func NewEditor() *editor { - w := world.NewWorld(300, 300) - testWorldInit(w) e := &editor{ - world: w, + world: nil, wp: &world.RenderParams{ - CameraZoom: 1.0, - CameraXWorldFp: w.W / 2, - CameraYWorldFp: w.H / 2, - // Viewport sizes and margins will be filled from draw(w,h). - Options: &world.RenderOptions{DisableWrapScroll: false}, + CameraZoom: 1.0, + Options: &world.RenderOptions{DisableWrapScroll: false}, }, canvasScale: 1.0, hits: make([]world.Hit, 5), @@ -234,7 +272,6 @@ func NewEditor() *editor { return e.draw(wPx, hPx) }) - e.canvas = newInteractiveRaster(e, e.raster, e.onMapLayout, e.onScrolled, e.onDragged, e.onDradEnd, e.onTapped) e.pan = NewPanController(e) // Wire coalescer: it schedules raster.Refresh() on UI thread and renders once per draw call. @@ -251,11 +288,16 @@ func NewEditor() *editor { // Kick initial draw. e.RequestRefresh() - e.InitImage() return e } -func testWorldInit(w *world.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), @@ -283,10 +325,6 @@ func testWorldInit(w *world.World) { panic(err) } - // if _, err := w.AddLine(100, 20, 200, 30); err != nil { - // panic(err) - // } - if _, err := w.AddLine(100, 20, 200, 30, world.LineWithStyleID(lineStyle), world.LineWithPriority(500)); err != nil { panic(err) } diff --git a/client/ui.go b/client/ui.go index 979d351..b76bd66 100644 --- a/client/ui.go +++ b/client/ui.go @@ -4,8 +4,6 @@ import ( "image" "fyne.io/fyne/v2" - // adjust import path to your world package - // "your/module/world" "github.com/fogleman/gg" "github.com/iliadenisov/galaxy/client/world" ) @@ -30,6 +28,10 @@ This adapter enforces: - IndexOnViewportChange is called when viewport sizes changed (you can also include zoom if desired) */ +var ( + blankImage = image.NewRGBA(image.Rect(0, 0, 0, 0)) +) + // FyneExecutor posts functions onto the Fyne UI thread. type FyneExecutor struct{} @@ -76,25 +78,15 @@ func (e *editor) RequestRefresh() { // draw is the raster callback. It must be cheap and must not block on multiple re-renders. // It delegates coalescing + rendering decision to RasterCoalescer. func (e *editor) draw(wPx, hPx int) image.Image { - // Snapshot latest params and render once. - // e.mu.RLock() - // p := e.wp - // e.mu.RUnlock() - - // Request() already scheduled refreshes; Draw() actually renders for this callback. - // We bypass co.Draw(w,h) because we need to pass our snapshot to coalescer in a controlled way. - // The simplest pattern: keep coalescer as the sole driver: call co.Draw(w,h) here. - // But then coalescer uses its internal latest. So make sure we always call co.Request on updates. - // - // In normal operation you can just: return e.co.Draw(wPx,hPx) - // and never use p above. We'll do that to keep a single source of truth. - // _ = p return e.co.Draw(wPx, hPx) } // renderRasterImage renders the expanded canvas into the GGDrawer backing image, // 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 { + if e.world == nil { + return blankImage + } // 1) Viewport sizes come from raster draw callback. p.ViewportWidthPx = viewportW p.ViewportHeightPx = viewportH diff --git a/client/util.go b/client/util.go deleted file mode 100644 index 44902b8..0000000 --- a/client/util.go +++ /dev/null @@ -1,14 +0,0 @@ -package client - -import ( - "fmt" - - "fyne.io/fyne/v2" -) - -func PrintSize(c fyne.Canvas) { - if c == nil { - return - } - fmt.Println(c.Size()) -} diff --git a/client/widget.go b/client/widget.go index 49b7e17..095916b 100644 --- a/client/widget.go +++ b/client/widget.go @@ -20,7 +20,6 @@ func (r *rasterWidgetRender) Layout(size fyne.Size) { if r.onLayout != nil { r.onLayout(size) } - // fmt.Println("widget layout:", size.Width, size.Height, "raster:", r.canvas.raster.Size()) } func (r *rasterWidgetRender) MinSize() fyne.Size {