diff --git a/box.go b/box.go index 11e9b5a..6e39466 100644 --- a/box.go +++ b/box.go @@ -11,6 +11,7 @@ import "git.tebibyte.media/tomo/tomo/canvas" type box struct { backend *Backend parent parent + outer anyBox bounds image.Rectangle minSize image.Point @@ -45,9 +46,12 @@ type box struct { } func (backend *Backend) NewBox() tomo.Box { - return &box { + box := &box { backend: backend, } + box.drawer = box + box.outer = box + return box } func (this *box) Box () tomo.Box { @@ -88,10 +92,14 @@ func (this *box) SetBorder (border ...tomo.Border) { } func (this *box) SetMinimumSize (width, height int) { - this.minSize = image.Pt(width, height) + minSize := image.Pt(width, height) + if this.minSize == minSize { return } + this.minSize = minSize + if this.bounds.Dx() < width || this.bounds.Dy() < height { this.invalidateLayout() } + // TODO: alert the parent } func (this *box) SetPadding (padding tomo.Inset) { @@ -112,7 +120,7 @@ func (this *box) SetFocused (focused bool) { if this.Focused () && !focused { this.parent.window().focus(nil) } else if !this.Focused() && focused { - this.parent.window().focus(this) + this.parent.window().focus(this.outer) } } @@ -125,7 +133,7 @@ func (this *box) SetFocusable (focusable bool) { } func (this *box) Focused () bool { - return this == this.parent.window().focused + return this.outer == this.parent.window().focused } func (this *box) Modifiers () input.Modifiers { @@ -136,7 +144,7 @@ func (this *box) MousePosition () image.Point { return this.parent.window().mousePosition } -// ----- event handlers ----------------------------------------------------- // +// ----- event handler setters ---------------------------------------------- // func (this *box) OnFocusEnter (callback func()) event.Cookie { return this.on.focusEnter.Connect(callback) } @@ -179,8 +187,14 @@ func (this *box) OnKeyUp (callback func(key input.Key, numberPad bool)) event.Co // -------------------------------------------------------------------------- // func (this *box) Draw (can canvas.Canvas) { + this.drawBorders(can) pen := can.Pen() + pen.Fill(this.color) + pen.Rectangle(this.bounds) +} +func (this *box) drawBorders (can canvas.Canvas) { + pen := can.Pen() bounds := this.bounds for _, border := range this.border { pen.Fill(border.Color[tomo.SideTop]) @@ -210,15 +224,11 @@ func (this *box) Draw (can canvas.Canvas) { bounds = border.Width.Apply(bounds) } - pen.Fill(this.color) - pen.Rectangle(bounds) } func (this *box) doDraw () { if this.canvas == nil { return } - if this.drawer == nil { - this.Draw(this.canvas) - } else { + if this.drawer != nil { this.drawer.Draw(this.canvas) } } @@ -236,12 +246,12 @@ func (this *box) recursiveRedo () { this.doDraw() } -func (this *box) invalidateDraw () { - if this.parent == nil { return } - this.parent.window().invalidateDraw(this) +func (this *box) invalidateLayout () { + if this.parent == nil || this.parent.window() == nil { return } + this.parent.window().invalidateLayout(this.outer) } -func (this *box) invalidateLayout () { - if this.parent == nil { return } - this.parent.window().invalidateLayout(this) +func (this *box) invalidateDraw () { + if this.parent == nil || this.parent.window() == nil { return } + this.parent.window().invalidateDraw(this.outer) } diff --git a/canvasbox.go b/canvasbox.go index 1ccb038..fcabfb8 100644 --- a/canvasbox.go +++ b/canvasbox.go @@ -8,9 +8,11 @@ type canvasBox struct { } func (backend *Backend) NewCanvasBox () tomo.CanvasBox { - return &canvasBox { + box := &canvasBox { box: backend.NewBox().(*box), } + box.outer = box + return box } func (this *canvasBox) Box () tomo.Box { diff --git a/containerbox.go b/containerbox.go index 525c187..9f1123a 100644 --- a/containerbox.go +++ b/containerbox.go @@ -1,8 +1,176 @@ package x +import "image" +import "image/color" import "git.tebibyte.media/tomo/tomo" +import "git.tebibyte.media/tomo/tomo/event" +import "git.tebibyte.media/tomo/tomo/canvas" + +type containerBox struct { + *box + + hOverflow, vOverflow bool + contentBounds image.Rectangle + scroll image.Point + + gap tomo.Gap + children []anyBox + layout tomo.Layout + + on struct { + contentBoundsChange event.FuncBroadcaster + } +} func (backend *Backend) NewContainerBox() tomo.ContainerBox { - // TODO - return nil + box := &containerBox { + box: backend.NewBox().(*box), + } + box.drawer = box + box.outer = box + return box +} + +func (this *containerBox) Box () tomo.Box { + return this +} + +func (this *containerBox) SetOverflow (horizontal, vertical bool) { + if this.hOverflow == horizontal && this.vOverflow == vertical { return } + this.hOverflow = horizontal + this.vOverflow = vertical + this.invalidateLayout() +} + +func (this *containerBox) ContentBounds () image.Rectangle { + return this.contentBounds +} + +func (this *containerBox) ScrollTo (point image.Point) { + // TODO: constrain scroll + this.scroll = point + this.invalidateLayout() +} + +func (this *containerBox) OnContentBoundsChange (callback func()) event.Cookie { + return this.on.contentBoundsChange.Connect(callback) +} + +func (this *containerBox) SetGap (gap tomo.Gap) { + if this.gap == gap { return } + this.gap = gap + this.invalidateLayout() +} + +func (this *containerBox) Add (child tomo.Object) { + box := assertAnyBox(child.Box()) + if indexOf(this.children, box) > -1 { return } + + box.setParent(this) + this.children = append(this.children, box) + this.invalidateLayout() +} + +func (this *containerBox) Delete (child tomo.Object) { + box := assertAnyBox(child.Box()) + index := indexOf(this.children, box) + if index < 0 { return } + + box.setParent(nil) + this.children = remove(this.children, index) + this.invalidateLayout() +} + +func (this *containerBox) Insert (child, before tomo.Object) { + box := assertAnyBox(child.Box()) + if indexOf(this.children, box) > -1 { return } + + beforeBox := assertAnyBox(before.Box()) + index := indexOf(this.children, beforeBox) + if index < 0 { return } + + box.setParent(this) + this.children = insert(this.children, index, box) + this.invalidateLayout() +} + +func (this *containerBox) Clear () { + for _, box := range this.children { + box.setParent(nil) + } + this.children = nil + this.invalidateLayout() +} + +func (this *containerBox) Length () int { + return len(this.children) +} + +func (this *containerBox) At (index int) tomo.Object { + if index < 0 || index >= len(this.children) { + return nil + } + return this.children[index] +} + +func (this *containerBox) SetLayout (layout tomo.Layout) { + this.layout = layout + this.invalidateLayout() +} + +func (this *containerBox) Draw (can canvas.Canvas) { + this.drawBorders(can) + pen := can.Pen() + pen.Fill(this.color) + + // TODO: do this in doLayout and save the result + rocks := make([]image.Rectangle, len(this.children)) + for index, box := range this.children { + rocks[index] = box.Bounds() + } + // TODO: use shatter algorithm here to optimize amount of pixels drawn + // and not draw over child boxes + for index, tile := range canvas.Shatter(this.bounds, rocks...) { + pen.Fill(color.RGBA { R: uint8(32 * (index + 2)), A: 255 }) + pen.Rectangle(tile) + } +} + +func (this *containerBox) window () *window { + if this.parent == nil { return nil } + return this.parent.window() +} + +func (this *containerBox) canvas () canvas.Canvas { + return this.box.canvas +} + +func (this *containerBox) doLayout () { + this.box.doLayout() + // TODO: possibly store all children as tomo.Box-es and don't allocate a + // slice here + previousContentBounds := this.contentBounds + boxes := make([]tomo.Box, len(this.children)) + for index, box := range this.children { + boxes[index] = box + } + if this.layout != nil { + // TODO maybe we should pass more information into Arrange such + // as overflow information and scroll + this.layout.Arrange ( + this.InnerBounds().Sub(this.scroll), + this.gap, + boxes) + } + if previousContentBounds != this.contentBounds { + this.on.contentBoundsChange.Broadcast() + } +} + +func (this *containerBox) recursiveRedo () { + this.doLayout() + this.doDraw() + for _, child := range this.children { + child.recursiveRedo() + } } diff --git a/go.mod b/go.mod index 1f1ee10..4674fec 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.20 require ( git.tebibyte.media/tomo/ggfx v0.3.0 - git.tebibyte.media/tomo/tomo v0.7.3 + git.tebibyte.media/tomo/tomo v0.8.0 git.tebibyte.media/tomo/xgbkb v1.0.1 github.com/jezek/xgb v1.1.0 github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 diff --git a/go.sum b/go.sum index 38f7759..1b663d0 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ git.tebibyte.media/tomo/tomo v0.7.2 h1:15dMJm4Sm339b23o9RZSq87u99SaF2q+b5CRB5P58 git.tebibyte.media/tomo/tomo v0.7.2/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= git.tebibyte.media/tomo/tomo v0.7.3 h1:eHwuYKe+0nLWoEfPZid8njirxmWY3dFmdY+PsPp1RN0= git.tebibyte.media/tomo/tomo v0.7.3/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= +git.tebibyte.media/tomo/tomo v0.8.0 h1:Sqvos2Huf0mSHFZ0FJrBZiH8Ro/gmQPHCvK6Qr29SBo= +git.tebibyte.media/tomo/tomo v0.8.0/go.mod h1:lTwjpiHbP4UN/kFw+6FwhG600B+PMKVtMOr7wpd5IUY= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1/go.mod h1:P5Du0yo5hUsojchW08t+Mds0XPIJXwMi733ZfklzjRw= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJXZHsaM8b6OLVo6muQUQd4CwkH/D3fnnbHXA= diff --git a/system.go b/system.go index 9d1aef6..83cf00b 100644 --- a/system.go +++ b/system.go @@ -44,6 +44,14 @@ type anyBox interface { recursiveRedo () } +func assertAnyBox (unknown tomo.Box) anyBox { + if box, ok := unknown.(anyBox); ok { + return box + } else { + panic("foregin box implementation, i did not make this!") + } +} + func (window *window) SetRoot (root tomo.Object) { if window.root != nil { window.root.setParent(nil) @@ -51,7 +59,7 @@ func (window *window) SetRoot (root tomo.Object) { if root == nil { window.root = nil } else { - box := root.Box().(anyBox) + box := assertAnyBox(root.Box()) box.setParent(window) window.invalidateLayout(box) window.root = box diff --git a/util.go b/util.go new file mode 100644 index 0000000..86e1143 --- /dev/null +++ b/util.go @@ -0,0 +1,20 @@ +package x + +func indexOf[T comparable] (haystack []T, needle T) int { + for index, test := range haystack { + if test == needle { + return index + } + } + return -1 +} + +func remove[T any] (slice []T, index int) []T { + return append(slice[:index], slice[index + 1:]...) +} + +func insert[T any] (slice []T, index int, element T) []T { + slice = append(slice[:index + 1], slice[index:]...) + slice[index] = element + return slice +}