diff --git a/balance/numbers.go b/balance/numbers.go index 31c1c8f..49f50af 100644 --- a/balance/numbers.go +++ b/balance/numbers.go @@ -10,7 +10,7 @@ var ( CanvasScrollSpeed int32 = 8 // Default chunk size for canvases. - ChunkSize = 1000 + ChunkSize = 100 // Default size for a new Doodad. DoodadSize = 100 diff --git a/editor_ui.go b/editor_ui.go index 3f43411..45bbd1b 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -20,9 +20,11 @@ type EditorUI struct { Scene *EditorScene // Variables + StatusBoxes []*string StatusMouseText string StatusPaletteText string StatusFilenameText string + StatusScrollText string selectedSwatch string // name of selected swatch in palette selectedDoodad string @@ -51,6 +53,15 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI { StatusMouseText: "Cursor: (waiting)", StatusPaletteText: "Swatch: ", StatusFilenameText: "Filename: ", + StatusScrollText: "Hello world", + } + + // Bind the StatusBoxes arrays to the text variables. + u.StatusBoxes = []*string{ + &u.StatusMouseText, + &u.StatusPaletteText, + &u.StatusFilenameText, + &u.StatusScrollText, } u.Canvas = u.SetupCanvas(d) @@ -84,6 +95,10 @@ func (u *EditorUI) Loop(ev *events.State) { u.StatusPaletteText = fmt.Sprintf("Swatch: %s", u.Canvas.Palette.ActiveSwatch, ) + u.StatusScrollText = fmt.Sprintf("Scroll: %s Viewport: %s", + u.Canvas.Scroll, + u.Canvas.Viewport(), + ) // Statusbar filename label. filename := "untitled.map" @@ -142,6 +157,7 @@ func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame { // SetupCanvas configures the main drawing canvas in the editor. func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { drawing := uix.NewCanvas(balance.ChunkSize, true) + drawing.Name = "edit-canvas" drawing.Palette = level.DefaultPalette() if len(drawing.Palette.Swatches) > 0 { drawing.SetSwatch(drawing.Palette.Swatches[0]) @@ -367,6 +383,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { } can := uix.NewCanvas(int(buttonSize), true) + can.Name = filename can.LoadDoodad(doodad) btn := ui.NewRadioButton(filename, &u.selectedDoodad, si, can) btn.Resize(render.NewRect( @@ -452,38 +469,23 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { BorderSize: 1, } - cursorLabel := ui.NewLabel(ui.Label{ - TextVariable: &u.StatusMouseText, - Font: balance.StatusFont, - }) - cursorLabel.Configure(style) - cursorLabel.Compute(d.Engine) - frame.Pack(cursorLabel, ui.Pack{ - Anchor: ui.W, - PadX: 1, - }) + var labelHeight int32 + for _, variable := range u.StatusBoxes { + label := ui.NewLabel(ui.Label{ + TextVariable: variable, + Font: balance.StatusFont, + }) + label.Configure(style) + label.Compute(d.Engine) + frame.Pack(label, ui.Pack{ + Anchor: ui.W, + PadX: 1, + }) - paletteLabel := ui.NewLabel(ui.Label{ - TextVariable: &u.StatusPaletteText, - Font: balance.StatusFont, - }) - paletteLabel.Configure(style) - paletteLabel.Compute(d.Engine) - frame.Pack(paletteLabel, ui.Pack{ - Anchor: ui.W, - PadX: 1, - }) - - filenameLabel := ui.NewLabel(ui.Label{ - TextVariable: &u.StatusFilenameText, - Font: balance.StatusFont, - }) - filenameLabel.Configure(style) - filenameLabel.Compute(d.Engine) - frame.Pack(filenameLabel, ui.Pack{ - Anchor: ui.E, - PadX: 1, - }) + if labelHeight == 0 { + labelHeight = label.BoxSize().H + } + } // TODO: right-aligned labels clip out of bounds extraLabel := ui.NewLabel(ui.Label{ @@ -503,7 +505,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { frame.Resize(render.Rect{ W: d.width, - H: cursorLabel.BoxSize().H + frame.BoxThickness(1), + H: labelHeight + frame.BoxThickness(1), }) frame.Compute(d.Engine) frame.MoveTo(render.Point{ diff --git a/guitest_scene.go b/guitest_scene.go index 63610d8..2e42db5 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -151,6 +151,7 @@ func (s *GUITestScene) Setup(d *Doodle) error { }) } + // Main frame widgets. frame.Pack(ui.NewLabel(ui.Label{ Text: "Hello World!", Font: render.Text{ @@ -174,6 +175,17 @@ func (s *GUITestScene) Setup(d *Doodle) error { Padding: 4, }) cb.Supervise(s.Supervisor) + + // Put an image in. + img, err := ui.OpenImage(d.Engine, "exit.bmp") + if err != nil { + log.Error(err.Error()) + } + frame.Pack(img, ui.Pack{ + Anchor: ui.NE, + Padding: 4, + }) + frame.Pack(ui.NewLabel(ui.Label{ Text: "Like Tk!", Font: render.Text{ diff --git a/level/chunk.go b/level/chunk.go index fa37431..efd80e2 100644 --- a/level/chunk.go +++ b/level/chunk.go @@ -3,8 +3,12 @@ package level import ( "encoding/json" "fmt" + "image" + "math" + "os" "git.kirsle.net/apps/doodle/render" + "golang.org/x/image/bmp" ) // Types of chunks. @@ -17,6 +21,14 @@ const ( type Chunk struct { Type int // map vs. 2D array. Accessor + + // Values told to it from higher up, not stored in JSON. + Point render.Point + Size int + + // Texture cache properties so we don't redraw pixel-by-pixel every frame. + texture render.Texturer + dirty bool } // JSONChunk holds a lightweight (interface-free) copy of the Chunk for @@ -48,6 +60,134 @@ func NewChunk() *Chunk { } } +// Texture will return a cached texture for the rendering engine for this +// chunk's pixel data. If the cache is dirty it will be rebuilt in this func. +func (c *Chunk) Texture(e render.Engine, name string) render.Texturer { + if c.texture == nil || c.dirty { + err := c.ToBitmap("/tmp/" + name + ".bmp") + if err != nil { + log.Error("Texture: %s", err) + } + + tex, err := e.NewBitmap("/tmp/" + name + ".bmp") + if err != nil { + log.Error("Texture: %s", err) + } + + c.texture = tex + c.dirty = false + } + return c.texture +} + +// ToBitmap exports the chunk's pixels as a bitmap image. +func (c *Chunk) ToBitmap(filename string) error { + canvas := c.SizePositive() + imgSize := image.Rectangle{ + Min: image.Point{}, + Max: image.Point{ + X: c.Size, + Y: c.Size, + }, + } + + if imgSize.Max.X == 0 { + imgSize.Max.X = int(canvas.W) + } + if imgSize.Max.Y == 0 { + imgSize.Max.Y = int(canvas.H) + } + + img := image.NewRGBA(imgSize) + + // Blank out the pixels. + for x := 0; x < img.Bounds().Max.X; x++ { + for y := 0; y < img.Bounds().Max.Y; y++ { + img.Set(x, y, render.RGBA(255, 255, 0, 153).ToColor()) + } + } + + // Pixel coordinate offset to map the Chunk World Position to the + // smaller image boundaries. + pointOffset := render.Point{ + X: int32(math.Abs(float64(c.Point.X * int32(c.Size)))), + Y: int32(math.Abs(float64(c.Point.Y * int32(c.Size)))), + } + + // Blot all the pixels onto it. + for px := range c.Iter() { + img.Set( + int(px.X-pointOffset.X), + int(px.Y-pointOffset.Y), + px.Swatch.Color.ToColor(), + ) + } + + fh, err := os.Create(filename) + if err != nil { + return err + } + defer fh.Close() + + return bmp.Encode(fh, img) +} + +// Set proxies to the accessor and flags the texture as dirty. +func (c *Chunk) Set(p render.Point, sw *Swatch) error { + c.dirty = true + return c.Accessor.Set(p, sw) +} + +// Delete proxies to the accessor and flags the texture as dirty. +func (c *Chunk) Delete(p render.Point) error { + c.dirty = true + return c.Accessor.Delete(p) +} + +// Rect returns the bounding coordinates that the Chunk has pixels for. +func (c *Chunk) Rect() render.Rect { + // Lowest and highest chunks. + var ( + lowest render.Point + highest render.Point + ) + + for coord := range c.Iter() { + if coord.X < lowest.X { + lowest.X = coord.X + } + if coord.Y < lowest.Y { + lowest.Y = coord.Y + } + + if coord.X > highest.X { + highest.X = coord.X + } + if coord.Y > highest.Y { + highest.Y = coord.Y + } + } + + return render.Rect{ + X: lowest.X, + Y: lowest.Y, + W: highest.X, + H: highest.Y, + } +} + +// SizePositive returns the Size anchored to 0,0 with only positive +// coordinates. +func (c *Chunk) SizePositive() render.Rect { + S := c.Rect() + return render.Rect{ + X: c.Point.X * int32(c.Size), + Y: c.Point.Y * int32(c.Size), + W: int32(math.Abs(float64(S.X))) + S.W, + H: int32(math.Abs(float64(S.Y))) + S.H, + } +} + // Usage returns the percent of free space vs. allocated pixels in the chunk. func (c *Chunk) Usage(size int) float64 { return float64(c.Len()) / float64(size) diff --git a/level/chunker.go b/level/chunker.go index 501d529..316fafa 100644 --- a/level/chunker.go +++ b/level/chunker.go @@ -30,6 +30,8 @@ func NewChunker(size int) *Chunker { func (c *Chunker) Inflate(pal *Palette) error { for coord, chunk := range c.Chunks { log.Debug("Chunker.Inflate: expanding chunk %s", coord) + chunk.Point = coord + chunk.Size = c.Size chunk.Inflate(pal) } return nil @@ -66,6 +68,58 @@ func (c *Chunker) IterViewport(viewport render.Rect) <-chan Pixel { return pipe } +// IterViewportChunks returns a channel to iterate over the Chunk objects that +// appear within the viewport rect, instead of the pixels in each chunk. +func (c *Chunker) IterViewportChunks(viewport render.Rect) <-chan render.Point { + pipe := make(chan render.Point) + go func() { + sent := make(map[render.Point]interface{}) + for x := viewport.X; x < viewport.W; x += int32(c.Size / 4) { + for y := viewport.Y; y < viewport.H; y += int32(c.Size / 4) { + + // Constrain this chunksize step to a point within the bounds + // of the viewport. This can yield partial chunks on the edges + // of the viewport. + point := render.NewPoint(x, y) + if point.X < viewport.X { + point.X = viewport.X + } else if point.X > viewport.X+viewport.W { + point.X = viewport.X + viewport.W + } + if point.Y < viewport.Y { + point.Y = viewport.Y + } else if point.Y > viewport.Y+viewport.H { + point.Y = viewport.Y + viewport.H + } + + // Translate to a chunk coordinate, dedupe and send it. + coord := c.ChunkCoordinate(render.NewPoint(x, y)) + // fmt.Printf("IterViewportChunks: x=%d y=%d chunk=%s\n", x, y, coord) + if _, ok := sent[coord]; ok { + continue + } + sent[coord] = nil + + if _, ok := c.GetChunk(coord); ok { + fmt.Printf("Iter: send chunk %s for point %s\n", coord, point) + pipe <- coord + } + } + } + + // for cx := topLeft.X; cx <= bottomRight.X; cx++ { + // for cy := topLeft.Y; cy <= bottomRight.Y; cy++ { + // pt := render.NewPoint(cx, cy) + // if _, ok := c.GetChunk(pt); ok { + // pipe <- pt + // } + // } + // } + close(pipe) + }() + return pipe +} + // IterPixels returns a channel to iterate over every pixel in the entire // chunker. func (c *Chunker) IterPixels() <-chan Pixel { @@ -152,6 +206,8 @@ func (c *Chunker) Set(p render.Point, sw *Swatch) error { if !ok { chunk = NewChunk() c.Chunks[coord] = chunk + chunk.Point = coord + chunk.Size = c.Size } return chunk.Set(p, sw) diff --git a/level/chunker_test.go b/level/chunker_test.go index 8d02fb6..d417a3d 100644 --- a/level/chunker_test.go +++ b/level/chunker_test.go @@ -1,6 +1,7 @@ package level_test import ( + "fmt" "testing" "git.kirsle.net/apps/doodle/level" @@ -81,5 +82,149 @@ func TestWorldSize(t *testing.T) { t.Errorf("WorldSizePositive not as expected: %s <> %s", zero, test.Expect) } } - +} + +func TestViewportChunks(t *testing.T) { + // Initialize a 100 chunk image with 5x5 chunks. + var ChunkSize int32 = 100 + var Offset int32 = 50 + c := level.NewChunker(int(ChunkSize)) + sw := &level.Swatch{ + Name: "solid", + Color: render.Black, + } + + // The 5x5 chunks are expected to be (diagonally) + // -2,-2 + // -1,-1 + // 0,0 + // 1,1 + // 2,2 + // The chunk size is 100px so place a single pixel in each + // 100px quadrant. + fmt.Printf("size=%d offset=%d\n", ChunkSize, Offset) + for x := int32(-2); x <= 2; x++ { + for y := int32(-2); y <= 2; y++ { + point := render.NewPoint( + x*ChunkSize+Offset, + y*ChunkSize+Offset, + ) + fmt.Printf("in chunk: %d,%d set pt: %s\n", + x, y, point, + ) + c.Set(point, sw) + } + } + + // Sanity check the test canvas was created correctly. + worldSize := c.WorldSize() + expectSize := render.Rect{ + X: -200, + Y: -200, + W: 299, + H: 299, + } + if worldSize != expectSize { + t.Errorf( + "Test canvas world size wasn't as expected:\n"+ + "Expected: %s\n"+ + " Actual: %s\n", + expectSize, + worldSize, + ) + } + if len(c.Chunks) != 25 { + t.Errorf( + "Test canvas chunk count wasn't as expected:\n"+ + "Expected: 25\n"+ + " Actual: %d\n", + len(c.Chunks), + ) + } + + type TestCase struct { + Viewport render.Rect + Expect map[render.Point]interface{} + } + var tests = []TestCase{ + { + Viewport: render.Rect{X: -10000, Y: -10000, W: 10000, H: 10000}, + Expect: map[render.Point]interface{}{ + render.NewPoint(-2, -2): nil, + render.NewPoint(-2, -1): nil, + render.NewPoint(-2, 0): nil, + render.NewPoint(-2, 1): nil, + render.NewPoint(-2, 2): nil, + render.NewPoint(-1, -2): nil, + render.NewPoint(-1, -1): nil, + render.NewPoint(-1, 0): nil, + render.NewPoint(-1, 1): nil, + render.NewPoint(-1, 2): nil, + render.NewPoint(0, -2): nil, + render.NewPoint(0, -1): nil, + render.NewPoint(0, 0): nil, + render.NewPoint(0, 1): nil, + render.NewPoint(0, 2): nil, + render.NewPoint(1, -2): nil, + render.NewPoint(1, -1): nil, + render.NewPoint(1, 0): nil, + render.NewPoint(1, 1): nil, + render.NewPoint(1, 2): nil, + render.NewPoint(2, -2): nil, + render.NewPoint(2, -1): nil, + render.NewPoint(2, 0): nil, + render.NewPoint(2, 1): nil, + render.NewPoint(2, 2): nil, + }, + }, + { + Viewport: render.Rect{X: 0, Y: 0, W: 200, H: 200}, + Expect: map[render.Point]interface{}{ + render.NewPoint(0, 0): nil, + render.NewPoint(0, 1): nil, + render.NewPoint(1, 0): nil, + render.NewPoint(1, 1): nil, + }, + }, + // { + // Viewport: render.Rect{X: -5, Y: 0, W: 200, H: 200}, + // Expect: map[render.Point]interface{}{ + // render.NewPoint(-1, 0): nil, + // render.NewPoint(0, 0): nil, + // render.NewPoint(1, 1): nil, + // }, + // }, + } + + for _, test := range tests { + chunks := []render.Point{} + for chunk := range c.IterViewportChunks(test.Viewport) { + chunks = append(chunks, chunk) + } + + if len(chunks) != len(test.Expect) { + t.Errorf("%s: chunk count mismatch: expected %d, got %d", + test.Viewport, + len(test.Expect), + len(chunks), + ) + } + + for _, actual := range chunks { + if _, ok := test.Expect[actual]; !ok { + t.Errorf("%s: got chunk coord %d but did not expect to", + test.Viewport, + actual, + ) + } + delete(test.Expect, actual) + } + + if len(test.Expect) > 0 { + t.Errorf("%s: failed to see these coords: %+v", + test.Viewport, + test.Expect, + ) + } + } } diff --git a/render/interface.go b/render/interface.go index 6e0698a..f051e8c 100644 --- a/render/interface.go +++ b/render/interface.go @@ -28,6 +28,10 @@ type Engine interface { DrawText(Text, Point) error ComputeTextRect(Text) (Rect, error) + // Texture caching. + NewBitmap(filename string) (Texturer, error) + Copy(t Texturer, src, dst Rect) + // Delay for a moment using the render engine's delay method, // implemented by sdl.Delay(uint32) Delay(uint32) @@ -38,6 +42,12 @@ type Engine interface { Loop() error // maybe? } +// Texturer is a stored image texture used by the rendering engine while +// abstracting away its inner workings. +type Texturer interface { + Size() Rect +} + // Rect has a coordinate and a width and height. type Rect struct { X int32 @@ -83,6 +93,26 @@ func (r Rect) IsZero() bool { return r.X == 0 && r.Y == 0 && r.W == 0 && r.H == 0 } +// Add another rect. +func (r Rect) Add(other Rect) Rect { + return Rect{ + X: r.X + other.X, + Y: r.Y + other.Y, + W: r.W + other.W, + H: r.H + other.H, + } +} + +// Add a point to move the rect. +func (r Rect) AddPoint(other Point) Rect { + return Rect{ + X: r.X + other.X, + Y: r.Y + other.Y, + W: r.W, + H: r.H, + } +} + // Text holds information for drawing text. type Text struct { Text string diff --git a/render/sdl/texture.go b/render/sdl/texture.go new file mode 100644 index 0000000..f6e1630 --- /dev/null +++ b/render/sdl/texture.go @@ -0,0 +1,54 @@ +package sdl + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/render" + "github.com/veandco/go-sdl2/sdl" +) + +// Copy a texture into the renderer. +func (r *Renderer) Copy(t render.Texturer, src, dst render.Rect) { + if tex, ok := t.(*Texture); ok { + var ( + a = RectToSDL(src) + b = RectToSDL(dst) + ) + r.renderer.Copy(tex.tex, &a, &b) + } +} + +// Texture can hold on to SDL textures for caching and optimization. +type Texture struct { + tex *sdl.Texture + width int32 + height int32 +} + +// Size returns the dimensions of the texture. +func (t *Texture) Size() render.Rect { + return render.NewRect(t.width, t.height) +} + +// NewBitmap initializes a texture from a bitmap image. +func (r *Renderer) NewBitmap(filename string) (render.Texturer, error) { + log.Debug("NewBitmap: open from file %s", filename) + + surface, err := sdl.LoadBMP(filename) + if err != nil { + return nil, fmt.Errorf("NewBitmap: LoadBMP: %s", err) + } + defer surface.Free() + + tex, err := r.renderer.CreateTextureFromSurface(surface) + if err != nil { + return nil, fmt.Errorf("NewBitmap: create texture: %s", err) + } + + log.Debug("Created texture") + return &Texture{ + width: surface.W, + height: surface.H, + tex: tex, + }, nil +} diff --git a/ui/image.go b/ui/image.go new file mode 100644 index 0000000..94e9993 --- /dev/null +++ b/ui/image.go @@ -0,0 +1,82 @@ +package ui + +import ( + "fmt" + "path/filepath" + "strings" + + "git.kirsle.net/apps/doodle/render" +) + +// ImageType for supported image formats. +type ImageType string + +// Supported image formats. +const ( + BMP ImageType = "bmp" + PNG = "png" +) + +// Image is a widget that is backed by an image file. +type Image struct { + BaseWidget + + // Configurable fields for the constructor. + Type ImageType + texture render.Texturer +} + +// NewImage creates a new Image. +func NewImage(c Image) *Image { + w := &Image{ + Type: c.Type, + } + if w.Type == "" { + w.Type = BMP + } + + w.IDFunc(func() string { + return fmt.Sprintf(`Image<"%s">`, w.Type) + }) + return w +} + +// OpenImage initializes an Image with a given file name. +// +// The file extension is important and should be a supported ImageType. +func OpenImage(e render.Engine, filename string) (*Image, error) { + w := &Image{} + switch strings.ToLower(filepath.Ext(filename)) { + case ".bmp": + w.Type = BMP + case ".png": + w.Type = PNG + default: + return nil, fmt.Errorf("OpenImage: %s: not a supported image type", filename) + } + + tex, err := e.NewBitmap(filename) + if err != nil { + return nil, err + } + + w.texture = tex + return w, nil +} + +// Compute the widget. +func (w *Image) Compute(e render.Engine) { + w.Resize(w.texture.Size()) +} + +// Present the widget. +func (w *Image) Present(e render.Engine, p render.Point) { + size := w.texture.Size() + dst := render.Rect{ + X: p.X, + Y: p.Y, + W: size.W, + H: size.H, + } + e.Copy(w.texture, size, dst) +} diff --git a/uix/canvas.go b/uix/canvas.go index 394fc91..e25f1ad 100644 --- a/uix/canvas.go +++ b/uix/canvas.go @@ -145,6 +145,14 @@ func (w *Canvas) Loop(ev *events.State) error { Swatch: w.Palette.ActiveSwatch, } + log.Warn( + "real cursor: %d,%d translated: %s widget pos: %s scroll: %s", + ev.CursorX.Now, ev.CursorY.Now, + cursor, + P, + w.Scroll, + ) + // Append unique new pixels. if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel { if lastPixel != nil { @@ -160,6 +168,7 @@ func (w *Canvas) Loop(ev *events.State) error { w.pixelHistory = append(w.pixelHistory, pixel) // Save in the pixel canvas map. + log.Info("Set: %s %s", cursor, pixel.Swatch.Color) w.chunks.Set(cursor, pixel.Swatch) } } else { @@ -177,8 +186,8 @@ func (w *Canvas) Viewport() render.Rect { return render.Rect{ X: w.Scroll.X, Y: w.Scroll.Y, - W: S.W - w.BoxThickness(2), - H: S.H - w.BoxThickness(2), + W: S.W - w.BoxThickness(2) + w.Scroll.X, + H: S.H - w.BoxThickness(2) + w.Scroll.Y, } } @@ -218,15 +227,114 @@ func (w *Canvas) Present(e render.Engine, p render.Point) { H: S.H - w.BoxThickness(2), }) - for px := range w.chunks.IterViewport(Viewport) { - // This pixel is visible in the canvas, but offset it by the - // scroll height. - px.X -= Viewport.X - px.Y -= Viewport.Y - color := px.Swatch.Color - e.DrawPoint(color, render.Point{ - X: p.X + w.BoxThickness(1) + px.X, - Y: p.Y + w.BoxThickness(1) + px.Y, - }) + // Get the chunks in the viewport and cache their textures. + for coord := range w.chunks.IterViewportChunks(Viewport) { + if chunk, ok := w.chunks.GetChunk(coord); ok { + tex := chunk.Texture(e, w.Name+coord.String()) + src := render.Rect{ + W: tex.Size().W, + H: tex.Size().H, + } + + // If the source bitmap is already bigger than the Canvas widget + // into which it will render, cap the source width and height. + // This is especially useful for Doodad buttons because the drawing + // is bigger than the button. + if src.W > S.W { + src.W = S.W + } + if src.H > S.H { + src.H = S.H + } + + dst := render.Rect{ + X: p.X + w.Scroll.X + w.BoxThickness(1) + (coord.X * int32(chunk.Size)), + Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + (coord.Y * int32(chunk.Size)), + + // src.W and src.H will be AT MOST the full width and height of + // a Canvas widget. Subtract the scroll offset to keep it bounded + // visually on its right and bottom sides. + W: src.W, // - w.Scroll.X, + H: src.H, // - w.Scroll.Y, + } + + // log.Warn( + // "chunk: %s src: %s dst: %s", + // coord, + // src, + // dst, + // ) + + // If the destination width will cause it to overflow the widget + // box, trim off the right edge of the destination rect. + // + // Keep in mind we're dealing with chunks here, and a chunk is + // a small part of the image. Example: + // - Canvas is 800x600 (S.W=800 S.H=600) + // - Chunk wants to render at 790,0 width 100,100 or whatever + // dst={790, 0, 100, 100} + // - Chunk box would exceed 800px width (X=790 + W=100 == 890) + // - Find the delta how much it exceeds as negative (800 - 890 == -90) + // - Lower the Source and Dest rects by that delta size so they + // stay proportional and don't scale or anything dumb. + if dst.X+src.W > p.X+S.W { + // NOTE: delta is a negative number, + // so it will subtract from the width. + delta := (S.W + p.X) - (dst.W + dst.X) + src.W += delta + dst.W += delta + } + if dst.Y+src.H > p.Y+S.H { + // NOTE: delta is a negative number + delta := (S.H + p.Y) - (dst.H + dst.Y) + src.H += delta + dst.H += delta + } + + // The same for the top left edge, so the drawings don't overlap + // menu bars or left side toolbars. + // - Canvas was placed 80px from the left of the screen. + // Canvas.MoveTo(80, 0) + // - A texture wants to draw at 60, 0 which would cause it to + // overlap 20 pixels into the left toolbar. It needs to be cropped. + // - The delta is: p.X=80 - dst.X=60 == 20 + // - Set destination X to p.X to constrain it there: 20 + // - Subtract the delta from destination W so we don't scale it. + // - Add 20 to X of the source: the left edge of source is not visible + if dst.X < p.X { + // NOTE: delta is a positive number, + // so it will add to the destination coordinates. + delta := p.X - dst.X + dst.X = p.X + dst.W -= delta + src.X += delta + } + if dst.Y < p.Y { + delta := p.Y - dst.Y + dst.Y = p.Y + dst.H -= delta + src.Y += delta + } + + // If the destination rect would overflow our widget bounds, trim + // it off. + + // if w.Name == "edit-canvas" { + // log.Info("%s: copy %+v -> %+v", w.Name, src, dst) + // } + e.Copy(tex, src, dst) + } } + + // for px := range w.chunks.IterViewport(Viewport) { + // // This pixel is visible in the canvas, but offset it by the + // // scroll height. + // px.X -= Viewport.X + // px.Y -= Viewport.Y + // color := render.Cyan // px.Swatch.Color + // e.DrawPoint(color, render.Point{ + // X: p.X + w.BoxThickness(1) + px.X, + // Y: p.Y + w.BoxThickness(1) + px.Y, + // }) + // } }