Raycaster example works

This commit is contained in:
Sasha Koshka 2023-04-20 00:22:29 -04:00
parent dbee2ff5a9
commit 698414ee65
5 changed files with 5 additions and 693 deletions

View File

@ -1,104 +0,0 @@
package containers
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type childManager struct {
onChange func ()
children []tomo.LayoutEntry
parent tomo.Parent
theme theme.Wrapped
config config.Wrapped
}
// Adopt adds a new child element to the container. If expand is set to true,
// the element will expand (instead of contract to its minimum size), in
// whatever way is defined by the container's layout.
func (manager *childManager) Adopt (child tomo.Element, expand bool) {
if child0, ok := child.(tomo.Themeable); ok {
child0.SetTheme(manager.theme.Theme)
}
if child0, ok := child.(tomo.Configurable); ok {
child0.SetConfig(manager.config.Config)
}
child.SetParent(manager.parent)
manager.children = append (manager.children, tomo.LayoutEntry {
Element: child,
Expand: expand,
})
manager.onChange()
}
// Disown removes the given child from the container if it is contained within
// it.
func (manager *childManager) Disown (child tomo.Element) {
for index, entry := range manager.children {
if entry.Element == child {
manager.clearChildEventHandlers(entry.Element)
manager.children = append (
manager.children[:index],
manager.children[index + 1:]...)
break
}
}
manager.onChange()
}
// DisownAll removes all child elements from the container at once.
func (manager *childManager) DisownAll () {
for _, entry := range manager.children {
manager.clearChildEventHandlers(entry.Element)
}
manager.children = nil
manager.onChange()
}
// Children returns a slice containing this element's children.
func (manager *childManager) Children () (children []tomo.Element) {
children = make([]tomo.Element, len(manager.children))
for index, entry := range manager.children {
children[index] = entry.Element
}
return
}
// CountChildren returns the amount of children contained within this element.
func (manager *childManager) CountChildren () (count int) {
return len(manager.children)
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (manager *childManager) Child (index int) (child tomo.Element) {
if index < 0 || index > len(manager.children) { return }
return manager.children[index].Element
}
// ChildAt returns the child that contains the specified x and y coordinates. If
// there are no children at the coordinates, this method will return nil.
func (manager *childManager) ChildAt (point image.Point) (child tomo.Element) {
for _, entry := range manager.children {
if point.In(entry.Bounds) {
child = entry.Element
}
}
return
}
func (manager *childManager) clearChildEventHandlers (child tomo.Element) {
child.DrawTo(nil, image.Rectangle { }, nil)
child.SetParent(nil)
if child, ok := child.(tomo.Focusable); ok {
if child.Focused() {
child.HandleUnfocus()
}
}
}

View File

@ -1,587 +0,0 @@
package containers
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/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// TODO: using the event propagator core might not be the best idea here. we
// should have slightly different behavior to sync the focused element with the
// selected cell. alternatively we could pass a callback to the propagator that
// fires when the focused child changes. this would also allow things like
// scrolling to the focused child (for this element and others).
type tableCell struct {
tomo.Element
tomo.Pattern
image.Rectangle
}
// TableContainer is a container that lays its contents out in a table. It can
// be scrolled.
type TableContainer struct {
*core.Core
*core.Propagator
core core.CoreControl
topHeading bool
leftHeading bool
columns int
rows int
scroll image.Point
warping bool
grid [][]tableCell
children []tomo.Element
contentBounds image.Rectangle
forcedMinimumWidth int
forcedMinimumHeight int
selectedColumn int
selectedRow int
config config.Wrapped
theme theme.Wrapped
onSelect func ()
onScrollBoundsChange func ()
}
// NewTable creates a new table element with the specified amount of columns and
// rows. If top or left heading is set to true, the first row or column
// respectively will display as a table header.
func NewTableContainer (
columns, rows int,
topHeading, leftHeading bool,
) (
element *TableContainer,
) {
element = &TableContainer {
topHeading: topHeading,
leftHeading: leftHeading,
selectedColumn: -1,
selectedRow: -1,
}
element.theme.Case = tomo.C("tomo", "tableContainer")
element.Core, element.core = core.NewCore(element, element.redoAll)
element.Propagator = core.NewPropagator(element, element.core)
element.Resize(columns, rows)
return
}
// Set places an element at the specified column and row. If the element passed
// is nil, whatever element occupies the cell currently is removed.
func (element *TableContainer) Set (column, row int, child tomo.Element) {
if row < 0 || row >= element.rows { return }
if column < 0 || column >= element.columns { return }
childList := element.children
if child == nil {
if element.grid[row][column].Element == nil {
// no-op
return
} else {
// removing the child that is currently in a slow
element.unhook(element.grid[row][column].Element)
childList = childList[:len(childList) - 1]
element.grid[row][column].Element = child
}
} else {
element.hook(child)
if element.grid[row][column].Element == nil {
// putting the child in an empty slot
childList = append(childList, nil)
element.grid[row][column].Element = child
} else {
// replacing the child that is currently in a slow
element.unhook(element.grid[row][column].Element)
element.grid[row][column].Element = child
}
}
element.rebuildChildList(childList)
element.children = childList
element.redoAll()
}
// Resize changes the amount of columns and rows in the table. If the table is
// resized to be smaller, children in cells that do not exist anymore will be
// removed. The minimum size for a TableContainer is 1x1.
func (element *TableContainer) Resize (columns, rows int) {
if columns < 1 { columns = 1 }
if rows < 1 { rows = 1 }
if element.columns == columns && element.rows == rows { return }
amountRemoved := 0
// handle rows as a whole
if rows < element.rows {
// disown children in bottom rows
for _, row := range element.grid[rows:] {
for index, child := range row {
if child.Element != nil {
element.unhook(child.Element)
amountRemoved ++
row[index].Element = nil
}}}
// cut grid to size
element.grid = element.grid[:rows]
} else {
// expand grid
newGrid := make([][]tableCell, rows)
copy(newGrid, element.grid)
element.grid = newGrid
}
// handle each row individually
for rowIndex, row := range element.grid {
if columns < element.columns {
// disown children in the far right of the row
for index, child := range row[columns:] {
if child.Element != nil {
element.unhook(child.Element)
amountRemoved ++
row[index].Element = nil
}}
// cut row to size
element.grid[rowIndex] = row[:columns]
} else {
// expand row
newRow := make([]tableCell, columns)
copy(newRow, row)
element.grid[rowIndex] = newRow
}
}
element.columns = columns
element.rows = rows
if amountRemoved > 0 {
childList := element.children[:len(element.children) - amountRemoved]
element.rebuildChildList(childList)
element.children = childList
}
element.redoAll()
}
// Selected returns the column and row of the cell that is currently selected.
// If no cell is selected, this method will return (-1, -1).
func (element *TableContainer) Selected () (column, row int) {
return element.selectedColumn, element.selectedRow
}
// OnSelect sets a function to be called when the user selects a table cell.
func (element *TableContainer) OnSelect (callback func ()) {
element.onSelect = callback
}
// Warp runs the specified callback, deferring all layout and rendering updates
// until the callback has finished executing. This allows for aplications to
// perform batch gui updates without flickering and stuff.
func (element *TableContainer) Warp (callback func ()) {
if element.warping {
callback()
return
}
element.warping = true
callback()
element.warping = false
element.redoAll()
}
// Collapse collapses the element's minimum width and height. A value of zero
// for either means that the element's normal value is used.
func (element *TableContainer) Collapse (width, height int) {
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
}
// CountChildren returns the amount of children contained within this element.
func (element *TableContainer) 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 *TableContainer) Child (index int) (child tomo.Element) {
if index < 0 || index > len(element.children) { return }
return element.children[index]
}
func (element *TableContainer) Window () tomo.Window {
return element.core.Window()
}
// NotifyMinimumSizeChange notifies the container that the minimum size of a
// child element has changed.
func (element *TableContainer) NotifyMinimumSizeChange (child tomo.Element) {
element.updateMinimumSize()
element.redoAll()
}
// DrawBackground draws a portion of the container's background pattern within
// the specified bounds. The container will not push these changes.
func (element *TableContainer) DrawBackground (bounds image.Rectangle) {
if !bounds.Overlaps(element.core.Bounds()) { return }
for rowIndex, row := range element.grid {
for columnIndex, child := range row {
if bounds.Overlaps(child.Rectangle) {
element.theme.Pattern (
child.Pattern,
element.state(columnIndex, rowIndex)).
Draw(canvas.Cut(element.core, bounds), child.Rectangle)
return
}}}
}
func (element *TableContainer) HandleMouseDown (x, y int, button input.Button) {
element.Focus()
element.Propagator.HandleMouseDown(x, y, button)
if button != input.ButtonLeft { return }
for rowIndex, row := range element.grid {
for columnIndex, child := range row {
if image.Pt(x, y).In(child.Rectangle) {
element.selectCell(columnIndex, rowIndex)
return
}}}
}
func (element *TableContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyLeft: element.changeSelectionBy(-1, 0)
case input.KeyRight: element.changeSelectionBy(1, 0)
case input.KeyUp: element.changeSelectionBy(0, -1)
case input.KeyDown: element.changeSelectionBy(0, 1)
case input.KeyEscape: element.selectCell(-1, -1)
default: element.Propagator.HandleKeyDown(key, modifiers)
}
}
// ScrollContentBounds returns the full content size of the element.
func (element *TableContainer) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *TableContainer) ScrollViewportBounds () image.Rectangle {
bounds := element.Bounds()
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *TableContainer) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
if position.X < 0 {
position.X = 0
}
maxScrollWidth := element.maxScrollWidth()
if position.X > maxScrollWidth {
position.X = maxScrollWidth
}
element.scroll = position
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *TableContainer) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *TableContainer) ScrollAxes () (horizontal, vertical bool) {
return true, true
}
func (element *TableContainer) changeSelectionBy (column, row int) {
column += element.selectedColumn
row += element.selectedRow
if column < 0 { column = 0 }
if row < 0 { row = 0 }
element.selectCell(column, row)
}
func (element *TableContainer) selectCell (column, row int) {
if column < -1 { column = -1 }
if row < -1 { row = -1 }
if column >= element.columns { column = element.columns - 1 }
if row >= element.rows { row = element.rows - 1 }
if column == element.selectedColumn && row == element.selectedRow {
return
}
oldColumn, oldRow := element.selectedColumn, element.selectedRow
element.selectedColumn = column
element.selectedRow = row
if oldColumn >= 0 && oldRow >= 0 {
element.core.DamageRegion(element.redoCell(oldColumn, oldRow))
}
if column >= 0 && row >= 0 {
element.core.DamageRegion(element.redoCell(column, row))
}
if element.onSelect != nil {
element.onSelect()
}
}
func (element *TableContainer) maxScrollHeight () (height int) {
viewportHeight := element.Bounds().Dy()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *TableContainer) maxScrollWidth () (width int) {
viewportWidth := element.Bounds().Dx()
width = element.contentBounds.Dx() - viewportWidth
if width < 0 { width = 0 }
return
}
func (element *TableContainer) hook (child tomo.Element) {
if child0, ok := child.(tomo.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(tomo.Configurable); ok {
child0.SetConfig(element.config.Config)
}
child.SetParent(element)
}
func (element *TableContainer) unhook (child tomo.Element) {
child.SetParent(nil)
child.DrawTo(nil, image.Rectangle { }, nil)
}
func (element *TableContainer) rebuildChildList (list []tomo.Element) {
index := 0
for _, row := range element.grid {
for _, child := range row {
if child.Element == nil { continue }
list[index] = child.Element
index ++
}}
}
func (element *TableContainer) state (column, row int) (state tomo.State) {
if column == element.selectedColumn && row == element.selectedRow {
state.On = true
}
return
}
func (element *TableContainer) redoCell (column, row int) image.Rectangle {
padding := element.theme.Padding(tomo.PatternTableCell)
cell := element.grid[row][column]
pattern := element.theme.Pattern (
cell.Pattern, element.state(column, row))
if cell.Element != nil {
// give child canvas portion
innerCellBounds := padding.Apply(cell.Rectangle)
artist.DrawShatter (
element.core, pattern,
cell.Rectangle, innerCellBounds)
cell.DrawTo (
canvas.Cut(element.core, innerCellBounds),
innerCellBounds,
element.childDrawCallback)
} else {
// draw cell pattern in empty cells
pattern.Draw(element.core, cell.Rectangle)
}
return cell.Rectangle
}
func (element *TableContainer) redoAll () {
if element.warping || !element.core.HasImage() {
element.updateMinimumSize()
return
}
maxScrollHeight := element.maxScrollHeight()
if element.scroll.Y > maxScrollHeight {
element.scroll.Y = maxScrollHeight
}
maxScrollWidth := element.maxScrollWidth()
if element.scroll.X > maxScrollWidth {
element.scroll.X = maxScrollWidth
}
// calculate the minimum size of each column and row
var minWidth, minHeight float64
columnWidths := make([]float64, element.columns)
rowHeights := make([]float64, element.rows)
padding := element.theme.Padding(tomo.PatternTableCell)
for rowIndex, row := range element.grid {
for columnIndex, child := range row {
width, height := padding.Horizontal(), padding.Vertical()
if child.Element != nil {
minWidth, minHeight := child.MinimumSize()
width += minWidth
height += minHeight
fwidth := float64(width)
fheight := float64(height)
if fwidth > columnWidths[columnIndex] {
columnWidths[columnIndex] = fwidth
}
if fheight > rowHeights[rowIndex] {
rowHeights[rowIndex] = fheight
}
}
}}
for _, width := range columnWidths { minWidth += width }
for _, height := range rowHeights { minHeight += height }
// ignore given bounds for layout if they are below minimum size. we do
// this because we are scrollable in both directions and we might be
// collapsed.
bounds := element.Bounds().Sub(element.scroll)
if bounds.Dx() < int(minWidth) {
bounds.Max.X = bounds.Min.X + int(minWidth)
}
if bounds.Dy() < int(minHeight) {
bounds.Max.Y = bounds.Min.Y + int(minHeight)
}
element.contentBounds = bounds
// scale up those minimum sizes to an actual size.
// FIXME: replace this with a more accurate algorithm
widthRatio := float64(bounds.Dx()) / minWidth
heightRatio := float64(bounds.Dy()) / minHeight
for index := range columnWidths {
columnWidths[index] *= widthRatio
}
for index := range rowHeights {
rowHeights[index] *= heightRatio
}
// cut up canvas
x := float64(bounds.Min.X)
y := float64(bounds.Min.Y)
for rowIndex, row := range element.grid {
for columnIndex, _ := range row {
width := columnWidths[columnIndex]
height := rowHeights[rowIndex]
cellBounds := image.Rect (
int(x), int(y),
int(x + width), int(y + height))
var id tomo.Pattern
isHeading :=
rowIndex == 0 && element.topHeading ||
columnIndex == 0 && element.leftHeading
if isHeading {
id = tomo.PatternTableHead
} else {
id = tomo.PatternTableCell
}
element.grid[rowIndex][columnIndex].Rectangle = cellBounds
element.grid[rowIndex][columnIndex].Pattern = id
element.redoCell(columnIndex, rowIndex)
x += float64(width)
}
x = float64(bounds.Min.X)
y += rowHeights[rowIndex]
}
element.core.DamageAll()
// update the minimum size of the element
if element.forcedMinimumHeight > 0 {
minHeight = float64(element.forcedMinimumHeight)
}
if element.forcedMinimumWidth > 0 {
minWidth = float64(element.forcedMinimumWidth)
}
element.core.SetMinimumSize(int(minWidth), int(minHeight))
// notify parent of scroll bounds change
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *TableContainer) updateMinimumSize () {
if element.forcedMinimumHeight > 0 && element.forcedMinimumWidth > 0 {
element.core.SetMinimumSize (
element.forcedMinimumWidth,
element.forcedMinimumHeight)
return
}
columnWidths := make([]int, element.columns)
rowHeights := make([]int, element.rows)
padding := element.theme.Padding(tomo.PatternTableCell)
for rowIndex, row := range element.grid {
for columnIndex, child := range row {
width, height := padding.Horizontal(), padding.Vertical()
if child.Element != nil {
minWidth, minHeight := child.MinimumSize()
width += minWidth
height += minHeight
if width > columnWidths[columnIndex] {
columnWidths[columnIndex] = width
}
if height > rowHeights[rowIndex] {
rowHeights[rowIndex] = height
}
}
}}
var minWidth, minHeight int
for _, width := range columnWidths { minWidth += width }
for _, height := range rowHeights { minHeight += height }
if element.forcedMinimumHeight > 0 {
minHeight = element.forcedMinimumHeight
}
if element.forcedMinimumWidth > 0 {
minWidth = element.forcedMinimumWidth
}
element.core.SetMinimumSize(minWidth, minHeight)
}
func (element *TableContainer) childDrawCallback (region image.Rectangle) {
element.core.DamageRegion(region)
}

View File

@ -103,7 +103,7 @@ func (game *Game) tick () {
if game.stamina < 0 {
game.stamina = 0
}
tomo.Do(game.Invalidate)
if statUpdate && game.onStatUpdate != nil {
tomo.Do(game.onStatUpdate)

View File

@ -57,12 +57,13 @@ func run () {
topBar.Adopt(elements.NewLabel("Health:"))
topBar.AdoptExpand(healthBar)
container.Adopt(topBar)
container.AdoptExpand(game)
container.AdoptExpand(game.Raycaster)
game.Focus()
game.OnStatUpdate (func () {
staminaBar.SetProgress(game.Stamina())
})
game.Start()
window.OnClose(tomo.Stop)
window.Show()

View File

@ -138,6 +138,8 @@ func (element *Raycaster) Focus () {
element.entity.Focus()
}
func (element *Raycaster) SetEnabled (bool) { }
func (element *Raycaster) Enabled () bool { return true }
func (element *Raycaster) HandleFocusChange () { }