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
|
||||
|
||||
import "image"
|
||||
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 verticalEntry struct {
|
||||
y int
|
||||
minHeight int
|
||||
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.
|
||||
// Vertical arranges elements vertically. Elements at the start of the entry
|
||||
// list will be positioned at the top, and elements at the end of the entry list
|
||||
// will positioned at the bottom. All elements have the same width.
|
||||
type Vertical struct {
|
||||
*core.Core
|
||||
core core.CoreControl
|
||||
// If Gap is true, a gap will be placed between each element.
|
||||
Gap bool
|
||||
|
||||
gap, pad bool
|
||||
children []verticalEntry
|
||||
selectable bool
|
||||
// If Pad is true, there will be padding running along the inside of the
|
||||
// layout's border.
|
||||
Pad bool
|
||||
}
|
||||
|
||||
// NewVertical creates a new vertical layout. If gap is set to true, a gap will
|
||||
// be placed between each child element. If pad is set to true, padding will be
|
||||
// be placed around the inside of this element's border. Usually, you will want
|
||||
// these to be true.
|
||||
func NewVertical (gap, pad bool) (element *Vertical) {
|
||||
element = &Vertical { }
|
||||
element.Core, element.core = core.NewCore(element)
|
||||
element.gap = gap
|
||||
element.pad = pad
|
||||
element.recalculate()
|
||||
return
|
||||
}
|
||||
// Arrange arranges a list of entries vertically.
|
||||
func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
|
||||
if layout.Pad {
|
||||
width -= theme.Padding() * 2
|
||||
height -= theme.Padding() * 2
|
||||
}
|
||||
freeSpace := height
|
||||
expandingElements := 0
|
||||
|
||||
// SetPad sets whether or not padding will be placed around the inside of this
|
||||
// element's border.
|
||||
func (element *Vertical) SetPad (pad bool) {
|
||||
changed := element.pad != pad
|
||||
element.pad = pad
|
||||
if changed { element.recalculate() }
|
||||
}
|
||||
|
||||
// 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
|
||||
// count the number of expanding elements and the amount of free space
|
||||
// for them to collectively occupy
|
||||
for _, entry := range entries {
|
||||
if entry.Expand {
|
||||
expandingElements ++
|
||||
} else {
|
||||
_, entryMinHeight := entry.MinimumSize()
|
||||
freeSpace -= entryMinHeight
|
||||
}
|
||||
}
|
||||
|
||||
selectable := false
|
||||
for _, entry := range element.children {
|
||||
if entry.element.Selectable() { selectable = true }
|
||||
expandingElementHeight := 0
|
||||
if expandingElements > 0 {
|
||||
expandingElementHeight = freeSpace / expandingElements
|
||||
}
|
||||
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:
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Vertical) AdvanceSelection (direction int) (ok bool) {
|
||||
// TODO:
|
||||
return
|
||||
}
|
||||
|
||||
func (element *Vertical) recalculate () {
|
||||
var x, y int
|
||||
if element.pad {
|
||||
x, y := 0, 0
|
||||
if layout.Pad {
|
||||
x += 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.SetTitle("vertical stack")
|
||||
|
||||
layout := layouts.NewVertical(true, true)
|
||||
window.Adopt(layout)
|
||||
container := basic.NewContainer(layouts.Vertical { true, true })
|
||||
window.Adopt(container)
|
||||
|
||||
layout.Adopt(basic.NewLabel("it is a label hehe"))
|
||||
layout.Adopt(basic.NewButton("yeah"), false)
|
||||
layout.Adopt(button := basic.NewButton("wow"), false)
|
||||
label := basic.NewLabel("it is a label hehe")
|
||||
button := basic.NewButton("press me")
|
||||
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.Show()
|
||||
|
22
tomo.go
22
tomo.go
@ -150,6 +150,28 @@ type Window interface {
|
||||
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
|
||||
|
||||
// Run initializes a backend, calls the callback function, and begins the event
|
||||
|
Reference in New Issue
Block a user