Lists are a thing now

Looks like child bounds arent clipped properly though, ugh
This commit is contained in:
Sasha Koshka 2023-04-18 02:59:44 -04:00
parent 6b13e772a9
commit 0bf5c3b86c
7 changed files with 420 additions and 95 deletions

View File

@ -102,10 +102,10 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity {
return nil
}
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer)) {
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok {
callback(parent)
callback(parent, entity.element)
}
entity.parent.forMouseTargetContainers(callback)
}
@ -211,7 +211,8 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
func (entity *entity) SelectChild (index int, selected bool) {
child := entity.children[index]
if element, ok := entity.element.(tomo.Selectable); ok {
if element, ok := child.element.(tomo.Selectable); ok {
if child.selected == selected { return }
child.selected = selected
element.HandleSelectionChange()
}

View File

@ -212,11 +212,11 @@ func (window *window) handleButtonPress (
point.X, point.Y,
input.Button(buttonEvent.Detail))
}
callback := func (container tomo.MouseTargetContainer) {
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseDown (
point.X, point.Y,
input.Button(buttonEvent.Detail),
underneath.element)
child)
}
underneath.forMouseTargetContainers(callback)
}
@ -238,12 +238,12 @@ func (window *window) handleButtonRelease (
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail))
}
callback := func (container tomo.MouseTargetContainer) {
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail),
dragging.element)
child)
}
dragging.forMouseTargetContainers(callback)
}

View File

@ -212,14 +212,9 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P
switch id {
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
case tomo.PatternDead: return defaultTextures[0][offset]
case tomo.PatternRaised:
if c.Match("tomo", "listEntry", "") {
return defaultTextures[10][offset]
} else {
return defaultTextures[1][offset]
}
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton:
switch {
case c.Match("tomo", "checkbox", ""):
@ -272,16 +267,8 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
// Padding returns the default padding value for the given pattern.
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
switch id {
case tomo.PatternRaised:
if c.Match("tomo", "listEntry", "") {
return artist.I(4, 8)
} else {
return artist.I(8)
}
case tomo.PatternSunken:
if c.Match("tomo", "list", "") {
return artist.I(4, 0, 3)
} else if c.Match("tomo", "progressBar", "") {
if c.Match("tomo", "progressBar", "") {
return artist.I(2, 1, 1, 2)
} else {
return artist.I(8)

View File

@ -13,18 +13,19 @@ type cellEntity interface {
// Cell is a single-element container that satisfies tomo.Selectable. It
// provides styling based on whether or not it is selected.
type Cell struct {
entity cellEntity
child tomo.Element
enabled bool
padding bool
theme theme.Wrapped
entity cellEntity
child tomo.Element
enabled bool
theme theme.Wrapped
onSelectionChange func ()
}
// NewCell creates a new cell element. If padding is true, the cell will have
// padding on all sides. Child can be nil and added later with the Adopt()
// method.
func NewCell (child tomo.Element, padding bool) (element *Cell) {
element = &Cell { padding: padding }
func NewCell (child tomo.Element) (element *Cell) {
element = &Cell { enabled: true }
element.theme.Case = tomo.C("tomo", "cell")
element.entity = tomo.NewEntity(element).(cellEntity)
element.Adopt(child)
@ -42,7 +43,7 @@ func (element *Cell) Draw (destination canvas.Canvas) {
pattern := element.theme.Pattern(tomo.PatternTableCell, element.state())
if element.child == nil {
pattern.Draw(destination, bounds)
} else if element.padding {
} else {
artist.DrawShatter (
destination, pattern, bounds,
element.child.Entity().Bounds())
@ -54,9 +55,7 @@ func (element *Cell) Layout () {
if element.child == nil { return }
bounds := element.entity.Bounds()
if element.padding {
bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds)
}
bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds)
element.entity.PlaceChild(0, bounds)
}
@ -80,6 +79,7 @@ func (element *Cell) Adopt (child tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
@ -93,9 +93,7 @@ func (element *Cell) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
if child, ok := element.child.(tomo.Enableable); ok {
child.SetEnabled(enabled)
}
element.invalidateChild()
}
// SetTheme sets this element's theme.
@ -104,11 +102,26 @@ func (element *Cell) SetTheme (theme tomo.Theme) {
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
// OnSelectionChange sets a function to be called when this element is selected
// or unselected.
func (element *Cell) OnSelectionChange (callback func ()) {
element.onSelectionChange = callback
}
func (element *Cell) Selected () bool {
return element.entity.Selected()
}
func (element *Cell) HandleSelectionChange () {
element.entity.Invalidate()
element.invalidateChild()
if element.onSelectionChange != nil {
element.onSelectionChange()
}
}
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
@ -132,11 +145,15 @@ func (element *Cell) updateMinimumSize () {
width += childWidth
height += childHeight
}
if element.padding {
padding := element.theme.Padding(tomo.PatternTableCell)
width += padding.Horizontal()
height += padding.Vertical()
}
padding := element.theme.Padding(tomo.PatternTableCell)
width += padding.Horizontal()
height += padding.Vertical()
element.entity.SetMinimumSize(width, height)
}
func (element *Cell) invalidateChild () {
if element.child != nil {
element.child.Entity().Invalidate()
}
}

315
elements/list.go Normal file
View File

@ -0,0 +1,315 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
type listEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
type List struct {
entity listEntity
scratch map[tomo.Element] scratchEntry
scroll image.Point
contentBounds image.Rectangle
columnSizes []int
selected int
forcedMinimumWidth int
forcedMinimumHeight int
theme theme.Wrapped
onScrollBoundsChange func ()
}
func NewList (columns int, children ...tomo.Selectable) (element *List) {
if columns < 1 { columns = 1 }
element = &List { selected: -1 }
element.scratch = make(map[tomo.Element] scratchEntry)
element.columnSizes = make([]int, columns)
element.theme.Case = tomo.C("tomo", "list")
element.entity = tomo.NewEntity(element).(listEntity)
for _, child := range children {
element.Adopt(child)
}
return
}
func (element *List) Entity () tomo.Entity {
return element.entity
}
func (element *List) Draw (destination canvas.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
}
func (element *List) Layout () {
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
columnIndex := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
columnIndex = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
if columnIndex >= len(element.columnSizes) {
nextLine()
}
width := element.columnSizes[columnIndex]
height := int(entry.minSize)
if len(element.columnSizes) == 1 && width < bounds.Dx() {
width = bounds.Dx()
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.X += width + margin.X
columnIndex ++
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *List) Adopt (child tomo.Element) {
element.entity.Adopt(child)
element.scratch[child] = scratchEntry { }
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) Disown (child tomo.Element) {
index := element.entity.IndexOf(child)
if index < 0 { return }
if index == element.selected {
element.selected = -1
element.entity.SelectChild(index, false)
}
element.entity.Disown(index)
delete(element.scratch, child)
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) DisownAll () {
func () {
for index := 0; index < element.entity.CountChildren(); index ++ {
index := index
defer element.entity.Disown(index)
}
} ()
element.scratch = make(map[tomo.Element] scratchEntry)
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) {
if child, ok := child.(tomo.Selectable); ok {
index := element.entity.IndexOf(child)
if element.selected == index { return }
if element.selected >= 0 {
element.entity.SelectChild(element.selected, false)
}
element.selected = index
element.entity.SelectChild(index, true)
}
}
func (element *List) HandleChildMouseUp (int, int, input.Button, tomo.Element) { }
func (element *List) HandleChildMinimumSizeChange (child tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) HandleChildFlexibleHeightChange (child tomo.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) DrawBackground (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *List) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Collapse forces a minimum width and height upon the list. If a zero value is
// given for a dimension, its minimum will be determined by the list's content.
// If the list's height goes beyond the forced size, it will need to be accessed
// via scrolling. If an entry's width goes beyond the forced size, its text will
// be truncated so that it fits.
func (element *List) Collapse (width, height int) {
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *List) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *List) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *List) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *List) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *List) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *List) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *List) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
for index := range element.columnSizes {
element.columnSizes[index] = 0
}
height := 0
rowHeight := 0
columnIndex := 0
nextLine := func () {
height += rowHeight
rowHeight = 0
columnIndex = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
if columnIndex >= len(element.columnSizes) {
if index > 0 { height += margin.Y }
nextLine()
}
child := element.entity.Child(index)
entry := element.scratch[child]
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
entry.minBreadth = float64(entryWidth)
entry.minSize = float64(entryHeight)
element.scratch[child] = entry
if rowHeight < entryHeight {
rowHeight = entryHeight
}
if element.columnSizes[columnIndex] < entryWidth {
element.columnSizes[columnIndex] = entryWidth
}
columnIndex ++
}
nextLine()
width := 0; for index, size := range element.columnSizes {
width += size
if index > 0 { width += margin.X }
}
width += padding.Horizontal()
height += padding.Vertical()
if element.forcedMinimumHeight > 0 {
height = element.forcedMinimumHeight
}
if element.forcedMinimumWidth > 0 {
width = element.forcedMinimumWidth
}
element.entity.SetMinimumSize(width, height)
}

View File

@ -2,10 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -16,18 +14,16 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0))
window.SetTitle("List Sidebar")
container := containers.NewContainer(layouts.Horizontal { true, true })
container := elements.NewHBox(true, true)
window.Adopt(container)
var currentPage tomo.Element
turnPage := func (newPage tomo.Element) {
container.Warp (func () {
if currentPage != nil {
container.Disown(currentPage)
}
container.Adopt(newPage, true)
currentPage = newPage
})
if currentPage != nil {
container.Disown(currentPage)
}
container.Adopt(newPage, true)
currentPage = newPage
}
intro := elements.NewLabel (
@ -39,7 +35,7 @@ func run () {
})
mouse := testing.NewMouse()
input := elements.NewTextBox("Write some text", "")
form := containers.NewContainer(layouts.Vertical { true, false})
form := elements.NewVBox(false, true)
form.Adopt(elements.NewLabel("I have:", false), false)
form.Adopt(elements.NewSpacer(true), false)
form.Adopt(elements.NewCheckbox("Skin", true), false)
@ -47,13 +43,21 @@ func run () {
form.Adopt(elements.NewCheckbox("Bone", false), false)
art := testing.NewArtist()
makePage := func (name string, callback func ()) tomo.Selectable {
cell := elements.NewCell(elements.NewLabel(name, false))
cell.OnSelectionChange (func () {
if cell.Selected() { callback() }
})
return cell
}
list := elements.NewList (
elements.NewListEntry("button", func () { turnPage(button) }),
elements.NewListEntry("mouse", func () { turnPage(mouse) }),
elements.NewListEntry("input", func () { turnPage(input) }),
elements.NewListEntry("form", func () { turnPage(form) }),
elements.NewListEntry("art", func () { turnPage(art) }))
list.OnNoEntrySelected(func () { turnPage (intro) })
1,
makePage("button", func () { turnPage(button) }),
makePage("mouse", func () { turnPage(mouse) }),
makePage("input", func () { turnPage(input) }),
makePage("form", func () { turnPage(form) }),
makePage("art", func () { turnPage(art) }))
list.Collapse(96, 0)
container.Adopt(list, false)

View File

@ -1,6 +1,6 @@
package main
// import "image"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
@ -18,48 +18,49 @@ func run () {
textBox := elements.NewTextBox("", copypasta)
disconnectedContainer := elements.NewHBox(false, true)
// list := elements.NewList (
// elements.NewListEntry("This is list item 0", nil),
// elements.NewListEntry("This is list item 1", nil),
// elements.NewListEntry("This is list item 2", nil),
// elements.NewListEntry("This is list item 3", nil),
// elements.NewListEntry("This is list item 4", nil),
// elements.NewListEntry("This is list item 5", nil),
// elements.NewListEntry("This is list item 6", nil),
// elements.NewListEntry("This is list item 7", nil),
// elements.NewListEntry("This is list item 8", nil),
// elements.NewListEntry("This is list item 9", nil),
// elements.NewListEntry("This is list item 10", nil),
// elements.NewListEntry("This is list item 11", nil),
// elements.NewListEntry("This is list item 12", nil),
// elements.NewListEntry("This is list item 13", nil),
// elements.NewListEntry("This is list item 14", nil),
// elements.NewListEntry("This is list item 15", nil),
// elements.NewListEntry("This is list item 16", nil),
// elements.NewListEntry("This is list item 17", nil),
// elements.NewListEntry("This is list item 18", nil),
// elements.NewListEntry("This is list item 19", nil),
// elements.NewListEntry("This is list item 20", nil))
// list.Collapse(0, 32)
// scrollBar := elements.NewScrollBar(true)
// list.OnScrollBoundsChange (func () {
// scrollBar.SetBounds (
// list.ScrollContentBounds(),
// list.ScrollViewportBounds())
// })
// scrollBar.OnScroll (func (viewport image.Point) {
// list.ScrollTo(viewport)
// })
list := elements.NewList (
1,
elements.NewCell(elements.NewLabel("This is list item 0", false)),
elements.NewCell(elements.NewLabel("This is list item 1", false)),
elements.NewCell(elements.NewLabel("This is list item 2", false)),
elements.NewCell(elements.NewLabel("This is list item 3", false)),
elements.NewCell(elements.NewLabel("This is list item 4", false)),
elements.NewCell(elements.NewLabel("This is list item 5", false)),
elements.NewCell(elements.NewLabel("This is list item 6", false)),
elements.NewCell(elements.NewLabel("This is list item 7", false)),
elements.NewCell(elements.NewLabel("This is list item 8", false)),
elements.NewCell(elements.NewLabel("This is list item 9", false)),
elements.NewCell(elements.NewLabel("This is list item 10", false)),
elements.NewCell(elements.NewLabel("This is list item 11", false)),
elements.NewCell(elements.NewLabel("This is list item 12", false)),
elements.NewCell(elements.NewLabel("This is list item 13", false)),
elements.NewCell(elements.NewLabel("This is list item 14", false)),
elements.NewCell(elements.NewLabel("This is list item 15", false)),
elements.NewCell(elements.NewLabel("This is list item 16", false)),
elements.NewCell(elements.NewLabel("This is list item 17", false)),
elements.NewCell(elements.NewLabel("This is list item 18", false)),
elements.NewCell(elements.NewLabel("This is list item 19", false)),
elements.NewCell(elements.NewLabel("This is list item 20", false)))
list.Collapse(0, 32)
scrollBar := elements.NewScrollBar(true)
list.OnScrollBoundsChange (func () {
scrollBar.SetBounds (
list.ScrollContentBounds(),
list.ScrollViewportBounds())
})
scrollBar.OnScroll (func (viewport image.Point) {
list.ScrollTo(viewport)
})
container.Adopt(elements.NewLabel("A ScrollContainer:", false), false)
container.Adopt(elements.NewScroll(textBox, true, false), false)
// disconnectedContainer.Adopt(list, false)
disconnectedContainer.Adopt(list, false)
disconnectedContainer.Adopt (elements.NewLabel (
"Notice how the scroll bar to the right can be used to " +
"control the list, despite not even touching it. It is " +
"indeed a thing you can do. It is also terrible UI design so " +
"don't do it.", true), true)
// disconnectedContainer.Adopt(scrollBar, false)
disconnectedContainer.Adopt(scrollBar, false)
container.Adopt(disconnectedContainer, true)
window.OnClose(tomo.Stop)