This repository has been archived on 2023-08-08. You can view files and clone it, but cannot push or open issues or pull requests.
tomo-old/elements/basic/list.go

466 lines
12 KiB
Go
Raw Normal View History

2023-02-01 23:48:16 -07:00
package basicElements
2023-01-23 00:05:09 -07:00
import "fmt"
import "image"
2023-02-01 23:48:16 -07:00
import "git.tebibyte.media/sashakoshka/tomo/input"
2023-01-23 00:05:09 -07:00
import "git.tebibyte.media/sashakoshka/tomo/theme"
2023-02-07 22:22:40 -07:00
import "git.tebibyte.media/sashakoshka/tomo/config"
2023-02-01 23:48:16 -07:00
import "git.tebibyte.media/sashakoshka/tomo/canvas"
2023-01-23 00:05:09 -07:00
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// List is an element that contains several objects that a user can select.
type List struct {
*core.Core
2023-01-30 15:01:47 -07:00
*core.FocusableCore
2023-01-23 00:05:09 -07:00
core core.CoreControl
2023-01-30 15:01:47 -07:00
focusableControl core.FocusableCoreControl
2023-01-23 21:54:12 -07:00
pressed bool
2023-01-23 00:05:09 -07:00
contentHeight int
forcedMinimumWidth int
forcedMinimumHeight int
2023-01-23 21:54:12 -07:00
2023-01-23 00:05:09 -07:00
selectedEntry int
scroll int
entries []ListEntry
2023-02-08 12:36:14 -07:00
config config.Wrapped
theme theme.Wrapped
2023-02-07 22:22:40 -07:00
2023-01-23 00:05:09 -07:00
onScrollBoundsChange func ()
onNoEntrySelected func ()
2023-01-23 00:05:09 -07:00
}
// NewList creates a new list element with the specified entries.
func NewList (entries ...ListEntry) (element *List) {
2023-02-08 12:36:14 -07:00
element = &List { selectedEntry: -1 }
element.theme.Case = theme.C("basic", "list")
2023-02-07 22:22:40 -07:00
element.Core, element.core = core.NewCore(element.handleResize)
2023-01-30 15:01:47 -07:00
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
2023-01-23 00:05:09 -07:00
element.entries = make([]ListEntry, len(entries))
for index, entry := range entries {
element.entries[index] = entry
}
element.updateMinimumSize()
return
}
2023-01-31 12:54:43 -07:00
func (element *List) handleResize () {
2023-01-23 00:05:09 -07:00
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
}
2023-01-23 00:05:09 -07:00
element.draw()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
2023-02-07 22:22:40 -07:00
// SetTheme sets the element's theme.
func (element *List) SetTheme (new theme.Theme) {
2023-02-08 12:36:14 -07:00
if new == element.theme.Theme { return }
element.theme.Theme = new
2023-02-07 09:27:59 -07:00
for index, entry := range element.entries {
2023-02-08 12:36:14 -07:00
entry.SetTheme(element.theme.Theme)
2023-02-07 09:27:59 -07:00
element.entries[index] = entry
}
2023-02-07 22:22:40 -07:00
element.updateMinimumSize()
2023-02-07 09:27:59 -07:00
element.redo()
}
2023-02-07 22:22:40 -07:00
// SetConfig sets the element's configuration.
func (element *List) SetConfig (new config.Config) {
2023-02-08 12:36:14 -07:00
if new == element.config.Config { return }
element.config.Config = new
2023-02-07 09:27:59 -07:00
for index, entry := range element.entries {
2023-02-07 22:22:40 -07:00
entry.SetConfig(element.config)
2023-02-07 09:27:59 -07:00
element.entries[index] = entry
}
2023-02-07 22:22:40 -07:00
element.updateMinimumSize()
2023-02-07 09:27:59 -07:00
element.redo()
}
func (element *List) redo () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
2023-01-23 00:05:09 -07:00
// 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) {
2023-02-10 22:18:21 -07:00
if
element.forcedMinimumWidth == width &&
element.forcedMinimumHeight == height {
return
}
2023-01-23 00:05:09 -07:00
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
2023-02-10 22:18:21 -07:00
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
element.redo()
2023-01-23 00:05:09 -07:00
}
2023-02-01 23:48:16 -07:00
func (element *List) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
2023-01-30 15:01:47 -07:00
if !element.Focused() { element.Focus() }
2023-02-01 23:48:16 -07:00
if button != input.ButtonLeft { return }
2023-01-23 21:54:12 -07:00
element.pressed = true
if element.selectUnderMouse(x, y) && element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
2023-01-23 00:05:09 -07:00
}
2023-02-01 23:48:16 -07:00
func (element *List) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
2023-01-23 21:54:12 -07:00
element.pressed = false
2023-01-23 00:05:09 -07:00
}
2023-01-23 21:54:12 -07:00
func (element *List) HandleMouseMove (x, y int) {
if element.pressed {
if element.selectUnderMouse(x, y) && element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
2023-01-23 00:05:09 -07:00
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
2023-02-01 23:48:16 -07:00
func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
altered := false
2023-01-26 09:53:49 -07:00
switch key {
2023-02-01 23:48:16 -07:00
case input.KeyLeft, input.KeyUp:
altered = element.changeSelectionBy(-1)
2023-02-01 23:48:16 -07:00
case input.KeyRight, input.KeyDown:
altered = element.changeSelectionBy(1)
2023-02-01 23:48:16 -07:00
case input.KeyEscape:
altered = element.selectEntry(-1)
2023-01-26 09:53:49 -07:00
}
if altered && element.core.HasImage () {
2023-01-26 09:53:49 -07:00
element.draw()
element.core.DamageAll()
}
2023-01-23 00:05:09 -07:00
}
2023-02-01 23:48:16 -07:00
func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
2023-01-23 00:05:09 -07:00
// ScrollContentBounds returns the full content size of the element.
func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
return image.Rect (
0, 0,
1, element.contentHeight)
}
// ScrollViewportBounds returns the size and position of the element's viewport
// relative to ScrollBounds.
func (element *List) ScrollViewportBounds () (bounds image.Rectangle) {
return image.Rect (
0, element.scroll,
0, element.scroll + element.scrollViewportHeight())
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *List) ScrollTo (position image.Point) {
element.scroll = position.Y
if element.scroll < 0 {
element.scroll = 0
} else if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
}
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// ScrollAxes returns the supported axes for scrolling.
func (element *List) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *List) scrollViewportHeight () (height int) {
2023-02-26 20:20:17 -07:00
padding := element.theme.Padding(theme.PatternSunken)
return element.Bounds().Dy() - padding[0] - padding[2]
2023-01-23 00:05:09 -07:00
}
func (element *List) maxScrollHeight () (height int) {
height =
element.contentHeight -
element.scrollViewportHeight()
if height < 0 { height = 0 }
return
}
func (element *List) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// OnNoEntrySelected sets a function to be called when the user chooses to
// deselect the current selected entry by clicking on empty space within the
// list or by pressing the escape key.
func (element *List) OnNoEntrySelected (callback func ()) {
element.onNoEntrySelected = callback
}
2023-01-23 00:05:09 -07:00
// CountEntries returns the amount of entries in the list.
func (element *List) CountEntries () (count int) {
return len(element.entries)
}
// Append adds an entry to the end of the list.
func (element *List) Append (entry ListEntry) {
// append
2023-02-10 22:18:21 -07:00
entry = element.resizeEntryToFit(entry)
2023-02-08 12:36:14 -07:00
entry.SetTheme(element.theme.Theme)
2023-02-07 22:22:40 -07:00
entry.SetConfig(element.config)
2023-01-23 00:05:09 -07:00
element.entries = append(element.entries, entry)
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// EntryAt returns the entry at the specified index. If the index is out of
// bounds, it panics.
func (element *List) EntryAt (index int) (entry ListEntry) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index))
}
return element.entries[index]
}
// Insert inserts an entry into the list at the speified index. If the index is
// out of bounds, it is constrained either to zero or len(entries).
func (element *List) Insert (index int, entry ListEntry) {
if index < 0 { index = 0 }
if index > len(element.entries) { index = len(element.entries) }
// insert
element.entries = append (
element.entries[:index + 1],
element.entries[index:]...)
2023-02-10 22:18:21 -07:00
entry = element.resizeEntryToFit(entry)
2023-01-23 00:05:09 -07:00
element.entries[index] = entry
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Remove removes the entry at the specified index. If the index is out of
// bounds, it panics.
func (element *List) Remove (index int) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.Remove index out of range: ", index))
}
// delete
element.entries = append (
element.entries[:index],
element.entries[index + 1:]...)
// recalculate, redraw, notify
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
// Replace replaces the entry at the specified index with another. If the index
// is out of bounds, it panics.
func (element *List) Replace (index int, entry ListEntry) {
if index < 0 || index >= len(element.entries) {
panic(fmt.Sprint("basic.List.Replace index out of range: ", index))
}
// replace
2023-02-10 22:18:21 -07:00
entry = element.resizeEntryToFit(entry)
2023-01-23 00:05:09 -07:00
element.entries[index] = entry
// redraw
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
2023-02-10 20:26:34 -07:00
// Select selects a specific item in the list. If the index is out of bounds,
// no items will be selecected.
func (element *List) Select (index int) {
if element.selectEntry(index) {
element.redo()
}
}
2023-01-23 21:54:12 -07:00
func (element *List) selectUnderMouse (x, y int) (updated bool) {
2023-02-26 20:20:17 -07:00
padding := element.theme.Padding(theme.PatternSunken)
bounds := padding.Apply(element.Bounds())
2023-01-23 21:54:12 -07:00
mousePoint := image.Pt(x, y)
dot := image.Pt (
bounds.Min.X,
bounds.Min.Y - element.scroll)
2023-01-23 21:54:12 -07:00
newlySelectedEntryIndex := -1
for index, entry := range element.entries {
entryPosition := dot
dot.Y += entry.Bounds().Dy()
if entryPosition.Y > bounds.Max.Y { break }
if mousePoint.In(entry.Bounds().Add(entryPosition)) {
newlySelectedEntryIndex = index
break
}
}
2023-01-26 09:53:49 -07:00
return element.selectEntry(newlySelectedEntryIndex)
}
func (element *List) selectEntry (index int) (updated bool) {
if element.selectedEntry == index { return false }
element.selectedEntry = index
if element.selectedEntry < 0 {
if element.onNoEntrySelected != nil {
element.onNoEntrySelected()
}
} else {
2023-01-24 14:41:12 -07:00
element.entries[element.selectedEntry].RunSelect()
}
2023-01-23 21:54:12 -07:00
return true
}
func (element *List) changeSelectionBy (delta int) (updated bool) {
newIndex := element.selectedEntry + delta
if newIndex < 0 { newIndex = len(element.entries) - 1 }
if newIndex >= len(element.entries) { newIndex = 0 }
return element.selectEntry(newIndex)
}
2023-01-23 00:05:09 -07:00
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
2023-02-10 22:18:21 -07:00
bounds := element.Bounds()
2023-02-26 20:20:17 -07:00
padding := element.theme.Padding(theme.PatternSunken)
entry.Resize(padding.Apply(bounds).Dx())
2023-01-23 00:05:09 -07:00
return entry
}
func (element *List) updateMinimumSize () {
element.contentHeight = 0
for _, entry := range element.entries {
element.contentHeight += entry.Bounds().Dy()
}
minimumWidth := element.forcedMinimumWidth
minimumHeight := element.forcedMinimumHeight
if minimumWidth == 0 {
2023-01-23 21:54:12 -07:00
for _, entry := range element.entries {
2023-02-10 22:18:21 -07:00
entryWidth := entry.MinimumWidth()
2023-01-23 21:54:12 -07:00
if entryWidth > minimumWidth {
minimumWidth = entryWidth
}
}
2023-01-23 00:05:09 -07:00
}
if minimumHeight == 0 {
minimumHeight = element.contentHeight
2023-01-23 00:05:09 -07:00
}
2023-02-26 20:20:17 -07:00
padding := element.theme.Padding(theme.PatternSunken)
minimumHeight += padding[0] + padding[2]
2023-01-23 00:05:09 -07:00
element.core.SetMinimumSize(minimumWidth, minimumHeight)
}
func (element *List) draw () {
bounds := element.Bounds()
2023-02-26 20:20:17 -07:00
padding := element.theme.Padding(theme.PatternSunken)
innerBounds := padding.Apply(bounds)
state := theme.State {
Disabled: !element.Enabled(),
2023-01-30 15:01:47 -07:00
Focused: element.Focused(),
}
2023-01-23 00:05:09 -07:00
dot := image.Point {
innerBounds.Min.X,
innerBounds.Min.Y - element.scroll,
2023-01-23 00:05:09 -07:00
}
innerCanvas := canvas.Cut(element.core, innerBounds)
2023-01-23 00:05:09 -07:00
for index, entry := range element.entries {
entryPosition := dot
dot.Y += entry.Bounds().Dy()
if dot.Y < innerBounds.Min.Y { continue }
if entryPosition.Y > innerBounds.Max.Y { break }
2023-01-23 00:05:09 -07:00
entry.Draw (
innerCanvas, entryPosition,
2023-01-30 15:01:47 -07:00
element.Focused(), element.selectedEntry == index)
2023-01-23 00:05:09 -07:00
}
covered := image.Rect (
0, 0,
innerBounds.Dx(), element.contentHeight,
).Add(innerBounds.Min).Intersect(innerBounds)
pattern := element.theme.Pattern(theme.PatternSunken, state)
2023-02-26 20:20:17 -07:00
artist.DrawShatter (
2023-03-11 23:47:58 -07:00
element.core, pattern, bounds, covered)
2023-01-23 00:05:09 -07:00
}