The beginnings of a layout system
This commit is contained in:
parent
b1fd021120
commit
6eed70e79e
136
elements/basic/container.go
Normal file
136
elements/basic/container.go
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
||||||
|
|
||||||
|
type Container struct {
|
||||||
|
*core.Core
|
||||||
|
core core.CoreControl
|
||||||
|
|
||||||
|
layout tomo.Layout
|
||||||
|
children []tomo.LayoutEntry
|
||||||
|
selectable bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewContainer (layout tomo.Layout) (element *Container) {
|
||||||
|
element = &Container { }
|
||||||
|
element.Core, element.core = core.NewCore(element)
|
||||||
|
element.SetLayout(layout)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) SetLayout (layout tomo.Layout) {
|
||||||
|
element.layout = layout
|
||||||
|
element.recalculate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) Adopt (child tomo.Element, expand bool) {
|
||||||
|
child.SetParentHooks (tomo.ParentHooks {
|
||||||
|
MinimumSizeChange:
|
||||||
|
func (int, int) { element.updateMinimumSize() },
|
||||||
|
SelectabilityChange:
|
||||||
|
func (bool) { element.updateSelectable() },
|
||||||
|
})
|
||||||
|
element.children = append (element.children, tomo.LayoutEntry {
|
||||||
|
Element: child,
|
||||||
|
})
|
||||||
|
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.updateSelectable()
|
||||||
|
element.recalculate()
|
||||||
|
if element.core.HasImage() { element.draw() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disown removes the given child from the container if it is contained within
|
||||||
|
// it.
|
||||||
|
func (element *Container) Disown (child tomo.Element) {
|
||||||
|
for index, entry := range element.children {
|
||||||
|
if entry.Element == child {
|
||||||
|
entry.SetParentHooks(tomo.ParentHooks { })
|
||||||
|
element.children = append (
|
||||||
|
element.children[:index],
|
||||||
|
element.children[index + 1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.updateSelectable()
|
||||||
|
element.recalculate()
|
||||||
|
if element.core.HasImage() { element.draw() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Children returns a slice containing this element's children.
|
||||||
|
func (element *Container) Children () (children []tomo.Element) {
|
||||||
|
children = make([]tomo.Element, len(element.children))
|
||||||
|
for index, entry := range element.children {
|
||||||
|
children[index] = entry.Element
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountChildren returns the amount of children contained within this element.
|
||||||
|
func (element *Container) CountChildren () (count int) {
|
||||||
|
return len(element.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child returns the child at the specified index. If the index is out of
|
||||||
|
// bounds, this method will return nil.
|
||||||
|
func (element *Container) Child (index int) (child tomo.Element) {
|
||||||
|
if index < 0 || index > len(element.children) { return }
|
||||||
|
return element.children[index].Element
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) Handle (event tomo.Event) {
|
||||||
|
switch event.(type) {
|
||||||
|
case tomo.EventResize:
|
||||||
|
resizeEvent := event.(tomo.EventResize)
|
||||||
|
element.core.AllocateCanvas (
|
||||||
|
resizeEvent.Width,
|
||||||
|
resizeEvent.Height)
|
||||||
|
element.recalculate()
|
||||||
|
element.draw()
|
||||||
|
|
||||||
|
// TODO:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) AdvanceSelection (direction int) (ok bool) {
|
||||||
|
// TODO:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) updateSelectable () {
|
||||||
|
selectable := false
|
||||||
|
for _, entry := range element.children {
|
||||||
|
if entry.Selectable() { selectable = true }
|
||||||
|
}
|
||||||
|
element.core.SetSelectable(selectable)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) updateMinimumSize () {
|
||||||
|
element.core.SetMinimumSize(element.layout.MinimumSize(element.children))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) recalculate () {
|
||||||
|
bounds := element.Bounds()
|
||||||
|
element.layout.Arrange(element.children, bounds.Dx(), bounds.Dy())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Container) draw () {
|
||||||
|
bounds := element.core.Bounds()
|
||||||
|
|
||||||
|
artist.Rectangle (
|
||||||
|
element.core,
|
||||||
|
theme.BackgroundImage(),
|
||||||
|
nil, 0,
|
||||||
|
bounds)
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
for _, entry := range element.children {
|
||||||
|
artist.Paste(element.core, entry, entry.Position)
|
||||||
|
}
|
||||||
|
}
|
@ -1,136 +1,90 @@
|
|||||||
package layouts
|
package layouts
|
||||||
|
|
||||||
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
// import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
|
|
||||||
type verticalEntry struct {
|
// Vertical arranges elements vertically. Elements at the start of the entry
|
||||||
y int
|
// list will be positioned at the top, and elements at the end of the entry list
|
||||||
minHeight int
|
// will positioned at the bottom. All elements have the same width.
|
||||||
element tomo.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vertical lays its children out vertically. It can contain any number of
|
|
||||||
// children. When an child is added to the layout, it can either be set to
|
|
||||||
// contract to its minimum height or expand to fill the remaining space (space
|
|
||||||
// that is not taken up by other children or padding is divided equally among
|
|
||||||
// these). Child elements will all have the same width.
|
|
||||||
type Vertical struct {
|
type Vertical struct {
|
||||||
*core.Core
|
// If Gap is true, a gap will be placed between each element.
|
||||||
core core.CoreControl
|
Gap bool
|
||||||
|
|
||||||
gap, pad bool
|
// If Pad is true, there will be padding running along the inside of the
|
||||||
children []verticalEntry
|
// layout's border.
|
||||||
selectable bool
|
Pad bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVertical creates a new vertical layout. If gap is set to true, a gap will
|
// Arrange arranges a list of entries vertically.
|
||||||
// be placed between each child element. If pad is set to true, padding will be
|
func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
|
||||||
// be placed around the inside of this element's border. Usually, you will want
|
if layout.Pad {
|
||||||
// these to be true.
|
width -= theme.Padding() * 2
|
||||||
func NewVertical (gap, pad bool) (element *Vertical) {
|
height -= theme.Padding() * 2
|
||||||
element = &Vertical { }
|
}
|
||||||
element.Core, element.core = core.NewCore(element)
|
freeSpace := height
|
||||||
element.gap = gap
|
expandingElements := 0
|
||||||
element.pad = pad
|
|
||||||
element.recalculate()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPad sets whether or not padding will be placed around the inside of this
|
// count the number of expanding elements and the amount of free space
|
||||||
// element's border.
|
// for them to collectively occupy
|
||||||
func (element *Vertical) SetPad (pad bool) {
|
for _, entry := range entries {
|
||||||
changed := element.pad != pad
|
if entry.Expand {
|
||||||
element.pad = pad
|
expandingElements ++
|
||||||
if changed { element.recalculate() }
|
} else {
|
||||||
}
|
_, entryMinHeight := entry.MinimumSize()
|
||||||
|
freeSpace -= entryMinHeight
|
||||||
// SetGap sets whether or not a gap will be placed in between child elements.
|
|
||||||
func (element *Vertical) SetGap (gap bool) {
|
|
||||||
changed := element.gap != gap
|
|
||||||
element.gap = gap
|
|
||||||
if changed { element.recalculate() }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adopt adds a child element to the vertical layout. If expand is set to true,
|
|
||||||
// the element will be expanded to fill a portion of the remaining space in the
|
|
||||||
// layout.
|
|
||||||
func (element *Vertical) Adopt (child tomo.Element, expand bool) {
|
|
||||||
_, minHeight := child.MinimumSize()
|
|
||||||
child.SetParentHooks (tomo.ParentHooks {
|
|
||||||
// TODO
|
|
||||||
})
|
|
||||||
element.children = append (element.children, verticalEntry {
|
|
||||||
element: child,
|
|
||||||
minHeight: minHeight,
|
|
||||||
})
|
|
||||||
if child.Selectable() { element.core.SetSelectable(true) }
|
|
||||||
|
|
||||||
element.recalculate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disown removes the given child from the layout if it is contained within it.
|
|
||||||
func (element *Vertical) Disown (child tomo.Element) {
|
|
||||||
for index, entry := range element.children {
|
|
||||||
if entry.element == child {
|
|
||||||
entry.element.SetParentHooks(tomo.ParentHooks { })
|
|
||||||
element.children = append (
|
|
||||||
element.children[:index],
|
|
||||||
element.children[index + 1:]...)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
expandingElementHeight := 0
|
||||||
selectable := false
|
if expandingElements > 0 {
|
||||||
for _, entry := range element.children {
|
expandingElementHeight = freeSpace / expandingElements
|
||||||
if entry.element.Selectable() { selectable = true }
|
|
||||||
}
|
}
|
||||||
element.core.SetSelectable(selectable)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Children returns a slice containing this element's children.
|
|
||||||
func (element *Vertical) Children () (children []tomo.Element) {
|
|
||||||
children = make([]tomo.Element, len(element.children))
|
|
||||||
for index, entry := range element.children {
|
|
||||||
children[index] = entry.element
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountChildren returns the amount of children contained within this element.
|
|
||||||
func (element *Vertical) CountChildren () (count int) {
|
|
||||||
return len(element.children)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Child returns the child at the specified index. If the index is out of
|
|
||||||
// bounds, this method will return nil.
|
|
||||||
func (element *Vertical) Child (index int) (child tomo.Element) {
|
|
||||||
if index < 0 || index > len(element.children) { return }
|
|
||||||
return element.children[index].element
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Vertical) Handle (event tomo.Event) {
|
|
||||||
switch event.(type) {
|
|
||||||
case tomo.EventResize:
|
|
||||||
element.recalculate()
|
|
||||||
// TODO:
|
|
||||||
|
|
||||||
// TODO:
|
x, y := 0, 0
|
||||||
}
|
if layout.Pad {
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Vertical) AdvanceSelection (direction int) (ok bool) {
|
|
||||||
// TODO:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Vertical) recalculate () {
|
|
||||||
var x, y int
|
|
||||||
if element.pad {
|
|
||||||
x += theme.Padding()
|
x += theme.Padding()
|
||||||
y += theme.Padding()
|
y += theme.Padding()
|
||||||
}
|
}
|
||||||
// TODO
|
|
||||||
|
// set the size and position of each element
|
||||||
|
for index, entry := range entries {
|
||||||
|
if index > 0 && layout.Gap { y += theme.Padding() }
|
||||||
|
|
||||||
|
entries[index].Position = image.Pt(x, y)
|
||||||
|
entryHeight := 0
|
||||||
|
if entry.Expand {
|
||||||
|
entryHeight = expandingElementHeight
|
||||||
|
} else {
|
||||||
|
_, entryHeight = entry.MinimumSize()
|
||||||
|
}
|
||||||
|
y += entryHeight
|
||||||
|
entryBounds := entry.Bounds()
|
||||||
|
if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight {
|
||||||
|
entry.Handle (tomo.EventResize {
|
||||||
|
Width: width,
|
||||||
|
Height: entryHeight,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MinimumSize returns the minimum width and height will be needed to arrange
|
||||||
|
// the given list of entries.
|
||||||
|
func (layout Vertical) MinimumSize (entries []tomo.LayoutEntry) (width, height int) {
|
||||||
|
for index, entry := range entries {
|
||||||
|
entryWidth, entryHeight := entry.MinimumSize()
|
||||||
|
if entryWidth > width {
|
||||||
|
width = entryWidth
|
||||||
|
}
|
||||||
|
height += entryHeight
|
||||||
|
if layout.Gap && index > 0 {
|
||||||
|
height += theme.Padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if layout.Pad {
|
||||||
|
width += theme.Padding() * 2
|
||||||
|
height += theme.Padding() * 2
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
@ -13,12 +13,21 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(2, 2)
|
window, _ := tomo.NewWindow(2, 2)
|
||||||
window.SetTitle("vertical stack")
|
window.SetTitle("vertical stack")
|
||||||
|
|
||||||
layout := layouts.NewVertical(true, true)
|
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||||
window.Adopt(layout)
|
window.Adopt(container)
|
||||||
|
|
||||||
layout.Adopt(basic.NewLabel("it is a label hehe"))
|
label := basic.NewLabel("it is a label hehe")
|
||||||
layout.Adopt(basic.NewButton("yeah"), false)
|
button := basic.NewButton("press me")
|
||||||
layout.Adopt(button := basic.NewButton("wow"), false)
|
button.OnClick (func () {
|
||||||
|
label.SetText (
|
||||||
|
"woah, this button changes the label text! since the " +
|
||||||
|
"size of this text box has changed, the window " +
|
||||||
|
"should expand (unless you resized it already).")
|
||||||
|
})
|
||||||
|
|
||||||
|
container.Adopt(label, true)
|
||||||
|
container.Adopt(basic.NewButton("yeah"), false)
|
||||||
|
container.Adopt(button, false)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
22
tomo.go
22
tomo.go
@ -150,6 +150,28 @@ type Window interface {
|
|||||||
OnClose (func ())
|
OnClose (func ())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LayoutEntry associates an element with layout and positioning information so
|
||||||
|
// it can be arranged by a Layout.
|
||||||
|
type LayoutEntry struct {
|
||||||
|
Element
|
||||||
|
Position image.Point
|
||||||
|
Expand bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Layout is capable of arranging elements within a container. It is also able
|
||||||
|
// to determine the minimum amount of room it needs to do so.
|
||||||
|
type Layout interface {
|
||||||
|
// Arrange takes in a slice of entries and a bounding width and height,
|
||||||
|
// and changes the position of the entiries in the slice so that they
|
||||||
|
// are properly laid out. The given width and height should not be less
|
||||||
|
// than what is returned by MinimumSize.
|
||||||
|
Arrange (entries []LayoutEntry, width, height int)
|
||||||
|
|
||||||
|
// MinimumSize returns the minimum width and height that the layout
|
||||||
|
// needs to properly arrange the given slice of layout entries.
|
||||||
|
MinimumSize (entries []LayoutEntry) (width, height int)
|
||||||
|
}
|
||||||
|
|
||||||
var backend Backend
|
var backend Backend
|
||||||
|
|
||||||
// Run initializes a backend, calls the callback function, and begins the event
|
// Run initializes a backend, calls the callback function, and begins the event
|
||||||
|
Reference in New Issue
Block a user